# Callers

Callers tells you, for everyone who uses your website or your public agent (MCP server or A2A agent), who acted, for
whom, how that was proven and what they did. This page explains what you see on the Callers screen and how to send
Double Agent the facts it needs: the customer, the grant and the scopes. Double Agent only verifies and labels. Your
own login, OAuth server or agent platform still decides who gets in.

## An example

Asha is signed in to your store. She gives her shopping agent read-only access to her orders through OAuth, with the
scope `orders:read`. The agent calls your order-status API. Your server checks the token, sees the `act` claim naming
the agent, and reports it to Double Agent with `evaluate()`. In Callers you see one row: the agent, acting for one
customer (shown as a hash, never her id), level `delegated`, grant `oauth`, `read`, scopes `orders:read`.

Later the agent tries to cancel an order, which needs `orders:write`. That action was outside the grant. The row now shows
a scope violation, and your webhook gets one `caller.scope_violation` event for that grant that day. Whether the cancel
went through was your server's call, and the event says which (`served` or `denied`).

## Two doors

| | Website | Public agent |
|---|---|---|
| Reported by | The browser script, your [edge adapter](/docs/intent#edge-adapter) and your server through `evaluate()` | The recorder in your agent, [`@doubleagent-so/observe`](/docs/agent-analytics) |
| Who | The class and the [attribution](/docs/rest#visit-attribution): agent, operator, controller | The counterparty: signature key, principal, card URL, MCP `clientInfo`, declared name or network, plus the OAuth client id and the acting agent |
| For whom | The customer your server names on `evaluate()`, hashed, and the grant it acts under | The customer your auth middleware verified, hashed, and the grant |
| Proven how | A [verification level](#verification-levels) and who verified it | The same |
| Doing what | Journey, actions and outcomes, [intent](/docs/intent) | Calls (tool, resource, prompt, message), read or write, outcome, [intent](/docs/intent#agent-endpoints) |

## Verification levels

Each caller gets the highest level its evidence supports. Levels are separate facts. A signature proves the operator,
not the customer. A login proves an account, not that the agent may write.

| Level | Means | Evidence |
|---|---|---|
| `unknown` | Nothing but the network | A network hash |
| `declared` | A claim | A user agent, a declared name, an Agent Card URL, MCP `clientInfo`, an ERC-8004 declaration |
| `verified` | A proof of the agent or its operator | A Web Bot Auth signature, a published IP list, an ERC-8128 request signature, or a signature your server checked. See [Verified agents](/docs/cover) |
| `authenticated` | Your own auth accepted the caller | A signed-in customer your server reports on `evaluate()`; an OAuth or MCP authorization subject your recorder reports |
| `delegated` | A grant from a customer to the agent | An OAuth token with an `act` claim, a PACT grant, an A2A or AP2 grant your server reports |

Next to the level, Double Agent keeps who verified it:

| Shown as | Means |
|---|---|
| Verified by Double Agent (`double_agent`) | Double Agent checked the proof itself: Web Bot Auth, IP lists, ERC-8128, PACT receipts, and signatures your agent forwarded |
| Reported by your server (`reporter`) | Your server says it checked: a login, an OAuth subject, a signature your host verified, a grant your server accepted |

### Failures

A proof that did not check out is kept as a code next to the level, never dropped. A failure does not lower the level
by itself, but a grant that failed or expired gives no `delegated`. Up to 8 codes are kept per caller. A failure can be
a clock or configuration problem, so the words describe what was sent and never accuse.

| Code | Plain words |
|---|---|
| `wba.expired`, `wba.bad_signature`, `wba.key_not_found`, `wba.malformed`, `wba.invalid` | Sent a Web Bot Auth signature that had expired, did not match its published key, named a key missing from its key directory, could not be read, or did not check out |
| `ip.outside_published_list` | Named a client that publishes its addresses, from an address outside them |
| `erc8128.invalid`, `erc8128.unavailable` | Sent an ERC-8128 request signature that did not check out, or could not be checked at the time |
| `signature.body_not_forwarded` | Signed the request body, which Double Agent never receives, so the signature was not checked |
| `signature.unsupported`, `signature.unavailable` | Sent a request signature in a form Double Agent does not check, or that could not be checked at the time |
| `grant.expired`, `grant.bad_signature`, `grant.keys_unavailable`, `grant.claims_mismatch`, `grant.unsupported_alg`, `grant.malformed` | Presented a grant that had expired, whose signature did not match the issuer's keys, whose issuer's keys could not be fetched, that was issued for another agent, issuer or time, signed with an algorithm Double Agent does not accept, or that could not be read |
| `grant.reported_failed` | Presented a grant your own check did not accept |
| `card.bad_signature`, `card.foreign_key`, `card.unavailable` | Publishes an Agent Card whose signature did not match its key, signed with a key that is not on the card's own host, or that could not be checked at the time |
| `verification.budget` | Sent more proofs at once than Double Agent checks in one batch (16); the rest were not checked |

## The Callers screen

There is one Callers screen per site (`Sites › your site › Callers`) and one per public agent
(`Agents › your agent › Callers`). Each row is one agent acting for one customer:

- **Websites:** the agent (its catalog name or declared ERC-8004 name), else its operator, else the class, for each
  customer hash. Rows come from the site's sessions.
- **Public agents:** the counterparty, for each customer hash, from the calls your agent received. Rows also show the
  OAuth client ids and acting agents seen, the read / write / destructive split and, for a caller listed in the
  [agent directory](/agents/), its directory link and signed-card status.

Every row shows the level and who verified it, failures in plain words, the grant (protocol, scopes, read or write,
status), scope violations, activity, intent, and when it was first and last seen.

| You want to | Do this |
|---|---|
| Narrow the list | Filter by level, customer hash, grant id hash or intent. On sites also by actor kind and class; on agents also by counterparty kind, access, in grant, or a name search |
| See a longer period | Pick up to 90 days. Agent rows read raw calls, which are kept 30 days (`retained_from` says where they start) |
| See what one caller did | Open the row: it lists the sessions (sites) or calls (agents) behind it, filtered to its customer or grant |
| Hand it to an auditor | Export the row's customer or grant (see [Audit search and export](#audit-search-and-export)) |

At most 500 rows are shown; `truncated` is set when there are more. For a public agent, **Threats** lists the callers
whose intent is hostile, with the tools and resources they went for most.

## Grants

A grant is the permission a customer gave the agent acting for them. Both doors use one shape:

| Field | |
|---|---|
| `protocol` | `oauth`, `pact`, `a2a` or `ap2`. `pap` is reserved: see [Personal Agent Protocol](#personal-agent-protocol) |
| Principal | The customer the agent acts for. Always a hash (`principal_hash`) |
| `actor` | The agent acting for the customer (RFC 8693 `act.sub`, a PACT personal agent). Sent as is |
| `client_id` | The OAuth client id, or an MCP Client ID Metadata Document URL. Sent as is |
| `issuer` | Who issued the grant |
| `scopes`, `scopes_used` | Scopes granted; scopes used (from a PACT receipt) |
| `access` | `read` or `write` |
| `expires_at` | When the grant expires |
| Grant id | Always a hash (`grant_id_hash`); a raw id is hashed before anything stores it |
| `verification` | Your server's own check, `verified` or `failed` |

The grant's **status** is the worst that applies: `failed` (a check failed), `expired`, `verified` (Double Agent or your
server checked it), else `unverified`.

**In grant** compares the scopes an action needs (`scope_required`) with the scopes granted. It is `yes` when every one
is granted; `no` when one is missing, or the grant expired or failed; `unknown` without required scopes, without a
grant, or when the grant names no scopes.

### Send them from your website's server

Call `evaluate()` from [`@doubleagent-so/node`](/docs/server) (or [`POST /v1/evaluate`](/docs/rest#caller-identity))
after your server has checked the customer's token. It needs the site's secret key.

```ts
import { createDoubleAgent } from '@doubleagent-so/node';

const doubleagent = createDoubleAgent({
  secretKey: process.env.DA_SECRET_KEY,
  subjectKey: process.env.DA_SUBJECT_KEY, // optional: the same key as the recorder's subjectKey
});

const { caller } = await doubleagent.evaluate({
  sid, subject: customerId, observationId, action: 'cancel_order', outcome: 'ok', request,
  actorKind: 'agent', // 'self' when the customer acts themself
  scopeRequired: ['orders:write'],
  delegation: {
    protocol: 'oauth',
    clientId: 'client_9', actor: 'agent_7', scopes: ['orders:read'], access: 'read',
    expiresAt: new Date(claims.exp * 1000), grantId: claims.jti, verification: { status: 'verified' },
  },
});
// caller.level is 'delegated', caller.in_grant is 'no', and the site's webhook gets a scope violation.
```

`grantId` is an opaque id that Double Agent hashes. Never send a token.

### Send them from your public agent

Pass what your auth middleware checked to the recorder. The steps are on
[Record your public agent](/docs/agent-analytics#5-tell-double-agent-who-called). The signed-in customer works with
`@doubleagent-so/observe` 0.2.1. Grants, scopes and forwarded signatures need the next observe release.

## Read, write and destructive

Each call to a public agent can carry `access`: `read`, `write` or `destructive`. The MCP adapter reads it from the
tool's annotations in `tools/list`: `readOnlyHint: true` is `read`; otherwise `destructiveHint` (true unless the tool
says false) is `destructive`, and `destructiveHint: false` is `write`. A tool without annotations has no access. You can
set `access` yourself on any call. Like grants, this needs the next observe release.

## Scope violations

A scope violation is an action whose required scopes are known and not all granted (`in_grant: no`), or one your server
refused with `403 insufficient_scope`. It shows in the Callers row, and sends one `caller.scope_violation` webhook per
site or agent, grant (else customer, else caller) and UTC day.

Set the webhook here:

- **Site:** `Sites › your site › Settings › Debrief webhook`. It is the same URL as the [Debrief](/docs/debrief) webhook,
  so switch on `event.type`.
- **Public agent:** `Agents › your agent › Setup › Webhook`.

```http
POST /your/webhook
DoubleAgent-Signature: t=1791417600,v1=5f8a…

{ "id": "evt_scope_st_abc_2026-10-08_9a3f…", "type": "caller.scope_violation", "created": 1791417600,
  "data": { "surface": "site", "site": "st_abc", "sid": "s_1", "principal_hash": "3f…", "grant_id_hash": "9a3f…",
            "protocol": "oauth", "scopes_required": ["orders:write"], "scopes_granted": ["orders:read"],
            "access": "read", "kind": "served", "at": "2026-10-08T10:00:00.000Z" } }
```

- For an agent, `data` has `surface: "agent"`, `agent_id`, `operation_id` and `counterparty_id` instead of `site` and
  `sid`. Only inbound, non-test calls alert.
- `kind` is `served` (the action ran outside the grant) or `denied` (your server refused it).
- Verify the signature as in [Debrief](/docs/debrief).
- The `id` is the same all day for one grant, so you can deduplicate on it. A failed delivery is not retried.

## Agent-endpoint intent

Callers of a public agent also get an intent (probing tools, scraping resources, prompt injection, failed-auth bursts,
floods) from what they did in the range. The evidence and thresholds are on [Intent](/docs/intent#agent-endpoints).

## Audit search and export

Find everything one customer or one grant did, across both doors: agent calls and website `evaluate()` actions, newest
first, up to 90 days at a time. Search by a customer hash or a grant id hash
([`GET /v1/audit/search`](/docs/rest#caller-identity)), or export up to 50,000 rows as NDJSON or CSV
(`GET /v1/audit/export`). You need **View audit logs** on the account, and read access to each door. Every export is
recorded in the account's audit log.

**Audit retention** is an account setting from 1 to 90 days. Each store keeps the smaller of the setting and its own
retention: 30 days for agent calls, 90 for website sessions and events. Only shortening changes anything. It applies to
agent events, captured content and calls, and to website sessions and events that carry a customer or grant.

## Privacy

- **Hashed with your keys.** Customers and grant ids are only ever stored as hashes. The recorder hashes subjects with
  your subject key before they leave your server. On the website, Double Agent hashes the `evaluate()` subject with your
  account's key, unless you send `principal_hash` yourself. Give `@doubleagent-so/node` the same subject key and issuer
  as the recorder, and one customer links across both doors.
- **Never stored:** raw tokens, `Authorization` headers, cookies, raw grant ids, raw subjects. Signed request headers
  your agent forwards are checked when they arrive and dropped; they are never queued or stored.
- **PACT bearer tokens** (`pact-delegation`, `pact-agent`) are accepted only when you pass one explicitly as a proof.
  Double Agent verifies it in memory and drops it. Prefer a PACT receipt, which is a record, not a credential.
- **Sent as is:** OAuth client ids, actors and scopes, including MCP `authInfo` client ids and scopes. They name
  software and permissions, so don't put personal data in them.
- **Content stays home by default.** Prompt-injection checks run only where content capture is on, and store only the
  code.
- **Per account.** Customers and grants are linked within your account only, never across Double Agent customers.

## Supported standards

| Standard | What Double Agent reads | What it verifies itself | Level reached | Status |
|---|---|---|---|---|
| OAuth 2.0 token claims, RFC 8693 `act` | Issuer, subject (hashed), client id, `act.sub` as the actor, scopes, expiry, grant id (hashed), as your server reports them | Nothing: your server validates the token. Double Agent never sees tokens | `authenticated`, or `delegated` with an `act` claim or a grant (reported by your server) | Supported |
| MCP authorization (2026-07-28) | Client id, scopes, the required scopes of a `403 insufficient_scope`, and each request's `clientInfo` (a declaration only) | Nothing: your MCP server's auth decides | `authenticated` (reported by your server); `clientInfo` alone is `declared` | Supported |
| A2A security schemes, signed Agent Cards | Grants your server reports with protocol `a2a`; Agent Card signatures | Card signatures, with keys only from the card's own host; shown on the caller's directory link | `delegated` for a reported grant; a card URL alone is `declared` | Supported |
| Web Bot Auth | Request signatures (RFC 9421, `tag="web-bot-auth"`) | On websites; on agent endpoints, from the signed parts your agent forwards | `verified` (Double Agent). Proves the operator, not a customer | Supported |
| ERC-8128 | Ethereum request signatures | On websites and, from forwarded parts, on agent endpoints. A signature that covers a body digest cannot be checked from forwarded headers (`signature.body_not_forwarded`) | `verified` (Double Agent) | Supported |
| PACT 1.0 | Grants, receipts (scopes used) and, opt-in, delegation and agent tokens | Receipts and opt-in tokens, against the issuer's published keys (ES256, RS256). Argument hashes in receipts are not checked | `delegated` (Double Agent) | Supported |
| AP2 mandates, ACP orders | References on transactions (`mandate_ref`); AP2 grants your server reports | Nothing: references only | `delegated` for a reported AP2 grant | Supported |
| Personal Agent Protocol | – | – | – | **PAP ready on day one** |

On public agents, the recorder fields for client ids, scopes, grants, forwarded signatures and mandate references need
the next `@doubleagent-so/observe` release.

### Personal Agent Protocol

**PAP ready on day one.** No PAP specification has been published yet. The grant shape, the verification levels, the
reporting paths (your server and your agent's recorder) and the portal screens are in place, so PAP support is a parser
added when v0.1 is published. Until then a grant with protocol `pap` is refused. More on
[Personal Agent Protocol](/docs/personal-agent-protocol).

## Next

- [Record your public agent](/docs/agent-analytics): report callers from your MCP server or A2A agent.
- [`@doubleagent-so/node`](/docs/server): `evaluate()` and token checks on your website's server.
- [Debrief](/docs/debrief): verify the webhook signature.
