Provider quickstart
A SubEtha provider is a normal x402 resource server: the official
@x402/express middleware does the 402/verify/settle choreography, and the
@subetha/provider plugin supplies the subetha-zerc20 scheme. The provider
process holds no chain keys and never touches the chain — everything
settlement-shaped is delegated to an external facilitator over HTTP.
Everything on this page targets the local, non-production stack; the code
mirrors the pinned protocol spec and the in-repo demo provider, and the
expectations at the end are what those sources define.
1. Install (published packages only)
Section titled “1. Install (published packages only)”npm install @subetha/provider @x402/core @x402/express express@subetha/provider(Apache-2.0, ESM, Node.js >= 20) brings@subetha/x402-schemewith it as a regular dependency.@x402/coreand@x402/expressare the official x402 packages; the pinned product commit builds its demo provider against^2.17.0.- There is no facilitator package to install.
@subetha/facilitatoris private/workspace-internal — the facilitator is an external HTTP service you point a URL at, run from product-repo source (facilitator quickstart).
2. Point at an external facilitator
Section titled “2. Point at an external facilitator”# Local facilitator from the facilitator quickstart (loopback, port 4032).export FACILITATOR_URL=http://127.0.0.1:4032SubethaFacilitatorClient implements the official FacilitatorClient
interface over HTTP (/verify, /settle, /supported) plus SubEtha’s
POST /challenge extension — the call that mints a fresh one-time burn
address per offer. That extension exists because only the facilitator (the
seed holder) can derive a burn address; a resource server cannot mint its own
PaymentRequirements the way vanilla x402 sellers do.
3. Mount the scheme server and middleware
Section titled “3. Mount the scheme server and middleware”This is the provider wiring the pinned
docs/PROTOCOL.md §5
defines, arranged as a runnable Express app:
import express from "express";import { paymentMiddleware } from "@x402/express";import { x402ResourceServer } from "@x402/core/server";import { SubethaFacilitatorClient, SubethaSchemeServer, subethaAccepts, subethaHandler,} from "@subetha/provider";
const FACILITATOR_URL = process.env.FACILITATOR_URL ?? "http://127.0.0.1:4032";
const facilitator = new SubethaFacilitatorClient(FACILITATOR_URL);const scheme = new SubethaSchemeServer(facilitator);const server = new x402ResourceServer([facilitator]);server.register("eip155:31337", scheme);await server.initialize(); // hard-fails unless /supported advertises the kind
const app = express();app.use(express.json());app.use( paymentMiddleware( { "/api/*": { accepts: subethaAccepts(scheme, { network: "eip155:31337", price: "1000" }), }, }, server, ),);
// The handler returns a value and never touches `res` — subethaHandler makes// the single response commit after the middleware settles the payment.app.post("/api/complete", subethaHandler(async (req) => ({ body: { answer: 42 } })));
app.listen(4031);Initialization notes from the pinned sources:
await server.initialize()syncs the facilitator’s/supportedand fails fast when thesubetha-zerc20kind is not advertised — start the facilitator first.priceis the list price in token base units; the quotedamount(including any facilitator fee) comes from the facilitator per request.subethaAccepts(schemeServer, { network, price })builds the route’sacceptsentry with a dynamicpayTo: a fresh one-time burn address is minted per unpaid 402, and the client’saccepted.payTois echoed (not re-minted) on the paying request so the middleware’s offer match is deterministic.SUBETHA_PLACEHOLDER_PAYTOexists only for static quote-only route configs — real payment routes usesubethaAccepts.- A handler that throws becomes a 500 and the payment is not settled; on a failed settle the buffered success body is discarded and the client gets a 402 — nothing is served unpaid.
4. What to expect, and current limits
Section titled “4. What to expect, and current limits”With the facilitator up, an unpaid POST /api/complete draws a 402 whose
offer carries only the one-time burn address; a paying client’s retry settles
(accepted — response served with the PAYMENT-RESPONSE header) and the
proof-gated mint finalizes later, asynchronously
(accepted vs finalized).
Limits stated at the pinned commit:
- Single-process assumption. Live offers are cached in memory
(
maxCachedOffersdefault 5000); multi-instance deployments would need a shared offer cache. After a restart, in-flight offers fail the match and clients simply re-pay against a fresh 402. - Verify is point-in-time (TOCTOU). The middleware runs verify → handler → settle; a payer can invalidate its payment between verify and settle, in which case settle fails after your handler already ran. Funds are never at risk (the failed settle yields a 402, nothing is served), but expensive handlers deserve ordinary rate limiting — the pinned release supports only low-cost, non-streaming, side-effect-free handlers.
- The provider holds no keys. The payout destination lives
facilitator-side (
settlement.treasuryor the FeeSplit deployment); the route config carries no payout address or secret.
Next steps
Section titled “Next steps”- Facilitator quickstart — run the external service this page points at.
- Local end-to-end tutorial — the full flow with a payer on the other end.
- TypeScript API reference — the complete
@subetha/providersurface, including offer validation helpers.