DEV Community

Nikhil Ranka
Nikhil Ranka

Posted on

x402 Explained: HTTP-Native Micropayments for AI Agents (With Real Code)

x402 Explained: HTTP‑Native Micropayments for AI Agents (With Real Code)

Target audience: developers who are building autonomous AI agents and need a lightweight, standards‑based way to charge for each API call.


1. Why look at x402?

AI agents frequently call micro‑services: a language model endpoint, a vector search, a tool that runs a short script, etc. Charging per‑call with traditional invoicing or API‑key tiers adds operational overhead and forces the agent to maintain a balance off‑chain.

x402 repurposes the existing HTTP 402 Payment Required status code to convey a machine‑readable payment request directly in the response headers. The agent can then:

  1. Read the required amount, token, and network from the header.
  2. Sign a blockchain transaction that pays the service provider.
  3. Resend the original request with proof of payment.

All of this happens over plain HTTP/TLS; no new transport protocol is needed. The approach works for any EVM‑compatible chain (including Base) and any ERC‑20 token, making it a good fit for USDC‑based micropayments.


2. The x402 Payment Header

When a server wants to request payment, it returns 402 and includes a Payment header whose value is a JSON object:

{
  "scheme": "erc20",
  "network": "base",
  "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
  "amount": "0.01",          // exact amount required for this call
  "maxAmount": "0.10",       // optional upper bound to protect against over‑charging
  "payee": "0x1111…dead",    // service provider's wallet
  "timeout": 30              // seconds the client has to settle
}
Enter fullscreen mode Exit fullscreen mode

Fields explained

Field Meaning
scheme Payment mechanism – erc20 for ERC‑20 tokens, native for ETH, etc.
network Chain identifier (e.g., base, ethereum, polygon).
asset Token contract address (or "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeee" for native ETH).
amount Exact amount the server expects, in the token’s smallest unit (here USDC has 6 decimals, so "0.01" = 10 000 units).
maxAmount Optional ceiling; the client may refuse to pay more.
payee Destination address that must receive the funds.
timeout How long the client has to complete the payment before the request is considered stale.

If the client already paid, it can resend the request with an X-Payment header containing the transaction hash (or a signed receipt). The server verifies the hash on‑chain and, if valid, processes the request and returns a normal 200.


3. Honest Trade‑offs

Aspect Benefit Cost / Limitation
Latency One extra round‑trip (402 → pay → retry) adds ~200‑500 ms on Base, plus wallet signing time. May be unacceptable for ultra‑low‑latency loops (e.g., real‑time control).
UX for agents Fully programmatic; no UI redirects. Agents must hold a private key and have access to an RPC endpoint.
Gas cost Paying a tiny amount of USDC means the gas fee is paid in ETH on Base (currently ~$0.0001 per tx). The gas cost can exceed the service price for sub‑cent calls, making the model uneconomical for very cheap ops.
Reliance on RPC Uses standard JSON‑RPC; any provider works. If the RPC is censored or down, payments fail.
Replay protection The timeout field and optional nonce in the data field prevent old txs from being reused. Developers must still enforce idempotency on the service side.
Regulatory Payments are token transfers; no KYC built‑in. Depending on jurisdiction, facilitating micro‑transactions may trigger money‑transmitter rules.

These trade‑offs are realistic; x402 is not a magic bullet but a pragmatic way to embed settlement into HTTP when the overhead is acceptable.


4. Minimal Working Example

Below are two self‑contained snippets: a Node.js/Express server that protects an endpoint with x402, and a browser‑/Node‑compatible client that uses ethers.js to pay and retry.

Assumptions

  • Base RPC URL: https://base-mainnet.g.alchemy.com/v2/<YOUR_KEY>
  • USDC contract on Base: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 (6 decimals)
  • Service wallet (payee): 0x1111111111111111111111111111111111111111 (replace with your own)
  • Private key for the agent: stored securely in an env var AGENT_PRIVKEY.

4.1 Server (server.js)


javascript
// server.js
import express from 'express';
import { ethers } from 'ethers';

const app = express();
const PORT = 3000;

// Configuration – replace with your own values
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const PAYEE = '0x1111111111111111111111111111111111111111';
const NETWORK = 'base';
const AMOUNT_USDC = '0.01'; // 0.01 USDC per call
const MAX_AMOUNT_USDC = '0.10';

// Helper: format amount as integer units (USDC has 6 decimals)
function toUnits(amountStr) {
  return ethers.parseUnits(amountStr, 6);
}

// Middleware that enforces x402 payment
async function requirePayment(req, res, next) {
  // Look for proof of payment in X-Payment header
  const txHash = req.get('X-Payment');
  if (txHash) {
    try {
      const provider = new ethers.JsonRpcProvider(process.env.BASE_RPC);
      const tx = await provider.getTransaction(txHash);
      if (!tx) throw new Error('Tx not found');
      const receipt = await provider.waitForTransaction(txHash);
      if (receipt.status !== 1) throw new Error('Tx failed');

      // Basic checks: correct token, recipient, amount
      if (tx.to?.toLowerCase() !== USDC_ADDRESS.toLowerCase())
        throw new Error('Wrong token contract');
      // For ERC-20 transfer we need to inspect input data; here we assume a simple transfer
      // In production you'd decode the ERC-20 Transfer event from receipt logs.
      // For brevity we skip full validation – see note below.
    } catch (e) {
      return res.status(402).set('Payment', JSON.stringify({
        scheme: 'erc20',
        network: NETWORK,
Enter fullscreen mode Exit fullscreen mode

Top comments (0)