Docs / x402

x402 Pay-Per-Call

Use Soundside AI tools with USDC on Base — no account, no API key, no signup. Ideal for autonomous agents that need to call tools without a pre-funded balance.

BetaLive on Base mainnet (real USDC). More providers coming soon.

Quick start

Recommended: use the x402 Python library

The x402 library handles the full 402 → sign → retry cycle automatically. Do not hand-roll the payment-signature header — the encoding is non-trivial and manual construction is the #1 source of errors.

Install (x402 is not yet on PyPI — install from GitHub)
pip install "git+https://github.com/coinbase/x402.git#subdirectory=python/x402&egg=x402[evm]" requests eth-account
Python — complete working example (sync)
import json, os, requests
from eth_account import Account
from x402 import x402ClientSync
from x402.http import encode_payment_signature_header, X_PAYMENT_HEADER
from x402.mechanisms.evm.exact import register_exact_evm_client
from x402.mechanisms.evm.signers import EthAccountSigner
from x402.schemas import PaymentRequired

ENDPOINT = "https://mcp.soundside.ai/mcp"

# Set up wallet + payment client
account = Account.from_key(os.environ["WALLET_PRIVATE_KEY"])  # 0x-prefixed hex
payment_client = x402ClientSync()
register_exact_evm_client(payment_client, EthAccountSigner(account))

def call_tool(tool: str, args: dict, timeout: int = 120) -> dict:
    """Call a Soundside MCP tool with automatic x402 payment."""
    session = requests.Session()

    # 1. Initialize MCP session (required for each call)
    init_r = session.post(ENDPOINT, json={
        "jsonrpc": "2.0", "id": "1", "method": "initialize",
        "params": {"protocolVersion": "2025-11-25", "capabilities": {},
                   "clientInfo": {"name": "x402-example", "version": "1.0"}},
    }, headers={"Content-Type": "application/json",
                "Accept": "application/json, text/event-stream"}, timeout=30)
    session_id = init_r.headers.get("mcp-session-id", "")

    headers = {"Content-Type": "application/json",
               "Accept": "application/json, text/event-stream",
               "mcp-session-id": session_id,
               "x402-wallet": account.address}  # required for resource attribution
    payload = {"jsonrpc": "2.0", "id": "2", "method": "tools/call",
               "params": {"name": tool, "arguments": args}}

    # 2. First attempt — server returns 402 Payment Required
    r = session.post(ENDPOINT, json=payload, headers=headers, timeout=timeout)

    if r.status_code == 402:
        # 3. Parse requirements from response BODY (not a header)
        pr = PaymentRequired.model_validate(r.json())
        # 4. Sign off-chain EIP-3009 authorization (no gas needed)
        sig = encode_payment_signature_header(
            payment_client.create_payment_payload(pr))
        # 5. Retry with signed payment header
        r = session.post(ENDPOINT, json=payload,
                         headers={**headers, X_PAYMENT_HEADER: sig}, timeout=timeout)

    # 6. Parse SSE response — prefer structuredContent
    for line in r.text.splitlines():
        if line.startswith("data:"):
            obj = json.loads(line[5:])
            if "result" in obj:
                sc = obj["result"].get("structuredContent", {})
                if sc: return sc
                for ct in obj["result"].get("content", []):
                    if ct.get("type") == "text":
                        try: return json.loads(ct["text"])
                        except: return {"text": ct["text"]}
    return {}

# ── Examples ──

# Generate text (~$0.01 USDC)
result = call_tool("create_text", {
    "prompt": "Write a haiku about Base blockchain.",
    "provider": "vertex"})
print(result.get("message") or result.get("text"))

# Generate image (~$0.04 USDC)
result = call_tool("create_image", {
    "prompt": "A futuristic city at sunset",
    "provider": "minimax"})
print(f"Resource: {result.get('resource_id')}")
print(f"Wallet link: {result.get('wallet_link')}")

# Edit an image (~$0.01 USDC)
result = call_tool("edit_video", {
    "resource_id": result["resource_id"],
    "action": "add_text",
    "text": "Hello x402",
    "position": "bottom"})
