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

![image](https://zmscvxdouuytwoutqtfa.supabase.co/storage/v1/object/public/docs-media/8239d822-100a-40db-a93b-c0f4d39a076c.jpg)
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](https://zmscvxdouuytwoutqtfa.supabase.co/storage/v1/object/public/docs-media/43e6864b-3d63-4e5b-a597-2ed97cc9aa2c.png)

## 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](/docs/data-sources-and-fallbacks)).
