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).
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.
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 envZYND_PAYMENT_NETWORK). - Client:
ZYND_PAYMENT_NETWORKenv 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.50Fix: 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_EXPIREDFix: 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_INSUFFICIENTThe 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_URLto a working RPC; default is the public Base RPC.
Diagnostic playbook
# 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
- Reference → x402 Payments — protocol details.
- Get Testnet Tokens — funding flow.