Concepts
Trust model
The rulebook in code, the hash-chained ledger, the kill switch, and Binance's own controls underneath all three.
Olai's answer to "why would you let a model near your money" is four layers, three of which are in this repository and one of which is Binance's.
1. The rulebook is code, not a prompt
The rulebook is a plain object the owner writes. evaluate(rulebook, action, state) in
packages/agent/src/policy/engine.ts checks one proposed action against it and returns a
verdict. Three things make it load bearing:
It is a pure function. No clock read, no database read, no network. The same rulebook, action and state give the same verdict every time, which is what lets anyone replay a past decision from the ledger and get the same answer.
It runs before anything moves, in three places. Inside the buy_data tool before
deps.data.buy is ever called, in SessionRunner.ask the moment a proposal comes back, and
again in SessionRunner.approve against a fresh account snapshot. The second order check is not
redundant: the day's loss, the open positions and the kill switch can all change while a proposal
sits waiting for the owner.
It never throws. Nonsense input (NaN, Infinity, negative money, an unreadable timestamp) comes back as a refusal. An agent that crashes on bad input is an agent whose rules can be skipped by sending bad input.
The model is told about the rulebook in its system text, and it will sometimes try to act outside
it anyway. That is assumed. The defence is that its output is checked in code and never trusted
as a decision. Every refusal carries a stable rule id, for example order.max_size,
payment.daily_budget, risk.drawdown_halt, agent.killed. Those ids are part of the product
and do not change once shipped.
2. The ledger cannot be quietly edited
One SQLite file, one table, append only, hash chained
(packages/agent/src/ledger/ledger.ts and hash.ts).
- Every line's hash is sha256 over the previous line's hash plus the canonical JSON of this line. Edit any old line and every later hash stops matching.
verifyChain()walks the whole table, recomputes every hash, and returns the sequence number of the first line that does not hold up, with a plain-English reason.- Two SQL triggers,
ledger_no_updateandledger_no_delete, make UPDATE and DELETE raise an abort at the database level. Tampering has to go around SQLite entirely and edit the file bytes, which is exactly whatverifyChainis built to catch. - The timestamp is set by the ledger, never by the caller, so nobody can backdate a line.
- Money is stored as whole cents, because a day of one-cent payments added as floating point drifts, and a spend report that is a cent out is a report nobody trusts.
GET /api/ledger/verify exposes that check over the API, so the owner can ask at any moment.
3. The kill switch
SessionRunner.kill() sets a private flag. Two details make it hold:
SessionRunner.state()always ORs the local killed flag over whatever the account snapshot says, so a stale or wrong account read can never un-kill the agent.evaluate()refuses everything, orders and payments alike, the instantstate.killedis true, under the rule idagent.killed.
What it does not do: it does not cancel an order already sent to Binance. Kill stops the next thing, not the thing in flight. See Kill switch.
4. Binance's own controls, underneath all of it
These are outside Olai's code and Olai relies on them.
- No withdrawal permission exists on the trading key. The Binance API key Olai trades with
is scoped to Enable Spot Trading only, no withdrawals. Money cannot leave Binance through the
door Olai uses. The secret itself is never logged or sent as anything but an HMAC signature
(
packages/agent/src/exchange/sign.ts). - The Agentic sub-account is walled off from the owner's main account. The agent cannot pull funds into it.
- The Agentic Wallet has daily limits set only in the Binance app, including a separate
x402DailyLimitfor exactly the kind of spending Olai does.baw wallet settingscan read them and nothing can write them. - Wallet sends only go to addresses already in the owner's address book.
- The wallet sign-in is time-boxed at 48 hours, so an unattended machine stops being able to pay after two days.
- Binance's own skills document a confirm-before-execute pattern. Olai is not running inside
Claude Code, so it gets no external confirm prompt. The owner-approval gate in
SessionRunner.approveis the equivalent Olai implements itself.
The one door in
Every route under /api/* needs the owner's bearer token, checked in
packages/agent/src/api/auth.ts. Three details:
- The token lives in its own namespace: it starts with
ol.and is at least 24 characters, while session ids look likeol-<uuid>. Knowing an id can never make you the owner. - Both sides are sha256-hashed before
timingSafeEqual, so tokens of different lengths still compare in constant time and the real token's length is not leaked by timing. - The token is checked at boot too, so Olai refuses to start with a weak lock on the door.
On top of that: 60 requests a minute per address, counted on the socket address unless the deployment explicitly says it sits behind a proxy; a 64 KB body limit; one named browser origin in CORS; and every external input parsed with a schema before use.
What we did not fix
Being honest about the gaps is part of the model. The full list is in the threat model. The four that matter most to a reader here:
- Daily loss counts unrealised moves. It is computed from equity snapshots, so a held
position that merely drops in price today trips
maxDailyLossUsdthe same as a losing trade. - Some guards are in memory only. The pending session list and the one-signature-per-payment
guard do not survive a process restart. The ledger is still the durable record, and a durable
signature guard backed by the ledger exists in
ports/data.ts, but the in-process sets are gone after a restart. - Merchant text is trusted as data after schema validation. Nothing strips instruction-like text from a merchant's description before the model reads it. The defence is entirely downstream, in the rulebook refusing whatever the model proposes as a result.
- The model writes its own
dataUsedlist. Nothing cross-checks it against what the ledger recorded frombuy_datain the same session. Read the ledger, not the proposal, for what was actually bought. - A compromised trading key could still trade. The Binance API key has no withdrawal permission, but whoever holds it could buy or sell on the sub-account up to its balance and pay trading fees on every order. It could never move money off Binance.