Research mode · the archive

Research works without a signing key.

Research mode works without a signer. Market data, charts, alerts, backtests, validations, missions, and wakes remain available. Only actions that require a signature refuse, and one resolver enforces that boundary. A separate archive process records history that Hyperliquid no longer serves.

READS FLOW · NO SIGNER ANYWHERE IN THE PATH archive WS candles · 1-min tail poll chart RPC isChartReadEntitled · no key asked panels chart · watchlist · alerts · scenes SIGN PATH · REFUSED AT THE ONLY SEAM order intent placeTradingOrder · trading_enter resolveSigner execution layer · fail-closed signer_not_configured no POST /exchange · nothing signed
top lane always runs; bottom lane stops at the signer, before any wire HyperliquidExecutionService.ts:870-871

The UI explains research mode in one line.

T3 Trade signs orders with a Hyperliquid key you supply. If you have not supplied one, the app does not stop at the door: "Everything that reads the market keeps working, and only signing refuses" (docs/user/research-mode.md:1-5). The trade home and the settings workspace each carry exactly one line that says so, and nothing else nags.

~5,000 bars
what Hyperliquid serves per interval, and nothing older; a 1m series reaches back about three and a half days (archive/config.ts:4-9)
1 min
the archive's tail poll, refreshing every candle series the WS feed cannot vouch for (archive/archiver.ts:4-6)
30 min
funding pulled forward from a high-water mark, every half hour (archive/archiver.ts:6-7)
0 keys
credentials the archiver holds: it "never authenticates, never sees a key, and never touches an order endpoint" (archive/config.ts:11-16)

The line itself is a constant, rendered by one function that stays silent unless the server reports the signer as explicitly unarmed: describeSignerState returns RESEARCH_MODE_LINE only when signerArmed === false, and null when the field is absent, because servers that predate the field had one meaning (apps/web/src/components/trading/tradeHomePresentation.ts:87-93). The field is an optional boolean on the account view (packages/contracts/src/trading.ts:899).

The comment above the constant states the design position: research mode is "deliberately not a modal, not a nag, and not an onboarding flow: research mode is a working state, and the only thing a user needs is to know it before an order is refused rather than after" (tradeHomePresentation.ts:82-86). Settings shows the same sentence plus a pointer to the user doc (apps/web/src/components/trading/TradingWorkspacePanel.tsx:440-455).

What keeps working is the list the doc commits to: charts on every timeframe, the watchlist and universe search, price/time/metric alerts that arm and fire, backtests against recorded bars at real fees and funding, forward validation on live bars, missions that wake, read and journal, and published plans drawn on the chart with declared levels (docs/user/research-mode.md:14-32). The Missions watches wakes loop runs in full; only the capital-moving half of it refuses.

One resolver rejects every unsigned write.

There is no feature flag called research mode. The boundary is a single function in the execution layer, and the layer graph is wired so the read surfaces never get near it.

The signer is resolved inside HyperliquidExecutionService by resolveSigner: if the key config yields None, the effect fails with TradingExecutionError({ stage: "signer_not_configured" }), "reported as signer_not_configured rather than a defect. Every wire path runs through this" (apps/server/src/trading/HyperliquidExecutionService.ts:503-519). The resolution used to be pasted at six call sites; it is one function now. submitOrder resolves the signer as step 1, before market metadata, before the order book, before any preview (HyperliquidExecutionService.ts:865-871).

The key itself comes from InterimSignerConfig: the env var T3_TRADES_INTERIM_SIGNER_KEY or the default machine-local file ~/.t3trade/secrets/hyperliquid-interim-signer-key.bin. "When the key is absent the signer resolves to Option.none() and every signable action is rejected", deliberately, because this is the only code path that spends testnet capital (InterimSignerConfig.ts:19-32). How the key is loaded, checked and kept out of logs is the Signer & authority page's story.

