<!-- Generated raw Markdown for /reference/configuration; product pin 638fa1e9ca7face19a442cef43754f0d627c7705. -->

Everything on this page describes the **local, non-production** SubEtha stack
and is taken from the pinned product sources. All key/secret values shown are
**placeholders** (`0x…`) — supply your own via the environment, never in a
config file, and never commit them anywhere.

## Facilitator: TOML configuration

The facilitator daemon starts from a single TOML config file. The annotated
reference is
[`apps/facilitator/fac.example.toml`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/apps/facilitator/fac.example.toml);
the full operator runbook is
[`docs/FACILITATOR.md`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/docs/FACILITATOR.md).
The facilitator itself is **source-only** — it is built from the product
repository workspace and started from its build output; there is no published
package for it. Everything is explicit: the daemon has **no default network,
contracts, or keys**, so it can never silently run against the wrong chain.

| key | required / default | meaning |
|---|---|---|
| `profile` (top level) | unset = treated as **non-local** | `"local"` \| `"testnet"` \| `"production"`. Explicit classification only — never inferred from bind address or chain id |
| `network.rpc_url` | required | EVM RPC endpoint (credential-free URL — `check` warns on embedded credentials) |
| `network.indexer_url` | required | zERC20 indexer |
| `network.decider_url` | required | decider-prover (batch/Nova path only — not probed by `check`) |
| `network.chain_id` | required | must match what the RPC reports |
| `network.token` / `network.verifier` / `network.hub` | required | the zERC20 deployment (checked for deployed code) |
| `listen.host` | `127.0.0.1` | non-loopback binds require `SUBETHA_ADMIN_TOKEN` |
| `listen.port` | `4032` | |
| `listen.trusted_proxies` | unset | IP addresses / CIDR blocks fed verbatim to Express's standard `trust proxy` |
| `settlement.treasury` | required | mint destination — the FeeSplit address in split mode |
| `settlement.permit_burner` | optional | deployed PermitBurner; enables the gasless permit path when set |
| `settlement.journal` | required | crash-recovery JSONL path, or `"none"` to opt out (warned; local experiments only — pending work is lost on restart) |
| `settlement.challenge_ttl_seconds` | `300` (min 1) | challenge lifetime |
| `settlement.expired_challenge_retention_seconds` | `3600` (min 0) | how long an expired open challenge's match data stays in memory for late-inclusion checks and investigation |
| `fee.*` | omit the table for no fee | `bps`, `min_per_payment`, `charge_to` (`"payer"` or `"provider"`), `recipient`, `splitter`. In split mode `splitter` must equal `settlement.treasury`, `min_per_payment` must be `"0"`, `bps >= 1` |
| `daemon.finalize_interval_ms` | `15000` (min 1000) | finalize-loop period; failures back off exponentially (up to 10×) |
| `daemon.shutdown_grace_ms` | `10000` | SIGTERM: max wait for in-flight work before forced exit |
| `daemon.rate_limit_per_min` | `120` | shared per-key limit on the money endpoints (`/challenge` `/verify` `/settle`); `0` disables |
| `daemon.max_open_challenges` | `1000` (min 1) | global cap on open challenges — `POST /challenge` answers 503 at the cap |
| `daemon.challenge_per_key_per_min` | `20` | per-key issuance rate on `POST /challenge` (429); `<= 0` disables |

A config can be validated without starting the daemon (`check` probes config,
RPC, contracts, splitter, journal and keys, and exits non-zero with a `✗` line
per problem).

### Facilitator secrets (environment only)

Keys have **no defaults** and are never read from the config file.

| variable | required | used for |
|---|---|---|
| `SUBETHA_TREASURY_PK` | always | seed derivation (in split mode: entropy only) + custodial fee-sweep signing |
| `SUBETHA_RELAYER_PK` | always | submitting teleport mints + relaying gasless permit burns |
| `SUBETHA_ADMIN_TOKEN` | non-loopback bind, any non-local profile, and when `profile` is unset | `Authorization: Bearer …` for `/admin/*`. Unset on a local loopback config ⇒ `/admin/*` is **locked** (all requests get 401) |

The well-known anvil dev keys are accepted (with a warning) only when **all
four** predicates hold: `profile = "local"`, loopback `listen.host`, loopback
RPC host, and `chain_id` 31337. Any missing predicate aborts startup. Use
placeholders like `0x…` everywhere else — real keys belong only in your own
environment management.

### Profiles

Payments in the pinned release are **local-experiment only**. The top-level
`profile` key classifies the deployment; classification is explicit only, and
an unset `profile` is treated as non-local (fail-safe). A non-local profile
(`testnet`, `production`, or unset-with-token) starts **gated**: payment
acceptance is disabled (`/challenge`, `/verify`, `/settle` refuse with
profile-gating errors — see the
[wire protocol reference](/reference/protocol/)) and chain-changing
finalization is frozen.

## Payer: `SUBETHA_*` environment variables

The TypeScript agent tools
([`apps/agent-tools/src/env.ts`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/apps/agent-tools/src/env.ts))
and the Python payer's `SubethaClient.from_env`
([`python/src/subetha/client.py`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/python/src/subetha/client.py))
read the **same variable names**. The human operator sets keys and spending
limits here — nothing security-relevant is ever taken from (or exposed to) an
agent's tool arguments.

| variable | default | meaning |
|---|---|---|
| `SUBETHA_PAYER_PK` | — (required to pay) | 32-byte hex private key of the paying account. Quoting does not need it. Placeholder only in docs — never commit a real key |
| `SUBETHA_RPC_URL` | `http://127.0.0.1:8545` | EVM RPC endpoint for chain reads (and, in self-transfer mode, broadcasting) |
| `SUBETHA_MODE` | `permit` | `permit` (gasless: sign only) or `self-transfer` (broadcast the burn yourself, paying gas) |
| `SUBETHA_TIMEOUT_MS` | `30000` | HTTP timeout (milliseconds, positive integer) |
| `SUBETHA_ALLOWED_HOSTS` | `localhost,127.0.0.1` | comma-separated hostname allowlist checked before any request leaves; `*` allows all (the default is the safe local-stack-only posture) |
| `SUBETHA_ALLOWED_NETWORK` | `eip155:31337` | CAIP-2 network pin for acceptable offers |
| `SUBETHA_ALLOWED_TOKEN` | unset | token contract address pin; unset accepts any token on the allowed network |
| `SUBETHA_MAX_PER_PAYMENT` | unset | per-payment cap in token base units |
| `SUBETHA_MAX_TOTAL` | unset | total-spend cap in token base units (per network\|token pair) |
| `SUBETHA_APPROVE_ABOVE` | unset | payments quoted above this (base units) require a human yes through an approval channel; unset = approval flow off |
| `SUBETHA_APPROVAL_TIMEOUT_MS` | `120000` | how long to wait for the human approval |

Base-unit variables must be plain integers; malformed values are rejected at
load with a named error rather than silently ignored.

## Local stack context

All defaults above (loopback URLs, chain id 31337, anvil dev keys) describe
the local development stack. Bringing that stack up is documented in the
product repository:
[`docs/RUNNING-THE-STACK.md`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/docs/RUNNING-THE-STACK.md)
and
[`docs/BUILDING-THE-STACK.md`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/docs/BUILDING-THE-STACK.md).