print(f"Edited: {result.get('resource_id')}")

# Analyze media (~$0.01 USDC)
result = call_tool("analyze_media", {
    "resource_id": result["resource_id"],
    "analysis_type": "technical"})
print(f"Type: {result.get('metadata', {}).get('mime_type')}")

Fund your wallet with USDC on Base: send real USDC from Coinbase (select the Base network, not Ethereum mainnet). A small amount of ETH on Base is also needed for gas (~0.002 ETH is more than enough).

Discovery

Machine-readable pricing and status

GET https://mcp.soundside.ai/api/x402/status
{
  "enabled": true,
  "endpoint": "https://mcp.soundside.ai/mcp",
  "settlement_provider": "coinbase",
  "pay_to_mode": "static_wallet",
  "network": "eip155:8453",
  "token": "USDC",
  "facilitator_url": "https://api.cdp.coinbase.com/platform/v2/x402",
  "pricing_model": "ceiling-quote",
  "enabled_tools": [
    { "tool": "create_text",  "provider": "vertex",  "price_usdc": "0.01", "sync": true },
    { "tool": "create_image", "provider": "minimax", "price_usdc": "0.04", "sync": true },
    { "tool": "create_video", "provider": "luma",    "price_usdc": "0.40", "sync": false },
    ...
  ],
  "docs": "https://soundside.ai/docs/x402"
}

Always fetch this to get the live network and tool surface before making a call. The per-call 402 response is always authoritative for the exact payment amount.

Protocol

How the 402 cycle works

The x402 library handles all of this for you. This section is for agents implementing the protocol from scratch.

  1. Send your tool call — POST to /mcp with a standard MCP tools/call JSON-RPC body. No auth header needed.
    Step 1 — initial request
    POST https://mcp.soundside.ai/mcp
    Content-Type: application/json
    
    {
      "jsonrpc": "2.0",
      "id": "1",
      "method": "tools/call",
      "params": {
        "name": "create_text",
        "arguments": { "prompt": "Write a haiku about Base." }
      }
    }
  2. Receive 402 — The response body is a JSON PaymentRequired object. The accepts[0] entry has the deposit address, amount, and network.
    Step 2 — 402 response body
    {
      "x402Version": 2,
      "error": "Payment required",
      "accepts": [{
        "scheme": "exact",
        "network": "eip155:8453",
        "asset":   "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
        "amount":  "10000",
        "payTo":   "0x<stripe-issued deposit address>",
        "maxTimeoutSeconds": 300,
        "extra": { "name": "USDC", "version": "2" }
      }]
    }
  3. Sign an EIP-3009 authorization — This is an off-chain signature (no on-chain transaction, no gas). You authorize the transfer of USDC from your wallet to the payTo address using EIP-712 typed data and EIP-3009 transferWithAuthorization.
  4. Retry with payment-signature header — Re-send the identical request body with the payment-signature header set to a base64-encoded JSON PaymentPayload:
    Step 4 — payment-signature header structure
    // payment-signature = base64( JSON.stringify({
    {
      "x402Version": 2,
      "payload": {
        "signature": "0x<EIP-712 sig>",
        "authorization": {
          "from":        "0x<your wallet>",
          "to":          "0x<payTo from step 2>",
          "value":       "10000",
          "validAfter":  "0",
          "validBefore": "<now + maxTimeoutSeconds as unix timestamp>",
          "nonce":       "0x<32 random bytes, hex>"
        }
      },
      "accepted": { <exact copy of the accepts[0] object from step 2> }
    }
    // ))
    
    // The "accepted" field must be an exact copy of accepts[0].
    // Any mismatch causes: "Invalid payment signature header"
  5. Receive 200 — Facilitator verifies the authorization and executes the on-chain transfer. The tool result is in the response body.
    Step 5 — success
    HTTP/1.1 200 OK
    PAYMENT-RESPONSE: <settlement confirmation>
    
    {
      "jsonrpc": "2.0",
      "id": "1",
      "result": {
        "content": [{ "type": "text", "text": "Base in spring bloom..." }]
      }
    }

