Skip to content

Verifying x402 Endpoints: The 5-Check Buyer Recipe

• Updated
14 min read
Markdown Share this post: Share on X Share this post on X Share on LinkedIn Share this post on LinkedIn

Buyer-side verification surfaced all 4 hostile-seller failure modes, 3 caught by preflight checks and the replay blocked by chain state, and flagged 1 of 3 third-party endpoints. The recipe’s first three checks run before any signature exists, so probing a hostile or broken seller costs a request, not money; the last two verify settlement after it. The evidence for the happy path is one confirmed settlement: 0.01 USDC on Base mainnet, verified end to end and reproduced in full below (Table 2).

The seller side of this protocol is already documented in x402 on Cloudflare Workers, which covers the deployment that issues these challenges. This post stays on the buyer side of that boundary: what an agent should verify about a seller before it authorizes a transfer. The economics of why sub-cent settlement matters are in Pay-Per-Call APIs: $0.005 Calls, Measured Treasury Costs; here the question is narrower and safer, namely how to pay only endpoints that behave.

What Can Go Wrong Before an Agent Pays?

Three things can go wrong, and all three happen before the buyer sends anything. First, the challenge can lie: the amount or destination address in the PAYMENT-REQUIRED header can differ from what the seller’s own manifest advertises. Second, the receipt can be incomplete or missing fields, so the buyer cannot prove what happened. Third, the endpoint may not speak the protocol at all, returning an error page where a challenge should be.

An autonomous agent is the worst-case reader of these responses. It has no human to notice that the pay-to address changed, and it will happily sign whatever the header asks for. The failure modes are also silent from the outside: a hostile seller that overcharges by ten times still returns a well-formed HTTP response, so status codes alone detect nothing. Independent academic analyses corroborate the threat model: deployed x402 systems have been shown vulnerable to challenge manipulation, replay, and facilitator trust failures (Li et al. 2026; Wang et al. 2026), which is precisely the gap a buyer-side recipe addresses. The experiments behind this post built a verification harness with three parts: a happy-path run against our own endpoint, a hostile mock seller with four planted failure modes, and a decode-only probe of three third-party endpoints. One disclosure applies throughout: the author operates aptu.dev, so the first experiment verifies our own endpoint, and the third-party results should be read as a snapshot, not a verdict on those operators.

The defense is a verification chain: three checks before any signature exists, two proofs after payment, and every experiment in this post exercises part of it.

Agent Seller Facilitator Chain before any signature (checks 1-3) 1. POST, no payment headers 2. 402 + PAYMENT-REQUIRED 3. decode: v2 shape, amount, payTo (check 1) 4. GET manifest 5. manifest fields 6. cross-check + preflight guard (checks 2-3) after payment (checks 4, then 5) 7. POST + PAYMENT-SIGNATURE (EIP-3009) 8. settle 9. transferWithAuthorization (only spend) 10. Transfer event 11. settlement confirmed 12. 200 + PAYMENT-RESPONSE receipt 13. receipt gate: fields + tx hash (check 4) 14. verify tx hash on 2 RPCs (check 5)
Figure 1: One verification pass across the four actors. The buyer never talks to the facilitator: the signed payment rides to the seller in a header, the seller relays settlement, and the receipt comes back in the paid response. Steps 1 to 6 run before any signature exists; step 9 is the only spend; steps 13 and 14 are checks 4 and 5, receipt gate first, then on-chain proof. Dashed arrows are responses and receipts.

What Does a Valid v2 Challenge Look Like?

A valid x402 v2 challenge is a base64url-encoded JSON object in the PAYMENT-REQUIRED response header. Decoding it is free and takes milliseconds, which makes it the cheapest verification step in the whole recipe. The decoded object must carry version 2, an accepts array, and per-network fields for the asset, the amount in base units, and the destination address.

Two Fields Decide the Payment

