Skip to content

Agent Payments (x402 Protocol)

AI agents can pay for API calls directly from their own wallet using the x402 protocol — no API key required. The server returns HTTP 402 with payment requirements, and the agent signs a cryptographic payment authorization to proceed.

x402 vs API Key

JarvisClaw supports two authentication methods — both use x402 under the hood:

MethodHeaderHow it works
API KeyAuthorization: Bearer sk-...Platform signs x402 on your behalf using your HD wallet (User HD → x402 → JC1)
Private Key (x402 direct)PAYMENT-SIGNATURE: <base64>Your agent signs x402 directly from its own wallet

X-PAYMENT works too

X-PAYMENT is accepted as an alias for PAYMENT-SIGNATURE and carries the same base64 payload — useful if your x402 client library emits the ecosystem-standard name. If you send both, PAYMENT-SIGNATURE takes precedence.

Both methods settle on-chain via x402

When you use an API key, the server automatically executes the x402 payment flow from your platform HD wallet — you don't need to handle the 402 → sign → retry cycle yourself. When you use a private key directly, your agent handles that cycle (the SDK does this transparently).

Which should I use?

  • API Key — Simplest integration. Works like any standard API. Your HD wallet balance (Base USDC or Solana USDC) is charged automatically with multi-chain fallback.
  • Private Key — Full sovereignty. Your agent holds its own keys and pays per-request. Best for autonomous agents that manage their own funds.

How it works

With Private Key (direct x402)

  1. Agent sends a request without an auth header (or with an empty one).
  2. Server returns HTTP 402 with payment requirements (amount, token, chain, recipient).
  3. Agent signs an x402 payment authorization with its wallet private key.
  4. Agent resends the request with the PAYMENT-SIGNATURE header.
  5. Server verifies the payment via the CDP facilitator contract and returns the response.

With API Key (platform-managed x402)

  1. User sends a request with Authorization: Bearer sk-... header.
  2. Server looks up the user's HD wallet (Base or Solana).
  3. Server automatically executes x402 settlement from the HD wallet (with multi-chain fallback: Base → Solana if Base balance insufficient).
  4. On success, server returns the response. On settlement failure, returns 402 with instructions to top up.

Supported Chains

ChainNetwork IDKey FormatToken
Base (L2)eip155:8453Hex 0x... (64 hex chars)USDC
Base Sepoliaeip155:84532Hex 0x...USDC (testnet)
Solanasolana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpBase58 keypairUSDC

That is the complete list. Chains named elsewhere in these docs — a DEX swap target, an RPC query network — are chains being queried or traded on, not chains we accept payment on. Payment always settles on Base or Solana.

Requirements

A wallet with USDC on one of the supported chains. The SDK detects the chain automatically from the key format — no config flag needed.


The SDK handles the entire 402 → sign → retry flow transparently. Just pass private_key instead of api_key.

Install

shell
# EVM (Base chain) — default
pip install jarvisclaw[agent]

# Solana
pip install jarvisclaw[solana]

# Both
pip install jarvisclaw[agent,solana]

Base Chain (EVM)

python
from jarvisclaw import ChatClient, ImageClient, AudioClient, SearchClient

# Pass your EVM private key (hex, starts with 0x)
chat = ChatClient(private_key="0x<your-hex-private-key>")

# Chat — SDK handles 402 → EIP-712 sign → retry automatically
response = chat.complete("Hello, I'm an AI agent paying with my wallet")
print(response)

# Check on-chain USDC balance
balance = chat.get_balance()
print(f"Wallet balance: ${balance:.2f} USDC")

# Streaming works the same way
for chunk in chat.stream("Tell me about x402"):
    print(chunk, end="")

# Image generation (auto-polls until ready)
image = ImageClient(private_key="0x<your-hex-private-key>")
img = image.generate("A futuristic city")
print(img.url)

# Audio TTS
audio = AudioClient(private_key="0x<your-hex-private-key>")
result = audio.speech("Hello world", voice="sarah")

# Web search
search = SearchClient(private_key="0x<your-hex-private-key>")
results = search.query("latest AI news")

Solana

python
from jarvisclaw import ChatClient

# Pass your Solana keypair (base58, exported from Phantom/Solflare)
chat = ChatClient(private_key="<your-base58-solana-keypair>")

# Same API — SDK detects Solana from key format and signs SPL transfers
response = chat.complete("Hello from Solana!")
print(response)

# Check SOL-chain USDC balance
print(f"Balance: ${chat.get_balance():.2f} USDC")

Async

python
from jarvisclaw.aio import ChatClient

async with ChatClient(private_key="0x<your-hex-private-key>") as chat:
    text = await chat.complete("Say hello")
    print(text)

Method 2: Go SDK

The Go SDK supports x402 payments on Base (EVM) with automatic 402 handling.

Install

shell
go get github.com/api-jarvisclaw/go-sdk/v2@latest

EVM (Base Chain)

