Guides
Write a rulebook
Every field of the rulebook, what it means, what it refuses, and the value a fresh install starts with.
The rulebook is the contract between the owner and the agent. It is written once, stored in the
ledger every time it changes, and checked in code before anything moves. The shape is defined by
rulebookSchema in packages/agent/src/policy/rulebook.ts, and the starter values below come
from defaultRulebook in packages/agent/src/rulebook/store.ts.
The starter rulebook
This is what a fresh install runs under: small sizes, three liquid markets, and approval required on every order, so the first thing you ever see is Olai asking permission rather than Olai trading.
{
"version": 1,
"name": "Olai starter rulebook",
"maxOrderUsd": 20,
"maxDailyLossUsd": 10,
"maxPositionUsdPerSymbol": 50,
"allowedSymbols": ["BNBUSDT", "BTCUSDT", "ETHUSDT"],
"allowShort": false,
"allowLeverage": false,
"maxDataSpendUsdPerDay": 1,
"maxDataSpendUsdPerCall": 0.05,
"cooldownSecondsBetweenOrders": 60,
"requireApprovalAboveUsd": 0,
"oneSidePerMarket": true,
"drawdownTiers": [
{ "lossUsd": 5, "action": "halve" },
{ "lossUsd": 10, "action": "halt" }
]
}Every field
version
Always the number 1. It is a literal in the schema, not a free number. It exists so a future
rulebook shape can be told apart from this one.
name
Text, 1 to 80 characters, trimmed. It appears in the ledger line every save writes
(Rulebook "<name>" was set by the owner), so give it something you will recognise months later.
Default: Olai starter rulebook.
maxOrderUsd
The biggest single order, in US dollars. An order above it is refused with rule id
order.max_size before the owner ever sees it.
This is a ceiling, not the number actually used. Drawdown tiers can halve it during the day, and
the verdict reports the number in force as effectiveMaxOrderUsd.
Default: 20.
maxDailyLossUsd
The most the account may be down today before trading stops for the day. Rule id
risk.daily_loss_reached, and it sets the effective max order to zero.
Read the honest limit here: today's loss is measured from an equity snapshot taken at the first
account read of the UTC day (packages/agent/src/account/state.ts), so a held position that
merely drops in price counts the same as a losing trade.
Default: 10.
maxPositionUsdPerSymbol
The most that may be open in one market after the order lands. Olai nets long and short per
symbol, then checks the resulting size against this. Rule id order.max_position.
Default: 50.
allowedSymbols
The markets Olai may trade, as Binance symbols. Each has to match [A-Z0-9]{4,20} in upper
case, for example BNBUSDT. At most 50 of them, because a rulebook with more than 50 markets is
not a rulebook. Anything not on the list is refused with order.symbol_not_allowed, and the
refusal names the list.
Default: ["BNBUSDT", "BTCUSDT", "ETHUSDT"].
allowShort
true lets Olai sell a market it has nothing long in. false refuses that with
order.short_not_allowed. Selling into an existing long is always allowed.
Default: false.
allowLeverage
Always false. It is a literal in the schema, so it is not a setting you can turn on. Olai is a
spot agent: leverage is a capability the product does not have. Any proposal with leverage above
1 is refused with order.leverage_not_allowed.
Default: false.
maxDataSpendUsdPerDay
The daily budget for buying data over x402. Olai adds up what it has settled today and refuses
the payment that would cross the line, with payment.daily_budget.
This is Olai's own ceiling. The Agentic Wallet has its own separate x402DailyLimit set in the
Binance app, and the lower of the two wins in practice.
Default: 1.
maxDataSpendUsdPerCall
The most one data call may cost. Two things use it: the payment check refuses anything above it
with payment.max_per_call, and the Bazaar search clamps whatever price cap the model asked for
down to this number, so the model cannot raise its own budget by asking for a bigger one.
It has to be less than or equal to maxDataSpendUsdPerDay. The schema refuses to save a rulebook
where a single call could cost more than the whole day.
Default: 0.05.
cooldownSecondsBetweenOrders
Whole seconds between orders, 0 to 86400. Inside the window an order is refused with
order.cooldown and the reason says how many seconds are left. A last-order time stamped in the
future means a clock problem somewhere, and the safe reading of a clock problem is to wait.
Default: 60.
tradingHoursUtc
Optional. An object like { "start": 8, "end": 20 }, both whole hours 0 to 23, read in UTC.
Outside the window an order is refused with order.trading_hours. A window that wraps midnight
works: { "start": 22, "end": 4 } means 22:00 to 04:00. The two hours cannot be equal, because
that window has no meaning.
Leave it out to trade around the clock. It is absent from the starter rulebook.
requireApprovalAboveUsd
Orders above this size need the owner to approve. The starter value is 0, so every order does.
It is the one rule that does not refuse: it sets requiresApproval on the verdict, with rule id
order.needs_approval.
Approval is a real HTTP call with the owner's token, never something the model can produce.
Default: 0.
oneSidePerMarket
Always true, a literal in the schema. Olai will not hold both directions in one market and
will not flip a position through zero in a single order. Two refusals come from it, both with
order.one_side_per_market: buying while a short is open, and a sell bigger than the long that
would leave the market short. The second refusal tells you the largest sell that would be
allowed.
Default: true.
drawdownTiers
A list of at most ten { lossUsd, action } pairs, where action is halve or halt. As the
day's loss passes each tier, that tier's action applies:
halvecuts the biggest single order in half, rule idrisk.drawdown_halve.haltstops trading for the day, rule idrisk.drawdown_halt.
Tiers apply in the order you wrote them, so two halve tiers in a row cut size to a quarter.
That is deliberate: it is what asking for size to keep shrinking as the day gets worse looks
like.
Default: [{ "lossUsd": 5, "action": "halve" }, { "lossUsd": 10, "action": "halt" }].
Rules on the numbers themselves
Every dollar field is checked as money, not trusted as a number: it has to be a real number, not negative, at most two decimal places, and no larger than 1,000,000,000. Every rule inside the engine is then decided in whole cents, because comparing dollars as floating point gets the edge case wrong: 0.1 plus 0.2 is not 0.3 in binary, and a 0.30 daily budget would look breached by a payment that exactly fills it.
A rulebook that fails any of this is refused at save time with the exact field and reason. As a
second line of defence, evaluate re-checks the shape at runtime and refuses everything if the
rulebook does not look real, so a file edited by hand into nonsense stops the agent rather than
loosening it.
Saving one
From the desk
Open Details and stay on the first tab, Rulebook. Step 1 of the welcome card, Check the
rulebook, shows the rules in force in one line and has an Edit link that lands on the same
tab. The tab is a form matching every field above one for one: money fields carry a $ prefix,
"Markets it may trade" adds a chip per symbol, "May sell what it does not hold" and "Only trade
between two hours, UTC" are switches, and "Cut back after losing" lists the drawdown tiers with an
"Add a tier" button. A box marked "Not yours to change" explains that leverage is off and one side
per market is on, because those are not settings, they are things Olai cannot do. Save the
rulebook submits the form; a save the schema refuses shows the exact reason under the field it
belongs to, because the agent returns issues per field and the form indexes them by path
(packages/web/src/components/desk/rulebook-panel.tsx, mounted by
packages/web/src/components/conversation/drawer.tsx), and a save that succeeds says "Saved, and
recorded in the ledger." The save shows up on the Ledger tab as "Rulebook saved", with the
name of the rulebook.
From the command line
The same write as a plain HTTP call, useful for scripting a whole rulebook at once:
curl -X PUT http://localhost:4000/api/rulebook \
-H "Authorization: Bearer $OLAI_OWNER_TOKEN" \
-H "Content-Type: application/json" \
-d @rulebook.jsonTwo copies are kept on purpose. The JSON file at OLAI_RULEBOOK_PATH is what the agent reads
when it starts. The ledger holds every version that was ever in force, as a rulebook.set line.
The file can be edited by anyone with the machine; the ledger line cannot be changed without
breaking the hash chain, so a rulebook that was quietly loosened is always provable after the
fact.
The write itself is atomic: the file goes to a temporary name in the same directory and is renamed over the old one, so a crash halfway through leaves the previous rulebook intact rather than half a file the agent cannot read.