BelethBELETH
Overview
Markdown

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

image 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

image

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