go
package main

import (
    "context"
    "fmt"
    "log"

    jc "github.com/api-jarvisclaw/go-sdk/v2"
)

func main() {
    ctx := context.Background()

    // x402 wallet-based auth (no API key needed)
    client, err := jc.NewClient(jc.WithPrivateKey("0x<your-hex-private-key>"))
    if err != nil {
        log.Fatal(err)
    }

    // Chat returns the reply text directly
    text, err := client.Chat(ctx, "auto", "Hello from Go x402!")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(text)

    // On-chain USDC balance
    balance, _ := client.GetBalance(ctx)
    fmt.Printf("Wallet balance: $%.4f USDC\n", balance)
}

API Key (no direct x402)

go
// API key auth — the platform settles x402 from your HD wallet
client, err := jc.NewClient(jc.WithAPIKey("sk-..."))
if err != nil {
    log.Fatal(err)
}

Constructors return an error

jc.NewClient returns (*Client, error). Discarding the second value will not compile.

Solana not yet supported in Go

The Go SDK currently supports EVM (Base chain) only. For Solana x402 payments, use the Python SDK.


Method 3: Raw Python (Manual x402 Handshake)

If you don't want to use the SDK, you can implement the x402 payment flow manually with requests and eth_account.

Dependencies

shell
pip install requests eth_account

Step-by-step: EVM (Base Chain)

python
"""Manual x402 payment flow — no SDK required."""
import base64
import json
import os
import time

import requests
from eth_account import Account
from eth_account.messages import encode_typed_data

BASE_URL = "https://api.jarvisclaw.ai"
PRIVATE_KEY = os.environ["WALLET_PRIVATE_KEY"]  # 0x...

# Derive wallet address
account = Account.from_key(PRIVATE_KEY)
print(f"Wallet: {account.address}")

# --- Step 1: Send request (will get 402) ---
resp = requests.post(
    f"{BASE_URL}/v1/chat/completions",
    headers={"Content-Type": "application/json"},
    json={
        "model": "auto",
        "messages": [{"role": "user", "content": "Hello from raw x402!"}],
    },
)

if resp.status_code != 402:
    # No payment needed (shouldn't happen without auth, but just in case)
    print(resp.json())
    exit()

# --- Step 2: Parse 402 payment requirements ---
body = resp.json()
payments = body.get("accepts", body.get("payments", []))
resource = body.get("resource", {})

# Find an EVM payment option
payment = None
for p in payments:
    if p.get("network", "").startswith("eip155:"):
        payment = p
        break

if not payment:
    raise ValueError("No EVM payment option in 402 response")

pay_to = payment["payTo"]
amount = int(payment["amount"])            # in micro-USDC (6 decimals)
network = payment["network"]               # e.g. "eip155:8453"
asset = payment["asset"]                   # USDC contract address
max_timeout = payment.get("maxTimeoutSeconds", 300)

# The facilitator is a TOP-LEVEL field, not inside payment["extra"].
# For EVM options, extra carries the EIP-712 domain: {"name", "version"}.
facilitator = body.get("facilitator", "")
domain_name = payment.get("extra", {}).get("name", "USD Coin")
domain_version = payment.get("extra", {}).get("version", "2")

chain_id = int(network.split(":")[1])      # 8453 for Base mainnet

print(f"Payment required: {amount / 1_000_000:.4f} USDC → {pay_to}")

# --- Step 3: Sign EIP-712 TransferWithAuthorization ---
valid_after = 0
valid_before = int(time.time()) + max_timeout
nonce = os.urandom(32)  # random 32-byte nonce

# EIP-712 domain for USDC on Base — name/version come from payment["extra"]
domain = {
    "name": domain_name,
    "version": domain_version,
    "chainId": chain_id,
    "verifyingContract": asset,
}

# TransferWithAuthorization message (EIP-3009)
message = {
    "from": account.address,
    "to": pay_to,
    "value": amount,
    "validAfter": valid_after,
    "validBefore": valid_before,
    "nonce": nonce,
}

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

# Sign with eth_account
signable = encode_typed_data(domain, types, message)
signed = account.sign_message(signable)

# --- Step 4: Build PAYMENT-SIGNATURE payload ---
payload = {
    "x402Version": 2,
    "scheme": "exact",
    "network": network,
    "payload": {
        "signature": signed.signature.hex(),
        "authorization": {
            "from": account.address,
            "to": pay_to,
            "value": str(amount),
            "validAfter": str(valid_after),
            "validBefore": str(valid_before),
            "nonce": nonce.hex(),
        },
    },
    "extensions": {},
}

signature_header = base64.b64encode(
    json.dumps(payload, separators=(",", ":")).encode()
).decode()

# --- Step 5: Retry with PAYMENT-SIGNATURE header ---
resp2 = requests.post(
    f"{BASE_URL}/v1/chat/completions",
    headers={
        "Content-Type": "application/json",
        "PAYMENT-SIGNATURE": signature_header,
    },
    json={
        "model": "auto",
        "messages": [{"role": "user", "content": "Hello from raw x402!"}],
    },
)

