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
pip install "x402[evm]" httpx
Python — complete working example
import asyncio, json, os
from eth_account import Account
from x402 import x402Client
from x402.http import (
    PAYMENT_REQUIRED_HEADER, X_PAYMENT_HEADER,
    decode_payment_required_header, encode_payment_signature_header,
)
from x402.mechanisms.evm.exact import register_exact_evm_client
from x402.mechanisms.evm.signers import EthAccountSigner
import httpx

ENDPOINT = "https://mcp.soundside.ai/mcp"
NETWORK  = "eip155:8453"  # Base mainnet

async def main():
    account = Account.from_key(os.environ["WALLET_PRIVATE_KEY"])
    signer  = EthAccountSigner(account)
    payment_client = x402Client()
    register_exact_evm_client(payment_client, signer, networks=NETWORK)

    # Initialize MCP session
    with httpx.Client(timeout=30) as c:
        r = c.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"})
        session_id = r.headers.get("mcp-session-id")

    headers = {"Content-Type": "application/json",
               "Accept": "application/json, text/event-stream",
               "mcp-session-id": session_id}

    async def call_tool(tool, args, timeout=60):
        payload = {"jsonrpc": "2.0", "id": "2", "method": "tools/call",
                   "params": {"name": tool, "arguments": args}}
        with httpx.Client(timeout=timeout) as c:
            r = c.post(ENDPOINT, json=payload, headers=headers)
        if r.status_code == 402:
            pr = decode_payment_required_header(
                r.headers.get(PAYMENT_REQUIRED_HEADER))
            sig = encode_payment_signature_header(
                await payment_client.create_payment_payload(pr))
            with httpx.Client(timeout=timeout) as c:
                r = c.post(ENDPOINT, json=payload,
                           headers={**headers, X_PAYMENT_HEADER: sig})
        return r.json() if r.status_code == 200 else r.text

    # Generate text — ~$0.01
    print(await call_tool("create_text",
          {"prompt": "Write a haiku about Base blockchain."}))

    # Generate image — ~$0.04
    print(await call_tool("create_image",
          {"provider": "minimax", "prompt": "A futuristic city at sunset"}))

asyncio.run(main())

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.39", "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.01ceiling; charged as quoted
create_imageminimax / luma / grok / vertex / runway$0.01–$0.04varies by provider
create_videoluma / minimax / vertex / runway / grok$0.28–$3.20async; includes Vertex Veo 3
edit_videosoundside.ai$0.0121 editing actions
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 — use create_text or create_image
x402_async_not_enabled400Tool is async; x402 is sync-only in beta — use /mcp with auth for async
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