Code Snippet 1: The decoded PAYMENT-REQUIRED challenge from the happy-path run, with the two fields a buyer must read before signing.
{
  "x402Version": 2,
  "accepts": [
    {
      "scheme": "exact",
      "network": "eip155:8453",
      "amount": "10000",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "payTo": "0x169313d28075a6dDB70E7e5190a1Eddc0d105671",
      "maxTimeoutSeconds": 60
    }
  ]
}PAYMENT-REQUIRED

The two highlighted fields are the whole decision. payTo is where the money goes, and amount is how much, expressed in base units: 10000 base units of USDC is 0.01 USDC because USDC carries six decimals. If either field is absent, malformed, or surprising, the correct action is to stop. The unpaid decode-only POST in the happy-path experiment took 0.039 seconds, so this check is effectively free. The shape itself is defined by the x402 v2 specification, which fixes the header, the accepts array, and the scheme fields a buyer can rely on.

How Do You Cross-Check the Challenge Against the Manifest?

The manifest is the seller’s public advertisement of what it sells and at what price. The challenge is what the endpoint actually demands at request time. A trustworthy seller keeps these identical, so the buyer should fetch the manifest, decode the challenge, and compare all four payment-bearing fields: amount, payTo, asset, and network. x402 does not standardize where a seller advertises; in our runs the manifest is the seller’s skill.md discovery document, which lists each endpoint’s price and pay-to address, and a buyer needs to know where its target seller makes the same claim.

The mock-seller experiment tested exactly this cross-check by planting mismatches. In the amount-mismatch mode the challenge demanded 99999 base units while the manifest said 10000, a tenfold overcharge. In the payTo-mismatch mode the destination address differed between the two. In the receipt-missing-key mode the settlement receipt came back without a required field. Quoted-versus-charged value consistency is treated as a top-level safety property in formal analyses of agentic payments (Ling et al. 2026), so this check is not paranoia; it is the property the literature says fails first.

Four Planted Defects, Four Catches

Table 1: Mock-seller detection outcomes: buyer-side cross-checks caught the three planted challenge and receipt defects, the same-nonce replay was blocked by chain state, and the honest baseline passed.
ModeDetectedExpectedWall clock
baselineno (correct)no1.2 s
amount-mismatch (99999 vs 10000)yesyes1.1 s
payTo-mismatchyesyes1.1 s
receipt-missing-keyyesyes1.1 s
EIP-3009 replay (same nonce)yes (EVM revert)yes12.4 s

The pattern to internalize from Table 1 is that detection is binary and cheap: each hostile mode either trips a declarative assertion or it does not, and none of the checks require spending anything.

How Do You Verify Settlement Actually Happened?

After paying, the buyer should not trust the seller’s word that settlement occurred. The happy-path run verified the receipt against the chain directly, using two independent RPC endpoints, mainnet.base.org and base.publicnode.com, so no single provider could fabricate a confirmation. The settlement moved exactly 0.01 USDC, 10000 base units, to the challenge’s payTo address from the declared payer (both addresses in Table 2).

The Facilitator Owns tx.from

One detail in that verification matters more than it first appears. The transaction’s from field is the Coinbase facilitator 0xb87e...5860, not the buyer, because the facilitator submits the settlement on the buyer’s behalf. Payer identity therefore comes from the Transfer event’s from argument, not from tx.from. A verification script that reads tx.from as the payer will fail on every facilitator-mediated payment, so this is the single most common mistake to design around.

What Verification Proves, and What It Costs

The paid run completed in 1.11 seconds of wall clock, against 0.039 seconds for the unpaid decode-only POST. That difference is the price of certainty: under a second and a half buys an on-chain, dual-source confirmation of exactly where the money went.

