What is an "IP fact"?

An IP fact is a small, durable statement about an IP address — something that's true today and that you might want to reason about. We split them into two layers:

  • /basic — the foundation: public vs private, IPv4 vs IPv6, and a special-range classification. Pure offline computation — free everywhere.
  • /full — enrichment on top of /basic: country, Tor exit-node status, hosting platform / CDN (Azure, AWS, GCP, Oracle, DigitalOcean, Linode, Vultr, Hetzner, Fastly, Cloudflare, Akamai, OVH, Alibaba Cloud, Tencent Cloud, plus the address space Google, Microsoft 365, and GitHub publish for their own services — with the service tag or region where the provider publishes it), known AI / search crawler, iCloud Private Relay egress, ASN, reverse DNS (the PTR name, and whether it forward-confirms), and the TLS certificate the address presents (subject, names, which names forward-confirm, and whether the address is in the certificate).

Both layers are exposed via the REST API and the MCP server. The basic layer is also served by the CLI and by the in-page WebMCP tools on this site.

Public/private + address family (the /basic layer)

The /basic lookup answers a few questions, fast — and entirely offline:

  1. Canonical form. The parsed, normalized address (ip). Input is strict: a dotted quad without leading zeros, or RFC 4291 IPv6 text (IPv4-mapped ::ffff:8.8.8.8 is answered as 8.8.8.8). Shorthand such as 1.2.3, hexadecimal octets, a bare integer, brackets and zone identifiers (%eth0) are rejected with a 400 instead of being reinterpreted as a different address.
  2. Public vs private. isPublic is true only for ordinary, globally routable space. RFC 1918 and RFC 4193 private space, loopback, link-local, CGNAT and every other IANA special-purpose block read false. Those addresses short-circuit before we hit any external provider — no leaks, no cost.
  3. Address family. "IPv4" or "IPv6".
  4. Special range. Which IANA special-purpose block the address falls in, named after the RFC that defines it (see below); null for ordinary public space.

No network calls, no payment — pure computation, which is why it's free on every surface. Country code is not a basic fact: it needs a geolocation provider (Azure Maps today, behind a swappable abstraction), so it ships in the paid /full layer.

Enriched facts (the /full layer)

The enriched lookup wraps /basic and adds the bits you actually want when investigating a suspicious request:

{
  "ip": "172.224.226.5",
  "countryCode": "GB",
  "isPublic": true,
  "addressFamily": "IPv4",
  "specialRange": null,
  "isTor": false,
  "azure": null,
  "aws": null,
  "platform": { "provider": "akamai", "service": "cdn", "region": null, "city": null, "source": "asn" },
  "crawler": null,
  "privateRelay": { "countryCode": "GB", "region": "GB-EN", "city": "London" },
  "asn": { "number": 36183, "name": "AKAMAI-AS - Akamai Technologies, Inc.", "countryCode": "US" },
  "reverseDns": { "name": "a172-224-226-5.source.akaquill.net", "forwardConfirmed": false },
  "tlsCertificate": null
}

Live lookup, re-fetched at most hourly — the same answer the API gives right now.

That one is an iCloud Private Relay egress in London. Apple runs the relay's egress hop on partner networks, so the same address is announced by AS36183 (AKAMAI-AS - Akamai Technologies, Inc.). platform infers akamai from that AS number ("source": "asn"). The PTR name a172-224-226-5.source.akaquill.net does not forward-confirm — a carrier's label, not proof of the host. The signals stack; read them together. Each enrichment field is null when it doesn't apply: platform, crawler, and privateRelay are set only when a published list matches, a public IP usually has an asn, reverseDns is set when the address has a PTR record that answers in time, and tlsCertificate when port 443 answers a handshake without SNI (a shared edge like this one presents nothing without a name). Every field is documented in its own section below.

Addresses in any special-purpose block — private, loopback, documentation, reserved and the rest — return the same shape but with countryCode: "??", isPublic: false, a non-null specialRange, and every enrichment field (azure, aws, platform, crawler, privateRelay, asn, reverseDns, tlsCertificate) as null — no provider calls, no DNS queries, no probe, no telemetry, no surprise cost.

Special-range classification (specialRange)

The IETF sets aside address blocks for particular purposes and IANA lists them in its special-purpose address registries. An address in one of those blocks is not an ordinary host on the public internet, so isPublic is false and specialRange names the block, in the wire form shown below. null means publicly routable. Every value is explainable with an RFC; the classification is computed offline, so these addresses never reach any external lookup.

IPv4

ValueBlockDefined byWhat it means
unspecified0.0.0.0/8RFC 791 §3.2, RFC 1122"This network"; 0.0.0.0 itself is "this host", the unspecified address a machine uses before it has one.
private10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16RFC 1918Private networks: homes, offices and clouds reuse these behind NAT; the same address exists in millions of places.
cgnat100.64.0.0/10RFC 6598Shared address space for carrier-grade NAT: an ISP's internal side, never routed on the internet.
loopback127.0.0.0/8RFC 1122The machine itself.
linkLocal169.254.0.0/16RFC 3927Self-assigned when no DHCP answers; valid only on one link.
ietfProtocol192.0.0.0/24RFC 6890Assignments for IETF protocols (for example the IPv4 "dummy" address 192.0.0.8). Two anycast addresses inside it are globally reachable and therefore stay public: 192.0.0.9 (RFC 7723, Port Control Protocol) and 192.0.0.10 (RFC 8155, TURN).
documentation192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24RFC 5737TEST-NET-1/2/3: examples in manuals and RFCs, never assigned to anyone.
reserved192.88.99.0/24RFC 7526The former 6to4 relay anycast block, withdrawn from use in 2015 and held in reserve.
benchmarking198.18.0.0/15RFC 2544Reserved for benchmarking network equipment in isolated labs; a packet from here on the public internet is a misconfiguration.
multicast224.0.0.0/4RFC 5771Group addresses, not hosts: a destination many receivers subscribe to.
reserved240.0.0.0/4RFC 1112 §4"Class E", reserved since 1989 and still unassignable; about a sixteenth of the IPv4 space, so random or spoofed addresses land here often.
broadcast255.255.255.255RFC 919The limited-broadcast address: every host on the local link.

