Turn any API route into an agent-payable, sub-cent USDC storefront on Circle Arc.
PayGate402 is a drop-in withPaywall() wrapper for Next.js routes. An unpaid request gets
HTTP 402 Payment Required; an autonomous agent retries with a signed authorization and gets the
resource. Payments settle in USDC on Arc Testnet via Circle Gateway batching — which
amortizes gas across thousands of payments and makes prices as low as $0.000001 economically
real. No accounts, no API keys: payment is identity.
Each payment carries the buyer's on-chain agent identity and reputation (ERC-8004 agent #668408, reputation 80/100). The seller prices and gates endpoints by that reputation — while the actual spend comes from disposable per-run wallets. The same live dashboard also visualizes ERC-8183 agent-to-agent jobs: each job's escrow lifecycle (
Created → Funded → Submitted → Completed → Rated) animates as a tx-linked stepper as it settles on Arc — see below ↓.
Why Arc specifically? Gateway batches many offchain EIP-3009 authorizations into a single onchain settlement (the
GatewayWalletBatchedx402 scheme), and USDC is Arc's native gas token. On a generic chain, per-call gas would dwarf a $0.001 charge — this pattern only pencils out here.
PayGate402 stacks four of Arc's agentic-economy primitives into one coherent loop — an agent gets an identity, earns reputation by paying reliably, is priced and gated by that reputation, and can be hired by other agents for escrowed work — all settling in USDC, all observable on a live dashboard.
flowchart TB
subgraph BUYER["Buyer agent"]
W["Ephemeral spend wallet · GatewayClient"]
AID["ERC-8004 identity #668408 · signed proof-of-control"]
end
subgraph SELLER["PayGate402 · Next.js seller"]
PW["withPaywall — 402 challenge · verify · settle"]
GATE["Reputation gate + dynamic price"]
JOBRUN["run-job — create · fund · submit · complete · rate"]
STORE["JSON store · .data/payments.json + jobs.json"]
DASH["Live dashboard"]
end
subgraph ARC["Arc Testnet — USDC is the native gas token"]
GW["Circle Gateway · batched USDC settlement"]
IDR["ERC-8004 IdentityRegistry"]
RPR["ERC-8004 ReputationRegistry"]
AC["ERC-8183 AgenticCommerce · USDC escrow"]
end
W -->|"request → 402 → sign EIP-3009 → retry"| PW
AID -.->|"X-Agent-Id / X-Agent-Signature"| PW
AID -.->|"register()"| IDR
PW --> GATE
GATE -.->|"read score"| RPR
PW -->|"verify + settle"| GW
PW --> STORE
JOBRUN -->|"escrow lifecycle"| AC
JOBRUN -.->|"giveFeedback()"| RPR
JOBRUN --> STORE
STORE --> DASH
| Plane | What it adds | Arc primitive | PayGate402 code |
|---|---|---|---|
| Payments | An agent buys an API call for sub-cent USDC; gas amortized across thousands of payments | Circle Gateway batching (GatewayWalletBatched x402 scheme) |
lib/paywall.ts |
| Identity | Each payer carries a verifiable, on-chain identity — not just an address — proven by signature | ERC-8004 IdentityRegistry |
lib/erc8004.ts, lib/agentauth.ts |
| Reputation | The seller prices and gates endpoints by the agent's on-chain score | ERC-8004 ReputationRegistry |
lib/reputation.ts |
| Jobs | One agent hires another; USDC sits in escrow until an evaluator approves the deliverable | ERC-8183 AgenticCommerce |
lib/erc8183.ts, lib/jobs.ts |
| Observability | Live revenue + job-lifecycle visualization, no external services | zero-dep JSON store | lib/store.ts, lib/jobs.ts, app/dashboard |
The rest of this README walks each plane bottom-up. For a narrative version, see the blog post.
A real run: an autonomous buyer agent — presenting its on-chain ERC-8004 identity (agent #668408) —
discovers and pays for its endpoints in sub-cent USDC, settled via Gateway batching, spending from
disposable per-run wallets. The dashboard above shows the live revenue tagged with that identity — and
the agent's reputation, which the seller uses to price and gate endpoints (see below). In an earlier
high-volume run the same agent made 429 nanopayments totaling $0.50 USDC before auto-stopping at
its --limit. The landing page lists the endpoints:
Any unpaid request returns 402 with a base64 PAYMENT-REQUIRED header describing how to pay:
$ curl -i http://localhost:3000/api/premium/fx-rate # unpaid
HTTP/1.1 402 Payment Required
payment-required: eyJ4NDAyVmVyc2lvbiI6Mi... # base64 — decodes to:
scheme = exact
network = eip155:5042002 # Arc Testnet
amount = 500 # = $0.0005 USDC (6 decimals)
payTo = 0x575162bE... # the seller
extra = { name: "GatewayWalletBatched", verifyingContract: 0x0077777d7EBA…19B9 }
The agent signs an EIP-3009 authorization for that requirement and retries with a
Payment-Signature header; the server verifies + settles via Gateway and returns 200 + the data:
$ npm run agent -- --limit 0.5
Ephemeral agent wallet: 0x8e3c22…
Acting as ERC-8004 on-chain agent #668408 (0x8eaa…B607)
Depositing 1 USDC into Gateway Wallet... done (available: 1)
#1 POST summarize -> 0.002 USDC (388ms) [spent: 0.002000/0.500000]
#2 POST keywords -> 0.001 USDC (438ms) [spent: 0.003000/0.500000]
#3 GET fx-rate -> 0.0005 USDC (361ms) [spent: 0.003500/0.500000]
...
Spent 0.501500 / 0.500000 USDC (limit reached)
sequenceDiagram
participant Agent as Buyer agent
participant API as PayGate402 (seller)
participant GW as Circle Gateway
Agent->>API: request /api/premium/*
API-->>Agent: 402 + PAYMENT-REQUIRED (GatewayWalletBatched)
Note over Agent: sign EIP-3009 authorization (offchain, gasless)
Agent->>API: retry + Payment-Signature header
API->>GW: facilitator.verify() + settle()
GW-->>API: settled (batched onchain)
API-->>Agent: 200 + resource + PAYMENT-RESPONSE
Paywalling a route is one line:
import { withPaywall } from "@/lib/paywall";
const handler = async (req: NextRequest) => NextResponse.json({ ... });
export const POST = withPaywall(handler, "$0.002", "/api/premium/summarize");A wallet address is anonymous and disposable. PayGate402's buyer agent can instead carry a verifiable on-chain identity via ERC-8004 — Arc's native registry for agent identity and reputation.
npm run register-agent # mints an ERC-8004 identity NFT for the buyer wallet, writes AGENT_IDThis calls register(agentURI) on Arc's IdentityRegistry
(0x8004A818BFB912233c491871b3d84c89A494BD9e), pointing at the agent card served at
/.well-known/agent-card.json, and mints an identity NFT. The agent then presents that identity on
every purchase (X-Agent-Id / X-Agent-Address headers), the paywall records it, and the dashboard
links each payment to the agent's on-chain identity.
The result: an agent with a stable, verifiable identity autonomously buys APIs — while paying from throwaway per-run spend wallets. Identity and spend are decoupled.
Identity unlocks the real payoff: the seller can price and gate APIs by an agent's on-chain
reputation. PayGate402 reads the agent's aggregate score from the ERC-8004 ReputationRegistry
(0x8004B663…8713) on every request and adjusts:
// reputable agents (score >= 60) pay half price
withPaywall(handler, "$0.002", "/api/premium/summarize", { discount: { atScore: 60, price: "$0.001" } });
// high-frequency feed only reputable agents may call (HTTP 403 below the bar)
withPaywall(handler, "$0.0001", "/api/premium/firehose", { minScore: 60 });Reputation comes from feedback. The seller rates the agent after it pays reliably (ERC-8004 forbids rating your own agent, so the seller — a different wallet than the agent's owner — is a valid reviewer):
npm run give-feedback -- --score 80 # seller records on-chain feedback for the agentThen the gate flips, live (verified on Arc testnet):
# before feedback (reputation 0)
GET /api/premium/firehose -> 403 reputation gate (requiredScore 60, yourScore 0)
POST /api/premium/summarize -> 402 ... amount 2000 ($0.002, full price)
# after `npm run give-feedback` (reputation 80)
GET /api/premium/firehose -> 402 ... amount 100 ($0.0001, unlocked)
POST /api/premium/summarize -> 402 ... amount 1000 ($0.001, 50% discount)
That's the agentic-commerce loop — identity → reputation → price/access, all on-chain. (Reads are
cached ~30s; getSummary is queried via the agent's getClients list, since it reverts on an empty set.)
Identity is verified, not just claimed. An X-Agent-Id header alone is spoofable — anyone could
claim a reputable agent's id. So PayGate402 only honors a claimed identity when the caller also sends an
X-Agent-Signature over a timestamped message; the server recovers the signer and checks it is the
agent's on-chain owner (ownerOf / getAgentWallet) before consulting reputation. A spoofed id or
forged signature falls through to anonymous (full price, gated endpoints denied):
spoof id, NO signature -> firehose 403, summarize $0.002 (no benefit)
forged signature -> firehose 403, summarize $0.002 (signer != owner)
valid owner signature -> firehose 402, summarize $0.001 (unlocked + discount)
The paywall is "an agent buys an API call." ERC-8183 is "an agent hires another agent to do a job", with USDC held in on-chain escrow until the work is approved — and it reuses the same ERC-8004 identities, closing the loop identity → hired → delivered → paid → reputation.
npm run run-jobThis runs the whole lifecycle on testnet via Arc's AgenticCommerce contract
(0x0747EEf0706327138c69792bF28Cd525089e4583): the client (BUYER) hires a freshly-registered provider
agent, funds escrow, the provider submits a deliverable hash, an evaluator releases payment, and the
client leaves on-chain reputation feedback. A real run:
[1] provider registers an ERC-8004 identity -> agent #719410
[2] createJob -> job #124751 status 0 (Open)
[4] approve + fund (0.05 USDC) status 1 (Funded)
[5] submit deliverable hash status 2 (Submitted)
[6] evaluator approves status 3 (Completed) — provider received 0.05 USDC
[7] client leaves ERC-8004 feedback (job-quality 95)
The dashboard visualizes the whole lifecycle. run-job persists each phase to .data/jobs.json
as it happens, so the dashboard (which polls every 2.5s) animates the job advancing
Created → Funded → Submitted → Completed → Rated — every node links to its on-chain transaction,
and the provider's earned reputation shows as a ★ score:
See docs/ERC-8183-jobs.md for roles, the full lifecycle, status enum, and caveats.
| Path | What it is |
|---|---|
lib/paywall.ts |
The reusable core. withPaywall(handler, price, endpoint) — x402 402 + Gateway verify/settle. |
lib/store.ts |
Zero-dependency JSON payment store (swap for a DB later). |
lib/arc.ts / lib/text.ts |
Arc constants; zero-dep summarizer/keyword helpers. |
lib/erc8004.ts |
ERC-8004 Identity + Reputation registry addresses + ABIs. |
lib/reputation.ts |
Reads an agent's ERC-8004 reputation (cached) for gating/pricing. |
lib/agentauth.ts |
Verifies an agent's signed proof-of-control before trusting its claimed id. |
lib/erc8183.ts |
AgenticCommerce (ERC-8183) job-escrow address + ABI. |
lib/jobs.ts |
Zero-dependency JSON store for the ERC-8183 job lifecycle + step timeline (drives the dashboard). |
public/.well-known/agent-card.json |
The buyer agent's metadata card (its agentURI). |
app/api/premium/* |
Four paid routes: summarize ($0.002, ½ off at rep≥60), keywords ($0.001), fx-rate ($0.0005), firehose ($0.0001, rep-gated ≥60). |
app/api/payments, app/api/jobs, app/api/gateway/balance |
Dashboard data (revenue store + job store + seller Gateway balance). |
app/page.tsx, app/dashboard/page.tsx |
Landing + live dashboard (revenue and the ERC-8183 job-lifecycle stepper). |
agent/buyer.mts |
Autonomous buyer agent (GatewayClient) with a --limit spend cap. |
scripts/generate-wallets.mts |
Creates seller + buyer wallets into .env.local. |
scripts/register-agent.mts |
Mints the buyer's ERC-8004 on-chain identity, writes AGENT_ID. |
scripts/give-feedback.mts |
Seller records on-chain ERC-8004 feedback (reputation) for the agent. |
scripts/run-job.mts |
Runs a full ERC-8183 agent-to-agent job (hire → escrow → deliver → pay → rate). |
npm install
npm run generate-wallets # writes SELLER_* and BUYER_* into .env.local
# Fund BUYER_ADDRESS with Arc Testnet USDC at https://faucet.circle.com
npm run register-agent # (optional) mint the buyer's ERC-8004 on-chain identity
npm run give-feedback -- --score 80 # (optional) seller rates the agent -> unlocks gated/discounted endpoints
npm run dev # seller app on http://localhost:3000
npm run agent -- --limit 0.5 # buyer agent pays your endpoints until 0.5 USDC spentWatch payments stream in at http://localhost:3000/dashboard.
curl -i http://localhost:3000/api/premium/fx-rate # -> 402 + PAYMENT-REQUIRED header- Chain ID
5042002· RPChttps://rpc.testnet.arc.network· Explorerhttps://testnet.arcscan.app - USDC
0x3600000000000000000000000000000000000000(6-decimal ERC-20 / 18-decimal native gas) - Gateway Wallet
0x0077777d7EBA4688BDeF3E311b846F25870A19B9· CCTP domain26 - Gateway facilitator (testnet):
https://gateway-api-testnet.circle.com
Payment verification failed / authorization_validity_too_short — Circle Gateway-testnet
requires the EIP-3009 authorization to stay valid for at least minValiditySeconds (currently
604800 = 7 days, see GET https://gateway-api-testnet.circle.com/v1/x402/supported). The client
builds the window as maxTimeoutSeconds + 600, so maxTimeoutSeconds in lib/paywall.ts must be
≥ ~604200. We use 691200 (8 days). Heads-up: the upstream circlefin/arc-nanopayments sample —
and even the SDK's own helper — still ship the stale 345600 (4 days), which Gateway now rejects.
To inspect what Gateway accepts for a chain:
curl -s https://gateway-api-testnet.circle.com/v1/x402/supported \
| node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>console.log(JSON.stringify(JSON.parse(s).kinds.find(k=>k.network==='eip155:5042002').extra,null,2)))"When verification fails, the real reason is logged server-side (and to
.next/dev/logs/next-development.log) as [paywall] verify failed (...): <reason>.
- Testnet only. Never commit private keys (
.env.localand.data/are gitignored). - Circle Agent Wallet spending policies are mainnet-only; on testnet the agent enforces the cap
itself via
--limit. - Settlement IDs shown in the dashboard are Gateway batch references (off-chain batched), not per-payment on-chain tx hashes — that's the whole point of batching.
- The
fx-rateroute is an indicative mock — wire it to Arc's native StableFX (FxEscrow0x867650F5eAe8df91445971f14d89fd84F0C9a9f8) for executable quotes. withPaywallrecords to a JSON file (.data/payments.json) so the app runs with zero external services. For production, swaplib/store.tsfor a real database.
Built on Circle's @circle-fin/x402-batching.
Paywall + agent structure adapted from circlefin/arc-nanopayments
(Apache-2.0).


