ooda/ is the local observe-orient-decide-act harness inside the @onchainai/automation monorepo (@clawd/ooda-harness). It powers paper pulse checks, journals, and optional TUI output.
Paper-trading and devnet only — no real funds, no mainnet connections, no key handling.
Bridged into the Automaton runtime via src/ooda/bridge.ts (getOodaHealth, ooda_health tool).
# From monorepo root or this directory
cd ooda
npm install
npm run lint # tsc --noEmit
npm test # real unit tests (state/validate/observe/decision/TUI)
npm run loop -- --ticks 50 --sleep 0.25 # deterministic, no TUI
npm run loop -- --ticks 200 --sleep 0.4 --tui | npm run tui
npm run loop -- --goblin --ticks 100 --llm # aggressive paper/devnet mode
# Isolated journal path for CI / concurrent runs (do not clobber journal/ticks.jsonl)
OODA_JOURNAL_PATH=./journal/ci-ticks.jsonl npm run loop -- --ticks 5 --sleep 0 --seed 42
# From monorepo root
npx tsx ooda/loop.ts --ticks 100 --sleep 0.25
npx tsx ooda/loop.ts --ticks 200 --sleep 0.4 --tui | npx tsx ooda/tui.ts
npx tsx ooda/loop.ts --goblin --ticks 100 --llmA zero-dependency browser dashboard lives at ooda/web/:
npm run web # starts on http://127.0.0.1:4173
OODA_WEB_PORT=4321 npx tsx web/server.tsPages
/— paper dashboard: live price chart, stat cards, positions table, tick feed, run form (ticks/sleep/seed, goblin + LLM toggles), SSE-driven. Tailsjournal/ticks.jsonlfor ANY run (including ones started from the CLI in another terminal)./trade.html— live (mainnet) trading page: connect a Phantom wallet, get DFlow quotes/unsigned txs via/api/live/*, sign & send in the wallet. The server never holds a private key — only proxies quotes/confirmation (Helius fallback RPC).
Endpoints: GET /api/status, GET /api/config, GET /api/journal?n=, GET /api/stream (SSE), POST /api/run, POST /api/stop, GET /api/live/status, GET /api/live/order, GET /api/live/balance, GET /api/live/confirm.
24/7 deployment (Fly.io) — runs the paper loop forever with a persistent journal:
cd ooda
fly launch --copy-config --no-deploy --name clawd-ooda --region iad
fly volumes create ooda_journal --size 1 --region iad
fly secrets set SOLANA_RPC_URL=https://api.devnet.solana.com
fly secrets set OPENAI_API_KEY=... # optional — enables --llm decisions
fly deployConfig lives in fly.toml + Dockerfile: binds 0.0.0.0, honors Fly-injected PORT, mounts the journal volume at /data/journal/ticks.jsonl, and sets OODA_AUTORUN=1 so loop.ts reruns forever (default 500 ticks @ 5s = ~42 min/run, then auto-restarts). Watch it live at https://clawd-ooda.fly.dev/.
| Fly env var | Meaning |
|---|---|
OODA_AUTORUN=1 |
continuously rerun the paper loop |
OODA_TICKS / OODA_SLEEP |
ticks per run / seconds between ticks |
OODA_SEED |
deterministic seed |
OODA_LLM / OODA_GOBLIN |
enable LLM / goblin mode on the deployed loop |
OODA_JOURNAL_PATH |
journal path (defaults to the /data volume) |
The deployed loop is paper + devnet only — observe.ts rejects mainnet RPC URLs at startup and that guard is never bypassed. The live-trading page remains browser-wallet-signed and is unchanged by deployment.
ooda/
├── loop.ts ← main harness (CLI entry point)
├── observe.ts ← market data adapters (synth + Helius/Pyth stub)
├── state.ts ← position book, PnL accounting, type definitions
├── validate.ts ← safety validator (enforces CLAWD.md rules)
├── clawd-decision.ts← AI decision function (multi-provider LLM chain)
├── journal.ts ← append-only tick journal writer/reader
├── tui.ts ← ANSI TUI dashboard (reads JSONL from loop.ts --tui)
├── CLAWD.md ← per-tick system prompt + config frontmatter
├── goblin.md ← GOBLIN MODE variant config
├── test/ ← real unit tests (npm test)
└── journal/
└── ticks.jsonl ← append-only operational state
The monorepo runtime (@onchainai/automation) bridges this package via
src/ooda/bridge.ts and exposes first-class tools to the agent loop:
| Tool | Purpose |
|---|---|
ooda_health |
Health + catalog of this package |
ooda_run |
Run N deterministic paper ticks in-process (no LLM) |
ooda_decide |
One-shot SMA decision from candle closes |
ooda_journal |
Read trailing journal/ticks.jsonl entries |
All tools are paper / devnet only — same safety contract as this harness. Integrated with Dark Clawd automaton lineage (creator CLI + crustacean installer at monorepo root).
The orchestrator. Runs the OODA cycle for N ticks:
- Observe — calls
SynthObserver.tick()(or Helius/Pyth when wired), optionally fetches a perps OI signal from../perps/clawd-agents-perps/ - Orient/Decide — calls
clawdDecision()(LLM) ordeterministicDecision()(SMA crossover) orsignalToDecision()(perps OI) - Validate — passes raw decision through
validate()before applying - Act —
openPosition/closePosition/ hold - Journal — appends every tick to
journal/ticks.jsonl
CLI flags:
| Flag | Default | Description |
|---|---|---|
--ticks N |
50 | Number of ticks to run |
--sleep N |
0.25 | Seconds between ticks |
--seed N |
42 | PRNG seed for synth candles |
--llm |
false | Use LLM for decisions |
--tui |
false | Emit JSONL for TUI renderer |
--goblin |
false | Enable GOBLIN MODE |
--perps-oi |
false | Fetch live OI signal from perps module |
--perps-symbol |
SOL-PERP | Symbol for perps OI fetch |
--perps-oi-mock |
false | Use mock data for OI signal |
--commit-every N |
0 | Git-commit journal every N ticks |
Kill-switch: exits with code 1 after loss_killswitch_consecutive consecutive losing trades. Configurable in CLAWD.md frontmatter.
SynthObserver— seeded deterministic candle generator usingmulberry32PRNG. Produces OHLCV candles with a slight upward drift. Used by default.observeFromHelius()— stub for a real Pyth/Helius RPC adapter. Falls back to synth until wired.rejectMainnet(rpcUrl)— hard guard; throws on any mainnet RPC URL (bypassed only withMAINNET_OK=1).isStale(candles)— staleness check; returnstrueif the last candle is older thanmaxAgeSeconds.
To plug in real data, replace SynthObserver usage in loop.ts with a call to observeFromHelius() once the Pyth account decode is implemented.
In-memory state for one loop run. Reconstructed from journal/ticks.jsonl on restart.
Types: Side, Position, Book, Candle, State
Functions:
createState(startingCash)— initialize with 10 SOL-equivalent cashopenPosition(state, side, size_lamports, currentPrice)— deducts cash, appends to bookclosePosition(state, positionId, currentPrice)— computes PnL (long: profit on price rise; short: profit on price fall), updatesconsecutive_losses/total_pnl_lamportsunrealisedPnl(state, currentPrice)— sum of unrealised PnL across open positions
Called on every raw LLM or deterministic output before any state mutation. Invalid decisions are logged as "rejected" and the tick proceeds as a hold.
Enforces:
actionmust behold | open | closereasonrequired, max 140 chars- Prompt-injection guard: rejects reasons containing
private_key,seed phrase,mnemonic, etc. open.sidemust belong | shortopen.size_lamportsmust be a positive integer ≤max_position_size_lamports- v0: one position at a time (rejects
openwhen a position is already open) close.position_idmust exist in the book
parseClawdConfig(markdownContent) — extracts the YAML frontmatter from CLAWD.md / goblin.md and validates that mode=paper and network=devnet.
Assembles the per-tick prompt from CLAWD.md + live observations and calls an LLM. Returns one parsed JSON decision.
Provider priority (uses first key found):
XAI_API_KEY→grok-4.3-fast(orXAI_MODEL)DEEPSEEK_API_KEY→deepseek-v4-flash(viaDEEPSEEK_BASE_URL)ZKROUTER_API_KEY(orOPENROUTER_API_KEY) →nex-agi/nex-n2-pro:freeviaZKROUTER_BASE_URLANTHROPIC_API_KEY→claude-haiku-4-5-20251001(orANTHROPIC_MODEL)- Fallback →
deterministicDecision()(no key needed)
deterministicDecision(obs) — 5-candle SMA crossover: opens long when price < SMA × 0.995, opens short when price > SMA × 1.005, closes on reversal. No API key required.
The prompt is assembled fresh each tick — stateless, no conversation history.
The ooda/ directory now carries its own package.json so the harness is reproducible as an open-source subproject. It declares the actual runtime dependencies used here:
openaifor the OpenAI-compatible router slotexecafor optional journal commitschalkfor the ANSI TUItsxandtypescriptfor local execution and linting
Append-only JSONL log at journal/ticks.jsonl. Every tick (including rejected and killswitch ticks) is written as one JSON line.
TickEntry fields: tick, now, candles_last3, book_snapshot, decision, outcome (applied | rejected | killswitch), violation?, pnl_lamports?, total_pnl_lamports?, consecutive_losses?, event?
Functions:
appendTick(entry)— createsjournal/dir if needed, appends one JSON linereadLastEntries(n)— returns last N entries (injected into the next tick's observations)clearJournal()— marks empty for a fresh run (non-destructive)journalPath()— returns the absolute path for display
The journal is the harness's memory. On restart, replay it to reconstruct state.
Review
ooda/journal/before committing if you run long live sessions.
Reads JSONL from loop.ts --tui on stdin and renders a live dark-themed dashboard with chalk.
Features:
- Full-width box-drawing border (magenta)
- Tick progress bar
- SOL price with unicode sparkline (
▁▂▃▄▅▆▇█) coloured green/red per move - Last decision + outcome
- PnL / cash / open positions / consecutive losses stats row
- Rolling 6-line action log with timestamps
- Kill-switch and done banners
Pipe usage:
npx tsx ooda/loop.ts --ticks 200 --sleep 0.4 --tui | npx tsx ooda/tui.tsConfig frontmatter + system prompt loaded by loop.ts each run (and by clawd-decision.ts each tick).
Frontmatter keys:
mode: paper # must be "paper"
network: devnet # must be "devnet"
max_action_per_tick: 1
max_position_size_lamports: 1000000
loss_killswitch_consecutive: 3The body is the LLM's instruction set: what decisions it can return, the hard rules it must follow, and the strategy guidelines (SMA, mean reversion, OI delta, quick loss cuts).
mode: paper
network: devnet
max_position_size_lamports: 5000000 # 5× normal
loss_killswitch_consecutive: 5
goblin: true
dark_defi_armed: true
tick_sleep_ms: 0
model: grok-4.3-fastActivated with --goblin. Loads goblin.md instead of CLAWD.md, forces --llm, sets sleep to 0ms, and defaults to 100 ticks. Same safety contract (paper + devnet), but maximally aggressive strategy:
- Aggressive mean reversion on 3-tick windows
- Momentum continuation on 2+ same-direction ticks
- Take profit at +1%, cut loss at -0.5%
- Follows OI expansion with price, fades OI expansion against price
All enforced in code — not just prompt guidance:
mode: paperandnetwork: devnetare validated at startup; any other value throws- Mainnet RPC URLs are rejected before any network call
- No private key handling exists anywhere in this module
- Position size is hard-capped per tick
- One position at a time (v0)
- Kill-switch halts the process on consecutive losses
- Every decision (including rejected ones) is journalled
| Variable | Used by | Description |
|---|---|---|
OODA_JOURNAL_PATH |
journal | Override JSONL path (tests/CI isolation) |
XAI_API_KEY |
clawd-decision | Grok API key (priority 1) |
XAI_MODEL |
clawd-decision | Override Grok model |
DEEPSEEK_API_KEY |
clawd-decision | DeepSeek key (priority 2) |
DEEPSEEK_BASE_URL |
clawd-decision | DeepSeek base URL |
ZKROUTER_API_KEY |
clawd-decision | Preferred Clawd router key on the public zk.x402.wtf stack (priority 3) |
ZKROUTER_BASE_URL |
clawd-decision | Override the default router base (https://clawdrouter-zk.fly.dev/v1) |
OPENROUTER_API_KEY |
clawd-decision | Compatibility fallback for the same OpenAI-format router slot |
OPENROUTER_MODEL |
clawd-decision | Override the router model |
ANTHROPIC_API_KEY |
clawd-decision | Claude key (priority 4) |
ANTHROPIC_MODEL |
clawd-decision | Override Claude model |
SOLANA_RPC_URL |
loop | RPC URL (mainnet URLs rejected) |
MAINNET_OK |
observe | Set to 1 to bypass mainnet guard (still no signing path) |
OODA_WEB_PORT / OODA_WEB_HOST |
web | Dashboard bind (Fly injects PORT, default host 0.0.0.0) |
OODA_AUTORUN / OODA_TICKS / OODA_SLEEP / OODA_SEED / OODA_LLM / OODA_GOBLIN |
web | 24/7 autorun daemon config (Fly) |
DFLOW_API_KEY |
web/live | DFlow prod host + API key for live quotes |
HELIUS_API_KEY / HELIUS_RPC_URL |
web/live | Helius balance/confirmation (falls back to public RPC) |
| Surface | Role |
|---|---|
pnpm-workspace.yaml |
Lists "ooda" workspace package |
Root package.json files |
Ships ooda with the Automaton pack |
src/ooda/bridge.ts |
getOodaHealth() / catalog for boot + tools |
| Tool | ooda_health on the Automaton primary tools surface |
# From monorepo root (after npm run build)
node -e "import('./dist/ooda/bridge.js').then(m=>console.log(m.getOodaHealth()))"