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. 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.

  • 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
git checkout 85fd50ac23a6ba222641a90f954e3a6f87efb85b # the commit this page is verified against
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" # "local" | "testnet" | "production" — see the Profiles section 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 (pending work is lost on restart)

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. 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.

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,}
  • Shutdown and restart. Stop the daemon with SIGTERM. On the next start, pending settlement work is restored from the crash-recovery journal configured at settlement.journal.
  • Journal opt-out. journal = "none" disables the journal; pending (accepted-but-not-finalized) work is lost across a restart.
  • Liveness. GET /healthz is the read-only liveness surface — probe it from your supervisor to confirm the daemon is up without touching settlement state.

The top-level profile key accepts three values: local, testnet, and production. In the pinned release they behave as follows:

  • Explicit local is the only profile with payment acceptance and the finalize loop enabled. Everything on this page runs under it.
  • testnet, production, and an unset profile are all treated as non-local: the daemon starts only when the required SUBETHA_ADMIN_TOKEN is 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.

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.