x402 revives HTTP 402 Payment Required for gasless stablecoin
(USDC) payments. Unlike MCP / A2A / AG-UI (no money), x402 is a settlement layer that sits beneath them.
Agents.KT ships both halves, both experimental:
- Seller (
X402PaymentGate, #4527) — an agent gets paid: gate a served endpoint behind payment. The agent holds no key and takes no custody. - Buyer (
X402Client+X402Account, #4528) — an agent autonomously pays for a resource. This is where irreversible money moves, so it is guardrails-first.
⚠️ Test on a testnet first. Everything below works unchanged on Base Sepolia with fake USDC — see Sandboxes / testnets. Point it at mainnet only when you mean it.
val gate = X402PaymentGate(
PaymentRequirements(
network = "base", maxAmountRequired = "10000", // atomic units; USDC = 6 decimals -> 0.01 USDC
payTo = "0xSellerPublicAddress", // public recipient, NOT a secret
asset = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
resource = "/premium",
extra = mapOf("name" to "USD Coin", "version" to "2"),
),
facilitator = HttpFacilitatorClient("https://x402.org/facilitator"),
)
// front any JDK HttpHandler …
httpServer.createContext("/premium", gate.gate(downstreamHandler))
// … or any agents.kt serve surface:
AgUiServer.from(agent, payment = gate).start() // also NlWebServer.from / A2AServer.fromPer request: no X-PAYMENT → 402 with { x402Version, error, accepts:[requirements] } (the terms);
X-PAYMENT present → facilitator.verify → settle → set X-PAYMENT-RESPONSE → serve. Fails closed —
any failure (invalid payment, settle error, unreachable facilitator) returns 402 and never serves unpaid. The
seller never runs a custodial settler; a hosted FacilitatorClient does the EIP-712/3009 verify + on-chain
settle.
val account = X402Account.fromPrivateKey(
privateKeyHex = System.getenv("X402_KEY"), // below the model layer — never in a prompt
policy = X402SpendPolicy(
maxValuePerPayment = BigInteger.valueOf(10_000), // hard cap per payment (atomic units)
allowedPayTo = setOf("0xKnownSeller"), // pin recipients (neutralizes redirected-payTo injection)
allowedNetworks = setOf("base", "base-sepolia"),
confirm = { plan -> humanApproves(plan) }, // optional HITL gate; sees network/payTo/value/resource
),
)
val client = X402Client(account)
val resp = client.get("https://seller.example/premium") // 402 -> sign -> retry -> 200
// read the settlement receipt: resp.headers().firstValue("X-PAYMENT-RESPONSE")What happens on a 402: the client parses the seller's accepts[], picks the first offer the policy
permits (supported scheme, known token domain + chainId, within caps), builds an EIP-3009
transferWithAuthorization, signs the EIP-712 digest, and replays the request with a base64 X-PAYMENT
header. Nothing acceptable → X402PaymentDeniedException listing why each offer was skipped — no signature,
no money moved.
x402 moves irreversible money and the canonical failure is a prompt-injected agent draining a wallet (Grok/Bankr ≈ $150–200k, Freysa $47k are confirmed-real). So:
- The key lives in
X402Account, constructed in operator code — never serialized, logged, or placed in a prompt. The LLM drives the request; it cannot read the key or widen the policy. X402SpendPolicyis checked before any signature.maxValuePerPaymentbounds the blast radius;allowedPayTo/allowedNetworkspin the counterparty;confirmadds a human in the loop.- The buyer is gasless — EIP-3009 means the facilitator submits the tx and pays gas, so a wallet needs USDC but not necessarily native gas.
Signing is real secp256k1 + Keccak-256 + EIP-712 (BouncyCastle; no web3j/kethereum), pinned byte-for-byte
against ethers.js vectors.
Three escalating levels — you can test the entire signing path for real without spending anything:
| Level | Money | How |
|---|---|---|
| Hermetic | none, no chain | Inject a fake FacilitatorClient (e.g. one that ecrecovers the signature locally). The full buyer→402→sign→verify loop runs with zero network. This is how the test suite proves the loop. |
| Testnet | fake (testnet USDC) | network = "base-sepolia", USDC 0x036CbD53842c5426634e7929541eC2318f3dCF7e, facilitator https://x402.org/facilitator (free) or the Coinbase CDP facilitator. X402Account already maps base-sepolia → chainId 84532. Get testnet USDC from a faucet. |
| Mainnet | real USDC | A mainnet facilitator + network = "base". The experimental, guardrails-gated path. |
External facilitators (June 2026):
https://x402.org/facilitator— free community testnet facilitator for Base Sepolia (eip155:84532).- Coinbase CDP facilitator — testnet + mainnet; 1,000 tx/month free, then $0.001/tx (gas billed separately).
The
HttpFacilitatorClientfield names (isValid/payer/success/transaction) follow the x402 facilitator REST spec — verify them against your chosen live facilitator's responses before production.
// A fake facilitator that does the real cryptographic check, with no chain and no money:
class RecoveringFacilitator(val expected: String) : FacilitatorClient {
override fun verify(header: String, req: PaymentRequirements): FacilitatorVerification { /* ecrecover signer */ }
override fun settle(header: String, req: PaymentRequirements) = FacilitatorSettlement(success = true, /* … */)
}
// stand up X402PaymentGate(req, RecoveringFacilitator(buyerAddr)) and drive X402Client(account) at it ->
// a genuine signature flows buyer -> 402 -> sign -> seller -> verify -> 200, entirely in-process.Seller (X402PaymentGate) |
Buyer (X402Client / X402Account) |
|
|---|---|---|
| Holds a key? | No | Yes — in X402Account, below the model layer |
| Custody? | No (hosted facilitator settles) | No (gasless EIP-3009; facilitator settles) |
| LLM touches money? | No (HTTP-layer gate) | No (drives the request; can't sign or widen policy) |
| Failure mode | Fails closed → 402 |
X402PaymentDeniedException → no payment |
Scoped ERC-4337 session keys (on-chain caps — the strongest guardrail), the upto metered scheme, Solana,
cross-payment velocity limits, and an agent-tool wrapper (payForResource).