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, asynchronousAccepted 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.
Prerequisites
Section titled “Prerequisites”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.
Step 1 — bring up the zERC20 stack
Section titled “Step 1 — bring up the zERC20 stack”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:
pnpm installpnpm -r buildStep 3 — fail-fast prerequisite check
Section titled “Step 3 — fail-fast prerequisite check”pnpm --filter @subetha/demo-official run deploy-checkExpected: 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.
Step 4 — run the flow
Section titled “Step 4 — run the flow”The in-repo demo drives one payment end to end in a single process — payer,
provider (:4031), and facilitator logic wired together:
pnpm --filter @subetha/demo-official run runThe 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
payTois a fresh one-time burn address. The✓ F1line 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 treasuryline appear, followed by the treasury balance delta. That is the asynchronous half completing, minutes afteraccepted.
Optional: the gasless variant
Section titled “Optional: the gasless variant”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 same flow, component by component
Section titled “The same flow, component by component”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:
- Facilitator — config with
profile = "local",check, thenstart(loopback:4032). It must be up first: the provider hard-fails at initialization without its/supported. - Provider — Express app with the official
middleware and
subethaAccepts, listening on:4031. - A payer — TypeScript 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.
Where to go next
Section titled “Where to go next”- Accepted vs finalized — the two-phase settlement model this tutorial just demonstrated.
- Wire protocol reference — the exact headers, payloads, and settle semantics on the wire.
- Trust & Limits — what you trusted while running this, and what the construction does not promise.