Table 2: Echo-verify assertions, timings, and discovery latency from the happy-path run: settlement of 0.01 USDC verified on two independent RPCs.
CheckOutcome
settlement amount0.01 USDC exactly to challenge payTo (10000 base units)
payer / payTo0xd0D40C485C6878DF38485E71bcc0Ad95Fb97FA5E / 0x169313d28075a6dDB70E7e5190a1Eddc0d105671
settlement tx0xa3143d32319392227c017ce86dceb2a8e377b7e4f32c21f2693a6925e91f201c
paid-run wall clock1.11 s (unpaid decode-only POST 0.039 s)
discovery latency (15 GETs)median 0.0388 s, IQR 0.0315-0.0393 s
receipt + Transfer eventverified on both RPCs; tx.from = Coinbase facilitator 0xb87e1a2cc2b4643f2892768e80e41167f17c5860

Preflight probing is also cheap on the discovery side, where the discovery GET is the unpaid request that fetches the seller’s manifest and challenge. Figure 2 shows the fifteen discovery GET latencies from the happy-path run, every raw observation rendered; the median was 0.0388 seconds with an IQR of 0.0315 to 0.0393 seconds, so the decode-and-cross-check steps are practical even inside a tight agent loop.

Discovery GET latency, 15 observations (seconds) Discovery GET latency (seconds)IQR (n=15) 0.0388 0 0.02 0.04 0.06 0.08 0.10 Discovery GET (n=15)
Figure 2: Fifteen discovery GET latencies from the happy-path run, every raw observation shown. The median of 0.0388 s and IQR of 0.0315 to 0.0393 s are descriptive of a single sample set, not a performance claim.

Can a Signed Payment Be Replayed?

Yes, a signed payment authorization can be replayed by a hostile seller, and the defense is chain state rather than buyer-side code. EIP-3009 payments carry a nonce, and the USDC contract marks an authorization as used once it settles. The mock-seller experiment forked Base at block 54035433 with Anvil and attempted to settle the same nonce twice, a probe aimed at the replay-adjacent weakness class documented against deployed endpoints (Li et al. 2026).

The Contract, Not the Client, Blocks Replay

The second settlement reverted at the EVM state level with the reason FiatTokenV2: authorization is used or canceled, after 12.4 seconds. The replay attempt never produced a second successful transfer, which is the property a buyer needs. The protection lives in the token contract, so it holds even when the seller is the one replaying, and it costs the buyer nothing to benefit from. Two caveats keep this honest: the run was a single trial, and the recorded transaction hash from the fork is a fork-local reference, not a mainnet transaction, so it should not be looked up on a block explorer expecting a match.

What Do Real Third-Party Endpoints Look Like?

The third experiment probed three live third-party x402 endpoints with a hard no-payment guard, sending zero payment-bearing headers. Two of the three returned protocol-compatible v2 challenges, in 0.6 and 0.7 seconds; one diverged with a 404 in 0.4 seconds. No payment was made and no settlement occurred in this experiment, which is the point of a preflight probe.

Table 3: Preflight-external probe outcomes: two of three third-party endpoints returned v2-compatible challenges and one diverged with a 404; no payment-bearing header was sent.
EndpointResultChallenge amount
kaisha (kaisha-api.hp-vladic.workers.dev, GET /resolve)v2-compatible5000 base units (0.005 USDC)
blockrun (blockrun.ai, POST /api/v1/chat/completions)v2-compatible2000 base units (0.002 USDC)
agent402.tools (agent402.tools, POST /api/extract)diverged: 404, no challengen/a

The Divergent Endpoint

The divergent endpoint returned a 404 with no challenge at all, in 0.4 seconds. Its manifest does advertise a pay-to address, 0xaBF4...a9D0 (full address in the evidence repository), but that address is manifest-advertised rather than challenge-issued, so a buyer that trusted the manifest alone would have signed a payment against an endpoint that never issued a valid challenge. A single unretried probe cannot tell you which of the usual causes applies here, an unannounced route deprecation, downtime, or a protocol mismatch, and that limitation is itself the operational point: the manifest tells you what a seller intends to charge, only the challenge tells you what it is asking for right now, and only a fresh probe tells you whether the two still agree. Manifests age out faster than endpoints.

