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.
A venue used to have exactly one credential set per tier, because the key names said so: the
grammar is {VENUE}_{TIER}{SUFFIX} and there was no place in it for a second account.
crates/vike-model/src/account_keys.rs opens with the requirement that changed that — an operator
may hold twenty hyperliquid accounts, demo and live, and wants every one visible and independently
armable.
The key grammar, and why the label goes at the end
The obvious spelling, {VENUE}_{TIER}_{LABEL}{SUFFIX}, is unparseable against keys that already
exist: several bespoke suffixes are multi-word, so OANDA_DEMO_ACCOUNT_ID would read equally well
as tier DEMO, label ACCOUNT, suffix _ID — a real live-credential key silently reclassified as
somebody's account. So the label is appended after the whole of today's key, behind a separator the
existing grammar cannot produce:
{VENUE}_{TIER}{SUFFIX} the DEFAULT account — unchanged, byte for byte
{VENUE}_{TIER}{SUFFIX}__{LABEL} a LABELLED accountACCOUNT_SEPARATOR is a double underscore. That no existing key contains one is checked rather
than assumed — no_existing_credential_key_contains_the_separator folds the enumerable key grid and
sweeps the bespoke literals out of every bridge's source — so the parse is total: split at the first
__, the left half is today's key verbatim, the right half is the label, and it stays correct for
a suffix invented tomorrow. A label is [A-Z0-9]+, at most MAX_LABEL_LEN (24) characters because
it becomes part of a lock filename, and DEFAULT is refused by name since it would be a second,
silent name for the account an unlabelled key already addresses. AccountLabel::parse is
case-sensitive and does not repair: alt is refused, not uppercased, because a spelling the loader
fixes is a spelling nobody learns.
AccountRef::route_key is the identity a process addresses an account by — the bare venue id for
the default account, exactly what ExecutionEngine::new already seeds, and venue#LABEL for a
labelled one. It is the engine's route_key, the name of the LIVE-<route_key>.lock sentinel
vike_ops::live_lock claims for one live process per venue account, and the key the journal
records; the tier is deliberately not in it. accounts_in_store derives every account from the key
names in the credential store, so the enumeration is never a list anybody maintains.
The per-account ceiling inherits nothing
policy.toml gains an [accounts] table beside [venues], and VenuePolicy::account resolves an
account's ceiling as min(the venue's line, the account's own line). The asymmetry between the two
defaults is the whole safety argument: the default account with no line of its own inherits the
venue line exactly, so today's files resolve unchanged; a labelled account with no line of its
own resolves paper. A bybit = "live" line was written when bybit had one account, and reading it
as consent for an account that did not exist then is precisely the silent escalation the
ceiling exists to prevent. The venue line caps both cases, so
bybit = "paper" disarms every bybit account whatever their own lines say.
vike_mount::venue_account_arming is the per-account projection — the rows the Data Manager renders
and the selection the mount fans out from, one function so the "Effective" column cannot disagree
with what the mount does. known_accounts unions the credential store (which accounts exist) with
the [accounts] table (which have a stated ceiling): an account with keys and no line appears as
ArmingBlock::AccountNotNamed, which tells the operator how to arm it, and an account with a line
and no keys appears as NoCredentials, so a typo'd label cannot vanish. ArmingBlock::NoAccountSupport
names a venue whose mount arm cannot address a second account at all; vike_mount's
arm_addresses_accounts is the authority for that set, and its doc says one venue remains,
dukascopy.
make_engine_accounts mounts one engine per active account. The default account is always mounted,
paper included; a labelled account is mounted only when its ceiling permits something above paper
and its credentials load — a paper engine for an account nobody armed would be a second local
book nobody asked for. Result order is a contract: default first, labels in order. Each account is
mounted on its own symbol, and a strategy mount names the account it trades on
(vike_exec::MountSpec::account, which crosses the wire and is persisted with the
runtime topology); the default account is spelled as an absent field,
so an account-less mount's bytes are unchanged.
The shared-book rule reports and never refuses
crates/vike-config/src/venue_accounts.rs states a correction first: the symbol is not part of
the rule. An earlier version refused two active accounts armed on one symbol, and the thing it
refused is an ordinary spread — long on account A, short on account B. Two accounts are two wallets;
the venue nets nothing between them.
The real hazard is two of this process's engines pointed at the same venue book — an agent key
signing for a master whose own key is also configured, or one credential set pasted under two
labels. Each engine reads "I hold 1" while the venue holds 2, and reconcile's auto-applied
PositionDrift rewrites each onto a total that
includes the other's. shared_books finds every pair of distinct, active accounts of one venue
resolving to one book, and make_engine_accounts emits one warning per pair naming the venue, both
labels and the book — and mounts both. The operator wrote both credential sets by hand under
explicit labels; a refusal would strand a venue on paper over a configuration they may have meant,
while a line naming the two labels lets them recognise a paste error in seconds. paper is not
active, since the paper exchange fills locally; demo is, because a demo account is a real book
with real rejections.
Where the book cannot be determined, nothing is said
vike_mount::book_identity answers per venue. An EIP-712 venue derives its wallet address from the
private key in the store, so the answer is exact and offline; a key/secret venue's store holds an
HMAC key and nothing that names an account, so it contributes no ArmedBook and no pair can form —
a placeholder would compare equal to every other unknown and manufacture a pair. The declared
residual: the same api key and secret pasted under two labels on binance, bybit, okx or deribit is
the same book and the table says nothing, because a row may only name values that are safe to log.
One more thing follows the account rather than the venue: Event::AccountState names no symbol, so
account_event_sender scopes each account's exec-event lane to its route key, and a second
account's balances land in its own book rather than the venue's default engine.
Mounting
A run profile names the whole assembly in one auditable file, a strategy mount has a deterministic identity with a durable state sidecar beside it, and runtime mounts survive a restart through a sidecar of their own rather than through the journal.
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.