Above that seam, the composition root in runtimeLayer.ts builds read surfaces on layers that structurally cannot sign. HyperliquidReadLayerLive merges exactly three services: the info client, the market resolver, and the read gateway (runtimeLayer.ts:81-85). TradingCoreLayerLive, which the file documents as "Mission services that do not require the exchange write path", builds on it: mission projection and service, market price, chart, universe (runtimeLayer.ts:89-123). The signer, the exchange client and the nonce coordinator appear only in TradingFoundation, the layer the execution consumers build on (runtimeLayer.ts:153-165).

A typed refusal at one boundary

No code path discovers a missing signer by throwing deep inside a signature routine. The one resolver answers for all of them, with a typed stage a surface can quote verbatim. A research install is not an error state to be caught; it is the same binary with the foundation's signer chip empty.

The wall of honest refusals.

Every refusal names what still works, quotes the book where a quote is still possible, and refuses at arm time rather than at fire time. These are the real strings, verbatim from the repository.

trade home · settings
Research mode: no trading key is configured. Observation, backtests and validations work; orders will be refused.
The single research-mode line, one constant rendered in two places, silent the moment a signer is armed.
tradeHomePresentation.ts:87-93
manual ticket · keyless preview
no trading signer is configured on this environment, so this order would be refused; charts, watchlists, alerts, backtests and validations all work without one. The book quotes (market) bid (best) ask (best), so a (side) now would fill near (level)
The preview still reads the public book and interpolates the live quote; an unreadable book costs the numbers, never the sentence. Refusal code no_trading_signer.
TradingManualEntryService.ts:202-218, 265-270
agent entry · trading_enter
nothing was placed: this environment has no trading account (no signer armed), so an entry cannot be priced, sized or signed. Arm a testnet account, then enter again.
One honest refusal at the same seam the watch tool refuses at, not an account read that errors the whole call after the mission was already bound. Refusal code needs_trading_account.
TradingEntryService.ts:245-257
every keyless wake
no trading account is attached here: no signer is armed, so sizing is unavailable and orders will be refused; observation, backtests and validations are unaffected
One sentence per wake, naming both halves: what will not work and what still does. "A keyless install is a research terminal, not a broken one."
TradingWakeupComposer.ts:811-821
position-scoped alert · at arm time
a '(condition)' condition needs a mission position; account alerts take price, metric, derived, or time
Profit, give-back and fill alerts are refused when armed, not armed and left silent: "An alert that can never fire is worse than no alert, because silence reads as 'the level was not reached'". The code interpolates the asked condition kind.
TradingAlertService.ts:202-207 · docs/user/research-mode.md:41-44
mandate · stand-in capital
No account could be read when this mission was created, so the capital below is a stand-in rather than a balance. Sizing is unavailable and orders will be refused.
A mandate nobody could size is labelled as the stand-in it is: "Saying so beats presenting the fallback as measured."
TradingWorkspacePanel.tsx:96-104

The pattern is one rule applied everywhere: a refusal must be more informative than the action it refused. The ticket quotes the book it cannot trade on. The wake says what is unaffected. The mandate admits its number is a stand-in. None of them pretend, and none of them nag.

What each surface reads, and what it cannot touch.

Six research surfaces, one shared shape: each is wired to reads and its own tables, and the wiring itself is the claim that it cannot place an order.

Charts

read gateway + archive

The chart pairs a live snapshot with candle history, sharing the read gateway so the two "never disagree on freshness"; it "reads a closed window out of the archive before it asks the exchange, which only serves the last ~5,000 bars" (runtimeLayer.ts:99-105). Entitlement is checked, not identity: "The chart RPC must not become a free Hyperliquid proxy", so a mission on the market or membership in the follow set entitles, and anything else fails with "Market (name) is neither followed nor held by a mission" (chartReadEntitlement.ts:1-18, 52-63; enforced at ws.ts:2322-2343). A terminal mission still entitles the windowed review chart, because "refusing a terminal mission on the windowed path would refuse every review chart there is" (chartReadEntitlement.ts:12-16).

Backtests

archive only

"trading_backtest reads the same archive and nothing else." The archive layer is provided explicitly at the wiring "so the one dependency it has is visible" (runtimeLayer.ts:221-227). A backtest runs against bars that already exist, at the fees and funding a real trade would have paid (docs/user/research-mode.md:23-25). There is no second price source to consult; the repository records that decision as an ADR that is explicitly not decided (docs/internals/historical-price-adapter-decision.md).

