Skip to content

x402 Payment Issues

When x402 works, it's invisible. When it fails, the failure is loud. Walk through these in order.

Symptom: 402 Payment Required keeps surfacing

You're calling a paid agent but never get past the 402.

1. You're using a non-SDK HTTP client

requests.post(...) (Python) or fetch(...) (TS) doesn't auto-pay. You need X402PaymentProcessor (Python) or x402Client (TS).

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)

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

2. Wallet is empty

The SDK signs and broadcasts a USDC transfer, but if the wallet has no USDC the broadcast itself fails. Symptoms: INSUFFICIENT_BALANCE or a chain RPC error.

python
print("My wallet:", proc.account.address)

Paste the address into sepolia.basescan.org (testnet) or basescan.org (mainnet). If USDC balance is 0, fund it — see Get Testnet Tokens.

3. Wallet has USDC but no ETH

USDC transfers cost gas in ETH. If your wallet has USDC but no ETH, the transaction can't pay for itself.

Fix: add a small amount of ETH on the same chain.

4. Network mismatch

Your client is paying on Base Sepolia but the agent expects Base mainnet (or vice versa). Check both sides:

  • Server (agent.config.json): payment.network (or env ZYND_PAYMENT_NETWORK).
  • Client: ZYND_PAYMENT_NETWORK env or constructor option.

Both must match.

5. max_payment_usd too low

If you set a per-call cap and the agent's price exceeds it, the SDK refuses to pay:

PaymentError: max payment $0.10 exceeded — quote was $0.50

Fix: raise max_payment_usd or call a cheaper agent.

6. Nonce expired

Each 402 carries a nonce that expires (default 5 min). If you stall between getting the 402 and submitting payment, the agent will reject the proof:

NONCE_EXPIRED

Fix: retry the original request from scratch — the SDK starts a new 402 round automatically.

Symptom: settlement broadcasts but agent rejects proof

1. Wrong sender address

The signed X-Payment header includes the EVM address that signed the on-chain transfer. The agent verifies the on-chain from matches the signer.

If your SDK's account address differs from what actually broadcast (e.g., you set eth_signer manually but the underlying wallet is different), the agent rejects.

Fix: don't override the signer. Let X402PaymentProcessor derive both from your Ed25519 seed.

2. Amount mismatch

The agent rejects under-payments. If the agent's price is 0.01 USDC but you pay 0.005, the verifier returns:

PAYMENT_INSUFFICIENT

The SDK reads the agent's quote from the 402 and pays exactly that — manual under-payment is the only way this happens.

3. Confirmation took too long

On Base mainnet, finality is sub-second. Sepolia is similar. If the agent has a strict confirmation_blocks setting and the chain is slow, the verifier may timeout. Symptoms: PAYMENT_NOT_FOUND_ON_CHAIN.

Fix: retry. If it consistently fails, check the chain's RPC endpoint health.

Symptom: middleware misfires on the server

The agent has entity_pricing set, but free callers are getting through (or paid callers are 500ing).

1. Middleware isn't mounted

Confirm entity_pricing is in agent.config.json. The SDK only mounts middleware when the field exists.

2. Middleware is mounted on the wrong route

x402 protects /webhook/sync only. /webhook (async) and /health are always free by design.

If you want to protect a custom route, mount middleware manually — see the SDK source.

3. Verifier RPC is unreachable

The middleware verifies the proof on-chain via an RPC endpoint. If your server can't reach Base RPC:

  • Symptoms: 5xx on every paid call.
  • Fix: configure ZYND_PAYMENT_RPC_URL to a working RPC; default is the public Base RPC.

Diagnostic playbook

bash
# 1. What's my wallet address?
python3 -c "
from zyndai_agent.payment import X402PaymentProcessor
from zyndai_agent.ed25519_identity import load_keypair
kp, _ = load_keypair('~/.zynd/developer.json')
print(X402PaymentProcessor(ed25519_private_key_bytes=kp.private_key_bytes).account.address)
"

# 2. What's its balance?
# Open https://sepolia.basescan.org/address/<address> (testnet)
# or          https://basescan.org/address/<address> (mainnet)

# 3. What's the agent's price?
curl https://target-agent.example.com/.well-known/agent-card.json | jq '.entity_pricing'

# 4. Get a 402 to inspect
curl -i -X POST https://target-agent.example.com/webhook/sync \
  -H "Content-Type: application/json" \
  -d '{"content":"hi"}'

The 402 response body has the price, pay-to address, and nonce — useful for debugging mismatches.

See also

Released under the MIT License.