Skip to main content
x402 is Allium’s micropayment protocol that gives you full control over the payment flow. Your client handles the 402 Payment Required negotiation, constructs EIP-712 typed data, signs via a wallet provider, and retries the request with a payment signature.

Quickstart with Agent

The fastest way to get started — let your AI agent handle setup and payments for you.
1

Install allium-cli

curl -sSL https://agents.allium.so/cli/install.sh | sh
allium-cli is still in early beta.
2

Install agent skills

npx skills add allium-labs/skills --yes
3

Open your agent

Start Claude Code, Codex, or any compatible AI agent.
4

Ask a question

Set up Allium and fetch the latest balances for vitalik.eth

How It Works

1

Send request

Make a normal API request to any payment-enabled endpoint.
2

Receive 402

The server responds with 402 Payment Required and an accepts array of payment options.
3

Sign payment

Your client constructs and signs an EIP-712 payment authorization via Privy server wallets — no private keys or gas needed.
4

Retry with signature

Resend the original request with the payment signature header attached.
5

Settlement

Allium verifies the authorization and settles the USDC payment onchain via Coinbase.
6

Get data

Payment confirmed — you receive your data with a 200 response.

Supported Tokens & Networks

x402 payments use USDC on the following networks:
NetworkUSDC Contract AddressEIP-712 Domain
Base (mainnet)0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913name: USD Coin, version: 2
Solana (mainnet)EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1vname: USDC, version: 2

Implementation

This guide uses Privy server wallets for signing. Privy handles wallet creation and EIP-712 signing via its API — you never touch private keys or pay gas.

Prerequisites

  • Python 3.8+
  • Privy account — free, takes 30 seconds
  • USDC on Base or Solana (mainnet)

1. Set Up Privy

1

Create a Privy app

Go to dashboard.privy.io, sign up, and click Create App.
2

Copy your credentials

Copy your App ID and App Secret from the dialog and store them securely.
3

If you closed the dialog too soon

Go to Configuration → App settings → Basics → New Secret to generate a new App ID and App Secret pair.
4

Set environment variables

export PRIVY_APP_ID="your-app-id"
export PRIVY_APP_SECRET="your-app-secret"

2. Install Dependencies

pip install privy-client httpx

3. Create a Wallet

Create a server-managed wallet on Privy. This wallet will sign x402 payments on your behalf.
import os
from privy import PrivyAPI

client = PrivyAPI(
    app_id=os.environ["PRIVY_APP_ID"],
    app_secret=os.environ["PRIVY_APP_SECRET"],
)

wallet = client.wallets.create(chain_type="ethereum")
print(f"Wallet ID: {wallet.id}")
print(f"Address: {wallet.address}")
Save the wallet ID as an environment variable:
export PRIVY_WALLET_ID="your-wallet-id"
Fund this wallet address with USDC on Base (or Base Sepolia for testing).
If testing, get free testnet USDC from Circle’s faucet. Select Base Sepolia as the network.

4. Make a Paid API Call

Here’s a complete working example that fetches the current price of ETH. The x402_request helper handles the full payment flow:
  1. Send the API request
  2. Receive a 402 response containing an accepts array of payment options
  3. Select a payment option matching your network (Base or Base Sepolia)
  4. Construct EIP-712 typed data for a USDC TransferWithAuthorization (EIP-3009)
  5. Sign via Privy using eth_signTypedData_v4
  6. Build the x402 v2 PaymentPayload and retry with the PAYMENT-SIGNATURE header
import base64
import json
import os
import secrets

import httpx
from privy import PrivyAPI

PRIVY_APP_ID = os.environ["PRIVY_APP_ID"]
PRIVY_APP_SECRET = os.environ["PRIVY_APP_SECRET"]
WALLET_ID = os.environ["PRIVY_WALLET_ID"]
BASE_URL = "https://agents.allium.so"

# "eip155:8453" for Base mainnet, "eip155:84532" for Base Sepolia testnet
TARGET_NETWORK = "eip155:84532"

privy = PrivyAPI(app_id=PRIVY_APP_ID, app_secret=PRIVY_APP_SECRET)

# Retrieve the wallet address — needed for the authorization "from" field
wallet = privy.wallets.get(wallet_id=WALLET_ID)
WALLET_ADDRESS = wallet.address


