Inspiration
Every flight is already a bet — you just don't get to price it. Gate boards and flight trackers treat delay as a status light. I wanted the opposite: a live market where On-time, Delayed, and Canceled have prices that move as people take a view, then settle against the real outcome.
The name is the joke and the thesis. Flight Risk is not a gambling product (no real money, ever). It is a mechanism demo: can you run a three-outcome automated market maker on live aviation data, keep an honest ledger, and still put a verifiable receipt on Solana in a weekend?
What it does
Flight Risk is a prediction market for real flights.
- A departures-style board lists biddable flights (seeded demo legs mixed with a small AviationStack batch). Odds for the three outcomes are live LMSR prices, in tabular mono, and they flip when the book moves — no page refresh.
- Open a row and you get a ticket: price history from a Timescale one-minute continuous aggregate, current odds with up/down deltas, and a boarding-pass-stub bet slip (quote, then confirm).
- Log in with Auth0. Spend virtual credits. The server re-quotes on every bet and rejects the fill if the price walked more than 2% slippage.
- When a flight lands or cancels, the market resolves once. Winning shares redeem 1:1 in credits. Losing shares are already paid for. The US DOT/BTS 15-minute rule decides On-time vs Delayed.
- Solana devnet does not hold the book. It holds receipts: a user-signed memo on each bet, a house-signed memo on each payout, both linked to Explorer.
A 3-minute demo cannot wait on a real 737. An unlisted /demo console lands a seeded flight on demand (Delayed wins at 45 minutes) and resets a fresh book for the next run-through. Judges never have to touch a database.
How I built it
Stack (deliberate, not a grab bag): Next.js 16 App Router + TypeScript, Tailwind v4 tokens (no component kit), Tiger Cloud Postgres/Timescale, Auth0 SDK v4 (proxy.ts, not the old handleAuth()), Solana web3.js + wallet adapter + SPL Memo, AviationStack, Vercel Cron.
The book is LMSR with liquidity (b = 100) and (n = 3) outcomes. Cost and prices are the textbook formulas — I locked golden values in Vitest and refused to eyeball them:
[ C(\mathbf{q}) = b \ln \sum_{i} e^{q_i / b}, \qquad p_i(\mathbf{q}) = \frac{e^{q_i / b}}{\sum_{j} e^{q_j / b}} ]
Buying (\Delta) shares of outcome (i) costs (C(\mathbf{q} + \Delta \mathbf{e}_i) - C(\mathbf{q})). That cost is what the quote endpoint returns and what the bet endpoint re-computes inside a transaction.
Tiger Data is load-bearing, not a logo. flight_status_events and market_price_ticks are hypertables. market_price_1min is a continuous aggregate with a refresh policy — the ticket chart reads the cagg, not a pile of raw ticks. Schema went on as raw SQL because Timescale DDL does not enjoy an ORM.
Auth0 v4 was a real migration, not a drop-in: session on the server, ensureUser on first login, /account protected. I learned the hard way that wrapping the whole layout in next/dynamic ssr: false for Phantom breaks Auth0's 307s. The wallet button is the only thing that is client-only.
Solana is receipts, not a casino. virtual_balance in Postgres is the ledger of record. After the server accepts a bet, the client asks Phantom/Solflare to sign a memo JSON. On resolve, a house keypair on devnet only writes a second memo into payout_solana_tx_signature. Two columns, two Explorer links, no mainnet, no real-value token.
UI started as a split-flap / terminal board, then I inverted the palette to a cruise-altitude sky: deep overhead blue fading to hazy horizon, three slow parallax cloud layers (paused under prefers-reduced-motion). Token names stayed (--terminal-black is now a pale sky) so the swap was one file plus a nav contrast fix — navy ink on navy header is invisible.
Built in staged slices (scaffold → schema → ingest → auth → LMSR → memos → board/ticket → demo rehearsal) and stopped after each stage so I did not ship a half-wired AMM.
Challenges I ran into
AviationStack's free tier is ~100 requests/month. A 30-second poll would burn the month before lunch. Cron is conservative (hours, not minutes). The live demo depends on seeded flights; real legs are atmosphere, not the payout moment.
Resolution must be boring. A retried cron or a double-clicked "Land" cannot pay twice. The resolver re-checks market.status = 'open' in the same transaction that flips it to resolved and writes payouts. Memos run after commit so Solana RPC never holds row locks.
Timescale continuous aggregates cannot be created inside an implicit simple-protocol transaction. I apply schema statement-by-statement. The cagg also lags the last minute (end_offset); the chart pins a live "now" point so 30/40/30 odds do not sit on a stale flat line that looks like a broken Y-axis.
Auth0 v4 + Solana wallet adapters fight Next.js in opposite directions (SSR redirects vs browser-only extensions). Scope ssr: false to the button, never the shell.
Official Solana airdrop 429'd the house wallet. A faucet funded the payout keypair so Explorer links were real, not TODO.
The late visual pivot. Dark board → light sky meant inverting background and text tokens. Primary buttons already used text-terminal-black on accent; after the swap that became light-on-cruise-blue, which is what you want — as long as the nav does not keep navy ink.
Accomplishments that we're proud of
- A full loop, twice, with no
psqlin the middle: bet → land seeded flight → Delayed wins → balance updates → payout memo on Solana Explorer. - LMSR that matches the spec's golden values, not "looks about 33%."
- Hypertables + a refreshing 1-minute aggregate that the UI actually reads.
- Idempotent resolve you can hammer. I did.
- A UI that treats numbers as the product: flap motion on change, clouds that stay atmosphere, no card-grid theme pack.
What I learned
- Quota is architecture. If your third-party API has 100 calls a month, your demo story cannot be "poll the world."
- Idempotency is a product feature when cron, retries, and a judge's double-click are the same code path.
- Off-chain ledger + on-chain receipt is an honest Solana weekend. A half-finished Anchor LMSR is not.
- Auth0 v4 is a different SDK. Read the current Next.js 16 pattern or you will debug middleware that no longer exists.
- Design tokens named for a dark theme will lie to you after a light inversion. Contrast is a checklist, not a vibe.
What's next for Flight Risk
The stretch I did not start on purpose: Gemini risk copy ("why is Delayed 40% on this route?") and Backboard memory of a returning user's book. Those only ship after the loop is boring.
If I keep going: more seeded books for back-to-back demos, tighter AviationStack use (or none, if quota is gone), and — only with time left — an Anchor program that holds LMSR state on-chain instead of memos. The memo trail stays the honest MVP.
Built With
- auth0
- aviationstack
- lmrs
- next.js
- node.js
- phantom
- postgresql
- react
- solana
- solflare
- tailwindcss
- tigerdata
- timescaledb
- typescript
- vercel
Log in or sign up for Devpost to join the conversation.