Skip to content

Tutorial: the local end-to-end flow

This walkthrough takes one paid API call through the whole local, non-production SubEtha flow. The success graph you are aiming for:

402 (offer: one-time burn address)
→ payment (zERC20 burn — signed permit, or self-broadcast)
→ accepted (settle succeeds; the resource is served) ← the request path ends here
→ finalized (proof-gated mint to the treasury) ← later, asynchronous

Accepted and finalized are different states on purpose. accepted is the synchronous outcome of the HTTP request: the facilitator’s /settle succeeded and the provider served the resource. finalized happens only later, when the facilitator’s finalize loop gets the transfer root proved and drives the proof-gated mint — no HTTP response in the flow ever says finalized. If this distinction is new, read accepted vs finalized first.

Explicitly, before anything pays:

requirement why reference
Node.js >= 20 and pnpm build and run the TypeScript workspace product docs/TUTORIAL.md
Foundry (anvil, cast, forge) the local chain and chain inspection; PermitBurner deploys product docs/FACILITATOR.md
A product-repo checkout the demo, the facilitator, and the provider all run from source git clone https://github.com/peaceandwhisky/SubEtha
The built local zERC20 stack at ../zerc20-work SubEtha settles against the official toolchain (BUSL-1.1, referenced via a local sibling link — never vendored) docs/BUILDING-THE-STACK.md
A payer account pre-funded with zERC20 the local deploy mints test tokens to it; there is no faucet docs/RUNNING-THE-STACK.md

Everything runs on loopback, on chain 31337, with anvil’s published dev keys — dev-only values that are never legitimate anywhere else.

Per the runbook, four services must be up (start order: anvil → postgres → decider → indexer):

service port
anvil (chain 31337) :8545
indexer :8080
decider-prover :8081
postgres ×2 (docker) :5432, :5433

The exact start commands, log locations, health checks (latestProvedIndex >= 1, payer balance > 0), and recovery procedures are the runbook’s job — follow docs/RUNNING-THE-STACK.md directly rather than a copy here.

Step 2 — install and build the workspace

Section titled “Step 2 — install and build the workspace”

From the product repo root:

Terminal window
pnpm install
pnpm -r build
Terminal window
pnpm --filter @subetha/demo-official run deploy-check

Expected: exit 0 when the stack is reachable and the payer holds zERC20. When something is missing (anvil/indexer/decider down, payer unfunded) it fails with a message pointing back at the runbook — fix and re-run before going further.

The in-repo demo drives one payment end to end in a single process — payer, provider (:4031), and facilitator logic wired together:

Terminal window
pnpm --filter @subetha/demo-official run run

The root prove takes ~1–2 minutes, so the finalized state is not instant. Expected tail on success (from the pinned demo README):

✓ F1: treasury / fullBurnAddress / generalRecipient absent from payer-facing 402
✓ finalized: minted 1234 to treasury, tx=0x…
treasury balance: 0 → 1234 (delta=1234)
✅ END-TO-END SUCCESS — private x402 payment settled via official @zerc20/sdk.

Reading the output against the success graph

Section titled “Reading the output against the success graph”
  • 402 — the run’s first request is unpaid; the offer’s payTo is a fresh one-time burn address. The ✓ F1 line asserts the payer-facing 402 carried only that burn address — never the treasury. (What this does and does not hide is the subject of the privacy caveats; amounts, for one, are public.)
  • payment → accepted — the payer pays and re-sends; settle succeeds and the resource is served. This is the end of the request path: at this moment the payment is accepted, not finalized.
  • finalized — only after the transfer root is proved does the ✓ finalized: minted … to treasury line appear, followed by the treasury balance delta. That is the asynchronous half completing, minutes after accepted.

By default the demo’s payer broadcasts the burn itself and pays its own gas. To run the same flow gasless (payer only signs; the facilitator broadcasts), deploy SubEtha’s PermitBurner helper once and pass PERMIT_MODE=true — the exact forge script command and env values are in the pinned docs/TUTORIAL.md §3. The expected marker line for that variant is ✓ gasless: payer native balance unchanged.

The one-command demo is the fastest proof. To run the roles as separate processes — the shape a real integration takes — compose the quickstarts, in this order:

  1. Facilitator — config with profile = "local", check, then start (loopback :4032). It must be up first: the provider hard-fails at initialization without its /supported.
  2. Provider — Express app with the official middleware and subethaAccepts, listening on :4031.
  3. A payerTypeScript or Python, pointed at http://127.0.0.1:4031/api/complete.

Then watch the same graph: the payer’s first request draws the 402, the paid retry comes back accepted with the resource, and the facilitator’s finalize loop (every 15 s by default) reports the mint once the root is proved.