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.
HyperliquidExecutionService.ts:870-871
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.
archive/config.ts:4-9)
archive/archiver.ts:4-6)
archive/archiver.ts:6-7)
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.
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).
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.
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.
no_trading_signer.
needs_trading_account.
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.
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.
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).
"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).
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).
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).
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).
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).
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).
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).
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).
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.
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).
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).
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).
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).
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.
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
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
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