Node status
Ask a running vike-tradehub node what it is doing — `vike-cli trade status`, one read-only observe-scoped command, which replaced the retired top-level `strategy-status` verb.
`strategy-status` is retired — the command is `vike-cli trade status`
There is no alias and no named refusal. The binary does not know the word strategy-status, so
typing it is an unknown-verb error that names no replacement — unlike sweep, walkforward and
study, which are refused by name with their successor printed. If you arrived here from a
bookmark, the command you want is vike-cli trade status --node HOST:PORT.
Ruling 17 folded the verb into the trade surface, which already holds an Observe connection to the
node, and merged the REPL's old state read into it at the same time. state is gone — from the
REPL and from vike-cli trade alike — because it was a READ and a WRITE on one word: state
printed the mode, state halted halted the daemon, and the only thing between them was a second
token. halt and resume are their own verbs now, named for what they do, and this one read
answers both former questions. Most of what the old page described survived the move; the frame
changed and the output gained the trading mode.
trade status is the shell-level answer to what is that daemon actually doing? — the trading mode
and the mounted-strategy registry, in one output. It is read-only: it authenticates under the
observe scope and there is no spelling of it that writes.
The wire verb existed before the command did. Until this module the only way to ask a running node what it was running from a shell was to compile a throwaway probe — the gap the I10 rehearsal found, and the problem this command was built to solve.
Two reads, because no single wire verb answers both
The registry comes from vike_tradehub_client::strategy_status (Request::StrategyStatus) and the
mode from vike_tradehub_client::snapshot_once (Request::Snapshot). Each is its own short-lived
vike_tradehub_client::Scope::Observe connection, registry first, mode second —
crates/vike-cli/src/cmd/trade_status.rs's run performs them in that fixed order. Neither frame
carries the other's fields, and merging them would mean a NODE_PROTO_VERSION bump, which breaks
the handshake against every running node, to save one round trip on a command a human types.
The registry read decides the exit code. It does not decide what is printed. Both halves are rendered as an answer or a reason there is not one, never as an absence:
- the mode read failed after the registry answered → the registry still prints, and the mode line
(or, under
--json, atrading_state_errorkey) says why it is missing; - the registry read failed on a node that answered — one too old for the verb, or one refusing it
— → the mode read is attempted anyway and printed, because
Request::Snapshotis a base verb of the protocol that every node has always served.crates/vike-cli/src/cmd/trade_status.rs'smode_read_worth_attemptingcarries the two kinds it deliberately does not retry under.
The exit stays the registry's, so a wrapper cannot read a partial answer as a whole one.
Why the mode half is worth two connections
Until 2026-09-10 a registry failure returned before the mode read ran, so trade status against a
node too old for the strategy-verbs capability printed no trading mode at all — the operator asked
a halted node what it was doing and was told only to upgrade it. "Is this node halted" must not be a
question you can ask and get silence to.
Usage
vike-cli trade status --node 10.0.0.5:9200vike-cli trade status --node 10.0.0.5:9200 --json| flag | meaning |
|---|---|
--node HOST:PORT | the node's observe address. Required — there is no default node to ask |
--json | the node's answer as JSON instead of the table |
-h, --help | the usage text, on stdout, exit 0 |
Both spellings of the valued flag parse — --node 10.0.0.5:9200 and --node=10.0.0.5:9200 — and a
token beginning -- is a flag, never a value, so a dangling --node is a named usage error rather
than an address of --json. That rule is crates/vike-cli/src/cmd/args.rs's is_flag_token,
spelled once for every subcommand because without it a valued flag ate whatever followed it,
including another flag. --json is a bare boolean and rejects an inline value (--json=1 →
--json takes no value). An unknown argument is rejected by name. See
global flags.
A bare word is a usage error, never a write: trade status halted is rejected as an unknown
argument, because the grammar takes flags only. That is the whole of ruling 17 expressed in the
parser — nothing that changes the daemon's mode may be spelled by adding a word to something that
does not. crates/vike-cli/src/cmd/trade_status.rs's parse is where it is enforced, and
a_bare_mode_word_is_a_usage_error_and_never_a_write pins it for halted, reducing, active and
state alike.
One credential, and it is the observe one
Both wire verbs are read-only, so the node answers them under either scope, and both client
functions always connect under Scope::Observe. The command therefore needs exactly one key:
VIKE_TRADEHUB_OBSERVE_KEY, resolved by the dispatcher the same way trade and mcp resolve
theirs — the process environment first, then <project>/settings/node.env, the same node-key
store the daemon itself reads. crates/vike-cli/src/cmd/nodekeys.rs's resolve argues that
precedence: an export is ephemeral, narrow and deliberate, while the store is the box's standing
configuration.
That same function trims whichever value it picks, because the server's
vike_tradehub_client::auth::from_vars trims too, and the agreement is load-bearing rather than
tidiness: an untrimmed client signs with secret\n against a server verifying secret, and the
mismatch comes back as an opaque auth refusal instead of your key file has a trailing space.
A box whose pair is still in the older <project>/settings/secrets.env keeps authenticating — the
fallback is a migration, not a deprecation that breaks a running deployment — and is told once, on
stderr, which file it read and which file the pair belongs in now. vike-cli backend setup writes
the new one. Nothing about the migration reaches stdout, which --json makes a document.
A control key cannot stand in for an observe key
The node verifies each scope against its own key, so "has any key" would be the wrong question here.
crates/vike-cli/src/cmd/trade_status.rs's run asks for the observe half specifically, and the
absent-key message is the observe-specific NodeKeyring::observe_absent_message — it names the
variable, both places it was looked for, and the store path a key would go in. No connection is
attempted without a key, so nothing about the address is reported.
Output
Human form: the trading mode, a daemon identity line, the daemon's effective-params line, a blank
line, then one aligned row per mount. Column widths come from the data, headers included, so a long
strategy name cannot shear the params column. The mode cell is uppercase exactly when it matters —
LIVE shouts, paper is the quiet default. The rendering is pure and pinned on a two-mount
fixture, one paper row and one live:
trading state: Active
node hub-a — paper — build vike-tradehub 0.1.0 (abc1234, clean)
params: spread=0.01 size=20
STRATEGY MODE PARAMS
spread_maker paper venue=polymarket symbol=TOK-A spread=0.01
np LIVE venue=polymarket symbol=TOK-B edge=0.02The mode is first because an operator asking what a node is doing reads the kill switch before the
roster, not after it. trading state: is Active, Reducing or Halted, and when the core sits
in safe state a second line follows it:
FAULT (core halted safe-state): fold panic in engine 0That block is crates/vike-cli/src/cmd/trade_status.rs's mode_lines, which sits in this command's
own module rather than in crate::cmd::trade, because three surfaces render it — this verb, the
REPL's status, and the REPL's snapshot — and they may not word a kill switch differently. Before
ruling 17 there were two spellings of it and they already disagreed.
--json
The payload the node sent is nested verbatim under strategy_status, with the mode fields
beside it. The nesting is the point: a machine consumer gets the wire schema, which cannot drift
from the node's, rather than a second hand-maintained shape. The two mode keys have no wire struct
of their own to be verbatim about. The document is pretty-printed —
crates/vike-cli/src/cmd/trade_status.rs's json_body ends in serde_json::to_string_pretty — so
it spans lines: read it with a parser, not with a line filter.
| key | present when | value |
|---|---|---|
trading_state | the mode read answered | "Active", "Reducing" or "Halted" |
fault | the mode read answered | the safe-state fault string, or null |
trading_state_error | the mode read failed, or was not attempted | the reason, as a string |
strategy_status | the registry read answered | the vike_tradehub_client::wire::WireStrategyStatus payload — identity, effective_params, mounts |
strategy_status_error | the registry read failed | the reason, as a string |
A half that could not be read is a key, never an omission. No mode is invented — trading_state
and fault are genuinely absent when the read failed — and trading_state_error is what stops the
document reading as this node has no mode. The earlier shape dropped the mode keys in silence, on
the argument that a consumer requiring trading_state would fail loudly; that holds for a strict
typed deserializer and for nothing else. jq .trading_state answers null for an absent key exactly
as it does for a null one, serde_json::Value indexing returns null for both, and a human sees a
document with one fewer line.
A mode read that was made and failed puts its reason on stderr as well, in both output modes,
because a --json stdout is a document a script parses, not a transcript anyone reads. A mode read
that was never attempted gets no line of its own: crates/vike-cli/src/cmd/trade_status.rs's
run guards that stderr line on a read that actually happened, and the reason it was skipped — the
registry failure — is already on stderr immediately above, where repeating it would bury it. The
JSON key is there either way.
The three registry failures, and what each one tells you
crates/vike-cli/src/cmd/trade_status.rs's failure_lines is pure, so each wording is pinned by a
unit test rather than needing a downgraded daemon to reproduce.
| condition | what is printed on stderr |
|---|---|
io::ErrorKind::Unsupported | the feature-negotiation refusal: the node's Welcome advertised no strategy-verbs capability. The client's own sentence names the missing capability and says nothing was sent; the added line is the action — upgrade that node's vike-tradehub and re-run |
io::ErrorKind::PermissionDenied | the observe handshake itself was refused. Points at the key, not the verb, and names vike-cli secrets path as the way to see which store this side read it from |
| anything else | an honest transport-shaped report naming the address |
There is no fourth arm, and one case is worth naming because the exit table below does separate it:
io::ErrorKind::InvalidData — the node answered and refused the verb, which is what a publisher
with no identity block sends — lands in that last row and prints the same cannot query line as an
unreachable socket. Where it is singled out is the rung
(crates/vike-cli/src/cmd/trade_status.rs's failure_exit) and the retry
(crates/vike-cli/src/cmd/trade_status.rs's mode_read_worth_attempting), never the wording. Do
not go hunting a distinct InvalidData message on a live box.
Every one of those lines names the verb as the operator typed it, from a single constant, so a message can never point back at the retired top-level spelling.
The old-node refusal is client-side: the verb is feature-negotiated through
crates/vike-tradehub-client/src/proto.rs's FEATURE_STRATEGY_VERBS, and against a node that does
not advertise it the client sends nothing after the handshake. An older node's serde cannot decode
the frame, so sending would produce an opaque decode error at the node instead of a diagnosis here.
The end-to-end test scripts exactly that node and asserts the connection carried no frame at all.
Exit codes
The rule is did the node answer. Everything a reachable box can tell you is a permanent
configuration fact, and a wrapper that retries one of those retries it forever; only a socket that
never got an answer is worth backing off and trying again.
crates/vike-cli/src/cmd/trade_status.rs's failure_exit decides the rung over the same
ErrorKind match that decides the words, so the two cannot come to disagree about which failure is
which.
| outcome | exit |
|---|---|
| the registry read answered | 0 — including when the mode read failed; that half is best-effort |
the node answered and refused: too old (Unsupported), observe key rejected (PermissionDenied), node-side error or unexpected response (InvalidData) | 1 |
| no answer came back: connection refused, unroutable, timed out, aborted | 3, the connect rung |
| no observe key resolved anywhere | 1 |
usage error — an unknown argument, a bare word, a missing --node, --json=1 | 2, the usage rung, on stderr |
-h / --help | 0, on stdout |
Those last two rows are decided in one place for the whole binary —
crates/vike-cli/src/cmd/args.rs's exit_for_parse_error, which turns the help short-circuit into a
success on stdout and puts everything the parser rejected on the usage rung rather than the
run-failure one — and crates/vike-cli/tests/help_cli.rs runs the real binary to assert the status,
the stream, and that no internal token leaks. The rungs themselves are
crates/vike-cli/src/exit.rs's Exit, and the split is what a wrapper branches on: a 2 is yours
to fix, a 3 is worth waiting on, a 1 means it ran and failed. A misspelled flag and a node that
answered and refused are deliberately not the same rung.
What is covered
The grammar, every renderer and both halves of the failure mapping — the words and the rung — are
pure and unit-tested in the module itself. The connections are
crates/vike-cli/tests/trade_status_cli.rs, which drives the shipped binary against a real loopback
node with no credentials, no venue and no external network:
trade_status_renders_the_mode_the_node_identity_and_mount_rows— both wire reads, the mode above an identity line and one row per mount, and nothing on stderr on the happy path;trade_status_json_nests_the_wire_payload_beside_the_mode— the machine surface parses and carries the node's own field names;an_identity_less_node_is_an_honest_error— the server-error path exits1, never3, and the mode still comes back;an_old_node_is_refused_client_side_but_still_answers_the_mode— a scripted node lackingstrategy-verbs: an actionable message, no frame sent on that connection, and the mode asked for and printed on a second one;a_failed_mode_read_is_a_json_key_and_a_stderr_line— the other direction, a node that answers the registry and refuses the snapshot;with_no_observe_key_anywhere_the_message_names_both_sourcesandlegacy_keys_still_authenticate_and_say_so— the absent-key message, and thesecrets.envbox that must keep working and must be told where the pair belongs now.
See also
vike-cli trade— the surface this verb lives in.haltandresumeare the verbs that change the mode, and they authenticate with the CONTROL key (VIKE_TRADEHUB_CONTROL_KEY), not this read's observe one.haltandresumeare the verbs that change the mode- The node protocol — the keys, the frames, the handshake
vike-cli secrets— where the key was read fromvike-tradehub— the daemon on the other end