x402 self-payment guard: 36 of 88,362 settlements
An open x402 pull request would make every facilitator reject a payment when the payer equals the recipient. The rule is cheap and correct. We ran it against the PR's own code, then counted what it would and would not catch in six hours of USDC authorization traffic on Base. It catches 36 settlements out of 88,362.
Volume is the number the agent-payments market quotes about itself. Analysts have already shown that raw transaction counts overstate real demand. In September TRM Labs removed self-payments, one-or-two-payer flows and thin sellers and about half of settled volume fell away. In October Chainalysis published its x402 chapter without saying how it separates agent payments from self-payments. So a protocol-level rule against self-payment looks like a natural fix. PR #3910 proposes exactly that.
We read the PR at head 2e9e6aa and the repository at 5847ee5 (2026-10-28), built a harness around the changed x402Facilitator, and ran a census on Base. Three results. The check has a real gap on the settle-only path. It is trivially bypassed by a second address. And the thing it targets is almost invisible in the data, while a different pattern is not.
What the PR changes
PR #3910 was opened by whawk46 on 2026-10-23. As of 2026-10-28 it is open, has no formal review, and the only comment is a Vercel authorization bot. It touches five files: two spec files and three under typescript/ (the @x402/core facilitator, its unit test and a changeset). It does not touch the Go or Python SDKs, and it does not touch any scheme package.
The spec text adds a new error code, invalid_self_payment, and a new section 10.3. The section says a facilitator MUST reject any verification or settlement where the resolved payer equals paymentRequirements.payTo, compared case-insensitively, across all schemes and networks. The PR body gives two reasons: preventing circular wash volume that "transfers zero net economic value", and blocking "settlement-gated catalog self-indexing abuse". The same section adds four pre-signing rules for facilitator-sponsored transaction groups. Those are a separate topic and we leave them alone here.
The code is a wrapper inside x402Facilitator.verify() and settle(). Each has two checks:
Before the scheme runs
A helper, extractPayloadPayer, looks for a payer in four places: payload.payer, payload.from, payload.authorization.from and payload.authorization.payer. If it finds one equal to payTo, the scheme is never called and the facilitator returns invalid_self_payment.
After the scheme runs
If the scheme returned success, the wrapper compares the payer field of the scheme's own response to payTo. A match overwrites the response with a failure and an empty transaction.
The second check exists because not every payload exposes a payer in those four places. Look at the shipped EVM package. An exact payment through the Permit2 route keeps its payer at payload.permit2Authorization.from, and the facilitator code reads it from there (permit2.ts, line 95 and again at lines 353, 483, 532 and 585). That path is not in the extractor's list. A Solana exact payload is a base64 transaction with no payer field at all. For those, only check 2 can fire.
The harness
We checked out the PR branch, installed @x402/core from the workspace and drove the real x402Facilitator class with a stub scheme. The stub does what the real exact schemes do for payer resolution. It returns the payer from the payload shape the scheme owns, and on settle() it increments a counter to mark that funds would have moved. We ran each case through verify() and through settle() without a prior verify. No network, no chain.
case op scheme.settle calls result
EIP-3009, payer == payTo verify 0 invalid_self_payment
EIP-3009, payer == payTo (mixed case) verify 0 invalid_self_payment
Permit2 exact, payer == payTo verify 0 invalid_self_payment
SVM-shaped (no payer in payload), payer==payTo verify 0 invalid_self_payment
EIP-3009, payer A != payTo B verify 0 ok
EIP-3009, payer == payTo settle 0 invalid_self_payment, tx ""
EIP-3009, payer == payTo (mixed case) settle 0 invalid_self_payment, tx ""
Permit2 exact, payer == payTo settle 1 invalid_self_payment, tx ""
SVM-shaped (no payer in payload), payer==payTo settle 1 invalid_self_payment, tx ""
EIP-3009, payer A != payTo B settle 1 success, tx 0xabc
Read the settle block. For the EIP-3009 route the guard fires before anything is submitted. For the Permit2 route and for a payload with no payer field, the scheme's settle() ran once. Then the wrapper rewrote a success into a failure and dropped the transaction hash. With a real scheme, that means a transfer was submitted and mined, the facilitator paid the gas, and the resource server received success: false and an empty transaction. A seller that follows the protocol withholds the response. A payment exists on-chain that the seller's own records say never happened.
How likely is this? It needs a settle call with no preceding verify. The protocol allows that, and our Sui audit exercised it, but the standard resource-server middleware verifies first, and verify returns invalid_self_payment through check 2 before any money moves. So the exposure is narrow: direct /settle callers and custom servers. The fix is also small. Resolve the payer through the scheme before submitting, or add permit2Authorization.from to the extractor and run the payer comparison inside each scheme's verify step, where the payer is already typed. We note this is a property of the PR as written. Our stub models payload shapes, so a reviewer should reproduce it against the real Permit2 scheme before treating it as confirmed.
The last row is the one that matters for the rest of the post. A payer address that differs from payTo by a single character passes. Anyone who wants wash volume owns two addresses.
Three meanings of "self-payment" in one repository
The repository already uses the term for different things, and the PR does not mention them.
- Algorand exact. The facilitator code validates a zero-amount payment whose receiver equals its sender, commented
self-payment for fee coverage(avm/src/exact/facilitator/scheme.ts, around lines 590 to 608). It does not move value topayTo, so the PR's rule does not apply to it, but a spec that says "self-payment is prohibited" will read badly next to it. - Canton exact. Rule 11 of the Canton scheme spec is a "self-payment guard" with a different test: the proven sender MUST NOT equal the facilitator's
feePayerparty, errorinvalid_exact_canton_self_payment. That protects the facilitator, not the ledger. We covered that scheme in our Canton audit. - Solana exact. The SVM spec says
extra.feePayerMAY equalpayTo, as with merchant-sponsored fees, and describes what the transaction may contain when it does. Fee payer and recipient being the same party is a supported configuration.
None of these is a conflict with #3910, because the PR compares payer to payTo and these compare something else. But the new error name will collide in meaning with the Canton one, and a single generic code with a spec-wide MUST is a weaker contract than the per-scheme rules above. A scheme-scoped sentence, with a defined notion of payer per binding, would fit the existing structure better.
The census
Does self-payment show up in real traffic? We measured it. Method: on Base, USDC's AuthorizationUsed event is emitted by transferWithAuthorization, and it is immediately followed by a Transfer from the same payer. We joined the two, for every block in a window, through eth_getLogs on a public RPC. This is the settlement shape x402 exact uses on EVM. It is not x402-only. Any caller of transferWithAuthorization appears, and we have no facilitator attribution. We treat the result as a census of EIP-3009 settlements on Base USDC, and an upper bound on x402.
Window: Base blocks 53,183,836 to 53,194,635, which is 2026-10-28 03:10:19 to 09:10:17 UTC, almost exactly six hours. The script and raw rows are in our local docs/. Results:
settlements 88,362
distinct payers / payees 5,938 / 3,002
volume 505,882 USDC
payer == payee 36 settlements, 6 addresses, 1.19 USDC
2-cycles (A pays B, B pays A) 4 pairs, 10 settlements, 10.98 USDC
3-cycles 0
payments whose payee also pays 0.2%
payers with a single payee 64.8%
So the self-payment rule would have rejected 36 settlements, 0.04% of the count and 0.0002% of the dollars. The six addresses look like people testing their own setup: round amounts. We cannot tell their purpose from the chain, and that is the point. A rule that costs almost nothing and removes almost nothing is fine to ship, but nobody should expect it to move a market statistic.
The cycle searches say the same thing from the other side. Four pairs of addresses paid each other, ten settlements, eleven dollars. No three-address loop. Circular flow between two or three wallets within a window of hours is not where the volume is.
The pattern the rule cannot see
One pair of distinct addresses accounts for 8,386 settlements, 9.5% of everything in the window, at an average of one US cent each, 83.86 USDC in total. A second pair added 1,211 more. Together, two payer/payee pairs hold 10.9% of the settlements and less than 0.02% of the dollars. We do not call that wash. A single agent can legitimately hammer a single paid API, and the shape fits a polling loop. But if a leaderboard counted settlements, this one pair would be 9.5% of it. The self-payment rule sees none of it, because the two addresses differ.
The payee side is as skewed. The ten largest payees receive 64.1% of settlements. 22.3% of settlements go to payees who were paid by fewer than ten distinct payers in the window, and those same payees hold 61.2% of the dollars. If we also require that the top two payers not exceed 90% of a payee's settlements, 77.7% of settlements and 38.8% of the volume survive. These are our approximations of the screens TRM described, in a six-hour slice, not a reproduction of their method.
Dollars and counts are different markets
The median settlement is 0.02 USDC and the 99th percentile is 5.00 USDC. Yet 194 payments above 100 USDC carry 95.3% of the dollars. The largest single payment, 199,900 USDC, is 39.5% of the whole window by itself. The five largest payees take 89.4% of the volume. Whatever those payments are, they are not one-cent agent calls, and they are probably not x402. A headline "volume" figure built from EIP-3009 events mixes two populations, and the count and the dollar totals are driven by different ones.
What it means for LLM4Agents
Our gateway is a payee. Every 402 we issue names a payTo, and the market will measure us the way it measures every other payee: settlements, distinct payers, concentration. Three consequences follow.
First, the guard is harmless to our real buyers and could bite our own tooling. If a facilitator we use adopts #3910, a test or monitoring script that pays our endpoint from the wallet that is also our payTo would start failing with invalid_self_payment. Synthetic checks should pay from a separate wallet. Bearer-token calls are unaffected.
Second, our own volume claims need the screens the market has already adopted. When we quote activity we should give distinct payers and the largest payer's share next to the settlement count, because the census shows that one pair can be a tenth of the count and under 0.02% of the money. The reverse also applies: one 200k payment is 40% of a six-hour window's dollars.
Third, the real abuse economics sit on our side of the meter. A buyer who splits a workload across many wallets defeats any per-payer limit and any payer-equals-payee rule alike. The defense is metering by what is being consumed, not by who signs. We covered that boundary in the spend-controls audit and in the two-phase gap audit.
Staying on the frontier
Concrete steps, in order.
One. Adopt the cheap guard in our own verification path when we touch it, but do it inside the scheme's verify step where the payer is typed, so Permit2 and Solana payloads are covered and nothing is rewritten after a submit. Watch #3910 for review and for a note on the settle-only path.
Two. Put the payer-diversity screen into our reporting: distinct payers, top-payer share and top-pair share, per window. Re-run the census script monthly. It takes about ten minutes of RPC time for six hours of blocks and costs nothing.
Three. Keep two wallets for our own synthetic traffic and document that the settlement guard may be enforced by the facilitators a buyer uses.
Four. Stop treating payer-equals-payTo as a wash signal anywhere internally. Use pair concentration and one-sided flow instead, and treat anything above a threshold as a question to investigate, not a verdict.
Five. Track the parity gap. The PR changes the TypeScript core only, while the Go and Python SDKs and independent facilitators such as x402-rs would have to implement the MUST on their own. A spec requirement implemented in one language is a weaker guarantee than the spec text suggests. Related reading: how the Bazaar indexes sellers. Its spec leaves cataloguing as an implementation detail, so any ranking built on settlements inherits whatever screens the operator chose.
Pay per call, in stablecoins, over an OpenAI-compatible API
One 402, one signature, 345+ models. No subscription, no minimum.
Register your agent