Case study

Exodus Tracker — composite crisis index from 22 live data sources

A real-time global stress index that fuses bizjet ADS-B, FX, equities, GDELT, AIS chokepoints, FRED, USGS, UNHCR, Polymarket, Wikipedia, Google Trends and more into a single 0–100 score, with a causal-leverage layer on top.

Exodus Tracker — composite crisis index from 22 live data sources

Exodus Tracker is a Next.js 16 + React 19 application that synthesises 22 independent crisis indicators into a single composite called the Exodus Tracker Index (ETI) — a 0–100 stress score, refreshed every five minutes, with full transparency about how it was computed. On top of the composite sits a causal-leverage layer that turns “how stressed is the system?” into “which indicator is worth pulling on if you want to actually move it?”

ETI gauge with 22 weighted component contributions, each independently scored 0–100, then collapsed into the composite with calibrated weights and correlation dampening.

What the product does

  • Pulls 22 live data streams every 5 minutes — aviation (OpenSky), FX (Frankfurter + open.er-api), equities + VIX + MOVE (CBOE + Stooq + Yahoo), prediction markets (Polymarket), news tone + civil unrest (GDELT GKG + GDELT Events 2.0), internet outages (Cloudflare Radar), shipping chokepoints (AISStream WebSocket), Treasury + credit + Fed-balance-sheet stress (FRED), sovereign CDS proxy, crypto capital flight (Yadio USDT P2P + DefiLlama supply), commodities (Brent / WTI / NG / gold / silver), travel advisories (US State Dept), seismic events (USGS), refugee outflows (UNHCR), public attention (Wikipedia pageviews + Google Trends).
  • Scores each indicator independently on a [0, 1] scale via hand-calibrated piecewise-linear curves whose breakpoints map to historically meaningful regimes.
  • Composes the 22 scores into one ETI value with weighted summation, correlation dampening (three clusters of related signals are de-double-counted), and capped renormalisation so missing data sources degrade gracefully rather than silently inflating remaining components.
  • Maps the composite to a 5-level alert ladder (Calm / Elevated / Pronounced / Severe / Extreme).
  • Surfaces the historical analog — past snapshot whose component vector is most similar by cosine similarity — so the user reasons from a concrete past pattern instead of an abstract score.
  • Runs a daily Claude-generated narrative briefing grounded in current events via web search.

What makes the math interesting

The composite collapses 22 signals in different units, on different cadences, with different baselines, into one number that stays interpretable. Three design choices carry most of the weight:

  1. Per-signal piecewise-linear curves instead of z-scores. A VIX of 30 is mapped to 0.7 because that’s historically where the index has signalled “stressed regime” — not because it’s “1.6σ above last month’s mean.” Calibration is in the open, legible to a reviewer, stable across volatility regimes.
  2. Correlation dampening. VIX, FX volatility, and MOVE are reading the same underlying risk-off event, so the index groups them into a market-panic cluster and only counts the strongest member at full weight (others at 50% / 25%). Same for the rates-credit cluster and the OpenSky aviation cluster.
  3. Capped renormalisation when sources go offline. Naive renormalisation boosts the remaining components by however much is needed to keep the score on a 0–100 scale, which is methodologically wrong under correlated outages. ETI caps the renorm factor at 1.3 — beyond that, the score degrades gracefully and the dashboard surfaces a “coverage capped” badge.

The full methodology is documented in the companion blog post.

The AEC leverage layer

22-node causal graph with 68 hand-encoded signed edges. The AEC math finds the dominant eigenvector of (I − M)ᵀ(I − M), producing per-node Effectiveness and Controllability indices.

A composite score tells you how stressed the system is. It doesn’t tell you which indicator is worth pulling on. The AEC layer answers that question with a port of the Algorithm for Effective Controls — originally Fortran routines from the Cognition system, reimplemented in TypeScript.