IPv6

ValueBlockDefined byWhat it means
unspecified::RFC 4291The unspecified address.
loopback::1RFC 4291The machine itself.
nat64Local64:ff9b:1::/48RFC 8215Local-use prefix for IPv4/IPv6 translation inside one network. (The well-known NAT64 prefix 64:ff9b::/96, RFC 6052, is globally reachable and stays public.)
discardOnly100::/64RFC 6666Traffic to this prefix is silently dropped; used to black-hole unwanted flows.
dummy100:0:0:1::/64RFC 9780The dummy IPv6 prefix: a placeholder source address for multipoint BFD sessions, never a reachable endpoint (added to the registry in 2025).
teredo2001::/32RFC 4380Teredo tunnelling: IPv6 carried inside IPv4 UDP through NATs; the host behind it is an IPv4 client.
benchmarking2001:2::/48RFC 5180The IPv6 benchmarking block, the counterpart of 198.18.0.0/15.
orchid2001:20::/28RFC 7343ORCHIDv2: cryptographic hash identifiers that look like addresses but are never announced as routes. IANA's registry marks the block globally reachable; ipfacts still reports it as a special range so an identifier is not mistaken for a host.
ietfProtocol2001::/23RFC 2928IETF protocol assignments, the IPv6 counterpart of 192.0.0.0/24; Teredo, benchmarking and ORCHIDv2 above are carved from it. Its globally reachable sub-blocks stay public: the Port Control Protocol, TURN and DNS-SD anycast addresses (2001:1::1–2001:1::3), AMT 2001:3::/32, AS112-v6 2001:4:112::/48, and the drone remote-ID prefix 2001:30::/28.
documentation2001:db8::/32, 3fff::/20RFC 3849, RFC 9637Example prefixes for documentation; 3fff::/20 was added in 2024.
srv65f00::/16RFC 9602Segment Routing over IPv6 SIDs: instructions to routers inside an operator's network, not endpoints.
privatefc00::/7RFC 4193Unique local addresses, the IPv6 counterpart of RFC 1918.
linkLocalfe80::/10RFC 4291Valid only on one link; every IPv6 interface has one.
multicastff00::/8RFC 4291Group addresses, not hosts.

Blocks the registries mark as globally reachable are ordinary public space here (specialRange: null): the NAT64 well-known prefix 64:ff9b::/96, the AS112 and AMT blocks in both families, the Port Control Protocol, TURN and DNS-SD anycast addresses, and the drone remote-ID prefix 2001:30::/28. So is the 6to4 prefix 2002::/16 (RFC 3056), which the registry leaves unmarked. The source of truth is IANA's IPv4 and IPv6 special-purpose registries. Every block in the tables above is marked not globally reachable there (multicast lives in IANA's separate multicast registries), with two deliberate readings: Teredo, which the registry leaves unmarked, and ORCHIDv2, which it marks reachable, are reported as special ranges because neither names a host you can reach.

Tor exit-node detection

isTor reflects whether the IP is currently advertised as a Tor exit node on the public Tor consensus. We refresh the list every 30 minutes from check.torproject.org and keep it in memory for O(1) lookups.

This is a "is it on the list right now" check — not a "has this IP ever been a Tor exit" check. False negatives are possible when a node is rotating.

Example: 192.42.116.20 is a useful Tor-exit test address when it is present in the current Tor consensus.

Azure + AWS cloud classification

For Azure IPs we return the service tag that the address belongs to — "AzureCloud", "AzureFrontDoor.Frontend", "Storage.eastus", etc. Source is the official ARM Microsoft.Network/locations/{location}/serviceTags endpoint, refreshed daily.

For AWS IPs we return the service and region from the published ip-ranges.json dataset — { "service": "S3", "region": "us-east-1" }. Region is null when AWS does not publish one.

Region is populated when an Azure service tag carries one (Storage.eastus → "eastus"); for cloud-wide tags like AzureCloud region is null.

Tor and cloud attribution are independent signals. A Tor exit relay can run on cloud infrastructure, so an IP can be both isTor: true and attributed to Azure or AWS.

azure and aws are the original cloud lanes and stay on the wire for compatibility. The newer platform field (next section) unifies them with GCP, Oracle, DigitalOcean, Linode, Vultr, Hetzner, Fastly, Cloudflare, and the rest of the providers listed there, so a caller has one place to look — it carries the same match, so an Azure IP answers both azure: { … } and platform: { "provider": "azure", … }.

Examples to try: Azure 20.232.0.137, AWS 3.5.140.1, Tor 192.42.116.20.

