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, and cloud provider classification (Azure and AWS — with the specific service tag where the provider publishes it).

Both layers are exposed via the REST API, the MCP server, and a CLI.

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).
  2. Public vs private. RFC 1918 / RFC 4193 / loopback / link-local. Private IPs short-circuit before we hit any external provider — no leaks, no cost.
  3. Address family. "IPv4" or "IPv6".
  4. Special range. the RFC classification of reserved space (see below).

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": "20.232.0.137",
  "countryCode": "US",
  "isPublic": true,
  "addressFamily": "IPv4",
  "specialRange": null,
  "isTor": false,
  "azure": { "service": "AzureCloud", "region": null },
  "aws": null
}

Private IPs return the same shape but with countryCode: "??", a non-null specialRange, and azure / aws as null — no provider calls, no telemetry, no surprise cost.

Special-range classification

Every lookup classifies the address against the reserved IPv4/IPv6 ranges defined by the RFCs. specialRange is null for publicly routable space, otherwise one of:

  • Private — RFC 1918 / RFC 4193 (ULA).
  • Loopback127.0.0.0/8, ::1.
  • LinkLocal169.254.0.0/16, fe80::/10.
  • Cgnat — RFC 6598 carrier-grade NAT space.
  • Documentation — RFC 5737 / RFC 3849 example ranges.
  • Multicast, Broadcast, Unspecified.

Private, reserved, and documentation IPs short-circuit before any external geolocation call — the classification is purely local.

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.

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

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). Returns the enriched shape.

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. 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.

No setup needed in Chrome 149–156 — this site is in the WebMCP origin trial through November 17, 2026, so the tools are already live on this origin. (Edge 147+ or 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: the input is a JSON string, and the lookup key is target:

// what tools does this page carry?
await navigator.modelContextTesting.listTools();
// → [ my_ip, ip_facts_basic, ip_facts_full ] with descriptions + schemas

// your public IP
await navigator.modelContextTesting.executeTool("my_ip", "{}");
// → { "ip": "203.0.113.42" }

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

// the paid layer — returns directions, not facts
await navigator.modelContextTesting.executeTool("ip_facts_full", "{}");

(navigator.modelContextTesting is the console hook in today's origin-trial builds and may be renamed as the spec settles; 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.

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 country / public-private classification stays free.

Sandbox & testnet — try x402 with play money

Before pointing real USDC at 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 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 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 + Tor + Azure/AWS classification). x402-paid — first call returns 402 with a v2 payment challenge; retry with X-PAYMENT.
GET /full Same enriched facts, for the caller's own IP. x402-paid.
GET /livez 200 if the process is alive. None.
GET /readyz 200 if dependencies are ready. None.
GET /version Service name, semver, and commit SHA. None.

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

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 wraps the same lookups for terminal and scripting use — pipe an IP in, get JSON out:

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

$ ipfacts full 20.232.0.137
{ ..., "isTor": false, "azure": { "service": "AzureCloud" } }

$ ipfacts full 3.5.140.1
{ ..., "aws": { "service": "EC2", "region": "ap-northeast-2" } }

$ ipfacts full 192.42.116.20
{ ..., "isTor": true }

basic is free; full negotiates x402 payment with a configured wallet, the same way the MCP ip_facts_full tool does.

Limits & freshness
  • Tor list: refreshed every 30 minutes.
  • Azure ServiceTags: refreshed every 24 hours.
  • AWS ip-ranges: refreshed every 24 hours.
  • 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.