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. The example uses 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-08-07)
Prerequisites
Section titled “Prerequisites”- Node.js >= 20 —
@subetha/x402-schemeis ESM-only and targets Node 20+. - A running local stack for the reproducible path this page follows:
- anvil on chain 31337 at
http://127.0.0.1:8545, - a provider answering 402s (the in-repo demo
provider listens on
:4031), - a facilitator settling for that provider.
- anvil on chain 31337 at
Without the provider and facilitator running, the script below starts fine but has no 402-gated resource to pay for.
1. Install (published packages only)
Section titled “1. Install (published packages only)”npm install --save-dev tsx@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:8545The 0x… value is a placeholder — the script will not run with it. On the
local stack, replace it with one of the dev account private keys anvil prints
at startup, and make sure that account holds test tokens to pay with (the
tutorial covers funding). Anvil’s dev keys are
well-known shared keys: use them only against chain 31337.
Quoting 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 payerPk = process.env.SUBETHA_PAYER_PK;if (!payerPk) { throw new Error("SUBETHA_PAYER_PK is not set — see step 2 above.");}
const account = privateKeyToAccount(payerPk 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());Save this as pay.mts and run it with tsx — the packages are ESM, so this is
the shortest path that handles both TypeScript and ESM loading:
npx tsx pay.mts(Alternatively, remove the one as `0x${string}` cast, save the file as
pay.mjs, and run node pay.mjs — Node >= 20 runs the ESM file natively.)
On success the script prints the paid response’s status and body — schematically:
200 { …the provider's resource body… }The exact body is whatever the provider serves; the payment outcome itself
travels in the PAYMENT-RESPONSE header described in
section 4.
Two constructor behaviors worth knowing before the first run
(client.ts):
- Construction requires
rpcUrlor an injectedpublicClient. - In the default
"permit"mode the client fails fast 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.