Case Study · Documentation
GooseMarket.
A Waterloo-based prediction market. Students create binary polls and trade YES / NO shares with virtual currency, priced live by an automated market maker. Built as a full-stack team project, from AMM math to admin tooling.
At a glance
Market
Binary YES / NO polls
Pricing
LS-LMSR market maker
Backend
Flask API + Supabase
Frontend
React + React Query
Overview
A campus prediction market
GooseMarket lets students bet on campus questions - who wins an event, whether something ships on time - with virtual currency instead of cash. Each question is a binary market, and the current price reads directly as the crowd's implied probability that it resolves YES.
The interesting part is the pricing. Rather than matching buyers to sellers, an automated market maker always quotes a price using a liquidity-sensitive LMSR, so there is always a trade available and the price moves smoothly as opinion shifts. Around that engine sits a full product: auth, an admin review and resolution flow, tags, positions, streaks, and a leaderboard.
Architecture
React → Flask → Supabase
Client · React SPA (Vite + React Query)
Routes & UI
React Router
React Query
fetch + cache
Backend · Flask REST API
Auth · Polls · Trades · Admin
REST routes
LS-LMSR engine
live pricing
Data · Supabase (Postgres)
profiles · polls · trades
core tables
tags · poll_tags
topics
The browser never speaks to Supabase directly. Flask proxies every operation with the service-role key and enforces auth via the session cookie.
The market maker
How a price moves
Shares, not odds
Every poll is a binary market. Buying YES or NO mints shares; the market tracks a net quantity per outcome, q_yes and q_no.
LMSR cost
Market state is a cost function C(q) = b · log(e^(q_yes/b) + e^(q_no/b)). The price you pay for a trade is exactly the change in C that it causes.
Live prices
YES and NO prices are the softmax of the quantities - e^(q/b) normalized to sum to 100¢ - so a price always reads directly as an implied probability.
Liquidity that scales
The liquidity parameter isn’t fixed: b = b0 · √Q grows with market size, so early trades move the price meaningfully while thick markets stay stable.
Poll lifecycle
Create → Approve → Trade → Resolve
Create
Any user proposes a poll - title, description, end date, and tags. It enters an admin review queue rather than going live immediately.
Approve
An admin reviews the proposal and approves, edits, or rejects it. Only approved polls become public and tradeable.
Trade
Users buy and sell YES / NO shares. The AMM quotes a live price, and balances and positions update on every trade.
Resolve
After the end date an admin resolves the outcome. Winning shares settle against the balance and the leaderboard updates.
Backend
API surface
POST /api/auth/*login · register · verify · logout, over a cookie session.
GET · POST /api/pollsList all polls or create a new one (pending review).
GET · PUT /api/polls/{id}Fetch or edit a single poll.
GET /api/polls/{id}/priceLive LS-LMSR YES / NO price, plus /estimate and /stats.
POST /api/trades/buy · sellExecute a trade against the market maker.
POST /api/positions · /api/userA user’s open positions and profile / balance.
/api/admin/*review · approve · reject · update · resolve flows.
GET /api/leaderboardRanked traders by realized performance.
Details
Design decisions
Server holds the keys
The Supabase service-role key lives only in the Flask API. The browser talks to /api/* with a cookie session and never touches Supabase directly, so trusted operations stay server-side.
Play money, real incentives
Balances, daily bonuses, and streaks drive engagement without any real cash, and a leaderboard ranks traders - the game-theory of a market without the regulatory weight.
A tested engine
A pytest suite covers the parts that must not break: pricing, buy/sell trades, poll creation, positions, and tags - the AMM math has its own tests.
Built like a product
The repo carries a full engineering doc set - charter, domain model, requirements, use cases, and a test plan and report - alongside the code.
Built with