Startup preflight
A pure go/no-go gate that runs once before the first live order — clock skew, credential validity, disk headroom and network — with per-venue thresholds derived from measurement, a bound on every leg, and a demotion to paper that is enforced.
crates/vike-mount/src/preflight.rs opens with the sentence that justifies it: on mainnet day the
first real order must not be the probe. A drifted host clock used to be discovered as a binance
-1021 INVALID_TIMESTAMP on the first signed order, a dead key at the first submit, a full disk
by the recorder and the journal dying in silence, a dead resolver by every reconnect failing at the
hostname step. The preflight turns each into a structured report an operator reads at t=0.
Pure core, injected probes
Nothing in preflight.rs performs I/O. Every observation arrives through PreflightProbes, a
four-method trait — local time, a venue's server time, a cheap authenticated read, free bytes under
a directory — so the whole decision surface is unit-testable with no network, venue or filesystem.
crates/vike-mount/src/startup.rs is the one production wiring: run_startup_preflight builds the
real probes and vike_run::build_node runs it at the top of the venue mount. An unwired leg returns
NO_PROBE, which never reads as a pass: the clock and disk legs warn — "could not measure" is not
evidence of a fault — while the credential leg fails, because "we could not prove these keys
work" must not mount a venue live.
The clock leg
A clock reading has four outcomes. A measured skew is judged against that venue's own thresholds; a
venue that publishes a clock and did not answer warns; a venue declared to have no clock leg where a
drift costs it nothing is not-applicable and never a warning; and a venue with no leg whose auth
signs the clock into the order path warns as a disclosed gap — polymarket is the one such row.
crate::server_time is the roster-gated table that declares which is which.
The thresholds are per venue because one global number was measurably false. The binance and bybit
signers hard-code a 5000 ms recv_window, shared between local skew, one-way latency and venue
queueing, so DEFAULT_CLOCK_FAIL_MS is 2500 — half the budget, past which one ordinary latency
spike pushes orders outside the window — and DEFAULT_CLOCK_WARN_MS is 500. Only a venue that
rejects an order over drift may fail: deribit and ig stamp no timestamp and hyperliquid binds the
clock into a nonce valid for about a day, so they carry no fail threshold and warn at
CANARY_CLOCK_WARN_MS, 1000 ms. The measurement that forced the split, from an NTP-disciplined
prod2 on 2026-08-09: bybit read +9..23 ms, okx +10..37, aster +15..33 — while forty samples of
hyperliquid's testnet node inside three minutes ranged from -220 to -424 ms. That is the venue's
clock wandering, not the host's.
A reading is only as sharp as its round trip: the midpoint correction assumes a symmetric path, so
the verdict is taken from what the reading proves — |skew| - rtt/2, floored at zero. Bybit's demo
host once read 182 ms of apparent skew on a 549 ms round trip while every other sample read 13-23
ms; under the floor, a slow link cannot manufacture a warning. Up to DEFAULT_CLOCK_SAMPLES (3)
readings are taken, the smallest round trip wins, and resampling happens only while the band still
straddles a threshold — one call in every ordinary case.
Every leg is bounded, and the bounds are derived
Each clock read is a blocking REST call before the first venue mounts: seven wired venues, three
samples each, on the shared 30 s agent is 630 s of possible stall. Two bounds replace that — a 3 s
per-read CLOCK_READ_TIMEOUT, and a leg-wide budget from clock_budget_for of 1200 ms per venue
actually read, clamped between 5000 and 15000 ms — so the worst case is 18 s at the ceiling and
11.4 s today. The fixed 5000 ms it replaced was sized against a six-read 1781 ms measurement; by
2026-08-22 a healthy pass summed to 2973 ms, so one dead venue needed 5973 and eight venues went
unread. The credential leg is bounded separately: CREDENTIAL_PROBE_TIMEOUT is 5 s per attempt,
CREDENTIAL_PROBE_ATTEMPTS is two — one retry, spent only on a probe that answered with an error,
never on a timeout — and credential_budget_for is 3000 ms per probed venue clamped between
6000 and 20000.
The credential leg, and why the demotion is enforced
One probe is built for each of five venues — binance, bybit and okx through build_recon_client,
plus alpaca and ctrader through their own bridge factories — whose credentials resolve and whose
ceiling arms them, each performing that venue's own cheap authenticated read through its reconcile
client. Every other reconciled venue builds its ReconClient inside its own mount arm, which this
pre-mount step never calls, so it is never probed — and preflight never demotes a venue it did not
check. ctrader authenticates during construction, so its probe defers construction into the
closure, because building it eagerly would turn a refused grant into no row at all. Every probe's
error names the host it could not reach.
For its whole life the report was advisory — a fail row was logged and the venue mounted live
anyway, because a transient timeout raised the same fail as a refusal. CredentialGap removes that
blocker. Rejected means the venue answered inside the bound and refused the signed
read — a fail, and the venue is mounted paper for the session. Unanswered means nothing came back
inside the bound — a warning, and the venue mounts as configured. The enforcement withholds every
{VENUE}_-prefixed key from the map the mount reads, so make_engine sees absent credentials and
lands on the paper fallback it always had: refused keys and absent keys are the same fact, and the
old behaviour mounted live precisely when the venue had just said the keys do not work. One residual
is stated: a fast non-auth error such as a 429 inside the bound reads as rejected and demotes; the
retry stops a single blip doing it, and paper is the safe side.
A per-venue fail never flips the go bit. Only a global check — no disk headroom, no network — makes
PreflightReport::go false, and even that does not ground the process: a daemon that refuses to
start under Restart=on-failure crash-loops, so a global fail is logged at error and the mount
proceeds.
Disk, network, and what the arming ceiling gates
The disk leg was dark for a year because free space is not reachable from std; it is armed on
unix through a safe rustix::fs::statvfs and reports a declared reason on Windows, warning below
5 GiB and failing below 1 GiB over directories the caller passes — which is how it measures the
journal directory the mount actually built. The network leg spawns a NetProbe and waits at most
500 ms for its first round.
Every venue set is derived through would_mount_live_under_policy — the same predicate the mount
arms on, so preflight and mount cannot disagree about what is armed. Until that was threaded, a
deployment whose policy said ig = "paper" still had IG
contacted at every start, with a credentialed read. policy: None reads all-paper and contacts
nothing; a box with no credentials, or a full store under an all-paper policy, makes not one
network call. VIKE_PREFLIGHT_SKIP=1 — the exact string, read from the real process environment —
removes every leg. Probe errors never carry a key, secret or passphrase, the report's lines read
[FAIL] credentials (okx): … — remediation, and the arming ceiling
stays the gate consulted above all of this.
Accounts
More than one account per venue — a credential-key grammar that cannot misread an existing key, a per-account arming ceiling that inherits nothing for a second account, and a shared-book rule that reports instead of refusing.
The live data seam
DataClient and LiveDataSink — the live mirror of the history store that every venue feed implements, where subscribing returns nothing and every datum arrives through a sink chosen at construction.