Back to Projects
Complete Case study Featured

AstroAI: An Evidence-Backed Chart Engine Cutover

Python Swiss Ephemeris kerykeion NASA JPL Horizons Evaluation Migration

A migration where the evidence came first: 15 edge-case charts, an independent reference, and a one-line rollback.

Moved AstroAI's natal chart computation from a paid hosted API to local Swiss Ephemeris math. The cutover was approved on an offline fidelity harness that checks every planet, angle, and house cusp of 15 edge-case charts against NASA JPL Horizons and independent ephemeris calls.

worst planet error vs NASA JPL Horizons (bar: 60")
1.9"
worst planet error vs NASA JPL Horizons (bar: 60")
edge-case charts compared element by element
15
edge-case charts compared element by element
house cusp difference vs direct Swiss Ephemeris
0.000"
house cusp difference vs direct Swiss Ephemeris
per-chart latency, hosted call vs local math
1-30 s → ms
per-chart latency, hosted call vs local math

The problem

Production computed every natal chart by calling a hosted Astrologer API on RapidAPI. That meant one paid credit per call, 1 to 30 seconds per request with 429 retry loops, and a chart-wheel endpoint that never returned an SVG for the app's payload.

The same library can run locally, but a quietly wrong chart is worse than a slow one. Users can't check planetary positions themselves, so the switch needed proof, not just a passing smoke test.

Constraints

  • Accuracy bar of one arc-minute (60").
  • No production API key was reachable from the build environment, so a live local-versus-hosted diff wasn't possible. The evidence had to come from independent ground truth.
  • The cutover had to be reversible with one environment variable, and the hosted engine had to stay available as a fallback.
  • Never return a wrong chart. Malformed output and ambiguous input both have to fail closed.

What I built

  • A CHART_ENGINE switch (local real engine, hosted, or fake). Zodiac, house system, and perspective are passed explicitly, so a change in a library default can't make the engines diverge, and subjects are built fully offline.
  • A fidelity harness (experiments/chart_fidelity.py) that computes each chart locally and compares every element: planet longitudes, Ascendant and MC, all 12 Placidus cusps, and aspects. References are pinned NASA JPL Horizons longitudes plus direct Swiss Ephemeris calls whose Julian day is derived separately through zoneinfo. It writes a machine-generated evidence table and has a live mode for diffing against the hosted API when a key is available.
  • An edge-case chart set covering polar north (Tromsø 69.6°N), the deep south (Ushuaia, McMurdo 77.85°S), southern-hemisphere cities, historical dates (Paris 1900 and 1935), a future date (Tokyo 2075), the 2000 leap day, the date line (Kiritimati, Samoa 2011), DST gaps and overlaps, and midnight boundaries.
  • A fail-closed output validator for every engine: all ten planets, four angles, and twelve house cusps must be present, finite, and internally consistent before a chart can be saved or shared.
  • A runtime fallback to the hosted engine for real infrastructure failures. Input-validation errors such as an ambiguous DST time are re-raised before the fallback, so they are never silently guessed.

How it's verified

  • Against NASA JPL Horizons, the worst planet difference across the two baseline charts was 1.9" (about 0.0005°), roughly 30x inside the arc-minute bar. Regression tests pin those Horizons values with a 0.02° tolerance and run offline.
  • The Ascendant, MC, and all 12 house cusps matched direct Swiss Ephemeris calls to 0.000".
  • In 13 of the 15 edge-case charts, the worst element matched the independent reference to 0.00". Paris 1900 differs only from the independent reference, because of historical local-mean-time and Delta-T modelling that local and hosted engines share, so local-versus-production parity holds. Both DST charts are rejected rather than guessed.
  • 64 regression tests cover the switch: test_chart_engine.py (34), test_chart_fidelity.py (21), and test_chart_engine_validation.py (9).
  • The harness surfaced a migration trap: a legacy aspects API in the new library version silently drops the lunar nodes. The real engine uses the newer API instead.

Results

  • The cutover was approved on 2026-07-08, and local computation is now the production default. Natal charts no longer spend hosted-API credits, and one environment variable switches back if needed.
  • Charts gained a rendered wheel that the hosted endpoint never returned.
  • The remaining differences are written down. Only one is behavioral (ambiguous DST times), it fails safe with a friendly re-enter prompt, and none affect position accuracy.

TODO (Matt): numbers to add

  • TODO: Monthly hosted-API spend before the cutover, and what it is now.

How coding agents fit in

  • Built through agent PRs (#25 added the local engine mode, #67 added fail-closed output validation). The written fidelity report and evidence appendix were the artifacts reviewed before approving the production switch.