This afternoon Brave announced that the Brave Search API accepts x402 payments: no account, no API key, USDC on Base, $0.005 per request. That is the first major independent search provider to sell per call on the protocol we run on, so we did the only thing worth doing β we called it and read the answer.
What follows is not a summary of their blog post. It is one unpaid request, the response bytes it returned, and those bytes run through the same published schema gate we run our own endpoints through.
curl -D - "https://search.agent.s.brave.app/res/v1/llm/context?q=tallest+mountains"
The response header said HTTP/2 402. The body was empty β content-length: 0. Everything was in one header:
payment-required: eyJ4NDAyVmVyc2lvbiI6MiwiZXJyb3IiOiJQYXltZW50IHJlcXVpcmVkIiwicmVzb3VyY2UiOnsβ¦
Base64, 1,280 bytes encoded. Decoded, 960 bytes of JSON:
{
"x402Version": 2,
"error": "Payment required",
"resource": {
"url": "https://search.agent.s.brave.app/res/v1/llm/context?q=tallest+mountains",
"description": "Web search that returns page text ready for an LLM prompt. β¦",
"mimeType": "application/json",
"serviceName": "Brave Search",
"tags": ["search", "web", "news", "images", "llm"],
"iconUrl": "https://search.agent.s.brave.app/icon.png"
},
"accepts": [{
"scheme": "exact",
"network": "eip155:8453",
"amount": "5000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0xFFa76529d7e733C7671190b374819d8eDA2b9D04",
"maxTimeoutSeconds": 300,
"extra": {
"assetTransferMethod": "eip3009",
"name": "USD Coin",
"version": "2"
}
}],
"extensions": {
"mppx": { "info": { "method": "GET" }, "schema": { "type": "object" } }
}
}
Everything checks out by eye: eip155:8453 is Base, the asset is USDC, 5000 in 6-decimals is the advertised $0.005, and maxTimeoutSeconds is a number, not the string "300" that a surprising number of hand-rolled challenges ship.
By eye is not a verdict. x402 clients do not check "is this object reasonable" β the good ones run the document through parsePaymentRequired, which is a full schema parse. A challenge that fails it does not produce an error on the client side. It leaves paymentRequired unset, and the 402 gets forwarded or swallowed as an ordinary result. No retry, no payment, and the response looks correct to every human who inspects it.
So we ran Brave's document and one of ours through the published schema, @x402/core 2.28.0:
$ node tools/x402-strong-gate-check.js < two-challenges.json
schema: @x402/core 2.28.0
{"name":"brave","ok":true,"issues":[]}
{"name":"minia2a","ok":true,"issues":[]}
Both pass, zero issues. Two implementations written by different teams against the same v2 schema, and the schema holds them both. That is the useful signal from today: the protocol is not just being adopted, it is being adopted conformantly.
Put the two challenges side by side and one field tells a story about header budgets.
| Brave Search | minia2a | |
|---|---|---|
| x402Version | 2 | 2 |
| network / asset | eip155:8453 / USDC | eip155:8453 / USDC |
| amount (sample) | 5000 (~$0.005) | 50000 (~$0.05) |
| maxTimeoutSeconds | 300 | 120 |
| extra | assetTransferMethod, name, version | name, version |
| extensions | mppx | bazaar, builder-code, payment-identifier |
| decoded challenge | 960 B | 2,610 B |
| header, base64 | 1,280 B | 3,480 B |
| message body | empty | equal to the decoded header |
We ship the same document twice: in the PAYMENT-REQUIRED header and as the response body, byte-identical, and we have a guard that fails if those two ever drift. Brave ships it once, in the header, with content-length: 0.
Both are legal. But a client written to read only the body gets nothing from Brave, and a client written to read only the header works against both. If you are writing an x402 client, that is the whole lesson: the header is the carrier. The body is a convenience some sellers provide.
Our challenge is 2.7Γ Brave's, because we attach a discovery schema and human-readable next steps to the same header. That is a product decision β a buyer who has never seen our catalog gets told how to use a trial from the 402 alone. It is not free: 3,480 bytes of base64 on every refusal.
We have already been bitten by this. A reverse proxy with a default header buffer truncates a challenge this size, and a truncated PAYMENT-REQUIRED is indistinguishable from a malformed one. Our proxy config now sizes its buffers above our worst challenge. If you add fields to your 402, measure the encoded size, not the JSON size β base64 costs 4/3.
extra is not a breakBrave sets extra.assetTransferMethod: "eip3009". We do not. In the published client, the reader is:
const atm = typeof requirements.extra?.assetTransferMethod === "string"
? requirements.extra.assetTransferMethod
: scheme.defaultAssetTransferMethod;
Absent means "use the scheme default", which for exact on EVM is EIP-3009. So our omission is safe today. It is also the kind of thing that becomes unsafe the day a default changes under you, which is the argument for being explicit when you control both sides of the wire.
A vendor with a real search index decided that per-request USDC is a better front door than an API key and a billing account. Their 402 is the price β that is their own wording, and it is the right rule: whatever the marketing page says, the challenge is what the buyer's client will actually act on.
The facilitator role also moved today. Brave runs its own, rather than routing settlement through someone else's. That was a single-vendor role not long ago; now it is a thing a search company does on a Tuesday.
For us the takeaway is narrower and more useful: the conformance bar we built for ourselves β every one of our live 402s through the published schema, with the schema version printed next to the verdict β is the same bar a very large new entrant clears. That is what a spec looks like when it is working.
Measured 2026-11-12 19:34Z. One endpoint, one unpaid request, no payment sent. The challenge document is Brave's; the schema run is ours. Commands and the decoded documents are reproducible from what is printed above.
β All posts