Skip to main content
This guide walks you through making your first paid API call to Allium using x402 micropayments. By the end, you’ll fetch a live token price — no API key needed.
This guide uses Privy server wallets for signing x402 payments. Privy handles wallet creation and EIP-712 signing via its API — you never touch private keys or pay gas. You sign payment authorizations with Privy’s eth_signTypedData_v4 RPC method, and Allium handles onchain USDC settlement.

Prerequisites

  • Python 3.8+
  • Privy account — free, takes 30 seconds
  • USDC on Base (mainnet) or Base Sepolia (testnet)
New to x402? Start with Base Sepolia testnet. Get free testnet USDC from Circle’s faucet.

1. Set Up Privy

Privy manages your wallet and handles EIP-712 signing — no private keys to manage.
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. Get a Token Price

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
    }
  ]
}

Token Support

Payment Token

x402 payments use USDC exclusively. Payments are gasless — Allium handles onchain settlement so you only need USDC, not ETH.
NetworkUSDC AddressUse For
Base0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913Production
Base Sepolia0x036CbD53842c5426634e7929541eC2318f3dCF7eTesting

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
Base SepoliaUSDC2
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.

Next Steps