Learn
What an "IP fact" is, where the data comes from, and how to consume it from code or an AI agent. Skim or jump to a section.
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:
- Canonical form. The parsed, normalized address
(
ip). - Public vs private. RFC 1918 / RFC 4193 / loopback / link-local. Private IPs short-circuit before we hit any external provider — no leaks, no cost.
- Address family.
"IPv4"or"IPv6". - 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).Loopback—127.0.0.0/8,::1.LinkLocal—169.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:
- Payer — the lab client's wallet
(
0xcB7afF71…aE4bC13): view transactions. - Recipient — the sandbox
payTo(0xE8E20587…fc5A02C69): view transactions.
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.