# IP Almanac: agent guide IP Almanac answers questions about public IPs and domains from observed DNS: which domains were seen resolving to an IP and when, what a domain resolved to over time, and each IP's mapped ASN, published provider ranges and PTR names. It also says whether an IP is listed as one of Tor exits, iCloud Private Relay egress and DuckDuckGo and Google crawlers. For a network (an AS number) it lists the mapped IP ranges and what was observed in them. It is served over HTTP and as an MCP server. This page is everything an agent needs to call it and read the answers correctly. ## When to use it - **Shared-hosting investigation: who else is on this IP.** `lookup_ip` for the count and a sample, then `ip_domains` for the full list with dates. Coverage is strongest on shared-hosting platforms, website builders and CDNs that serve popular sites, where one IP can carry thousands of domains. - **Phishing and brand-abuse investigation.** A suspicious domain is usually new or long-tail, so it is rarely in `domain_history`. Resolve it yourself, then call `lookup_ip` on each address: the routing network, the provider range and PTR name where known, and the established domains on the same address. Treat shared domains as neighbours, not as the same owner. - **Infrastructure mapping.** `domain_history` for where a popular domain pointed and when it moved; `lookup_ip` on each address for its network, provider and CDN ranges. - **Traffic triage.** `lookup_ip` on a visitor or log IP says whether it is listed as one of Tor exits, iCloud Private Relay egress and DuckDuckGo and Google crawlers (`tor_exit`, `relay` and `crawler` in `provider_ranges`). This works for any IP the operators list, not only those in the seed. It says how the IP is used, never who hosts it or whether the traffic is wanted; see Usage ranges. Expect little or nothing for residential and ISP addresses, small dedicated servers, subdomains (only registered domains are queried, so `www.` and `mail.` names are mostly absent) and domains outside the seed list. Say so to the user rather than reading an empty answer as "nothing there"; see Coverage limits. ## Connect API base URL: https://skeleton-api-ouax.onrender.com Lookups need an API key in the `Authorization` header: ``` Authorization: Bearer ``` Create a key at https://app.ipalmanac.com/dashboard/api-keys: sign in with an email link, then create a key in the console. Never put the key in the URL; a key in a query parameter is refused. ### MCP The MCP server is at https://skeleton-api-ouax.onrender.com/mcp (Streamable HTTP, JSON responses). It serves both MCP protocol eras: clients that send a protocol version on every request (2026-07-28) and clients that open with `initialize`. Configure it with the same header: ```json { "mcpServers": { "ip-almanac": { "type": "http", "url": "https://skeleton-api-ouax.onrender.com/mcp", "headers": {"Authorization": "Bearer "} } } } ``` In Claude Code: ``` claude mcp add --transport http ip-almanac https://skeleton-api-ouax.onrender.com/mcp --header "Authorization: Bearer " ``` Listing the tools needs no key. Calling one runs the same key check, rate limit and quota as the HTTP API, and returns the same JSON as the matching endpoint, as both `structuredContent` and text. A refused call comes back as a tool error (`isError: true`) whose text carries the error code from the table under Errors, after the tool name: `Error executing tool lookup_ip: invalid_ip: ...`. ## Endpoints and tools | HTTP | MCP tool | Answers | | --- | --- | --- | | `GET /v1/ip/{ip}` | `lookup_ip` | One public IP: `asn` (with the network's `category` and `network_role`), `is_datacenter`, `is_cdn`, `provider`, `prefix`, `provider_ranges`, `ptr`, and `domains` (count, a sample of 10, first and last observation). | | `GET /v1/ip/{ip}/domains` | `ip_domains` | Reverse IP: every domain seen resolving to the IP, one item per answer interval, oldest first. Current answers only unless `include_ended=true`. | | `GET /v1/domain/{domain}/history` | `domain_history` | DNS history (from our own resolver): the domain's current A and AAAA state (`outcome`, `values`, `cnames`, `stale`) and every answer interval it gave, oldest first. | | `GET /v1/asn/{asn}` | `lookup_asn` | One network: its name and registry country, the IP ranges the mapping assigns to it, the published cloud, hosting and CDN ranges inside them, and `observed` (IPs and domains seen there). Ranges are paginated. | | `GET /v1/domain/{domain}/hosting` | `domain_hosting` | Hosting history: the networks (routing ASN and cloud, hosting or CDN provider) a domain's A and AAAA answers moved through, one run per stretch on one network, oldest first, with `first_observed`, `last_confirmed` and `ended_at`. A move happened between a run's `last_confirmed` and its `ended_at`. A return to a network is a new run. Counts one request. | Use `lookup_ip` first for an IP; it is one request and says how many domains there are. Use `ip_domains` when you need them all, and `domain_history` when the question starts from a domain. Inputs: an IP is IPv4 or IPv6; an IPv4-mapped IPv6 address is looked up as its IPv4 address. Private, reserved, loopback, link-local, documentation, CGNAT and multicast addresses (bogons) are refused by `ip_domains`; `lookup_ip` answers them with `is_bogon: true`, the IANA block in `bogon` and nothing else, and does not count them. Every other `lookup_ip` answer reads `is_bogon: false`, which does not mean the address is allocated. An AS number is `64500` or `AS64500`. A domain is a hostname with at least two labels; it is lowercased, its trailing dot dropped and Unicode labels punycoded. Every answer also carries `coverage` (what was observed, from where, and `semantics`: these reading rules), `attribution` (the sources to credit) and `release` (`id` and `built_at` of the data snapshot that answered). ## Reading the answers - Null means unknown, never false. `is_datacenter` and `is_cdn` are `true` only inside a published provider range of that category (cloud or hosting; CDN). Outside every range they are `null`, which does not mean the IP is not hosted. - `provider` and `prefix` come only from cloud, hosting and CDN ranges. `provider_ranges` can also list ranges that say how an IP is used (`tor_exit`, `relay` and `crawler`); those never name the provider or set `is_datacenter`. - Empty means unobserved here, not absent. No domains on an IP means none of the seed domains were seen resolving to it, not that no site is hosted there. `domain_history` says which: `observed: false` means the name was never queried here (it is outside the seed list), so tell the user it is not covered rather than that it has no DNS. - A CDN edge is not the customer's origin, and domains sharing an IP share no implied owner. - `asn` is an imported IPtoASN mapping, not a BGP observation. The routing ASN is not necessarily the address holder or the tenant: many CDN-routed IPs sit outside the CDN's published ranges and read `is_cdn: null`. ASN 0 ("not routed") reads as `asn: null`. - `asn.category` (`isp`, `hosting`, `cdn`, `business`, `banking`, `education_research`, `government_admin`) is this API's own classification of the routing network where it has one and ipverse as-metadata's where it has none; `asn.category_source` says which (`own` or `ipverse`, `null` when `category` is). `asn.network_role` (`tier1_transit`, `major_transit`, `midsize_transit`, `access_provider`, `content_network`, `stub`) comes from ipverse as-metadata. Both describe the routing network's primary function and role, never the IP's tenant: a hosting ASN can carry corporate gateway egress, and an ISP or transit network can read `hosting`. `null` is unclassified. They never set `is_datacenter`, which stays `true` only inside a published range. - `ptr` is `null` when the IP's PTR was never checked. `ptr.names` holds every current name, usually one. - DNS outcomes stay distinct: `positive`; negative (`nxdomain`, `nodata`); transient (`timeout`, `servfail`, `malformed`). A transient failure never erases an earlier answer; it makes the state `stale`. A state is also stale when it was never confirmed or was last confirmed more than seven days ago (`stale_reason`). - A domain's history keeps every answer it gave, including private or reserved addresses; those addresses have no IP lookup. - Times are UTC, ISO 8601. `first_observed` and `last_confirmed` are when this service saw an answer, not when the record was created or changed at the source. ## Coverage limits - The seed is a list of popular registered domains (the Majestic Million). Long-tail and newly registered domains, subdomains, and IPs that only they use, are mostly absent. Residential and ISP addresses rarely carry any. - One vantage point and one resolver. Answers that vary by location (GeoDNS, anycast) are seen as that resolver sees them. - Forward records are A and AAAA, with the CNAME chain. PTR is checked for a growing share of observed IPs; `coverage.ptr` gives the counts when the release reports them. - History starts when collection started; `coverage.observation_window` gives the span. - Provider ranges come from a few large providers' published feeds. Many hosting IPs match none of them and read `null`. - The data is a snapshot, rebuilt periodically; `release.built_at` says when it was built. - Not offered: geolocation, abuse scores, WHOIS or company ownership. ## Usage ranges `provider_ranges` lists every published range that holds the IP. Besides cloud, hosting and CDN ranges it can hold ranges that say how the IP is used, each from the operator's own list: - `tor_exit`: the address was seen as a Tor exit in the Tor Project's exit list (one `/32` per address, about the last day). `metadata.kind` is `exit_observed`. IPv6 exits are not in that list. - `relay`: the address is in Apple's published iCloud Private Relay egress ranges. Many users share one egress address, much like carrier NAT, so one address is not one person. - `crawler`: the address is in a crawler range its operator publishes (DuckDuckGo and Google). `metadata.operator` names the operator and `metadata.bot` the list. The range says the operator crawls from that address; it does not say that a given request came from its crawler. Entries as `lookup_ip` returns them (from the operators' lists as published on 2026-10-07): ```json {"prefix": "171.25.193.25/32", "provider": "tor-exit", "category": "tor_exit", "metadata": {"kind": "exit_observed"}, "first_seen": "2026-10-07T00:42:05+00:00", "last_seen": "2026-10-07T00:42:05+00:00"} {"prefix": "172.224.226.0/27", "provider": "apple-private-relay", "category": "relay", "metadata": {}, "first_seen": "2026-10-07T00:00:00+00:00", "last_seen": "2026-10-07T00:00:00+00:00"} {"prefix": "66.249.64.0/27", "provider": "google-common-crawlers", "category": "crawler", "metadata": {"bot": "google-common-crawlers", "operator": "Google"}, "first_seen": "2026-10-06T14:47:46+00:00", "last_seen": "2026-10-06T14:47:46+00:00"} ``` These never set `provider`, `prefix`, `is_datacenter` or `is_cdn`, and an IP outside every list reads no such range: that means not listed, never a "no". `first_seen` and `last_seen` say when the list first and last carried the range. ## Networks `lookup_asn` (`GET /v1/asn/{asn}`) reads the same IPtoASN mapping as `lookup_ip`'s `asn`: an imported mapping, not a BGP observation, and the routing ASN is not necessarily the holder of every address it routes. - `is_bogon` is `true` for an AS number IANA reserves for private use, documentation or another special purpose (AS_TRANS 23456, 64496-65551, 4200000000-4294967295), with the block in `bogon`. Such a number is not a public network: expect no ranges. `false` does not mean the number is allocated. - `name` and `country` are the mapping's; `category`, `category_source` and `network_role` are the network's primary function (this API's own classification, or ipverse as-metadata's where it has none), where that comes from, and its connectivity role (`null` is unclassified), read as `lookup_ip`'s `asn.category`; `routes` counts its IPv4 and IPv6 ranges and `ipv4_addresses` the addresses in them. An ASN the mapping does not list answers with no ranges. - `providers` counts the published cloud, hosting and CDN ranges that lie inside the ASN's ranges, per feed. A provider range larger than the ASN's own is not counted, and an empty list means none matched, not that the network hosts nothing. - `observed` is the IPs in the ASN's ranges with at least one current domain, and the distinct domains whose current answer is one of them. It is `null` when the data snapshot does not count them: unknown, never zero. - `ranges` lists the mapped ranges, IPv4 then IPv6, each with its CIDR blocks, paginated like `ip_domains`. ## Pagination `ip_domains`, `domain_history` and `lookup_asn` (its ranges) return up to `limit` items (default 100, at most 1000) and a `next_cursor`. Pass `next_cursor` back unchanged as `cursor` with the same other arguments for the next page; it is `null` on the last page. Each page counts one request. A cursor is valid until the data snapshot changes; after that it answers `cursor_expired` and you start again without one. ## Errors HTTP errors are JSON: `{"error": "", "message": ""}`. Over MCP the same code follows the tool name in the tool error's text. | Status | `error` | Meaning | Counted | | --- | --- | --- | --- | | 400 | `invalid_cursor` | The cursor was not issued for this query. | No | | 401 | `unauthorized` | The key is missing, malformed, revoked or expired. | No | | 402 | `quota_exceeded`, `project_cap_exceeded` | The monthly quota or the project's cap is used up. A lapsed subscription falls back to the free plan's limits. | No | | 410 | `cursor_expired` | The data changed since the cursor was issued; start again without one. | No | | 422 | `invalid_ip`, `non_public_ip`, `invalid_domain`, `invalid_asn` | The input can't be looked up. An out-of-range `limit` gets FastAPI's `{"detail": [...]}` instead. | No | | 429 | `rate_limited` | Too many requests this minute, or too many failed keys from one address. Wait `Retry-After` seconds. | No | | 503 | `release_unavailable`, `service_unavailable` | No data loaded, or a brief overload. Retry shortly. | No | ## Quotas and rate limits HTTP responses carry the key's limits: `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (requests per minute), and `X-Quota-Limit`, `X-Quota-Used` and `X-Quota-Remaining` (requests this month). A key whose project has a monthly cap also gets `X-Project-Quota-Limit`, `X-Project-Quota-Used` and `X-Project-Quota-Remaining`. ## Attribution Every answer lists its sources in `attribution`. Credit them when you republish the data; the seed list (Majestic Million) is licensed CC BY 3.0.