Pay per request with x402
Tutorial — fund a wallet, handle your first 402 Payment Required, sign the payment, and retry successfully. No account, no API key.
Overview
This tutorial takes you from "I want to call ShopSniffer from an autonomous agent with no account" to "I have a completed report". We'll fund a Base-network wallet with USDC, trigger a 402 Payment Required response, sign the payment, and retry successfully. The end state: a working x402-paid request and a reusable pattern for the rest of your agent.
You'll need:
- A Node.js or Python environment (examples in both)
- A wallet with ~$11 of USDC on the Base network (we'll cover getting there)
x402-client(Node) orx402-py(Python) — the official helper libraries
x402 is an open protocol that revives HTTP 402. A server responds 402 Payment Required with on-chain payment instructions; the client pays and retries. ShopSniffer implements x402 only on POST /api/jobs — read endpoints are free. See x402 payment for the protocol reference.
Step 1 — get USDC on Base
You need a small amount of USDC on the Base network — specifically on the official Base USDC contract. Each job costs exactly $9.99 USDC; top up to ~$11 to cover fees and one retry.
Easiest path:
Create or use an EVM wallet
MetaMask, Rabby, or any wallet that supports Base. If you're going fully automated, generate a fresh private key — x402 works great with ephemeral wallets.
Fund with USDC on Base
Options:
- Bridge from Ethereum via the Base Bridge (takes a few minutes, gas required on mainnet)
- Fund via Coinbase — Coinbase supports direct withdrawals to Base for USDC with no bridge step
- Cross-chain swap via Across, LiFi, Squid, or any bridge aggregator
Verify the balance
Open basescan.org and paste your address. Confirm you see USD Coin (USDC) with your balance. If it shows 0, your USDC is on the wrong network.
Don't send USDC from a chain ShopSniffer doesn't accept and expect it to work. The server verifies on-chain settlement on Base specifically. If you pay on Arbitrum, Polygon, or Ethereum L1, the payment proof will fail verification and your request will be rejected.
Step 2 — trigger a 402
First, let's see the 402 response with a naive request that has no auth:
bashcurl -v -X POST https://shopsniffer.com/api/jobs \ -H "Content-Type: application/json" \ -d '{"domain": "allbirds.com"}'
You'll get back:
httpHTTP/1.1 402 Payment Required Content-Type: application/json
json{ "error": "Payment Required", "x402": { "version": "1", "accepts": [ { "scheme": "exact", "network": "base", "maxAmountRequired": "9990000", "resource": "https://shopsniffer.com/api/jobs", "payTo": "0x767131d92c41D56546eC72fecD0F1d63900fa9D9", "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "description": "Create a Shopify store analysis report…" } ] } }
Key fields:
| Field | Meaning |
|---|---|
maxAmountRequired | 9,990,000 smallest USDC units = $9.99 (USDC has 6 decimals) |
payTo | Where to send the payment on Base |
asset | Base USDC contract (verify this before paying!) |
network | Must be base |
Step 3 — pay and retry
The manual way involves constructing an EIP-3009 transferWithAuthorization signature. Don't do that by hand — use an official helper library.
typescriptimport { withX402Payment } from "x402-client"; import { privateKeyToAccount } from "viem/accounts"; // Ephemeral wallet — replace with your own private key management const account = privateKeyToAccount( process.env.WALLET_PRIVATE_KEY as `0x${string}`, ); async function createJob(domain: string) { const res = await withX402Payment( "https://shopsniffer.com/api/jobs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ domain }), }, account, ); if (!res.ok) { const err = await res.text(); throw new Error(`Job creation failed: ${res.status} ${err}`); } const { job } = await res.json(); console.log(`Created job ${job.id}`); return job; } await createJob("allbirds.com");
pythonfrom x402 import X402Client from eth_account import Account import os, requests account = Account.from_key(os.environ["WALLET_PRIVATE_KEY"]) client = X402Client(account=account) def create_job(domain: str): res = client.request( "POST", "https://shopsniffer.com/api/jobs", json={"domain": domain}, ) res.raise_for_status() job = res.json()["job"] print(f"Created job {job['id']}") return job create_job("allbirds.com")
bash# 1. Get the 402 response RESP=$(curl -s -X POST https://shopsniffer.com/api/jobs \ -H "Content-Type: application/json" \ -d '{"domain": "allbirds.com"}') # 2. Extract payTo, asset, maxAmountRequired PAY_TO=$(echo "$RESP" | jq -r '.x402.accepts[0].payTo') ASSET=$(echo "$RESP" | jq -r '.x402.accepts[0].asset') AMOUNT=$(echo "$RESP" | jq -r '.x402.accepts[0].maxAmountRequired') # 3. Sign EIP-3009 transferWithAuthorization (use cast, foundry, or ethers) PROOF=$(cast tx-sign ...) # beyond the scope of this tutorial # 4. Retry with X-PAYMENT header curl -X POST https://shopsniffer.com/api/jobs \ -H "Content-Type: application/json" \ -H "X-PAYMENT: $PROOF" \ -d '{"domain": "allbirds.com"}'
This is educational — in production use the helper libraries.
Step 4 — verify success
A successful x402-paid request returns the normal job creation response:
json{ "job": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "domain": "allbirds.com", "slug": "allbirds-com", "status": "pending", "payment_tx_hash": "0x…", "payment_sender": "0xYourWallet…" } }
The payment_tx_hash and payment_sender fields are your receipt — they record which on-chain transaction paid for which job. Keep them if you need auditing.
From here it's identical to any authenticated job — poll status, subscribe to the WebSocket, fetch the report. See the jobs API for the full lifecycle.
Debugging
Things that go wrong, and how to fix them:
Your X-PAYMENT header is malformed or the on-chain transaction hasn't settled yet. Wait 10-15 seconds and retry once. If it's still failing, check:
- You're using the Base network (chainId 8453), not Ethereum
- The USDC asset address matches the one in the 402 response exactly
maxAmountRequiredis exactly $9.99 — don't underpay
Confirm USDC balance on basescan.org for your wallet address. You also need a small amount of ETH on Base for gas — even EIP-3009 sponsored payments may require a few cents of ETH depending on the client library.
Check the transaction on basescan. If it's Success, the issue is your X-PAYMENT header encoding — the helper libraries handle this but a hand-rolled encoder might produce the wrong signature digest. Use the official client.
This is rare — it means the on-chain settlement succeeded but our verification pipeline hit an error. Save the tx_hash and contact support with the transaction hash. We'll either credit a job or refund the USDC.
When to use x402 vs API keys
| Situation | Use |
|---|---|
| Long-running server integration, predictable usage | API key |
| Autonomous agent spinning up per-task | x402 |
| One-shot scripts that don't want credential management | x402 |
| High-volume daily usage | Pro Monthly subscription (Stripe) |
| CI pipeline running on every PR | API key |
x402 shines when you want zero credential lifecycle management. For steady-state usage, API keys are simpler and cheaper (no gas, no on-chain latency).