# Graph & cognition

**An agent about to fetch a URL has about a millisecond to decide whether it should — and "run it through a blocklist" isn't an answer, it's a guess with a timestamp.**

A static feed is hours behind a fast-flux C2 domain; `whois` tells you who registered a name, not who's serving traffic behind the CDN in front of it; and none of it comes back with the receipts a human, an auditor, or another agent needs to trust the call. Whisper answers differently: one call against a live graph of the internet — DNS, BGP, WHOIS, TLS, hosting, threat-intel, fused and current — that hands back a verdict **and the exact evidence behind it**, in under 300ms.

## The question every agent asks before it acts

Three shapes of the same question come up constantly, and none of them are answerable from a flat list:

- *"Who actually runs this host?"* — `api.openai.com` and half the internet sit behind Cloudflare or a load balancer. The IP tells you the CDN, not the tenant.
- *"Is this indicator safe to touch?"* — an IP might be a Tor exit, a bulletproof host, or a freshly-registered domain that hasn't earned a reputation yet either way.
- *"Why should I believe that?"* — a verdict with no reasoning attached is not a decision you can defend later, to a compliance reviewer or to yourself.

Whisper's cognition surface is the graph-first answer to all three, and it's the same graph that powers [graph-first resolution](/docs/resolver) — the policy engine that decides whether a query even leaves an agent's `/128` runs these exact verbs internally, on your behalf, before every answer.

## The surface: named verbs over one graph

Every verb below is a read-only `CALL` against the shared graph. They group into four jobs:

