Zonara Trainer
Zonara Trainer – Fitness Coach: support, privacy policy and Health & Safety terms
Translation pending
Personal NBA betting analytics powered by BALLDONTLIE
Translation pending
Sports & fitnessGitHub @jposhie1777OnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Repository information has not been collected yet.
No updates detected yet. GitHub releases will appear here.
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗A personal NBA betting analytics web app built on the BALLDONTLIE (BDL) NBA API, GOAT tier.
Personal, non-commercial project. Informational only, not betting advice.
Nothing runs unless the page is open, except the free GitHub Actions live bets tracker (below):
/api/finals?date= is cached on the CDN for 30 days, so BDL is hit about once per date, ever. Today's date is never treated as final.npm install
cp .env.example .env.local # BDL_TIER=mock
npm run baseline # optional: builds a mock baseline in public/data/mock-baseline
npm run dev # http://localhost:3000
Mock mode serves realistic fixtures shaped like the official OpenAPI spec. They include a game in the 3rd quarter, a game at halftime (so halftime mode shows suggestions), injuries, opening and live odds, and props with deeplinks.
The key is read only from server-side environment variables. It is never logged, never sent to the browser, and never prefixed with NEXT_PUBLIC_.
BDL_API_KEY with your key and give it access to this repository.BDL_TIER with the value goat (or allstar / trial / free)..devcontainer/devcontainer.json) also prompts for both secrets and runs npm install.echo $BDL_TIER and test -n "$BDL_API_KEY" && echo key set.For a quick local test you can instead put them in .env.local, which is git-ignored.
Project → Settings → Environment Variables:
| Name | Value | Environments |
|---|---|---|
BDL_API_KEY | your key (mark it Sensitive) | Production (and Preview if you use it) |
BDL_TIER | goat | same |
SITE_PASSWORD | a password you choose (mark it Sensitive) | same |
Redeploy after changing them. Without SITE_PASSWORD the site is open to anyone who finds the URL.
The baseline backfills seasons 2025 (2025-26) and 2026 (2026-27 to date) into zstd Parquet files under public/data/baseline/{season}/:
| File | Contents |
|---|---|
games.parquet | finished games |
teams.parquet | the 30 teams |
players.parquet | players seen in box scores |
player_game_stats.parquet | full-game player stats (all fields) |
player_period_stats.parquet | player stats per quarter (period 1-4) |
advanced_stats.parquet | advanced stats v2, full game (period 0) and per quarter where BDL has it |
lineups.parquet | lineup positions (BDL lineups start with the 2025 season) |
team_period_summaries.parquet | per game, per quarter: points, possessions and pace, derived from play-by-play (raw plays are not stored) |
Run it in the Codespace:
npm run baseline # seasons 2025,2026 through yesterday (ET)
npm run baseline -- --seasons 2026 # only this season
npm run baseline -- --through 2026-12-31 # stop at a date
npm run baseline -- --rebuild-only # rewrite Parquet from the checkpoint, no BDL calls
npm run baseline -- --skip-images # skip the headshot mapping step
.baseline-work/<tier>/dates/ and recorded in checkpoint.json. If it stops, run it again and it continues.public/data/baseline/manifest.json, including the last date covered.public/data/nba-person-ids.json (see Images).Then commit and push the files so Vercel serves them:
git add public/data/baseline public/data/nba-person-ids.json
git commit -m "Update baseline"
git push
Without a baseline the app still works. On first load the browser fetches finished dates one by one from /api/finals. Those responses are CDN-cached for 30 days, so it is slow only once. The baseline just makes first loads fast and keeps BDL calls low.
On page load the browser:
manifest.json./api/season-dates which dates after the manifest's last date have finished games./api/finals?date=YYYY-MM-DD.Dates the baseline later covers are pruned automatically. If IndexedDB is unavailable or corrupt, the app rebuilds it (Settings → Rebuild local game data forces this). A small pill in the header shows syncing / up to date.
ALL-STAR (BDL_TIER=allstar) runs at 60 req/min with the Trial / Free feature set below.
GOAT (BDL_TIER=goat) | Trial / Free (trial / free) | |
|---|---|---|
| Rate budget | 600 req/min | 5 req/min |
| Games, stats, per-quarter stats, injuries | ✓ | ✓ (injuries may need a paid tier) |
| Odds, opening odds, player props, deeplinks | ✓ | ✗ (pages show a notice instead) |
| Advanced stats v2, lineups, play-by-play | ✓ | ✗ (pace falls back to box-score possessions; positions fall back to the roster) |
| Full baseline time | about 20-40 min | many hours (about 15 calls per date at 5/min) |
Endpoints your tier can't use come back as an empty list with an explanation, so nothing crashes.
production branch. It is the live site; main keeps getting new work.
On GitHub: Code → branch menu (main) → type production → Create branch production from main.jposhie1777/New-Prop-Analyzer.
If it is not listed, use Adjust GitHub App Permissions to give Vercel access to the repo.npm run build).BDL_API_KEYBDL_TIER = goatSITE_PASSWORDproduction the live branch: Project → Settings → Environments → Production → Branch Tracking, change main to production, then save.production, then deploy.https://<project>.vercel.app and sign in with SITE_PASSWORD.
On iPhone: Share → Add to Home Screen.postinstall copies the DuckDB-WASM files into public/duckdb/, so the browser never loads code from a third-party CDN.
main, tried in the Codespace.main (and every PR) also gets a Preview deployment with its own URL. It is in the PR's Vercel comment and in Deployments.
production and compare main, review the list of changes, then Merge. The live site updates in about 2 minutes./api/finals and cached by the CDN, the first time the site is opened after the games end.npm run baseline (and npm run baseline:shots once) in the Codespace, commit public/data/baseline, and release it like any other change. A bigger baseline means fewer top-up requests on a fresh device.Picks, the slip, the record and settings live in each browser's storage. The Codespace URL and the Vercel URL each keep their own.
All TTLs live in src/config/cacheTtls.ts:
| Data | CDN cache |
|---|---|
| Schedule | 5 min |
| Injuries | 10 min |
| Opening odds / props | 10 min |
| Live box scores | 30 s |
| Live odds / props | 60 s |
| Finished dates | 30 days, immutable |
| Player search | 1 day |
Every route sends s-maxage + stale-while-revalidate, so Vercel's CDN answers repeats without running a function.
The Live Bets page is filled by .github/workflows/live-tracker.yml, which runs on GitHub (free for a public repo) so no page has to stay open.
BDL_API_KEY.live-data branch (never merge it; Vercel ignores it via vercel.json). The site reads them from raw.githubusercontent.com, which can lag up to about 5 minutes. The branch is public like the rest of the repo.Local test: BDL_TIER=mock npm run live:track -- --once --data-dir live-data, then LIVE_DATA_DIR=live-data npm run dev.
.github/workflows/site-data.yml runs every morning at about 6:23 AM Eastern, after every game is final. It runs scripts/refreshSiteData.ts, which does three things:
public/data/baseline;snapshot.json, which holds the team ratings, rest days, injured-scorer averages, 100-in-3 profiles and live quarter profiles worked out ahead of time.Everything is force-pushed as one commit to the site-data branch, so its history never grows. Never merge that branch.
The site reads these files from raw.githubusercontent.com. Slate, Lines, 100 in 3 and Live use the snapshot right away, and the in-browser database keeps loading in the background. If the snapshot is not from today, or the branch is missing, everything falls back to the old path. The workflow uses the same BDL_API_KEY secret as the live tracker. To run it now: Actions tab → Nightly site data → Run workflow.
Pick NCAAB in the sport selector at the top of the menu. College is game lines only (spread, total, moneyline), with the same confidence ratings, filters and slip as NBA.
BDL_API_KEY (ALL-STAR tier, 60 calls a minute). ALL-STAR has no team stats or odds, so box scores are rebuilt from play-by-play: shots, rebounds, assists, turnovers, steals, blocks and fouls per team and per player, plus possessions.ODDS_API_KEY as a GitHub Actions secret; without it the odds job skips quietly.NCAAB nightly data (5:47 AM ET in winter, 6:47 AM in daylight time) writes ratings, team files, box scores and the graded record to the ncaab-data branch. NCAAB odds (7:05 AM, 2:35 PM, 5:35 PM ET in winter; 8:05 AM, 3:35 PM, 6:35 PM in daylight time) writes odds and picks to ncaab-odds. Both force-push a single commit and never deploy. The site reads them straight from raw.githubusercontent, so pages load small JSON files and no in-browser database.NCAAB nightly data by hand from the Actions tab. It backfills last season's box scores (about 100 minutes per 6,000 games at 60 calls a minute; leftovers continue the next night). The log reports how many rebuilt box scores match the final score.BDL_TIER=mock npx tsx scripts/ncaab/refreshNcaab.ts --out .ncaab-local/data then BDL_TIER=mock npx tsx scripts/ncaab/pullOdds.ts --out .ncaab-local/odds --data .ncaab-local/data, and npm run dev in mock mode serves them.The slip and these settings follow you between devices: your sportsbooks, default book, state, EV threshold, wager and 100-in-3 settings. Appearance and layout stay per device. The data is stored in a secret GitHub gist that only your server can read.
SYNC_GITHUB_TOKEN with that token, then redeploy.The first device to sync uploads what it already has. After that, the newest change wins: saves happen about a second after a change, and other devices pull when you switch back to the tab. Sync only runs when SITE_PASSWORD is set. Settings → Sync across devices shows the status.
/)
/players, /player/[id])
/three-quarters)
/live)
/slip)
src/config/sportsbookLinks.ts, marked verify). Without a bet-slip link the pick is copied so you can paste it into the book's search.npm run images:map, which also runs at the end of the baseline. It matches by name + team, then by unique name. Put fixes in src/config/nbaPersonOverrides.json as { "<bdlPlayerId>": <nbaPersonId> }.npm test # Vitest: odds math, rate budget, client, finals builder, aggregates (real DuckDB SQL), grader, cost model
npm run lint # TypeScript type-check
npm run build
docs/bdl-nba-openapi.yml, copied from BDL's balldontlie-api/mcp repo). The deeplinks object (event + selections, each url or url_template with {state}, {pickType}, {wagerAmount}) comes from the docs page, and the code treats it as optional.period filter (1-4) only works for completed games. Live box scores are cumulative.await pulseQuery("SELECT * FROM team_scope_lines LIMIT 5").src/config/ cache TTLs, tiers, Vercel limits, app settings, headshot overrides
src/lib/bdl/ BDL client, rate budget, types, mock fixtures
src/lib/server/ finals builder, route helpers, usage counters
src/lib/data/ row schemas, flattening, play-by-play team summaries, manifest
src/lib/analytics/ DuckDB views, loaders, aggregates, halftime and 100-in-3 models
src/lib/math/ odds and probability math
src/lib/tracking/ pick record and grader
src/lib/client/ browser data sync, IndexedDB, hooks, slip, settings
src/app/ pages and API routes
scripts/ baseline, headshot mapping, DuckDB asset copy
/record)
/costs)
x-vercel-cache), function invocations and duration.src/config/vercelLimits.ts, marked verify./settings): sportsbook state, EV threshold, coupon wager amount, local data reset.