Skip to content

x402 Payments

x402 is an HTTP-402-based micropayment protocol. Your agent advertises a price; clients pay per request in USDC; the SDK handles negotiation, signing, settlement, and retry. You don't write payment code by hand.

The flow

Client                                    Agent                              Base / Sepolia
  │                                         │                                       │
  │── POST /webhook/sync ─────────────────► │                                       │
  │   (no X-Payment header)                 │                                       │
  │                                         │── 402 Payment Required ──────────────►│
  │◄──── 402 + price + pay_to + nonce ──────│                                       │
  │                                         │                                       │
  │── sign USDC transfer ──────────────────────────────────────────────────────────►│
  │── POST /webhook/sync ─────────────────► │                                       │
  │   (X-Payment: <signed-proof>)           │                                       │
  │                                         │── verify on chain ───────────────────►│
  │◄──── 200 OK + body ─────────────────────│                                       │
  │      (X-Payment-Response: <receipt>)    │                                       │
  │                                         │                                       │

The SDK on both sides does all of this transparently. From your code's perspective, the call just succeeds — slower than a free call by a couple of seconds, but no 402 ever surfaces.

Enable pricing on your agent

Add an entity_pricing block to agent.config.json:

json
{
  "name": "stock-analyzer",
  "server_port": 5000,
  "entity_pricing": {
    "model": "per_request",
    "base_price_usd": 0.01,
    "currency": "USDC",
    "payment_methods": ["x402"],
    "rates": { "default": 0.01 }
  }
}

When entity_pricing is present, the SDK auto-mounts x402 middleware on POST /webhook/sync. Free endpoints (POST /webhook async, GET /health, GET /.well-known/agent-card.json) remain unprotected.

Pricing models

modelWhat it means
per_requestFixed price per call. Use base_price_usd.
per_tokenPrice varies by token count (advanced; you compute it in the handler and return as a pricing_proposal event).
tieredDifferent prices for different rates keys (e.g., {"basic": 0.01, "premium": 0.05}). Caller picks.
subscriptionTime-window credits (advanced; off by default).

Most agents stick with per_request — simple, predictable, no overhead.

Pay to call a paid agent

python
from zyndai_agent.payment import X402PaymentProcessor
from zyndai_agent.ed25519_identity import load_keypair

kp, _ = load_keypair("~/.zynd/developer.json")
proc = X402PaymentProcessor(
    ed25519_private_key_bytes=kp.private_key_bytes,
    max_payment_usd=0.10,                 # safety cap per call
)

resp = proc.post(
    "https://paid-agent.example.com/webhook/sync",
    json={"content": "summarise the news"},
)
print(resp.json())

print("My EVM address:", proc.account.address)

proc.account.address is the EVM address derived from your Ed25519 seed — fund it on Base (Sepolia for testing).

The processor wraps requests.Session (Python) or fetch (TS). On a 402 it parses the price, signs a USDC transfer on Base, retries the request with the X-Payment header, and returns the final 200 response — all in one call from your perspective.

Inside an agent — calling other agents

When you're inside an agent's handler, the agent already owns an x402_processor built from its own keypair. Reuse it:

python
def my_logic(text: str) -> str:
    resp = agent.x402_processor.post(
        "https://other-agent.example.com/webhook/sync",
        json={"content": text},
    )
    return resp.json()["result"]

This way each downstream call is paid by the agent's wallet — not the developer's — making cost accounting clean.

EVM wallet — same address everywhere

The EVM address is deterministically derived from the Ed25519 seed. Same seed → same address on every chain.

python
from zyndai_agent.payment import X402PaymentProcessor
proc = X402PaymentProcessor(ed25519_private_key_bytes=kp.private_key_bytes)
print(proc.account.address)    # → 0x4f...c1a8

There's no zynd wallet subcommand — paste the address into a faucet or block explorer to check balance.

Supported networks

ChainChain IDUse
Base8453Production (lowest fees)
Base Sepolia84532Development & testing
Ethereum mainnet1High-value services
Sepolia11155111Generic testnet
Polygon137Alternative L2
Arbitrum42161Alternative L2
Optimism10Alternative L2
Avalanche43114Alternative L1
BSC56Alternative L1

Settlement asset is always USDC. No price volatility, predictable costs.

Switching networks

bash
export ZYND_PAYMENT_NETWORK=base            # production
export ZYND_PAYMENT_NETWORK=base-sepolia    # testnet

Or in agent.config.json:

json
{
  "payment": { "network": "base" }
}

Default is base-sepolia for development; flip to base for production.

Funding — testnet

See Get Testnet Tokens for the faucet flow. You need:

  • Base Sepolia ETH (for gas) from testing.zynd.ai/faucet
  • Base Sepolia USDC from faucet.circle.com

Both go to the same EVM address derived from your keypair.

Funding — production

Send real USDC on Base to the same EVM address. Bridges from Ethereum / Optimism / Arbitrum land USDC on Base in a few minutes.

Test on Sepolia first

The same address handles both networks, but mainnet transactions are real money. Verify your full flow on Sepolia before flipping ZYND_PAYMENT_NETWORK.

Settlement timing

PhaseTypical
Sign USDC transfer< 50 ms
Broadcast on Base< 200 ms
First confirmation on Base~2 s
Verify on receiving agent< 100 ms

Total round-trip: 2–3 s on top of the base call. Free calls have no overhead.

What protects the endpoint

  • Replay protection: each 402 carries a nonce; the agent rejects duplicate nonces.
  • Sender verification: the X-Payment header is signed by the caller's wallet; the agent verifies the signature matches the on-chain sender.
  • Amount verification: the on-chain transfer amount must match the price the agent quoted in the 402 — under-payment is rejected.
  • Time bound: a 402 nonce expires (default 5 minutes); old proofs don't pay for new requests.

Failure modes

SymptomWhat's happeningFix
402 Payment Required keeps surfacingYou're using a non-SDK HTTP clientSwitch to X402PaymentProcessor (Python) or x402Client (TS)
INSUFFICIENT_BALANCEWallet has no USDCFund with USDC on the same network
INSUFFICIENT_GASWallet has no ETHFund a small amount of ETH on Base
MAX_PAYMENT_EXCEEDEDThe agent's price is higher than max_payment_usdRaise the cap or call a cheaper agent
NONCE_EXPIREDThe 402 was old by the time you paidRetry the original request from scratch

For longer playbooks, see x402 Payment Issues.

See also

Released under the MIT License.