Live → flash402.xyz · Built on the x402 protocol · Base Sepolia
demo.mp4
This is a case study. The product is live; the source is private (it's on a path to monetization). Below is the architecture and the engineering decisions behind it, with representative code.
Charging for an API today means one of two bad options: bolt on an entire billing stack (accounts, API keys, Stripe, metering, invoicing), or give the data away. Neither fits machine callers — an AI agent can't sign up, hold an API key, or fill in a checkout form.
x402 revives the dormant HTTP 402 Payment Required status code so a caller
pays for a request in the request itself, in stablecoin, with no account.
Flash402 makes that a no-code product: paste any API URL, set a per-call price,
share a gateway link. Humans or agents pay per call; the money settles onchain
in USDC.
sequenceDiagram
participant C as Caller / AI agent
participant G as Flash402 Gateway
participant F as x402 Facilitator
participant U as Your API (upstream)
C->>G: GET /api/gw/{id}
G-->>C: 402 Payment Required (price · asset · payTo)
Note over C: sign EIP-3009 authorization<br/>(a signature, no gas)
C->>G: GET /api/gw/{id} + payment header
G->>F: verify + settle onchain (USDC)
F-->>G: settled ✓
G->>U: proxy the original request (path/query/method intact)
U-->>G: 200 + data
G-->>C: 200 + data (unlocked)
The gateway is a thin, payment-gated reverse proxy. Your API never changes; the proxy sits in front of it and only forwards the request once payment settles.
The whole product hinges on one server file: a catch-all route that proxies any path/query/method to the upstream and records the payment only when the upstream actually succeeds. No separate backend, no SDK for the API owner.
// app/api/gw/[id]/[[...path]]/route.ts
if (upstream.ok) {
const payer = payerFrom(r); // decode the signed payment header
await store.incrementHits(endpoint.id);
await store.recordPayment({ endpointId: endpoint.id, owner: endpoint.owner,
payer, status: "paid", trace: /* real trace */ });
}
export const GET = handle;
export const POST = handle;withX402 gates the handler on status < 400, so a payment is only ever
recorded against a genuine 200 from the real API — never a 402 or an error.
A proxy that fetches any URL a stranger submits is a textbook SSRF risk: point it
at 169.254.169.254 (cloud metadata) or localhost and it leaks internal
services.
The gateway resolves every upstream host and rejects private/reserved ranges — loopback, RFC-1918 private blocks, CGNAT, link-local (including the cloud-metadata address), and their IPv6 equivalents. Crucially, it fetches with redirects handled manually and re-runs that host check on every redirect hop, because a perfectly public URL can 3xx into an internal one. Requests are also bounded by a hard timeout so a slow or hostile upstream can't tie up the proxy.
Payments use the exact scheme over EIP-3009 transferWithAuthorization:
the caller signs a typed-data authorization (no gas, no onchain tx from them);
the facilitator submits it and covers gas. Flash402 adapts a connected viem
wallet into the shape x402's client expects:
// lib/x402-browser.ts
function signerFromWallet(wallet: WalletClient) {
const address = wallet.account!.address;
return {
address,
signTypedData: (msg) => wallet.signTypedData({ account: address, ...msg }),
};
}
const client = new x402Client().register(NETWORK, new ExactEvmScheme(signer));
const paidRes = await wrapFetchWithPayment(fetch, client)(url, { cache: "no-store" });Payment records are revenue history; endpoints have earnings attached. So
nothing is ever hard-deleted. "Delete an endpoint" flips an archived flag (the
gateway 404s, but earnings still count toward lifetime stats); "Clear" on the
payment log flips a hidden flag. There is no DELETE handler at all — the
API layer makes destruction impossible.
// lib/store.ts — soft-hide, never destroy
async hidePayments(owner: string, ids: string[]): Promise<void> {
/* set hidden = true on matching rows; the rows stay in the DB */
}
// app/api/payments/route.ts
// No DELETE handler: payment records are never destroyed.| Layer | Choice | Why |
|---|---|---|
| Framework | Next.js 16 (App Router, Turbopack) | One repo for the landing, the console, and the gateway API — the proxy is just a route handler. Server/client boundary keeps secrets off the client. |
| Payments | x402 (@x402/next, @x402/evm, @x402/fetch) |
Native HTTP-402 rail; withX402 gates route handlers; EIP-3009 gasless settlement. |
| Wallet | wagmi v3 + viem + Reown AppKit | Multi-wallet connect modal; viem wallet client feeds the x402 signer. |
| Data | Supabase (Postgres) | Server-side service_role only; RLS on; soft-delete/soft-hide flags. |
| Hosting | Vercel | Per-request serverless scaling suits spiky pay-per-call traffic; single-push deploys. |
Secrets (SUPABASE_SERVICE_ROLE_KEY, x402 config) live only in server code and
never reach the browser. The gateway, the store, and the SSRF guard are all
server-side.
- Multichain. The protocol is chain-agnostic; Base / Ethereum / Arbitrum /
Polygon have USDC + x402 support. The current single-network limit is the
facilitator, not the code: the public
x402.orgfacilitator settles Base Sepolia only. Mainnet means swapping to a facilitator that covers it (e.g. Coinbase CDP) — one config change. - Payment-log export, per-endpoint analytics, and richer agent-facing docs.
Built by web3xDev