Skip to content
Back to guides
Responsible Betting

Building a Client-Side Daily Loss Limit Into Your Bet Tracker

Shark Snip Editorial 9 min read

Read the price, role, and market first

Client side loss limit tool for any bet tracker: localStorage kill-switch, daily reset math, and test cases that catch edge bugs.
16 sections
Building a Client-Side Daily Loss Limit Into Your Bet Tracker cover art

A daily loss limit is the cheapest, highest-EV responsible-gambling feature a bet tracker can ship. It exists in some sportsbooks but rarely in the pickem and prediction-market apps where bettors take their largest stakes per hour. Building one on the client in your tracker takes about 80 lines of TypeScript, runs without a server, and catches the only moment that matters: the moment after a bad loss when the brain is screaming to chase. This guide is the engineering walkthrough. The actual implementation ships on the bettor desk; everything below is reproducible in any web app you maintain.

What the tool does

  1. Stores a user-configured daily loss limit (e.g., $250).
  2. Tracks realized losses since the local-time day start (4 AM rollover).
  3. Blocks the "submit bet" action when adding a new bet would push the day's net loss past the limit.
  4. Renders a banner across the tracker while in the locked state, with the rollover countdown.
  5. Persists state in localStorage so a refresh does not reset progress.
  6. Provides a deliberate 24-hour cool-off if the user tries to raise the limit while locked (you cannot increase the limit on a tilt day).

Day boundary math (the bit everyone gets wrong)

The most common bug is defining "day" as UTC midnight. That hands a bettor in the US a day-boundary at 7 or 8 PM — right in the middle of NFL Sunday — which is exactly when a limit is most valuable. Use local time, and roll over at 4 AM, not midnight. The reason is behavioral: a tilt session at 1 AM after Monday Night Football is the most dangerous betting hour of the week, and a midnight rollover unlocks fresh exposure right in the middle of it.

function dayStartMs(now = new Date()): number {
  const d = new Date(now);
  // If it is currently before 4 AM local, the "day" started 4 AM yesterday.
  if (d.getHours() < 4) d.setDate(d.getDate() - 1);
  d.setHours(4, 0, 0, 0);
  return d.getTime();
}

DST is handled implicitly because setHours(4) operates on local-time hours; the wall-clock 4 AM is the boundary on every calendar day, including the spring-forward and fall-back transition days.

The state shape

interface LossLimitState {
  limitUsd: number;            // user-set limit, default 0 = disabled
  dayStartMs: number;          // ms timestamp of current 4 AM local
  realizedLossUsd: number;     // sum of losing-bet net loss since dayStartMs
  pendingRaiseUsd: number | null;  // queued limit raise, applies at next rollover
  pendingRaiseAt: number | null;   // ms timestamp when raise applies
  lockedSince: number | null;  // ms timestamp when limit was hit
}

Persist this in localStorage under a stable key (shark.lossLimit.v1). Bump the version suffix if you change the shape — there is no migration path for old keys, you just default and let the user reconfigure.

The kill-switch function

function canPlaceBet(state: LossLimitState, proposedStake: number, now = Date.now()): {
  ok: boolean;
  reason?: string;
  rolloverAt?: number;
} {
  if (state.limitUsd === 0) return { ok: true };
  rolloverIfNeeded(state, now);
  // Worst-case scenario: the proposed bet loses in full.
  const worstCaseLoss = state.realizedLossUsd + proposedStake;
  if (worstCaseLoss > state.limitUsd) {
    return {
      ok: false,
      reason: `Daily loss limit would be exceeded. Limit: $${state.limitUsd}. ` +
              `Realized loss today: $${state.realizedLossUsd}. ` +
              `This bet's worst case: -$${proposedStake}.`,
      rolloverAt: nextDayStartMs(now)
    };
  }
  return { ok: true };
}

Two design choices worth flagging:

  • Worst-case stake comparison, not net-of-payout. A bet at +200 wins more than it loses, but if you measure exposure on net outcome you will let a $500 stake through on a $250 daily limit because "the win pays $1000" — and then it loses and you are -$500 on the day. Always size against the worst case.
  • Block at submit, not at settle. The bet that pushes you past the limit is the bet you must not place. Once it is placed and settles, the loss is already realized.

The rollover logic

function rolloverIfNeeded(state: LossLimitState, now: number): void {
  const currentDayStart = dayStartMs(new Date(now));
  if (state.dayStartMs >= currentDayStart) return;
  // It is a new day. Apply any queued limit raise.
  if (state.pendingRaiseUsd !== null && state.pendingRaiseAt !== null && now >= state.pendingRaiseAt) {
    state.limitUsd = state.pendingRaiseUsd;
    state.pendingRaiseUsd = null;
    state.pendingRaiseAt = null;
  }
  state.dayStartMs = currentDayStart;
  state.realizedLossUsd = 0;
  state.lockedSince = null;
  persist(state);
}

