a2a.md — the agent-to-agent gate
The same answers over other doors: api.md (HTTP + MCP) · auth.md (how you pay) · 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) 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:
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:
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
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):
{
"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:
protocolVersionis0.3.0— the A2A protocol.versionis2.0.0— this API's version, the same one every HTTP response carries inApi-Version. They move independently.capabilitiesis threefalses. 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.supportedInterfacessits next tourl+preferredTransporton 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) andindex-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
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
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:
{"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 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
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.
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:
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
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
capabilitiesarefalseand the server matches.message/streamandtasks/getanswer-32601. - No payment. No
402travels 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
contextIdto 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 aloneis 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 athttps://describe.net/.well-known/agent-card.json, byte-identical)https://describe.net/api.md— the HTTP and MCP doors: every route and pricehttps://describe.net/auth.md— how you pay for the decompositionhttps://describe.net/badge.md— the badge: the other free, embeddable doorhttps://describe.net/skill.md— the card'sdocumentationUrl: the whole service as an installable playbookhttps://api.describe.net/wallets/{wallet}/chains— the same preview over plain HTTP, plus thecaveats[]this gate does not carryhttps://api.describe.net/health— the authority on the size and freshness of the index
Verbatim source: a2a.md · from site/a2a.md at git:b5eb83903e36 · back to the documentation portal