Contents

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.

VariableMeaningDefault
WEBHOOK_SECRETHMAC key that verifies inbound billing and settlement webhooks (e.g. from a payment processor).none (required in production)
SIGNING_SECRETKey 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.

VariableMeaningDefault
PAYOUT_MIN_EARNED_MINORSmallest 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_MSMinimum 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_ATTEMPTSProvider attempts for one payout before the system gives up and reverses it.5
MAX_PAYOUT_AGE_MSLongest 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_BPSPayout-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.

VariableMeaningDefault
MAX_OUTBOX_ATTEMPTSDelivery attempts an outbox event gets before the relay dead-letters it (status dead).10
MAX_INBOX_ATTEMPTSApply attempts an inbox event gets before the apply worker dead-letters it (status dead). The inbound mirror of MAX_OUTBOX_ATTEMPTS.10
MAX_SUBSCRIPTION_ATTEMPTSConsecutive 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.

VariableMeaningDefault
SLA_PENDING_MSBudget for a payout in PENDING.30000
SLA_SUBMITTED_MSBudget for a payout in SUBMITTED. Distinct from MAX_PAYOUT_AGE_MS, which force-fails; this only schedules the next settle check.120000
SLA_DEFAULT_MSBudget 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.

VariableMeaningDefault
MATURITY_HORIZON_CARD_MSHold for a card top-up: its chargeback window. The anchor every other rail defaults to.dev 0 / prod req.
MATURITY_HORIZON_CRYPTO_MSHold for a crypto top-up.= card
MATURITY_HORIZON_STEAM_MSHold for a Steam-store top-up. Defaults to the card horizon until the rail’s refund window is decided.= card
MATURITY_HORIZON_META_MSHold for a Meta-store top-up. Same conservative default as Steam.= card
MATURITY_HORIZON_DEFAULT_MSHold 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.

VariableMeaningDefault
PLATFORM_FEE_BPSPlatform’s transaction fee in basis points. The example models a ~15.3% marketplace fee.1530
VELOCITY_LIMIT_MINORThe single-knob ceiling per window, in CREDIT minor units; both window classes fall back to it.dev 100000 / prod req.
VELOCITY_INFLOW_LIMIT_MINORCeiling for the inflow window (topUp, grantPromo) — the card-testing gate.= limit
VELOCITY_OUTFLOW_LIMIT_MINORCeiling for the outflow window (spend, subscribe, requestPayout) — the drained-wallet gate.= limit
VELOCITY_WINDOW_MSLength 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.

VariableMeaningDefault
SUBSCRIPTION_PRICE_MIN_MINORSmallest subscription price, in CREDIT minor units.10000 (100.00)
SUBSCRIPTION_PRICE_MAX_MINORLargest 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.

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

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

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

VariableMeaningDefault
REPLAY_WINDOW_MSHow long a signed webhook timestamp stays valid. Older ones are rejected.300000 (5 min)
ECONOMY_PAUSE_START_MSEpoch ms the window opens (inclusive).unset (no pause)
ECONOMY_PAUSE_END_MSEpoch 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.

VariableMeaningDefault
CREDIT_BUY_RATE / CREDIT_BUY_SCALEbuy rate: USD a user pays per credit at top-up.none (required in production)
CREDIT_PAR_RATE / CREDIT_PAR_SCALEpar rate: a credit’s backing value, used by the solvency check.none (required in production)
PAYOUT_RATE / PAYOUT_SCALEpayout rate: what a seller’s earned credits cash out at (equal to par).none (required in production)
PROCESSOR_URLEndpoint of the real payout provider for the processor port. Also honored in dev if set.none (required in production)
PROCESSOR_API_KEYBearer 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.

VariableMeaningDefault
DATABASE_URLA postgres:// or mysql:// connection string selects that engine; any other scheme throws.unset (in-memory store)
DB_POOL_MAXMax 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_URLAdds a Redis read-through cache the store consults before the database.unset (no cache)
SQS_QUEUE_URLDelivers outgoing events via an Amazon SQS queue. Wins if DISPATCHER_URL is also set.unset
DISPATCHER_URLPosts 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.

VariableMeaningDefault
DISPATCHER_DECLINEDDeclares a production deploy runs without a dispatcher on purpose.unset
PAYEES_DECLINEDDeclares a production deploy runs without a payee directory on purpose.unset
ANCHOR_DECLINEDDeclares 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.

VariableMeaningDefault
PORTPort the HTTP API listens on.3000
SHUTDOWN_TIMEOUT_MSHow long a graceful shutdown waits for in-flight work before forcing exit.5000
WORKER_INTERVAL_MSGap between worker sweep ticks.60000
WORKER_BATCHRows 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.

VariableMeaningDefault
TASKQ_DATABASE_URLEnables 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_IDEnables 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)

See also