Hosting platform + CDN attribution (platform)

platform names the hosting or CDN provider that publishes the IP as its own — { "provider": "digitalocean", "service": null, "region": "US-NY", "city": "New York", "source": "list" }. One field for every provider we index, so a caller has a single place to look. provider is a stable slug: azure, aws, gcp, oracle, digitalocean, linode, vultr, hetzner, fastly, cloudflare, akamai, ovh, alibaba, tencent — plus three that aren't clouds but publish the address space behind their own services: google (Google's non-cloud ranges, e.g. 8.8.8.8; service "Google"), microsoft365 (service Exchange, SharePoint, Skype, or Common), and github (service hooks, web, api, git, pages, packages, importer, github_enterprise_importer, or copilot). service is set where the provider publishes one (an Azure service tag, an AWS service, a GCP scope, a Microsoft 365 or GitHub service); region and city where the provider's geofeed carries them.

Sources: each provider's own published range list — GCP's cloud.json, Oracle's public IP ranges, the DigitalOcean, Linode, Vultr, and Hetzner geofeeds (RFC 8805; Hetzner's hetzner.com/geofeed.csv carries region and city), Fastly's public IP list, Cloudflare's ips-v4 / ips-v6, Google's goog.json (its non-cloud space, served from gstatic.com), the Microsoft 365 worldwide endpoints from endpoints.office.com, GitHub's api.github.com/meta (GitHub's own services only — not Actions runners, which live in Azure), plus the Azure service tags and AWS ip-ranges.json described above. All refreshed every 24 hours and matched in memory; the most specific prefix wins.

source says how the match was made. "list" means the provider's own published range list matched — authoritative. "asn" means nothing more specific was known and the attribution is inferred from the address's AS number belonging wholly to that provider. That is how Akamai (service "cdn"), OVH, Alibaba Cloud, and Tencent Cloud are attributed, since they publish no usable range list, and it is the fallback for DigitalOcean, Vultr, Cloudflare, and Linode addresses their list hasn't caught up with yet. ASN inference is lower confidence: it says who routes the prefix, not which product it is part of, and it carries no region or city — so weigh it accordingly. Whenever both could apply, "list" beats "asn".

Caveat: high-confidence when it hits, not exhaustive. Providers publish the ranges they choose to, so null means "not in any list we index, and not routed by a provider-owned AS" — not "not hosted in a cloud".

AI + search crawlers (crawler)

crawler identifies traffic from the crawlers whose operators publish their egress ranges — { "operator": "openai", "agent": "GPTBot" }. Operators and the agents they publish:

  • openai — GPTBot, OAI-SearchBot, ChatGPT-User.
  • google — Googlebot, Google-Special, Google-UserTriggered.
  • bing — bingbot.
  • perplexity — PerplexityBot, Perplexity-User.
  • apple — Applebot.

Why it matters: a request whose User-Agent claims to be Googlebot but arrives from an IP outside Google's published ranges is not Googlebot. The IP is the verification; the header is only a claim. The same logic tells you which AI crawlers are actually fetching your pages.

Sources: each operator's published JSON range list (OpenAI, Google, Bing, Perplexity, Apple), refreshed every 24 hours.

Caveat: only operators that publish ranges are covered, and plenty of bots don't. null means "not a known published crawler", not "not a bot".

iCloud Private Relay (privateRelay)

privateRelay is set when the IP is an egress address of Apple's iCloud Private Relay — { "countryCode": "GB", "region": "GB-EN", "city": "London" }. Apple publishes the location it assigns to each egress range, so the field carries the country, plus the region and city where Apple provides them.

This is its own category, and a trust signal. Private Relay requires an iCloud+ subscription, so behind each of these addresses is a paying, verified Apple customer — typically browsing in Safari — with Apple routing the connection so that no single party sees both who they are and where they're going. It is not a VPN, not a proxy service, and not anonymised abuse infrastructure. Treat it the way you'd treat a residential ISP address, and if your fraud rules bucket "VPN-like" traffic as risky, give Private Relay its own bucket.

Practical note: the country and city are Apple's stated egress location, chosen to be near the user and deliberately coarse. They won't necessarily match a geolocation database's answer for the same IP — including our own countryCode.

Expect company in the other fields. Apple's partner networks operate the relay's egress hop, so asn and platform for the same address may name a CDN such as Akamai rather than Apple — the live example under Enriched facts does exactly that. privateRelay is the authoritative signal; the others say who carries the traffic.

Source: Apple's published egress geofeed (mask-api.icloud.com/egress-ip-ranges.csv, RFC 8805 — roughly 288k rows), refreshed every 24 hours.

Caveat: high-confidence when it hits, not exhaustive. The list drifts as Apple rotates ranges, so absence of a match proves nothing.

ASN — who routes this IP (asn)

asn is the Autonomous System that announces the IP's prefix on the public Internet — { "number": 15169, "name": "GOOGLE", "countryCode": "US" }. It tells you who routes the address: an ISP, a cloud, a university, an enterprise. Combined with the facts above it answers "is this a home connection or a rented server?" far better than country alone.

Source: the iptoasn.com combined IPv4 + IPv6 table (built from BGP route collectors, PDDL-licensed, updated hourly upstream), refreshed every 24 hours and kept in memory as a sorted interval table; overlapping announcements resolve to the narrower range.

Caveat: the name is the registry's description of the AS, which is sometimes terse or stale, and countryCode is the AS's registration country — not where the IP is in use. Classifying the AS (residential vs hosting vs education) is not part of this fact yet.

Reverse DNS — what the IP calls itself (reverseDns)

reverseDns is the address's PTR record, plus whether it checks out — { "name": "dns.google", "forwardConfirmed": true }. A PTR record is the reverse-DNS entry that maps an IP back to a hostname (8.8.8.8 → 8.8.8.8.in-addr.arpa → dns.google) — the answer dig -x or host prints.

Forward-confirmed means the loop closes: the name from the PTR record resolves forward (A / AAAA) to a set of addresses that includes the one you started from. That round trip is the procedure Google documents for verifying that a request claiming to be Googlebot really is — reverse-look up the IP, then forward-look up the name and check it lands back on the same address. forwardConfirmed: false means the name exists but the forward lookup did not come back to this IP.

The name is a claim, not a verification. PTR records are set by whoever controls the address space, so a hostname can say anything its operator likes — mail.google.com on an IP that has nothing to do with Google. Treat the name as verified only when forwardConfirmed is true; otherwise it is a label, handy when reading a log, not a basis for trust.

Source: a live DNS query at lookup time (not a cached feed), with a 1.5-second budget for the reverse and forward lookups together. If that budget runs out, or the address has no PTR record, the field is null and the other facts are unaffected.

Caveat: absence proves nothing. Plenty of legitimate addresses have no PTR record, and a slow resolver turns a real name into null for that one call.

TLS certificate — what the IP presents on :443 (tlsCertificate)

tlsCertificate is the certificate the address hands to a client that connects to port 443 and sends no SNI (no server name) — the default certificate for that address. For Google's resolver it reads { "subject": "dns.google", "issuer": "WR2", "names": ["dns.google", "dns.google.com", "*.dns.google.com", …], "forwardConfirmed": ["dns.google"], "addressInSan": true, "notBefore": "…", "notAfter": "…", "selfSigned": false, "truncated": false }.

Why a certificate is an IP fact. Every other full-layer fact is something a third party says about the address: the ASN is the registry's word, the platform is a published range, the PTR name is set by whoever owns the block. The certificate is the one thing the address says about itself, in its own voice — whoever holds the private key chose those names — and, unlike a feed, it is observed live from the target and carries no data licence. Read next to reverse DNS it fills the other's gap: many cloud addresses have no PTR but present a real certificate, and a PTR set by the carrier says nothing about the service. The ASN says who owns the address; the certificate says who is using it.

  • names — the subject-alternative DNS names the certificate claims, lower-cased, wildcards kept, capped at 25 (truncated: true past the cap). On a host that is not behind a CDN these usually name the sites it serves.
  • forwardConfirmed — the subset of non-wildcard names that resolve (A / AAAA) back to this address inside the budget: the same round trip reverse DNS uses. A name in a certificate is a claim; a forward-confirmed name is a verified one.
  • addressInSan — the address itself appears among the certificate's IP entries. That is the strongest confirmation available: the operator put this exact address in a certificate a CA signed.
  • selfSigned — issuer equals subject. Validity is deliberately not enforced: the fact is what the host presents, valid or not, and an expired or self-signed certificate is itself worth knowing.

What the probe does. One TCP connection, one TLS handshake with the target name set to the IP literal (so no SNI is sent), read the certificate, close. No application data is ever sent — it is what your browser does when you type the address into the bar. A presented certificate is remembered for 10 minutes per address and a miss for 1 minute, so a popular address costs one connection, not one per lookup, and a transient failure is retried soon. The handshake shares a 1.5-second budget with the forward confirmations.

Why CDN addresses read null. Cloudflare, Fastly, Akamai, Azure Container Apps and most shared front ends refuse a handshake without SNI — they cannot know which tenant's certificate to present. So an address that fronts many sites shows nothing here, and that is the honest answer: the address is a door to an unknown number of tenants — the demo says so under the row. The name is the missing key: look up the domain instead and the card sends it as SNI and shows the certificate the domain serves, next to the domain's DNS hints.

Caveat: absence proves nothing. A closed port, a firewall that drops the connection, a slow handshake, or an SNI-only host all yield null, and none of them says anything about the address.

Domain hints — what the domain's DNS and certificate say (lookup page only)

Look up a domain name on the home page and the card gains a Domain section under the IP facts. It is built from the domain's public DNS and one TLS handshake that names it — nothing else — and it describes the domain, not the IP. That distinction is the point: behind a CDN the address you resolve is the edge, shared by millions of sites, and the domain's own records, plus the certificate its name unlocks, are the only public trace of the service stack behind it. These hints are website-only; the REST API, the MCP server, and the CLI stay strictly IP-only.

  • DNS host — from the NS records: Cloudflare, Amazon Route 53, Azure DNS, Google, NS1, DNSimple, GoDaddy, Namecheap, or the raw name servers when unrecognised.
  • Mail — from the MX records: Google Workspace, Microsoft 365, Proton, Zoho, Fastmail, iCloud, Mimecast, Proofpoint, or the raw exchanges.
  • Senders (SPF) — the include: terms of the v=spf1 record, mapped to the senders they authorise (Google, Microsoft 365, SendGrid, Mailgun, Amazon SES, Zoho, Mailchimp, HubSpot, Salesforce, Postmark).
  • DMARC — the p= policy at _dmarc.<domain>: none, quarantine, or reject.
  • Verified with — the platforms the owner has proved control of the domain to, from verification TXT records (google-site-verification, MS= for Microsoft 365, Meta, Apple, Atlassian, DocuSign, Stripe, GitHub, OpenAI, Notion, Zoom). Only the token name is shown; token values are never returned.
  • Hosting hints — an asuid record (Azure App Service / Container Apps custom-domain validation) or a CNAME whose target belongs to a hosting platform (Azure, AWS, GitHub Pages, Netlify, Vercel, Heroku, Fly.io, Cloudflare Pages, Google, Shopify, Squarespace, Wix, WP Engine). A hint, not a proof: it says the owner set the record up, not that the site is served from there today.
  • TLS certificate — one more handshake to the resolved address, this time with SNI set to the host you typed: the certificate the domain actually serves (subject, issuer, names, validity), whether it covers that host (coversHost, wildcard-aware), whether the address is in it, whether it is self-signed, and whether the chain validates against a public root store (trusted, with the reason code when it does not). This is the row that answers a null in the IP's own tlsCertificate: a shared front presents nothing without a name and the right certificate with one. Same probe discipline as the IP probe — no application data, a 1.5-second budget, remembered 10 minutes per host and address — and never for an address that is not publicly routable.
  • Fronted by — the one row that comes from the IP facts: the address's platform when it is a CDN (Cloudflare, Fastly, Akamai, CloudFront), so the card can say "fronted by Cloudflare; hosting hints: Azure".

Source: live DNS queries at lookup time (NS, MX, TXT, the CNAME chain, _dmarc, asuid) with a 4-second budget, and one TLS handshake with SNI to the resolved address with a 1.5-second budget. Any record that does not answer is simply absent; if nothing answers, the section is omitted.

Attackee threat check — seen attacking our bait hosts? (/threat)

A separate, paid lookup beside the attribution facts: was this address seen attacking Attackee's bait hosts? Attackee is ipfacts' own honeypot telemetry — exposed Windows RDP and SMB hosts that record the failed logons against them, since 2022. Ask with GET /threat/{ip} on api.ipfacts.com or the ip_threat_check MCP tool, $0.001 in USDC per call via x402 (waived during the current free preview). It is priced on its own and is not part of /full.

{
  "ip": "192.0.2.10",
  "listed": true,
  "source": "Attackee",
  "occurrences": 12,
  "firstSeen": "2022-06-01T00:00:00+00:00",
  "lastSeen": "2024-02-02T12:00:00+00:00",
  "countries": ["CN", "US"],
  "observation": "failed logons against Attackee's bait hosts",
  "window": { "from": "2021-12-31T23:48:03+00:00", "to": "2026-11-14T00:59:19+00:00" },
  "asOf": "2026-11-14T05:01:53+00:00"
}

(Illustrative: a documentation address, not a real listing.) An address that is not on the list answers "listed": false with occurrences, firstSeen, lastSeen and countries null; the window and asOf are always there.

How to read it. An observation, not a verdict: a listing says the address tried to log on to Attackee's bait hosts in that window, nothing more. Addresses are reassigned and NAT and VPN exits are shared, so weigh lastSeen; and "listed": false means "not seen by these bait hosts in this window", not a clean bill of health. The counts and dates aggregate every row the list holds for the address — it keeps one row per address and country — and countries holds the codes recorded with those attempts. It is often empty: the upstream has no country for most recent sightings. window spans the whole list (earliest first sighting to latest last sighting); asOf is when the list was last rebuilt, daily. ipfacts checks for a new list every hour.

When the list is not available (a deployment without it, or a replica that has not loaded it yet) the endpoint answers 503 problem details and the MCP tool an error, before any payment is asked for or settled. A malformed address is a 400, also before payment.

Why only Attackee: ipfacts is an attribution product, and the third-party abuse feeds that were checked are not licensed for resale. Attackee's data is ipfacts' own, so it can be sold, credited by name, as one source's observation.

MCP — call from an AI agent

mcp.ipfacts.com exposes the same lookups as MCP tools so an LLM agent can call them directly:

  • ip_facts_basic(ip) — free. Returns the foundation facts.
  • ip_facts_full(ip) — paid per call via x402 (see below), waived during the current free preview. Returns the enriched shape.
  • ip_threat_check(ip) — paid per call via x402 at its own price ($0.001 in USDC). Returns the Attackee observation as { "ip", "attackee": { … } }.

Built on the official MCP C# SDK; any MCP-capable client (Claude Desktop, an Agent Framework runtime, your own client) can plug in.

Connecting is one line — the transport is streamable HTTP:

# Claude Code
claude mcp add --transport http ipfacts https://mcp.ipfacts.com/mcp

# any MCP client's config — the only thing it needs
{ "url": "https://mcp.ipfacts.com/mcp" }

# sanity check: the discovery document
curl -s https://mcp.ipfacts.com

Point at https://mcp.sandbox.ipfacts.com/mcp instead to exercise the paid tool with testnet USDC (see Sandbox below).

MCP is dynamic — not a static SDK

The difference between calling ipfacts over MCP and wiring up an old-school REST client isn't the wire format — it's when the contract is bound. A REST SDK bakes the operations, their parameters, and their docs into your program at build time; adding a tool or changing a description means a developer re-reads the docs, edits code, and redeploys.

With MCP the agent asks the server tools/list at runtime and gets each tool's name, its description, and a JSON input schema back as data. The model reasons over those descriptions to decide what to call and how — so the description is the contract. Nothing about ipfacts' tools is compiled into the client; it picks ip_facts_full over ip_facts_basic purely from what the server says they do.

It's also live. MCP defines a notifications/tools/list_changed push: when a server adds a tool, edits a description, or changes a schema, it can notify connected clients, and a compliant runtime (Claude Desktop, a Microsoft Agent Framework host, your own client) re-fetches tools/list on the open session — no reconnect, no redeploy. Even without the push, every new session re-discovers the current tools, so a client never drifts from the live surface.

Practical upshot: when we add a lookup, rename a tool, or sharpen a description, agents adapt on their own — no client release. That adaptability is the trade-off: an agent reading descriptions at runtime is more flexible than a hardcoded SDK, and correspondingly less rigidly deterministic.

WebMCP — this very page carries tools

Beyond the remote MCP server, every page on this site registers in-page tools via WebMCP — a draft browser standard (Chrome 149+ origin trial, Edge preview) that lets a web page hand the browser's AI agent typed tools, so the agent calls a function instead of scraping the UI:

  • my_ip — your public IP, same data as /ip, plus the curl ip.ipfacts.com / curl myip.ipfacts.com one-liners. Free.
  • ip_facts_basic — the free basic layer, for an IP address or a domain name.
  • ip_facts_full — a signpost: returns the endpoints, pricing pointer, and sandbox instructions for the x402-paid surfaces. Full facts are never served free in-page.
  • ipfacts_access — how to use ipfacts beyond this page: the MCP server, the REST endpoints and the CLI, which calls are free or x402-paid, and how to pay.
  • ipfacts_payment_rules — the payment rules, one line each: when a paid call is charged and what each payment problem returns.

No setup needed in Chrome 149–162 or Microsoft Edge — this site is in the WebMCP origin trials (Chrome through March 30, 2027, Edge through December 29, 2026), so the tools are already live on this origin. (Other builds: enable the flag — edge://flags / chrome://flags, search "WebMCP", relaunch.)

Try it by hand, right now: open DevTools on any page here and paste into the Console. Two gotchas the examples bake in: executeTool takes the tool object from getTools() (not its name) plus an input object, and the lookup key is target:

// what tools does this page carry?
const tools = await document.modelContext.getTools();
tools.map((t) => t.name);
// → [ "ip_facts_basic", "ip_facts_full", "ipfacts_access",
//     "ipfacts_payment_rules", "my_ip" ]
const run = (name, input = {}) =>
  document.modelContext.executeTool(tools.find((t) => t.name === name), input);

// your public IP
await run("my_ip");
// → { "ip": "203.0.113.42", … }

// free basic facts — the input key is "target" (IP or domain)
await run("ip_facts_basic", { target: "8.8.8.8" });
// → { "ip": "8.8.8.8", "isPublic": true, "addressFamily": "IPv4",
//     "specialRange": null }

// how to use ipfacts beyond this page: MCP, REST, CLI, prices, x402
await run("ipfacts_access");

// when a paid call is charged, and what each payment problem returns
await run("ipfacts_payment_rules");

// the paid layer — returns directions, not facts
await run("ip_facts_full");

(document.modelContext is the API in today's origin-trial builds, checked in Chrome 157 on 2026-11-15, and may change as the spec settles; with the testing flag on, older builds also offer navigator.modelContextTesting. Chrome's supported route is the Model Context Tool Inspector extension, which lists, inspects, and invokes a page's tools from a panel.)

Or let an agent do it: ask your browser's built-in agent "what's my IP?" or "is 10.0.0.7 a private address?" while on this site — it discovers and calls the tools on its own.

No trial, no flag, no agent? Nothing changes — registration is progressive enhancement behind feature detection, and the page works identically without it.

x402 — pay per call, no subscription

The enriched ip_facts_full surface charges $0.0001 in USDC per call via the x402 protocol — HTTP 402 Payment Required with a machine-readable challenge that an agent's wallet can sign and retry. Symmetric on REST and MCP: both api.ipfacts.com/full/{ip} and the MCP ip_facts_full tool gate on payment; ip_facts_basic is free on both surfaces.

Free preview. Right now the full lookup is served without payment on the REST API and the MCP server: the x402 paywall is configured but temporarily waived, so no 402 is returned. The price above is what a call costs once the paywall is switched back on.

On REST the 402 carries a PAYMENT-REQUIRED header — a base64 x402 v2 PaymentRequirements envelope naming the network, asset, amount and recipient; the client signs an authorization and retries with a PAYMENT-SIGNATURE header. On MCP, ipfacts follows the x402 v2 MCP transport: a paid tool called without payment answers isError with the same x402 PaymentRequired object in structuredContent; the client signs it and calls the same tool again with the payment in _meta["x402/payment"], and the settlement comes back in _meta["x402/payment-response"]. The official x402 SDKs for TypeScript and Python handle this for you. The live quote is always the authoritative price.

The Attackee threat check (/threat/{ip} and ip_threat_check) is paid the same way at its own price, $0.001 in USDC per call, and follows the same paywall switch.

Why we like x402 for this: you only pay for the calls you make, agents can authorize spend without a human in the loop, and we don't have to run a billing portal for the long tail of integrations.

The free ip_facts_basic tool is not paywalled, so anything that just needs public/private, address family, or special-range classification stays free. Country code is part of the paid layer.

Payment rules — when you're charged, and what each error means

How ipfacts charges for x402-paid calls (REST /full and /threat, MCP ip_facts_full and ip_threat_check) and what each payment problem returns. ipfacts follows the x402 v2 specification, with one documented difference: an answer whose settlement is still pending is delivered. Every REST error an API client gets is application/problem+json, and a payment error's type links to its rule below. The same rules as JSON: api.ipfacts.com/payment-rules; in this page, the ipfacts_payment_rules WebMCP tool.

You pay only for an answer you get
ipfacts verifies the payment with the facilitator, runs the lookup, and settles only when the lookup succeeded. A malformed address is refused before any price is quoted, and a lookup that can't be answered (for example a threat check while the list isn't loaded) is never settled.
No payment: a 402 with the quote
A paid call without payment gets the quote. REST answers 402 with the base64 x402 v2 PaymentRequired object in the PAYMENT-REQUIRED header and a problem+json body (a browser asking for HTML gets a readable page instead). MCP answers isError with the same object in structuredContent and in content[0].text. The quote offers one way to pay: scheme exact, USDC by EIP-3009 transferWithAuthorization, the token's EIP-712 name and version in extra, and maxTimeoutSeconds 60.
Paying on the first request
The terms a 402 quotes are also published: GET /pricing carries the exact accepts entry for /full/{ip} (accepts), for /threat/{ip} (threat.accepts) and for each paid MCP tool (paid_tools[].accepts). Sign it and send the payment with the first request; resource is optional in an x402 v2 PaymentPayload. REST paid calls are GET with PAYMENT-SIGNATURE (another method is 405). On MCP the payment rides a normal session: after initialize, the first tools/call may carry _meta["x402/payment"]. While /pricing says paid: false (the free preview) no payment is needed, and one sent is ignored and never charged. Prices can change, so a payment for an old quote is refused (402) with the current one.
A malformed payment: 400
A PAYMENT-SIGNATURE header that isn't base64 JSON of an x402 v2 PaymentPayload carrying an exact EVM authorization and a signature, or that repeats a JSON key, is answered 400 with the reason in a problem+json body and a fresh PAYMENT-REQUIRED. On MCP the same payment in _meta["x402/payment"] gets isError with the quote and the reason. Nothing is charged.
A payment that doesn't fit the quote: 402
The payment must accept the quote exactly: the same scheme, network, asset, amount and payTo; the default EIP-3009 transfer (another assetTransferMethod or paymentFlow is refused); and an authorization to payTo for exactly the amount, valid now (validAfter ≤ now < validBefore). A mismatch, or a facilitator refusal such as insufficient balance or a bad signature, is a 402 with the x402 error code in PaymentRequired.error. The lookup didn't run and nothing is charged.
One signature pays for one call
Each signed authorization pays for one call. Sent again while the first call is still running ("payment authorization is already in use by another request"), or after it has bought an answer, it is refused with 402; once it has settled, its EIP-3009 nonce is spent and the facilitator refuses it too. Sign a new authorization for every call.
Paid: the answer and the settlement
A settled call answers 200 with the facts and the x402 SettlementResponse (success, transaction hash, network, payer) in the PAYMENT-RESPONSE header. On MCP the tool result carries it in _meta["x402/payment-response"].
Settlement refused: the answer is withheld
If the facilitator refuses the settlement, for example because the transfer failed on chain, the answer is withheld. REST answers 402 with the failed SettlementResponse in PAYMENT-RESPONSE and a fresh PAYMENT-REQUIRED; MCP answers isError with the failed SettlementResponse in _meta["x402/payment-response"]. Every refusal except settle-pending is final, and a new signed authorization may try again.
Settlement pending: you get the answer
settlement_pending means the facilitator sent the transfer but could not yet confirm it. ipfacts retries settlement once with the same signed payment, which cannot move money twice. If it is still pending, you get the answer anyway: REST 200 with the SettlementResponse (success false, errorReason settlement_pending, the transaction hash) in PAYMENT-RESPONSE; MCP a normal result with it in _meta["x402/payment-response"]. The transfer normally confirms: check the hash on chain, and don't pay again for that call. This is ipfacts' one difference from the x402 specification, where only a settled payment gets 200.
Facilitator unreachable: 500
If the facilitator can't be reached, or doesn't answer in time, the call fails with 500 problem+json (MCP: isError) and no answer. During verification nothing was charged. During settlement there is no transaction hash and the transfer may still happen: before paying again, check whether your authorization was used (the USDC contract's authorizationState(from, nonce) turns true). A settlement_pending without a transaction hash is treated the same way. ipfacts has no way to collect an answer later.
Free preview
While the paywall is waived, /pricing reports paid: false and free_preview: true, and the paid calls answer without payment. The quote and these rules apply again when the paywall is switched back on.
Sandbox & testnet — try x402 with play money

Before paying for real on api.ipfacts.com, there's a permanent testnet twin: api.sandbox.ipfacts.com and mcp.sandbox.ipfacts.com. Same binary, same tools — but it settles on Base Sepolia testnet through the free, public x402.org facilitator, so every call is paid in testnet USDC — play money, no real spend.

Payments are gasless (EIP-3009): the facilitator submits the on-chain transfer, so your wallet only needs testnet USDC, not Sepolia ETH. Grab some from the open faucet:

  • Testnet USDC faucet: faucet.circle.com — choose Base Sepolia, paste your wallet address, receive 20 test USDC. No login; up to once every 2 hours.

Because the payments are real on-chain transfers, you can watch them. The sandbox flow has two wallets — the client that pays and the payTo that collects — both inspectable on the Base Sepolia explorer:

Point your own client at the sandbox and your payer address shows up here too — the signed authorization and the USDC transfer are both on-chain and inspectable.

Your public IP (the /ip echo)

Every ipfacts host will tell you your own public IP — free, no auth, nothing to construct:

$ curl -s ip.ipfacts.com     # the shortest form
203.0.113.42

$ curl myip.ipfacts.com      # same answer, the guessable alias

$ curl ipfacts.com/ip        # same answer from the website host
$ curl api.ipfacts.com/ip    # same answer from the API host

The response is plain text by default — one line, no quotes — so it drops straight into shell scripts (ipify-compatible). Ask for JSON with Accept: application/json or ?format=json and you get { "ip": "203.0.113.42" } instead. HEAD is supported, so curl -I won't 405.

Related: on api.ipfacts.com, GET /basic and GET /full with no IP argument run the lookup on the caller's own IP — "what does ipfacts know about me?"

REST API reference
Endpoint Returns Auth
GET / Self-describing index — endpoint list, live x402 terms (network, asset, price), and deployed version. None.
GET /ip Caller's public IP — plain text by default, JSON via Accept or ?format=json. Also at ip.ipfacts.com, myip.ipfacts.com and ipfacts.com/ip. None.
GET /basic/{ip} Foundation facts (public/private, address family, special range) — offline, free. None.
GET /basic Same foundation facts, for the caller's own IP. None.
GET /full/{ip} Enriched facts (above + country, Tor, hosting platform / CDN, crawler, iCloud Private Relay, ASN, reverse DNS, TLS certificate). x402-paid — the first call returns 402 with a PAYMENT-REQUIRED challenge header; retry with a signed PAYMENT-SIGNATURE header. Waived during the current free preview.
GET /full Same enriched facts, for the caller's own IP. x402-paid (waived during the free preview).
GET /threat/{ip} The Attackee threat check: whether the IP was seen attacking Attackee's bait hosts, with counts, dates, countries and the list's window. 503 when the list is not available. x402-paid at its own price ($0.001 in USDC). Waived during the current free preview.
GET /pricing The live price of /full (asset, network, amount), whether the paywall is active, the listed plans — the projection the pricing page renders — and a threat block with the threat check's price. None.
GET /livez 200 if the process is alive. None.
GET /readyz 200 if dependencies are ready. None.
GET /version Service name, version (semver with the commit SHA as build metadata), and build time. None.

Base URL: https://api.ipfacts.com.

Errors are RFC 9457 problem details (application/problem+json): a malformed address is a 400 whose detail spells out the accepted forms.

The API is self-describing — no docs required to orient: curl -s api.ipfacts.com returns the index above as JSON, including the live x402 terms and the deployed version. The same host-routed handler makes curl -s mcp.ipfacts.com return the MCP discovery document — transport, endpoint, current tool list, and which tools are paid.

CLI

The ipfacts CLI is a thin wrapper over the free /basic lookup for terminal and scripting use — one argument in, JSON out:

$ ipfacts 8.8.8.8
{ "ip": "8.8.8.8", "isPublic": true, "addressFamily": "IPv4", "specialRange": null }

$ ipfacts 240.0.0.1
{ "ip": "240.0.0.1", "isPublic": false, "addressFamily": "IPv4", "specialRange": "reserved" }

$ IPFACTS_API_URL=https://api.sandbox.ipfacts.com ipfacts 2001:db8::1
{ "ip": "2001:db8::1", "isPublic": false, "addressFamily": "IPv6", "specialRange": "documentation" }

Exit status is 0 on success, 1 on an HTTP or network error (the message goes to stderr), and 2 for usage. IPFACTS_API_URL points it at another host. It is packaged as a .NET tool. The paid /full layer is REST and MCP only — the CLI does not negotiate x402.

Limits & freshness
  • Special ranges: classified offline from a table in the code; a new IANA block arrives with a release, not a feed.
  • Tor list: refreshed every 30 minutes.
  • Azure ServiceTags: refreshed every 24 hours.
  • AWS ip-ranges: refreshed every 24 hours.
  • Platform + crawler range lists, Private Relay geofeed, ASN table: refreshed every 24 hours.
  • Reverse DNS: live PTR + forward query per lookup, 1.5-second budget; null when it doesn't answer in time.
  • TLS certificate probe: one live handshake to port 443 per lookup, 1.5-second budget shared with the forward confirmations; a certificate is remembered 10 minutes per address, a miss 1 minute; null when the port doesn't answer or needs SNI.
  • Domain hints (lookup page only): live DNS queries per domain lookup, 4-second budget; missing records are omitted. Plus one TLS handshake with SNI to the resolved address, 1.5-second budget, remembered 10 minutes per host and address; no row when it does not answer.
  • Country lookup: Azure Maps — sub-second, no per-IP cache layer yet.
  • Rate limits: none enforced today on the free REST surface; please don't make us add some.