x402 payment middleware and facilitator client — gate any HTTP route behind a stablecoin micropayment in a few lines. Built on the x402 protocol (HTTP 402 "Payment Required" revived for machine-to-machine payments).
- Framework adapters for Next.js App Router and Express/Connect, plus a framework-agnostic core.
- Verification and on-chain settlement are delegated to a facilitator — the hosted Furlpay facilitator by default, or point at your own.
- USDC on Base and Solana resolved automatically; any SPL/ERC-20 asset via config.
These are different capabilities and this README used to blur them:
| Base | Arbitrum | Solana | |
|---|---|---|---|
| Middleware can emit a 402 | yes | yes | yes |
| Hosted facilitator verifies + settles | yes | yes | no |
The middleware is chain-agnostic — buildRequirements() resolves the right USDC
asset and returns a well-formed 402 for any network you configure. Settlement is
delegated to a facilitator, and the hosted Furlpay facilitator verifies EIP-3009
authorizations by EIP-712 ecrecover (plus ERC-1271 for contract wallets). That
is an EVM mechanism. Solana uses a different signature scheme, so the verifier
returns svm_verification_unavailable rather than accepting something it has not
checked — it fails closed, but it does fail.
So: point the middleware at Solana if you are running a facilitator that can settle SVM. If you are using the hosted default, use an EVM network.
- Zero runtime dependencies. Node 18+. TypeScript types included.
AI agents, scripts, and API clients cannot fill out card checkout forms. x402 lets a server answer 402 Payment Required with machine-readable payment requirements; the client signs a stablecoin authorization and retries with an X-PAYMENT header; the server verifies, settles on-chain, and serves the request. No accounts, no API keys, no subscriptions — pay per call.
npm install @furlpay/x402// app/api/premium/route.ts
import { withX402 } from "@furlpay/x402";
export const GET = withX402(
async () => Response.json({ data: "premium market signal" }),
{
payTo: "0xYourReceivingAddress",
network: "base", // or "solana"
amount: "10000", // atomic units of the asset; USDC has 6 decimals, so "10000" = $0.01
description: "Premium data",
}
);Unpaid requests receive a 402 with the payment requirements in the body; paid requests are settled and served with an X-PAYMENT-RESPONSE receipt header.
import express from "express";
import { expressX402 } from "@furlpay/x402";
const app = express();
app.get(
"/premium",
expressX402({ payTo: "0xYourAddress", network: "base", amount: "10000" }),
(req, res) => res.json({ data: "premium" })
);gate() implements the whole flow without any framework assumptions:
import { gate } from "@furlpay/x402";
const result = await gate(requestUrl, xPaymentHeaderOrNull, {
payTo: "0xYourAddress",
amount: "10000",
});
if (!result.paid) {
// result.status is 402 (challenge) or 400 (malformed header)
// result.body is the JSON to return
} else {
// result.payer, result.transaction, result.settlementHeader
}| Option | Type | Default | Description |
|---|---|---|---|
payTo |
string |
— | Receiving address on the target network. Required. |
network |
"base" | "solana" |
"base" |
Settlement network. |
asset |
string |
USDC on the chosen network | Token contract / mint address. |
amount |
string |
— | Price in atomic units of asset (USDC: 6 decimals). Required. |
description |
string |
"x402 payment" |
Human-readable description in the 402 challenge. |
facilitator |
string | Facilitator |
Furlpay hosted | Facilitator base URL, or your own Facilitator implementation (useful in tests). |
maxTimeoutSeconds |
number |
300 |
How long the signed payment stays valid. |
Talk to any x402 facilitator's supported / verify / settle API directly:
import { useFacilitator, DEFAULT_FACILITATOR } from "@furlpay/x402";
const facilitator = useFacilitator(); // hosted: https://furlpay.com/api/x402/facilitator
const local = useFacilitator("http://localhost:3000/api/x402/facilitator");
const { kinds } = await facilitator.supported(); // supported (scheme, network) pairs
const verdict = await facilitator.verify(payload, requirements);
const receipt = await facilitator.settle(payload, requirements);useFacilitator(baseUrl, fetchImpl) accepts a custom fetch for proxies and tests.
buildRequirements(resource, config) produces the spec-shaped PaymentRequirements object (scheme exact, resolved asset, timeout) if you need to construct challenges manually.
gate() verifies the presented payload against the requirements it built,
before forwarding anything to a facilitator:
| Checked locally | Refused when |
|---|---|
payTo |
the authorization is addressed to someone else |
| amount | authorization.value < maxAmountRequired (overpayment is fine) |
scheme / network / x402Version |
they differ from the challenge |
| validity window | expired, or not yet valid beyond a 5s skew allowance |
| shape | no signature, or no authorization object |
These hold even when the facilitator is wrong, misconfigured or hostile — which
matters because facilitator is a documented option, so "the hosted one" and
"any third party" are the same code path. A payment that fails them is never
submitted for settlement, so a mismatched payment does not move money on-chain
for a resource that is then refused.
skipLocalVerification: true restores the old delegate-everything behaviour for
a scheme this package cannot interpret. It is an escape hatch, not a tuning
knob: setting it makes any facilitator weakness a full bypass again.
Paid responses and 402 challenges are both stamped Cache-Control: no-store, private and Vary: X-PAYMENT, so a CDN or reverse proxy cannot serve a paid
response to a later unpaid client.
Field checks cannot stop cross-resource substitution: a payment for /a and a
payment for /b at the same price on the same server differ in no field the
authorization carries. The exact scheme simply does not name a resource.
Set bindingSecret and the 402 carries a signed quote the payer echoes back in
extra.quote:
withX402(handler, {
payTo: "0x...",
amount: "10000",
bindingSecret: process.env.X402_BINDING_SECRET, // server-held, never shipped
});The quote is HMAC'd over the resource, price, recipient and network, so a payer can return one but cannot mint one for a different route — and a quote stops verifying the moment any of those terms change.
Opt-in because it is a protocol change for payers, not because it is optional in any security sense.
gate() claims each payment before settling, so a replayed X-PAYMENT header
is refused rather than releasing the resource again. The release rule is the
part that matters:
| Settlement outcome | Claim |
|---|---|
| success | kept — the payment is spent |
| definite failure | released — no money moved, so the payer can retry |
| not yet deep enough | released — that 402 invites a retry |
| threw / timed out | kept — unknown is not "did not happen" |
The default store is in-memory and therefore single-instance only: its
atomicity comes from Node being single-threaded, which is worth nothing across
two workers. Behind a load balancer each instance keeps its own set and a
payment replays once per instance — inject a shared store (Redis SET NX) via
claimStore, or set singleUse: false to leave replay entirely to the
facilitator.
Local checks are field checks, not a signature check — verifying the
authorization's signature needs the chain's curve and is the facilitator's job.
Settlement finality is governed by requireSettlement (see
SettlementStrength), which refuses to release against a transaction the
facilitator has not claimed is strong enough.
That distinction only matters if you change the facilitator — and the facilitator
option exists precisely so you can. Against the hosted Furlpay facilitator you get
the hardened verifier described below. Against your own deployment, or any third
party, you get exactly what that facilitator enforces, plus the local checks above.
The hardened server-side verifier used by the hosted facilitator defends the four published x402 attack classes: authorization (server-side truth for every field), binding (HMAC over resource + method + amount + expiry), replay (single-use nonces and quote ids), and web-layer handling (size-capped, fail-closed header parsing). See the Furlpay security write-up for details.
Add the defenses at your own edge:
@furlpay/x402-guard— request binding (F1), atomic nonce linearization (F2), reserve-commit allowances (F3) and settlement capacity limits (F4), all failure-closed.@furlpay/settlement—assessSettlementdecides whether an observed on-chain state is strong enough to release, given the amount at risk. ASettleResponsereportingsuccesssays a transaction exists; it does not say the transaction is irreversible.
Neither is a dependency of this package, so neither is applied unless you wire it in.
The package ships a node:test suite covering the requirements builder, the 402/400/settlement paths of gate, the Next.js wrapper, and facilitator URL routing:
npm testInject a stub facilitator in your own tests via the facilitator option — no network needed.
- furlpay-solana-actions-template — Solana Actions and Blinks starter
- furlpay-node — the Furlpay API SDK
- Documentation
See CONTRIBUTING.md. Report vulnerabilities privately per SECURITY.md.
MIT