Per-strategy ownership: your kitchen, your keys
We dropped the global trading-user model. Each strategy now reads venue keys from its owner's vault.
Old model: one settings["exchanges"] dict in the database, holding
Binance + OKX keys, used by everyone. Whoever was logged in as admin
could put their keys there, and the trading engine would use them for
every strategy — long sleeves, intraday levels, paper, live.
This breaks as soon as you have two operators. It breaks worse when you think about onboarding a third. It breaks worst when you think about a team’s incident response: rotating keys means stopping every strategy.
The new model
Two columns and one new table:
PortfolioMeta.owner_user_id— every long-sleeve strategy is owned by exactly one user.TradingAccount(in the trading DB) — every trading-class strategy is owned by exactly one user, has a venue + credential label.UserCredential— Fernet-encrypted blob, one per (user, provider, label).
When the engine wants to trade paper_levels_bob, it asks _strategy_creds:
def _strategy_creds(strategy_id, provider):
# 1. Trading account?
ta = TradingAccount.find(strategy_id)
if ta and ta.owner_user_id:
return load_credential(ta.owner_user_id, ta.venue_provider, ta.credential_label)
# 2. Portfolio strategy?
pm = PortfolioMeta.find(strategy_id)
if pm and pm.owner_user_id:
return load_credential(pm.owner_user_id, provider, "default")
# 3. No owner → no creds. Engine logs warning and skips.
return {}
No fallback. A strategy without an owner trades nothing. The engine logs
WARNING: no credentials for strategy X and skips the tick. Quietly. No
ordering, no positions, no surprises.
What this enables
Onboarding is now a four-step admin form: create user → assign strategy → user adds keys to their vault → admin credits cash. Each step is reversible.
Key rotation is per-user. Bob revokes his Binance key from his exchange; he goes to /account, deletes the cred, generates a new one, saves. Next strategy tick picks up the new keys. No restart, no admin involvement, no other strategy affected.
Incident response when an operator is compromised: revoke their token (MCP stops working), rotate their venue keys (deletes from vault), and their strategies enter degraded mode. The blast radius is one user.
Trading-balance isolation
The compute_user_balance(user_id) helper joins both DBs and returns
only that user’s tree. There’s no global “fund AUM” view by default —
the marketing site shows it (Total AUM = sum of public strategies)
because we ship a is_public=True flag for that specific use case.
Internal ops dashboards show your own slice.
Why no fallback
We considered a temporary fallback to settings["exchanges"] for
backward compat. We removed it before shipping. Reasons:
- A fallback creates an attractive nuisance — operators set “default” keys “just for paper testing”, forget they exist, and then a real strategy without an owner picks them up.
- The engine’s behavior should be boring. “Strategy without an owner does nothing” is boring. “Strategy without an owner falls back to global keys” requires you to know about the fallback to predict it.
- The migration was a few rows of SQL. Old strategies got
is_public=Truefor read-only public visibility, no owner, no keys, no trading. New admins assign owners explicitly.
What the marketing site sees
The public /api/public/strategies/{id} endpoint strips
owner_user_id, control internals, signals, and the kill-switch
reason before responding. What you see on inite.fund/strategies is
exactly that: equity + drawdown + sleeves + last 50 trades + last
365 equity points. No identifiers, no internal state, no secrets.
The Astro pages SSR-fetch this endpoint with a 30-second edge cache. If you reload twice in a row you’ll see the same numbers; thirty seconds later they update. That’s our “real-time” — close enough for a track record page, far enough from the origin to absorb traffic.
What it cost
The refactor touched 12 files in the product repo, 2 new tables, a one-shot startup migration, ~600 lines of code, and ~70 new tests. Tests cover the ownership resolution, the encrypted vault, capital flows with idempotency, and the role-gated MCP tool calls.
Before: 1004 tests passing. After: 1180. Zero regressions.
The marketing site (this site) is built on the same model. The hero
metric strip you see at the top? That’s compute_user_balance(NULL)
aggregated over public strategies, fetched server-side, rendered into
HTML. No private data crosses the boundary.
- 2026-05-11A ledger you can't quietly rewrite
Every capital flow, every approval, every mode change lands in an audit log with a hash chain. Why this matters in trading software, and how to verify integrity in under a minute.
- 2026-04-22The non-linear trading balance
Why your trading NAV is allowed to be greater than your allocation — and why we keep them in different DBs.
- 2026-05-09When the engine stops
The algorithm runs itself. But in three cases it asks the operator, and in one more it simply lies down. A walk-through of how the HIL queue, strategy modes, and the kill switch fit together — and where the line between them is.