What Is the Minimum Buyer-Side Recipe?

The recipe distills to five checks, in the order an agent should run them. The first three cost nothing but a request and run with no wallet connected; the last two run after payment. Mapped onto Figure 1: checks 1 to 3 run before any signature exists, at steps 3 and 6 of the sequence; checks 4 and 5 are the post-payment proof, at steps 13 and 14, receipt gate first because the on-chain check needs the transaction hash the receipt carries.

  1. Decode the PAYMENT-REQUIRED header and confirm x402 version 2 shape: accepts present, amount and payTo well formed.
  2. Cross-check the challenge against the seller’s manifest: amount, payTo, asset, and network must all match exactly, in base units.
  3. Enforce a no-payment guard during preflight: block any request that would carry a payment signature, an x-payment header, or any header matching the payment prefix pattern.
  4. Validate the settlement receipt before anything else: a missing or malformed receipt, or one lacking required fields such as the transaction hash, marks the seller as failed, and the failure is recorded so the agent does not return.
  5. Once the receipt validates, verify settlement on two independent RPCs: exact amount to the challenge payTo, with payer identity taken from the Transfer event’s from argument, never from tx.from.
Code Snippet 2: The no-payment guard rule that keeps preflight probes free of any payment-bearing header.
const PAYMENT_HEADER_BLOCK = [/^payment-/i, /^x-payment$/i];

export function assertNoPaymentHeaders(headers: Headers): void {
  for (const name of [...headers.keys()]) {
    if (PAYMENT_HEADER_BLOCK.some((re) => re.test(name))) {
      throw new Error(`payment header blocked in preflight: ${name}`);
    }
  }
}preflight-guard.ts

The guard is deliberately blunt, and deliberately broad. The x402 v2 HTTP transport defines exactly three protocol headers, PAYMENT-REQUIRED, PAYMENT-SIGNATURE, and PAYMENT-RESPONSE, and the v1 generation used X-PAYMENT, so the prefix pattern and the exact match together catch every header a standard HTTP client or off-the-shelf x402 SDK could use to carry a signature. A preflight probe should never be one bug away from spending money, so the rule rejects the header names outright instead of trying to sanitize them. Combined with the decode and cross-check steps, it means the first three checks in the recipe run with no wallet connected at all. The remaining two checks run after payment, and they close the loop: a buyer that verifies settlement on two RPCs and treats a malformed receipt as a failure has an evidence trail for every cent that moved.

In an agent loop, the latency budget splits the recipe in two. The first three checks ride on requests the buyer already makes: the unpaid decode-only POST took 0.039 seconds and fifteen discovery GETs showed a 0.0388-second median, so running them synchronously adds under 40 milliseconds per call. Checks 4 and 5 cost 1.11 seconds per settlement, dominated by dual-RPC confirmation with free-RPC receipt lag mitigated by backoff polling; for high-frequency, sub-cent calls those belong in an asynchronous reconciliation worker rather than the request path, with a circuit breaker that blocks a seller whose background settlement check fails, ideally a fleet-wide blocklist so one agent’s failed cent spares the rest of the fleet the probe, while high-value or state-dependent calls can afford to gate on the same checks synchronously. Blast radius stays bounded by the protocol itself: each EIP-3009 authorization covers one transfer of a fixed amount, so a hostile seller can only take what the buyer agrees to sign, and a signing service that enforces a per-session allowance caps total exposure regardless.

Single runs per mode back these numbers, and all timings are descriptive rather than performance claims. The direction of the result is what generalizes: every failure mode a seller can introduce before payment is observable in a response the buyer already has, so verification belongs before the signature, not after the loss.

References


Previous Post
Pay-Per-Call APIs: $0.005 Calls, Measured Treasury Costs

Related Posts