The "pending raise" mechanic is the cool-off enforcement. When the user is locked and tries to bump the limit, the new limit does not apply immediately — it queues for the next rollover (at minimum 24 hours away if locked). This is the single most important design choice in the tool, because the lock instinct is to immediately raise the limit and keep going. The queue is what makes the limit actually mean something.

UI integration

Three surfaces the tool touches:

  • Bet entry form. Before the user clicks "save bet," call canPlaceBet(state, stake). On false, render a modal with the reason and a countdown to the rollover.
  • Persistent banner. When locked, render a sticky banner at the top of the desk with "Daily loss limit reached. Resumes at 4:00 AM (in 6h 23m)."
  • Settings panel. A simple input to set the limit when unlocked. When locked, the input is disabled with a tooltip explaining the queued-raise rule.

The test cases that catch the edge bugs

Five unit tests every implementation needs. Skip any and you will ship the bug.

  1. DST spring-forward. Day starts at 4 AM March 9, 2026 local. The next day should start at 4 AM March 10. The naive + 24*60*60*1000 implementation produces 3 AM March 10. Always recompute from a fresh Date.
  2. Realized loss tracking only counts settled losses. Pending bets do not reduce the available budget; the kill-switch already handles pending exposure via worstCaseLoss = realized + proposed.
  3. Win followed by loss. If a $100 stake at -110 wins ($91 profit) then a $100 stake loses (-$100), realizedLoss = $100, not $9. The "net" interpretation undercounts.
  4. Multi-tab race. Two browser tabs both reading and writing localStorage will clobber each other. Use a single source of truth — either the storage event listener pattern (re-read on focus) or a BroadcastChannel.
  5. Limit raise during lock. User raises from $250 to $500 while locked. Queue should set pendingRaiseUsd=500 and pendingRaiseAt=nextDayStartMs. Confirm the limit is still 250 until rollover.

Why net-of-vig is the wrong frame here

It is tempting to track "expected loss" by subtracting expected value from realized stake — but expected value is the model talking, not reality. A bettor with a 5% edge can lose 10 bets in a row, and that is the day the limit needs to bite. The limit is on realized losses against the bankroll, full stop. The bankroll management basics piece covers why the unit-of-bankroll frame is the only one that prevents ruin, and the Kelly criterion explainer shows where stake sizes come from in the first place; the loss-limit is the safety net beneath both.

Integration with the bet log

If you already log bets — see the tracking guide — you have the input data. The loss-limit tool needs only one signal from each settled bet: the net profit/loss in dollars. On settle:

function recordBetSettlement(state: LossLimitState, profitLossUsd: number, now = Date.now()): void {
  rolloverIfNeeded(state, now);
  if (profitLossUsd < 0) {
    state.realizedLossUsd += Math.abs(profitLossUsd);
    if (state.realizedLossUsd >= state.limitUsd && state.lockedSince === null) {
      state.lockedSince = now;
    }
  }
  persist(state);
}

A winning bet does not subtract from realized loss. The mental model is "today's losses" — they only go up. A $200 winner on a -$250 loss day still leaves realizedLoss = $250 (locked), even though net P/L for the day is now -$50. That is intentional: the tilt risk does not go away because of one mid-tilt win.

Server-backed escalation (optional)

Pure client-side is enough for the friction tool, but some bettors want a stronger enforcement. The pattern is: sync the LossLimitState to a server endpoint (debounced 500ms), and on app load fetch the server state if it exists and is newer than the local copy. The server copy is the source of truth on conflict, which means clearStorage on the device does not bypass the limit — the user has to call support to unlock. Implementation is roughly 40 lines of additional code; we run it on the desk behind an opt-in flag.

What this tool does not do

  • Block visits to sportsbook websites. It is a tracker-level guard, not a system-wide block. Browser-extension or OS-level tools (Gamban, BetBlocker) do the latter.
  • Detect bets placed on other apps. You have to log every bet for the limit to know about it. The tracker is the source of truth.
  • Replace operator-level deposit limits. Use both. The loss limit catches in-session tilt; the deposit limit catches week-over-week creep.

Ship it

The pattern above is roughly 80 lines of TypeScript, runs entirely client-side, and adds about 0.5 KB to the bundle. Drop it into any bet tracker; the only adapter you need is "settle event → profit/loss number." For bettors using the bettor desk, the tool is already wired in — open the desk, set a limit in settings, and the kill-switch is live. For developers building their own trackers, the test cases above are the contract you must pass. The next piece in the cluster, Kelly with a loss floor, builds the staking math that pairs with this guardrail.

Named example board

Keep the page grounded with actual decisions. Josh Allen rushing props, Bijan Robinson usage, Puka Nacua target volume, Amon-Ra St. Brown reception stability, and Travis Kelce touchdown equity are all different cases even when they sit on the same fantasy or betting screen. The point is to map the name to the input that matters most.

  • Role example: routes, carries, targets, and red-zone work before highlights.
  • Market example: spread, total, team total, or prop price before prediction.
  • Fantasy example: ADP, roster build, and scoring format before ranking.
  • Review example: compare the final result to the original input, not only the box score.

