Skip to content

Repository files navigation

OODA

OODA animated header

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).

Quickstart

# 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 --llm

Web dashboard + 24/7 Fly deploy

A 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.ts

Pages

  • / — paper dashboard: live price chart, stat cards, positions table, tick feed, run form (ticks/sleep/seed, goblin + LLM toggles), SSE-driven. Tails journal/ticks.jsonl for 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 deploy

Config 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 onlyobserve.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.

Architecture

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

Automaton agent tools

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).

File Reference

loop.ts — Main Harness

The orchestrator. Runs the OODA cycle for N ticks:

  1. Observe — calls SynthObserver.tick() (or Helius/Pyth when wired), optionally fetches a perps OI signal from ../perps/clawd-agents-perps/
  2. Orient/Decide — calls clawdDecision() (LLM) or deterministicDecision() (SMA crossover) or signalToDecision() (perps OI)
  3. Validate — passes raw decision through validate() before applying
  4. ActopenPosition / closePosition / hold
  5. 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.


observe.ts — Market Data

  • SynthObserver — seeded deterministic candle generator using mulberry32 PRNG. 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 with MAINNET_OK=1).
  • isStale(candles) — staleness check; returns true if the last candle is older than maxAgeSeconds.

To plug in real data, replace SynthObserver usage in loop.ts with a call to observeFromHelius() once the Pyth account decode is implemented.


state.ts — Position Book

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 cash
  • openPosition(state, side, size_lamports, currentPrice) — deducts cash, appends to book
  • closePosition(state, positionId, currentPrice) — computes PnL (long: profit on price rise; short: profit on price fall), updates consecutive_losses / total_pnl_lamports
  • unrealisedPnl(state, currentPrice) — sum of unrealised PnL across open positions

validate.ts — Decision Validator

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:

  • action must be hold | open | close
  • reason required, max 140 chars
  • Prompt-injection guard: rejects reasons containing private_key, seed phrase, mnemonic, etc.
  • open.side must be long | short
  • open.size_lamports must be a positive integer ≤ max_position_size_lamports
  • v0: one position at a time (rejects open when a position is already open)
  • close.position_id must exist in the book

parseClawdConfig(markdownContent) — extracts the YAML frontmatter from CLAWD.md / goblin.md and validates that mode=paper and network=devnet.


clawd-decision.ts — AI Decision

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):

  1. XAI_API_KEYgrok-4.3-fast (or XAI_MODEL)
  2. DEEPSEEK_API_KEYdeepseek-v4-flash (via DEEPSEEK_BASE_URL)
  3. ZKROUTER_API_KEY (or OPENROUTER_API_KEY) → nex-agi/nex-n2-pro:free via ZKROUTER_BASE_URL
  4. ANTHROPIC_API_KEYclaude-haiku-4-5-20251001 (or ANTHROPIC_MODEL)
  5. FallbackdeterministicDecision() (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.

package.json — local harness metadata

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:

  • openai for the OpenAI-compatible router slot
  • execa for optional journal commits
  • chalk for the ANSI TUI
  • tsx and typescript for local execution and linting

journal.ts — Tick Journal

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) — creates journal/ dir if needed, appends one JSON line
  • readLastEntries(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.


tui.ts — ANSI Dashboard

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.ts

CLAWD.md — Per-Tick Prompt

Config 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: 3

The 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).


goblin.md — GOBLIN MODE

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-fast

Activated 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

Safety Contract

All enforced in code — not just prompt guidance:

  • mode: paper and network: devnet are 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

Environment Variables

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)

Monorepo integration

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()))"

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages