Facilitator quickstart
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. Get the source and build
Section titled “1. Get the source and build”git clone https://github.com/peaceandwhisky/SubEthacd SubEthagit 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.