A daily 3-leg NHL player parlay from a transparent goals, points and shots model. Probabilities, not advice.
Sports & fitnessGitHub @peterfoengAvailability needs another checkOwner 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 daily 3-leg NHL player parlay. The model is a short, documented blend of recent and season rates, opponent shot volume and goalie save quality, turned into Poisson probabilities for anytime goals, anytime points, and shot totals. Prices are decimal. Times are Australia/Melbourne.
Model probabilities, not advice. Nothing here suggests a stake.
npm install
npm run job -- --date 2026-11-12
npm run dev
The app is at http://127.0.0.1:4317. With DATABASE_URL unset, slates and cached downloads live in ./data.
Grade a finished NHL date (box scores, not a made-up price):
npm run job -- --grade 2026-11-10
--force rebuilds a slate even after a game has started. Without it, a live or final game keeps the price it was saved with. Games that have not started are priced again, and the odds lines in the job result are from that run.
npm run job with no date does what the morning cron does: build the next open NHL slate and grade the one before it.
| Command | What it does |
|---|---|
npm run dev | Next.js dev server on port 4317 |
npm run job -- --date YYYY-MM-DD | Build that NHL slate |
npm run job -- --grade YYYY-MM-DD | Build if needed, then grade from box scores |
npm test | Unit tests (Poisson, blending, adjustments, parlay, grading) |
npm run typecheck | tsc --noEmit |
npm run lint | ESLint |
npm run build | Production build |
npm run migrate | Apply db/migrations/001_init.sql when DATABASE_URL is set |
Copy .env.example to .env.local. No real secrets ship in the repo.
| Variable | Required | Purpose |
|---|---|---|
DATABASE_URL | No | Postgres (Neon works). Unset uses the JSON file in DATA_DIR. |
CRON_SECRET | For /api/cron | Vercel sends Authorization: Bearer <CRON_SECRET>. The CLI does not use it. |
ODDS_API_KEY | No | The Odds API. The provider stays off when this is empty. Read when the job runs. |
ODDS_API_MARKETS | No | Default player_goal_scorer_anytime,player_points,player_shots_on_goal. |
ODDS_API_REGIONS | No | Default us. NHL player props are on US books; au does not carry them. |
ODDS_API_CACHE_HOURS | No | How long a priced Odds API response stays in http_cache. Default 6. |
ODDS_PROVIDERS | No | Default sportsbet,pointsbet,oddsapi. |
DATA_DIR | No | File store and download cache. Default ./data. |
https://api-web.nhle.com: schedule, rosters, game logs, live box scores. The open page polls /api/live every 60 seconds only while a game is on or about to start. That route is cacheable for 30 seconds. Nothing polls when the page is closed.api.au.pointsbet.com).ODDS_API_KEY. One US region, three player markets, decimal odds. A priced response is cached for several hours. The events list is free; each game costs markets × regions (3 credits with the defaults).A leg without a real price shows “no price” and is not eligible for the parlay. Edge is model probability minus 1 / decimal odds (vig left in).
See the About page in the app for the weights. Short version: last 10 games, decay 0.85, 65% recent once 80 minutes are in, Poisson probabilities, opponent shots-allowed factor and goalie goals-versus-expected clamped. The parlay is the three positive-edge prices from three different games that maximise the product of model probabilities.
vercel.json schedules GET /api/cron at 0 20 * * * UTC. On the Hobby plan the job runs once sometime during that hour (20:00–20:59 UTC):
The board is ready before 8:00am Melbourne. The job uses the Melbourne calendar date, already the new day by 6am. It builds the earliest NHL date that still has a game to play or in progress — the slate whose games start later that Melbourne day, including when an early game on it has already dropped the puck — and grades the date before it.
The route checks Authorization: Bearer $CRON_SECRET and returns 401 otherwise. Set CRON_SECRET in the Vercel project. The job is safe to run twice. Games that have not started are priced again. A live or final game keeps its saved price, and an unpriced leg stays “no price”. The odds block in the response is from this run. The Odds API line includes creditsUsed and requestsRemaining (x-requests-remaining) so a repeat inside the cache window shows 0 credits spent.
export const maxDuration = 60 is set on the cron route. The first run of a day downloads MoneyPuck and a few hundred game logs, so a cold start can use most of that minute. Later runs hit the cache. On Vercel’s free tier the cache is the database when DATABASE_URL is set (the file system does not persist between invocations).
Deploy:
CRON_SECRET. Add DATABASE_URL from a Neon (or other Postgres) store if you want the board to survive restarts. Run npm run migrate once against that database, or execute db/migrations/001_init.sql.ODDS_API_KEY./api/cron yourself with the bearer token.Local Postgres is optional. Without DATABASE_URL the app is fully usable from ./data.