x402 Vector 3: facilitator gas drain
Sections
- The economics
- The attack: maximum-CU failure
- Flavor A: legitimate-looking transfer that fails post-CU-burn
- Flavor B: valid-but-rejected mint
- Flavor C: ATA derivation mismatch
- Quantification
- Why the spec doesn’t address this
- Mitigations
- What I’d do if I were operating an x402 facilitator
- Bibliography
The x402 protocol has a fee model where the facilitator pays gas. This is the entire UX win — AI agents don’t need SOL to make payments. It’s also the entire economic attack surface.
Post 4 of the x402 attack surface series.
The economics
For each settled x402 transaction, the facilitator pays:
- 5,000 lamports base fee (Solana minimum, ~200).
compute_unit_limit × compute_unit_pricepriority fee (configurable, max 5 microlamports/CU per spec, max 40,000 CU).- Worst-case priority fee: 40,000 × 5 = 200,000 microlamports = 0.0002 SOL ≈ $0.04.
So per tx, facilitator outflow is bounded at ~$0.041. That’s fine for legitimate traffic. It’s not fine when an attacker generates valid-looking PAYMENT-SIGNATUREs at 1000 req/sec.
The attack: maximum-CU failure
Three flavors of failing transaction that maximally hurt the facilitator:
Flavor A: legitimate-looking transfer that fails post-CU-burn
Recall from Vector 2: the client controls the instruction list. Append a custom-program call that:
- Burns 35,000 CU in a no-op loop.
- Asserts
False, failing the entire tx.
Outcome: facilitator pays 5k base + 175,000 microlamports priority = full $0.041. Tx reverts. Merchant gets nothing. Repeat at scale.
Flavor B: valid-but-rejected mint
Specify the wrong mint in the SPL TransferChecked. The instruction validates client-side because the client controls the bytes. The instruction fails on-chain because the mint pubkey doesn’t match the ATA.
Solana’s runtime evaluates the entire instruction before it can detect the failure — so the facilitator pays the full CU consumption.
Flavor C: ATA derivation mismatch
The client supplies a destination ATA that derives from (owner=A, mint=USDC) but specifies (owner=B, mint=USDC) in the instruction. Solana’s transfer_checked verifies the ATA is consistent with the supplied owner+mint and rejects. CU consumed: full instruction cost.
All three flavors share the property: the facilitator’s /verify returns 200 (validators check structure, not bytes), the facilitator pays gas to settle, the tx reverts on-chain, the merchant doesn’t get paid, the attacker has cost the facilitator money for nothing in return.
Quantification
A single attacker on a residential Comcast connection can sustain ~100 req/sec to a single facilitator endpoint. At ~$0.04/tx:
- 100 req/sec × 60 sec/min × 240/min in facilitator gas**
- Across an 8-hour business day: $115,200/day
Multiple attackers behind different IPs (e.g., a botnet of 1000) and the daily cost crosses $100M. Facilitator margins on legitimate x402 traffic are pennies per transaction. A sustained gas-drain attack burns the facilitator’s runway in hours.
Why the spec doesn’t address this
The spec assumes a “trusted client” model. AI agents operate semi-autonomously and don’t have an incentive to attack the facilitator they’re paying — except when they do. Specific incentive structures that make this attractive:
- A competitor (rival facilitator) wants the target out of business.
- A nation-state actor wants to disrupt agentic-economy infrastructure.
- A researcher (this writer) wants to demonstrate the bug.
- An AI agent that’s been adversarially prompted to drain its own facilitator’s funds.
Threat (4) is the one I find most interesting. An LLM that’s been jailbroken via prompt injection could — at no cost to itself — execute the gas-drain attack against the facilitator, which is operationally what x402 was designed to make easy.
Mitigations
In rough order of effectiveness:
- Per-client rate limit on
/settle. The facilitator must enforce N transactions per client-keypair per minute. Default ~10 sounds fine; can be raised for trusted clients via API key. - CU budget cap. Facilitator overrides client’s
set_unit_limitandset_unit_priceinstructions; pins to ≤5,000 CU and 1 microlamport/CU. Reduces worst-case outflow per tx by ~10×. - Pre-flight simulation. Before adding feePayer signature, run
simulateTransaction. If sim returnsErr, reject before paying gas. Shifts cost to a quick simulation call. - Reputation-based throttle. Track each client-keypair’s settlement success ratio. Drop clients with under 50% success rate to a lower rate limit.
- Stake-or-pay deposits. Out-of-band: clients deposit a small SOL bond with the facilitator. Failed transactions debit from the bond. Removes the asymmetric-cost property entirely.
(1) is the bare minimum. (3) is the most operationally complex but also the most thorough. (5) is the Real Fix but requires protocol changes.
What I’d do if I were operating an x402 facilitator
# Pseudocode for the verify+settle endpoint
@app.post("/settle")
async def settle(req: SettleRequest):
client_pk = extract_client_pubkey(req.tx)
# 1. Per-client rate limit
if not await rate_limit.allow(client_pk, max=10, window=60):
return 429, "rate_limited"
# 2. Re-validate (don't trust /verify)
if not validate_tx(req.tx, expected_mint=USDC, max_cu=5_000):
return 400, "invalid_tx"
# 3. Pre-flight simulate
sim = await rpc.simulate_transaction(req.tx)
if sim.err is not None:
# Failed simulation = don't pay gas
return 400, "sim_failed"
# 4. Add feePayer sig + submit
signed = sign_with_feepayer(req.tx)
sig = await rpc.send_transaction(signed)
# 5. Wait for confirmation before returning success
await rpc.confirm_transaction(sig, level="confirmed")
return 200, {"signature": sig}
Cost of all this: ~50ms added latency per settlement, plus one extra RPC call. Worth it.
Bibliography
- Dax911/x402_mal/research/gas-drain-bench/
- Solana Compute Unit Pricing: solana.com/docs
- Cloudflare KV rate limiting patterns
Previous: Partial-signing injection ← · Next: AI-agent wallet drain →