Learn / Lesson 2

Receipts and how to verify spend

Compare a settlement response with the accepted offer, and separate a receipt hash from a verified spend figure. [1]

quotum x402 per-request intake remains closed. Do not sign a per-request payment. [2]

A receipt is a claim

In x402 v2, the server relays settlement in the PAYMENT-RESPONSE header: base64-encoded JSON, not a signed receipt. Decode it. A missing signature on that header is normal. Do not treat the decoded object as proof that a transfer happened. [1]

The settlement amount is optional. If a header leaves out the amount or payer, do not assume agreement or fill the gap with the value you hoped to see. Read the transaction instead. Check the recipient there too; the settlement header is not independent evidence of it. [1]

Keep the records separate

For an EVM exact payment using EIP-3009, the client signs and the facilitator submits the authorization. Its fields are from, to, value, validAfter, validBefore and nonce. The EIP-712 domain binds token name, version, chainId and verifyingContract. Check that the authorization’s “to” field equals the offer’s payTo, its value equals the amount, and its domain matches the asset and network. Nonces are random 32-byte values; the authorization also has a validity window. [1]

The chain records the transfer and the use of the authorization. The server response reports a settlement result. A service receipt can describe usage as well. These records answer different questions. Use the transaction and its events to check the payment, and keep the service record for the work being billed. [1]

The optional Offer and Receipt extension can add server-signed offers and receipts in eip712 or jws formats. The notes describe its wire shape as unstable. Do not mistake that optional signature for the ordinary PAYMENT-RESPONSE header. Keep the payment check beside the service claim. [1]

Four checks before you count it

If errorReason is settlement_pending, settlement is non-terminal: broadcast, not confirmed. It must include a transaction hash. Reconcile that transaction before any retry. A missing transaction is an incomplete response, not permission to sign again. [1]

Second, check the network and transaction. A response with success set to true but an empty transaction gives this lesson no transfer to inspect. The checker below refuses it. Even when the fields match, the checker asks for a chain check because it compares two claims without reading the chain itself. [1]

Third, compare amount and payer only when those fields are present. Amount is atomic token units. Do not fill a missing amount from the offer. Read the recipient from the chain transfer to the offer’s payTo. This checker has no recipient field and will not invent a match. Fourth, separate payment from delivery: quotum documents a pending response that proves neither delivery nor refund. [1] [2]

What a seat receipt proves

quotum documents one receipt row per seat per session. Its hash is keccak256 of the row JSON without the hash field. The keeper puts the receipt hash on chain in the Reported event, described as Reported(session, seat, usd, receiptHash). The spend figure comes from the provider usage API through the keeper. [2]

Do not recompute a live receipt hash from this page. The docs do not specify canonical JSON, and the sample module does not hash anything. A hash would only commit to exact bytes. It would not measure provider usage, and it would not prove the payment. The interactive control below hashes a fictional JSON string locally; it does not verify a fetched receipt. [2]

Try it

Change the sample spend below and compare the two hashes. Reset restores the original bytes. The sample includes call and house fields so you can see that the whole JSON string matters, not just the displayed spend. Its round values are teaching inputs, not observed transactions or a current seat allowance. [2]

Then choose a settlement case and inspect the checker result. Missing amount, missing payer, a mismatched amount, pending settlement and an empty transaction each need their own treatment. Read the retry lesson before replacing a payment after an interruption. All controls and the runnable snippet use local sample data; no wallet is needed. [1]

Break the receipt. Fictional local sample.
Provider readingReceipt rowCommitmentUsage sourceExact JSON bytesHash comparison 5

Original sample hash Run locally with JavaScript.

Current sample hash Run locally with JavaScript.

{"session":1,"seat":1,"capUsd":10,"spentUsd":5,"call":null,"house":null}

Sample only. No live receipt is loaded.

Choose a sample with JavaScript enabled.

Run it

Self-contained JavaScript sample. Run here or save the module and run it with Node. No network or credentials.

// Offline sample only. This compares claims; it does not verify a chain.
export function checkSettlement(s, agreed) {
  if (!s || typeof s !== 'object') return 'FAIL: invalid response';
  if (s.errorReason === 'settlement_pending') {
    return typeof s.transaction === 'string' && s.transaction.trim() ? 'PENDING: reconcile ' + s.transaction : 'FAIL: pending without transaction';
  }
  if (s.success !== true) return 'FAIL: settlement not successful; read errorReason and reconcile any transaction before retrying';
  if (typeof s.transaction !== 'string' || !s.transaction.trim()) return 'FAIL: missing transaction';
  const mismatches = Object.keys(agreed).filter(k => k in s && s[k] !== agreed[k]);
  if (mismatches.length) return 'FAIL: mismatch in ' + mismatches.join(', ');
  const missing = Object.keys(agreed).filter(k => !(k in s));
  if (missing.length) return 'CHECK CHAIN: missing ' + missing.join(', ');
  return 'CHECK CHAIN: header matches; verify transfer and recipient';
}
export function run() {
  const agreed = { network: 'eip155:0', payer: 'DEMO_PAYER', amount: '1000000' }; // fictional network and atomic units; do not send this
  const sample = { success: true, transaction: 'DEMO_TX', ...agreed };
  const { amount, ...withoutAmount } = sample;
  const { payer, ...withoutPayer } = sample;
  return [
    checkSettlement(sample, agreed),
    checkSettlement({ ...sample, amount: '2000000' }, agreed),
    checkSettlement(withoutAmount, agreed),
    checkSettlement(withoutPayer, agreed),
    checkSettlement({ success: false, errorReason: 'settlement_pending', transaction: 'DEMO_TX' }, agreed),
    checkSettlement({ ...sample, transaction: '' }, agreed),
    checkSettlement({ success: false, errorReason: 'SAMPLE_REJECTION' }, agreed)
  ];
}
if (typeof process !== 'undefined' && process.argv[1]?.endsWith('/receipts-and-how-to-verify-spend.mjs')) console.log(run().join('\n'));
Save sample module
The sample has not run yet.

Check yourself

The header says success. Has this checker independently verified the transfer? [1]

Read the explanation

It compares fields without reading a chain. [1]

The sample receipt hash matches. Has it independently verified provider usage? [2]

Read the explanation

The keeper reports provider usage with the receipt hash. [2]

A response says settlement_pending and includes a transaction. What next? [1]

Read the explanation

Pending is non-terminal and may still confirm. [1]

Sources

  1. x402 evidence baseline, 3 November 2026. Version 2 specification at the pinned commit, EVM exact, ERC-3009.
  2. quotum receipts, seat keys and Pay as you go. Site copy captured 5 November 2026.

Written by the team that sells capped inference keys. Check the cited docs and the chain yourself.