Architecture
One TypeScript monorepo, one database, two server roles.
┌──────────────── Strike Finance ─────────────────┐
│ /ws/price: markprice@1s, bookticker, depth, │
│ trade, miniticker · REST /price/v2/* │
│ REST /v2/*, /auth/builder/* · /ws/user-api │
└───────▲──────────────────────▲──────────────────┘
│ data network prices; │ market data per network;
│ orders, user streams │ Builder Connect
┌───────────────────────┴──────────┐ ┌───────┴──────────────────────────┐
│ worker process (role worker) │ │ api process (role api) │
│ market/collector → ticks │ │ Fastify HTTP + /ws fan-out │
│ market/vol → sigma │ │ market/feed: order book, stats, │
│ practice/settle → boxes │ │ Live market (per network) │
│ jobs (scheduler, leases) │ │ auth (sessions) │
│ strike/live → orders, │ │ practice/accept (atomic) │
│ user stream, reconciliation │ │ practice/sheets (signed quotes) │
│ │ │ strike/connect (builder connect)│
│ │ │ social (rooms, presence, lb) │
└───────────────┬──────────────────┘ └───────────────┬──────────────────┘
│ PostgreSQL 16 (single source of truth) │
└──────────── LISTEN/NOTIFY: tick, bet, live ────────────┘
▲
apps/web (Vite + React, canvas chart/grid)
apps/docs (VitePress)Processes
The server package runs in one of three roles:
| Role | Runs | Notes |
|---|---|---|
api | HTTP API, WebSocket fan-out, auth, tap acceptance, signed quote sheets, Strike's order book and statistics, Live mode's market, Builder Connect, social, and the built web app (WEB_DIST) | Stateless apart from in-memory rate limits and its own copy of Strike's public market data; can run more than one instance. |
worker | Price collector, volatility, settlement, the durable job scheduler, social housekeeping (battles, referral bonuses, presence), data retention, Strike live orders, user streams, reconciliation | Does all background work. Needs no browser. |
all | Both, in one process | Local development, single-machine setups and the Render Blueprint. |
The collector holds the worker's single connection to the data network's price stream. It is leader-elected with a Postgres advisory lock, so a second worker waits instead of double-recording. Each api process also holds its own public /ws/price connection per Strike network it follows: the data network's, plus the execution network's when that is a different one. That data is public, kept in memory and shown, never recorded, so it needs no leader: every api instance serves it from its own connections. Settlement jobs are leased (30 seconds) with FOR UPDATE SKIP LOCKED, so several workers can share the job queue safely. The server applies pending database migrations itself at start-up, under an advisory lock.
Data flow
Prices. The worker subscribes to <symbol>@markprice@1s and <symbol>@bookticker on the data network's public price stream and stores each mark tick (symbol, E, mark, index, received_at), plus the latest best bid and ask (also read from a REST snapshot every 5 s, because that stream only pushes on change). A duplicate (symbol, E) is ignored. A tick older than the latest stored one is kept as an observation but never moves the current price. Each stored tick is announced on NOTIFY; the api processes push it to browsers.
Market data (api). Each api process follows Strike's public market data itself, per network (Strike integration): an order-book copy per market (the depth stream for the 20 best levels plus a full REST snapshot every 2 s), the best bid and ask, the mark price and the market statistics. It serves the data network's book and statistics on /api/market/:symbol/book|stats and the market:<SYMBOL> channel. Live mode's market is the execution network's: when the networks differ, a second feed follows it, keeps its last 15 minutes of mark ticks in memory, and serves it on /api/exec/* and the exec:<SYMBOL> channel; when they are the same, those mirror the market ones. The liquidity gate, the live plan and its expected fill read only the execution network's feed. None of this data affects a practice quote, outcome or payout.
Tick quality. Every tick is checked when it is stored (price, timestamp against the receive time, a 1.5 % jump from the previous tick, a 5 % mark/index gap). A failing tick is stored but flagged, and leaves the chart, the quotes and settlement exactly as a gap does. The collector also tracks the median offset between Strike's timestamps and the server clock over the last 30 ticks, and treats the market as stale if it exceeds 2 s.
Quotes. On every tick, the api process prices the visible grid for that market and pushes a quote sheet. Each box is priced as the higher of the random-walk model and the empirical touch rate over the last 30 minutes of recorded ticks, which the process keeps in memory per market and primes from the database at start-up. Each quoted cell carries an HMAC-SHA256 tag binding the cell, band, window, multiplier, probability, S0, sigma and issue time. The client sends the tag back with a tap; the server verifies it without keeping per-quote state.
Taps. Acceptance checks run in order: auth, idempotent replay (a retried tap gets its original result), practice mode switch, rate limit, tag, quote age, cutoff, market staleness, stake limits, re-price with the same robust method. Then one database transaction does the idempotency insert, balance reservation, daily-limit and exposure checks, ledger entries, the bet row and its settlement job.
Settlement. The worker settles wins as ticks arrive, and runs a durable job for every box 2.5 s after its window ends; a box that still can't be decided 30 s after its window ends is voided. The guarded status update and the ledger entries share a transaction.
Retention. The worker prunes old ticks and feed-health events daily. Ticks are kept for at least 24 hours (7 days by default), and ticks within 2.5 s of the window of a box that is still open are never pruned. A settled box carries its own evidence, so receipts and replays don't depend on the raw ticks.
Live boxes. A live box and every order it needs are written to a durable outbox before anything is sent to Strike. The worker follows each connected user's Strike account stream, runs time exits, and reconciles from Strike's REST API after every reconnect. A box manages only its own exposure: closing orders are sized to what the box opened, never to a larger position on the account. The builder fee rate is stored on the box when it is accepted.
Clients. Pushes are hints. After every (re)connect the web app re-fetches REST state: ticks since its last one (from the market the board shows), its user, its open boxes.
Packages
| Path | Owns |
|---|---|
packages/core | Parameters, grid geometry, pricing, outcome evaluator, money math, quote tags, shared types. Pure, no I/O; imported by server and web. |
packages/strike-mock | A fake Strike price stream, REST API and user stream, for tests only. Never used by a deployment. |
packages/brand | Design tokens, logo, fonts. |
apps/server/src/db | Pool, migration runner, SQL migrations. |
apps/server/src/market | Collector, tick store, tick quality checks, gap and stale status, volatility, best bid and ask, and the liquidity gate (gate.ts). |
apps/server/src/market/feed.ts | One Strike network's public market data, in memory, for the api: the order-book copy, best bid and ask, quality-checked mark price with a 15-minute history, market statistics. |
apps/server/src/market/book.ts | The order-book copy: REST snapshots plus depth updates, staleness, exact sums per grid row. |
apps/server/src/market/exec.ts | Which feed serves what: the data network's feed and, when it differs, the execution network's; Live mode's market data, from the execution network only. |
apps/server/src/http/marketData.ts | /api/market/:symbol/book, /api/market/:symbol/stats, /api/exec/*, and the book, stats and exec:<SYMBOL> pushes. |
apps/server/src/practice | Quote sheets, acceptance, ledger, settlement. |
apps/server/src/jobs | Durable job scheduler and data retention. |
apps/server/src/strike | Request signing, Strike client, Builder Connect, key custody, live boxes, user stream, reconciliation. |
apps/server/src/social | Rooms, presence, feed, leaderboards, battles, referrals, replays and preview images. |
apps/server/src/auth | Sessions. |
apps/server/src/http | Fastify app, routes, WebSocket hub. |
apps/web | The web app (served by the server in production; works from a home-screen icon). |
apps/docs | This site. |
Design choices
| Choice | Why |
|---|---|
| TypeScript monorepo, pnpm workspaces, Node 22 | One language for the domain math shared by server and client. |
| PostgreSQL as the only state store: ticks, ledger, jobs, presence | Real transactions, row locks, SKIP LOCKED job leasing, LISTEN/NOTIFY, one dependency to host. |
Separate api and worker roles of one server package | Settlement and collection run with no browser; the API scales on its own. |
| Server-side custody of the Strike API-wallet key, AES-256-GCM at rest | Builder Connect entrusts the key to the builder; time exits, protective closes and reconciliation must work with the browser closed. The key can't withdraw. |
| Vite and React SPA, custom canvas renderer for chart and grid, CSS variables, no UI kit | Smooth on mobile, exact control over cell geometry. |
Practice settlement on Strike's markprice@1s mark price | Strike's own valuation price, nominally once a second, the same feed the chart shows. |
| Live entries via Strike's bracket strategy order with an IOC limit primary | Bounded entry price on thin books; reduce-only exits. |
| Order books and statistics followed by each api process, in memory | Public, display-only data that changes several times a second: every instance serves it from its own connection, and nothing high-rate passes through Postgres. |
| Live mode reads only the execution network's market | Take-profit and stop trigger on that network's mark price, so the board, gate and plan must show the market the orders meet. |
| A mock Strike server for tests only | Protocol contract tests and outage drills without touching the real Strike. |
Money
Practice amounts are integers in hundredths of a credit (12345 = 123.45). Prices from Strike are decimal strings, stored exactly. Floats appear only inside the pricing and outcome math. Every credit movement is a pair of ledger entries summing to zero across user:available, user:in_play and house:practice; balances are derived from, and reconciled against, the ledger in tests.