You have an API. Agents want to pay for it per call, in USDC, with no accounts and no invoices. That is exactly what the x402 protocol does: your route answers HTTP 402 with a payment challenge, the buyer's client signs a stablecoin authorization, a facilitator verifies and settles it on-chain, and your server serves the response. The wiring is an afternoon of glue code — unless you copy a kit that already runs.
This is that kit, and the facilitator it settles through by default is OMNIGATE, Script Master Labs' x402 facilitator on Base and Arc. The full kit — Express, FastAPI, and Hono versions, plus the proof — lives on the canonical page: the OMNIGATE starter kit. This article walks the Express version end to end.
What you are building
One paid route with five steps:
-
Challenge — an unpaid call gets HTTP 402 with your price, your wallet, and a discovery declaration, in the JSON body and the
PAYMENT-REQUIREDheader. -
Signature — the buyer's x402 v2 client signs an EIP-3009 authorization for your price, plus the facilitator's fee leg, and retries with the
PAYMENT-SIGNATUREheader. -
Verify — your server forwards the payload to the facilitator's
POST /verify. Free, read-only, and a refusal here broadcasts nothing. -
Settle — your server calls
POST /settle; the facilitator broadcasts. Your price lands in your wallet. -
Serve — you return the resource with a
PAYMENT-RESPONSEheader, and your resource is now listed in the facilitator's discovery index.
The Express middleware
// ─── x402 starter kit: Express ─────────────────────────────────────────────
// One paid route, settled through OMNIGATE (Script Master Labs' facilitator).
// Requires: npm i express (Node 18+ for global fetch)
// Verified 2026-11-04 against OMNIGATE's live /supported, a live 402
// challenge from a production SML route, and live /verify + /settle calls.
import express from "express";
const FACILITATOR = "https://mcp-x402.onrender.com"; // OMNIGATE
const PAY_TO = "0xYourWalletAddress"; // ← your wallet: settlement lands here
const PRICE = "20000"; // $0.02 — USDC atomic units (6 decimals)
const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
// The payment requirements your route advertises in its 402 challenge.
// Shape copied from a live SML production challenge (2026-11-04).
const REQUIREMENTS = {
scheme: "exact",
network: "eip155:8453", // Base (OMNIGATE also settles Arc, eip155:5042)
amount: PRICE,
asset: USDC_BASE,
payTo: PAY_TO,
maxTimeoutSeconds: 60,
extra: { name: "USD Coin", version: "2" }, // EIP-712 domain of Base USDC
};
function challenge(url, description, example) {
return {
x402Version: 2,
error: "payment_required",
resource: { url, description, mimeType: "application/json" },
accepts: [REQUIREMENTS],
extensions: {
bazaar: { // discovery declaration — this is
discoverable: true, // what gets you indexed on settle
info: {
input: { type: "http", method: "GET" },
output: { type: "json", example },
},
},
},
};
}
async function facilitator(path, paymentPayload) {
const r = await fetch(FACILITATOR + path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
x402Version: 2,
paymentPayload,
paymentRequirements: REQUIREMENTS,
}),
});
return r.json();
}
// OMNIGATE takes its 0.005 USDC fee as a second EIP-3009 authorization the
// buyer's client attaches at paymentPayload.fee (shape: OMNIGATE /supported).
// Your server never touches it — you forward the payload exactly as received,
// and /verify + /settle refuse a settlement whose fee leg is missing.
export function requirePayment(description, example) {
return async (req, res, next) => {
const url = `${req.protocol}://${req.get("host")}${req.originalUrl}`;
const body = challenge(url, description, example);
const header = req.get("PAYMENT-SIGNATURE"); // x402 v2 payment header
if (!header) {
return res
.status(402)
.set("PAYMENT-REQUIRED", Buffer.from(JSON.stringify(body)).toString("base64"))
.json(body);
}
let paymentPayload;
try {
paymentPayload = JSON.parse(Buffer.from(header, "base64").toString("utf8"));
} catch {
return res.status(402).json(body);
}
const verify = await facilitator("/verify", paymentPayload);
if (!verify.isValid) {
return res.status(402).json({ ...body, error: verify.invalidReason || "invalid_payment" });
}
const settle = await facilitator("/settle", paymentPayload);
if (!settle.success) {
return res.status(402).json({ ...body, error: settle.errorReason || "settlement_failed" });
}
res.set("PAYMENT-RESPONSE", Buffer.from(JSON.stringify(settle)).toString("base64"));
next();
};
}
const app = express();
app.get(
"/report",
requirePayment("One full report", { ok: true }),
(req, res) => res.json({ ok: true, report: "your paid payload here" })
);
app.listen(3000, () => console.log("paid API on :3000 — try: curl -i localhost:3000/report"));
Change two values — PAY_TO (your wallet; settlement lands there and nowhere else) and PRICE — and run it. curl -i localhost:3000/report should answer 402 with the challenge in the body and the PAYMENT-REQUIRED header. That shape is not invented: it is copied from a production route's live 402, byte for byte in the fields that matter.
Three details are doing real work in that code:
-
The
bazaarextension is the listing. A facilitator's discovery index is its settlement record. Settle a payment that carries the discovery declaration and your resource appears in OMNIGATE's /discovery/resources index automatically — 129 resources at the 2026-11-04 read. There is no submission form. -
The fee leg is the buyer's, not yours. OMNIGATE charges 0.005 USDC per settlement, flat, from the first settlement — no free allowance, no monthly minimum, no percentage of your price. The buyer's client attaches it as a second EIP-3009 authorization at
paymentPayload.fee(the shape is published in OMNIGATE's own machine-readable terms at/supported). Your server forwards the payload untouched; your price reaches your wallet in full. -
Refusals are safe. If a payment arrives without a valid fee leg,
/verifyrefuses it — in our test runs it answeredfacilitator_fee_required— and nothing is broadcast and neither authorization is consumed. Your route just re-issues the 402.
Why settle through OMNIGATE instead of wiring settlement yourself
The honest comparison, because a kit should say what it replaces. Doing this yourself means verifying EIP-3009 signatures, managing nonces and replay protection, running chain RPC, broadcasting transactions, and absorbing every edge case — and when you finish, no discovery index anywhere lists you, because no settlement record exists for one to read.
OMNIGATE does the same job for $0.005 per settlement, settled in USDC on Base and Arc, and throws in the thing you cannot build for yourself: the settlement is the listing. Your first settled payment puts you in an index of 129 resources, and that index feeds SML's public /x402-directory aggregation — 16,963 resources across all sources at its 2026-11-03 read. As for behaving correctly under load, SML's own fleet is the measured sample: a 2026-11-03 failure-mode audit found 224 of 241 (93%) of SML's unique listed resources across every public index answering a clean, parseable 402.
Tested, not just written
Before publishing this kit we ran all three snippets as local servers against the live facilitator on 2026-11-04:
- Unpaid call → HTTP 402, challenge keys exactly
x402Version / error / resource / accepts / extensions, header matching body. - Payment attempt with a deliberately broken signature and no fee leg → OMNIGATE's
/verifyrefused it withfacilitator_fee_required; the middleware surfaced that as a fresh 402. Nothing settled, nothing broadcast.
The FastAPI and Hono versions run the identical flow and passed the identical tests; both are on the canonical kit page next to this one. Arc is a second accepts entry away (network eip155:5042, asset 0x3600000000000000000000000000000000000000).
Honest limits
OMNIGATE's published settlement proof is SELF-PROOF: the operator's wallet settling to the operator's recipient, proving the rails settle end to end and charge exactly the published fee. It does not prove outside adoption — no external seller has settled through OMNIGATE yet, and the 129 resources in its index today are SML's own fleet. This kit is the open door; the index will show the first outside resource the day one settles. And a listing is not demand: the metric that matters is unique paying wallets over time, which no index publishes.
Selling on XRPL, Xahau, or Solana instead? Those rails run through SML's second facilitator, 402Proof, and are partner-keyed by design — the full fee table for every rail and the partner-key request block are on /settle-through-sml.
Script Master Labs LLC is a Service-Disabled Veteran-Owned Small Business (UEI G24VZA4RLMK3 · CAGE 21U51), operated by Timothy Walton in Kinston, North Carolina.
Top comments (0)