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"}.

RouteLimit
POST /v1/reviews30 per minute
GET /v1/reviews, GET /v1/reviews/recent300 per minute
POST /v1/verify, POST /v1/probe20 per minute
GET /healthno 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.

StatusMeans
200Read succeeded, or a replayed submission already on file
201Review stored and verified against the chain
202Review accepted but not yet verified; the chain was unreachable, and a cron retries
413Body over 4096 bytes
422The request was understood and refused — bad shape, or a settlement that does not prove the claim
429Rate limited
503The 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.

outcomeuseful · not_useful. The response either gave the buyer what they wrote down before paying, or it did not.

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

recoverynone · retried_same · went_elsewhere · abandoned. What the buyer did after a failure. Optional everywhere and never an outcome.

statuspending · verified · rejected. Where a review sits in verification.

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

ChainCAIP-2
Baseeip155:8453
Base Sepoliaeip155:84532
Avalancheeip155:43114
Avalanche Fujieip155:43113
Arbitrum Oneeip155:42161
BNB Smart Chaineip155:56
Solanasolana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp
Solana Devnetsolana: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 are 0x + 40 hex; settlements are 0x + 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.