Skip to main content

Overview

ouroborai uses the x402 protocol to gate access to agent routes. x402 extends HTTP with a native payment layer built on the 402 Payment Required status code. Every request to /agent/* costs $0.01 USDC on Arbitrum One. This approach removes the need for API key management on agent routes. Instead of signing up for an account and provisioning credentials, you pay per request using an on-chain USDC transfer authorization.
USDC on Arbitrum uses 6 decimal places. The default price of $0.01 is represented as 10000 in raw USDC units.

How it works

When a client makes a request to any /agent/* endpoint without a valid payment, the server responds with HTTP 402 and a JSON body describing what payment is required. The client constructs a signed EIP-3009 transferWithAuthorization payload, attaches it as a base64-encoded X-PAYMENT header, and retries the request.
1

Request without payment

The client sends a request to an agent endpoint. The server returns 402 Payment Required with a JSON body describing the accepted payment schemes, asset, amount, and recipient.
2

Construct payment payload

The client reads the 402 response, builds an EIP-3009 transferWithAuthorization message for the specified USDC amount, and signs it with their wallet.
3

Retry with X-PAYMENT header

The client re-sends the original request with the signed payment encoded as a base64 JSON string in the X-PAYMENT header.
4

Server verifies and processes

The server decodes the header, validates the EIP-712 typed data signature, checks the nonce for replay prevention, and proceeds with the request. A X-PAYMENT-RESPONSE header is attached to the response confirming success.

402 response format

When you call an agent endpoint without a payment header, the server returns this structure:
{
  "error": "Payment required",
  "x402Version": 1,
  "accepts": [
    {
      "scheme": "exact",
      "network": "arbitrum",
      "maxAmountRequired": "10000",
      "resource": "https://api.ouroborai.com/agent/prompt",
      "description": "ArbitrumAgent API — $0.01 USDC per request",
      "payTo": "0xYourFacilitatorAddress",
      "maxTimeoutSeconds": 300,
      "asset": "0xaf88d065e77c8cc2239327c5edb3a432268e5831",
      "extra": {
        "name": "USD Coin",
        "version": "2"
      }
    }
  ]
}
Key fields:
FieldDescription
schemePayment scheme. Currently only exact is supported.
networkTarget chain. Always arbitrum for this server.
maxAmountRequiredPrice in raw USDC units (6 decimals). 10000 = $0.01.
payToThe recipient wallet address for payment.
assetUSDC contract address on Arbitrum One.
extra.name / extra.versionEIP-712 domain parameters for USDC’s transferWithAuthorization.

Payment payload construction

The X-PAYMENT header contains a base64-encoded JSON object with the signed EIP-3009 authorization:
{
  "scheme": "exact",
  "network": "arbitrum",
  "payload": {
    "signature": "0x...",
    "authorization": {
      "from": "0xYourWalletAddress",
      "to": "0xFacilitatorAddress",
      "value": "10000",
      "validAfter": "0",
      "validBefore": "1716000000",
      "nonce": "0xUniqueNonceBytes32"
    }
  }
}
import { signTypedData, encodePacked, keccak256 } from "viem";
import { privateKeyToAccount } from "viem/accounts";

const USDC_ARBITRUM = "0xaf88d065e77c8cc2239327c5edb3a432268e5831";

const account = privateKeyToAccount("0xYourPrivateKey");

// Generate a unique nonce (bytes32)
const nonce = keccak256(
  encodePacked(["address", "uint256"], [account.address, BigInt(Date.now())])
);

const authorization = {
  from: account.address,
  to: "0xFacilitatorAddress", // from 402 response payTo
  value: 10000n,              // $0.01 USDC
  validAfter: 0n,
  validBefore: BigInt(Math.floor(Date.now() / 1000) + 300),
  nonce,
};

const signature = await signTypedData({
  account,
  domain: {
    name: "USD Coin",
    version: "2",
    chainId: 42161,
    verifyingContract: USDC_ARBITRUM,
  },
  types: {
    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" },
    ],
  },
  primaryType: "TransferWithAuthorization",
  message: authorization,
});

const paymentPayload = {
  scheme: "exact",
  network: "arbitrum",
  payload: {
    signature,
    authorization: {
      from: authorization.from,
      to: authorization.to,
      value: authorization.value.toString(),
      validAfter: authorization.validAfter.toString(),
      validBefore: authorization.validBefore.toString(),
      nonce: authorization.nonce,
    },
  },
};

const xPayment = btoa(JSON.stringify(paymentPayload));

Replay prevention

Every payment includes a unique nonce (bytes32). The server atomically claims each nonce to prevent the same payment from being used twice.
When REDIS_URL is set, nonces are stored using the SET NX EX pattern:
SET arb:nonce:{nonce} "1" EX 86400 NX
  • NX ensures the key is only set if it does not already exist (atomic claim).
  • EX 86400 sets a 24-hour TTL so stale nonces are automatically cleaned up.
If the SET returns null (key already existed), the payment is rejected as a replay.
When no Redis connection is available, nonces are tracked in a local Set. This works for single-instance development but does not survive restarts and cannot be shared across multiple server instances.

Revenue tracking

Every verified payment is recorded for per-facilitator revenue accounting. The payer address is extracted from the payment authorization and stored alongside the amount and request path. Revenue data is available through the admin dashboard:
  • GET /admin/revenue — total revenue, unique payers, recent payments
  • GET /admin/revenue/history?days=30 — daily breakdown
Revenue storage uses Redis when available (arb:revenue:* keys with 90-day TTL) and falls back to in-memory counters otherwise.

Development mode

For local development, set the SKIP_PAYMENT environment variable to bypass payment verification entirely:
SKIP_PAYMENT=true bun run apps/api/src/index.ts
When payment is skipped, the middleware sets the owner ID to "dev" and all agent routes are accessible without an X-PAYMENT header.
Never set SKIP_PAYMENT=true in production. Doing so disables all payment verification on agent routes.

Verification details

The server validates the following before accepting a payment:
  1. Scheme and network — must be exact and arbitrum
  2. Recipient — the to address must match the configured PAY_TO_ADDRESS
  3. Amount — the value must be greater than or equal to the required amount (default 10000)
  4. Sender — the from address must be present
  5. Nonce — the nonce must not have been previously claimed
  6. Signature — the EIP-712 typed data signature must be valid for the USDC transferWithAuthorization domain on Arbitrum (chain ID 42161)
If any check fails, the server returns 402 with {"error": "Invalid or insufficient payment"}.