DEV Community

John O'Malley Jr
John O'Malley Jr

Posted on

How to verify an x402 payment actually settled on-chain

How to verify an x402 payment actually settled on-chain

When an API endpoint charges an AI agent per call, somebody eventually has to answer a simple question: did this payment actually happen?

A dashboard saying "paid" is not an answer. A settlement notification is not an answer. Both are claims — someone, or some system, saying a payment happened. Claims can be early, replayed, mismatched, or just wrong. If you run a paid endpoint, you want proof, per payment, that you can check yourself. Here is the check, in order.

What does "settled" mean for an x402 payment?

An x402 payment on Base, in the EIP-3009 form, is an authorization, not just a transfer. Five things have to line up:

  • Payer: the wallet that authorized the payment.
  • Payee: the wallet that should receive it.
  • Amount: the exact USDC amount authorized.
  • Nonce: a one-time authorization identifier. EIP-3009 authorizations are single-use — once the nonce is consumed, that authorization cannot be spent again.
  • Validity window: the time range (validAfter to validBefore) during which the authorization may be used.

"Settled" means all five line up on-chain: the right payer authorized the right amount to the right payee, the nonce was actually consumed, and it happened inside the window. Anything less is a claim, not a settlement.

Check 1: Does the signature recover to the payer?

Take the signed authorization and recover the signer from its signature (ecrecover over the EIP-3009 typed data). The recovered address must equal the authorization's from field.

If it does not, stop. No other check rescues a payment whose authorization was not signed by the supposed payer.

Check 2: Was the nonce actually consumed on-chain?

Query the USDC contract's authorizationState for the payer and nonce. It must read consumed (true).

This is the check most people skip, and it is the one that catches the "it said paid, but nothing moved" case. A signed authorization sitting unspent is not a payment. An unconsumed nonce means the payment has not settled, whatever any notification said.

Check 3: Does the settlement transfer match the authorization?

Find the settlement transaction's USDC Transfer log and compare it, field by field, against the authorization: from, to, and value must match exactly.

A transfer for the wrong amount to the wrong address is not your payment, even if it arrived at about the right time.

Check 4: Did it settle inside the validity window?

Take the settled block's timestamp and confirm it falls inside [validAfter, validBefore].

A payment that settles outside its window is not a valid use of that authorization, even if the money moved.

Four checks, all passing, is a verified settlement. Anything else is failed or unverified — and that distinction should be stored next to the payment, not smoothed over.

Worked example: one real $0.005 settlement, check by check

This is a real settlement from the public log at https://demo.quinnsconveyance.com/insights — a $0.005 USDC payment on Base, payer 0xC3ADe26717A60ff29875E3b17f016c1fC75672B9, payee 0x3a97d5F616D26876020568bD631492F59cFB428C, settled October 19, 2026.

Transaction: https://basescan.org/tx/0x266847d9f6085e6756f04d791c8256c67749e89493a8da5671578caab733988d

  • Check 1, signature: PASS. The signature recovers to the payer.
  • Check 2, nonce consumed: on recheck, PASS. The authorization's nonce reads consumed on-chain.
  • Check 3, transfer match: PASS. The settlement transaction's USDC Transfer log matches payer, payee, and 5000 (the smallest USDC units for $0.005) exactly.
  • Check 4, validity window: on recheck, PASS. The settled block timestamp falls inside the authorization's window.

Verdict on recheck: verified, 4/4.

I am showing the recheck verdict deliberately, because the first pass on this same settlement did not look like that — and that is the next section.

Why a first check can fail and a recheck can pass

On this settlement's first pass, checks 1 and 3 passed, but checks 2 and 4 failed. The payment was real. The money had moved. What had not happened yet was chain-state propagation: verification ran at settlement time, before the chain state it reads had caught up, so the nonce check ran against a chain that had not yet recorded the consumption, and the window check ran against state that was not yet stable.

That pattern repeated across the whole batch. In the first pass over 16 logged settlements, 11 came back failed, 1 unverified, and 4 verified — and in every one of the 12 non-verified rows, exactly the same two checks failed (authorizationState and window), while signature and transfer match passed every time. There was not a single first-pass signature failure or transfer mismatch in the set. On recheck, all 16 verified, 4/4.

That is why verification cannot be a one-shot at settlement time. Quinn's Conveyance runs an automatic recheck at +10 minutes, and stores the recheck result alongside the first pass. A system that only checks once, immediately, will mark real payments as failed — and a system that quietly re-runs until it passes, without keeping the first result, will hide the timing problem entirely. Keep both results. The first pass tells you about timing; the recheck tells you about truth.

What to do when a check fails

Do not silently accept the payment, and do not silently drop it either. Record it as failed or unverified, surface it, and let a human (or a recheck) decide.

The rule I use, and the one I argued for publicly in my earlier verifier post: fail-open, but loud. Verification never blocks a settlement — if the verifier is down, the payment still lands — but it is recorded as unverified, permanently, not passed in silence. "Unverified" is honest. Silence is not.

And the same rule applies to the status convention itself: every settlement should carry a status — verified, failed, or unverified. Statuses, yes. Permission, no. A status is a trust signal; making it a gatekeeper would make the verifier a single point of failure for the whole ecosystem.

FAQ

Is a 402 response proof of payment?
No. A 402 response is a payment request. It proves an endpoint asked to be paid, not that it was.

Is a facilitator's confirmation enough?
It is a claim to verify. A facilitator saying "settled" is useful, but it is the same kind of statement as a dashboard saying "paid." Run the four checks against the chain before you count the revenue.

How long should I wait before rechecking?
Long enough for chain state to propagate. Quinn's uses +10 minutes for its automatic recheck. The exact number matters less than the principle: check once at settlement, then check again after the state you are reading has had time to settle. If the second check passes and the first did not, you had a timing problem, not a fraud problem — and you now have the record to prove which one it was.

Can I verify without a transaction hash?
Partially. Without a tx hash, checks 3 and 4 (transfer match and window) cannot run, and the verdict rests on signature recovery plus nonce consumption. That is still much better than a notification — but it is not a full verification. Get the tx hash when you can.

Try it on a real log

The public settlement log, with its verify_status and Recheck columns, is live at https://demo.quinnsconveyance.com/insights. Pick any row, run the four checks above against its Basescan link, and see whether you land on the same verdict. That is the whole point: verification you can redo yourself is the only kind worth trusting.

— John O'Malley, Quinn's Conveyance. I install this verification layer, alongside settlement logging, on paid API endpoints — john@quinnsconveyance.com.

Top comments (0)