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 building autonomous AI agents who need a lightweight, standards‑based way to charge for individual API calls.


1. Why look at 402 Payment Required?

The HTTP status code 402 Payment Required was reserved in the original spec but never saw widespread use. Recent work (the x402 proposal) repurposes it as a native micropayment layer that lives entirely in the request/response cycle—no separate SDK, no WebSocket handshake, just plain HTTP headers and an on‑chain payment proof.

For AI agents that compose many micro‑services (LLM inference, data fetch, tool execution), x402 offers:

  • Statelessness – each call carries its own payment proof; the server doesn’t need to maintain sessions.
  • Atomicity – the service is only rendered if a valid payment is verified.
  • Chain‑agnostic – any EVM‑compatible chain that supports ERC‑20 tokens can be used; the example below uses USDC on Base.

The trade‑off is that you now need a wallet, a way to sign a payment proof, and you accept the latency and cost of an on‑chain verification step (or a cheap off‑chain verifier). The following sections show a minimal, working implementation and highlight where the complexity appears.


2. The x402 Flow in a Nutshell

Client (AI Agent)                     Server (Service Provider)
----------------                     -----------------------
1. Build request                      |
   - Include `x402-payment` header    |
   - Contains: token, amount, chain,  |
     expiry, signature                |
   ------------------------------------>
2. Verify payment proof               |
   - Re‑compute signed data           |
   - Check token, amount, expiry      |
   - Optionally call a cheap verifier |
   (e.g., a read‑only contract call) |
   <------------------------------------
3. If valid → 200 OK + payload       |
   Else → 402 Payment Required       |
   (with `WWW-Authenticate` header)   |
Enter fullscreen mode Exit fullscreen mode

The x402-payment header is a JSON Web Token (JWT‑like) string that the client signs with its wallet’s private key. The server can verify the signature using the corresponding public key (or an Ethereum address derived from it).


3. Server‑Side Implementation (Node.js/Express)

Below is a complete, runnable example that you can copy into a file server.js and run with node server.js. It assumes you have an Ethereum wallet whose address is the service’s payee (the address that should receive USDC). The verifier uses a simple off‑chain check: the signature must be recoverable to the payee address, and the amount must match what you expect. In production you’d likely call a read‑only contract to confirm the token contract address and decimals, but for a demo the off‑chain check keeps the code short.

// server.js
import express from 'express';
import { ethers } from 'ethers';
import jwt from 'jsonwebtoken'; // we’ll use it only for compact JSON encoding

const app = express();
app.use(express.json());

// -------------------------------------------------------------------
// Configuration – replace with your own values
const PAYEE_ADDRESS = '0xYourServiceAddress'; // must be checksummed
const USDC_ON_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // USDC on Base (mainnet)
const EXPECTED_DECIMALS = 6;
const PRICE_IN_USDC = 0.05; // $0.05 per call
// -------------------------------------------------------------------

/**
 * Verify the x402-payment header.
 * Expected format: base64url(json) where json = {
 *   token: address,
 *   amount: string (in smallest unit, e.g. USDC 6 decimals),
 *   chainId: number,
 *   expiry: unix seconds,
 *   nonce: string (to prevent replay)
 * }
 * The whole object is signed with EIP‑191 personal_sign (`0x19` prefix) by the payer.
 */
function verifyX402Payment(header, req) {
  if (!header) throw new Error('Missing x402-payment header');

  // The header is a single base64url string
  const jsonBuf = Buffer.from(header, 'base64url');
  let payload;
  try {
    payload = JSON.parse(jsonBuf.toString('utf8'));
  } catch (_) {
    throw new Error('Invalid JSON in x402-payment');
  }

  const { token, amount, chainId, expiry, nonce, signature } = payload;
  if (!token || !amount || !chainId || !expiry || !nonce || !signature)
    throw new Error('Incomplete payment payload');

  // Basic sanity checks
  if (token.toLowerCase() !== USDC_ON_BASE.toLowerCase())
    throw new Error('Unsupported token');
  if (chainId !== 8453) throw new Error('Wrong chain (expected Base)');
  if (Date.now() / 1000 > expiry) throw new Error('Payment expired');
  const expectedAmount = ethers.utils.parseUnits(PRICE_IN_USDC.toString(), EXPECTED_DECIMALS).toString();
  if (amount !== expectedAmount)
    throw new Error(`Incorrect amount. Expected ${expectedAmount}, got ${amount}`);

  // Recover signer address from the signed message
  const message = ethers.utils.arrayify(
    ethers.utils.hashMessage(
      ethers.utils.defaultAbiCoder.encode(
        ['address', 'uint256', 'uint256', 'uint256', 'string'],
        [token, amount, chainId, expiry, nonce]
      )
    )
  );
  const recovered = ethers.utils.recoverAddress(message, signature);
  if (recovered.toLowerCase() !== PAYEE_ADDRESS.toLowerCase())
    throw new Error('Signature does not match payee');

  // Optional: nonce replay protection (store used nonces in a short‑lived cache)
  // For demo we skip it; in production use Redis or similar with TTL = expiry - now.
  return true;
}

// -------------------------------------------------------------------
// Middleware that enforces x402 payment
function requireX402Payment(req, res, next) {
  try {
    const auth = req.headers['x402-payment'];
    verifyX402Payment(auth, req);
    // Payment OK – attach verified data if downstream needs it
    req.x402 = { payer: ethers.utils.recoverAddress(
      ethers.utils.arrayify(ethers.utils.hashMessage(
        ethers.utils.defaultAbiCoder.encode(
          ['address', 'uint256', 'uint256', 'uint256', 'string'],
          [auth.token, auth.amount, auth.chainId, auth.expiry, auth.nonce]
        )
      )),
      auth.signature
    )};
    next();
  } catch (err) {
    // 402 response with a WWW‑Authenticate‑style header that tells the client
    // what to include. The spec proposes `x402-challenge`.
    res.set('x402-challenge', `token="${USDC_ON_BASE}",amount="${ethers.utils.parseUnits(PRICE_IN_USDC.toString(), EXPECTED_DECIMALS)}",chainId="8453"`);
    return res.status(402).send('Payment Required');
  }
}

// -------------------------------------------------------------------
// Example service: a trivial LLM‑like echo endpoint
app.post('/echo', requireX402Payment, (req, res) => {
  const { text } = req.body;
  if (!text) return res.status(400).send('{ "error": "missing text" }');
  // In a real agent this would call a model; here we just echo back.
  res.json({ echoed: text, paidBy: req.x402.payer });
});

// -------------------------------------------------------------------
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`x402 demo listening on :${PORT}`));
Enter fullscreen mode Exit fullscreen mode

How to run it

npm init -y
npm install express ethers jsonwebtoken
node server.js
Enter fullscreen mode Exit fullscreen mode

The service now expects every POST to /echo to carry an x402-payment header. If the header is missing or invalid, the client receives 402 Payment Required with a challenge header that tells it what to sign.


4. Client‑Side Usage (AI Agent)

Below is a minimal JavaScript snippet that an autonomous agent could embed in its tool‑calling loop. It uses ethers.js to sign the payload and fetch to make the HTTP request. The agent must control an Ethereum wallet (e.g., a MetaMask‑style provider or a private key) that holds enough USDC on Base to cover the fee.


javascript
// agent-call.js
import { ethers } from 'ethers';

// -------------------------------------------------------------------
// Configuration – replace with your agent's wallet and the service URL
const PRIVATE_KEY = '0xyourAgentPrivateKey'; // NE
Enter fullscreen mode Exit fullscreen mode

Top comments (0)