Reference tables
Cross-cutting tables that exist in no single spec. Come back to this page; do not read it through.
Chart of accounts
Account names are built only in ledger/accounts.ts. A string literal naming an account in any
other file is a future typo that no type will catch.
user:{u}:sub:{subaccountId}:{currency} balance held at the exchange
user:{u}:transit THE single basis for free balance
user:{u}:external mirror of the system boundary
user:{u}:income:{premium|interest|funding}
user:{u}:expense:{derive_fee|settlement|swap|borrow}
platform:gas:{chain} platform:hot:{chain}
platform:expense:gas:{chain} gas attributed to a user but not charged to them
platform:expense:overhead platform:fee_revenue
platform:expense:reconciliation unexplained journal-versus-exchange differenceSign, not debit/credit columns. Who bears a cost is read from the prefix: platform: means the
platform, anything else means the user. The subaccount account is keyed by subaccountId, not by
asset (migration 1787900000002).
Event types
deposit_detected, deposit_swept, bridged, subaccount_funded, subaccount_released,
option_sold, option_settled, swap_executed, swap_reverted, interest_accrued,
funding_paid, withdrawal_sent, gas_spent, trade_rejected, signal_received,
reconciliation_adjustment, stop_raised, stop_cleared.
Not all of them have payload schemas, deliberately: the journal must accept an event whose posting
rule has not been written yet. There are no postings for deposit_swept, swap_reverted,
trade_rejected, or for the OTM branch of option_settled.
Idempotency keys
| Source | Shape |
|---|---|
| An exchange trade | fill.tradeId — one event per fill (P5) |
| On-chain | tx:{chain}:{txHash}:{logIndex} |
| Accruals | accrual:{sub}:{type}:{bucketTs} |
| Option settlement | {sub}:{instrument}:{expirySec} |
| Funding a subaccount | subaccount_funded:{transactionId} |
| A release | release:{rowId} |
| Stopping a subaccount | stop:{sub}:{minute bucket} |
| A reconciliation adjustment | reconcile:{sub}:{account}:{bucket}:{delta} — the delta is mandatory in the key |
| A stop raised by reconciliation | reconcile:{sub}:stop:{bucket}:{run signature} — a different shape; do not confuse it with the previous row |
The run signature is assembled from the accounts and deltas of every divergence found. Two runs
within the same minute with different contents must each leave their own stop_raised rather than
collapsing into one.
For a transaction we sent ourselves, logIndex = 0. A second event about that same transaction
takes slot 1. Reuse the slot and append deduplicates the second event silently — the money
is simply never recorded.
Key constants, and where they live
Product thresholds live as constants in modules, not in the environment. They do not differ between deployments.
| Constant | Value | Address |
|---|---|---|
EPSILON_LADDER_BPS | [3, 6, 12, 25, 50, 100] | swap/slippage-ladder.ts:4 |
SANITY_THRESHOLD_BPS | 100 | swap/price-sanity.guard.ts:12 |
LADDER_DEADLINE_MS | 30_000 | swap/swap.module.ts:81 |
SWAP_RETRY_STALE_AFTER_MS | 30 min | swap/swap.module.ts:105 |
GAS_FLOOR_WEI (swap) | 1.5e14 | swap/swap.module.ts:67 |
GAS_FLOOR_WEI (sweep) | 1e12 | treasury/treasury.module.ts:56 |
CIRCLE_GAS_FLOOR_WEI | 2e15 — for a whole circle | conversions/conversions.module.ts:66 |
MARGIN_BUFFER_USD | '100' | conversions/conversions.module.ts:76 |
ACTIVE (withdrawal reserve) | "('requested')" | treasury/withdrawals.repository.ts:133 |
MAKER_WINDOW_MS | 20 min | positions/steps/placing.step.ts:19 |
LOCK_TTL_MS | 2 min | positions/cycles.repository.ts:5 |
RETRY_MS (wheel) | 60 s | wheel/wheel.runner.ts:25 |
| Wheel deadline | 20:00 UTC | wheel/wheel.runner.ts:52-60 |
NONCE_TTL_MS | 5 min | auth/auth.module.ts:16 |
TOKEN_TTL | '7d' | auth/auth.module.ts:17 |
Environment variables
NODE_ENV PORT TRADING_PROFILE DATABASE_URL REDIS_URL DERIVE_HTTP_URL DERIVE_WS_URL
DERIVE_CHAIN_RPC_URL MASTER_PRIVATE_KEY SESSION_PRIVATE_KEY GAS_WALLET_PRIVATE_KEY
DERIVE_WALLET_ADDRESS PLATFORM_FEE_RATE RECONCILE_EPSILON WEB_ORIGIN MASTER_MNEMONIC
JWT_SECRET SIWE_DOMAIN DERIVE_CHAIN_ID WEBHOOK_SECRET WITHDRAWAL_MIN_USD WITHDRAWAL_MAX_USD
WITHDRAWAL_STUCK_MINUTES RPC_URL_ETHEREUM RPC_URL_OPTIMISM RPC_URL_BASE RPC_URL_ARBITRUM
SWAP_WALLET_PRIVATE_KEY PRE_CONVERT_LEAD_MINUTES CONVERSION_FAST_TICK_SECONDS
CONVERSION_STUCK_MINUTES WHEEL_ENABLED RECONCILE_MODE TELEGRAM_BOT_TOKEN TELEGRAM_CHAT_IDThe schema has no defaults — every variable is required. The exceptions that do have one:
PRE_CONVERT_LEAD_MINUTES (10), CONVERSION_FAST_TICK_SECONDS (5), CONVERSION_STUCK_MINUTES
(30), WHEEL_ENABLED (false), RECONCILE_MODE (observe).
Exactly two are optional outright — TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID. Without them
alerting is not disabled; it falls back to logger.warn.
HTTP surface
GET /assets GET /:asset/chart GET /:asset/fees
GET /:asset/strategies WS /ws/strategies WS /ws/prices
GET /auth/nonce POST /auth/verify
GET /deposit-address
GET /withdrawals/routes POST /withdrawals GET /withdrawals
POST /positions GET /positions DELETE /positions/:id
POST /subaccounts/:asset/release
GET /portfolio/summary GET /portfolio/statement
POST /webhooks/signals GET /signals/state
GET /trading-config GET /healthPOST /positions, POST /subaccounts/:asset/release and POST /withdrawals all answer 202 —
the work is done by a background tick.
Authentication is explicit per controller through @Authenticated(); there is no global guard.
Everything market-data — the showcase, both WebSockets, charts, fees, the asset list — is public by
design.