# auth.md — how to authenticate with describe

> What the API serves and what it costs:
> [api.md](https://describe.net/api.md) ·
> [api.html](https://describe.net/api.html) ·
> [llms.txt](https://describe.net/llms.txt).
> This page is only about how you pay.

**There is no account, no registration, no API key, no OAuth, no OIDC, and no
token. Payment *is* the authentication.** You pay per request with
[x402](https://x402.org) over USDC, on any of six EVM networks. Nothing needs to
happen before your first call. If you can sign an EIP-3009 authorization, you are
already a customer.

Everything on this page was verified against the live API on **2026-08-11**; the
`/feed` row, on **2026-08-13**; the free-route table (every row answers 200) and
the 402 challenge (headers, body shape, recipient, base-unit amounts) were
re-verified on **2026-08-21**. On **2026-08-30** the route tables, the challenge
headers and body, and the three `/.well-known/` claims were re-measured against
production, and what had drifted was corrected **in place, with the correction
left written** — the paragraphs in italics below are those corrections, not
history for its own sake. The date is per-claim on purpose — restating one date
for the whole page would assert a re-check that did not happen.

---

## Why there is no OAuth discovery document here

An agent that reads `/.well-known/openid-configuration`, follows it to an
authorization endpoint and finds nothing has been sent on a trip that cannot
succeed. So we do not publish one. Specifically, describe does **not** serve:

| Not published · HTTP 404 | Why |
|---|---|
| `/.well-known/openid-configuration` | There is no OpenID Provider. No issuer, no `authorization_endpoint`, no `token_endpoint`, no `jwks_uri` exist. |
| `/.well-known/oauth-authorization-server` | There is no OAuth authorization server, so there is no `agent_auth` block, no `register_uri`, and no credential to claim or revoke. |

*(Both verified `404` on both hosts, 2026-08-30.)*

The absence is the accurate signal. Do not look for a bearer token — there is
none to get, and no request to this API is ever improved by an `Authorization`
header.

### `/.well-known/oauth-protected-resource` **is** published — and says so honestly

*Correction, 2026-08-30: this page carried that path in the table above, as
"not published", with the reasoning "RFC 9728 requires an `authorization_servers`
array; ours would have to be empty or invented". The document has in fact been
served since 2026-08-21, it answers `200` on both hosts, and it names **this very
page** as its `resource_documentation` — so an agent following the pointer was
reading a page that denied the pointer existed. The correction is kept written,
because the original reasoning was the right question with the wrong answer: an
empty array is not an invented one.*

```bash
curl -s https://api.describe.net/.well-known/oauth-protected-resource
```

```json
{
  "resource": "https://api.describe.net",
  "resource_name": "describe.net — ERC-8004 reputation API",
  "resource_documentation": "https://describe.net/auth.md",
  "authorization_servers": [],
  "scopes_supported": [],
  "bearer_methods_supported": [],
  "x402": {
    "protocol": "x402+HTTP",
    "pricing": "https://api.describe.net/pricing",
    "documentation": "https://describe.net/auth.md"
  }
}
```

**`authorization_servers: []` is the claim, not a gap.** RFC 9728 lets a
protected resource describe itself; an empty array is the machine-readable way
to say *this resource is real and there is no token issuer for it* — which is
exactly true, and strictly better than a 404, because a 404 cannot distinguish
"we thought about it and there is none" from "nobody configured this". The empty
`scopes_supported` and `bearer_methods_supported` say the same thing twice more.
The non-standard `x402` block is where an agent goes next: it names the pricing
route and this document. Same body from `https://describe.net/.well-known/oauth-protected-resource`.

---

## What is free, and what costs money

Free routes need no payment and no headers at all. **Start here — reading the
free tier costs nothing and tells you whether the paid tier is worth it.**

| Free · HTTP 200 | What it gives you |
|---|---|
| `GET https://api.describe.net/wallets/{wallet}/chains` | Per-chain ERC-8004 identities and reputation for **one** wallet — `agent_ids`, `final_score`, `total_reviews`, `distinct_raters`, plus `caveats[]` (a subset — see below). The preview before paying for `/reputation/wallet/{wallet}`: if nothing is here, there is nothing to buy. |
| `GET https://api.describe.net/search/{query}` | The one box: an EVM address, a Solana base58 id or an agent id. Answers what it was read as and one row per chain, each with the wallet that owns it *there*. Exact match only; never in the singular. |
| `GET https://api.describe.net/pricing` | Every price, the reason for each price, and the current free tier. Machine-readable. |
| `GET https://api.describe.net/health` | Liveness and how far the index has scanned on each chain. |
| `GET https://api.describe.net/chains` | Every indexed chain side by side — agents, ratings, distinct raters, `stale_hours`. |
| `GET https://api.describe.net/chains/{network}` | One chain of the index — its stats, its recent activity, its top wallets. |
| `GET https://api.describe.net/manifesto` | The principles as data, each with `enforced_by` — the file or route that makes it true. |
| `GET https://api.describe.net/leaderboard` | First page — the shop window. Ranked by evidence, not by raw average. Takes no parameters: a query param answers 422 pointing at the metered `GET /leaderboard/page`. |
| `GET https://api.describe.net/stats/facets` | Every rating facet in the index with its volume and mean. |
| `GET https://api.describe.net/stats/types` | Declared-type coverage across indexed agents. |
| `GET https://api.describe.net/stats/issuers` | Who wrote the index: ratings grouped by the host of their `feedbackURI`, with distinct agents and raters per issuer, plus a `no_host` row. Free because it is the route somebody audits *us* with. |
| `GET https://api.describe.net/feed` | The newest ratings across every chain, newest first. A feed of facts — every row already happened in a finalised block. |
| `GET https://api.describe.net/badge/{wallet}.svg` | The embeddable badge: one SVG with the global score, its distinct raters and the date of the data. Free by the cost rule — one URL serving the same bytes to everybody, cached an hour at the edge. See [badge.md](https://describe.net/badge.md). |
| `POST https://api.describe.net/a2a` | The same free preview over A2A JSON-RPC (`message/send`). Agent card at `/.well-known/agent-card.json`. See [a2a.md](https://describe.net/a2a.md). |
| `POST https://api.describe.net/mcp` | MCP over Streamable HTTP. Free door: the metered tools relay the 402 of the route behind them and forward the signature you pass in `payment`. |
| `GET https://api.describe.net/.well-known/oauth-protected-resource` | RFC 9728 self-description — see the section above. A `/.well-known/` route that charged would be a contradiction in terms. |
| `GET https://api.describe.net/openapi.json` | OpenAPI 3.1 description. |
| `GET https://api.describe.net/docs` · `/redoc` | Human documentation. |

*The `/chains`, `/chains/{network}` and `/manifesto` rows were missing from
this table and were added on **2026-08-21**, each verified live with an
HTTP 200 that day — the per-claim dates above are unchanged.*

*The `/search/{query}`, `/stats/issuers`, `/badge/{wallet}.svg`, `/a2a`, `/mcp`
and `/.well-known/oauth-protected-resource` rows were added on **2026-08-30**,
each verified live that day. All six had been answering `200` for days before
they were named here: a free route missing from the table of free routes reads
as a route that does not exist, and an agent that reads this page to decide what
it can do without paying was being told less than the truth.*

Metered routes answer a question about **one named subject**. Each returns
**HTTP 402** until paid.

| Metered · HTTP 402 | Price (USD) |
|---|---|
| `GET /reputation/wallet/{wallet}` | 0.01 |
| `GET /reputation/wallet/{wallet}?snapshot=true` | 0.05 — the only route that **writes**: it records a durable, citable receipt row with `inputs_digest` + `policy_version`. |
| `GET /reputation/wallet/{wallet}/history` | 0.03 |
| `GET /reputation/agent/{network}/{agent_id}` | 0.02 |
| `GET /reputation/rater/{wallet}` | 0.01 — the **other side of the grain**: this wallet as a *rater*, not as a subject. Totals emitted, distinct subjects, dispersion, per-chain split, favourite-subject share, roles from `tag1`. It emits **no score**: ranking raters would be a new verdict, and this index returns evidence. Added 2026-08-30 to this table; live and metered since 2026-08-29. |
| `GET /leaderboard/page` | 0.01 — paging past the shop window (`offset`, `type`, `limit` ≤500), every call. Split from `/leaderboard` on 2026-08-21 so the free page could live in the CDN. |

Prices come from the live challenge, never from this table. This table can go
stale; the `accepts[]` array in the 402 cannot.

Every metered 402 also carries a `free_preview` naming the free route that
answers the *same* question about the *same* subject — except
`/reputation/rater/{wallet}`, which deliberately has none: the free
`/wallets/{wallet}/chains` previews the wallet as a **subject**, and previewing
a different question would be worse than previewing nothing.

---

## The flow, in five steps

### 1. Call the route you want. Do not pre-authorize.

Asking without paying is free and is the intended first move — the 402 is the
price quote, not an error.

```
GET /reputation/wallet/0xf9d1…d5f1 HTTP/1.1
Host: api.describe.net
```

### 2. Read the challenge

```
HTTP/1.1 402 Payment Required
Content-Type: application/json
X-Accept-Payment: x402 USDC 2.0
X-Pricing-Version: <the pricing version in force — read it, do not assume it>
Payment-Required: <the JSON body below, base64>
Cache-Control: no-store
```

**The pricing version is not written on this page on purpose.** It travels in the
`X-Pricing-Version` header and again in `pricing.version` of the body, and it
bumps whenever a price *or* the free/paid line moves — which is exactly the event
that would make a number copied into this file wrong. It is the same argument
this page already makes about prices, applied to the thing that versions them.
*(Correction, 2026-08-30: until today this example and the JSON below both read
`cost-tiered@1`. Four bumps had shipped without either line moving — the
leaderboard split, `/search`, the badge and rater profile, and `/stats/issuers`.
The fix is not a newer number: it is not printing one.)*

The body is the contract:

```json
{
  "x402Version": 2,
  "scheme": "exact",
  "resource": "GET /reputation/wallet/0xf9d1…d5f1",
  "amount": "0.01",
  "price_usd": "0.01",
  "token": "USDC",
  "maxTimeoutSeconds": 120,
  "retryable": true,
  "recipient": "0xe4dc963c56979E0260fc146b87eE24F18220e545",
  "supportedChains": [8453, 43114, 42161, 10, 137, 42220],
  "accepts": [
    {
      "scheme": "exact",
      "network": "eip155:8453",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "amount": "10000",
      "payTo": "0xe4dc963c56979E0260fc146b87eE24F18220e545",
      "maxTimeoutSeconds": 120,
      "extra": { "name": "USD Coin", "version": "2" }
    }
  ],
  "pricing": { "version": "<in force>", "tier": "wallet-lookup", "why": "…" },
  "next_action": "…",
  "free_preview": { "endpoint": "GET /wallets/{wallet}/chains", "gives": "…" },
  "see_also": ["GET /search/{query}", "GET /badge/{wallet}.svg", "…"]
}
```

`accepts[]` carries one entry per network and **is the only thing you should
sign against**. `amount` inside `accepts[]` is in token base units — USDC has 6
decimals, so `"10000"` is $0.01. The top-level `amount` is the human-readable
dollar figure; do not sign that one. The challenge also carries a `facilitator`
identifier, which you do not need in order to pay.

Two fields are worth reading **before** you decide to pay at all, and both are
in the body above:

- **`free_preview`** — the free route that answers the same question about the
  same subject. Call it first; if it comes back empty, this charge buys nothing.
- **`see_also`** — the structured list of every free door of the service, in the
  challenge itself. Measured 2026-08-30 it carried sixteen entries, from
  `GET /search/{query}` to `POST /a2a` and `POST /mcp`. It is a list because a
  paragraph of prose is not something an agent can branch on; read it from the
  challenge rather than from any table, here included.

*(The block above names fifteen top-level fields and the prose names a
sixteenth, `facilitator`. The live 402 carried **twenty-two** on 2026-08-30; the
six this page does not walk through are `description`, `error`, `message`,
`mimeType`, `recipients` and `service`. Do not take that on faith — asking for a
challenge is free: `curl https://api.describe.net/reputation/wallet/0x715dc035ffb97dd7bb4095c6670138ba05bb4e6d`.)*

### 3. Verify who you are about to pay — do this every time

The only address describe will ever ask you to pay is:

```
0xe4dc963c56979E0260fc146b87eE24F18220e545
```

Check `payTo` against it before signing. This address is also pinned as a
constant in our MCP server (`PUBLISHED_TREASURY` in `describenet/mcp_server.py`) for exactly
this reason: a 402 is a payment instruction that arrived over the network, and
the one defense a payer always has is comparing it to a value published out of
band. **If `payTo` differs, do not pay.** Either the challenge did not come from
describe, or our treasury changed and this document is stale. Both are
resolved by asking, not by signing.

### 4. Sign an EIP-3009 authorization

Pick one entry from `accepts[]`. Sign a `TransferWithAuthorization` for that
`amount`, to that `payTo`, for that `asset`, on that `network`. Encode the x402
payload as base64.

The six accepted networks, with the USDC contract each challenge names
*(order updated 2026-08-21; verify against the live 402, which is always the
authority — a challenge issued before the update may still list the same six
networks in the previous order)*:

| Network | Chain ID | USDC |
|---|---:|---|
| Base | 8453 | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
| Avalanche | 43114 | `0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E` |
| Arbitrum | 42161 | `0xaf88d065e77c8cC2239327C5EDb3A432268e5831` |
| Optimism | 10 | `0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85` |
| Polygon | 137 | `0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359` |
| Celo | 42220 | `0xcebA9300f2b948710d2653dD7B07f33A8B32118C` |

Ethereum L1 is deliberately absent: settlement gas is paid for you, and paying
L1 gas to collect one cent would be selling at a loss. There is no Solana path —
the challenge advertises only networks where the treasury actually has an
address.

**You never send a private key anywhere.** You sign locally and send the
signature. Ways to produce that base64 string:

- an MCP wallet — `ows_sign_eip3009` (Open Wallet Standard) or `pay_x402` (PayBox);
- Python — `pip install -U "uvd-x402-sdk[wallet]"`. Take the latest: measured
  2026-08-30, the service itself runs **0.70**. *(Until today this line pinned a
  floor of `>=0.42.0`. It was never false — the floor still admits every current
  release — but it named a version nothing runs any more, on either side of the
  wire, and it is the snippet an integrator copies verbatim. A published floor
  that nobody tests is a suggestion to install something untested.)*

  ```python
  from decimal import Decimal
  from uvd_x402_sdk import X402Client, X402Config

  client = X402Client(config=X402Config(recipient_evm=pay_to))
  client.connect_with_signer(...)          # key stays on your side
  header = client.create_authorization(
      pay_to=pay_to, amount_usd=Decimal("0.01"), chain_name="base",
  )
  ```

### 5. Replay the exact same request with `X-PAYMENT`

Same method, same path, same query string — one header more.

```
GET /reputation/wallet/0xf9d1…d5f1 HTTP/1.1
Host: api.describe.net
X-PAYMENT: <base64 payload>
```

Payment is verified and settled on-chain **before** the query runs. On success:

```
HTTP/1.1 200 OK
X-Payment-Receipt: 0x<settlement transaction hash>
X-Payment-Reused: false
```

`X-Payment-Receipt` is a public hash, not a secret. Keep it: it is how you
reconcile what you read against what moved on-chain.

---

## Rules that will save you money

**One authorization per paid call.** The nonce is consumed when the facilitator
settles. A credential that already paid will not pay again — signing one per
call is the design, not a workaround.

**The receipt is reusable, but only for the identical resource.** The receipt is
keyed by `sha256(X-PAYMENT | resource)`. Re-sending the same header for the
*same* URL reads again for free until it expires (a window between 300 and 1800
seconds), and the response says so with `X-Payment-Reused: true`. Re-sending it
for a *different* resource will not work — that is a deliberate guard, not a
bug. Wallet A's receipt cannot unlock wallet B.

**A 503 is not a 402. Do not re-sign on a 503.**

```json
{
  "error": "facilitator_unavailable",
  "message": "Your authorization was NOT considered used.",
  "retryable": true,
  "retry_after_seconds": 5
}
```

This means our settlement path was unreachable, not that you failed to pay.
Retry the same request with the **same** `X-PAYMENT` header after `Retry-After`.
Signing a fresh authorization here can make you pay twice.

**A 4xx after you sent `X-PAYMENT`** is almost always a spent credential or one
signed for a different amount or resource. Retrying with the same header fails
identically. Ask again without `X-PAYMENT`, read the fresh challenge, sign
against that one.

---

## Identity, quotas, and what we know about you

There is no client identity here — no account, no key, no session — so there is
no per-client quota to apply for and no tier to be upgraded into. Price is the
throttle: metered routes cost what `GET /pricing` says they cost, and that is
the whole access-control model. You are never identified, never profiled, and
never asked to prove who you are — only that you paid.

That is a statement about authentication, not a promise of unlimited
throughput: like any service, this one sits behind infrastructure that may
shape or reject abusive traffic. If you get a `429`, back off and retry; it is
not a payment problem and re-signing will not fix it.

---

## Also worth reading

- `https://docs.describe.net` — the documentation hub: every guide plus the
  rendered OpenAPI (Swagger, ReDoc) and a citable snapshot of the schema.
- `https://describe.net/api.md` (twin of `api.html`) — what the API serves,
  what each route costs, and the decision loop.
- `https://describe.net/badge.md` — the free embeddable badge, and what it
  deliberately cannot do.
- `https://describe.net/a2a.md` — the A2A door, for a client that speaks
  `message/send` rather than HTTP.
- `https://describe.net/llms.txt` — the whole surface, for agents.
- `https://describe.net/.well-known/api-catalog` — machine-readable catalog
  (RFC 9727): OpenAPI description, documentation, pricing, health.
- `https://api.describe.net/pricing` — free, authoritative, and explains the
  reasoning behind every price.
- `https://describe.net/.well-known/mcp/server-card.json` — the MCP server, if
  you would rather call tools than write HTTP.
- `https://describe.net/.well-known/agent-skills/index.json` — the skill index;
  the vet-then-pay loop itself lives at
  `https://describe.net/.well-known/agent-skills/vet-a-counterparty/SKILL.md`.

**One last thing, because it is the reason this API exists:** never make a
decision on `final_score` alone. Every paid response ships `distinct_raters`,
`top_client_share`, `self_rated` and `caveats[]` alongside the number — the last
one being the traps those very numbers trigger, already written out, each with a
stable `code` you can branch on (`caveats[].code`, never `caveats[].text`; the
codes are listed in [api.md](https://describe.net/api.md)). None of it moves the
score, and none of it fills a null one. A score without its raters is a rumor.
