# a2a.md — the agent-to-agent gate

> The same answers over other doors:
> [api.md](https://describe.net/api.md) (HTTP + MCP) ·
> [auth.md](https://describe.net/auth.md) (how you pay) ·
> [badge.md](https://describe.net/badge.md) (the embeddable badge).
> This page is only about A2A.

**One endpoint, one method, no account, no payment: `POST https://api.describe.net/a2a`
speaks JSON-RPC 2.0 and answers `message/send` with a wallet's free per-chain
ERC-8004 reputation preview.** A2A ([Agent2Agent](https://a2a-protocol.org)) is
how one agent talks to another without either of them knowing the other's API.
Ours wraps what is already free — it sells nothing and it signs nothing.

**This file is the only documentation this gate will ever have, and that is a
measured fact, not a figure of speech.** Both A2A routes are registered with
`include_in_schema=False`, so they are absent from the OpenAPI description and
will never appear in Swagger or ReDoc:

```bash
curl -s https://api.describe.net/openapi.json | python -c "import sys,json; p=json.load(sys.stdin)['paths']; print(len(p), '/a2a' in p, '/.well-known/agent-card.json' in p)"
# → 18 False False        (measured 2026-08-30)
```

Do not send an agent to `/docs` looking for this endpoint. There is nothing there
to find. Everything below was measured with `curl` against the live API on
**2026-08-30** (build `a6f49ca`).

---

## The two routes

| Route | Method | Price | What it is |
|---|---|---|---|
| `https://api.describe.net/.well-known/agent-card.json` | `GET` | free | The Agent Card: what this agent is, where it lives, what it can do |
| `https://api.describe.net/a2a` | `POST` | free | The JSON-RPC endpoint |

The card — and **only** the card — is also served from the site, byte-identical,
because the site proxies it from the API rather than keeping a copy that could
drift:

```bash
curl -s https://api.describe.net/.well-known/agent-card.json -o a.json
curl -s https://describe.net/.well-known/agent-card.json     -o b.json
cmp a.json b.json && echo identical     # → identical (2026-08-30)
```

The endpoint itself is **not** proxied, and it does not need to be — the card
names it with an absolute URL on `api.describe.net`. Measured, so the mistake is
cheap to avoid:

```
POST https://describe.net/a2a      → 404      ← the site does not serve this
POST https://api.describe.net/a2a  → 200
GET  https://api.describe.net/a2a  → 405 {"detail":"Method Not Allowed"}
```

---

## The Agent Card

```bash
curl -s https://api.describe.net/.well-known/agent-card.json
```

The full object, with the three long `description` strings elided for this page
(the URL returns them; everything else below is verbatim, 2026-08-30):

```json
{
  "protocolVersion": "0.3.0",
  "name": "describe.net reputation agent",
  "version": "2.0.0",
  "description": "…",
  "url": "https://api.describe.net/a2a",
  "preferredTransport": "JSONRPC",
  "supportedInterfaces": [
    { "url": "https://api.describe.net/a2a", "transport": "JSONRPC", "protocolBinding": "JSONRPC" }
  ],
  "provider": { "organization": "Ultravioleta DAO", "url": "https://ultravioletadao.xyz" },
  "documentationUrl": "https://describe.net/skill.md",
  "capabilities": { "streaming": false, "pushNotifications": false, "stateTransitionHistory": false },
  "defaultInputModes": ["text/plain"],
  "defaultOutputModes": ["text/plain"],
  "skills": [
    { "id": "vet-wallet",   "name": "Vet a wallet",  "description": "…",
      "tags": ["reputation", "erc-8004", "vetting", "x402"],
      "examples": ["0x715dc035ffb97dd7bb4095c6670138ba05bb4e6d",
                   "What is the reputation of 0x715dc035ffb97dd7bb4095c6670138ba05bb4e6d?"] },
    { "id": "index-status", "name": "Index status",  "description": "…",
      "tags": ["reputation", "erc-8004", "status"],
      "examples": ["status", "help"] }
  ]
}
```

Four things in there are worth reading slowly:

- **`protocolVersion` is `0.3.0`** — the A2A protocol. **`version` is `2.0.0`** —
  this API's version, the same one every HTTP response carries in `Api-Version`.
  They move independently.
- **`capabilities` is three `false`s.** No streaming, no push notifications, no
  state-transition history. That is not modesty, it is the rule this repository
  works under: a card that declares *less* than the server does is prudent; a card
  that declares *more* is the lie that discovery exists to prevent. Everything the
  card claims, the endpoint does.
- **`supportedInterfaces` sits next to `url` + `preferredTransport` on purpose.**
  New validators want the first form; A2A 0.3 clients read the second. Both name
  the same single endpoint.
- **Two skills, and they are the whole surface**: `vet-wallet` (send a message
  containing an address) and `index-status` (send anything else).

---

## The only method: `message/send`

Everything else — `message/stream`, `tasks/get`, `tasks/cancel`,
`tasks/pushNotificationConfig/*`, `agent/authenticatedExtendedCard` — answers
`-32601`. There is nothing hidden behind an undocumented verb.

### The request

```bash
curl -s -X POST https://api.describe.net/a2a \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":"1","method":"message/send","params":{"message":{"role":"user","messageId":"m1","parts":[{"kind":"text","text":"What is the reputation of 0x715dc035ffb97dd7bb4095c6670138ba05bb4e6d?"}]}}}'
```

No headers beyond `Content-Type`. No `Authorization` — there is none to get, and
no request here is improved by one ([auth.md](https://describe.net/auth.md)
explains why that is a design decision and not an omission).

Tolerances, all measured:

| You send | What happens |
|---|---|
| `parts[].kind` **or** `parts[].type` | both accepted |
| a non-text part | ignored without error (`defaultInputModes` is `text/plain`) |
| no `params` at all | falls through to the index summary — verified with `{"jsonrpc":"2.0","id":null,"method":"message/send"}` |
| `role`, `messageId` | not read; harmless |
| `id` as string, int or `null` | echoed back exactly as sent |

### The response

Always a `message` result — **never a `Task`**:

```json
{"jsonrpc":"2.0","id":"1","result":{"kind":"message","role":"agent","messageId":"fbb5db97-ed9c-4fb4-93fd-6fcf0726c04e","parts":[{"kind":"text","text":"ERC-8004 reputation of 0x715dc035ffb97dd7bb4095c6670138ba05bb4e6d (free preview, per chain):\n- base: 3 identities, score 98.270763, 642 ratings from 467 distinct raters\n\nNever decide with the score alone — a score without its raters is a rumor. The paid breakdown (who wrote every rating, when, in which transaction) costs $0.01 via x402: GET https://api.describe.net/reputation/wallet/0x715dc035ffb97dd7bb4095c6670138ba05bb4e6d — how to pay: https://describe.net/auth.md"}]}}
```

The query takes milliseconds, so returning a `Task` with intermediate states
would be theatre — asynchrony performed rather than needed. `messageId` is a
fresh UUID per answer. Parse the text from `result.parts[0].text`.

Note that A2A serves the score **raw** (`98.270763`). It is the one door that
does not apply the canonical two-decimal display format; if you are rendering it
for a human, apply the rule from [api.md](https://describe.net/api.md) yourself.

---

## What it answers — the three branches

Dispatch is a single pass over your text, in this order.

### 1. An EVM address anywhere in the sentence

The address is matched *inside* the text, so `"What is the reputation of 0x…?"`
works as well as the bare address. Multi-chain wallets get one line per chain:

```
ERC-8004 reputation of 0x000000000000000000000000000000000000dead (free preview, per chain):
- base: 59 identities, score 56.649123, 57 ratings from 8 distinct raters
- celo: 1 identities, score 74.333333, 12 ratings from 2 distinct raters
- bsc: 14 identities, score 100.000000, 1 ratings from 1 distinct raters
- ethereum: 7 identities, score 12.000000, 1 ratings from 1 distinct raters
- polygon: 6 identities, no ratings yet, 0 ratings from 0 distinct raters
```

`no ratings yet` is not a zero. **No evidence and a bad score are different
facts**, and this index refuses to collapse them anywhere, including here.

A wallet with no identity on any indexed chain gets its own sentence — because
that, too, is a different fact:

```
0x000000000000000000000000000000000000beef: no ERC-8004 identity found on any
indexed chain. That is an answer, not an error — 'not registered anywhere' and
'registered but unrated' are different facts. Free re-check any time:
GET https://api.describe.net/wallets/{wallet}/chains
```

### 2. A base58 string

Resolved against the index before answering, because in Solana **a wallet and an
agent id are the same syntax** — a Metaplex Core asset looks exactly like an
account. Sending every base58 string down the wallet path used to answer "no
identity found" about agents this very service publishes for free, which is a
false negative indistinguishable from a fact. So the string is resolved first:

```
A7jngtmJJoRa95pkMZi41Uqn2mRESN5etMiv8WRKYpmQ is an AGENT ID (a Metaplex Core asset), not a wallet:
- solana: agent id A7jngtmJJoRa95pkMZi41Uqn2mRESN5etMiv8WRKYpmQ, owned by unknown, score 25.23301

Paid breakdown (every rating with its transaction, $0.02 via x402): GET https://api.describe.net/reputation/agent/solana/A7jngtmJJoRa95pkMZi41Uqn2mRESN5etMiv8WRKYpmQ — how to pay: https://describe.net/auth.md
```

A base58 string that **is** a wallet gets the per-chain preview of branch 1.

### 3. Anything else — the live index status

```bash
curl -s -X POST https://api.describe.net/a2a -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"message/send","params":{"message":{"parts":[{"kind":"text","text":"status"}]}}}'
```

```
describe.net — the reputation index for humans, AI agents and robots. Live right
now: 470,064 agents and 552,375 ratings across 11 chains.

Send me a wallet address (EVM 0x… or Solana base58) and I answer with its
per-chain ERC-8004 reputation, free. The full breakdown is $0.01 via x402 on the
HTTP API: https://describe.net/api.md
```

Those figures are the ones this endpoint returned at **18:21 UTC on 2026-08-30**.
The shape reproduces; the numbers do not, and should not — the indexer runs
hourly. `GET https://api.describe.net/health` is the authority on the size of the
index, and nothing on this page restates it.

---

## What is free here, and what is not

The whole gate is free — the card, the endpoint, every branch above. It is free
because it serves what is *already* free by product promise: the per-chain
preview (the same answer as `GET /wallets/{wallet}/chains`) and the live size of
the index. One cached run serves everybody.

**The paid decomposition is not served over A2A at all.** Who wrote each rating,
when, in which transaction — that stays on the x402 HTTP routes, and every A2A
answer points at them by URL and price. Payment is the authentication, and
speaking A2A does not change that. There is no x402 challenge over JSON-RPC here,
no `402` in this envelope, and no way to pay through this door. See
[auth.md](https://describe.net/auth.md).

`GET https://api.describe.net/pricing` is the authority and carries the reason
for each price in the response body.

---

## 🔴 Known gap: A2A answers carry no caveats

**As of 2026-08-30, the A2A response is the only one of this service's four doors
that serves a flagged wallet with no flag.** Written here rather than left for
somebody to discover, because a reputation index that hides its own defects has
no standing to decompose anybody else's.

Measured the same minute, same wallet, four doors:

| Door | What it says about `0x…dEaD` |
|---|---|
| `GET /wallets/0x…dEaD/chains` (free) | `caveats: [{"code": "burn-address", …}]` |
| `GET /badge/0x…dEaD.svg` (free) | `aria-label="describe.net: 60.75 · burn address · 10 raters · 2026-08-30"`, value box in muted grey |
| MCP `describe_check_wallet` (free) | `data.caveats[0].code == "burn-address"` |
| **`POST /a2a`** | `- base: 59 identities, score 56.649123, …` — **no mention of anything** |

The subject is one of three well-known burn addresses: nobody controls it,
anybody can rate it, nobody can answer for its reputation. The ratings are real
on-chain facts and are served on purpose — filtering them would be exactly the
verdict this index refuses to issue. What is missing here is the *distinction*,
and only in this door.

**So: do not treat an A2A answer as a complete one.** If you are going to
republish it, or decide money on it, fetch the caveats yourself — free, no
payment, one call:

```bash
curl -s "https://api.describe.net/wallets/0x000000000000000000000000000000000000dEaD/chains"
# → caveats: [{"code":"burn-address", "text":"…"}], global_score: 60.745614   (2026-08-30)
```

Branch on `caveats[].code`, which is stable, not on `caveats[].text`, which is
prose *(and which ships in Spanish today — measured 2026-08-30)*. That array is
the **public subset**: what can be computed from public data without touching the
paid decomposition. The evidence-quality caveats — concentration, single rater,
campaign — read fields only the paid response computes, and shipping half of them
would be worse than shipping none: a list that looks complete and is not.

This entry stays until the endpoint carries the subset itself. When it does, the
correction gets written next to this paragraph rather than replacing it — whoever
remembers the gap deserves to know when it closed.

---

## Errors — all four modes, measured

JSON-RPC answers `200` even when the envelope carries an `error`; the only
non-`200` is a body that is not JSON at all.

| Case | HTTP | Body |
|---|---:|---|
| unknown method | `200` | `{"jsonrpc":"2.0","id":3,"error":{"code":-32601,"message":"Method not found: message/stream. This agent implements message/send only (capabilities.streaming is false — the card does not promise more than the server does)."}}` |
| not a JSON-RPC 2.0 envelope | `200` | `{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Invalid Request: expected a JSON-RPC 2.0 object"}}` |
| unparseable body | `400` | `{"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"Parse error: body is not JSON"}}` |
| `GET` instead of `POST` | `405` | `{"detail":"Method Not Allowed"}` |

If your client raises on `HTTP 200` bodies containing `error`, that is your
client's bug and not ours — the transport succeeded, the call did not.

---

## CORS: open, on purpose

```bash
curl -s -D - -o /dev/null -X OPTIONS https://api.describe.net/a2a \
  -H 'Origin: https://example.com' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: content-type'
```

```
HTTP/1.1 204 No Content
access-control-allow-origin: *
access-control-allow-methods: POST, GET, OPTIONS
access-control-allow-headers: content-type
access-control-max-age: 600
```

Wide-open CORS is scoped to the protocol endpoints (`/a2a` and `/mcp`) and
nothing else, for one reason: **MCP and A2A exist so that other agents can talk
to us**, including agents running in a browser. There is no credential to leak
through a permissive origin, because there is no credential.

---

## What this gate does not do

- **No streaming, no tasks, no push.** All three `capabilities` are `false` and
  the server matches. `message/stream` and `tasks/get` answer `-32601`.
- **No payment.** No `402` travels in this envelope; the paid routes are named in
  every answer and live on the HTTP API.
- **No writes, ever.** This service signs nothing on any chain and holds no keys.
  A2A changes nothing about that.
- **No memory.** There is no session, no conversation state, no `contextId` to
  carry. Each call is answered on its own.
- **No verdict.** The answer is evidence with a price attached to its
  decomposition, never a recommendation. `Never decide with the score alone` is in
  the response text because it is the whole thesis of this index.

---

## Related

- `https://api.describe.net/.well-known/agent-card.json` — the card (also at
  `https://describe.net/.well-known/agent-card.json`, byte-identical)
- `https://describe.net/api.md` — the HTTP and MCP doors: every route and price
- `https://describe.net/auth.md` — how you pay for the decomposition
- `https://describe.net/badge.md` — the badge: the other free, embeddable door
- `https://describe.net/skill.md` — the card's `documentationUrl`: the whole
  service as an installable playbook
- `https://api.describe.net/wallets/{wallet}/chains` — the same preview over
  plain HTTP, **plus the `caveats[]` this gate does not carry**
- `https://api.describe.net/health` — the authority on the size and freshness of
  the index
