In October 2026, a group of strangers on the internet converged on an uncomfortable fact about the x402 payment protocol — the HTTP-native standard that lets AI agents pay for resources with stablecoins:
The official client will sign whatever amount a server declares, as long as it fits under your spending cap.
Not because of a bug in the signature scheme. Not because of a broken facilitator. Because the client's only defense is a cap — and a cap answers "how much is too much in total", never "is this the price I agreed to".
This is the story of how that gap was found, measured on Base mainnet, cross-checked by three independent implementations, and — within 72 hours — fixed in a PR to the official SDK. It is also the story of why agent payments need a policy layer, and what we are building about it.
The finding
It started with x402 issue #3866: a report that the official TypeScript client, when it receives a 402 Payment Required response, selects a payment requirement, checks it against a per-payment spending cap, and signs — without ever comparing the declared amount to anything the caller actually intended to pay.
The attack doesn't need to be dramatic. A merchant server (or a compromised one, or a malicious proxy rewriting a 402) declares 10× the price mid-session. Your agent's $1 cap happily covers a $0.10 charge for a resource that cost $0.01 yesterday. No human sees a checkout page — there is no checkout page. That is the entire point of agent payments, and exactly why the gap matters.
Three independent implementations verified the behavior within two days:
- Boundra (us) replayed the published S-BS-06 fixture corpus through our policy engine — 6/6 agreement with the control-mode gate.
- Tanilo's x402-guard reproduced it at the hook level with a mocked loopback seller.
- 402Shield mapped it model-to-model against their payment-intent drift approach.
Same verdict everywhere: the cap is not a price check.
Then someone measured mainnet
The strongest moment in the thread came from Agentic Finance Graph, who ran a settlement-side measurement on Base mainnet (Oct 12–23, 2026): across 80 Bazaar-listed sellers with ≥20 settlements each — 32,605 settlements totaling $18,950.78 — they found:
- 1,126 settlements (3.5%) were ≥10× the seller's own median price
- 25 settlements were both ≥100× and ≥$1, totaling $1,928.23
- The largest: $588.00 paid to a seller whose median price is $0.01
Everyone in the thread — including the measurer — is careful to frame this correctly: on-chain, these shapes are indistinguishable from legitimate usage-based pricing, so read it as the ceiling of the overcharge vector, not evidence of exploitation. The 13 payers behind the big cases were single purchases, not loops.
But that ceiling establishes something crucial: amounts far above a seller's usual price settle on mainnet in the normal course of things. If a caller-side price expectation existed, it would have had something to act on. Today, nothing acts.
The fix: expectedAmount in the official SDK
Discussion converged on placement: the check belongs inside the client's spend controls, on the default path, before the signer runs — not in an opt-in hook every developer must remember to register.
So we built it: PR #3920 to x402-foundation/x402 adds expectedAmount to x402Client spend controls:
const client = new x402Client();
client.register("eip155:8453", schemeClient);
client.setSpendControls({
maxAmountPerPayment: "$1", // the cap: maximum exposure
expectedAmount: "$0.01", // the expectation: the price you agreed to
expectedAmountTolerance: 1.5, // optional repricing band (symmetric)
expectedPayTo: "0xSeller...", // optional: caller-approved recipient
});
What it enforces, pre-signature:
-
Amount drift — explicit expectation or
"first-accepted"anchoring; a mid-session hike (or a bait drop beyond the band) throwsQuoteMismatchErrorbefore the signer is called. The cap alone would have signed it. -
Recipient substitution — the first committed
payToper(resource, scheme, network, asset)binds the recipient; a payTo-only change at an unchanged amount is rejected. A substituted recipient with a raised amount cannot re-anchor itself. - Anchor integrity — a rejected quote never replaces the accepted anchor; the original price still pays.
- Fail-closed — an offer that cannot be evaluated (decimal-ledger amount, unknown asset decimals) is rejected explicitly, not silently signed.
-
Provenance — every rejection carries
source: "caller-config" | "quote-ledger"andfield: "amount" | "payTo", so integrators can handle it programmatically instead of parsing messages.
The thread's reviewer community then did something rare and wonderful: they built an A1–A9 acceptance matrix and cross-checked our PR branch empirically — 12 recorded sessions, signer-call counts, per-field fault injection. Round 1: 8/9 pass, recipient binding (A5) the gap. We fixed it the same night. Round 3 review found a concurrency race (two concurrent first calls could both pass an empty-ledger check) and a silent-pass branch; both closed within hours — payload creation is now serialized per client, and unsupported expectations fail closed.
That is what open-source review is supposed to look like. 798 tests passing, verified commits, and a sharper design than any of us would have produced alone.
The cap is the floor. The expectation is the check.
One line from the thread deserves to be a motto:
A spending cap limits the maximum exposure; it does not, by itself, verify that the requested amount matches the caller's intended price.
If you run agents that pay over x402 today, three things you can do right now:
- Set an explicit price expectation where you know the price — once #3920 lands, that's one line in spend controls.
- Watch your settlement trail — compare settled amounts against the seller's median; Agentic Finance Graph offered to re-run the measurement for any specific payTo or threshold.
- Put a policy layer in front of the signer — allow, ask, or veto before anything is signed.
Where Boundra fits
Full disclosure: this is the problem our company exists to solve, and the PR is how we think infrastructure should be built — in the open, at the protocol layer, verified by people who don't work for us.
Boundra is the policy decision point for agent payments. The SDK control handles the amount; Boundra handles everything around it: signed user policies (the root of trust is your key, not our database), per-agent scopes with strictest-wins, an ASK path that escalates to a human signature, spend tracking, and a signed boundra.verdict/v1 receipt for every decision — verifiable offline by anyone, issued by the same key that receives our revenue.
One URL, zero install:
{ "mcpServers": { "boundra": { "url": "https://api.boundra.io/mcp" } } }
No signed policy → default-deny. Our evaluation API is itself an x402 merchant: agents pay $0.003 USDC per semantic evaluation, settled for real on Base mainnet (proof). The business model and the product demo are the same transaction.
The interface is dying; agents are becoming the buyers. The money they carry deserves the same rigor we'd demand of any counterparty — starting with the simplest question of all:
Is this the price I agreed to?
Boundra is open source: github.com/gobeyondfj-cmd/boundra. The replay fixtures, the acceptance-matrix responses, and the SDK PR are all public. Verify everything — that's the point.
Top comments (1)
The cap vs. agreed-price distinction is the whole ballgame, and the mainnet numbers make it concrete: 1,126 settlements at 10x the seller's own median means the intent check was missing in production, not just in theory. expectedAmount with a tolerance band is the right primitive, and putting it on the default path in spend controls is exactly where it belongs, because opt-in hooks are how gaps like this survive in the wild.
The thing I keep coming back to is that this is really two separate state machines: the agent's spending authority and the human's price intent. A cap constrains the first, an expectation encodes the second, and conflating them is what let the client sign anything under the cap. The policy layer needs to sit between proposal and signature, where the amount is compared against something the caller actually committed to, not just an upper bound. First-accepted anchoring is a nice middle ground, though I'd want the anchor explicit in the request rather than inferred from whatever 402 arrives first.
Glad the acceptance matrix made it into the writeup, that level of verification is rare.