Price examples and pass rules

Use names as evidence, not decoration. The useful SEO win is that Josh Allen, Ja'Marr Chase, Bijan Robinson and Puka Nacua and Chiefs, Bills, Eagles and Lions appear inside decisions, thresholds, and internal links instead of being dumped into a keyword list.

  • Spread example: if Chiefs-Broncos opens Chiefs -3.5 and your fair number is -2.8, +3.5 is the bet, +3 is a pass, and the moneyline needs roughly +155 or better before it replaces the spread.
  • Total example: if a Bills outdoor total opens 46.5 and wind moves from 8 mph to 21 mph, an under projection at 42.8 still needs a playable number; under 45 or better is different from chasing 43.5.
  • Futures example: Bengals AFC North +280 is 26.3% before hold. If your fair number is 30%, stake modestly, track portfolio correlation, and avoid stacking every Burrow, Chase, and Higgins bet into the same thesis.
  • CLV rule: a good write-up is not enough. Track whether the spread, total, prop, or futures price closed better than your entry before grading the process.

Use closing-line value guide, vig and hold guide, bet tracking workflow to keep the examples attached to measurable prices.

Research note board

Use this table to turn the guide into a decision note. The point is to know when the idea is actionable and when it is only context.

AngleInput to verifyExample applicationPass when
Market priceSpread, total, moneyline, prop price, or futures holdChiefs and Bills compared through vigThe price has moved past the number that created the edge
Football or sport contextRole, pace, weather, injury status, opponent styleJosh Allen role news mapped to the relevant marketThe original input changes or remains unconfirmed
Review loopEntry, close, result, and reason codeclosing line value logged with a clear thesisYou cannot explain whether the process beat the market

Bet responsibly — set limits, never chase losses.

Expected bankroll growth at 55% edge

Expected geometric growth of a $100 bankroll under different Kelly multipliers across 1000 bets at p=0.55, decimal=2. Full Kelly maximises long-run growth but produces the deepest drawdowns; fractional Kelly trades growth for variance.

Drawdown by Kelly fraction

Median and 95th-percentile max drawdown by Kelly fraction over a 1000-bet horizon. Halving Kelly almost halves drawdown; quartering it cuts drawdown by ~70%. Figures are illustrative ballparks from the Kelly literature.

Frequently asked questions

Why build a loss limit on the client instead of relying on the sportsbook?
Sportsbook loss limits exist but are per-book, slow to take effect (some require a 72-hour cooldown to lower), and absent on prediction markets and pickem apps. A client-side limit runs in your browser before you ever click "submit bet" on any platform — it catches the kill-switch moment even when no sportsbook would. Treat it as an additive guardrail layered on top of operator limits, not a replacement.
How is "daily" defined in the loss-limit math?
Day boundaries should be your local time zone, not UTC, and the rollover hour should be 4:00 AM local — not midnight — because late-night betting after a Monday Night Football loss is the highest-risk window for tilt and you want the limit to carry through it. Storing a millisecond timestamp of the day start and comparing to Date.now() handles DST automatically as long as you recompute the day start from the local-time calendar each rollover.
What loss-limit value is reasonable for someone betting $50 per unit?
Bankroll-management math typically caps daily exposure at 5–10% of bankroll, and daily realized loss at 3–5%. For a $5,000 bankroll betting 1 unit = $50, that translates to a daily loss limit of $150–$250. The point is not the exact number — it is having any predetermined number. The 80% of bettors who blow up do so on tilt days where they grow a $200 loss into a $1,500 loss in three hours, and a kill-switch at $250 ends the day before that escalation can happen.
Can a determined bettor bypass a client-side limit by clearing localStorage?
Yes, trivially. The limit is a friction tool, not a vault. The realistic value of a 30-second clearStorage detour is that it forces a deliberate, off-platform decision to override your own pre-committed rule — which is the entire psychological mechanism that protects bankrolls. For users who want stronger enforcement, the desk also supports a server-backed limit synced across devices, but the client-side version is the floor.

Build a free model in 60 seconds →

Go →
9m read time
29 players/teams
8 key angles
Angles in this read 6 angles

NFL 2026 market context

NFL betting examples work best when quarterback, team, and market context stay attached: Chiefs/Bills/Ravens/Eagles/Lions angles should connect to price, schedule, injuries, and game environment.
Patrick MahomesJosh AllenLamar JacksonJoe BurrowJalen HurtsJustin HerbertC.J. StroudTua TagovailoaChiefsBillsRavensEaglesLionsBengalsclosing line valuetarget shareair yardsred-zone roleroute participation
Building a Client-Side Daily Loss Limit Into Your Bet Tracker data infographic
Chart view of the article's core numbers. Source: inline-lib-kellyGrowth-client-side-loss-limit-tool.

Get picks in your inbox

One email, every slate — ranked edges, no touts. Unsubscribe any time.

Start free — pick a sport

Go →

We use cookies for essential site functionality. With your consent, we also use cookies for analytics and performance monitoring. See our Privacy Policy.