POST /v1/reviews

Publish a review of a paid call. It is kept only if the settlement it names proves the payment it claims.

POST https://api.x402spend.dev/v1/reviews
Content-Type: application/json

The submission

Strict: any field not listed here is rejected rather than ignored.

FieldTypeRequiredNotes
schemanumberyesMust be 2
resourceUrlstringyesAbsolute URL, query string and fragment removed
taskClassstringnoMax 64 chars. Free text; see the note below
networkstringyesCAIP-2, or a common alias
assetstringyesContract address or mint. Never a symbol
amountstringyesAtomic units, decimal digits only, max 30 digits
payTostringyesThe endpoint's address
transactionstringyesThe settlement hash or signature
payerstringyesThe address that paid
outcomestringyesuseful or not_useful
reasonstringconditionalRequired on not_useful, refused on useful
recoverystringnoWhat you did after a failure
notestringnoMax 500 chars. Free text — and it is published
paidMsnumbernoNon-negative integer; the payment leg's duration
tsstringyesISO 8601 timestamp

asset must be an address because verification must never guess which token a name meant. A review claiming one asset could otherwise be proved by a transfer of another.

`taskClass` is filtered by the client, not by the server

This endpoint accepts any taskClass up to 64 characters, because it cannot know what vocabulary a given client uses. The x402-spend client publishes one only when it is shared vocabulary — geocode_forward, geocode_reverse, search_known, search_relevance, weather, price — and keeps anything else local unless asked.

If you are writing your own client, that rule is yours to implement. It is worth implementing: taskClass is the field most likely to be built from a variable at a call site, and a published dataset gains nothing from a category only one caller uses.

{
  "schema": 2,
  "resourceUrl": "https://api.example.com/search",
  "taskClass": "search_known",
  "network": "base-sepolia",
  "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
  "amount": "10000",
  "payTo": "0x209693bc6afc0c5328ba36faf03c514ef312287c",
  "transaction": "0x58156fc561bdca322af0a351b5ee9540a5656a829ec061cbcd30a1bb62a478c6",
  "payer": "0xa2290443ca0a5a599dccc14e4db4df5196e91a59",
  "outcome": "useful",
  "paidMs": 842,
  "ts": "2026-09-14T12:00:00.000Z"
}

Responses

{ "id": "…", "status": "verified", "verified": true }
StatusBodyWhen
201status: "verified"The chain proved the claim
200status as storedThis settlement and payer were already on file
202status: "pending"The chain was unreachable; a cron retries every 5 minutes, up to 5 attempts
422{ "error": … }Bad shape, or the settlement does not prove the claim
503{ "error": …, "detail"? : … }Storage failed after the chain answered

Idempotency

(transaction, payer) is unique. Re-posting the same settlement does not re-read the chain: a proved settlement cannot change, which is what makes this route safe to retry blindly after a timeout.

A replay returns 200. If the outcome or note differ from what is stored, the label is updated; if the payment facts differ — a different amount, asset or recipient for the same settlement — the submission is refused 422 rather than silently overwriting a verified row.

Why a review is refused

422 bodies carry one of these. Fixed reasons:

ReasonMeans
transaction not foundThe node has no such transaction
transaction did not succeed on chainIt reverted or errored
no Transfer log sent by payerNothing in the transaction was sent by payer
no Transfer log from payer to payToPayer sent something, but not to payTo
ambiguous: multiple matching transfersTwo transfers could be the payment; neither is provable
could not attribute the matching transfer to a payerThe transfer has no recorded authority
payer and payTo are the same addressPaying yourself proves a transfer, not a purchase
network not supportedNot an eip155: or solana: id
transaction has no metadata to verify againstSolana returned the transaction without meta
amount must be atomic units as a decimal stringamount was not digits

Others name the specific value that failed, for example no Transfer log from payer to payTo for amount 10000.

The EVM rule

An ERC-20 Transfer log on the asset's own contract, sent by payer, received by payTo, for exactly amount, in a transaction whose status is 0x1.

The payer is read from the transfer log, never from the transaction sender. Under EIP-3009 transferWithAuthorization the facilitator broadcasts and pays the gas, so tx.from is the facilitator and not the buyer.

The Solana rule

Exactly one token transfer crediting payTo's associated token account for the mint in asset, across top-level instructions and the full CPI trace. Zero is not a payment; two is ambiguous. Overpayment is tolerated — a smart wallet may round up — and the amount actually moved is recorded; a shortfall is not.

The payer is the transfer's own authority, never the fee payer: a sponsored transaction is signed and paid for by the facilitator.

If the node returns no CPI trace at all, the documented fallback applies: payTo's balance rising by at least amount confirms the payment as receipt_only, which shows the recipient was credited without showing who credited them.