Forward validation

archive + own paper tables

An idea is watched on live bars for as long as asked, and every paper trade is recorded (docs/user/research-mode.md:25-27). The service "reads the same archive and writes only its own paper tables"; the explicit dependency set "is the claim that it cannot place an order" (runtimeLayer.ts:228-235). The chart even draws an armed thesis's paper trades as markers, as a read that "can never fail the chart" (runtimeLayer.ts:106-114).

Research scenes

SQL and Crypto

The UI shows research scenes below the message composer. The WS handler says it outright: "Research mode through and through: scenes are research output, the read needs no signer and no mission", and the only scope checked is the thread's own (ws.ts:2299-2306). The scene record's dependency set is "SQL and Crypto, and nothing that could reach an order" (runtimeLayer.ts:242-245).

Missions, plans, wakes

core layer, no write path

A keyless mission can be started, wakes on its triggers, reads the market and the archive, publishes plans and keeps its journal (docs/user/research-mode.md:28-32). The mission service is part of TradingCoreLayerLive, the layer documented as needing no exchange write path (runtimeLayer.ts:89-123). A published plan is recorded and drawn on the chart; "without a key nothing is placed on the venue, and the agent is told so on every publish". Every wake carries the no-account line, so the agent is "not left to discover this from a failed order" (docs/user/research-mode.md:46-50).

Alerts and watches

read gateway validation

Account-scoped alerts validate their markets through the read gateway, the same reads the watchlist uses (TradingAlertService.ts:214-221). Four condition kinds arm keyless, verbatim from ACCOUNT_WATCH_KINDS: price, metric, derived, time (TradingAlertService.ts:50-55). The line between arming and refusing is position scope, not signer presence, and it is drawn at arm time with the reason named (TradingAlertService.ts:202-207). A time watch has no market to validate; everything else is checked against the live universe (TradingAlertService.ts:214-216).

Ideas and events

SQL and Crypto

The hypothesis record (ideas, versions, runs) and the event calendar the engines read carry the same dependency set as the scene record: "SQL and Crypto, which is the same claim again: a filed idea is research, and nothing here can reach an order" (runtimeLayer.ts:236-241).

The archive: record it or lose it.

Hyperliquid's Info API "is a window, not a history": every candle interval is capped at roughly the most recent 5,000 bars, "so a 1m series reaches back about three and a half days and nothing older is ever served again. The only way the lab owns that history is to write it down as it goes by" (apps/server/src/trading/archive/config.ts:4-9). The supervisor's header says it shorter: "a minute not recorded is a minute gone" (ArchiveSupervisor.ts:4-6).

One sequential tick, forever

boot

Full-window backfill

Startup "always begins with a full-window backfill, which makes killing the process a supported way to operate it rather than an incident" (archive/archiver.ts:15-16).

continuous

WS candles between ticks

Candles arrive primarily over the WebSocket feed; "the poll is boot backfill and gap repair" (archive/archiver.ts:6-10). Sequential rather than three timers, so the single-flight Info client is never contended.

every minute

Tail refresh, context, book, heartbeat

Each tick refreshes the tail of every candle series the WS feed cannot vouch for, samples the derivatives context and the top of book, and prints a heartbeat (archive/archiver.ts:4-6).

every half hour

Funding from the high-water mark

Funding is pulled forward from a high-water mark, so backtests charge the rates a real trade would have paid (archive/archiver.ts:6-7).

failure

Nothing throws

A failed request returns null and the tick moves on; "a minute lost to a 429 is repaired a minute later without a retry queue" (archive/archiver.ts:12-15).

The archive is its own process, not a thread in the server. ArchiveSupervisor spawns it as a Node child "restarts it with backoff when it dies, reads its heartbeat off stdout" (ArchiveSupervisor.ts:11-13). It starts with the server, but only while the trading lease is held, so a second server against the same state never double-runs mission machinery; the archive line sits inside the same guard (apps/server/src/orchestration/Layers/OrchestrationReactor.ts:37-47).

