Skip to content

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.

applies to @subetha/x402-scheme 0.1.0 (npm, verified 2026-07-27)
Terminal window
npm install @subetha/x402-scheme @x402/core @x402/fetch
  • @subetha/x402-scheme is the payer-side scheme plugin (Apache-2.0, ESM, Node.js >= 20). viem arrives with it as a regular dependency.
  • @x402/core and @x402/fetch are the official x402 packages; the pinned product commit builds its payer apps against ^2.17.0 of 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:

Terminal window
# 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:8545

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 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 rpcUrl or an injected publicClient.
  • In the default "permit" mode the client fail-fasts when an offer lacks extra.helper — it never silently degrades to a gas-paying transfer. The gasless path therefore needs the facilitator configured with a deployed PermitBurner (settlement.permit_burner); with a bare facilitator config, use mode: "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:

  1. 402 — the first request is unpaid; the provider answers 402 Payment Required with a PAYMENT-REQUIRED header carrying the offer, whose payTo is a fresh one-time burn address.
  2. PaymentwrapFetchWithPayment intercepts the 402, has SubethaSchemeClient build the subetha-zerc20 payload (permit mode: two signatures, no transaction), and retries with PAYMENT-SIGNATURE.
  3. Accepted — the facilitator’s settle succeeds, the response arrives with your resource body and a PAYMENT-RESPONSE header whose SettleResponse has success: true and extra.phase: "accepted". This is the terminal state of the HTTP request.
  4. 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.