API reference
Everything the server accepts and returns, written so a client in any language can be built against it without reading the TypeScript.
Base URL
https://api.x402spend.dev
The older https://x402-spend-reviews.g-764.workers.dev still answers and is the same deployment. New clients should use api.x402spend.dev.
Authentication
None, on every route documented here. Submitting a review is authenticated by the chain rather than by a key: a review is kept because its settlement proves it, not because the sender held a credential. There is nothing to sign up for and no token to rotate.
The one exception is the operational route POST /internal/retry, which takes Authorization: Bearer <ADMIN_TOKEN> and is not part of the public API.
Rate limits
Per client IP, per route class. Exceeding one returns 429 with {"error":"rate limit exceeded"}.
| Route | Limit |
|---|---|
POST /v1/reviews | 30 per minute |
GET /v1/reviews, GET /v1/reviews/recent | 300 per minute |
POST /v1/verify, POST /v1/probe | 20 per minute |
GET /health | no limit |
/health is deliberately exempt: a health check that can be throttled is not a health check. The demo routes are an order of magnitude tighter than the read side because each call costs either an RPC round trip or an outbound fetch to somebody else's server.
Requests and errors
POST bodies are JSON and capped at 4096 bytes; a larger one is 413. Every route validates strictly — a field the schema does not name is rejected rather than ignored, so a client sending something it should be keeping local finds out immediately instead of publishing it.
An error is always a JSON object with an error string:
{ "error": "payer: must be a 20-byte hex address" }
Validation failures name the field and the check it failed, joined by ; when there is more than one. A 503 may carry a detail alongside error with what went wrong reaching the chain.
| Status | Means |
|---|---|
200 | Read succeeded, or a replayed submission already on file |
201 | Review stored and verified against the chain |
202 | Review accepted but not yet verified; the chain was unreachable, and a cron retries |
413 | Body over 4096 bytes |
422 | The request was understood and refused — bad shape, or a settlement that does not prove the claim |
429 | Rate limited |
503 | The chain could not be reached, or the write failed |
422 is the interesting one
A 422 on POST /v1/reviews is usually not a client bug. It means the settlement was read and did not prove what the review claimed. That refusal is the product working, and the error string names the single check that failed.
CORS
GET routes answer any origin, so the data can be embedded anywhere. POST /v1/verify and POST /v1/probe answer the site's own origins only, because they are browser-facing lookups that cost the server money. POST /v1/reviews sends no CORS headers at all: publishing a review is a server-to-server act, and no web page should be able to talk a visitor's browser into performing one on their behalf. Calling it from a server is unaffected — CORS is a browser rule.
Schema version
Every submission carries "schema": 2 and every row returned echoes it. The number is the review vocabulary, not the API: it went to 2 when four outcome labels became two outcomes plus a reason code. A body sending "schema": 1 is refused by name, with a message saying what replaced it, rather than being read under the new vocabulary — discarded and failed do not map onto useful/not_useful without inventing a reason nobody recorded.
The vocabularies
Three closed sets. Values are the wire format.
outcome — useful · not_useful. The response either gave the buyer what they wrote down before paying, or it did not.
reason — no_response · empty · malformed · wrong · stale · insufficient. Required on not_useful, refused on useful, because "it worked" is not a finding about anything. wrong, empty and malformed stay distinct on purpose: the difference between an endpoint that is broken and one that is lying is a different fact about a seller.
recovery — none · retried_same · went_elsewhere · abandoned. What the buyer did after a failure. Optional everywhere and never an outcome.
status — pending · verified · rejected. Where a review sits in verification.
proof — payment_traced · receipt_only. How well a payment was attributed. payment_traced means the transfer itself was found, so the payer is known. receipt_only means the credit was seen but not its sender.
Networks
Networks are CAIP-2. Common spellings are normalised on the way in, so base, base-mainnet and eip155:8453 all mean the same chain and none is refused over spelling.
| Chain | CAIP-2 |
|---|---|
| Base | eip155:8453 |
| Base Sepolia | eip155:84532 |
| Avalanche | eip155:43114 |
| Avalanche Fuji | eip155:43113 |
| Arbitrum One | eip155:42161 |
| BNB Smart Chain | eip155:56 |
| Solana | solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp |
| Solana Devnet | solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1 |
A network being named is a separate question from its being verifiable: a chain with no RPC endpoint configured is refused with no RPC endpoint configured for <id>, which is a more honest answer than refusing it for how it was spelled.
Spelling, by family
Which spelling is correct depends on the network, so both are checked together:
eip155:*— addresses are0x+ 40 hex; settlements are0x+ 64 hex. Case-insensitive, since EIP-55 checksums are only casing, and stored lowercased.solana:*— addresses are base58 pubkeys; settlements are base58 64-byte signatures. Base58 is not case-insensitive and is stored exactly as sent.