# Agent access

Double Agent's account agent lives on `https://app.doubleagent.so`. An AI agent registers itself, its human owner approves it by email, and it then reads the sites and managed agents of its accounts with its own `daa_` token.

## Endpoints

| Endpoint | What |
|---|---|
| `GET https://app.doubleagent.so/.well-known/agent-card.json` | The A2A 1.0 Agent Card, with a 0.3 interface, signed with ES256 |
| `GET https://app.doubleagent.so/.well-known/jwks.json` | The public key that verifies the card and every push |
| `POST https://app.doubleagent.so/a2a` | A2A JSON-RPC, versions 1.0 and 0.3. Send `A2A-Version`. |
| `POST https://app.doubleagent.so/mcp` | MCP over Streamable HTTP with JSON responses, versions `2025-11-25`, `2025-06-18` and `2025-03-26` |

Both offer the same tools. Request bodies are at most 64 KiB.

## Register

1. Call `register_agent` with `name` (1–100 characters) and, optionally, `owner_email`, `requested_role` (`viewer` or `admin`, default `viewer`; the owner decides), `card_url`, `mcp_url` and `token_name`. Without `pow` it fails with `pow_required`, and `details` holds `{ challenge, difficulty, format, expires_at }`.
2. Find a `solution` of 1–64 characters of `[A-Za-z0-9_-]` so that the SHA-256 of the UTF-8 string `da-agents|<challenge>|<solution>` has `difficulty` leading zero bits (18 by default). The challenge expires after 10 minutes and works once; reusing it gives `invalid_pow`.
3. Call `register_agent` again with `pow: { challenge, solution }`. The answer has `agent_id`, `registration_id` (`arg_…`), `status`, `token` (`daa_…`, shown once), `token_id`, `owner_email_masked`, `expires_at`, `next[]` and `owner_email_sent`.
4. Without an owner email the status is `needs_owner_email`. Call `set_owner_email` (at most 3 changes per registration; each change voids the previous approval link; `owner_email_sent: false` means fix the address or retry), or on A2A reply on the registration task with the data part `{ "owner_email": "…" }`.
5. The owner gets a fixed email with no text from your agent and signs in with that address. They see the name, the unverified URLs, the request's country and the requested role, pick an account they own or administer (or a new one) and a role (`viewer` or `admin`, never `owner`), and approve or decline.
6. Learn the outcome by polling `get_registration` or `whoami` no faster than once a minute, with A2A `GetTask` (the task id is the registration id), or by push ([Push notifications](/docs/agent-push)). Statuses: `needs_owner_email`, `pending`, `approved`, `declined`, `expired`. A2A task states: `input-required` until approved, then `completed`; `rejected` when declined; `canceled` when expired.
7. A registration expires after 7 days without approval, and its token is revoked.

Nothing is fetched at registration. `card_url` and `mcp_url` must be public HTTPS on the default port with a DNS name (no IP address, no credentials). The owner sees them as unverified until a proof passes; see [Agent verification](/docs/agent-verification).

## Roles

An agent is a `viewer` or an `admin`, never `owner`. Whatever the role, an agent never holds a team or account permission: it cannot see or change members, invitations, ownership or account settings. Person-only routes (agent approval, device approval, invitations) answer `403` with `human_only`.

A viewer reads sites, analytics and managed agents. An admin also manages sites, domains, keys and managed agents. Agent actions appear in the account's audit log as the agent.

People list, edit and remove agents and revoke their tokens on the portal's Team page. Agents cannot.

## Tokens

A pending token works only for `set_owner_email`, `get_registration` and `whoami`. Every other tool answers `agent_pending` until the owner approves; then the same token works.

Tokens do not expire on a clock. An agent has at most 10 active tokens:

- `create_token` makes another named token.
- `rotate_token` replaces a token; the old value stops working at once.
- `revoke_token` revokes another token. A token cannot revoke itself.
- `list_tokens` lists tokens, never their values.

New values are shown once. A revoked token answers `401`.

An approved token also authenticates the HTTP API at `https://api.doubleagent.so` as `Authorization: Bearer daa_…`, within the agent's role.

Store tokens in a file with mode 0600 or in a secret manager. Never put a token in a repository, a prompt, a log, a JSON-RPC `id` or MCP `clientInfo`.

## Tools

Send the token as `Authorization: Bearer daa_…`. Auth `none` needs no token, `pending` takes any valid token including one awaiting approval, and `token` needs an approved token.

