Skip to content

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.

  • Node.js, pnpm, and Foundry (anvil is 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 deployed token / verifier / hub addresses. Build it with docs/BUILDING-THE-STACK.md, start it with docs/RUNNING-THE-STACK.md.
Terminal window
git clone https://github.com/peaceandwhisky/SubEtha
cd SubEtha
pnpm install
pnpm -r build

The 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:

Terminal window
cp apps/facilitator/fac.example.toml fac.toml
$EDITOR fac.toml

A 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 required
rpc_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 = 31337
token = "0x…" # your deployed zERC20 token
verifier = "0x…" # your deployed verifier
hub = "0x…" # your deployed hub
[listen]
host = "127.0.0.1" # non-loopback binds require SUBETHA_ADMIN_TOKEN
port = 4032
[settlement]
treasury = "0x…" # mint destination
# permit_burner = "0x…" # deployed PermitBurner — enables the gasless permit path
journal = "./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:

Terminal window
export SUBETHA_TREASURY_PK=0x# placeholder — seed derivation + custodial fee-sweep signing
export 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.

Terminal window
node apps/facilitator/dist/cli.js check --config fac.toml

check 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 out
Terminal window
node apps/facilitator/dist/cli.js start --config fac.toml

start 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,}
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.

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.