AGENTUTIL
DocsGetting Started

Your First x402 Payment

Walk through a complete x402 payment step by step.

Prerequisites

  • A wallet with USDC on Base (set up a wallet)
  • Some ETH on Base for gas (~$1 is plenty)

The Flow

1. POST request → 402 Payment Required
2. Parse payment requirements
3. Sign EIP-3009 authorization with wallet
4. Retry with X-PAYMENT header
5. Get task_id + poll_token
6. Poll for result

Step by Step

1. Make Initial Request

curl -X POST https://api.agentutil.dev/v1/email.verify \ -H "Content-Type: application/json" \ -d '{"email": "test@gmail.com"}'

Response (402):

{ "x402Version": 2, "accepts": [{ "scheme": "exact", "network": "eip155:8453", "amount": "10000", "resource": "https://api.agentutil.dev/v1/email.verify", "payTo": "0x124CA4b34041D33914f94Cb5Fbaa5e4075EB08D7", "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "extra": { "name": "USD Coin", "version": "2" } }] }

2. Sign the Payment

The 402 response tells you:

  • amount: 10000 units = $0.01 USDC (6 decimals)
  • payTo: Where to send the payment
  • extra.name/version: Required for EIP-712 signature domain

Sign an EIP-3009 TransferWithAuthorization:

const domain = { name: "USD Coin", // from extra.name version: "2", // from extra.version chainId: 8453, // Base mainnet verifyingContract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", }; const message = { from: walletAddress, to: "0x124CA4b34041D33914f94Cb5Fbaa5e4075EB08D7", value: 10000n, validAfter: 0n, validBefore: BigInt(Math.floor(Date.now() / 1000) + 3600), nonce: BigInt("0x" + crypto.randomUUID().replace(/-/g, "")), }; const signature = await wallet.signTypedData({ domain, types, primaryType: "TransferWithAuthorization", message });

3. Build Payment Header

Encode the signature and authorization as base64:

const paymentPayload = { x402Version: 2, scheme: "exact", network: "eip155:8453", payload: { signature, authorization: { from: walletAddress, to: payTo, value: "10000", validAfter: "0", validBefore: validBefore.toString(), nonce: nonce.toString(), }, }, }; const header = Buffer.from(JSON.stringify(paymentPayload)).toString("base64");

4. Retry with Payment

curl -X POST https://api.agentutil.dev/v1/email.verify \ -H "Content-Type: application/json" \ -H "X-PAYMENT: eyJ4NDAyVm..." \ -d '{"email": "test@gmail.com"}'

Response (202):

{ "task_id": "task_abc123", "poll_token": "pt_eyJhbGci...", "status": "pending", "poll_url": "/v1/tasks/task_abc123" }

5. Poll for Result

Use the poll_token — no additional payment needed:

curl https://api.agentutil.dev/v1/tasks/task_abc123 \ -H "Authorization: Bearer pt_eyJhbGci..."

Response (when complete):

{ "id": "task_abc123", "tool": "email.verify", "status": "complete", "output": { "email": "test@gmail.com", "deliverable": false, "status": "undeliverable", "quality_score": 0 } }

What Happened?

  1. 402 Response — Server said "pay me first"
  2. EIP-3009 Signature — Your wallet authorized USDC transfer (no gas spent yet)
  3. X-PAYMENT Header — Sent signed authorization with retry
  4. CDP Facilitator — Coinbase verified signature and settled payment on Base
  5. Task Created — You got task_id + poll_token
  6. Free Polling — Poll token lets you check status without more payments

Verify On-Chain

Check your wallet on basescan.org:

  • USDC transfer for $0.01
  • To: 0x124CA4b34041D33914f94Cb5Fbaa5e4075EB08D7
  • On Base mainnet

Full Script

Copy-paste this complete example:

import { createWalletClient, http } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { base } from "viem/chains"; const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`; const API_URL = "https://api.agentutil.dev/v1/email.verify"; const USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; async function main() { const account = privateKeyToAccount(PRIVATE_KEY); const wallet = createWalletClient({ account, chain: base, transport: http(), }); console.log("Wallet:", account.address); // Step 1: Make request, get 402 console.log("\n1. Requesting email.verify..."); const res1 = await fetch(API_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: "test@gmail.com" }), }); if (res1.status !== 402) { throw new Error(`Expected 402, got ${res1.status}`); } const paymentReq = await res1.json(); const offer = paymentReq.accepts[0]; console.log("Payment required:", offer.amount, "units (~$0.01)"); // Step 2: Sign EIP-3009 authorization console.log("\n2. Signing payment authorization..."); const validAfter = 0n; const validBefore = BigInt(Math.floor(Date.now() / 1000) + 3600); const nonce = BigInt("0x" + crypto.randomUUID().replace(/-/g, "")); const domain = { name: offer.extra.name, version: offer.extra.version, chainId: 8453, verifyingContract: USDC as `0x${string}`, }; const types = { TransferWithAuthorization: [ { name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" }, { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }, ], }; const message = { from: account.address, to: offer.payTo as `0x${string}`, value: BigInt(offer.amount), validAfter, validBefore, nonce, }; const signature = await wallet.signTypedData({ domain, types, primaryType: "TransferWithAuthorization", message, }); // Step 3: Build payment header const paymentPayload = { x402Version: 2, scheme: "exact", network: "eip155:8453", payload: { signature, authorization: { from: account.address, to: offer.payTo, value: offer.amount, validAfter: validAfter.toString(), validBefore: validBefore.toString(), nonce: nonce.toString(), }, }, }; const paymentHeader = Buffer.from(JSON.stringify(paymentPayload)).toString("base64"); // Step 4: Retry with payment console.log("\n3. Submitting with payment..."); const res2 = await fetch(API_URL, { method: "POST", headers: { "Content-Type": "application/json", "X-PAYMENT": paymentHeader, }, body: JSON.stringify({ email: "test@gmail.com" }), }); if (!res2.ok) { const err = await res2.text(); throw new Error(`Payment failed: ${res2.status} ${err}`); } const task = await res2.json(); console.log("Task created:", task.task_id); console.log("Poll token:", task.poll_token.slice(0, 20) + "..."); // Step 5: Poll for result console.log("\n4. Polling for result..."); let result; for (let i = 0; i < 10; i++) { await new Promise((r) => setTimeout(r, 1000)); const res3 = await fetch(`https://api.agentutil.dev/v1/tasks/${task.task_id}`, { headers: { Authorization: `Bearer ${task.poll_token}` }, }); result = await res3.json(); console.log(` Attempt ${i + 1}: ${result.status}`); if (result.status === "complete") break; } console.log("\n✅ Result:", JSON.stringify(result.output, null, 2)); } main().catch(console.error);

Run It

npm install viem PRIVATE_KEY=0x... npx tsx first-payment.ts

Next Steps