vike
Concepts

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 account

ACCOUNT_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.

On this page