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: country, public vs private, IPv4 vs IPv6, and a special-range classification.
  • /full — enrichment on top of /basic: 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.

Country + public/private (the /basic layer)

The /basic lookup answers a few questions, fast:

  1. Country. ISO-3166 two-letter code, sourced from Azure Maps. We return "??" when the geolocation provider has no answer (private IPs, reserved ranges, unknown allocations).
  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).

The provider abstraction means we can swap geolocation backends without consumers noticing — Azure Maps today, something else tomorrow.

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.

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.

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.

REST API reference
Endpoint Returns Auth
GET /basic/{ip} Foundation facts (country, public/private, address family, special range). 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 /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.

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
{ "countryCode": "US", "isPublic": true, "addressFamily": "IPv4" }

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