Sportsbook
The catalog
The sportsbook is a tree, and every level of it is something you merchandise:
Sport → Category (region) → Competition (league) → Event (fixture)
└─► Market (a question)
└─► Outcome (a price)
An event is one fixture — home versus away, a kickoff time, a status. A market is one question about it: match result, over/under goals, handicap. An outcome is one answer, carrying a decimal price. Tapping an outcome adds it to the bet slip.
Which markets players see, what they are called and where they sit on the event page is a catalog of its own, configured per provider under Betting → Markets with per-sport overrides: each market can be shown or hidden, renamed, placed in a display group (the tabs on the event page), ordered, and kept off or on the sport boards. A market type the catalog has not seen before arrives hidden until staff review it, so nothing unexpected goes live; every market the feed sends is stored, and visibility is a staff decision.
Events carry a popularity score computed at ingest from league prestige, team prestige, derby and stage. You can override it per event with a manual boost, and pin an event as the lobby hero; there is no automatic hero without a pin.
The lobby is one board with a row of views — Popular, Live, Trending, Pre-Match and Starting soon — each of which fixes both which events show and their order. The sports sidebar lists the same views, and each day header on the board is named after the earliest kickoff on it. Staff set the order of that list in the backoffice, can hide any view except My Bets, and can add their own lobby pages (built in the page constructor, see Rankings & Discovery) as entries beside the built-in views; a page appears only while it is active. Trending ranks on real player activity (views, search clicks, bet-slip adds and bettors). How every list is ordered is described in Rankings & Discovery.
Visibility is yours to control. Sports, categories and competitions each carry a visibility flag and a sort order, editable from the backoffice. Every player-facing read applies the full chain, so hiding a competition hides it everywhere — including from bet placement, not just from the lobby. The catalog page works one bookmaker at a time: it opens on the first active bookmaker, inactive ones are marked in the picker, and sort order is kept per bookmaker, so dragging a sport into place never mixes two feeds.
Following leagues and teams
A signed-in player can follow a competition from its page and follow a team or player (a competitor) from the event page. Followed fixtures get their own Following view on the lobby board: live matches first, then soonest kickoff, so a followed team's next match is never reordered by popularity. The view appears only for signed-in players.
Following does not send notifications: the earlier live-match alert was removed on 22 October 2026, because the match simulator produced a flood of duplicates.
Live betting
Prices update in the player's browser over a live stream: odds flash up or down as they move, scores tick, and markets suspend when the feed says so. Live scores render per sport family — soccer, set-based sports like tennis, and clock-and- period sports each get their own presentation.
The player site is built to look like a modern book: a live events strip, a featured hero with a countdown, market grids by family, league rails, and a grid/table view toggle.
Live football matches carry a match tracker on the event page: a one-line bar in the hero (who is pressing, a momentum sparkline) that opens into a pitch view, stats and timeline tabs and a minute-by-minute strip of incidents. Momentum and pressure are derived from the changes in the feed's attack statistics, so the tracker appears only for matches whose feed sends them. On desktop a player can track up to four matches; each gets a tile on the toolbar showing the score and flashing when a goal goes in, and opens into a floating tracker card.
The event page groups markets into tabs by display group, and a Combo tab (see below) appears where same-game combos are on offer. When a match finishes, its page keeps the markets on show as closing prices — muted and not bettable — rather than replacing them with an empty state. Cancelled matches show their refund message instead.
The bet slip
One pick places as a single. Two or more combine into a multi (accumulator): one stake, prices multiplied, all legs must win.
Two rules the slip enforces before the server has to:
- Same-event conflicts are blocked in the slip, with an inline reason — two outcomes from the same fixture are not independent and cannot be multiplied together. The one exception is a same-game combo, which is priced as a whole (below).
- Price changes are never filled silently. At placement the player's expected price is compared to the live one. If it moved, the bet is rejected and the new price is returned so the player can review. Players choose their own policy in settings: accept all changes, accept only improvements, or accept none.
Each wallet currency has its own minimum stake, worth roughly ten US cents (0.10 for USD, EUR, GBP and the USD stablecoins; 0.000001 BTC; 0.00004 ETH), so crypto wallets can place small bets too.
The slip offers quick stakes and a MAX button, shows a totals block, and strikes through a price's previous value when it moves.
Odds formats. Prices can be shown as decimal (the default), fractional, American, Hong Kong, Malay or Indonesian. The player switches in the bet slip's settings menu, and the choice is saved on their profile so it follows them across devices. Prices are stored and paid as decimal, floored to two decimals (what is shown is what is paid); every other notation is converted from that figure and rounded against the player, so a converted price never overstates the return.
Placement produces a receipt — selections, stake, potential return — distinct from a plain success message.
The slip also redeems the player's promotional tokens — free bets and bet boosts (a single-use token that adds a share of a won bet's winnings, up to a cap) — and applies the operator's price promotions:
- Accumulator boosts. An operator can run several accumulator boost ladders at once, each with its own audience. At placement the server checks each audience for that player and applies the ladder that pays the most for the slip; the slip's boost meter shows the best one.
- Featured price boosts. A boosted price on a chosen selection. The slip shows the boosted price's maximum stake, flags a stake above it and holds the Place button. If a boost is refused, the slip says why (for example, stake over the cap) rather than claiming the bet goes on at the normal price.
Boosted and free-bet receipts report the return the bet will actually pay. A free bet's potential return is stake × (odds − 1), because the stake itself is never paid back, and a boosted return is capped at the operator's maximum payout, the same cap settlement applies. Acca and price boosts are benefits with audiences checked per player; see Benefits Catalog.
Quick bet
Quick bet is one-tap betting for singles. A player turns it on from a switch in the slip header; while it is armed, tapping a price places a single at the chosen stake straight away, and the phone slip pill shows the armed stake. Each quick bet carries a short undo window (5 seconds pre-match by default; undo is off for live matches unless the operator enables it, then 3 seconds by default), shown as an undo bar. Staff control the switch and both windows from the Quick bet card on the Betting page; turning it off hides the switch and refuses new quick bets, while placed ones still settle. All the usual checks (limits, price-change policy, balance, player status) apply.
Same-game combos
A combo lets a player build one bet from several picks in the same match, for example a result plus a goals total. Because those picks are correlated, the price is not the product of the legs: the active provider prices the combination as a whole, and the player sees a quote that is held for 30 seconds. Combos are off by default, switched on for the sportsbook by staff (Betting page), and available only when the active provider prices them (LSports, not The Odds API), for football, basketball, baseball and American football, pre-match only, and for the provider's team markets (no player props). Players pick from suggested combos or build their own in the event's Combo tab. A combo is graded pick by pick; a lost pick loses it at once. Turning the switch off hides combos but leaves placed ones to settle.
My Bets
Placed bets live in their own section, split into open and settled. Open bets are grouped by when they resolve: live now first, then one group per day of the next kickoff (Today, Tomorrow, then dated days, or "to be confirmed"), each headed with the day. Each bet is a two-part ticket: the event on one side (competition, teams, score or kickoff) and the bet on the other (market, pick, price, stake and return or verdict), side by side on wide screens and stacked on phones. Each bet shows human-readable selection names (not internal ids), and a multi shows its first three legs with won, lost, void or open markers, a progress bar and the total odds, and the rest behind a toggle, so a player can see at a glance where an accumulator stands. Free bets carry a gift icon. The list is filtered and counted on the server (open, settled, won, lost, void, cashed out, plus a search by event, team, market or selection) so the counts stay right on long histories.
Cash-out
A player can take the value of an open bet early, in full or in part. Cash-out is off until the operator turns it on (Betting → Cash-out). The price is the bet's fair value from the current odds, less a margin (5% by default, with per-sport overrides; a multi takes the highest margin among its sports), and never more than the bet could still pay or its payout cap. The player gets a quote that is held for a short time (10 seconds by default); at execution the bet is priced again, and if the value has dropped past the operator's tolerance (2% by default) the cash-out is refused and the new value shown. A partial cash-out closes the same share of the stake at the original odds and leaves the rest running; the operator sets the smallest cash-out and the smallest stake that must remain (both in US dollars), whether partials and live cash-out are allowed at all. What is offered is always rounded down to the currency's scale.
Not eligible: free bets, bets carrying a bet, price or accumulator boost, same-game combos, bets held for review, bets with no open leg, and bets with a leg on a hidden, suspended, stale-feed or past-kickoff market. A multi can be cashed out; settled legs count at their result. A cash-out is a real payout and counts in handle and reporting; a fully cashed-out bet earns no XP, missions or rakeback, and a partly cashed-out bet that later settles earns them on the stake still open.
Settlement
When results land, each selection is graded and the bet resolves. Grading is score-based and handles the cases that matter:
| Result | Meaning |
|---|---|
| Won / Lost | The straightforward cases |
| Push | Stake returned — the line landed exactly on the result |
| Half won / half lost | Quarter-line (Asian) handicaps split at grade time |
| Void | Leg refunded; a multi recomputes on its remaining legs |
A multi is lost the moment any leg is lost — nothing the other legs do can bring it back — so it settles at once rather than waiting for the rest of the fixtures.
Settlement runs inside a single locked transaction with idempotent references, so a repeated settlement pass cannot pay twice, and a partially applied settlement cannot exist. It is also lean: a September 2026 pass cut the database queries per settled bet by about 40%, with identical ledger postings.
Two protections handle bets whose price may have been unreliable. A pick placed after the match ended settles void. A pick taken while the feed could not be trusted — live coverage lapsed, or the provider's heartbeat stale — is held at settlement whatever the result, and staff settle or void it from the Bets to review section of manual resolve.
Staff can settle events manually from the backoffice — as a correction or an override — and can void an individual bet. Voiding a free bet refunds nothing, because its stake was never the player's cash.
Manual results stick. Every market result records who wrote it. A feed result arriving later is a no-op against a staff-entered one, and a bet that staff voided by hand is skipped by settlement, so neither is silently undone.
Manual resolve is a split view: the queue of events waiting on a result (filterable by why each waits) on the left; on the right the result, the markets it settles, and every other market graded outcome by outcome. The result is entered in the sport's own terms rather than one home–away pair: football at full time with extra time and penalties, hockey by regulation plus overtime or shootout, points sports by the final (and regulation if it went to overtime), tennis as games per set with retirement or walkover, sets, frames or maps for darts, snooker, table tennis, volleyball, badminton and esports, and the winner for combat sports. A sport with no defined shape is settled market by market, never guessed from a bare score. Markets without a line settle from the result on their own; totals and handicaps are suggested and staff accept them, because feeds fold different units into one market type. A retired or walked-over tennis match settles by a rule chosen at settle time (standard, or void everything), overridable per market. The preview and the settle share one plan, so the confirm shows exactly what is paid to winners and the house result, using the same payout cap settlement uses.
Outrights in manual resolve. A league-long outright (Premier League winner, top four, relegation, top goalscorer) has no score, so it has no result form: each open market lists its participants and staff mark the winners. The first pick on a market marks everyone else lost (eliminated participants included, and still changeable); several winners are fine, and Void refunds a participant's bets. An outright joins the queue once its end date has passed; one with no end date, or one still running, joins when staff press Mark ended on its row in the Events page (the mark is staff's own and a later feed update of the end date does not undo it). It goes through the same settlement as a feed grade, so re-sending the same grades pays nothing twice. Settling its markets never closes the outright, since it may hold markets nobody has bet on; staff close it from the queue once nothing is open.
Automatic clean-ups keep the catalog honest. An hourly job refunds open bets on events that still have no result 72 hours after kickoff and ends those events. And a game (from either feed) more than 12 hours past kickoff with no result and no open bets is simply ended, so postponed or unreported fixtures do not sit in the catalog forever; games that do carry open bets stay with settlement and the refund job. For LSports, a live match that has been quiet on every stream for an hour and over four hours past kickoff is ended the same way. The clean-up never re-voids a market that already has a result.
Trading controls
Stake and payout ceilings. Three kinds of operator limit — maximum single stake, maximum stake per rolling 24 hours, and maximum payout — are configurable by scope: globally, per sport, per competition, per market type, or per player. The resolver picks the most specific limit that applies, but never lets it exceed the lowest operator-wide ceiling matching the bet, so a per-player override can tighten a limit but not lift it above the book's own. For a multi it takes the strictest limit across the legs. There is a preview tool in the backoffice so you can see what a limit would do before enabling it, and a Stake limits screen (Betting) lists every limit with an enable toggle and an amount editor, and adds global or per-player limits. A second enabled limit for the same kind, scope and currency is refused with a clear message.
Limits set once, in US dollars. A limit set in USD applies to every currency: a bet in another currency is held to it converted at the current rate, unless you have set a limit in that currency for the same kind and scope, which then wins. If no exchange rate is available the bet is refused rather than let through unlimited. The rolling 24-hour cap adds up the player's sportsbook stakes across every currency they hold, so holding several wallets does not multiply it. The payout cap in force at placement is frozen on the bet: settlement pays up to that figure even if the rate has moved since, so a winner is never paid less than they were promised. Boosted returns are capped by the same figure.
Provider-staleness protection. Each odds provider carries a configurable staleness threshold and a heartbeat that only advances on a fully clean ingest run. If the feed goes quiet past the threshold, its events and markets surface as suspended at read time — no database write is needed, so the guard holds even if every background job is down. This is fail-closed by design: a dead feed stops being bettable rather than leaving stale prices live. For LSports, a keepalive reconciler also compares the feed's list of active fixtures with the catalog: a fixture we missed is re-pulled from a snapshot, a live match that has dropped off the list is re-checked, and mismatches are exposed as metrics. It never writes an event status itself and alerts, rather than reopens, a match already ended here.
Live coverage window. For LSports, a live match the provider stops listing as live keeps its odds only for a grace period (90 seconds by default) before its markets are taken off the board. Staff can edit each provider's feed-silence limit and this window from the provider's page. Event status only moves forward (a finished or closed match is never put back to live), a finished match's status comes from the live score stream, and finished or settled markets read as suspended.
Stuck-event queue. The backoffice lists events that settlement could not resolve (six or more hours past kickoff and still not ended). Staff can mark them ended or closed — gated on all their bets already being settled — or void them in bulk: select any number (select-all, shift-click ranges, search and competition filters), give a reason, and every open market on those events is refunded and the events closed. The void runs in small batches with a progress bar and a stop button, each market void is its own settlement, and a partial failure is reported per event and safe to re-run. Manual event status changes are audited. The backoffice event list can also be filtered by sport.
Feeds
| Source | Role |
|---|---|
| LSports | A push feed for live in-play betting. The ingester process consumes the message stream — fixtures, live scores, market updates, settlement, heartbeats — and normalizes it into the catalog. |
| The Odds API | A polled feed covering pre-match and in-play odds (match result and totals), live scores and final results, which drive automatic settlement. |
| TheSportsDB / API-Football | Artwork only: team crests, league badges, competitor nationalities. Backfilled by scheduled sweeps, not on the hot path. |
Every source normalizes to the same catalog shape, so swapping or adding a supplier does not touch the bet engine.
One active provider, switched from the backoffice. Staff can add any supported feed (LSports, The Odds API) as a betting provider from the backoffice. A new provider is created switched off, and the page flags one whose feed is not running. Activating a provider switches the others off; before activating one that has never synced, or whose feed is not running, the confirm dialog warns that the sportsbook will show nothing from it until its feed delivers data. Both feeds follow the switch without a restart: an inactive feed stops writing fixtures and odds but still settles the bets already placed on it, and LSports re-seeds its catalog from a full snapshot when it becomes active again. The feed is also recorded raw for 120 days for diagnosis and replay.
How The Odds API poller spends its quota. The Odds API is a pay-per-request service with no push stream and no live-status or suspension signal, so the poller infers them: a game goes live at kickoff, a market that stops being quoted in play is suspended, and a game that drops out of live scores is ended. It runs inside the same ingester process as LSports and sets its pace from two inputs:
- Viewers. While anyone has the sportsbook open, live odds refresh every 40 seconds (the provider's own in-play refresh rate) and live scores every minute. With nobody watching, both slow to every 5 minutes, and live markets are held suspended between those slow polls rather than left open on stale prices.
- Remaining credits. Below 3,000 credits the poller stays on the slow cadence; below 500 it stops fetching odds and scores and keeps only the catalog refresh and settlement going.
Pre-match odds for the configured live sports refresh hourly for the next 48 hours; settlement runs every 2 hours with an hourly backstop. An optional background tier fills the catalog for every other sport the provider lists: pre-match match-result odds and results only, twice a day, with no in-play betting. The default cadence is sized for the provider's 20,000-credit monthly plan. Because its slowest healthy poll is 5 minutes apart, the poller sets its provider's staleness threshold to 15 minutes.
There is also a dev-only match simulator that fabricates a realistic live feed — kickoff, ticking scores, drifting odds, goal-triggered suspension, feed heartbeats — and drives it through the real ingestion path. It is useful for training staff and demonstrating live betting without a paid feed subscription. It is doubly gated off in production and never settles or moves money.
Scope & Roadmap
Implemented. Full catalog with operator-controlled visibility and ordering; lobby board views including activity-ranked Trending, with staff-ordered navigation and constructed lobby pages; following leagues and competitors; the market catalog; quick bet; same-game combos; cash-out in full or part; early loss settlement of multis; held-bet review; live in-play betting from either feed (LSports needs a contract and credentials) with streaming price updates; singles and accumulators; price-change protection with per-player policy; score-based settlement including push and quarter-line splits, run automatically by The Odds API poller; void handling with multi recomputation; operator stake, 24-hour and payout limits by scope with a USD base and a per-bet frozen payout cap; fail-closed staleness protection; manual resolve by sport-specific score shapes, with manual results and staff voids protected from the feed; the stake limits screen; accumulator and price boosts as audience-checked benefits with several ladders per operator; per-currency minimum stakes; two-part My Bets tickets; manual void; the stuck-event queue with bulk void; adding and switching data providers from the backoffice without a restart; closing prices on finished matches.
Partial. Markets cover match result, totals and handicaps; from The Odds API only match result and totals are fetched (background-tier sports: match result only), so handicaps need LSports. The Odds API's in-play prices refresh at most every 40 seconds, and only while someone is watching — slower than a push feed. One bookmaker's prices are used from within a feed — there is no line-shopping, no consensus pricing, and no margin-stripping to build your own book from the feed's fair probabilities.
Not built. Player-prop markets (first goalscorer, shots, cards) arrive from LSports as generic markets, hidden until staff review them, and are settled by hand or from the feed's own results; combos exclude them. Outright (futures) markets are not offered. Cash-out does not cover same-game combos or boosted bets. There is no in-house risk or liability management beyond stake and payout ceilings, no multi-provider failover (one provider is active at a time), and no managed-trading integration. Following has no notifications.