Strategy & Process
Universal principles + sport-specific application
Bet Tracker β how the system works
Current as of 2026-09-17. This file describes the system as it is today. History, superseded methods, incident write-ups and the reasoning behind past decisions live in CHANGELOG.md.
A daily handicapping report analyses the slate, ranks 3-5 plays by assessed win probability, and publishes to bet-tracker-9ck.pages.dev. Mitch picks one and stakes 12.5% of bankroll.
1. Daily rhythm
| Time (ET) | What happens |
|---|---|
| 11:10 | Scheduled task "Daily Bet Report" fires (cron 0 11 * * *, +10 min jitter). Seven days a week. |
| +25 min | Minimum research time. Section 3 may not be written before this. |
| 11:50 | Hard cutoff. Stop researching, build. |
| 12:00 | Windows Task Scheduler runs deploy.bat β pushes site/dist to GitHub; Cloudflare Pages publishes. |
Build time is no longer a risk: archiving pre-reset reports took a full build from ~178 seconds to ~10 seconds (2026-09-17), which is why the cutoff moved from 11:45 to 11:50.
Reports carry Start: and End: timestamps in the header instead of a "Generated" line, so run length is visible. Added 2026-09-18 after several runs finished suspiciously fast with thin analysis.
The 25-minute floor exists because early completion signals shallow work, not a simple slate. The prompt says so explicitly and lists work minimums: at least 8 games analysed, each with confirmed starters/goalie/QB plus at least one Tier-1 factor carrying a real number β or an explicit "could not obtain". Counts of games analysed and sources fetched go in Research Limitations.
2. The method
Six steps, in order. The daily prompt holds the machinery; sport specifics live in playbooks.
- Read the playbooks for each in-season sport.
- Verify β ground truth from the league schedule API, then confirm every other source matches today's actual participants.
- Analyse 6-8 games in prose on the playbook's factors.
- Price β DraftKings primary, bet365 shopped where both are readable. Line movement is evidence.
- Calibrate β subtract the measured overconfidence discount, then show both raw and calibrated confidence.
- Rank by calibrated confidence, highest first.
Selection is by assessed win probability, not by edge. Edge % and EV are computed and displayed as labels β a negative-edge play can still be recommended, shown honestly. This is deliberate: Mitch wants hit-rate value and a daily card, and gating on edge produced 1-3 plays a week.
The tradeoff, recorded once: injuries, form, rest and matchups are already in the price. Fundamentals-first analysis will usually agree with the market. See Β§9 β this is an experiment with a scheduled review, not a settled method.
What actually moves the needle
Markets price team quality, records and star power efficiently. Value lives in what they absorb slowly: overnight usage, late lineup and injury news, weather that changed after the line was set, and metrics that predict better than the ones the public watches. If a pick's whole case is season records and "they're the better team", it has restated the line β the prompt requires saying so.
3. Playbooks
playbooks/mlb.md Β· nhl.md Β· nba.md Β· nfl.md β plus _TEMPLATE.md holding the structure and research questions.
MLB and NFL are built (v1, research-based). NHL and NBA are stubs marked "NOT YET BUILT" β the prompt prefers a sport with a real playbook and must disclose when it uses a stub. Build NHL and NBA before their October openers.
Both built playbooks found the same shape of error: the number everyone watches isn't the one that predicts. - MLB β short-sample ERA is noise; xFIP/SIERA predict. The ERAβxFIP gap is the signal, and it points opposite to intuition. - NFL β win-loss record is noise; EPA/play differential predicts. Fumble recoveries are ~random (1.6% of second-half variance), so a record built on turnover margin is a fade, not a buy.
Expect the same when NHL and NBA get built β look for the descriptive-vs-predictive split first.
Conventions: - Hard Rules are binding; the prompt's self-check audits them. - Every claim carries an evidence tag: [STRONG] quantified in peer-reviewed/primary-data work Β· [MODERATE] documented but single-source or small sample Β· [WEAK] plausible, unquantified, tiebreaker only. Reports must name which tag an argument rests on. - Sources split into verified working vs β not yet verified. A failing unverified source gets reported, never replaced with a guess. - Each ends with an Evidence Ledger β claim, strength, open question.
Edit these in chat; they're the per-sport configuration layer. Shared rules stay in the prompt so they can't drift between sports.
Scope: MLB Β· NHL Β· NBA Β· NFL only. NCAAF and NCAAB were dropped 2026-09-16.
4. Books, markets, odds, staking
Books: DraftKings only, for reporting purposes. All prices come from the ESPN core API. bet365 stays open for the profit boosts Mitch wraps SGPs around, and he can shop a play himself if he wants β but the report no longer prices it.
VegasInsider was dropped 2026-09-18. It was unreliable day after day and served exactly one purpose: bet365 moneyline shopping. Edge % never needed it β Edge = calibrated confidence β DraftKings' implied probability, straight off the quoted price. Multi-book consensus was only required by the market-inefficiency approach retired on 09-16.
β ESPN exposes only one provider. Verified 2026-09-18: count: 1 (DraftKings) even at limit=50. There is no second book available from the reliable feed, so genuine line shopping isn't possible through automation right now.
Markets: Moneyline Β· Spread Β· Over/Under. Written exactly β a run line or puck line is Spread. These strings go verbatim into the tracker's Leg Market column. No props (dropped 2026-09-16: unreliable source, heavy research load).
Odds band: decimal 1.50 β 2.10. Below 1.50 the payout doesn't justify the risk. Above 2.10, win-probability estimates historically collapsed (5 wins from 16 picks). Revisit at 100+ graded picks.
Calibration discount β subtract from every confidence estimate:
| Odds | Subtract |
|---|---|
| < 1.80 | 3 points |
| 1.80 β 2.10 | 4 points |
β Measured on a mostly-prop sample. Little calibration history on game lines β this is a starting assumption. See Β§9.
Staking: 12.5% of bankroll, one play per day, chosen by Mitch from the ranked list. The report never chooses or sizes. Straight singles only β Mitch builds boost SGPs himself afterwards.
5. The tracker β bet_tracker.xlsx
Sheets: Dashboard Β· Log Β· BR Adjustments Β· Strategy Β· Picks.
Log columns: Bet ID | Date | Category | Type | Stake | Outcome | P&L | Closing BR | Notes | Leg Market (J) | Leg Result (K) | Sport (L)
Sport (column L) added 2026-09-20 to drive the Record by Sport table. Values: MLB / NFL / NBA / NHL. Capture it on every bet as it's logged β a blank lands the row in an "Unlabelled" bucket on the Statistics page, which is deliberately visible rather than hidden, because a blank means the logging step was missed.
Main-leg scoring
Mitch often wraps the recommended play in an SGP with filler legs purely to unlock a bet365 boost. The record is scored on the anchor leg (Leg Result), not on whether the ticket cashed.
If the anchor hit but a filler leg sank the ticket, that is a Win. Outcome and P&L describe what the ticket did. Hit rate and P&L are therefore allowed to disagree β a row with result = Win and negative pnl is correct, not an error to fix.
Expect visible divergence: filler legs run ~90%+ each, so two of them leave ~81% ticket survival on a winning anchor.
Record definition
Log rows dated 2026-09-14 or later, Category = Main only. Excludes Fun Parlay, World Cup, Side Bet, Adjustment.
Picks sheet
Date | Sport | Rank | Pick | Est Odds | Stated Hit % | Result | Bet Placed
Every report play is graded here, whether or not it was bet. This is the scoreboard for Β§9 and the reason the sheet survived the reset. From 2026-09-16 onward Stated Hit % holds the calibrated figure, not the raw one β any analysis spanning that date must split there.
6. The website
Pages: Dashboard Β· Reports Β· Monthly Reviews Β· Strategy (renders this file) Β· Bet History Β· Statistics.
Clean reset, 2026-09-14 β display-layer only. build.py holds RESET_DATE = 2026-09-14 and RESET_BANKROLL = 48.26; _live_rows() filters every site-facing loader. Every pre-reset row remains in the xlsx. Don't "clean up" the spreadsheet to match the site β the xlsx is the record, the site is a view.
The old era ended at $51.60; the new one opens at $48.26. The $3.34 gap is deliberately unreconciled β two separate books, by decision.
Statistics page: Money Flow Β· Overall Record (main-leg scored) Β· Bet Type Analysis (Moneyline / Spread / Over-Under / Player Prop) Β· Record by Sport (MLB / NFL / NBA / NHL) Β· All Bet Categories (Main Bets / Side Bets) Β· Monthly Breakdown.
Bet Type Analysis and Record by Sport share a basis: main bets only, scored on the main leg, current era. Both always render all their rows even at 0-0, so a table reads as "nothing yet" rather than "broken".
load_era_breakdown() and load_pick_rank_accuracy() still exist in build.py but are not called β removed from display, data still collected. To restore a section, call the loader and pass it to the template.
Reports: only current-era reports render. Pre-2026-09-14 reports live in reports/archive/ β on disk, out of the build. load_reports() globs non-recursively, so archiving a file removes it from the site. clean_dist() clears dist/reports/*.html each build so archived pages don't linger live.
Deploy pipeline
build.py β site/dist/ β deploy.bat (git add/commit/push) β github.com/four0132/bet-tracker β Cloudflare Pages β bet-tracker-9ck.pages.dev
The git repo is at site/dist/, not the project root. Cloudflare runs no build β it serves the repo as static files.
Scripts:
- rebuild_and_deploy.bat β build and publish, output on screen, clears stale git locks. Use this for manual runs.
- deploy.bat β publish only; writes to deploy.log, so failures are silent. This is what Task Scheduler runs.
- set_deploy_time.bat β shows the Windows publish schedule, offers to set it to daily 12:00.
- check_github_login.bat β read-only; shows where the GitHub credential is stored.
- save_github_token.bat β paste a new token at renewal.
GitHub credentials
Remote URL is clean (no embedded token). The token lives in Windows Credential Manager, encrypted, via Git Credential Manager. Active token: bet-tracker-deploy-v2, fine-grained, four0132/bet-tracker only, Contents read/write.
π΄ EXPIRES 2027-09-13. When it does, the unattended deploy fails silently (deploy.bat writes to a log, not the screen). Three warnings exist: a scheduled reminder on 2027-08-13, a countdown in rebuild_and_deploy.bat (update TOKEN_EXPIRY at the top when renewing), and this note.
Don't repeat these dead ends: GCM's browser sign-in doesn't work on this machine (fails with connection was closed, drops to a console prompt). Feeding a token to git credential approve from a batch file fails β use the PowerShell script. Never clear the stored credential before the replacement is proven.
7. Verified sources
Work reliably:
- https://statsapi.mlb.com/api/v1/schedule?sportId=1&date=YYYY-MM-DD&hydrate=probablePitcher β schedule, times, confirmed starters
- https://statsapi.mlb.com/api/v1/schedule?...&fields=dates,date,games,gamePk,status,detailedState,teams,away,home,team,name,score,isWinner β compact finals for grading
- https://sports.core.api.espn.com/v2/sports/{path}/events?dates=YYYYMMDD β event IDs
- https://sports.core.api.espn.com/v2/sports/{path}/events/{id}/competitions/{id}/odds β DraftKings moneyline, spread and total, decimal, open vs current, never truncates
- https://www.vegasinsider.com/{sport}/odds/las-vegas/ β 8-book moneyline board incl. bet365 and DraftKings
Tested and rejected: ScoresAndOdds and OddsChecker (no bet365 odds in fetchable HTML) Β· VegasInsider /odds/run-line/ (empty body, JS-rendered) Β· the in-app browser on VegasInsider (blocked).
β TRUNCATION IS NOT A FAILURE
When web_fetch reports "result exceeds maximum allowed tokens", the full content was saved to a file and the path is in the message. Grep it or Read with offset/limit. Every past claim that a market "couldn't be priced due to truncation" was false β this silently destroyed two consecutive cards. "Truncation" is banned as a research limitation. Wide odds rows make Grep print [Omitted long matching line] β that means it matched; read that line number.
Freshness check
Always confirm an odds source's contents match today's participants before pricing. On 2026-09-15 a report recommended five pitchers who weren't playing, from a board serving the previous day. A partial match is still a fail.
8. Scheduled tasks
| Task | When | Purpose |
|---|---|---|
| Daily Bet Report | 0 11 * * * (fires 11:10) |
The daily card |
| Monthly Bet Strategy Review | 0 9 1 * * |
Scoreboard check, playbook compliance, source audit, methodology proposals. Read-only β changes nothing without approval. Rewritten 2026-09-17. |
| GitHub Token Renewal Reminder | once, 2027-08-13 | Token expiry warning |
Deleted 2026-09-16: daily-bet-report-saturday, daily-bet-report-weekend (folded into the daily task). Their SKILL.md files remain under C:\Users\mitch\Documents\Claude\Scheduled\ if the prompts are ever wanted.
9. Open items β the watch list
π΄ CALIBRATION REVIEW β the important one. Hit rate is the stated objective, so realized vs stated hit rate measures whether fundamentals-first handicapping works at all. Review at ~30-50 graded plays (roughly 4-6 weeks from 2026-09-16). Three outcomes:
- Realized β calibrated and P&L positive β it works, continue.
- Hit rate good, P&L negative β winning at prices too short to profit. Fix the odds floor, not the method.
- Realized well below calibrated β fundamentals-first isn't beating the market here, and that conversation happens with data instead of theory.
Don't skip this. It's the whole reason the Picks sheet survived the reset. The monthly review now runs it automatically β but Oct 1 will have roughly 15 graded plays, which is too few to conclude anything. Expect the first genuinely informative review on Nov 1. Treat October's as a dry run of the machinery.
Game-line calibration is unmeasured. The 3/4-point discounts came from a prop-heavy sample. Re-measure by market once ~30 game-line picks are graded; spreads and totals may calibrate differently.
π΄ The MLB pitcher metric is weaker than the playbook was built to assume. xFIP and SIERA β the two estimators research says actually predict β cannot be fetched. FanGraphs was tested twice (2026-09-19 and 09-20): its leaderboard blows the fetch ceiling at every page size, pageitems returns an empty body, and the saved file is single-line JSON that bash can't slice because tool-results sits outside the sandbox mounts.
Substituted: xERA from Baseball Savant (/leaderboard/expected_statistics?type=pitcher&year=2026&min=q&csv=true) β verified working, line-delimited CSV, greppable by surname, with era_minus_xera_diff precomputed.
But be clear about what that costs. The predictive hierarchy is SIERA > xFIP > xERA β FIP, and the playbook elsewhere says FIP adds nothing over ERA. So Β§2 now runs on an estimator roughly level with the one it was written to reject. It is still the right call β it's fetchable, precomputed, and directly answers "does this ERA overstate current skill" β but the playbook now: widens the flag threshold to 1.00/1.75, tags the signal [MODERATE] not [STRONG], and forbids carrying a play on the xERA gap alone.
Open question for the monthly review: do plays citing the ERAβxERA gap beat plays that don't? If not, Β§2 is decoration.
β WIND β SOLVED 2026-09-21. Outdoor totals are playable again in both sports.
MET Norway β free, no key, verified working:
https://api.met.no/weatherapi/locationforecast/2.0/compact?lat={LAT}&lon={LON}
Hourly timeseries ~3 days out, so the 11:10 run reaches an evening first pitch or an 8:20 PM kickoff. Returns wind_speed in m/s (Γ 2.237 for mph) and wind_from_direction in degrees.
- NFL needs speed only β H2 and H3 are pure speed rules, no bearing table required. 15 mph = 6.7 m/s; 20 mph = 8.9 m/s.
- MLB needs direction at Wrigley. Centre field sits at ~31Β° (NNE), so wind from ~200β230Β° blows out (the 10.95-run park) and wind from ~20β60Β° blows in (7.72 runs). Only Wrigley has a verified bearing and only Wrigley has the evidence β for other parks use speed alone and say so rather than guessing an orientation.
- statsapi remains the confirmation when it populates near first pitch, giving out/in directly (
"7 mph, Out To CF"). If both are available and disagree, prefer statsapi β observed beats forecast.
Tested and dead, don't retry: api.weather.gov (empty), api.open-meteo.com (empty, tested four ways across two days), wttr.in (timeout, inconclusive).
π‘ NFL injury status β partial. Tested 2026-09-21.
site.api.espn.com/apis/site/v2/sports/football/nfl/summary?event={id} works and carries an injuries block with status, date and player name. It's ~80k chars over 5 very long lines β Read can't chunk lines that long, so use Grep: "status":"(Out|Doubtful|Questionable)".{0,220} returns each entry cleanly.
But the data is thin. On the Week 3 event tested, every entry was Questionable and dated 20-22 August β stale preseason designations, with zero Out or Doubtful. It did not carry that week's official practice report. Check dates; treat anything older than ~7 days as stale. H1's fallback chain and line-move block trigger stay the primary path β this does not restore the v1 assumption that a current designation is reliably available.
Dead: espn.com/nfl/injuries (empty), core.apiβ¦/teams/{id}/injuries (hangs, while events on the same host is instant).
β site.api.espn.com is intermittently unavailable β down entirely 09-20, fine 09-21. sports.core.api.espn.com (odds) and statsapi.mlb.com have stayed up throughout. Never conclude an endpoint is dead from one timeout β check whether a known-good endpoint on the same host responds first.
π‘ NFL EPA β unobtainable; fallback in place. Tested 2026-09-21.
EPA/play was Β§2's core metric and cannot be fetched: nflverse's GitHub release CSVs return an empty body (release assets redirect to a CDN the fetch tool won't follow), rbsdm and nfelo are interactive web apps rather than data endpoints, and ESPN publishes no EPA field.
Substituted: ESPN team statistics β sports.core.api.espn.com/v2/sports/football/leagues/nfl/seasons/{YEAR}/types/2/teams/{teamId}/statistics. Verified working; ~78k chars over long lines, so Grep it rather than Read. Carries totalGiveaways, totalTakeaways, totalPointsPerGame, yardsPerGame, thirdDownConvPct. One fetch per team, two per game β pull only for shortlisted games.
Why the fallback is defensible. The hierarchy is EPA > point differential > record, so this costs real accuracy and conclusions drop to [MODERATE]. But the specific insight Β§2 exists for β records are inflated by turnover luck, and fumble recoveries are ~random β is measured directly by turnover differential. EPA is a cleaner way to see what's left after stripping that out; it isn't required to spot the illusion in the first place.
Still unverified: Statcast park factors, umpire assignments, bullpen usage, DVOA, official inactives feed.
β Design lesson β 2026-09-20: a Hard Rule must never depend on one source
NFL H1 required a dated QB designation. The one feed providing it went dead, and all 14 NFL games became no-plays β the report was correct by its own rules, but the rules were wrong. Rewritten so H1 can be satisfied three ways (designation β depth chart β presumptive starter, labelled as such), and blocks only when uncertainty is positively indicated: a QB listed Doubtful/Questionable that can't be resolved, or a line carrying the signature of QB news β β₯2-point spread move, a move through a key number, or spread and total diverging by β₯1.5 each.
That second test came from the run itself, which spotted MIN @ CHI, PHI @ TEN and PIT @ NE all showing that pattern and called it "classic starting-QB-out signature". Correct read β it's now the documented fallback when the feed is down.
The same principle is now in _TEMPLATE.md so NHL and NBA don't repeat it. And a stricter sourcing rule: nothing is marked β
verified until it has returned usable data in an actual run. NFL v1 listed two sources as verified that had never been tested; both were dead, and it cost a full Sunday.
The 50-minute buffer. Watch for reports mentioning the 11:45 cutoff in Research Limitations, or a site publishing yesterday's card.
Build the remaining playbooks. NFL before Sunday; NHL and NBA before their October openers.
10. Things that are true and easy to get wrong
- Hit rate and P&L may disagree. Main-leg scoring, Β§5. Not a bug.
- Most sides at most books price negative against de-vigged consensus most days. That's the vig, not a bad slate. Edge exists only where a book is a genuine outlier.
- bet365 is not a bad book β it posted the best price on 8 of 25 moneyline sides when measured. An earlier note claiming otherwise was wrong.
- De-vigged consensus is a yardstick, never a bettable price. Measuring edge against it measures your own model error.
- EV amplifies estimate error by the odds.
EV = (p Γ d) β 1, and all the error lives inp. That's why ranking is on Kelly/confidence and stars are on Edge, never EV. - The xlsx is the record; the site is a view. Never edit the spreadsheet to match the website.