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.