describe — API and MCP, for agents
Markdown twin of https://describe.net/api.html. Same content, no chrome. Canonical HTML: https://describe.net/api.html
This same guide is also published at https://docs.describe.net/guides/api.md (rendered beside it as
guides/api.html). It is not a second version: the documentation hub COPIES this file byte for byte — a test in the repo compares the bytes, so a copy that stops being verbatim turns the suite red. If the two ever do differ, the hub is the one that is behind, and https://docs.describe.net/build.json says by how much: itsguides_sourcefield names the commit its copy came from. The full documentation index — every guide, the frozen schema, Swagger and ReDoc — is https://docs.describe.net/.
describe is an ERC-8004 reputation index over every chain we read. It does not sell you a score — the chain is public and you can average it yourself. It sells you what a score is made of: how many distinct counterparties wrote it, how many times each one spoke, in which facet, dated on-chain, each with its transaction.
The offer, in four lines
| What you buy | Composition. Distinct raters, concentration, self-rating gap, per-facet scores, per-chain split, activity — in one response, with the policy_version that produced it. |
| What it costs | 0.01 USDC per wallet lookup. The full vet (lookup + history + every rating with its transaction) is 0.06. A citable, replayable receipt adds 0.05. Reconnaissance is free. |
| How you pay | x402. No account, no API key, no signup. The first call answers 402 with the challenge; you sign an EIP-3009 USDC authorization and repeat the same call. The facilitator pays the gas. Suggested wallet: PayBox — install it or check it is in use, then its pay_x402 signs the challenge (paybox.md). |
| Why it is worth it | The full vet is 0.12 % of a 50 USD bounty. Doing it yourself is hours of RPC across every chain, and it still leaves you resolving ownership and inventing a scoring policy. |
Prices in this file are a snapshot. The authority is GET /pricing, and above
that, the 402 challenge of your own call — the price can depend on the
parameters, which a per-route table cannot express.
Start here: the minimum
base_url https://api.describe.net
openapi https://api.describe.net/openapi.json
auth none — no keys, no registration, no OAuth
payment x402 · EIP-3009 · USDC · the facilitator pays the gas
wallet (suggested) PayBox · https://describe.net/paybox.md
treasury (pinned) 0xe4dc963c56979E0260fc146b87eE24F18220e545
curl https://api.describe.net/health
curl https://api.describe.net/pricing
curl https://api.describe.net/leaderboard
curl https://api.describe.net/stats/facets
curl https://api.describe.net/reputation/wallet/0x715dc035ffb97dd7bb4095c6670138ba05bb4e6d
The first four are free and answer 200. The fifth answers 402 with its
challenge — amount, token, recipient and supported chains. That is the intended
path, not an error.
Free
GET /search/{query}— the one box: paste what you have. An EVM address, a Solana base58 id, or an ERC-8004 agent id. Answersinterpretations(what the shape allows) andmatches(what the index found), one row per chain, each with the wallet that owns it there, itsagent_ids,final_scoreand thedetail_routeto pay for its decomposition. It never answers in the singular, and that is not caution: the same numeric id exists on up to ten chains at once and — measured 2026-08-23 over 26,736 ids present on two or more chains — not one has the same owner on all of them. In Solana a wallet and a Metaplex Core asset are the same syntax, so a base58 string gets both readings, labelled, without the route picking one. Exact match only: no prefix, no wildcard, no listing, no paging, no name search. A well-formed query the index has never seen is200withmatches: [], never404; only a query with none of the accepted shapes is422withaccepted_shapes.GET /names/resolve?name={name}andGET /names/reverse?address={address}— on-chain names, both ways. ENS (names, subnames, CCIP-Read), Basenames (*.base.eth), DNS names imported into ENS, Unstoppable and Avvy (.avax), read on-chain by describe.net with theuvd-describe-sdkresolver;.solis detected and answeredunsupported_systemfor now. Always200, with the outcome inside:errorisnullor one ofnot_found,invalid_name,reverse_mismatch,expired,unsupported_system,rpc_unavailable, andtriednames the systems that were asked. A reverse name is returned only if it resolves back to the same address. The chain is never yours to pick: each route takes its one parameter, and anything else is422. It never reads the reputation index — resolve here, then look the address up. The two routes share one small, GLOBAL rate-limit budget, not a per-client one: when it is spent the gateway answers429to every caller until it refills, and answers cached by CloudFront do not consume it (readRateLimit-Policyon their responses)GET /wallets/{wallet}/chains— per-chain ERC-8004 identities and reputation for one wallet:agent_ids,final_score,total_reviews,distinct_raters, pluscaveats[](see "Caveats"). The preview before paying for/reputation/wallet/{wallet}— if nothing is here, there is nothing to buy.GET /reputation/by-resource?url={resource_url}— who collects at an x402 resource, asked by its URL — never an address back. describe.net reads thepayToof eachaccepts[]entry from the facilitator's public bazaar (GET https://facilitator.ultravioletadao.xyz/discovery/resources) and answers one row per collector: its CAIP-2network,identity_status(erc8004_identity·no_erc8004_identity·not_indexed),has_identity,identity_count, theagentto link by id ({network, agent_id}, forGET /reputation/agent/{network}/{agent_id}) andreviews— computed from the same facts as/wallets/{wallet}/exists(identity, ratings, distinct raters and the date of the last rating that counts).no_erc8004_identityis not bad reputation: the collector's address owns no identity on a chain the index reads, which is the common case for bazaar endpoints (ownership only — an agent paid at a wallet that is not its owner reads as no identity). The URL is a lookup key, never fetched, and never echoed back:resources[i]answers the i-thurl. Only the bazaar row stored under exactly that URL counts (scheme and host case-insensitive, default port and your fragment ignored; if that row does not exist, the URL exactly as written).collectors_checkablesays whether afoundlisting's collectors could have been compared with the live endpoint at all: only anhttplisting that isalive— necessary, not sufficient (the facilitator skips the comparison when the 402 is unreadable, and never makes it for an MCP listing);falsemeans they are only what whoever registered the URL declared. Per URLstatusisfound,not_in_bazaar(every candidate came back in one read from the bazaar replica that answered),lookup_truncated(more candidates than one read holds and the URL was not among those read — not proof of absence),quarantined(the bazaar quarantined the listing; its collectors are not described) orbazaar_unavailable(ask again; never cached). Repeaturlup to 10 times for a batch — and do: the route has one small, GLOBAL rate-limit budget on each of its route keys (GETandHEAD, with and without/v1), not a per-client one, because it waits on the facilitator on the same capacity the paid routes use; when it is spent the gateway answers429to every caller until it refills (readRateLimit-Policyon its responses). Built for directories and routers that must never hold a payment address.GET /badge/{wallet}.svg— one embeddable SVG per wallet: the global score, how many distinct raters it is made of, and the date of the data, inside the pixel. An<img>, not a widget: it cannot script, cannot track and cannot be clickjacked. Cached an hour at the edge. Full guide: https://describe.net/badge.mdGET /health— liveness plus per-chain index coverageGET /pricing— every tier, its price, and the reason for itGET /manifesto— the manifesto as data: principles wired to the code that enforces themGET /chainsandGET /chains/{network}— per-chain aggregates: which chain to look at before paying to look at a subjectGET /feed— the newest ratings across every chain, newest firstGET /leaderboard— the first page (100 rows). Takes no parameters: a query param answers 422 pointing atGET /leaderboard/page, the metered paging routeGET /stats/facets— the index-wide facet tableGET /stats/types— breakdown by declared typeGET /stats/issuers— who wrote the index. Ratings grouped by the host of theirfeedbackURI, withdistinct_agentsanddistinct_ratersper issuer, plus ano_hostrow for the ratings that carry no URI. Free on purpose, and the reason is uncomfortable: this is the route somebody audits us with — how much of the index was written by products of our own house. Advisory, and it never moves a score.GET /categories— the directory's catalog. The few categories that can already be pointed at in the index, each with a stableslug(at most 15 bytes), its label and definition in English and Spanish, thetag_suffixesit counts and the measurement that admitted it — plus the offer measured live:providers_at_least, a lower bound of distinct subjects rated as executors there (null means no offer, never zero), andcoverage, how much of the index carries a category at all. Alphabetical by slug, because the order of a directory is never sold. No people, no geography, no score per category;catalog_versionversions the data andGET /healthsays which copy is in force.GET /openapi.json— the schema; Swagger UI at https://api.describe.net/docs, ReDoc at https://api.describe.net/redocPOST /a2a— the same free preview over A2A JSON-RPC (message/send), for an agent that speaks A2A rather than HTTP. Agent card at/.well-known/agent-card.json. Full guide: https://describe.net/a2a.md
Metered
| Route | USD | What it answers |
|---|---|---|
GET /reputation/wallet/{wallet} |
0.01 | Reputation of a wallet on every chain we read, with its composition |
GET /reputation/wallet/{wallet}/history |
0.03 | How the score moved, dated by on-chain time |
GET /reputation/agent/{network}/{agent_id} |
0.02 | One agent and every rating with its txHash, revoked ones included and marked |
GET /reputation/rater/{wallet} |
0.01 | The other side of the grain: the wallet as a rater. Totals emitted, distinct subjects, dispersion, per-chain split, favourite-subject share, role breakdown from tag1. It emits no score — ranking raters would be a new verdict |
GET /reputation/wallet/{wallet}?snapshot=true |
0.05 | Writes a durable receipt row with inputs_digest and policy_version |
GET /leaderboard/page |
0.01 | Paging with offset, widening limit (≤500) or filtering by type — every call |
/reputation/rater/{wallet} is the one metered route with no free_preview
in its 402, and the absence is deliberate: /wallets/{wallet}/chains previews
the wallet as a subject, which is a different question from the wallet as a
rater. Offering it there would preview an answer to a question you did not
ask.
Why the split: free is what one cached run serves everybody — the aggregate index
has exactly one answer per cache window — plus the number and the location of a
named subject (/search/{query}, /wallets/{wallet}/chains), which come off
materialised views. Metered is the decomposition of that subject: who rated it,
how many times each one spoke, in which facet, dated, with its transaction — where the
key space is the whole index and the long tail never hits a cache. Until 2026-08-23
this paragraph drew the line at "the question about one named subject"; that was
already inexact for the free per-chain gate, and /search moved it for good.
Charging for /search would also mean billing typos: the paywall runs before any
validation, so a mispasted string quotes exactly like a good one.
The decision loop
The window that matters is between GET /tasks/{id}/applications and
POST /tasks/{id}/assign — routes on your own task marketplace (e.g.
Execution Market), not on describe. After the assign, the EIP-3009 signature
has already picked the worker — the nonce includes the receiver — and the money
is committed.
- Ask for the free list and keep the candidates.
GET /leaderboardcarriesfinal_score,shrunk_scoreanddistinct_ratersin the same row, so you can discard without paying. - Buy the composition of every finalist.
GET /reputation/wallet/{wallet}. - Apply the five rules below. Do not look at the number.
- Read the facets of the same object — the receipt is reused for the same
canonical
resource(X-Payment-Reused). - If you need it: history and evidence. Before using
/history, readcoverage.undated_reviews: if it is high, the series is no good for deciding. - If you owe somebody an account:
?snapshot=true. What you buy is not the number, it is the commitment to the number.
The five rules
| If in the response… | Then | Why |
|---|---|---|
concentration == null |
do not decide with this | Not "no concentration" — the signal is degrading. The API degrades the secondary aggregate rather than failing the whole response. |
distinct_raters < min_raters |
no evidence, which is not zero | 7,577 of 15,763 scored wallets have exactly one rater, and 22 % of those average a flat 100 — against 1 % among those with three (measured 2026-08-10). |
total_reviews / distinct_raters >= campaign_per_rater |
campaign: discard | Over the 3,000 best-ranked wallets the median is 1.00 and the p90 is 2.0. Careful: top_client_share alone does not catch this — the largest campaign in the index has a share of ~0.14 (measured 2026-08-11). |
top_client_share >= top_share, and NOT the row below |
one single voice | Agent base/888 has 1,200 ratings where one rater wrote 98.4 %. It scores 99.23 (measured 2026-08-11). |
top_client_share >= top_share, top_client_index_subjects >= top_client_reach and top_client_ratings <= top_client_burst × (top_client_index_ratings / top_client_index_subjects) |
the shape of one sensor — still one source | The second half of the row above, split on 2026-09-20 because it was the larger half: measured on the live index, 41,719 of the 43,711 subjects the share cut marked (95.4 %) are this. Both halves or neither. Fan-out alone is a number an attacker owns end to end — rate 100 subjects once each and a farm reads as a scanner — and the pilot's hand-labelled corpus already contained one that did (base/18132: fan-out 808, but 17 ratings here against a 1.2 global mean). The second half asks whether that rater behaved here the way it behaves everywhere. |
self_rated.gap > 15 |
it overrates itself | Self-rating stays out of the average but is published with its gap. This cut is yours: reading_policy.self_gap is null because the index publishes the gap and does not judge it. |
The names in the left column are fields of reading_policy, in the free
GET /health — the same object the paid endpoint reads to fill caveats[].
They are named rather than typed here on purpose: until 2026-08-26 this table
carried its own copies and they had already drifted from the code that charges
for them. Each rule fires on its own (reading_policy.combine is
"independent"); requiring two at once is a blind filter, not a strict one.
These five rules are not a traffic light. They filter what is grossly broken. The facets are where you look once a candidate passed the filter.
caveats[] — the same rules, already fired
Every response that carries a score carries caveats[]: the traps these
numbers trigger, written out, derived from the fields of that same response and
never from a second query — which is why it costs nothing extra.
Since 2026-08-28 each entry is an object, not a string:
"caveats": [
{ "code": "burn-address",
"text": "Esta wallet es una direccion de quema bien conocida: …" }
]
(measured live on the free door, 2026-08-30:
curl https://api.describe.net/wallets/0x000000000000000000000000000000000000dead/chains)
The contract, and it is the point of the field: text may change without
notice — be reworded, re-measured, even translated. code never changes.
Branch on caveats[].code, never on caveats[].text. The codes carry no
version and do not bump policy_version, because a caveat is advisory by
construction: a code names a cut, it does not move a score or a price.
These codes are the whole set, and it is frozen by a test — adding or renaming
one is deliberately red, and tests/test_caveat_codes_publicados.py ties this
table to the code:
code |
Fires when |
|---|---|
no-score |
there is no score to read; null, never zero |
concentration-degraded |
concentration came back null — the signal is down, not absent |
single-rater |
exactly one distinct rater |
few-raters |
below reading_policy.min_raters |
top-client-share |
at or over reading_policy.top_share, and the next row does NOT apply: one actor concentrated on THIS subject, which is the wash-trading shape. ⚠️ Narrowed on 2026-09-20 — before that date it also fired on scanners, and that was 95.4% of its marks (see the next row). If you branch on this code, you are getting the same shape with far fewer false alarms. It still fires on a wide-reach rater that broke its own rhythm here — fan-out does not buy an exemption on its own |
top-client-scanner |
same share cut as the row above, different reading, and it takes two measurements of the top rater, never one: its fan-out (concentration.top_client_index_subjects) is at or over reading_policy.top_client_reach, and what it wrote here (concentration.top_client_ratings) is within reading_policy.top_client_burst times what it leaves on a typical subject (top_client_index_ratings / top_client_index_subjects). That is the shape of a scanner's telemetry. Measured on the live index 2026-09-20: of the 43,711 subjects the share cut marked, 41,719 (95.4%) are this. It is not an all-clear, and it is not a verdict about intent — the shape can be bought by rating hundreds of subjects at the same rate first, one sensor is still one source, and the number still has no second voice behind it |
campaign-per-rater |
at or over reading_policy.campaign_per_rater ratings per rater |
self-rated |
the subject rated itself; the gap is published, not judged |
burn-address |
the subject is a known burn address — real on-chain ratings about something nobody controls |
thin-chain |
the wallet spans two or more chains and at least one of them is held up by fewer than reading_policy.min_raters people. Names each thin chain with its rater count and its weight — the distinct_raters / (distinct_raters + m) credibility that chain carries in the global score. A one-person chain weighs little, never zero |
facilitator-authored |
agent route only (GET /reputation/agent/{network}/{id}): at least one row of ratings[] has author_class: facilitator-authored — the facilitator's wallet relayed it, so that row's client is not the counterparty and any per-rater count merges all such rows into one. Carries no numbers: nothing is aggregated by author |
brigading-suspected |
agent route only, and off by default (GET /health → brigading.caveat_enabled): the rater set has the shape of a coordinated burst — many DISTINCT wallets with one rating each, most of them with no prior history, arriving together. This is the OPPOSITE of what top-client-share and campaign-per-rater measure, and the two populations barely overlap: measured over the whole index on 2026-09-18, concentration marked 10,252 subjects, the burst 540, and the intersection was 13. Carries the probability and a suggested reading weight; it never moves the score |
Things that bite:
- An empty list is not "verified clean." It means no trap fired on the fields present in this response.
- On the free
GET /wallets/{wallet}/chainsthe list is a SUBSET — only what is computable from the public aggregate (today:burn-addressand, since 2026-09-05,thin-chain— every row carriesdistinct_ratersandweight). An emptycaveats[]there does not promise that the paid decomposition is quiet. - The free route says what it did not evaluate. Since 2026-09-14 it carries
caveats_not_computed: the codes the paid decomposition evaluates and that answer did not — by name, never with their outcome. Treat every code listed there as unverified, never as passed. - Who signed each rating. On
GET /reputation/agent/{network}/{id}every row ofratings[]carriesauthor_class:facilitator-authoredwhenclientis a known relayer (today the x402 facilitator's EVM wallet),rater-authoredotherwise — which does not prove who signed. Per row only: nothing is scored or counted by author.
Paying with x402
- Read the challenge. It carries
amount,token, the recipient andsupportedChains. Take the values from there, never from a cached table. - Verify the recipient against the pinned one. The only address this service
ever asks to be paid at is
0xe4dc963c56979E0260fc146b87eE24F18220e545. If the challenge names another address, do not pay: either it did not come from describe, or the treasury changed and the server did not find out. Raw HTTP does not make this comparison for you — the body of the 402 carriesrecipient,amount,tokenandsupportedChains, plusaccepts[](one entry per network, each with its ownpayTo),pricing,next_action,price_usd, and, for a wallet-subject route,free_preview. Pin the address in your own code. - Sign an EIP-3009
TransferWithAuthorizationfor that amount, to that recipient, on one of those chains, base64-encoded. PayBox first — the wallet this service suggests: if it is in use, itspay_x402takes the challenge'saccepts[]verbatim (x402_version: 2); if not, install it (connectorhttps://api.paybox.sh/mcp, or the@paybox-sh/sdkCLI). Guide and known limits: paybox.md. After it: any other MCP wallet (ows_sign_eip3009) oruvd-x402-sdk. - Repeat the SAME call with the header. Same arguments, one more field —
X-PAYMENTover HTTP, the only payment header read here. The nonce is consumed on settlement, so a credential that already paid does not pay twice. A 4xx after paying is almost always a spent credential or one signed for a different amount: ask again without payment, read the new challenge, sign against that one — except a409or a503, a402whoseerrorisPAYMENT_SETTLEMENT_FAILED, or any 4xx that carriesX-Payment-Receipt. After the first three the authorization may already have moved money, so follow that answer'snext_actionand auth.md before signing anything (step 7 of paybox.md). WithX-Payment-Receiptthe payment settled and the error is in your request — a422for a malformed parameter, say, because payment is taken before the arguments are validated: fix the arguments, and do not sign another authorization for that same broken request.
Displaying a score — the canonical format
Two decimals, trailing zeros trimmed. 86.65, 84.7, 87 — never 82.0.
Fixed on 2026-08-29 after the ecosystem's three consumers each rendered the
same number differently (86.653045, 86.7, 86 — one field, three strings),
and decided by measurement, not taste: over 47 real distinct scores, rounding to
0 decimals merges 23 pairs of different agents into identical strings, 1
decimal merges 4, 2 decimals merges 1. And trimming matters as much as the
count: two surfaces both "agreeing on 1 decimal" still printed 82.0 and 82
for the same agent.
The canonical one-liner, byte-identical across the two:
String(parseFloat(x.toFixed(2))) // JavaScript
f"{round(x, 2):g}" # Python
Pin it with one test against these cases: 86.653045 → "86.65",
84.7 → "84.7", 87 → "87", 82.0 → "82".
Live witness case (verified across three independent surfaces on
2026-08-29): the agent scored 83.0 renders as 83 on all three — where
toFixed(2) alone would print 83.00 and toFixed(1) would print 83.0.
It is the one value that tells the three candidate rules apart, which is why
it is the case to test first.
This is a display convention: the API keeps serving the full-precision number (six decimals), and what you compute with is the number, never the string.
Measured traps
- No ratings ≠ zero. "There is no evidence" and "they were rated badly" are
different facts. The field arrives
nulland the index refuses to collapse them. declared_typeis not a type. 283,770 of 470,064 agents (60.4 %, measured 2026-08-30) areunknown, and the second largest "type" is the URL of the EIP schema — with its typo variant. ERC-8004 has no type field. Never treat it as verification. (Until 2026-08-30 this line read "69,517 of 82,835 agents (84 %, measured 2026-08-11)". That was true when it was written and it stayed legal — it carried its date — but the index has grown 5.7× since, and the share had moved 24 points. The correction stays written: a dated figure does not stop being stale, it only stops being a lie. Read it live fromGET /stats/types.)tag1is free text on-chain. The longest facet in this index is 471 characters: a paragraph about gardening used as a label. Escape everything that comes from the chain.undated_reviews > 0means the last point of the series can legitimately sit below the profile'sfinal_score. Neither is a bug.- The leaderboard does not order by average. It orders by the Bayesian mean.
shrunk_scoreanddistinct_raterstravel in the response so the order can be recomputed by hand. - The policies are versioned separately.
policy_versionis the score's.ordering_policy,rater_weight_policyandconfidence_policydo not move a single score; merging them would mark every rating as recomputed each time a list changes.GET /healthis the authority on how many there are — this line has already been wrong once, in the same batch that added the fourth.
What is broken
Honesty about our own state, because a reputation index that makes up its own status has no authority to decompose anybody else's.
-
~~
GET /stats/typesfails at the origin after ~25 s~~ — does not reproduce; corrected 2026-08-30. The bullet is kept, struck through, because deleting it would hide that this page published a false status about itself — which is the one thing a reputation index cannot afford to do quietly. It was re-measured with exactly the discriminating check the bullet itself asked for — a cache-buster, so the edge cannot answer for the origin:curl -s -o /dev/null -D - "https://api.describe.net/stats/types?cb=$RANDOM$RANDOM"Three consecutive runs on 2026-08-30, every one
X-Cache: Miss from cloudfront(so every one went to the origin): HTTP 200 in 0.229 s · 0.190 s · 0.198 s, full body,total_agents: 470064. The route is on the critical path again. The lesson survives the bug: a200with anAgeheader proves nothing about the origin — that check is still how you tell a live route from a warm cache, here and anywhere else on this API. -
Cold latency is seconds, not milliseconds. The
402short-circuits before any SQL runs, so the challenge comes back fast; the paid response does the work. Ask for the vet before you open the assignment window, in parallel with everything else — not inside it.
The live version of this list is measured in your own browser, per endpoint, at https://describe.net/api.html#status.
MCP
Stdio tools wrapping the same endpoints, priced like the routes behind them:
the tools that wrap free routes are free, the tools that decompose a named
subject are metered. describe_pricing is free and explains all the others;
describe_check_wallet is also free and answers GET /wallets/{wallet}/chains
— whether a wallet has anything worth paying for, before you spend on
describe_lookup_wallet. When you do not even know what you are holding,
describe_resolve is free too and answers GET /search/{query}: what the string
was read as, and one row per chain with the wallet that owns it there.
Fifteen tools: six are priced like the routes behind them; the other nine
never charge — leaderboard and facets also have free variants (first page / no
wallet). Fourteen were measured on 2026-08-30 with tools/list against the live
server; the fifteenth, describe_categories (the directory's catalog, free), was
added on 2026-09-14 and is counted by the test suite, not yet by a live run.
(This line said "thirteen: five priced" until 2026-08-30 and "fourteen: eight
free" until 2026-09-14. Each was true when counted; no count written in prose
turns red on its own.) Reproduce it yourself in one call:
curl -s -X POST https://api.describe.net/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
The server card is the live list — this count is dated, the card is not; call
describe_pricing first and it tells you what exists.
{
"mcpServers": {
"describe-net": {
"command": "python",
"args": ["/absolute/path/to/the/repo/mcp/server.py"],
"env": { "DESCRIBENET_API_URL": "https://api.describe.net" }
}
}
}
That /absolute/path/to/the/repo is literal: the repository is not published, so
this config only works if you already have the code. In the meantime the tools
do nothing the GET calls above cannot — the server is a wrapper, not a capability. It
does not sign, does not custody, cannot move funds, and computes nothing: no scoring
rule lives there, every tool calls the canonical endpoint. Server card:
https://describe.net/.well-known/mcp/server-card.json
WebMCP
If you visit api.html with a browser, seven tools are registered on the page
via navigator.modelContext.provideContext(): describe_net_index_status,
describe_net_top_wallets, describe_net_price_quote, describe_net_check_wallet,
describe_net_lookup_wallet, describe_net_wallet_by_chain,
describe_net_open_profile. describe_net_check_wallet is free and answers whether
a wallet has anything worth paying for. The metered ones return the 402 challenge
with the recipient already compared against the pinned treasury; they cannot sign
and hold no keys.
Twin files AND content negotiation — you have both
Content negotiation works. Ask for /, /index.html or /api.html with
Accept: text/markdown and you get the markdown twin:
curl -s -o /dev/null -w '%{content_type}\n' -H 'Accept: text/markdown' https://describe.net/api.html
# text/markdown; charset=utf-8 (measured 2026-08-30)
curl -s -o /dev/null -w '%{content_type}\n' https://describe.net/api.html
# text/html (control, same URL, no header)
The response also carries Vary: Accept, Accept-Encoding, which is what keeps a
third-party cache from serving one to the other.
(Correction, 2026-08-30: until today this section opened with "Accept: text/markdown on api.html returns HTML … S3 does not negotiate content",
and it was wrong from 2026-08-21 onward — the day the CloudFront Function
shipped. llms.txt had said the opposite, on this same site, for nine days.
The line mattered more than most: it is the one an agent reads before deciding
not to try. The analysis below is kept unchanged, because it is still why the
feature was built the way it was.)
The twins also stay at fixed URLs (/api.md, /auth.md, /skill.md,
/workflows.md, /badge.md, /a2a.md, /index.md, /manifesto.md), and that
remains the most reliable way to fetch them — no header, no negotiation, no
cache subtleties. The two mechanisms coexist on purpose:
- A twin
.mdfile. Zero infrastructure change, zero added latency, and CloudFront already serves.mdastext/markdown; charset=utf-8. Cost: storage measured in kilobytes. - A CloudFront Function on viewer-request rewriting the URI when
Acceptcontainstext/markdown. Costs about USD 0.10 per million invocations — which is not the real cost. The real cost is the cache: naïvely this requires addingAcceptto the cache key, and getting that wrong poisons it — whoever warms an object first decides what everybody else receives, HTML or Markdown, at random. This site has already eaten that exact bug once with theOriginheader on/leaderboardand/stats/*, where the edge cached withoutOriginin the key and browsers got a response with no CORS header. Splitting the cache key also halves the hit rate for every object. What ships instead rewrites the URI before the cache key is computed, so the agent and the browser land on two different objects and neither can be served the other's bytes. That is the whole trick, and it is why the twin files had to exist first: only a path with a real twin is ever rewritten — promising markdown for a page with no twin would be an announced 404.
Discovery for this file: <link rel="alternate" type="text/markdown" href="/api.md">
in the HTML head, plus /llms.txt.
Related
https://docs.describe.net— the documentation hub: this guide and the others, plus the rendered OpenAPI (Swagger and ReDoc) and a citable snapshot of the schemahttps://describe.net/auth.md— how to authenticate (you do not; payment is the auth)https://describe.net/paybox.md— the suggested wallet: install PayBox or check it is in use, pay and sign with it here, and its known limitshttps://describe.net/badge.md— the embeddable badge: one SVG per wallet, how to embed it and what it does not dohttps://describe.net/a2a.md— the A2A door:message/sendover JSON-RPC, the agent card, and what it answershttps://describe.net/skill.md— the umbrella skill: the whole service as an installable playbookhttps://describe.net/workflows.md— satellite workflows that build on the umbrella skillhttps://describe.net/.well-known/agent-skills/vet-a-counterparty/SKILL.md— the focused vet loop: fromdescribe_pricingto a citable receipt, with the checks in betweenhttps://describe.net/.well-known/api-catalog— RFC 9727 linksethttps://describe.net/llms.txt
SDKs
You never have to hand-roll this API:
| Package | What it is | Install |
|---|---|---|
uvd-describe-sdk (Python: pip install uvd-describe-sdk) |
Reputation client (TS/JS + Python, same contract). Score never travels without composition; null never 0; failures cached as failures; caveats passed through uninterpreted with scope. |
npm i uvd-describe-sdk |
uvd-x402-sdk |
x402 payments (TS) — sign and settle the 402 challenges of the paid routes. | npm i uvd-x402-sdk |
uvd-x402-sdk (Python) |
Same, Python — plus build_uvd_feedback_params (v0.71+), the TAGGING-UVD emitter that fails loud where the reader would degrade silently. |
pip install uvd-x402-sdk |
Zero-code: the badge and the copy-paste widget.
Verbatim source: api.md · from site/api.md at git:386e875182bb · back to the documentation portal