| Group | Verb | What it answers |
|---|---|---|
| **Attribution** | `identify(target)` | Who really operates a host or IP — operator, category, and the resolution chain that gets you there, even behind a CDN |
| | `asset(id)` | The full graph record for one node — hostname, IPv4/IPv6, ASN, or organization — by identifier |
| | `psl(domain)` | The registrable domain boundary for a name, per the [Public Suffix List](https://publicsuffix.org/) — `sub.example.co.uk` → `example.co.uk` |
| **Risk** | `assess(targets[])` | A risk verdict for one or more indicators — verdict, severity, category (Tor exit, bulletproof hosting, scanner, …) |
| | `explain(target)` | The reasoning behind an `assess` verdict — which feeds fired, the score, how fresh the observation is |
| | `threatintel(indicator)` | Direct hits against the fused threat-intelligence feeds for one indicator, unscored |
| | `lookupTorRelay(ip)` | Whether an address is a current Tor relay or exit node |
| | `lookupTlsFingerprint(ja3\|ja4)` | Known clients/tooling associated with a TLS client fingerprint |
| **Topology** | `walk(node, depth)` | Traverse outward from a node along graph edges — infrastructure clusters, shared-hosting genealogies |
| | `origins(prefix)` | BGP origin history for a prefix — who has announced it, and when |
| | `topAsnsByPrefixCount()` | Internet-wide aggregate: ASNs ranked by announced prefix count |
| | `variants(domain)` | Look-alike / typosquat domain variants for a brand or name |
| **Monitoring** | `watch(target)` | Register a standing watch on an entity so future graph changes surface as events |
| | `history(target)` | The historical timeline of DNS, WHOIS, and ownership changes for a target |
| | `submit(indicator)` | Contribute an observation into the shared graph |

Every verb is live today. A caller may also run **arbitrary read-only Cypher** — `MATCH` patterns of your own — directly against the shared subgraph; the named verbs are just the common queries pre-packaged as one call.

## How to ask: two doors, one graph

Both doors run the identical query engine and return the identical shape. The only difference is how the caller is authenticated.

**With your key, from anywhere**, over the gateway:

```bash
curl -s https://graph.whisper.security/api/query \
     -H 'X-API-Key: whisper_live_…' \
     --data "CALL whisper.assess(['185.220.101.1'])"
```

**Keyless, from inside a connected agent** — the query rides the agent's own routable address, and that `/128` *is* the credential, so no key travels on the wire at all:

```bash
curl -s https://[2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4]/api/query \
     --data "CALL whisper.identify('api.stripe.com')"
```

The same keyless pattern is exposed as a plain HTTP endpoint too, purpose-built for the single most common cognition question — *"is the thing I'm about to connect to safe?"* — asked from inside an agent that has no key on hand at all:

```bash
curl -s https://rdap.whisper.online/identify/api.openai.com
# -> {"host":"api.openai.com","operator":"Cloudflare","category":"cdn",
#     "evidence":"RESOLVES_TO -> IPV4 -> DELEGATED_TO -> cloudflare"}

# the identical call from outside a connected agent's address:
curl -s https://rdap.whisper.online/identify/api.openai.com
# -> 403 — the same lock that authenticates the agent is what authorizes the answer
```

That 403 is not a bug to route around — it's the point. `identify` from `/identify/<host>` is scoped to callers whose *source address* is already a Whisper agent; the network layer does the authentication, so the endpoint never has to trust a header a caller could forge.

## Under the hood: what "evidence" actually means

The query language is [openCypher](https://opencypher.org/) — the same declarative graph pattern language used across the property-graph ecosystem — sent as plain text in the request body and answered as JSON rows. A verb like `identify` is a named, parameterized Cypher procedure; running it is equivalent to writing the underlying `MATCH` pattern yourself, without needing to know the schema.

What matters for trust is what comes back alongside the verdict. Every attribution or risk answer carries an `evidence` field that is the literal traversal the graph made to reach its conclusion — a chain of typed nodes and relationships, not a black-box score:

```
identify('api.openai.com')
  -> operator=Cloudflare · category=cdn
  -> evidence: (HOSTNAME)-[:RESOLVES_TO]->(IPV4)-[:DELEGATED_TO]->(ORGANIZATION:Cloudflare)

assess(['185.220.101.1'])
  -> suspicious · MEDIUM · Tor exit
explain('185.220.101.1')
  -> 3 feeds · score 5.3 · last seen 3h ago
```

`identify` walked `RESOLVES_TO` (a DNS answer) into `DELEGATED_TO` (a WHOIS/RDAP organization edge) and stopped at an `ORGANIZATION` node — that's the whole chain, reproducible, and disprovable if it's wrong. `explain` unpacks an `assess` verdict the same way: which of the fused threat-intel feeds contributed, the resulting score, and the age of the freshest observation behind it. Nothing here is a magic number; it's a graph traversal you can read.

## Dual example: attribution, the hard way and the easy way

**With stock tools** — chase the resolution chain yourself and hope the last hop tells you something:

```bash
dig +short api.openai.com                          # CNAME/A chain, ends on a CDN edge IP
whois -h whois.arin.net 104.18.0.0                  # org on the IP -- usually the CDN, not the tenant
curl -s https://rdap.whisper.online/ip/104.18.0.0 | jq .entities
# best case: "Cloudflare, Inc." -- correct, but you still don't know who's BEHIND Cloudflare
```

None of those three commands tells you the tenant behind the edge — CDNs exist specifically to hide that. `identify` says "Cloudflare, cdn" honestly rather than guess further; for names it fully resolves and owns, the chain terminates at the real organization instead.

**With Whisper** — the same investigative chain, pre-walked, in one round trip:

```bash
curl -s https://graph.whisper.security/api/query -H 'X-API-Key: whisper_live_…' \
     --data "CALL whisper.identify('api.openai.com')" | jq .
# {"host":"api.openai.com","operator":"Cloudflare","category":"cdn",
#  "evidence":"RESOLVES_TO -> IPV4 -> DELEGATED_TO -> cloudflare"}
```

## Dual example: risk, the hard way and the easy way

**With stock tools**, checking one IP against Tor and whatever abuse databases you have accounts for means several disjoint tools that don't agree on a scale or a timestamp:

```bash
curl -s https://check.torproject.org/torbulkexitlist | grep -qx 185.220.101.1 && echo "tor exit"
whois -h whois.abuseipdb.example 185.220.101.1 2>/dev/null   # only if you have an abuse-DB account
```

**With Whisper**, one verdict and one explanation, on the same fused graph the resolver itself queries:

```bash
curl -s https://graph.whisper.security/api/query -H 'X-API-Key: whisper_live_…' \
     --data "CALL whisper.assess(['185.220.101.1'])"        # -> suspicious · MEDIUM · Tor exit
curl -s https://graph.whisper.security/api/query -H 'X-API-Key: whisper_live_…' \
     --data "CALL whisper.explain('185.220.101.1')"         # -> 3 feeds · score 5.3 · seen 3h ago
```

## Latency, and what happens when the graph is slow

Cognition calls target under 300ms end to end, including the graph traversal. That budget matters beyond the API itself: [graph-first resolution](/docs/resolver) calls `assess` on the resolver's hot path before letting a DNS query proceed, under the same strict timeout — and if the graph doesn't answer inside it, the resolver treats that as **no opinion and fails open**, never a resolution outage. A slow cognition call degrades gracefully everywhere it's used; it never becomes the single point of failure for traffic that has nothing to do with it.

> A verdict without evidence is an opinion. Every `assess`, `identify`, and `explain` response here returns the graph rows behind the answer — the same rows you could walk yourself with raw Cypher — so the decision holds up outside the call that made it.

## Next

- [Graph-first resolution](/docs/resolver) — the same graph, called on the DNS hot path to gate what an agent can resolve
- [Control plane](/docs/control-plane) — `whisper.agents`, the sibling verb that provisions and governs identity rather than answering questions about it