Pricing

Ceiling-quote model

The amount in the 402 response is the exact payment charged.

ToolProviderPrice (USDC)Notes
create_textvertex / grok / minimax$0.01Sync
create_imageminimax / luma / grok / vertex / runway$0.02–$0.08Sync (varies by provider)
create_videoluma / minimax / vertex / runway / grok$0.28–$1.60Async — poll with x402_session_token
create_audiominimax / vertex$0.01–$0.04TTS sync, sound effects async
create_musicminimax$0.04Async — poll with x402_session_token
create_artifactsoundside.ai$0.01Sync (presentations, charts, docs)
edit_videosoundside.ai$0.01Sync (trim, concat, crossfade, speed, loop, etc.)
edit_audio / compose_media / apply_effect / extract_mediasoundside.ai$0.01Sync (editing tools)
analyze_mediasoundside.ai$0.01–$0.03Sync ($0.03 for vision_qa)
lib_listFreeUse REST endpoints instead (see below)
lib_manage$0.01Create/update/delete library entities
lib_share$0.01Share projects by email
Full live pricing: GET /api/x402/status

Behavior

Stateless and authenticated modes

  • No auth header (default) — fully anonymous. Results returned directly in the response. For create_image, the image is returned as a signed URL. Nothing is saved to a library.
  • With auth header — add Authorization: Bearer <token> to identify as a Soundside user. Payment still goes via x402, but results can be saved to your library if you pass project_id or collection_id.

Browser access

Wallet link in x402 responses

After a successful tool execution via x402, the response includes a wallet_link field in structuredContent — a short-lived signed URL (15-minute TTL) that grants browser access to the payer's library.

Example response with wallet_link
{
  "jsonrpc": "2.0",
  "id": "1",
  "result": {
    "structuredContent": {
      "success": true,
      "resource_id": "abc-123",
      "url": "https://storage.googleapis.com/...",
      "wallet_link": "https://www.soundside.ai/auth/wallet-link?token=eyJ...",
      "x402_session_token": "eyJ..."
    }
  }
}

For agents: Surface the wallet_link URL to the human user. Visiting it creates a full browser session — no password, no wallet extension, no signup required. The user can then view and manage their generated assets at /library and /projects.

Use x402_session_token for async resource polling on /api/x402/resource/<resource_id> and /api/x402/resources. Send it as either Authorization: Bearer <token> or X-Session-Token: <token>. It is also returned in the X-Session-Token response header.

For operators: these are server-issued tokens, not client secrets. Configure WALLET_LINK_SECRET to enable wallet_link, and optionally set X402_SESSION_TOKEN_SECRET for a dedicated polling-token signing key. If the latter is unset, the server reuses WALLET_LINK_SECRET.

For browser users: The login page at /login also includes a "Continue with Wallet" button that uses SIWE (Sign-In with Ethereum) — connect MetaMask or Coinbase Wallet, sign a message, and get a session tied to the same wallet identity.

Both flows produce the same session. Assets generated via x402 payments appear in the user's library immediately after sign-in.

Error reference

x402 error codes

CodeHTTPMeaning / Fix
unknown_tool422Tool doesn't exist — call tools/list first
invalid_tool_arguments400Argument validation failed — check tools/list for the input schema
invalid_provider422Provider not available — check tools/list for valid providers
x402_validation_failed400Missing required params for this tool/mode — no payment charged, fix args and retry
Invalid payment signature header402Use the x402 library — accepted block must be an exact copy of the 402 response accepts[0]
x402_tool_not_enabled403Tool not on x402 allowlist — check /api/x402/status for enabled tools
x402_provider_not_enabled403Provider not supported on x402 — check status endpoint for valid providers
x402_arguments_not_allowed403project_id/collection_id blocked without bearer token
x402_replay_detected402Nonce already used — generate a fresh nonce per call
x402_settlement_failed402Facilitator rejected settlement — check network and asset