# Record your public agent

This page is for a business that runs an MCP server or an A2A agent for its customers, such as a support or ordering
agent. You add the recorder `@doubleagent-so/observe` to your server, and every call to your agent shows in the portal:
who called, for whom, what they did and how it ended.

Before you start: an Owner or Admin role on a Double Agent account, and a server written in TypeScript or JavaScript
(Node 20+, Cloudflare Workers, Bun or Deno).

## 1. Add the agent and copy its key

1. Open [app.doubleagent.so/agents](https://app.doubleagent.so/agents) and click **Add agent**.
2. Give it a name, for example "Orders MCP". The Agent Card URL is optional and only for A2A agents.
3. Pick the first source: **Live** for production traffic, **Test** for local runs and staging. The portal keeps the two
   apart, and you can add the other one later in Setup.
4. Leave **Capture message content** on if you want to read the messages later, or turn it off to keep metadata only.
   See [What is kept](#what-is-kept).
5. Click **Create agent and key** and copy the ingest key. It is shown once. Store it as the secret or environment
   variable `DOUBLEAGENT_AGENT_KEY`.

The key can only send events for this agent's source. It cannot read anything. If you lose it, rotate it in
`Agents › your agent › Setup › Keys`.

## 2. Install the recorder

```sh
npm install @doubleagent-so/observe
```

It has no runtime dependencies. If you use the official SDKs, `@a2a-js/sdk` 1.3+ and `@modelcontextprotocol/sdk` 1.29+
work with it.

## 3. Wrap your server

Pick the one that matches how your agent is built. All of them use one recorder:

```ts
import { createRecorder } from '@doubleagent-so/observe';

const recorder = createRecorder({ key: process.env.DOUBLEAGENT_AGENT_KEY!, adapter: 'orders-mcp@1.0.0' });
```

| Your server | Use | From |
|---|---|---|
| MCP server on the MCP TypeScript SDK | `instrumentMcpTransport` | `@doubleagent-so/observe/mcp` |
| MCP server you wrote yourself (Streamable HTTP) | `withMcpTelemetry` | `@doubleagent-so/observe/mcp` |
| A2A agent on `@a2a-js/sdk` | `instrumentA2AHandler` and `instrumentTaskStore` | `@doubleagent-so/observe/a2a` |
| A2A agent on Workers, Hono or any fetch-style server | `withA2ATelemetry` | `@doubleagent-so/observe/a2a` |
| Any other protocol | `recorder.startOperation` | `@doubleagent-so/observe` |

**MCP on the SDK.** Wrap the transport before you connect it:

```ts
import { randomUUID } from 'node:crypto';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { instrumentMcpTransport } from '@doubleagent-so/observe/mcp';

const transport = instrumentMcpTransport(new StreamableHTTPServerTransport({ sessionIdGenerator: randomUUID }), {
  recorder,
  role: 'server',
  binding: 'streamable-http',
  issuer: 'https://auth.example.com',
});
await server.connect(transport);
```

**A2A on `@a2a-js/sdk`.** Wrap the request handler and the task store, so task changes made in the background are
recorded too:

```ts
import { DefaultRequestHandler, InMemoryTaskStore } from '@a2a-js/sdk/server';
import { instrumentA2AHandler, instrumentTaskStore } from '@doubleagent-so/observe/a2a';

const taskStore = instrumentTaskStore(new InMemoryTaskStore(), { recorder });
const handler = instrumentA2AHandler(new DefaultRequestHandler(card, taskStore, executor), { recorder, issuer: 'my-idp' });
```

**A2A on Cloudflare Workers.** Build the recorder inside `fetch`, where the secret is available, and set `waitUntil`:

```ts
import { createRecorder } from '@doubleagent-so/observe';
import { withA2ATelemetry } from '@doubleagent-so/observe/a2a';

// app is your Hono app (or any fetch handler) that serves the A2A routes.
let handle;

export default {
  fetch(request, env, ctx) {
    handle ??= withA2ATelemetry(app.fetch, {
      recorder: createRecorder({ key: env.DOUBLEAGENT_AGENT_KEY, adapter: 'orders-a2a@1.0.0', flushIntervalMs: 0 }),
      waitUntil: true,
    });
    return handle(request, env, ctx);
  },
};
```

**Any other protocol.** Start an operation, record its messages, then finish it:

```ts
const op = recorder.startOperation({
  protocol: { name: 'custom:quote-api', version: '1.0', binding: 'http-json' },
  direction: 'inbound',
  method: 'quotes.create',
  kind: 'tool',
  target: 'flight-quote',
});
op.message({ role: 'caller', parts: [{ kind: 'data', json: request }] });
op.finish({ outcome: 'ok' });
```

The recorder never throws into your code and never changes your requests or responses. Your handler's own errors are
recorded and rethrown unchanged.

### On Workers and serverless, flush every request

The recorder sends events in batches every 2 seconds. A serverless runtime can stop that work as soon as the response
goes out, and the events are lost. So:

- Set `flushIntervalMs: 0` on the recorder.
- `withA2ATelemetry` and `withMcpTelemetry` with `waitUntil: true` flush for you.
- With `instrumentMcpTransport`, the A2A SDK wrappers or `startOperation`, call `ctx.waitUntil(recorder.flush())` at
  the end of each request.

On a long-running server, call `await recorder.shutdown()` before the process exits.

## 4. Send a test event

Deploy, then open `Agents › your agent › Setup › Install` and click **Send test event**. It checks the key, ingest,
storage and the dashboard, and shows each step as it passes: Received, Stored, Visible. The test shows in Operations as
a call from "Double Agent test". You can send 10 test events an hour per source.

Then call your agent once yourself and open Operations. If nothing shows, check that the **Live** / **Test** switch at
the top of the screen matches the source you created.

## 5. Tell Double Agent who called

Without any help, a caller is `declared` at best: the MCP `clientInfo` or the name it gave, which proves nothing. Your
server already knows more. Pass what your own auth checked, and the caller moves up the
[verification levels](/docs/callers#verification-levels).

With `@doubleagent-so/observe` 0.2.1 you can pass:

- **The signed-in customer.** Return `{ authenticated: { issuer, subject } }` from `identify(request)` on
  `withMcpTelemetry` or `withA2ATelemetry`. With the SDK wrappers, the MCP SDK's `authInfo` and the A2A SDK's
  `context.user` are read for you. The caller becomes `authenticated`.
- **A signature your server checked.** Add `signature: { scheme: 'web-bot-auth', key_id, verified_by: 'reporter' }`.
  The caller becomes `verified`, reported by your server.

```ts
export default {
  fetch: withMcpTelemetry(handleMcp, {
    recorder,
    waitUntil: true,
    // verifyAccessToken is your own: it checks the token and returns the customer id.
    identify: async (request) => ({ authenticated: { issuer: 'https://auth.example.com', subject: await verifyAccessToken(request) } }),
  }),
};
```

The subject is hashed with your key before it leaves your server. Tokens and `Authorization` headers are never sent.

### Grants, scopes and forwarded signatures (next observe release)

These fields are in the recorder's source but not in 0.2.1. They need the next `@doubleagent-so/observe` release:

| What | How | Gives you |
|---|---|---|
| OAuth claims, including the acting agent (`act`) | `oauthEvidence(claims)` from `identify` | Client id, scopes, and `delegated` when an agent acts for the customer |
| A Web Bot Auth or ERC-8128 signature for Double Agent to check itself | `signedRequestEvidence(request)` | `verified`, checked by Double Agent |
| A PACT, A2A or AP2 grant | `delegation` on the counterparty | `delegated`, with scopes, read or write, and expiry |
| A refusal for missing scopes | `parseInsufficientScope(wwwAuthenticate)`; the fetch wrappers do it for every 403 | A [scope violation](/docs/callers#scope-violations) |
| Read, write or destructive | `access` and `scopeRequired` on `startOperation`; the MCP adapter reads tool annotations | The read / write split in Callers |

The fields and what each level means are on [Callers](/docs/callers#grants).

## What shows where

All of these are under `Agents › your agent`:

| Screen | Answers |
|---|---|
| Overview | How many conversations and calls, how many tasks finished, error rate and latency, against the previous period |
| Conversations | One conversation as a timeline: each call, task state, message and payment, with timings |
| Tasks | Which tasks are open, waiting for input, stalled or done. Set the stall threshold (10 minutes to 4 hours) in Setup › Sources |
| Counterparties | Who calls your agent and who it calls, and how strongly each one is identified |
| Callers | Each caller and the customer it acts for, with its level, grant and scope violations. See [Callers](/docs/callers) |
| Threats | Callers that probe tools, scrape resources, try prompt injection or flood. See [Intent](/docs/intent#agent-endpoints) |
| Revenue | What callers paid and what serving them cost, by counterparty, tool and task |
| Operations | Every call, filtered by outcome, direction or kind |

Revenue shows only to Owners, Admins and roles allowed to see revenue. To record money, see
[Revenue and cost](https://github.com/doubleagent-so/observe#revenue-and-cost) in the recorder's README. x402 payments
are recorded without any code.

## Get alerts when a caller goes outside its grant

In `Agents › your agent › Setup › Webhook`, set a URL and a signing secret. Double Agent sends one
`caller.scope_violation` event per grant and day when a caller acts outside the scopes it was granted. Verify each
delivery as in [Debrief](/docs/debrief). The payload is on [Callers](/docs/callers#scope-violations).

## What is kept

- **Metadata, always:** who called, the method and target, the outcome, timings and task states.
- **Message content, only with capture on.** The recorder sends message text and data by default. Double Agent keeps it
  for 30 days only for sources with **Capture message content** on, and drops it otherwise. Only Owners and Admins can
  read it, and every read is logged. Turn capture off for a source in Setup › Sources, and only Owners can delete what
  was captured. To never send content at all, use `createRecorder({ key, content: false })`.
- **Never sent:** file bytes and file URLs, access tokens, `Authorization`, `Cookie` and `Proxy-Authorization` headers.
- **Not seen:** your agent's internal tool calls, model calls and reasoning. Only what crosses the protocol boundary is
  recorded.

Operations are kept for 30 days. The recorder sends at most 1,000 requests a minute per source, in batches of up to 100
events.

## Next

- [Callers](/docs/callers): read the Callers screen and send grants.
- [Intent](/docs/intent#agent-endpoints): what puts a caller in Threats.
- The full recorder API, outbound calls and limits: the [observe README](https://github.com/doubleagent-so/observe).
