Developers
Owner API
Every route in src/api/app.ts with its method, auth, body, response and errors.
Every route lives in packages/agent/src/api/app.ts. There is one user, the owner, and one
token. The owner's desk (packages/web/src/app/app/page.tsx, components under
packages/web/src/components/conversation) is the one browser client of this API; every example
below also works from a terminal, which is how they are written.
What applies to every route
- Auth. Every route under
/api/*needsAuthorization: Bearer <owner token>, checked byownerAuthinpackages/agent/src/api/auth.ts./healthneeds no token. The desk keeps this token in the browser'ssessionStorage, under the keyolai.owner-token(packages/web/src/lib/token.ts), reads it straight into this header on every call, and never places it in a URL, a cookie, a React key, or a log; a 401 clears it and drops back to the gate screen, "One token. One owner." The OAuth routes are a separate group, described at the bottom of this page. - Rate limit. 60 requests per 60 seconds per address, counted on the socket address unless
OLAI_TRUST_PROXYistrue, in which case the first entry ofX-Forwarded-Foris used. Over the limit is a 429. - Body limit. 64 KB on every route. Over it is a 413.
- CORS. One origin,
OLAI_WEB_ORIGIN, allowed for GET, POST, PUT and OPTIONS with theAuthorizationandContent-Typeheaders. This is the one origin the desk itself runs from; nothing else in a browser can call this API. The check runs before the token is looked at, because a browser has to be told no on the preflight. - Errors. The caller learns what they did wrong and nothing else. An unexpected error is a flat 500 with no detail, because a stack trace or a file path in an HTTP body is a map of the machine.
Routes
GET /health
No token. Returns { ok: true, dryRun, killed, version }. This is what a monitor watches.
GET /api/rulebook
Returns the stored rulebook, or the starter default when nothing has been saved yet. See Write a rulebook.
PUT /api/rulebook
Body: a rulebook object. Returns the saved, validated rulebook. Writes a rulebook.set ledger
line.
A rulebook that fails the schema is a 400 carrying issues, the exact list of fields and reasons
from the validator. A file that cannot be written is a 500.
POST /api/ask
Body: { "question": string }, 1 to 2000 characters after trimming. Returns a session record:
id, question, createdAt, status, and, once the analyst finishes, proposal and
verdict.
This is the only route that spends Anthropic tokens and the only one that can trigger a data
payment. Ledger lines it can produce: question, discovery, payment.preview,
payment.signed, payment.settled, data.received, note, proposal, rule.refused.
An empty question is a 400.
GET /api/sessions
Returns every session record held in memory, oldest first. Records do not survive a restart. The ledger does.
GET /api/sessions/:id
One session record. 404 when there is no session with that id. An id longer than 128 characters is a 400.
POST /api/sessions/:id/approve
No body. Returns the updated session record. See Approve or reject for the full order of checks.
Ledger lines: approval, then order.sent, then order.filled or order.failed. Or
rule.refused if the second rulebook check fails.
409 when the session is not pending. 404 when the id is unknown.
POST /api/sessions/:id/reject
Body: { "reason"?: string }, up to 500 characters, defaulting to "The owner gave no reason."
Returns the updated session record and writes a rejection line. 409 when the session is not
pending.
POST /api/kill
No body. Returns { "killed": true } and writes a kill line.
POST /api/resume
No body. Returns { "killed": false } and writes a resume line.
GET /api/ledger
Query: afterSeq (a whole number, 0 or more), limit (1 to 500, default 100), kinds (a
comma-separated list of line kinds), sessionId.
Returns { entries, nextAfterSeq }. nextAfterSeq is the sequence number to send as afterSeq
for the next page, or null at the end. An unknown kind is a 400 naming it.
GET /api/ledger/verify
Returns { ok: true, length } or { ok: false, brokenAtSeq, reason }. Recomputes every hash in
the table.
GET /api/account
Returns the account state the rulebook is checked against: equity, today's loss, today's data spend, open positions, the time of the last order, and the killed flag.
The first call of a new UTC day writes one note line recording the day's opening equity, which
is the number today's loss is measured from. A failed exchange read is a 502.
GET /api/wallet
Returns { status, settings, balances } from the baw CLI. When no wallet is configured it
returns { status: 'unavailable', settings: null, balances: [] }; when the wallet is not signed
in it returns the status with nulls rather than an error, because "not connected" is an answer,
not a failure. A CLI failure is a 502.
GET /api/bazaar/search
Query: query (1 to 200 characters) and optional maxUsdPrice (positive, at most 1000). Returns
{ resources }. 503 when the agent has no Bazaar connection configured, 502 when the Bazaar call
fails.
GET /api/events
A server-sent event stream. The first thing written is a ready event, so a client knows its
subscription is live before it does anything that would produce an event. A heartbeat comment
follows every 15 seconds to stop a proxy closing an idle stream. At most 16 streams may be open
at once; the seventeenth is a 503.
This is the live commentary feed, not the durable record. The desk reads it with fetch and a
stream reader rather than EventSource, because EventSource cannot carry the Authorization
header the token needs (packages/web/src/lib/sse.ts).
Status codes
| Code | When |
|---|---|
| 400 | The body or query failed its schema. issues carries the detail. |
| 401 | Missing or wrong bearer token. Every refusal is logged with the address and the path. |
| 404 | No such session, or no such route. |
| 409 | The session is not in a state where that action makes sense. |
| 413 | The body is over 64 KB. |
| 429 | Over 60 requests in the window from this address. |
| 502 | An upstream failed: the exchange, the wallet CLI, or the Bazaar. |
| 503 | A dependency is not configured, or too many event streams are open. |
| 500 | Something unexpected. The detail is in the agent log, not in the response. |
The OAuth routes
packages/agent/src/mcp/oauth.ts exports a separate router, mounted at the root by
packages/agent/src/boot.ts. These routes are outside the /api/* prefix that the owner token
guards.
| Route | Method | What it does |
|---|---|---|
/oauth/client-metadata.json |
GET | Olai's client description. Public on purpose: Binance fetches it, and its URL is Olai's client id. |
/oauth/start |
GET | Owner token required. Redirects to Binance's consent screen with PKCE and a fresh state. |
/oauth/callback |
GET | Binance returns here. Checks state, exchanges the code, writes the token file. Answers { connected, scope, expiresAt } or a 400 with the failure code. |
/oauth/status |
GET | Owner token required. { connected, clientId, scope, expiresAt }. |
/oauth/disconnect |
POST | Owner token required. Deletes the stored token. |
/oauth/client-metadata.json and /oauth/callback are public on purpose: Binance fetches the
first and redirects to the second, and the callback is protected by the PKCE state check. The
three routes that start, inspect or drop the Binance session take the owner token like every
/api route. See the threat model.