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 API calls.


1. Why look at x402?

Autonomous agents frequently need to consume paid services — LLMs, data feeds, compute — without a human in the loop to manage subscriptions or API keys. Traditional approaches (API keys, OAuth, prepaid balances) either require out‑of‑band account management or add latency because the agent must first query a billing service.

The x402 proposal repurposes the HTTP status code 402 Payment Required to turn every request into a self‑contained payment negotiation. If the client can satisfy the payment challenge, the server immediately serves the resource; otherwise the client receives a clear, machine‑readable instruction on how to pay.

Because the protocol lives entirely in HTTP headers, it works with any language, any transport (including HTTP/2 or QUIC), and does not require a separate billing microservice. The downside is that the agent must be able to sign and broadcast a blockchain transaction, which adds complexity and depends on chain finality.


2. The x402 flow in a nutshell

Step Actor Action
1 Client Sends a normal GET/POST to the protected endpoint.
2 Server If no valid payment proof is present, returns 402 with a Payment-Required header that contains a JSON‑encoded invoice: amount, asset, chain, destination, nonce, expiry, and a server signature.
3 Client Parses the invoice, builds and signs a transaction that pays the exact amount to the destination, waits for inclusion (or a configurable confirmation depth), then extracts the transaction hash.
4 Client Retries the original request, adding a Payment header whose value is the transaction hash (or a signed receipt).
5 Server Verifies the payment on‑chain (matching amount, asset, destination, nonce, and that the transaction is sufficiently confirmed). If valid, returns 200 and the requested data; otherwise another 402 with an error code.

The protocol is deliberately stateless on the server side: the nonce prevents replay attacks, and the signature guarantees the invoice really came from the claimed service.


3. Minimal server implementation (Node.js + Express)

Below is a compact, production‑ready sketch that you can drop into an existing Express app. It assumes you have a wallet whose private key is stored in an environment variable (SERVER_PRIVATE_KEY) and that you want to accept USDC on Base (Chain ID 8453).

// x402-server.js
require('dotenv').config();
const express = require('express');
const { ethers } = require('ethers');
const crypto = require('crypto');

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

// Configuration
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // Base USDC
const CHAIN_ID = 8453;
const SERVER_ADDRESS = new ethers.Wallet(process.env.SERVER_PRIVATE_KEY).address;

// Helper: create a deterministic nonce per (path, method, ip)
function makeNonce(req) {
  const hash = crypto.createHash('sha256')
    .update(`${req.method}|${req.path}|${req.ip}`)
    .digest('hex');
  return `0x${hash.slice(0, 16)}`; // 8‑byte hex
}

// Helper: sign the invoice
async function signInvoice(invoice) {
  const wallet = new ethers.Wallet(process.env.SERVER_PRIVATE_KEY);
  const msgHash = ethers.utils.solidityKeccak256(
    ['address', 'uint256', 'address', 'uint256', 'bytes32', 'uint256'],
    [
      invoice.destination,
      invoice.amount,
      invoice.asset,
      invoice.chainId,
      ethers.utils.keccak256(ethers.utils.toUtf8Bytes(invoice.nonce)),
      invoice.expiry,
    ]
  );
  const signature = await wallet.signMessage(ethers.utils.arrayify(msgHash));
  return signature;
}

// Middleware that enforces x402
async function requirePayment(req, res, next) {
  const auth = req.headers['payment'];
  if (auth) {
    // Verify payment proof
    const txHash = auth.trim();
    try {
      const provider = new ethers.providers.JsonRpcProvider(
        process.env.BASE_RPC_URL
      );
      const tx = await provider.getTransaction(txHash);
      if (!tx) throw new Error('tx not found');
      const receipt = await tx.wait();
      if (receipt.status !== 1) throw new Error('tx failed');

      // Basic on‑chain checks
      if (tx.to.toLowerCase() !== SERVER_ADDRESS.toLowerCase())
        throw new Error('wrong recipient');
      if (tx.chainId !== CHAIN_ID) throw new Error('wrong chain');
      const [, , asset, amountStr] = await provider.call({
        to: USDC_ADDRESS,
        data: '0x70a08231000000000000000000000000' + tx.from.slice(2).padStart(64, '0'),
      });
      // In a real service you'd decode the ERC‑20 transfer event; omitted for brevity.
      // Here we trust the amount matches the invoice (the client must include it).
      // For brevity we skip full ERC‑20 verification; production code should verify.
      return next(); // payment ok
    } catch (e) {
      return res.status(402).json({ error: 'invalid payment', details: e.message });
    }
  }

  // No payment proof → issue an invoice
  const nonce = makeNonce(req);
  const expiry = Math.floor(Date.now() / 1000) + 300; // 5 min
  const amount = ethers.utils.parseUnits('0.05', 6); // $0.05 USDC (6 decimals)

  const invoice = {
    destination: SERVER_ADDRESS,
    amount: amount.toString(),
    asset: USDC_ADDRESS,
    chainId: CHAIN_ID,
    nonce,
    expiry,
  };

  const signature = await signInvoice(invoice);
  const paymentRequired = {
    invoice,
    signature,
    // Optional: a human‑readable description
    description: 'Access to AI‑agent endpoint',
  };

  res.set('Payment-Required', JSON.stringify(paymentRequired));
  return res.status(402).send('Payment required');
}

// Example protected route
app.get('/agent/analyze', requirePayment, (req, res) => {
  // Your agent logic goes here
  res.json({ result: 'analysis complete', timestamp: Date.now() });
});

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

What this code does

  • Generates a reproducible nonce from the request’s method, path, and client IP to prevent replay.
  • Builds an invoice that specifies the exact USDC amount, the Base contract address, chain ID, nonce, and expiry.
  • Signs the invoice with the server’s ECDSA key so the client can verify authenticity.
  • On receipt of a Payment header, it extracts the transaction hash, fetches the transaction and receipt from an RPC node, and performs minimal sanity checks (recipient, chain, status). A production implementation would also decode the ERC‑20 Transfer event to confirm the amount and asset.
  • Returns 402 with a JSON‑encoded Payment-Required header when no proof is present.

4. Client‑side handling (Python + viem)

Below is a minimal autonomous‑agent snippet that calls the protected endpoint, interprets the 402 response, builds and signs a USDC transfer on Base using a private key, and retries the request.


python
# x402-client.py
import os, json, time, requests
from eth_account import Account
from eth_account.messages import encode_defunct
from web3 import Web3

BASE_RPC = os.getenv("BASE_RPC_URL")
USDC = Web3.to_checksum_address("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")
PRIVATE_KEY = os.getenv("AGENT_PRIVATE_KEY")   # agent's wallet
ACCOUNT = Account.from_key(PRIVATE_KEY)
WEB3 = Web3(Web3.HTTPProvider(BASE_RPC))

def build_usdc_transfer(to, amount_wei):
    """
    Returns a raw ERC‑20 transfer transaction (E
Enter fullscreen mode Exit fullscreen mode

Top comments (0)