Architecture
Two independent processes that only ever talk through a shared Supabase database, so the public dashboard stays up even when the trading host is off.
Last updated August 31, 2026
Beleth is deliberately not a monolith. It is two processes that share one
database and never call each other directly. The split is a reliability choice:
the public showcase stays reachable and shows the last known state even if the
trading machine is switched off — only the production of new decisions pauses.
The agent
A Python 3.11 process on a private home server (a ThinkPad T470), packaged as a
single Docker service with restart: unless-stopped. It is outbound-only —
no inbound ports, no public domain, nothing to reach it from the internet.
A resident loop (scripts/run_agent.py) runs one cycle per symbol by launching
scripts/check_market_data.py as a fresh subprocess each time, so a crash or
hang dies with the subprocess and the loop survives. Market hours come from the
Alpaca clock: full cycles run every ~5 minutes only while the market is open;
outside hours the loop just writes an agent_status heartbeat every ~15 minutes
so the dashboard can tell "alive, market closed" from "agent down". It reads the
agent_status.paused kill-switch flag every iteration and fails closed. A
512 MiB memory cap turns any leak into a clean restart rather than a starved
host.
Stack: alpaca-py for paper trading and market data, the OpenAI SDK pointed at
OpenRouter for the LLM decision layer (a free tool-calling model, swappable by
config), plus httpx, pydantic-settings, pyyaml. If the model call fails,
the cycle degrades to the deterministic no-trade — never to a trade.
The webapp
A Next.js 16 App Router app (React 19, Tailwind v4) on Vercel — public homepage and authenticated dashboard in one deploy. It reads Supabase with the anon key under row-level security for anonymous, public-user and demo-admin views, and uses the service role only for master-admin server actions. It also reads the Alpaca paper account directly, server-side, for the live equity curve and positions.
The shared database
Supabase Postgres is the single source of truth. The agent writes every
decision, risk-check outcome, trade, heartbeat and event with service-role
credentials; the webapp reads. Supabase Auth carries the four access states. The
kill switch is the one control that flows the other way: the webapp calls an
RPC that sets agent_status.paused, and the runner obeys it on its next loop.
Data flow

When something goes down
- Agent host offline — the dashboard still serves the last persisted state plus live Alpaca reads; only new decisions stop. The heartbeat age tells a reader which it is.
- Webapp / Vercel offline — the agent keeps trading and persisting; the dashboard catches up when it returns.
- Supabase unreachable from the agent — the cycle cannot persist, so no order is sent: an order never goes out unlogged.
- OpenRouter down — deterministic no-trade.
- FRED down — the cycle trades without the VIX size taper and says so (see Data sources and fallbacks).