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 },
},
},
omnigateFee: { // OMNIGATE facilitator fee, disclosed
facilitator: FACILITATOR, // in the challenge itself so a buyer's
feeAmountAtomic: "5000", // presign check sees the FULL total
feeAsset: USDC_BASE, // before signing: price + fee.
feePayTo: "0x5BBd32e0478e49A7540EF2D760E0B0F9AA6328b2",
feeUsdc: "0.005",
model: "flat per-settlement, optional โ waived for standard payments inside the daily waiver budget; this kit attaches the leg",
buyerTotalAtomic: (BigInt(PRICE) + 5000n).toString(), // $0.025 at the default $0.02 price
note: "The fee rides as a second EIP-3009 authorization at paymentPayload.fee. Buyer total = amount + feeAmountAtomic.",
},
},
};
}
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's 0.005 USDC fee is OPTIONAL: a standard payment with no fee
// leg verifies and settles, the fee waived inside OMNIGATE's daily waiver
// budget (500/day across all payTos, 100/day per payTo, UTC). This kit's
// flow attaches the leg anyway โ a second EIP-3009 authorization at
// paymentPayload.fee (shape: OMNIGATE /supported) โ so kit settlements
// pay it. Your server never touches the leg: you forward the payload
// exactly as received. The challenge discloses the fee up front
// (extensions.omnigateFee above), so a presign check sees the total.
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 optional โ this kit attaches it anyway. OMNIGATE's fee is 0.005 USDC per settlement, flat โ and the fee leg is optional: a standard payment with no fee leg settles with the fee waived inside a daily budget (500 settlements/day across all payTos, 100/day per payTo, UTC); past the budget a fee-less payment is refused until the next UTC day. This kit's flow attaches the leg as a second EIP-3009 authorization at
paymentPayload.fee(the shape is published in OMNIGATE's own machine-readable terms at/supported), so settlements through the kit pay it. No monthly minimum, no percentage of your price. Your server forwards the payload untouched; your price reaches your wallet in full either way. -
Refusals are safe. If a payment arrives with an invalid fee leg, or fee-less after the daily waiver budget is spent,
/verifyrefuses it โfacilitator_fee_requiredโ and nothing is broadcast and no 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)