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. It is
source-only: subetha-facilitator is a private workspace package inside
the product repository, published to no registry (npm answers 404 for
@subetha/facilitator — see
Versions & compatibility). You build it from the
product repo and run its build output. Everything below is the local,
non-production profile; commands and outputs 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 SubEthapnpm 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" # required for local experiments — see "Profiles" 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 (local experiments only)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. Any missing predicate aborts
startup. 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,…}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 |
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 |
/admin/* |
operations surface; locked (401) unless SUBETHA_ADMIN_TOKEN is set |
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.
Profiles: local-only payment acceptance
Section titled “Profiles: local-only payment acceptance”Payments in the pinned release are local-experiment only. The top-level
profile key classifies the deployment explicitly — it is never inferred
from bind address or chain id, and an unset profile is treated as
non-local (fail-safe). Any non-local profile starts gated: /challenge,
/verify, and /settle refuse with profile-gating errors and chain-changing
finalization is frozen. In practice: the daemon accepts payments only with
profile = "local", a loopback bind, a loopback RPC, and chain 31337.
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.