TW_DOOR_ENABLED | Default off. The one switch for the bot door: GET /torii/door, GET /torii/skill.md, GET /torii/mcp-tools and the mandate_* tools on /mcp?profile=full. Off: the three routes answer 404 like a missing path, the tools are not listed and a call to one is refused as not available. On (1, true, yes, on): all open. Read on every request, so it needs a restart of the API process, not a code change. The owner routes under /torii/mandates are not behind it |
TW_MANDATE_FACTORY_ADDRESS | The vault factory. While unset, create-call answers configured: false and no vault exists |
TW_USDC_ADDRESS | USDC address when it is not in deployments/base-sepolia.json. The deposit watcher falls back to the built-in Base Sepolia USDC (with a logged warning) when neither exists |
TW_RPC_URL | The RPC the chain reader and receipt checks use |
TW_BEST_WAY_IN | JSON {"chain","asset"} that fills a missing destination in deposit routes |
TW_BUDGET_ENFORCE | Default off. On: a paid read over the agent's daily Cherry budget is refused (mandate_budget_exceeded) |
TW_ALERTS_DRY_RUN | Default on (1): the alerts worker counts and sends nothing |
TW_DEPOSIT_WATCH_DRY_RUN, TW_DEPOSIT_MAX_BLOCKS_PER_TICK (4000), TW_DEPOSIT_TICK_BUDGET_SEC (60) | The deposit watcher's dry run; per tick it reads consecutive 200-block chunks (never above the eth_getLogs ceiling) until the block cap or the time budget, committing the checkpoint after each chunk |
TW_VAULT_POLL_DRY_RUN, TW_VAULT_POLL_SEC, TW_VAULT_POLL_MAX | The per-mandate vault poller (nw_tw_vault_poll). Dry run is on by default |
TW_VAULT_EVENTS_DRY_RUN, TW_VAULT_EVENTS_SEC, TW_VAULT_EVENTS_MAX_BLOCKS_PER_TICK (4000; old name TW_VAULT_EVENTS_MAX_BLOCKS), TW_VAULT_EVENTS_MAX_SPAN (200 blocks per eth_getLogs: the public Base Sepolia node's limit, a larger span needs a paid RPC; halved to 100 on -32005; a 413 for one address is halved once, then the chunk is left unscanned and retried next run), TW_VAULT_EVENTS_START_BLOCK, TW_VAULT_EVENTS_REVERTS (1; 0 turns the revert block scan off), TW_VAULT_EVENTS_REVERT_BLOCKS_PER_TICK (150; blocks the revert scan reads per run on its own stored block torii_vault_reverts, one eth_getBlockByNumber each; it does not limit the event range), TW_VAULT_EVENTS_REVERT_BACKFILL_MAX_BLOCKS (2000; the first revert backfill of a vault reads at most this many blocks before the revert cursor, older attempts from before the scan existed are not reconstructed), TW_VAULT_EVENTS_REVERT_BUDGET_SEC (60; the revert scan stops cleanly after this many seconds and resumes next run), TW_GETLOGS_MAX_ADDRESSES (20; addresses per eth_getLogs call; a 413 halves the address list first), TW_GETLOGS_MAX_SPAN (200; blocks per eth_getLogs call in every Torii chain read, deposit-watch included; the public node's limit; capped at 200 whatever the value unless TW_GETLOGS_ALLOW_LARGE_SPAN=1, set only with a paid RPC; every other span or chunk value is clamped to it) | The factory-wide event sweep (torii_vault_events), run on Chuncheon, not on Render. Dry run (1) is the default: it reads and counts, writes nothing and does not move its stored block |
TW_WORKER_SCHEMA_ENSURE | Default off. The four Chuncheon workers send no DDL; a missing table makes them log one line and exit 2. 1 runs the look-first ensure at start (testing only). Apply the schema as a job: python -m svc.common.schema_ensure torii_agent_wallet |
TW_WORKER_STATEMENT_TIMEOUT_MS, TW_WORKER_LOCK_TIMEOUT_MS | Worker DB session limits (20000, 5000). A tick that hits one is skipped, exit 0 |
TW_IDEMPOTENCY_TTL_SEC | Default 900. An Idempotency-Key row still in_flight after this many seconds belonged to a call that crashed: the next retry takes it over instead of being told idempotency_in_flight forever. Checked on read, no sweep, no new column. Finished rows are never reclaimed |
TW_HALL_SNAPSHOT_STMT_TIMEOUT_MS | Session statement timeout of the Hall snapshot worker (nw_tw_hall_snapshot, run on Chuncheon every 2 min), default 30000. The worker fills nw_tw_hall_snapshot; the Hall routes only read it (pending until the first pass, stale after 10 min). No dry-run env: --once --dry-run on the command line |
TW_HALL_SNAPSHOT_KEEP_GOOD_SEC | Hall snapshot worker: an agent whose record cannot be built this pass keeps its last good stored row (flagged degraded, with that row's own computed_at) while the row is younger than this many seconds, default 3600; after that the honest record_error row replaces it |
TW_APPROVAL_ENABLED | Default on (1). Clause 6: a relayed execute-call above the mandate's approval amount is held until the owner decides. Off: nothing is held and clause 6 reads stored_pending |
TW_APPROVAL_TTL_HOURS, TW_APPROVAL_MAX_OPEN | How long a pending or approved approval item stays good (24), and how many pending items one mandate may hold (10). Past the limit the agent is refused with rate_limited |
TW_KEEPER_MARK_DRY_RUN (1), TW_KEEPER_MARK_SIGNER (cdp; dev only against a local node), TW_KEEPER_MARK_DEV_KEY (hardhat rehearsal only), TW_KEEPER_MARK_MIN_DELTA_BPS (50), TW_KEEPER_MARK_MAX_AGE_SEC (3600), TW_KEEPER_MARK_MAX (50 mandates per tick), TW_KEEPER_MARK_SEC (300, loop mode only), TW_WORTH_MARK_HEARTBEAT_SEC (900) | The keeper mark worker (python -m svc.worker.torii_keeper_mark --once, Chuncheon). Dry run is on by default: it reads the chain and logs the mark it would send. 0 sends markWorth from the CDP admin wallet, which must be the factory's keeper(). Worth = vault USDC balance (plus the owner's Hyperliquid account for a bound HL leg, see TW_HL_LEG_ENABLED); mandates with relay orders are not marked. Each tick it also stores one row per mandate in nw_tw_worth_marks (worth moved, or the last row is older than TW_WORTH_MARK_HEARTBEAT_SEC; dry run too; 45-day retention) for GET /torii/mandates/{id}/worth-series. Optional table: python -m svc.db.schema.apply_tw_live_schema (also creates nw_tw_worth_state, why a mandate has no mark, and nw_tw_agent_seen, the agent last-seen row) |
TW_SETTLE_GRACE_ENABLED | The API only (default 0). 1 makes the close-all plan skip the owner stop() step, and stop-call return the reason instead of calldata, between endsAt and endsAt plus one day. The deployed vault lets the owner stop at any time, so keep it 0 until the factory with SettleGraceActive is redeployed; switch it on together with that redeploy |
TW_TEST_VENUE_TARGETS | Comma-separated addresses of the test venue contract ToriiMandateTarget. Empty (default): the test-venue exit adapter is inert. Set only on a local rig |
TW_STOP_LINE_DRY_RUN, TW_STOP_LINE_SEC, TW_STOP_LINE_MAX | The stop-line keeper (python -m svc.worker.torii_stop_line --once, run on Chuncheon). Dry run is on by default (1): it reads positions and reports in its log what it would close; it sends no order and writes no row. Clause 7 reads stored_pending while it is on. 0 closes for real, with the relay's own key (NW_TORII_AGENT_KEY, NW_TORII_ACCOUNT_ADDRESS) and reduce-only orders only. Seconds between ticks (30); mandates per tick (20) |
TW_MANDATE_FACTORY_V2_ADDRESS | Leg money (needs TW_HL_LEG_ENABLED and TW_LEG_FUNDING=test). The v2 vault factory (ToriiMandateFactoryV2). Unset or malformed: the create-call answers configured: false while funding is on. Receipts and the event sweep match both factories |
TW_LEG_FUNDING | Default off (off | test; anything else is off). Phase T leg money: the vault funds the owner's Hyperliquid account through a keeper fundLeg and a house-lent HL testnet mirror, and gets the money back. Off: no v2 create-call, no /money, /hl-leg/return-call (POST), /hl-leg/return (404 tw_feature_disabled), no mandate_money tool, the mover and reconcile do nothing, the keeper adds no in-flight term. Needs TW_HL_LEG_ENABLED |
TW_GAS_DRIP_ENABLED | Default off. The one switch for POST /torii/gas/drip and the torii_gas_drip MCP tool: a little Base Sepolia test ETH from the house funding wallet to the caller's own wallet, only for an account that owns a draft, signed or working mandate, or is the designated agent of a signed or working one (else not_eligible, reason no_mandate_yet). Off: the route answers status: disabled and the tool is not listed. On: tools/list shows it in the torii and full profiles. On Base mainnet it is always disabled; no variable overrides that (the chain id is a constant and is checked against the RPC). Needs the table: python -m svc.db.schema.apply_tw_gas_drip_schema |
TW_GAS_DRIP_ETH (0.002), TW_GAS_DRIP_MIN_BALANCE_ETH (0.0005), TW_GAS_DRIP_DAILY_MAX (50), TW_GAS_DRIP_DAILY_ETH_MAX (0.1), TW_GAS_DRIP_ALERT_ETH (reserve + one drip + 0.0001 gas buffer, i.e. 0.0521), TW_GAS_DRIP_RESERVE_ETH (0.05), TW_GAS_DRIP_FUNDER_WALLET (the CDP admin account) | Gas drip tuning. Amount per drip; the balance under which a wallet gets one; drips and ETH per UTC day for the whole deployment (over either: capped); the funding balance left after a drip under which (and on every funding_low cap), once a UTC day, an alert row is written (nw_tw_alert_log, kind gas_drip_funding_low), the ops Telegram channel gets a message (notify_ops_summary: needs NW_NOTIFY_OPS_CHAT_ID and NW_NOTIFY_TG_BOT_TOKEN) and a WARNING is logged; the reserve the funding account keeps (a drip that would leave less than it, after the drip and a gas margin, answers capped with reason funding_low, so drips never eat the keeper's gas when the funder is the admin account); the name of the CDP account that pays. Per address: one drip, one more at least 7 days later when the balance is under 0.0001 ETH, never a third; two addresses per account |
TW_TEST_LEG_GATEWAY | The TestLegGateway on Base Sepolia. The create-call pins it as the vault's legGateway from this variable only, never from a request |
TW_HL_TEST_POOL_ADDRESS | The house pool's PUBLIC address (a value, not a secret). Read by the API (the return-call destination), the reconcile worker and the mover; when the mover also holds the key it must be that key's address (pool_address_mismatch otherwise) |
TW_HL_TEST_POOL_KEY | Secret, never printed or logged. Raw 32-byte key of the house pool: signs the pool's usdSend on Hyperliquid TESTNET and tops up gains on Base Sepolia. Chuncheon only (/etc/nightwatch/tw-leg.env, mover unit). Refused when TW_HL_NETWORK=mainnet |
TW_LEG_MIN_USDC | Default 6. Smallest first deposit of a v2 vault with a leg (tw_leg_min_deposit below it) and smallest first funding |
TW_LEG_INFLIGHT_MAX_SEC | Default 120 (Phase T). A fund or return row that has not moved for this long: the keeper withholds the mark (leg_transfer_stuck) |
TW_LEG_RETURN_FALLBACK_SEC | Default 86400. A closed mandate whose owner has not returned the leg money after this long is returned at the last good equity |
TW_LEG_MOVER_DRY_RUN (1), TW_LEG_MOVER_SEC (30, loop mode), TW_LEG_MOVER_MAX (20 mandates a tick), TW_LEG_KEEPER_SIGNER (falls back to TW_KEEPER_MARK_SIGNER, cdp; dev only against a local node) | The leg mover (nw-tw-leg-mover) |
TW_LEG_RECONCILE_DRY_RUN (1), TW_LEG_RECONCILE_SEC (300, loop mode) | The leg reconcile (nw-tw-leg-reconcile) |
(clause 6 on hl-order) | POST /torii/mandates/{id}/hl-order holds an open above the mandate's approval_above_usdc (HTTP 202, pending_approval) whenever TW_HL_LEG_ENABLED is on, with or without TW_LEG_FUNDING; it is switched off only by TW_APPROVAL_ENABLED=0 |
TW_HL_LEG_ENABLED | Default off. The one switch for the HL leg (a mandate trades in the owner's own Hyperliquid account; spec 2.2). Off: the five hl-leg / hl-order routes answer 404 tw_feature_disabled, the MCP tool mandate_hl_order is not listed and a call is an unknown tool, the Trade door has no extra check, the keeper marks vault balance only, the stop line uses the house-relay adapter. On (1, true, yes, on): needs TW_HL_LEG_SEED and the schema (python -m svc.db.schema.apply_tw_hl_leg_schema). Set on the API host and on Chuncheon (keeper mark and stop line). Read on every request |
TW_HL_LEG_SEED | Secret, never printed, never in a doc, chat or commit. Exactly 64 hex characters (32 random bytes; a leading 0x is allowed). Anything else is refused. The per-mandate Hyperliquid trade key is derived from it with HKDF-SHA256 and the mandate's public id and is never stored. With the flag on and no seed: the routes answer 503 not_configured, the stop-line worker refuses to start (exit 1) and a live close fails without guessing. Changing it orphans every bound leg (their approved keys no longer match): do not rotate it while a leg is live. Set on the API host and on Chuncheon (stop line) |
TW_HL_NETWORK | testnet (default) or mainnet: which Hyperliquid the HL leg reads and signs for. The Trade door and the relay keep their own NW_TORII_MAINNET |
NW_FORGE_SIGNAL_PUBLISH_ENABLED | Default off. Forge's switch for operator signal publishing (POST /forge/vaults/{id}/signals, MCP forge_signal_publish). Off: every call answers 503 flag_off and writes nothing. Read on every request. It is a Forge flag, listed here because door 3 depends on it |
NW_FORGE_SIGNAL_PUBLISH_RATE_PER_HOUR | Default 60, minimum 1. The most signals one vault may publish in an hour; past it, 429 rate_limited with retry_at |
TW_HOUSE_RUNNER_USER_ID | The user id of the house runner account. No default: unset, the runner worker exits 0 and does nothing, door 2 reports paper and the adopt draft names no agent. Set (with TW_HL_LEG_ENABLED on and TW_HL_NETWORK=testnet), door 2 reports testnet |
TW_HOUSE_RUNNER_KEY | Secret, never printed. The runner's agent key (scope tx); needed only for a live pass. Without it a signal is recorded skipped_no_runner_key |
TW_HOUSE_FOLLOW_DRY_RUN | Default 1 (dry): the worker records dry_ verdicts and places nothing. 0 places orders through each mandate's HL leg. Timeouts and limits: TW_HOUSE_FOLLOW_STMT_TIMEOUT_MS (30000), TW_HOUSE_FOLLOW_LOCK_TIMEOUT_MS (5000), TW_HOUSE_FOLLOW_MAX_SIGNALS (20 per mandate per pass) |
TW_KEEPER_HL_DEXES (,xyz), TW_KEEPER_HL_EXIT_BPS (15), TW_KEEPER_HL_MAX_AGE_SEC (120) | HL leg worth: the perp dexs read (empty name = main), the exit allowance in basis points of open position value, and how old a Hyperliquid answer may be before the mark is withheld |
TW_HL_LEG_MAX_OPEN_NOTIONAL_MULT | Default 2. The most open notional the HL leg may carry, as a multiple of the mandate's daily cap; checked on every opening hl-order against the positions on the owner's account. Positions carry over days, so the daily cap alone does not bound them |
TW_STOP_LINE_TRANSIENT_PAGE_TICKS (10) | A stop-line close that stays transient (Hyperliquid unreachable, partly filled) for this many ticks in a row makes the worker exit 1 so the unit pages; one tick is TW_STOP_LINE_SEC seconds (default 30), so ten ticks are about five minutes |
TW_HL_SLIPPAGE_PCT | Aggressive-price band of the leg's IOC orders as a fraction, default 0.05; a value outside 0 to 0.5 is ignored |
TW_HL_INFO_URL, TW_HL_EXCHANGE_URL | Test and local-rig only: point the HL leg at a fake Hyperliquid server. Honoured only for a loopback host (127.0.0.1, localhost); any other value is ignored. Never set in production |
TW_PUBLIC_WEB_BASE, TW_PUBLIC_API_BASE | The two public base URLs Torii puts in what it hands out, never the request Host. Web (TW_PUBLIC_WEB_BASE, else NW_PUBLIC_SITE_URL when set, default https://toriiwallet.com): the owner alert links. API (TW_PUBLIC_API_BASE, default https://api.nightwatchprotocol.com; NW_API_BASE is not read, it belongs to the workers and the record anchor): the URLs inside GET /torii/door and GET /torii/skill.md. docs in the door stays https://toriiwallet.com/llms.txt. Only https:// values are accepted; anything else is ignored and the default applies |
TW_POLL_SECONDS, TW_CONFIRMATIONS (default 2; every chain-log ingest, the poller, page loads, POST /vault and POST /vault-tx, reads to head minus this; a failed head read ingests nothing), TW_CHUNK_BLOCKS (200, deposit-watch; the public node's eth_getLogs limit), TW_START_LOOKBACK | Chain reader pacing |
TW_DEPOSIT_ADDR_BATCH (50), TW_DEPOSIT_BACKFILL_LOOKBACK (2000 blocks), TW_BLOCK_SECONDS (2), TW_REORG_WINDOW_SECONDS (3600), TW_REORG_RECHECK_MAX (50) | Deposit watcher: addresses per eth_getLogs filter; how far back a late-registered address is scanned once; the block time used to estimate its registration block; how long a confirmed deposit is re-checked for a reorg, and how many per poll |
TW_LINKS_FROM_NW | NightWatch to wallet links |
NW_PAYMASTER_URL, NW_PAYMASTER_KEY, NW_PAYMASTER_METHOD | Gas sponsorship (Coinbase paymaster, Base only). Fail-closed when the URL is unset. Not on today |