Its lock is deliberately not the trading lease. The trading lease is scoped to a state directory; the archive "is machine-wide market data that a dev server and the installed app read from the same file. So the supervisor takes a lease on the archive path itself, and a second server on the same machine supervises nothing" (ArchiveSupervisor.ts:15-19). Writer state is one explicit word, derived from the lease and the heartbeat: owned-writer, healthy-external-writer, restarting, stopped, stale, unavailable (ArchiveSupervisor.ts:46-67).

Which network it records is passed down as T3TRADE_ARCHIVE_NETWORK, so the recorded venue is the traded venue; a hand-run archiver with nothing set keeps its historical default of mainnet (archive/config.ts:42-50). A fresh install with no follow state records a cold-start floor of exactly two coins, BTC and ETH, "so a brand-new archive is not empty" (archive/config.ts:28-35).

What gets deep recording is the follow set: what is held, armed, watchlisted, what a chat is looking at, what was charted a moment ago. It is derived on read from tables that already exist, and it reaches the archiver "through a small JSON file beside the archive that the child re-reads each tick. A file rather than a table because the archive database has exactly one writer and this would have made it two" (FollowSetRegistry.ts:2-21).

Byte-stable replay

When an argument needs a fixed dataset, it uses the checked-in replay window: the 2026-08-13 ETH morning, "the grind lower from ~06:00 UTC, three boundary entries that failed, then the breakdown", with candles that are "the testnet feed's own record (the venue the missions trade), fetched once and checked in", so every replay of the window is the same replay, byte for byte (TradingReplayWindow20260813.ts:5-12).

Turning execution on changes one thing.

Supply the key and restart the server. "Nothing else changes: the research surfaces behave identically, and the research-mode line disappears" (docs/user/research-mode.md:56-60). The layers do not rewire; TradingFoundation simply resolves a signer where it resolved none, and the read path that never asked is untouched.

The same read paths run with or without a signer. Research mode is the full product without a configured signer, not a degraded mode with execution added later. Where this sits in the Big picture is exactly here: reads fan out to every surface, and the one write path funnels through a single guarded seam. What happens when the chip is full, from nonce lanes to protection invariants, is the Execution and Risk story; who may fill it, and how the key is guarded, is Signer & authority. The surfaces this page walked live on in every install, key or no key, on Web & desktop alike, and the terms they use are defined in the Glossary.

The boundary

  • apps/server/src/trading/HyperliquidExecutionService.ts:503-519 · resolveSigner, fail-closed
  • apps/server/src/trading/HyperliquidExecutionService.ts:865-871 · step 1 of submitOrder
  • apps/server/src/trading/runtimeLayer.ts:81-85 · read layer contents
  • apps/server/src/trading/runtimeLayer.ts:89-123 · core layer claim
  • apps/server/src/trading/runtimeLayer.ts:153-165 · foundation, where the signer lives
  • packages/contracts/src/trading.ts:899 · signerArmed, optional

The refusals

  • apps/web/src/components/trading/tradeHomePresentation.ts:82-93 · the one line
  • apps/server/src/trading/TradingManualEntryService.ts:195-218 · keyless quote
  • apps/server/src/trading/TradingEntryService.ts:245-257 · nothing was placed
  • apps/server/src/trading/TradingWakeupComposer.ts:811-821 · wake line
  • apps/server/src/trading/TradingAlertService.ts:50-55, 202-207 · arm-time refusal
  • docs/user/research-mode.md · the user-facing contract

The archive

  • apps/server/src/trading/ArchiveSupervisor.ts:4-19 · why it runs, how it locks
  • apps/server/src/trading/archive/archiver.ts:4-16 · the tick
  • apps/server/src/trading/archive/config.ts:4-50 · window, keys, network
  • apps/server/src/trading/FollowSetRegistry.ts:2-21 · one writer
  • apps/server/src/ws.ts:2299-2348 · scenes and entitlement
  • apps/server/src/trading/TradingReplayWindow20260813.ts:5-12 · the fixed morning