Skip to content

Latest commit

 

History

History
129 lines (99 loc) · 6.91 KB

File metadata and controls

129 lines (99 loc) · 6.91 KB

x402 — agent payments (#4526)

← Back to README

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.

Seller — charge for an endpoint

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.from

Per request: no X-PAYMENT402 with { x402Version, error, accepts:[requirements] } (the terms); X-PAYMENT present → facilitator.verifysettle → 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.

Buyer — pay for a resource (guardrails-first)

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.

The guardrails are not optional theater

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.
  • X402SpendPolicy is checked before any signature. maxValuePerPayment bounds the blast radius; allowedPayTo / allowedNetworks pin the counterparty; confirm adds 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.

Sandboxes / testnets

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 HttpFacilitatorClient field names (isValid / payer / success / transaction) follow the x402 facilitator REST spec — verify them against your chosen live facilitator's responses before production.

Hermetic test example

// 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.

Posture summary

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

Not yet

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).

Related

  • agui.md / a2a.md — serve surfaces you can put a payment gate in front of.
  • PRD §12.8 — the design rationale and threat model.