Skip to content

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.

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

From the root of your existing SubEtha checkout:

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