Facilitator quickstart
This guide requires existing access to the private SubEtha source repository
and an authorized local checkout. Product file references below are in that
checkout at 85fd50ac23a6ba222641a90f954e3a6f87efb85b. For published SDK installation,
see the payer and provider quickstarts; running payments still
requires a configured provider, facilitator, and settlement backend.
The facilitator is the settlement daemon: it serves the payment HTTP API,
derives one-time burn addresses, verifies and settles payments, and runs the
finalize loop — the asynchronous half of the two-phase settlement. The
facilitator is private, workspace-internal, and source-only: its workspace
exists inside the product repository and is not published to a registry (npm
answers 404 for @subetha/facilitator — see
Versions & compatibility).
You build it from the product repo and run its build output. The commands and
outputs below use the local profile and are taken from the pinned
operator runbook,
docs/FACILITATOR.md,
and labeled as what that runbook defines.
0. Prerequisites
Section titled “0. Prerequisites”- Node.js, pnpm, and Foundry (
anvilis the local chain; the runbook treats Foundry as a hard prerequisite). - A running local zERC20 stack — anvil (chain 31337,
:8545), indexer (:8080), decider-prover (:8081), and your deployedtoken/verifier/hubaddresses. Build it withdocs/BUILDING-THE-STACK.md, start it withdocs/RUNNING-THE-STACK.md.
1. Build from your authorized checkout
Section titled “1. Build from your authorized checkout”From the root of your existing SubEtha checkout:
git checkout 85fd50ac23a6ba222641a90f954e3a6f87efb85b # the commit this page is verified againstpnpm installpnpm -r buildThe daemon’s CLI is the build output at apps/facilitator/dist/cli.js. Do
not look for an npm install path — there isn’t one, by design.
2. Write a config (TOML), keep secrets in the environment
Section titled “2. Write a config (TOML), keep secrets in the environment”Copy the annotated reference config and edit it:
cp apps/facilitator/fac.example.toml fac.toml$EDITOR fac.tomlA minimal local config has this shape (placeholder addresses — use the ones
from your local deployment; every [network] field is required, with no
defaults, so the daemon can never silently run against the wrong chain):
profile = "local" # "local" | "testnet" | "production" — see the Profiles section below
[network] # the zERC20 deployment — ALL fields requiredrpc_url = "http://127.0.0.1:8545"indexer_url = "http://127.0.0.1:8080"decider_url = "http://127.0.0.1:8081"chain_id = 31337token = "0x…" # your deployed zERC20 tokenverifier = "0x…" # your deployed verifierhub = "0x…" # your deployed hub
[listen]host = "127.0.0.1" # non-loopback binds require SUBETHA_ADMIN_TOKENport = 4032
[settlement]treasury = "0x…" # mint destination# permit_burner = "0x…" # deployed PermitBurner — enables the gasless permit pathjournal = "./facilitator-journal.jsonl" # crash-recovery journal; "none" opts out (pending work is lost on restart)Secrets are environment-only — they have no defaults and are never read from the config file:
export SUBETHA_TREASURY_PK=0x… # placeholder — seed derivation + custodial fee-sweep signingexport SUBETHA_RELAYER_PK=0x… # placeholder — teleport mints + gasless permit-burn relaying# SUBETHA_ADMIN_TOKEN is optional on a local loopback config; unset ⇒ /admin/* is locked (401).The well-known anvil dev keys are accepted (with a warning) only when all
four local predicates hold: profile = "local", loopback listen.host,
loopback RPC host, and chain_id 31337. If a well-known dev key is supplied
while any predicate is missing, startup aborts. Everywhere else, real keys
belong in your own environment
management — never in a file, never in docs. Key-by-key details are in the
configuration reference.
3. Validate without starting
Section titled “3. Validate without starting”node apps/facilitator/dist/cli.js check --config fac.tomlcheck probes config, RPC, contracts, splitter, journal, and keys, and exits
non-zero with a ✗ line per problem. Against a healthy local stack the
runbook’s expected output ends with:
✅ configuration checks out4. Start
Section titled “4. Start”node apps/facilitator/dist/cli.js start --config fac.tomlstart logs one JSON line per event to stdout; the runbook’s expected
readiness marker is the started event:
{"ts":"…","level":"info","event":"started","listen":"127.0.0.1:4032","chain_id":31337,…}Operator lifecycle
Section titled “Operator lifecycle”- Shutdown and restart. Stop the daemon with
SIGTERM. On the nextstart, pending settlement work is restored from the crash-recovery journal configured atsettlement.journal. - Journal opt-out.
journal = "none"disables the journal; pending (accepted-but-not-finalized) work is lost across a restart. - Liveness.
GET /healthzis the read-only liveness surface — probe it from your supervisor to confirm the daemon is up without touching settlement state.
Profiles
Section titled “Profiles”The top-level profile key accepts three values: local, testnet, and
production. In the pinned release they behave as follows:
- Explicit
localis the only profile with payment acceptance and the finalize loop enabled. Everything on this page runs under it. testnet,production, and an unsetprofileare all treated as non-local: the daemon starts only when the requiredSUBETHA_ADMIN_TOKENis set, but remains payment-gated in this release because a confirmation/finality policy for non-local networks is not implemented.
This gating is current-release behavior, not a permanent architectural limitation — the non-local profiles are payment-gated because the confirmation/finality policy they need does not exist yet in the pinned release, not because non-local operation is out of scope.
Endpoints served
Section titled “Endpoints served”| endpoint | role |
|---|---|
GET /supported |
advertises the subetha-zerc20 kind; resource servers using the official middleware require it at initialization |
GET /healthz |
read-only liveness check for the running daemon |
POST /verify |
read-only substantive check of a payment payload |
POST /settle |
executes the payment; success == accepted |
POST /challenge |
SubEtha extension: mints a fresh one-time burn-address offer for a list price |
POST /history/auth/challenge |
optional participant-scoped wallet-login challenge |
POST /history/auth/verify |
optional participant-scoped wallet signature verification |
GET /history/payments |
optional payer/provider-scoped payment history list |
GET /history/payments/:paymentId |
optional payer/provider-scoped payment lookup |
POST /history/proofs/* |
optional proof issue, verify, and revoke routes; enabled as one group only when the complete proof issuer, trust-anchor, manifest, and durable-artifact configuration is present |
/admin/* |
operations surface; locked (401) unless SUBETHA_ADMIN_TOKEN is set |
The standalone CLI constructs the optional participant-scoped Payment History and identity dependencies only when the complete History environment is present. The proof issue/verify/revoke routes are registered as an all-or-nothing group only when the additional complete proof configuration is present; there is no verify-only or revoke-only mode. With incomplete or absent History configuration, it serves the settlement endpoints above without registering History routes. The Payment History reference describes the separate environment, identity, and proof-artifact boundaries.
Field-by-field request/response shapes are in the
wire protocol reference. The
finalize loop runs every daemon.finalize_interval_ms (default 15000 ms),
driving accepted burns to finalized — that asynchronous half is the reason
accepted is never finalized on the
request path.
Next steps
Section titled “Next steps”- Provider quickstart — the resource server that
points
SubethaFacilitatorClientat this daemon. - Local end-to-end tutorial — the full 402 → payment → accepted → finalized walkthrough.
- Configuration reference — every TOML key, default, and secret.