Architecture
Olbos is a TypeScript monorepo (pnpm + Turborepo). This doc maps the components, explains the two ideas that everything hangs off of — event sourcing and the adapter/rail seams — and traces how a request flows through the system.
The two big ideas
Event sourcing
The engine's state is not a database you mutate. It is an append-only log of events, and everything else is a projection of that log:
- the audit trail is the log, verbatim;
- the engine's current view (which strategies exist, their custody, their kill state) is a fold over the log;
- a replay of the log reconstructs the engine from nothing;
- the dashboard's narration feed is the log rendered to English.
If it isn't an event, it didn't happen.
This is why audit isn't a feature bolted on — it's the storage model. The log
lives in node:sqlite (a single-writer, append-only workload SQLite is ideal
for). See Events for the full vocabulary.
Seams: adapters and rails
Two things in this system are guaranteed to change — the venues (protocols come and go, SDKs drift) and the payment facilitator (x402 is young). Both are isolated behind interfaces so the core never depends on a specific one:
VenueAdapter— every yield source implements list / APY / position-math / deposit / withdraw. The engine only ever sees this interface. See Venues.PaymentRail—DevRail(localnet) andX402Rail(real x402) implement the samecollect()method. See Payments.
Component map
packages/
shared/ event schemas · policy types · validation · alert()
rails/ PaymentRail seam: DevRail | X402Rail
sdk/ @olbos/sdk — the 5-line agent integration (auto-pays x402)
mcp/ @olbos/mcp — Olbos as MCP tools (wraps the SDK)
cli/ @olbos/cli — `olbos init` onboarding wizard
services/
engine/ the heart: event log, custody, venue adapters, scorer,
risk gate, executor, strategy runtime, the CLI/watch loop
apps/
api/ x402-metered HTTP seller; multi-tenant session auth; hardening
dashboard/ Next.js mission control; wallet sign-in; read-only + kill ring
programs/
mock-venue/ Anchor lending program for localnet (the delphi/olympia venues)The engine, in detail (services/engine/src/)
| Module | Responsibility |
|---|---|
eventlog.ts | the append-only node:sqlite log; append / read / latestOfType |
custody.ts | per-strategy Swig: create, activate, payout, revoke, sign-wrap |
venues/ | the VenueAdapter interface, VenueRegistry, and the mock/Kamino/Marginfi adapters |
scorer.ts | net = headline − cost − risk haircut; utilization & TVL screens |
risk.ts | the pure-function risk gate — caps, buffer, allowlist |
executor.ts | simulate → send → confirm, with deterministic intent IDs |
strategy.ts | StrategyRuntime: read → plan → gate → execute one cycle |
dataClient.ts | engine-as-buyer: pays for premium data over x402 (spend-capped) |
cli.ts | status / deploy / rebalance / plan / watch / kill / log |
Request flow: a metered write
Tracing POST /v1/strategies/:id/rebalance:
agent ──x402 payment──▶ apps/api
│ rail.collect() ── settle via facilitator
│ log.append(x402.payment.received)
▼
StrategyRuntime.rebalanceOnce()
│ registry.list() ── live venue snapshots
│ scoreVenues() ── risk-adjusted ranking
│ plan() ── greedy fill + gates
│ checkAction() ── pure risk gate
│ custody.wrap(ixs) ── Swig sign-wrap
▼
Executor.execute(intentId, ixs)
│ simulate ── reject on failure
│ send · confirm
│ log.append(tx.simulated / tx.sent / tx.confirmed)
▼
log.append(rebalance.executed)The same shape holds for deploy, fund, and withdraw — collect payment, mutate through the runtime, log every step.
Custody integration point
The StrategyRuntime doesn't know about Swig. It asks its injected
StrategyCustody two things: whose positions to read (the Swig wallet PDA) and
how to sign venue instructions (wrap them in the engine's scoped role). When
custody is absent (legacy/localnet), it falls back to the engine keypair. This
keeps the planner and executor custody-agnostic. See Custody.
Determinism & idempotency
Every executed action carries an intent ID: a SHA-256 over the canonical decision inputs (strategy, kind, from/to venue, amount, and the sampled venue indexes that pin the decision context). Same decision → same ID across restarts. Before broadcasting, the executor checks the log:
- already
tx.confirmedfor this intent? → return it, don't re-send. tx.sentbut not confirmed? → check the signature's on-chain status before rebuilding.
This gives at-most-once execution: a crash mid-cycle never double-spends.
Multi-tenancy
A single engine instance serves many owners. Isolation is enforced at two layers:
- Custody — each strategy has its own Swig wallet; funds are isolated by construction, not by ledger discipline.
- API — in hosted mode (
OLBOS_REQUIRE_AUTH=1), reads are scoped to the signed-in owner via a stateless wallet signature; the audit log is filtered by correlation ID so one tenant never sees another's payments or transactions. See Security — multi-tenancy.
Reliability posture
- Simulate-first execution — nothing failing simulation is broadcast.
- At-most-once intents — crash-safe, no double-spend.
- Fail-safe kill — N consecutive cycle failures, or a venue health-invariant violation, halts the engine and alerts.
- Single-writer event log + a watch lockfile — two live planners is a bug, not redundancy; the lock prevents it.
- Restart-safe — kill state lives in the log, so a restarted engine holds after a kill instead of resuming.
See Operations for how these are configured and what's still on the hardening list.
