Developers

XGateway runs as a reverse proxy in front of your API. It answers unpaid requests with an x402 payment challenge, verifies the buyer's USDC payment on Base, and forwards paid requests to your API unchanged. Your API needs no billing code.

Building an agent? Give it this file.

A plain-text, machine-readable reference for paying an XGateway-protected API: parsing the 402, signing the payment, the headers, the networks and the error codes.

agent-reference.txt

How a paid request flows

  1. 1 Request. The buyer calls your route on XGateway, without payment.
  2. 2 402 challenge. XGateway replies 402 Payment Required with the price, the asset, the network and where to pay.
  3. 3 Authorise. The buyer signs an EIP-3009 USDC transfer authorisation. No gas: the facilitator submits it.
  4. 4 Pay. The buyer resends the request with the signed payment in the X-PAYMENT header.
  5. 5 Verify and forward. XGateway verifies and settles the payment, then forwards the request to your API.
  6. 6 Response. Your API's response goes back to the buyer. On fixed-price routes it also carries a session token in X-Payment-Response.

Pricing modes

Fixed price

A fixed USDC price per call, sold in bundles. One payment buys a bundle of calls and returns a JWT (in X-Payment-Response); the buyer sends it as Authorization: Bearer <jwt> on later calls until the credits or the session time run out, then pays again.

Value-based

A percentage of the value your API returns. XGateway forwards the request first, reads the value from the JSON response field message.txValueUSD, and quotes the fee in the 402. The response is held in escrow and released once the buyer pays. One payment per request.

Routes can also be passthrough (free), for endpoints you want behind the same gateway without a charge.

Payments: USDC on Base

Buyers pay in USDC with EIP-3009 TransferWithAuthorization signatures, so they only need USDC, not ETH for gas. Amounts in the 402 are in atomic units (6 decimals: 50000 = $0.05).

NetworkCAIP-2USDC contract
Base (mainnet)eip155:84530x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
Base Sepolia (testnet)eip155:845320x036CbD53842c5426634e7929541eC2318f3dCF7e

The 402 challenge

What an unpaid request gets back (testnet example):

HTTP/1.1 402 Payment Required
Content-Type: application/json

{
  "x402Version": 2,
  "error": "X-PAYMENT header is required",
  "accepts": [{
    "scheme": "exact",
    "network": "eip155:84532",
    "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
    "amount": "50000",
    "payTo": "0xMerchantWallet...",
    "maxTimeoutSeconds": 120,
    "extra": { "name": "USDC", "version": "2" }
  }]
}

extra.name and extra.version are the USDC contract's EIP-712 domain; use them as given (they differ between networks).

Minimal example (Node.js, ethers v6)

Pay a 402 and call the route. Save as an ES module (agent.mjs); req is accepts[0] from the 402.

import { ethers } from "ethers";
const wallet = new ethers.Wallet(PRIVATE_KEY);

const chainId = Number(req.network.split(":")[1]);
const nonce = ethers.hexlify(ethers.randomBytes(32));
const validBefore = Math.floor(Date.now() / 1000) + 120; // compute once: signed and sent values must match

const signature = await wallet.signTypedData(
  { name: req.extra.name, version: req.extra.version, chainId, verifyingContract: req.asset },
  { TransferWithAuthorization: [
    { name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" },
    { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" },
  ]},
  { from: wallet.address, to: req.payTo, value: req.amount, validAfter: 0, validBefore, nonce }
);

const authorization = { from: wallet.address, to: req.payTo, value: req.amount,
  validAfter: "0", validBefore: String(validBefore), nonce };
const header = btoa(JSON.stringify({ x402Version: 2, scheme: "exact", network: req.network, payload: {
  x402Version: 2, payload: { authorization, signature },
  accepted: { ...req, extra: { ...req.extra, assetTransferMethod: "eip3009" } } } }));

const resp = await fetch(URL, { headers: { "X-PAYMENT": header } });
const receipt = JSON.parse(resp.headers.get("X-Payment-Response")); // { token, transactionId }

Error codes

402
Payment required: read accepts and pay.
401
The session JWT expired: pay again.
400
Malformed X-PAYMENT header.
403
Insufficient USDC or an invalid signature.

See it work

Run a paid call against a sample endpoint on Base Sepolia.

Try it on testnet