Securing your x402-enabled MCP server.
Payment gating removes some attack surface — no API keys to leak — and adds new surface of its own. Replay attempts, verification shortcuts, and paid-but-abusive traffic all need handling on top of the protocol.
x402 gets one big thing right by construction: the agent pays before your handler runs, settlement is USDC on Base in about two seconds, and there are no chargebacks to claw revenue back later. But the protocol secures the payment, not your server. What you build around it decides whether someone can replay an old payment, skip verification, or pay a cent to burn dollars of your compute.
This guide covers the four layers that matter for a payment-gated MCP server, in the order attackers will probe them.
Verify settlement, never just the header
The classic x402 implementation bug is treating the presence of a payment payload as payment. A request carrying a well-formed X-PAYMENT header has proven nothing — the authorization must be verified and settled through your facilitator before any work happens.
Structure your code so the handler is unreachable without a settlement result. If the SDK or middleware gates the route, don't add side doors: no debug paths, no health-check variants that happen to execute the tool, no 'verify later' queue. Paid-then-served must be the only order of operations.
Reject replays explicitly
A settled payment authorizes exactly one execution. If your server doesn't track which payments it has already honored, a caller can resubmit the same proof and get free calls — your facilitator settles the transfer once, but your application is what decides how many responses that transfer buys.
Record an identifier for every payment you accept and refuse duplicates. Payment authorizations also carry validity windows; enforce them so a captured payload from last week is dead on arrival.
const seen = new Set<string>(); // use Redis with a TTL in production
export async function settleOnce(paymentId: string, settle: () => Promise<Receipt>) {
if (seen.has(paymentId)) {
throw new PaymentError(402, "payment already redeemed");
}
const receipt = await settle(); // facilitator verifies + settles on Base
seen.add(paymentId); // mark redeemed only after settlement
return receipt; // Ed25519-signed, returned to the caller
}Rate limit even paying callers
Payment is not authorization to flood. At $0.01 per call, an attacker can spend $10 to fire a thousand requests at your most expensive tool — if each call costs you more in upstream API fees or compute than it earns, paying traffic becomes a funded denial-of-wallet attack.
Apply per-wallet and global rate limits behind the paywall, and check that every tool's price clears its worst-case cost, not its average. Your slowest, most expensive tool is the one that needs the price margin and the tightest concurrency cap.
Keep standard MCP hygiene under the paywall
Payment gating doesn't sanitize inputs. Tools that touch filesystems, databases, or shells still need strict input validation and least-privilege credentials — a paying caller can inject a malicious path or query as easily as a free one, and they've only spent a cent for the attempt.
Isolate upstream secrets too. Your server holds the API keys for whatever it wraps; scope them tightly so a compromised tool can't read more than it serves, and never echo configuration or key material in error responses to paying callers.
Keep receipts; they're your audit trail
Every settled call produces an Ed25519-signed receipt, which means disputes are resolvable with cryptography instead of logs-versus-logs arguments. Store receipts alongside your request logs so you can correlate any response you served with the payment that bought it.
That trail also catches your own bugs: if receipts and served responses ever diverge in count, you've found either a replay slipping through or a handler running unpaid — both worth an immediate look.
FAQ
Can someone forge a payment and call my server free?
Not if you verify through a facilitator before serving — forging a USDC transfer authorization would require the payer's key, and settlement on Base is checkable. The realistic risks are implementation gaps on your side: serving before settlement completes, or honoring the same settled payment twice.
Why do I need rate limiting if every call is paid?
Because price and cost aren't the same number. If a call costs you more to serve (upstream fees, heavy compute) than it earns, an attacker can pay the minimum $0.01 per call and still drain you. Limits per wallet and per tool keep paid abuse bounded while you notice and reprice.
Do chargebacks change the threat model?
They remove one threat entirely. USDC settlement on Base is final — there's no payment-dispute channel where someone consumes your service and reverses the charge later. The flip side is that your remaining risks are all technical: verification, replay, and abuse of what a paid call can do.
What should I log for a payment-gated server?
At minimum: the payment identifier, the signed receipt, the tool called, and the caller's wallet address, correlated per request. That set lets you prove what was paid for, detect replays, spot a single wallet hammering one tool, and reconcile your console revenue against on-chain settlement.
Gate it properly.
Put x402 in front of your MCP server with settlement verified before the handler runs.