def x402_request(http, method, url, **kwargs):
    """Make an API request with automatic x402 payment signed via Privy."""
    headers = kwargs.pop("headers", {})
    response = http.request(method, url, headers=headers, **kwargs)

    if response.status_code != 402:
        return response

    # The 402 response contains an "accepts" array of payment options,
    # each specifying a network, amount, asset, and payTo address.
    # There is no pre-built "typed_data" — the client must construct it.
    payment_details = response.json()
    accepts = payment_details.get("accepts", [])

    # Select the payment option matching our network
    option = next((a for a in accepts if a["network"] == TARGET_NETWORK), None)
    if option is None:
        raise ValueError(f"No payment option for {TARGET_NETWORK}")

    # Parse chain ID from CAIP-2 network string, e.g. "eip155:84532" -> 84532
    chain_id = int(option["network"].split(":")[1])
    nonce = "0x" + secrets.token_hex(32)

    # Construct EIP-712 typed data for USDC TransferWithAuthorization (EIP-3009).
    # The domain name and version come from the payment option's "extra" field —
    # these differ per network (Base mainnet: "USD Coin", Base Sepolia: "USDC").
    typed_data = {
        "types": {
            "EIP712Domain": [
                {"name": "name", "type": "string"},
                {"name": "version", "type": "string"},
                {"name": "chainId", "type": "uint256"},
                {"name": "verifyingContract", "type": "address"},
            ],
            "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"},
            ],
        },
        "domain": {
            "name": option["extra"]["name"],
            "version": option["extra"]["version"],
            "chainId": chain_id,
            "verifyingContract": option["asset"],
        },
        # Privy expects "primary_type" (snake_case), not "primaryType" (camelCase)
        "primary_type": "TransferWithAuthorization",
        "message": {
            "from": WALLET_ADDRESS,
            "to": option["payTo"],
            "value": str(option["amount"]),
            "validAfter": "0",
            "validBefore": str(option["maxTimeoutSeconds"]),
            "nonce": nonce,
        },
    }

    # Sign with Privy server wallet
    sign_result = privy.wallets.rpc(
        wallet_id=WALLET_ID,
        method="eth_signTypedData_v4",
        params={"typed_data": typed_data},
    )

    # Build the x402 v2 PaymentPayload with resource, accepted, and payload fields
    payment_payload = {
        "x402Version": payment_details["x402Version"],
        "resource": {
            "url": option["resource"],
            "description": option.get("description", ""),
            "mimeType": option.get("mimeType", "application/json"),
        },
        "accepted": {
            "scheme": option["scheme"],
            "network": option["network"],
            "amount": str(option["amount"]),
            "asset": option["asset"],
            "payTo": option["payTo"],
            "maxTimeoutSeconds": option["maxTimeoutSeconds"],
            "extra": option.get("extra", {}),
        },
        "payload": {
            "signature": sign_result.data.signature,
            "authorization": {
                "from": WALLET_ADDRESS,
                "to": option["payTo"],
                "value": str(option["amount"]),
                "validAfter": "0",
                "validBefore": str(option["maxTimeoutSeconds"]),
                "nonce": nonce,
            },
        },
    }

    # Base64-encode the payload and send as PAYMENT-SIGNATURE header
    headers["PAYMENT-SIGNATURE"] = base64.b64encode(
        json.dumps(payment_payload).encode()
    ).decode()
    response = http.request(method, url, headers=headers, **kwargs)
    return response


# Fetch ETH price
with httpx.Client(timeout=30.0) as http:
    result = x402_request(
        http,
        "POST",
        f"{BASE_URL}/api/v1/developer/prices",
        json=[
            {
                "chain": "ethereum",
                "token_address": "0x0000000000000000000000000000000000000000",
            }
        ],
    )
    print(json.dumps(result.json(), indent=2))
Expected response:
{
  "items": [
    {
      "timestamp": "2026-02-10T09:27:11Z",
      "chain": "ethereum",
      "address": "0x0000000000000000000000000000000000000000",
      "decimals": 18,
      "price": 2023.6042396986138,
      "open": 2023.6042396986138,
      "high": 2023.6042396986138,
      "close": 2023.6042396986138,
      "low": 2023.6042396986138
    }
  ]
}

EIP-712 Domain Configuration

The USDC contracts use different EIP-712 domains per network. These values are required when constructing the TransferWithAuthorization typed data for signing. They are provided in the 402 response’s accepts[].extra field.
NetworkDomain NameVersion
BaseUSD Coin2
SolanaUSDC2
Privy signs the EIP-712 typed data via eth_signTypedData_v4, but the client must construct the typed data itself from the 402 response fields. The server does not provide a ready-to-sign typed_data object.

Frontend Integration

Building a web app with embedded wallets? Use Privy’s useX402Fetch hook for seamless client-side x402 payments in React.