Architecture
How Ninety is built
A TypeScript monorepo with a Python quant worker and a Rust/Anchor program. Every significant decision is recorded — 87 architecture decision records in docs/adr/, written before the code.
#The live path
The live path
#The parts
Fastify API
30 endpoints, fully typed and schema'd, with live Swagger. Auth, markets, quotes, orders, portfolio, moments, leaderboard, search, events, and a cost-aware rich-data proxy.
Single-writer LMSR engine
the market maker. Exactly one writer owns market state, enforced by a lease at boot, using journal-then-ack: the intent is journaled before it is acknowledged, so a crash cannot lose or double-apply a fill. One match is low-throughput; correctness matters far more than horizontal scale, so a single writer is the right trade.
Redis Streams bus
two planes: domain events and sys.* signals. No service ever calls another directly. The ingest worker can die without taking prices down. This is the decision that makes every other component replaceable.
cortex (Python)
the quant worker. De-vigging, the Poisson and Skellam inversions, and the Dixon-Coles grid. Python because scipy is worth the polyglot cost.
ingest / jobs workers
live TxLINE consumption; the settlement saga, the AI Booth, and the EarlyWhistle Telegram bot.
Anchor program (Solana)
verifies TxLINE's signed statistics on-chain before a market can settle.
Storage
Postgres via Prisma (Aiven), Valkey for cache and streams (Aiven).
#The two-source law
TxLINE owns everything that moves during a match — scores, goals, halts, prices, results. Baked static data owns only what sits still — flags, crests, stadiums, the 104-fixture skeleton. The tie-breaker is simple: if it changes during a match, TxLINE is the source of truth.
This exists because the free data tiers are rate-limited (10 requests/minute and 100/day), which makes per-request upstream calls impossible. So static data is fetched once at build time and committed.
#On-chain, and the forge
Access to the feed is gated by a real Solana transaction — a guest token, an on-chain subscribe, then activation. The chain is the gate, not a logo. Results are verified by the program, and there is no admin result path, by design.
Building the settlement path, we adversarially reviewed it and found that TxLINE's proof does not bind match finality on-chain. A permissionless caller could settle a wrong result using a genuine mid-match proof by selecting the batch. So settlement is fail-closed on purpose:
pub const SETTLEMENT_LIVE: bool = false; // compile-time, first statement of the settle handlerIt is a compile-time constant, not a config flag — flipping it requires a source change, a rebuild and a redeploy. We filed the finding back to TxODDS. We will not ship a settlement we can prove is forgeable, even in play-money.
#Design system
Every colour is a design token; a raw hex in a component fails the build. Numbers are SF Mono with tabular figures and one decimal; everything else is the system font — that single split is most of why the interface reads like an exchange rather than a dashboard. Motion is 150–250ms ease-out, and prefers-reduced-motion is honoured everywhere.
#Verified, not asserted
279 automated tests. A screen is not considered done until it has been screenshotted, looked at, and passed the read-out-loud test — enumerate every text element and confirm no two contradict each other. That test has caught more real bugs than every automated tool in the stack.