Given a 22 × 22 signed causal-influence matrix M, the math finds the dominant eigenvector of B = (I − dM)ᵀ(I − dM) and produces two indices per node:

  • X1 (Effectiveness): how strongly this node responds to system shocks (high = sensitive thermometer, watch it)
  • Y1 (Controllability): how much leverage this node has over the rest of the system (high = effective steering wheel, push it)

Eight named scenarios compile to different constraint sets — “what drives ETI right now,” “where would recovery first show up,” “financial contagion path,” “civil-unrest surge,” “energy escalation,” and so on — each producing a different X1/Y1 ranking against the same matrix.

The seed matrix has 68 hand-encoded edges authored from textbook macro priors (Reinhart-Rogoff sovereign-stress propagation, Helbling-Terrones panic chain, Krugman currency-crisis transmission, civil-unrest → state-driven internet shutdowns). Every Monday at 06:00 UTC a cron lets Claude refine the matrix with web-search-grounded reasoning: each edge change must cite a URL, the patch is capped at 8 changes per run and 0.2 weight delta per edge, and the new version lands as a canary until the top-3 levers stay unchanged or a human promotes it. Every change is queryable in matrix_edge_updates with before/after weights and citations.

A backtest validator computes the matrix’s per-cell Pearson r, sign-hit-rate, and MAE against historical component-delta vectors — so “is v23 better than v22” gets a numerical answer instead of a vibe.

The architecture in one breath

  • Next.js 16 App Router on Node ≥ 22, deployed on Ubuntu via PM2 + nginx + certbot
  • SQLite (Drizzle ORM) with ~30 normalised tables for time-aligned history; WAL mode, retention configurable down to “keep forever” for LLM corpora
  • Every sidecar is a self-contained module: upstream → in-memory cache → SQLite. Page reads hit DB first; ten crons (every 5 / 15 / 60 min depending on cadence) are the only things that touch upstreams
  • One pure composition function for ETI; one pure math module for AEC; one cosine-similarity sweep for analogs. The pieces compose, the layers don’t leak.
  • Claude Opus 4.7 for the daily briefing and weekly matrix refinement, both with the web-search tool, both audit-logged per call with token + cost tracking

What we shipped

  • 22 live sidecars, each with DB-first reads, in-memory cache, upstream fallback, and graceful degradation when the source is offline
  • The ETI composition layer — 22 piecewise-linear scoring functions, three correlation-dampening clusters, capped renormalisation, 5-level alert mapping
  • Historical analog finder via cosine similarity over the 13-dim component vector, surfacing top-3 past matches plus their top contributors
  • AEC leverage layer — TypeScript port of the Fortran routines, stabilised constrained-eigenvalue solver, eight named scenarios, propagate-only “counterfactual playground”
  • 22-node × 68-edge seed causal matrix authored from textbook priors with per-edge confidence and rationale
  • Weekly Claude refinement loop with web-search grounding, forced structured tool calls, stability caps, and a queryable audit log
  • Backtest validator for matrix versions — Pearson r, sign-hit-rate, MAE, ETI-score MAE, per-component breakdown
  • Daily AI briefing grounded in current events, persisted with full per-call token + cost audit
  • Operator-friendly admin at /admin/matrix — promote, refine-now, backtest, all gated behind a session-scoped admin key

Why this project matters

Most composite-index products fail in three predictable ways: they overuse z-scores and lose interpretability, they sum apples and oranges with weights nobody can defend, and they silently degrade when sources go offline. Exodus Tracker is built to avoid all three. The per-signal calibration is in the open, the correlation dampening is principled, the source-availability handling is transparent, and the causal-leverage layer on top makes the index actionable rather than purely descriptive — it tells you which lever to pull, not just how hot the room is.

That, plus the fact that the whole thing runs on one Node process and one SQLite file with no external services beyond a FRED_API_KEY and an ANTHROPIC_API_KEY, is the part of the project worth pointing to.

Working on something similar?

Use this case study as the starting point for the conversation.

Share the product, the stack, and what's slowing things down. That's usually enough to figure out if there's a fit.

Exodus TrackerWeb appData product