Payer quickstart — TypeScript
This quickstart wires the SubEtha payment scheme into the official x402 v2
client so that a plain fetch call pays for a 402-gated resource
automatically. Everything on this page targets the local, non-production
stack: loopback URLs, chain 31337, and dev placeholder keys. The code is
adapted from the pinned protocol spec and in-repo payer apps; the flow
expectations at the end describe what those sources define — run the
local end-to-end tutorial to see them for real.
@subetha/x402-scheme 0.1.0 (npm, verified 2026-07-27)
1. Install (published packages only)
Section titled “1. Install (published packages only)”npm install @subetha/x402-scheme @x402/core @x402/fetch@subetha/x402-schemeis the payer-side scheme plugin (Apache-2.0, ESM, Node.js >= 20).viemarrives with it as a regular dependency.@x402/coreand@x402/fetchare the official x402 packages; the pinned product commit builds its payer apps against^2.17.0of both.- No other SubEtha package is needed to pay. In particular there is no facilitator package to install — the facilitator is a source-only external service.
2. Configure the environment (local placeholders)
Section titled “2. Configure the environment (local placeholders)”Set the paying account and RPC endpoint in the environment, never in code.
These are the same SUBETHA_* names the product’s agent tools and the Python
client read — the full table is in the
configuration reference:
# Local anvil dev account only — placeholder; never a real key, never committed.export SUBETHA_PAYER_PK=0x…# Local anvil chain (31337).export SUBETHA_RPC_URL=http://127.0.0.1:8545Quoting an offer needs no key at all; only paying does.
3. Configure SubethaSchemeClient and wrap fetch
Section titled “3. Configure SubethaSchemeClient and wrap fetch”The scheme client structurally implements the official SchemeNetworkClient,
so it registers on the official x402Client like any other scheme, and the
official wrapFetchWithPayment does the rest (this is the payer wiring the
pinned
docs/PROTOCOL.md §6
defines):
import { SubethaSchemeClient } from "@subetha/x402-scheme";import { x402Client } from "@x402/core/client";import { wrapFetchWithPayment } from "@x402/fetch";import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount(process.env.SUBETHA_PAYER_PK as `0x${string}`);
const scheme = new SubethaSchemeClient({ account, mode: "permit", // gasless default; "self-transfer" broadcasts the burn itself rpcUrl: process.env.SUBETHA_RPC_URL ?? "http://127.0.0.1:8545",});
const client = new x402Client().register("eip155:31337", scheme);const fetchWithPay = wrapFetchWithPayment(fetch, client);
// A locally running provider (the in-repo demo provider listens on :4031).const res = await fetchWithPay("http://127.0.0.1:4031/api/complete", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ prompt: "hi" }),});console.log(res.status, await res.json());Two constructor behaviors worth knowing before the first run
(client.ts):
- Construction requires
rpcUrlor an injectedpublicClient. - In the default
"permit"mode the client fail-fasts when an offer lacksextra.helper— it never silently degrades to a gas-paying transfer. The gasless path therefore needs the facilitator configured with a deployedPermitBurner(settlement.permit_burner); with a bare facilitator config, usemode: "self-transfer"and the payer broadcasts the burn itself, paying gas.
Offers can carry a facilitator fee. parseFee(requirements) exposes
{ fee, bps, quoted, listPrice }, and feeAwarePolicy() is a ready-made
PaymentPolicy that ranks offers by the quoted total — the payer’s true
spend — never by list price. Details in the
TypeScript API reference.
4. What to expect: 402 → payment → accepted → finalized
Section titled “4. What to expect: 402 → payment → accepted → finalized”With a local provider and facilitator running, the pinned sources define this sequence for the call above:
- 402 — the first request is unpaid; the provider answers
402 Payment Requiredwith aPAYMENT-REQUIREDheader carrying the offer, whosepayTois a fresh one-time burn address. - Payment —
wrapFetchWithPaymentintercepts the 402, hasSubethaSchemeClientbuild thesubetha-zerc20payload (permit mode: two signatures, no transaction), and retries withPAYMENT-SIGNATURE. - Accepted — the facilitator’s settle succeeds, the response arrives
with your resource body and a
PAYMENT-RESPONSEheader whoseSettleResponsehassuccess: trueandextra.phase: "accepted". This is the terminal state of the HTTP request. - Finalized — later and asynchronously, the facilitator’s finalize loop
drives the proof-gated mint. No HTTP response ever says
finalized; accepted vs finalized explains why the distinction is load-bearing.
A client without the SubEtha scheme handler simply skips subetha-zerc20
offers — standard x402 scheme filtering.
Next steps
Section titled “Next steps”- Local end-to-end tutorial — bring up the whole stack and watch all four states.
- Payer quickstart — Python — the same role with spending-policy guardrails built in.
- TypeScript API reference — every export of the two published packages.