Developers
x402 on Robinhood Chain, open.
The no-code gateway covers most sellers. If you run your own server, use the same public facilitator and discovery feed. Building an agent that pays? Start with paying safely.
x402Fi is open source (Apache-2.0): RWA-ID/x402fiBehind the gateway
Paid calls reach your API with two extra headers: X-X402Fi-Secret (from your listing) and X-X402Fi-Payer. Check the secret so the paywall can't be bypassed.
// Your API, behind the x402Fi gateway: reject calls that skip the paywall.
app.use((req, res, next) => {
if (req.get("x-x402fi-secret") !== process.env.X402FI_SECRET) return res.status(403).end();
// Optional: who paid for this call
req.payer = req.get("x-x402fi-payer");
next();
});Facilitator
x402 v2, scheme exact, network eip155:4663, asset USDG. x402Fi pays the gas for settlement and never holds funds.
- POST /api/facilitator/verify
- POST /api/facilitator/settle
- GET /api/facilitator/supported
// Self-hosted paywall that uses the x402Fi facilitator (Robinhood Chain, USDG).
const FACILITATOR = "https://x402fi.dev/api/facilitator";
const requirements = {
scheme: "exact",
network: "eip155:4663",
amount: "10000", // 0.01 USDG (6 decimals); x402Fi minimum
asset: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168",
payTo: "0xYourWallet",
maxTimeoutSeconds: 120,
extra: { name: "Global Dollar", version: "1" },
};
app.get("/rates", async (req, res) => {
const header = req.get("payment-signature") ?? req.get("x-payment");
if (!header) {
const pr = { x402Version: 2, resource: { url: req.url, description: "FX rates", mimeType: "application/json" }, accepts: [requirements] };
return res.status(402).set("PAYMENT-REQUIRED", Buffer.from(JSON.stringify(pr)).toString("base64")).json(pr);
}
const paymentPayload = JSON.parse(Buffer.from(header, "base64").toString());
const body = JSON.stringify({ paymentPayload, paymentRequirements: requirements });
const v = await fetch(FACILITATOR + "/verify", { method: "POST", headers: { "content-type": "application/json" }, body }).then(r => r.json());
if (!v.isValid) return res.status(402).json({ error: v.invalidReason });
const data = await computeRates(); // do the work
const s = await fetch(FACILITATOR + "/settle", { method: "POST", headers: { "content-type": "application/json" }, body }).then(r => r.json());
if (!s.success) return res.status(402).json({ error: s.errorReason });
res.set("PAYMENT-RESPONSE", Buffer.from(JSON.stringify(s)).toString("base64")).json(data);
});Paying safely (for agents)
An agent that pays per call is spending real money on text written by strangers. Four checks keep it honest, using wormhole-x402 by AgentWormhole: open source, offline, and Robinhood Chain USDG is in its trusted domains.
- Scan the 402 before a model reads it. Descriptions are written by the seller and can carry instructions aimed at your agent.
- Sign only through the guard.
guardEvmSignerwraps your wallet so that the only thing it signs is the payment the 402 asked for: the quoted payee, amount, token and chain, for a short window. Everything else it refuses, including approvals and permits. - Set a spending cap. If the seller is the attacker, a matching payment can still be for your whole balance.
spendPolicyis the one control that needs your numbers, so it is off until you set it. - Check what arrived. x402Fi puts the SHA-256 of every paid response on its receipt and in
X-X402Fi-Response-SHA256; compare it with the bytes you got, and scan the text before your agent trusts it because it paid.
// npm install wormhole-x402 viem
import { evmQuoteFromRequirements, guardEvmSigner } from "wormhole-x402/evm";
import { inspectQuoteText } from "wormhole-x402/quotetext";
const USDG = "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168";
// 1. Get the price. A 402 is the seller's text: scan it before a model reads it.
const pr = await (await fetch(url)).json(); // HTTP 402
if (inspectQuoteText(pr).decision !== "allow") throw new Error("402 carries injected text");
const req = pr.accepts.find((a) => a.network === "eip155:4663");
// 2. Sign only through the guard. The quote comes from the 402, never from the model;
// the cap comes from you. Any other payee, amount, token or chain, a long-lived
// authorization, or any other signing request is refused.
const wallet = guardEvmSigner(account, () => evmQuoteFromRequirements(req), {
spendPolicy: {
maxPerPayment: { [USDG]: "50000" }, // $0.05 per call
maxTotal: { [USDG]: "5000000" }, // $5 over this wallet guard's life
},
});
const now = BigInt(Math.floor(Date.now() / 1000));
const signature = await wallet.signTypedData({
domain: { name: req.extra.name, version: req.extra.version, chainId: 4663, verifyingContract: req.asset },
types: { TransferWithAuthorization: [ /* from, to, value, validAfter, validBefore, nonce */ ] },
primaryType: "TransferWithAuthorization",
message: { from: account.address, to: req.payTo, value: BigInt(req.amount),
validAfter: now - 60n, validBefore: now + BigInt(req.maxTimeoutSeconds), nonce },
});
// 3. Pay: the same request, with the signed authorization.
const res = await fetch(url, { headers: { "PAYMENT-SIGNATURE": base64(JSON.stringify(
{ x402Version: 2, resource: pr.resource, accepted: req, payload: { signature, authorization } })) } });
// 4. Check what arrived before the model reads it.
const bytes = new Uint8Array(await res.arrayBuffer());
if (sha256hex(bytes) !== res.headers.get("x-x402fi-response-sha256")) throw new Error("not the response on the receipt");
const text = new TextDecoder().decode(bytes);
const body = (() => { try { return JSON.parse(text); } catch { return text; } })();
if (inspectQuoteText(body).decision === "refuse") throw new Error("paid response carries injected text");The complete, runnable version is scripts/pay-guarded.mjs, and test/buyer-guard.test.ts runs this recipe against x402Fi's own 402s, including the payments it must refuse.
Scan JSON responses as parsed values, as above: the scanner treats quoted text as someone describing an attack rather than making one, and in raw JSON every value is quoted. A clean scan catches careless injections; it does not prove a seller is honest. The guard and the cap are what hold. To check who you are paying, add the verified payee check.
Discovery
Every paid endpoint is published in a Bazaar-style feed that agents and indexers can crawl: /api/discovery/resources (JSON), with a readable view at /discovery.
Verified payee
// Name the payee in your 402 so agents can verify you (x402 payee-name extension)
"extensions": { "payee-name": { "info": { "name": "yourco.402api.eth" } } }
// Agents check it with:
import { verifyPayee, viemReader } from "@x402identity/payee";
const verdicts = await verifyPayee(paymentRequired, origin, viemReader(mainnetClient));Spec: payee-name extension. Records: org.x402.origins and the Robinhood Chain address (coin type 2147488311).
USDG details
- Address
- 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168
- Decimals
- 6
- EIP-712 domain
- name "Global Dollar", version "1", chainId 4663
- Transfer
- transferWithAuthorization (EIP-3009, v/r/s)