| Tool | Auth | What it does |
|---|---|---|
| `register_agent` | none | Starts registration. Without `pow` it returns a proof-of-work challenge. Called again with `{ challenge, solution }` it returns a pending token (shown once) and a registration that waits for the owner. |
| `set_owner_email` | pending | Sets or changes the owner's email (at most 3 changes). |
| `get_registration` | pending | Status: `needs_owner_email`, `pending`, `approved`, `declined` or `expired`. |
| `whoami` | pending | The agent's id, name, URLs, registration, accounts, roles and verification labels. |
| `list_tokens` | token | Token ids, names, status, created and last used. Never the values. |
| `create_token` | token | Another named token (at most 10 active). The value is shown once. |
| `rotate_token` | token | Revokes a token and returns a new one with the same name. |
| `revoke_token` | token | Revokes another token. The token making the call cannot revoke itself. |
| `update_profile` | token | Changes name, `card_url` or `mcp_url`. Changing a URL removes its verification at once. |
| `start_verification` | token | Opens a challenge (72 hours) proving control of the A2A endpoint, MCP server, card key, MCP Registry key, origin or domain. Takes `method` (`a2a_callback`, `mcp_callback`, `card_key`, `mcp_registry_key`, `well_known`, `dns`) and optional `target`. The secret and the steps are in this answer only. |
| `check_verification` | token | Runs the check for a started challenge (10 per hour). Takes `proof_id`; `jws` for `card_key`, `signature` for `mcp_registry_key`. A pass adds a verification label. |
| `list_verifications` | token | This agent's challenges and labels, newest first. |
| `list_sites` | token | Sites in the agent's accounts, within its role. |
| `site_summary` | token | Humans, bots and agents for one site (default: last 7 days). |
| `list_managed_agents` | token | Agents an account tracks with Double Agent observability. |
| `agent_summary` | token | Operations, conversations, errors and latency for one tracked agent. |

For A2A, send a data part `{ "skill": "<tool>", …arguments }`. For MCP, call `tools/call`.

## Conversations

Each protocol's own conversation id groups your calls into one conversation.

- A2A: The portal agent tells you: Keep the `contextId` from my first answer and send it on every later message; to answer a task, send its `taskId` with the same `contextId` (or none).
- MCP: Keep the `Mcp-Session-Id` from `initialize` and send it on every request; on 404, initialize again. An `mcs_…` session lasts 24 hours; after that the server answers 404.

Without a `contextId`, the agent mints one (`ctx_…`). A `contextId` of your own must be 1–128 of `[A-Za-z0-9_.:-]`, but prefer the `ctx_…` the agent mints. A reply whose `contextId` differs from its task's is rejected. A conversation id grants no access.

## Errors

| Code | What to do |
| --- | --- |
| `pow_required` | Solve the challenge in `details` and call `register_agent` again with `pow`. |
| `invalid_pow` | The solution is wrong, expired or already used. Call `register_agent` without `pow` for a new challenge. |
| `needs_owner_email` | Give the owner's email with `set_owner_email`, or reply on the registration task. |
| `agent_pending` | Wait for the owner to approve. Poll `get_registration` no faster than once a minute. |
| `unauthorized` | Send `Authorization: Bearer daa_…`. If the token was revoked, declined or expired, tell your human; do not register again. |
| `forbidden` | Your role does not allow it. Ask your human. |
| `rate_limited` | Wait `details.retry_after` seconds once, then stop and report. |
| `invalid_argument` | Fix the argument named in `details.field`. On A2A it arrives as JSON-RPC `-32602`, and an unknown skill lists the valid ones in `details.tools`. On MCP it is a tool result with `isError: true`, and an unknown tool is JSON-RPC `-32602` without `details`. |
| `not_found` | The id does not exist or is not yours. |
| `conflict` | The state changed, for example the owner already decided or the owner email changed 3 times. Read it again with `whoami`. |
| `unavailable` | Double Agent could not be reached. Try again later, once. |
| `internal` | Something failed on our side. Try again later, once, then report it. |

HTTP API errors are `{ "error": { "code", "message" } }`, and every `429` has `Retry-After`.

## Limits

- Registration: 5 solved `register_agent` calls per hour per IP address.
- Proof-of-work challenges: 30 per minute per IP address.
- Approval emails: 3 per day per owner address.
- Calls with one token: 120 per minute, tools and HTTP API together.
- Push config handshakes: 5 per hour per registration and 30 per hour per webhook host.
- Proofs started: 20 per hour per agent.
- Proof checks: 10 per hour per proof and 30 per hour per target host.
- Directory API: 60 requests per minute per IP address; reviews 30, check 10 and submissions 10 per minute.
- Observe event ingest (`POST https://api.doubleagent.so/v1/agent-events`): 1,000 requests per minute per agent source.

## Related

- [Push notifications](/docs/agent-push)
- [Agent Cards](/docs/agent-cards)
- [Agent verification](/docs/agent-verification)
- [REST API](/docs/rest)

AI agents can install everything here as a skill:

```sh
npx skills add doubleagent-so/skills
```
