File DA-016 View as Markdown

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

WebsitePublic agent
Reported byThe browser script, your edge adapter and your server through evaluate()The recorder in your agent, @doubleagent-so/observe
WhoThe class and the attribution: agent, operator, controllerThe counterparty: signature key, principal, card URL, MCP clientInfo, declared name or network, plus the OAuth client id and the acting agent
For whomThe customer your server names on evaluate(), hashed, and the grant it acts underThe customer your auth middleware verified, hashed, and the grant
Proven howA verification level and who verified itThe same
Doing whatJourney, actions and outcomes, intentCalls (tool, resource, prompt, message), read or write, outcome, intent

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.

LevelMeansEvidence
unknownNothing but the networkA network hash
declaredA claimA user agent, a declared name, an Agent Card URL, MCP clientInfo, an ERC-8004 declaration
verifiedA proof of the agent or its operatorA Web Bot Auth signature, a published IP list, an ERC-8128 request signature, or a signature your server checked. See Verified agents
authenticatedYour own auth accepted the callerA signed-in customer your server reports on evaluate(); an OAuth or MCP authorization subject your recorder reports
delegatedA grant from a customer to the agentAn 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 asMeans
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.

CodePlain words
wba.expired, wba.bad_signature, wba.key_not_found, wba.malformed, wba.invalidSent 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_listNamed a client that publishes its addresses, from an address outside them
erc8128.invalid, erc8128.unavailableSent an ERC-8128 request signature that did not check out, or could not be checked at the time
signature.body_not_forwardedSigned the request body, which Double Agent never receives, so the signature was not checked
signature.unsupported, signature.unavailableSent 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.malformedPresented 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_failedPresented a grant your own check did not accept
card.bad_signature, card.foreign_key, card.unavailablePublishes 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.budgetSent 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, 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 toDo this
Narrow the listFilter 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 periodPick up to 90 days. Agent rows read raw calls, which are kept 30 days (retained_from says where they start)
See what one caller didOpen the row: it lists the sessions (sites) or calls (agents) behind it, filtered to its customer or grant
Hand it to an auditorExport the row's customer or grant (see 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
protocoloauth, pact, a2a or ap2. pap is reserved: see Personal Agent Protocol
PrincipalThe customer the agent acts for. Always a hash (principal_hash)
actorThe agent acting for the customer (RFC 8693 act.sub, a PACT personal agent). Sent as is
client_idThe OAuth client id, or an MCP Client ID Metadata Document URL. Sent as is
issuerWho issued the grant
scopes, scopes_usedScopes granted; scopes used (from a PACT receipt)
accessread or write
expires_atWhen the grant expires
Grant idAlways a hash (grant_id_hash); a raw id is hashed before anything stores it
verificationYour 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 (or POST /v1/evaluate) after your server has checked the customer's token. It needs the site's secret key.

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. 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 webhook,

so switch on event.type.

  • Public agent: Agents › your agent › Setup › Webhook.
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.
  • 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.

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), 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

StandardWhat Double Agent readsWhat it verifies itselfLevel reachedStatus
OAuth 2.0 token claims, RFC 8693 actIssuer, subject (hashed), client id, act.sub as the actor, scopes, expiry, grant id (hashed), as your server reports themNothing: your server validates the token. Double Agent never sees tokensauthenticated, 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 decidesauthenticated (reported by your server); clientInfo alone is declaredSupported
A2A security schemes, signed Agent CardsGrants your server reports with protocol a2a; Agent Card signaturesCard signatures, with keys only from the card's own host; shown on the caller's directory linkdelegated for a reported grant; a card URL alone is declaredSupported
Web Bot AuthRequest signatures (RFC 9421, tag="web-bot-auth")On websites; on agent endpoints, from the signed parts your agent forwardsverified (Double Agent). Proves the operator, not a customerSupported
ERC-8128Ethereum request signaturesOn 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.0Grants, receipts (scopes used) and, opt-in, delegation and agent tokensReceipts and opt-in tokens, against the issuer's published keys (ES256, RS256). Argument hashes in receipts are not checkeddelegated (Double Agent)Supported
AP2 mandates, ACP ordersReferences on transactions (mandate_ref); AP2 grants your server reportsNothing: references onlydelegated for a reported AP2 grantSupported
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.

Next