Skip to content

BornToTry2022/paygate402

Repository files navigation

PayGate402

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.

PayGate402 seller dashboard — payments priced by the buyer agent's on-chain ERC-8004 reputation

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 GatewayWalletBatched x402 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.

Architecture overview

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
Loading
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.

Demo

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:

PayGate402 landing page

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)

How it works

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
Loading

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");

On-chain agent identity (ERC-8004)

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_ID

This 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.

Reputation-based pricing & gating (ERC-8004)

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 agent

Then 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)

Agent-to-agent jobs (ERC-8183)

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-job

This 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:

PayGate402 dashboard — an ERC-8183 agent-to-agent job, escrowed and released on Arc, with its full lifecycle stepper

See docs/ERC-8183-jobs.md for roles, the full lifecycle, status enum, and caveats.

What's in here

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).

Quick start

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 spent

Watch payments stream in at http://localhost:3000/dashboard.

Verify the 402 without paying

curl -i http://localhost:3000/api/premium/fx-rate          # -> 402 + PAYMENT-REQUIRED header

Arc Testnet facts

  • Chain ID 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app
  • USDC 0x3600000000000000000000000000000000000000 (6-decimal ERC-20 / 18-decimal native gas)
  • Gateway Wallet 0x0077777d7EBA4688BDeF3E311b846F25870A19B9 · CCTP domain 26
  • Gateway facilitator (testnet): https://gateway-api-testnet.circle.com

Troubleshooting

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>.

Notes & caveats

  • Testnet only. Never commit private keys (.env.local and .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-rate route is an indicative mock — wire it to Arc's native StableFX (FxEscrow 0x867650F5eAe8df91445971f14d89fd84F0C9a9f8) for executable quotes.
  • withPaywall records to a JSON file (.data/payments.json) so the app runs with zero external services. For production, swap lib/store.ts for a real database.

Credits

Built on Circle's @circle-fin/x402-batching. Paywall + agent structure adapted from circlefin/arc-nanopayments (Apache-2.0).

About

Drop-in x402 paywall that turns any API into an agent-payable, sub-cent USDC storefront on Circle Arc.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors