AI agents can now pay for things on their own. The x402 protocol reuses HTTP 402 Payment Required so a client — human or agent — pays per API call in USDC, no API keys, no accounts, no subscription. It's a genuinely nice primitive.
But there's a gap that bites the moment an agent acts on what it paid for: the payment rail proves the money moved. It says nothing about the bytes that came back. An agent can pay perfectly and still act on tampered or spoofed data.
So here's what I wired up, and how you can too: an Express endpoint that charges per call in USDC and hands the buyer a signed receipt they can verify before acting on the response. The packages underneath are free and MIT — this is just the assembly, shown plainly.
What we're building
client ──GET /price──▶ 402 Payment Required (USDC terms)
client ──pay USDC───▶ 200 OK + your data + an X-BYTE-Attestation receipt
client ──verify────▶ recompute hash, recover signer → act or refuse
Two small libraries do the work:
-
@foreseal/gate— seller-side Express middleware. Turns any upstream into a paid, attested endpoint. -
@payperbyte/sdk— buyer-side verifier.
Both MIT. You can absolutely wire them yourself; that's the point of this post.
Part 1 — the seller, in one middleware call
import express from "express";
import { trustMiddleware } from "@foreseal/gate";
const app = express();
app.use(express.json());
app.use(
"/price",
trustMiddleware({
upstream: "https://your-api.example.com/data", // your real endpoint
price: { perCallUsdc: "0.01" }, // or { perKBUsdc, floorUsdc }
payTo: "0xYourUSDCAddressOnBase", // where USDC settles
attestation: "delivery", // stamp every paid 200
}),
);
app.listen(3000);
That's the whole integration. An unpaid call gets a 402 with x402 USDC terms. On payment, the gate proxies your upstream and stamps the response with an EIP-712 attestation over the exact bytes it served — byte for byte — in an X-BYTE-Attestation header.
Part 2 — what the buyer sees
curl -i http://localhost:3000/price
# HTTP/1.1 402 Payment Required
# ... x402 payment terms (asset, amount, network, payTo) ...
The client pays in USDC, retries with the payment header, and gets 200 plus the data and the receipt. Standard x402 flow — the gate just adds the receipt.
Part 3 — verify before acting (the part that matters)
This is the half everyone skips. Before your code (or your agent) acts on the response, recompute the hash of the exact bytes and recover the signer. If either is wrong, refuse.
import { verifyFromGatewayResponse, ARBITRUM_SEPOLIA } from "@payperbyte/sdk";
const res = await fetch(url, { headers: paymentHeaders }); // your paid call
const body = await res.text(); // the EXACT bytes
const header = res.headers.get("x-byte-attestation");
const v = await verifyFromGatewayResponse(
body, header, ARBITRUM_SEPOLIA, gatewayAttester, // pin the seller's attester
);
if (!v.verified) throw new Error("refuse: " + v.reason);
// ...safe to act on `body`.
You can prove the mechanic offline, no wallet and no network — sign a sample receipt, then verify it and two attacks:
verify-before-act:
genuine → verified=true received bytes match the attested hash AND signer — safe to act
tampered byte → verified=false HASH MISMATCH — do not act
forged signer → verified=false bad recover — do not act
Accept genuine. Refuse tampered and forged. That's the gate.
The honest part: provenance, not truth
This is important enough to say out loud, because plenty of "verified data" pitches blur it:
- The receipt proves the bytes are authentic and unaltered, signed by the attester you pinned. Tamper-evident, signer-pinned, recomputable.
- It does not prove the data is correct. A genuine receipt over a wrong number still verifies — authentic bytes, garbage value.
So the claim is narrow and useful: "these are genuinely the bytes the seller signed," not "this number is right." You still decide whether to trust the seller. The receipt just removes the question of whether you got their actual bytes. Tell your own users which one you mean.
The one gotcha that trips everyone
The EIP-712 signing domain is anchored at chainId 421614 (Arbitrum Sepolia) — a frozen signing namespace for signature recovery. It is not a settlement chain. Payments settle in USDC on Base mainnet. So you pass ARBITRUM_SEPOLIA to the verifier even though the money moved on Base. Recovery happens in the domain; settlement happens on the rail. Mix them up and your signatures won't recover — budget five minutes of confusion here, then never again.
Testnet today, mainnet when you're ready
Default to base-sepolia: the public x402 facilitator advertises testnet, so the full 402 → pay → 200 → receipt loop works for free with testnet USDC, no keys. (On Base mainnet without a mainnet facilitator the paid route correctly fails closed.)
For real USDC on Base mainnet, point at the Coinbase CDP facilitator:
NETWORK=base
FACILITATOR_AUTH=cdp
CDP_API_KEY_ID=...
CDP_API_KEY_SECRET=...
# and: npm i @coinbase/x402
I ran this end-to-end on Base mainnet — the 402 advertises network: eip155:8453, the canonical Base USDC asset, and your payTo. Develop on testnet, flip one env block for mainnet. That's the only change.
Try it in one second
No install, no wallet — see the whole accept-genuine / refuse-tampered loop locally:
npx @foreseal/demo
It runs offline and shows an agent act on genuine bytes and refuse a tampered byte, a forged signature, a missing receipt, and a forked signing domain — in about a second.
Take it further
- Building an AI agent that should buy data over MCP?
byte-mcp-server(MIT) gives Claude Desktop / Claude Code / Cursor abuy → verify-before-acttool out of the box. - Want the wired-up, deploy-ready versions instead of assembling it yourself? I packaged two starter kits — an x402 + verify-before-act Express kit and an MCP agent kit (server + examples + deploy/integrate guides), $39 each. The libraries above stay free MIT; the kits are the afternoon you'd otherwise spend wiring them.
Either way: if your code acts on data it paid for, verify it first. Provenance is cheap. Acting on tampered bytes isn't.
Questions or corrections welcome in the comments — I'd rather fix something than leave it wrong.
Top comments (0)