print(f"Status: {resp2.status_code}")
print(resp2.json()["choices"][0]["message"]["content"])

402 Response Format

The server returns this structure when payment is required:

json
{
  "x402Version": 2,
  "accepts": [
    {
      "scheme": "exact",
      "network": "eip155:8453",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "amount": "1000",
      "payTo": "0x<seller-address>",
      "maxTimeoutSeconds": 300,
      "extra": { "name": "USD Coin", "version": "2" }
    }
  ],
  "facilitator": "https://api.cdp.coinbase.com/platform/v2/x402",
  "resource": {
    "url": "https://api.jarvisclaw.ai/v1/chat/completions",
    "description": "Chat completion",
    "mimeType": "application/json",
    "tag": "ai-model"
  },
  "extensions": {
    "bazaar": {
      "info": { "input": { "type": "http", "method": "POST", "discoverable": true } },
      "schema": { "properties": { "input": {}, "output": {} } }
    }
  }
}

The amount is in the token's smallest unit (for USDC: 6 decimals, so 1000 = $0.001).

Two things to note about the structure:

  • facilitator is top-level, not inside accepts[].extra.
  • extra is the EIP-712 domain for EVM options (name, version), and the fee payer for Solana options (feePayer).

The same JSON is also returned base64-encoded in the PAYMENT-REQUIRED response header, and extensions.bazaar carries the input/output schema that x402scan uses to mark the endpoint invocable.

When Solana is configured and a fee payer is known, the server includes a second payment option. Until the fee payer is resolved from the facilitator, Base is the only option listed — an SVM payment cannot be built without it:

json
{
  "accepts": [
    {
      "scheme": "exact",
      "network": "eip155:8453",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "amount": "1000",
      "payTo": "0x<seller-address>",
      "maxTimeoutSeconds": 300,
      "extra": { "name": "USD Coin", "version": "2" }
    },
    {
      "scheme": "exact",
      "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
      "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
      "amount": "1000",
      "payTo": "<seller-solana-address>",
      "maxTimeoutSeconds": 300,
      "extra": { "feePayer": "<fee-payer-address>" }
    }
  ]
}

The SDK selects the correct option based on the wallet key format:

  • Hex private key (0x...) → picks eip155:* (EVM/Base)
  • Base58 keypair → picks solana:* (Solana)

Cost & Safety

  • The SDK enforces a per-request safety cap (100 USDC max). If the server requests more, the SDK raises an error.
  • Use client.get_spending() to track cumulative session cost.
  • Use client.get_balance() to check remaining on-chain USDC balance.
  • All payments are on-chain authorizations — your wallet signs but never sends tokens directly. The facilitator contract executes the transfer only after the server delivers the response.

Environment Variables

VariableDescription
JARVISCLAW_API_KEYAPI key for Bearer auth (mutually exclusive with wallet)
JARVISCLAW_WALLET_KEYWallet private key — hex for EVM, base58 for Solana (SDK auto-detects)
JARVISCLAW_BASE_URLOverride API base URL (default: https://api.jarvisclaw.ai)

Multi-Chain Fallback

When using an API key with an HD wallet that has USDC on both Base and Solana, the platform automatically attempts settlement with fallback:

  1. Base (EVM) is tried first — lowest fees, fastest confirmation on L2.
  2. If Base settlement fails (insufficient balance, network error), the system falls back to Solana automatically.
  3. If both chains fail, the request returns an error — no upstream call is made.

TIP

This fallback is transparent to the user. Fund either chain (or both) and the platform picks the best available route. There is no config flag — it just works.

Settlement Safety

The platform enforces a strict rule: if user wallet settlement fails on all chains, the upstream provider is never called. This prevents the platform from paying out-of-pocket for requests that were not successfully charged to the user.


Solana ATA Pre-check

Before attempting a Solana x402 settlement, the platform performs an Associated Token Account (ATA) pre-check:

CheckWhat it verifies
Sender ATAThe payer wallet has an initialized USDC SPL token account
BalanceThe ATA holds sufficient USDC for the request amount

If the ATA does not exist or has zero balance, the Solana path is skipped immediately (no failed transaction simulation) and the system falls back to Base or returns an error.

Common issue

A new Solana wallet that has never held USDC will not have an ATA initialized on-chain. You must receive at least one USDC transfer (even 0.000001) to create the ATA before the x402 flow can settle on Solana.

How to initialize your Solana USDC ATA

  1. Send any amount of USDC (SPL) to your Solana wallet address — this automatically creates the ATA.
  2. Or use a Solana wallet app (Phantom, Solflare) to add the USDC token — some wallets create the ATA proactively.
  3. The platform's /v1/wallet/balance endpoint shows solana_usdc — if it returns a value, your ATA is ready.