Guides / How to verify a Solana x402 settlement
How to verify a Solana x402 settlement
An x402 payment on Solana ends with a base58 transaction signature and a claim that money moved. Whether you are the seller who accepted the payment, the buyer who wants a receipt that holds up, or an auditor checking someone else's books, the verification recipe is the same: one RPC call, a null check, and an integer subtraction. This guide walks through the recipe — and the handful of ways it goes wrong.
Start with the signature
When an x402 v2 payment settles, the flow is: the server returns HTTP 402 with a PAYMENT-REQUIRED header (base64 JSON), the client pays and retries with PAYMENT-SIGNATURE, and the server responds 200 with a PAYMENT-RESPONSE receipt. On Solana that receipt carries a base58 transaction signature, 87–88 characters long. On an EVM chain like Base you get a 0x-prefixed 64-hex-character transaction hash instead. The format alone tells you which rail you are looking at — useful when your logs mix both.
The rails also verify differently. On Base, the client signs an EIP-3009 transferWithAuthorization struct rather than a full transaction, and verification means reading ERC-20 Transfer event logs from the receipt; the recipient is the wallet address directly. On Solana, the client built a partially-signed versioned transaction — compute-budget instructions, a TransferChecked, and a memo — that a facilitator co-signed as fee payer and submitted. The funds land not at the payTo wallet itself but at that wallet's USDC associated token account (ATA). That indirection is exactly why the recipe below avoids parsing instructions at all.
The verification recipe
Which getTransaction parameters matter?
One call does the work:
getTransaction(signature, {
encoding: "jsonParsed",
commitment: "confirmed",
maxSupportedTransactionVersion: 0
})
All three parameters are load-bearing. jsonParsed returns token balance entries with readable owner and mint fields. commitment: "confirmed" lets you see the transaction within a second or two of settlement. And maxSupportedTransactionVersion: 0 is required because x402 payloads are versioned transactions — omit it and the node will not return them.
Then three checks, in order. First, the result must not be null — null means the transaction is not visible yet, or the node has pruned it. Second, meta.err must be exactly null; anything else means the transaction landed but failed, and no value moved. Third, the balance delta must check out.
How do you confirm the right amount reached the right wallet?
Diff meta.preTokenBalances against meta.postTokenBalances. Filter both lists to entries where owner equals your payTo wallet and mint equals your pinned USDC mint, then subtract. The net delta, computed as integers in atomic units, must meet the required amount. USDC uses 6 decimals, so an amount of "10000" is one cent. Never compare floats.
Why balance deltas beat parsing instructions
The tempting alternative is decoding the TransferChecked instruction and reading its amount. Resist it, for three reasons.
No ATA math. The on-chain destination is the recipient's associated token account, not the wallet you quoted in PaymentRequirements. Instruction parsing forces you to derive that ATA and match against it. Balance entries already carry the owner field, so you match against the wallet you actually quoted and never touch account derivation.
Token-2022 fees net out. If a mint charges a transfer fee, the instruction's amount is what the sender sent, not what the recipient received. The pre/post delta is, by definition, what actually arrived.
Instruction position is not fixed. An x402 Solana payload carries three to six instructions — the compute-budget pair, the transfer, a memo, and optionally Memo or Lighthouse extras — so an instruction parser has to locate the transfer before it can read it. The balance diff does not care where the transfer sat.
Pin the mint
Why is mint pinning the number one defense?
Anyone can create a token and name it USDC. The name proves nothing; only the mint address does. Circle's USDC mint on Solana mainnet is EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v; on devnet it is 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU. A verifier that matches by symbol, or skips the mint filter entirely, will accept a worthless look-alike as payment. Hard-code the exact mint in your verification path, and pin the network while you are at it: mainnet is CAIP-2 solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp, devnet is solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1. A devnet transaction that verifies cleanly is still worth zero dollars.
Want to see what an endpoint's payment requirements actually decode to before you write a line of verification code? The free x402 Inspector understands both Base and Solana.
Which commitment level should you use?
confirmed lands in roughly one to two seconds, and no optimistically-confirmed Solana block has ever been rolled back. For the small payments x402 is built around, that track record makes confirmed a reasonable operating point, and it is what the recipe above uses. finalized takes about thirteen seconds and gives you the chain's strongest guarantee — the sensible choice when the amount is large or the receipt feeds an audit trail. If you only need a yes/no on landing status without the full transaction, getSignatureStatuses with searchTransactionHistory: true is the lighter call.
RPC pitfalls
Why does verification return null or 403?
Two common causes, neither of them your code. api.mainnet-beta.solana.com blocks datacenter IPs with 403s and is not intended for production traffic — which describes most servers doing verification. And public nodes are non-archival: ask about a signature that is old enough and you get null, indistinguishable from “never happened.”
Two habits follow. Verify promptly, ideally in the same request cycle as settlement. And persist your own receipt — the signature, the parsed deltas, the timestamp — so you never depend on someone else's node remembering your payment. If you genuinely need to verify old signatures, use an archive-backed RPC provider.
The whole recipe in pseudocode
// inputs
sig = "..." // base58 signature, 87-88 chars, from PAYMENT-RESPONSE
payTo = "..." // the wallet quoted in PaymentRequirements
MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" // pinned
required = 10000 // atomic units: "10000" = $0.01 at 6 decimals
tx = rpc.getTransaction(sig, {
encoding: "jsonParsed",
commitment: "confirmed",
maxSupportedTransactionVersion: 0
})
if tx == null: reject("not visible yet, or node pruned it")
if tx.meta.err != null: reject("landed but failed - no value moved")
pre = sum(int(b.uiTokenAmount.amount)
for b in tx.meta.preTokenBalances
if b.owner == payTo and b.mint == MINT)
post = sum(int(b.uiTokenAmount.amount)
for b in tx.meta.postTokenBalances
if b.owner == payTo and b.mint == MINT)
if post - pre >= required: accept() // integer math, never floats
else: reject("wrong mint, wrong wallet, or underpaid")
The filter-then-sum shape handles the edge cases quietly: an account absent from preTokenBalances contributes zero, and a transaction that paid the right wallet in the wrong token nets to zero because the mint filter excludes it.
A note for auditors on raw counts
A verified settlement proves money moved. It does not prove anyone wanted the product. Analyst estimates put Base ahead on cumulative USD value settled while Solana leads on transaction count, and independent analyses estimated that a large majority of peak-period x402 transactions were non-organic — bots and self-dealing. If you are auditing a seller's claimed volume, verify a sample of signatures with the recipe above, but treat raw counts as an upper bound on real demand, not a measure of it.
Or skip all of this
Everything above is what Quartermaster runs so you do not have to. GET /v1/payment-proof ($0.003) takes a base58 Solana signature — or a 0x Base hash — and returns a verdict on whether the settlement is real. GET /v1/settlement-assurance ($0.03) goes deeper for payments you care more about. Prices are the same regardless of rail.
Verify without building a verifier. Point your x402 client at /v1/payment-proof ($0.003) or /v1/settlement-assurance ($0.03) — both accept Base hashes and Solana signatures directly. Agents can reach the same checks, plus 37 more tools, through the Quartermaster MCP server. And the x402 Inspector stays free if you would rather look before you pay.
Last updated 2026-07-17. Written from the x402 specification, the x402-foundation reference implementation, facilitator /supported endpoints, and live Coinbase Bazaar data. Corrections: dev@surewhynot.app.
More guides: Accept Solana USDC in your API with x402 · Base vs Solana for x402 sellers: an honest comparison · Common Solana x402 mistakes (and how to fix them)