Configuration
Every environment variable economy-lab reads, with its meaning and default.
Source src/config.ts#L303-L318loadConfigsrc/index.ts#L656openPortssrc/from-env.ts#L145resolveExternals.env.example
API Config
How configuration is read
economy-lab reads the environment once, at startup: the edge calls loadConfig, gets a
plain Config object, and
passes it in — the library modules never touch process.env. A misconfigured deploy fails
at startup, not deep inside a request.
That single read also means defaults live in one file. Every tunable below is parsed by
loadConfig with a fallback, so an unset or malformed value lands on the documented default
rather than leaving the config half-applied. These are the same defaults the operation and
background-worker pages cite.
A config override passed to openPorts (or defaultConfig) applies last-wins per knob,
except the record-valued knobs — maturityHorizonMs and payoutSla — which merge one level
deep: overriding one funding source or one SLA step keeps the others rather than replacing the
whole record.
A handful of variables (the signing secrets, the CREDIT-to-USD rates, the payout provider, and
the infrastructure URLs) are read outside loadConfig, when the edge wires up the
Economy and its ports. Those are grouped separately at
the end, because they pick adapters rather than tune policy.
Required secrets
Two secrets have no honest default. In production (NODE_ENV=production) a blank value for
either makes loadSecrets throw a single CONFIG.INVALID fault listing every missing key at
once.
| Variable | Meaning | Default |
|---|---|---|
WEBHOOK_SECRET | HMAC key that verifies inbound billing and settlement webhooks (e.g. from a payment processor). | none (required in production) |
SIGNING_SECRET | Key the signer uses to sign chain checkpoints and entitlement-gated URLs. See integrity. | none (required in production) |
Payouts
These tune when a payout is too small, too frequent, or too old to proceed. The minimums are counted in CREDIT minor units; see the money model for what a minor unit is.
| Variable | Meaning | Default |
|---|---|---|
PAYOUT_MIN_EARNED_MINOR | Smallest payout a user may request, counted only against earned CREDIT, not bought or promo-granted. The floor keeps a disbursement from costing more in rail fees than it pays out. | 2000000 (≈ $100) |
PAYOUT_MIN_INTERVAL_MS | Minimum time between a user’s payout requests. The 24h default matches the public payout docs of the platform the example models; that platform’s payout terms require 14 days, so a production deploy sets 1209600000. | 86400000 (24h) |
MAX_PAYOUT_ATTEMPTS | Provider attempts for one payout before the system gives up and reverses it. | 5 |
MAX_PAYOUT_AGE_MS | Longest a payout may sit in SUBMITTED before the worker force-fails it as timed out, because the provider never reported back. The default outlasts a rail’s one-to-two business-day settlement. | 259200000 (3d) |
PAYOUT_FEE_BPS | Payout-rail fee in basis points, charged on the USD a seller cashes out. This is the rail’s own cut, deducted from the disbursement, not platform revenue. | 150 (≈ 1.5%) |
The payout rate and the per-state payout SLAs also feed the saga; the SLAs live under the
worker group below, and the rate under externals.
Worker queues
The background worker retries deferred work, then
gives up after a cap so a poison message can’t wedge a queue. Each cap below is a number of
attempts; the worker dead-letters once attempts reaches it.
| Variable | Meaning | Default |
|---|---|---|
MAX_OUTBOX_ATTEMPTS | Delivery attempts an outbox event gets before the relay dead-letters it (status dead). | 10 |
MAX_INBOX_ATTEMPTS | Apply attempts an inbox event gets before the apply worker dead-letters it (status dead). The inbound mirror of MAX_OUTBOX_ATTEMPTS. | 10 |
MAX_SUBSCRIPTION_ATTEMPTS | Consecutive retryable renewal failures before the renewal sweep lapses a subscription instead of re-billing forever. | 10 |
The payout SLAs are per-state staleness thresholds the reconcile sweep folds over. A step
stuck past its threshold is overdue. DEFAULT covers any state not named.
| Variable | Meaning | Default |
|---|---|---|
SLA_PENDING_MS | Budget for a payout in PENDING. | 30000 |
SLA_SUBMITTED_MS | Budget for a payout in SUBMITTED. Distinct from MAX_PAYOUT_AGE_MS, which force-fails; this only schedules the next settle check. | 120000 |
SLA_DEFAULT_MS | Budget for any other state. | 60000 |
Maturity horizons
Topped-up funds wait before they can be spent or paid out, keyed by funding source. An
unlisted source falls back to the default entry. See credit
maturity for why the holding period exists.
The horizons split by environment. Outside production every unset horizon is 0 — funds clear
instantly, so the zero-config quickstart’s top-up can be spent in the next line. In production
MATURITY_HORIZON_CARD_MS is required (startup fails fast naming it, like the secrets): a real
deployment states its chargeback window rather than inheriting a silent policy number.
| Variable | Meaning | Default |
|---|---|---|
MATURITY_HORIZON_CARD_MS | Hold for a card top-up: its chargeback window. The anchor every other rail defaults to. | dev 0 / prod req. |
MATURITY_HORIZON_CRYPTO_MS | Hold for a crypto top-up. | = card |
MATURITY_HORIZON_STEAM_MS | Hold for a Steam-store top-up. Defaults to the card horizon until the rail’s refund window is decided. | = card |
MATURITY_HORIZON_META_MS | Hold for a Meta-store top-up. Same conservative default as Steam. | = card |
MATURITY_HORIZON_DEFAULT_MS | Hold for an unlisted funding source. Falls back to the card horizon. | = card |
Fees and the risk gate
PLATFORM_FEE_BPS is the platform’s cut on a sale, in basis points, where 10000 is 100%.
The velocity variables bound how fast one subject may spend before the risk check steps in;
see risk and velocity for the actor side of
that gate.
The velocity ceiling splits by environment the same way the maturity anchor does. Outside
production the default is 1,000 credits per window — sized so the velocity examples can trip
the gate. In production VELOCITY_LIMIT_MINOR is required (startup fails fast naming it, like
the secrets): a per-user hourly ceiling is a risk decision each deployment states for itself.
| Variable | Meaning | Default |
|---|---|---|
PLATFORM_FEE_BPS | Platform’s transaction fee in basis points. The example models a ~15.3% marketplace fee. | 1530 |
VELOCITY_LIMIT_MINOR | The single-knob ceiling per window, in CREDIT minor units; both window classes fall back to it. | dev 100000 / prod req. |
VELOCITY_INFLOW_LIMIT_MINOR | Ceiling for the inflow window (topUp, grantPromo) — the card-testing gate. | = limit |
VELOCITY_OUTFLOW_LIMIT_MINOR | Ceiling for the outflow window (spend, subscribe, requestPayout) — the drained-wallet gate. | = limit |
VELOCITY_WINDOW_MS | Length of the rolling window. Only a subject’s attempts inside the last window count; each ages out once older. | 3600000 (1h) |
Subscriptions
The price band bounds what a subscription may charge per period, so a typo’d price cannot silently bind a buyer to it. A price outside the band is refused at subscribe time as a wiring error.
| Variable | Meaning | Default |
|---|---|---|
SUBSCRIPTION_PRICE_MIN_MINOR | Smallest subscription price, in CREDIT minor units. | 10000 (100.00) |
SUBSCRIPTION_PRICE_MAX_MINOR | Largest subscription price, in CREDIT minor units. | 1000000 (10,000.00) |
The purchase catalog
Unset, top-up accepts any positive CREDIT amount. Set, TOP_UP_BUNDLES_MINOR names the only amounts a top-up may carry — the platform’s purchase bundles — and any other amount faults OP.MALFORMED, since a store client that offers an off-catalog amount is miswired.
| Variable | Meaning | Default |
|---|---|---|
TOP_UP_BUNDLES_MINOR | Comma-separated catalog of permitted top-up amounts, in CREDIT minor units (e.g. 60000,120000,240000). | unset (any amount) |
Platform sharding
A hot platform account (REVENUE, STORED_VALUE, TRUST_CASH, …) is one row, and concurrent postings of the same kind queue on its lock. PLATFORM_SHARDS splits each one across several rows so those postings spread out; platform sharding covers which accounts shard and how a posting picks its row.
| Variable | Meaning | Default |
|---|---|---|
PLATFORM_SHARDS | How many rows each hot platform account is split across. 1 is the unsharded ledger. | 1 |
Raising the count on a live ledger is safe: shard 0 keeps the bare account id, so existing balances already sit on shard 0 and new postings fan out from there. Lowering it is the careful direction. A smaller count reroutes every key, so rows that still hold balance can drop out of the rotation. Readers keep summing them and totals stay right, but nothing ever posts to them again, so drain before dropping the count back to 1.
The accrual split
Sharding relieves the platform accounts; a hot seller’s earned row needs the accrual split. With the flag on, spend and subscribe park seller shares on a SETTLEMENT_ACCRUAL shard and the worker’s drain sweep moves them to earned in batches. Meant to run with PLATFORM_SHARDS at 2 or more, so the parking shards don’t queue like the row they replace.
| Variable | Meaning | Default |
|---|---|---|
ACCRUAL_DRAIN | 1 turns the split on; seller balances then move when the accrualDrain sweep runs. | off |
Replay window and the maintenance pause
REPLAY_WINDOW_MS bounds how stale a signed request may be before it’s rejected as a possible
replay; see integrity. The two pause bounds open an optional
maintenance window.
| Variable | Meaning | Default |
|---|---|---|
REPLAY_WINDOW_MS | How long a signed webhook timestamp stays valid. Older ones are rejected. | 300000 (5 min) |
ECONOMY_PAUSE_START_MS | Epoch ms the window opens (inclusive). | unset (no pause) |
ECONOMY_PAUSE_END_MS | Epoch ms the window closes (exclusive). This is the resumesAt the decline reports. | unset (no pause) |
The pause is active only when both bounds parse as integers and start <= now < end. While
it holds, end-user discretionary writes are declined with ECONOMY_PAUSED; settlement (actor
system) and operator fixes still flow, and reads never reach the gate.
Production externals
These choose which adapter fills each port, so the edge reads them at wiring time rather than
through loadConfig. In production, openPorts fails fast with one preflight message naming
every rate or provider URL that is missing or malformed, so a prod process never runs
on the 1:1 dev rate or an auto-approve payout stub. Outside production these fall back to dev
stubs, and a local run needs none of them.
The six rate variables configure the fixed-point CREDIT-to-USD conversions for the
rates port: usd_minor = floor(credit_minor × rate / 10^scale). The
gap between buy and par is the platform spread.
| Variable | Meaning | Default |
|---|---|---|
CREDIT_BUY_RATE / CREDIT_BUY_SCALE | buy rate: USD a user pays per credit at top-up. | none (required in production) |
CREDIT_PAR_RATE / CREDIT_PAR_SCALE | par rate: a credit’s backing value, used by the solvency check. | none (required in production) |
PAYOUT_RATE / PAYOUT_SCALE | payout rate: what a seller’s earned credits cash out at (equal to par). | none (required in production) |
PROCESSOR_URL | Endpoint of the real payout provider for the processor port. Also honored in dev if set. | none (required in production) |
PROCESSOR_API_KEY | Bearer token sent to PROCESSOR_URL. | unset |
Infrastructure adapters
DATABASE_URL picks the storage backend, and the cache and dispatcher URLs pick their
transports. Each is optional: unset means the dependency-free in-memory adapter. These belong
to storage, read by
openPorts in src/index.ts.
| Variable | Meaning | Default |
|---|---|---|
DATABASE_URL | A postgres:// or mysql:// connection string selects that engine; any other scheme throws. | unset (in-memory store) |
DB_POOL_MAX | Max connections in the SQL engine’s pool. Excess concurrent submits queue for a connection, and a transaction holds exactly one for its whole life, so the queue always drains. | unset (driver default 10) |
REDIS_URL | Adds a Redis read-through cache the store consults before the database. | unset (no cache) |
SQS_QUEUE_URL | Delivers outgoing events via an Amazon SQS queue. Wins if DISPATCHER_URL is also set. | unset |
DISPATCHER_URL | Posts outgoing events over HTTP to your event bus. | unset (in-process delivery) |
Three of the optional ports — the dispatcher, the payee directory, and the checkpoint anchor —
production refuses to lose silently: leaving one unwired without a word is a preflight error.
Each has a decline flag (1 or true) that declares the deployment runs without that port on
purpose.
| Variable | Meaning | Default |
|---|---|---|
DISPATCHER_DECLINED | Declares a production deploy runs without a dispatcher on purpose. | unset |
PAYEES_DECLINED | Declares a production deploy runs without a payee directory on purpose. | unset |
ANCHOR_DECLINED | Declares a production deploy runs without a checkpoint anchor on purpose. | unset |
HTTP server and worker loop
The serve and worker entry point reads four more variables directly. They control the HTTP service port, shutdown grace, and the worker’s tick cadence and batch size.
| Variable | Meaning | Default |
|---|---|---|
PORT | Port the HTTP API listens on. | 3000 |
SHUTDOWN_TIMEOUT_MS | How long a graceful shutdown waits for in-flight work before forcing exit. | 5000 |
WORKER_INTERVAL_MS | Gap between worker sweep ticks. | 60000 |
WORKER_BATCH | Rows a worker sweep processes per tick. | 100 |
The package bridges
Two optional bridges in the host entry point (scripts/main.ts) compose the sibling packages in.
Each is off until its gate variable is set, and neither is read by the library itself; the seams
they wire are owned by the packages.
| Variable | Meaning | Default |
|---|---|---|
TASKQ_DATABASE_URL | Enables the @pwngh/taskq bridge: outbox events are enqueued as durable tasks on this Postgres database. The optional @pwngh/taskq package must be installed; TASKQ_POLL_MS tunes the poll (default 250). | unset (bridge off) |
TILIA_CLIENT_ID | Enables the economy-edge Tilia bridge: the shimmed processor, the payee directory, the float feed, and the webhook handler. TILIA_ENVIRONMENT picks staging or production; production also requires TILIA_PAYEE_DATABASE_URL and TILIA_WEBHOOK_SECRET. | unset (bridge off) |