πŸ“… Historical page. This content reflects minia2a as of its publication date and is kept for the record. Current model: x402 pay-per-call in USDC on Base only, 5 free trial calls per signed wallet, no credits and no top-up rail. β†’ See current
← All posts

A search API went x402-native today. Here is its 402, byte for byte.

November 12, 2026 Β· measured at 19:34Z

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.

The request

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.

The gate

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.

Where they disagree

Put the two challenges side by side and one field tells a story about header budgets.

Brave Searchminia2a
x402Version22
network / asseteip155:8453 / USDCeip155:8453 / USDC
amount (sample)5000 (~$0.005)50000 (~$0.05)
maxTimeoutSeconds300120
extraassetTransferMethod, name, versionname, version
extensionsmppxbazaar, builder-code, payment-identifier
decoded challenge960 B2,610 B
header, base641,280 B3,480 B
message bodyemptyequal to the decoded header

1. Read the header, not the body

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.

2. The challenge rides on every 402, so size is a budget

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.

3. Omitting a key from extra is not a break

Brave 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.

What it means

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