Date: 2026-04-02 Process: Architectural review — data sources, probability model, edge detection methodology Scope: NBA team totals market (individual team scoring over/under) Status: BUILT & DEPLOYED (2026-04-02)
A team total is a bet on how many points a specific team will score in a game (e.g., Lakers Over/Under 112.5). This is distinct from the game total which is the combined score of both teams.
Why it matters: Team totals are less efficiently priced than game totals because they require team-specific modeling rather than aggregate game modeling. This creates edge opportunities.
Kalshi series: KXNBATEAMTOTAL
KXNBATEAMTOTAL-26APR03BOSMIL (date + away + home)KXNBATEAMTOTAL-26APR03BOSMIL-BOS116 (team + line threshold)| Field | Value |
|---|---|
| Source | Pinnacle via Arcadia guest API intercept |
| Data | Per-team total line + over/under decimal odds |
| Method | Puppeteer page load intercepts guest.api.arcadia.pinnacle.com responses |
| Identification | type="total" + participantId present = team total |
| Storage | sports_odds_snapshots with source='pinnacle-teamtotal' |
| Freshness key | pinnacle-nba-teamtotal |
| Schedule | Adaptive cadence (2h/15m/5m) — same as main Pinnacle scraper |
When Pinnacle team total odds are unavailable, team totals are derived:
Home team total = (game_total + spread) / 2
Away team total = (game_total - spread) / 2
Example: Game total 225.5, home -5.5
Home TT = (225.5 + 5.5) / 2 = 115.5
Away TT = (225.5 - 5.5) / 2 = 110.0
This derivation uses Pinnacle's spread and game total — both already scraped on adaptive cadence.
| Source | What it provides for team totals |
|---|---|
| Pinnacle ML/Spread/Total | Game-level anchor, derivation fallback |
| DRatings | Per-game predicted scores (home/away separately) |
| Sagarin | Team power ratings, predicted spreads |
| GameSim | Monte Carlo predicted scores per team |
| NBA Stuffer | Pace, offensive/defensive ratings |
| NBA Advanced Stats | Off/def/net rating, pace, eFG%, TOV% |
| BBRef Team Stats | Backup stats source |
| Team Variance | Std dev, skewness, kurtosis of scoring distribution |
| Dimers | Win probabilities per game |
NBA team scoring is approximately normally distributed. The edge scanner uses:
teamSigma = gameSigma / sqrt(2)
Where:
gameSigma = 14 (NBA game total standard deviation)
teamSigma = 14 / sqrt(2) ≈ 9.9
Model probability = Normal CDF exceedance at threshold
P(team scores > threshold) = 1 - Phi((threshold - teamImplied) / teamSigma)
The team implied score comes from:
Both are valid. The direct Pinnacle line is preferred because it includes market information about team-specific factors (injuries, pace matchups, rest) that the derivation doesn't capture.
Team total odds from Pinnacle are de-vigged using the Shin method (same as game totals):
Shin de-vig removes the bookmaker margin while preserving the
favorite-longshot bias inherent in real odds.
The de-vigged over probability becomes the model's fair value anchor.
1. Pinnacle team total odds → de-vig → fair over/under probability
2. Build Normal curve around team implied score (σ ≈ 9.9)
3. For each Kalshi alt line:
a. Calculate model probability at that threshold
b. Compare to Kalshi mid-price
c. Calculate raw edge = model_prob - execution_price
d. Subtract Kalshi fee (7% of profit)
e. If net_edge > min_threshold → flag as executable
4. Store in sports_edges table with marketType='teamTotal'
| Filter | Value | Why |
|---|---|---|
| Min net edge | 2% (configurable) | Below this, fees eat the edge |
| Max net edge | 25% | Above this, likely a data error |
| Kalshi mid range | 10c - 90c | Extreme prices are illiquid |
| Max spread | 20c | Wide spreads = no real execution |
| Tail confidence | Must be "reliable" | Far-from-anchor lines degrade |
Distance from anchor affects model confidence:
rungDistance = |threshold - teamImplied| / 1.5
Thresholds far from the implied score have lower model confidence
because the normal approximation degrades in the tails.
(Team_pace - Opponent_pace) / 2 — faster pace = higher team totalTeam_OffRtg * (Opp_DefRtg / League_Avg_DefRtg) — offense adjusted for opponent defensecollector.ts)NBA Pinnacle scrapes now use scrapePinnacleWithTeamTotals() which:
source='pinnacle-teamtotal'edge-scanner.ts)The team total scan block (lines ~655-780):
marketType='teamTotal'standardize() + findKalshiCode()σ = gameSigma / sqrt(2))marketType: 'teamTotal' and teamCodedesk-config.ts)KXNHLTEAMTOTAL), MLB team totals (already in config), NCAAB team totals| Component | Status |
|---|---|
Kalshi series config (KXNBATEAMTOTAL) |
DONE |
| Pinnacle team total scraper (Arcadia API) | DONE |
Collector wiring (scrapePinnacleWithTeamTotals) |
DONE |
Storage in sports_odds_snapshots |
DONE |
| Edge scanner team total logic | DONE (existed) |
| Dashboard entries | DONE |
| Freshness monitoring | DONE |
Normal curve model (σ = gameSigma / sqrt(2)) |
DONE |
| Team-specific sigma calibration | NOT YET |
| Pace-adjusted model | NOT YET |
| Live team total tracking | NOT YET |