NBA Team Totals — Research Pipeline Ruling

Date: 2026-04-02 Scope: How team total data flows from collection through edge detection to execution Status: DEPLOYED (2026-04-02)


PIPELINE ARCHITECTURE

Pinnacle Page Load
    │
    ├── HTML Parse ──────────────> PinnacleMatchup[] (ML, spread, total)
    │                                   │
    │                                   ▼
    │                           storePinnacleRows()
    │                                   │
    │                                   ▼
    │                           sports_odds_snapshots
    │                           (source='pinnacle')
    │
    └── Arcadia API Intercept ──> PinnacleTeamTotal[] (per-team line + odds)
                                        │
                                        ▼
                                storePinnacleTeamTotalRows()
                                        │
                                        ▼
                                sports_odds_snapshots
                                (source='pinnacle-teamtotal')

Kalshi API Pull
    │
    ▼
KalshiAltLine[] (marketType='teamTotal', teamCode='BOS')
    │
    ▼
storeKalshiRows()
    │
    ▼
sports_odds_snapshots (source='kalshi')

Edge Scanner (scanForEdges)
    │
    ├── Pinnacle spread + total ──> impliedScores() ──> per-team expected score
    │
    ├── Normal curve (σ ≈ 9.9) ──> P(team > threshold)
    │
    ├── Compare model prob vs Kalshi execution price
    │
    ├── Apply 7% Kalshi fee
    │
    └── Store edge in sports_edges (marketType='teamTotal')

DATA COLLECTION SCHEDULE

Pinnacle Team Totals

Condition Interval
No games today Every 2 hours (baseline)
T-4h before first game Every 15 minutes
T-1h before first game Every 5 minutes
Closing snapshot At each game's start time

Team totals are collected in the same browser session as regular Pinnacle odds — no extra Chromium launches.

Kalshi Team Total Markets

Condition Interval
Baseline Every 30 minutes
T-1h before first game Every 5 minutes
Event-driven Immediate snap on Pinnacle movement
Market close Snapshot at game start

ARCADIA API DETAILS

How It Works

Pinnacle's React SPA fetches odds from guest.api.arcadia.pinnacle.com. During page load:

  1. Puppeteer intercepts all responses from the Arcadia domain
  2. Captures the x-api-key from request headers (reusable)
  3. Captures the league ID from URL path
  4. Captures matchup data (team names, participant IDs, alignments)
  5. Captures straight market data (all market types)

Identifying Team Totals

In the straight markets response:

Field Game Total Team Total
type "total" "total"
participantId absent present
period 0 (full game) 0 (full game)
prices[].designation "over" / "under" "over" / "under"
prices[].points game total line team total line

The participantId field is what distinguishes team totals from game totals.

Fallback

If the API intercept fails (no data captured during page load), the scraper:

  1. Uses the captured x-api-key + leagueId to call the API directly via page.evaluate()
  2. If that also fails, falls back to scrapePinnacleOdds() (regular scraper, no team totals)

STORAGE FORMAT

sports_odds_snapshots — Pinnacle Team Totals

Column Value Example
sport 'nba' 'nba'
game_id 'Away@Home::TT:TeamName' 'BOS@MIL::TT:Boston Celtics'
home_team Home team full name 'Milwaukee Bucks'
away_team Away team full name 'Boston Celtics'
source 'pinnacle-teamtotal' 'pinnacle-teamtotal'
snapshot_type 'scheduled' / 'closing' 'scheduled'
total Team total line 112.5
total_over_odds American odds for over -110
total_under_odds American odds for under -110
captured_at ISO timestamp '2026-04-02T15:30:00.000Z'

The game_id format Away@Home::TT:TeamName encodes the team identity so the edge scanner can match it to the correct Kalshi alt lines.

sports_edges — Team Total Edges

Column Value
market_type 'teamTotal'
team_code Kalshi team code (e.g., 'BOS')
pinnacle_anchor Team implied score (e.g., 115.5)
de_vig_method 'implied'
All other columns Same as spread/total edges

TEAM CODE MATCHING

The pipeline maps between three naming systems:

Pinnacle:  "Boston Celtics"
Kalshi:    "BOS"
ESPN:      "Boston Celtics"

Mapping uses standardize() from team-codes.ts + findKalshiCode():

  1. Pinnacle team name → standardized name via standardize()
  2. Standardized name → Kalshi 3-letter code via findKalshiCode()
  3. Kalshi alt line teamCode matched against Kalshi codes for home/away

EDGE SCANNER INTEGRATION

Input

scanForEdges() receives:

Team Total Scan Block

  1. Filter Kalshi lines: marketType === 'teamTotal' && teamCode
  2. Group by gameId
  3. For each game, find Pinnacle total + spread
  4. Derive impliedScores() → home/away expected scores
  5. For each team's alt lines:

Dedup

Each ticker is deduped per scan type per day:

dedupKey = `${alt.ticker}::shin`

FRESHNESS MONITORING

Key Source Updated by
pinnacle-nba-teamtotal Pinnacle Arcadia API collectPinnacle()
edge-scanner-nba-teamtotal Edge scanner output scanForEdges() (when wired)
kalshi-nba Kalshi API collectKalshi() (includes KXNBATEAMTOTAL)

SETTLEMENT

Team total settlement follows the same pattern as game totals:

  1. Final score obtained from ESPN scoreboard API
  2. Each team's final score compared to each alt line threshold
  3. Over = team scored MORE than threshold → YES wins
  4. Under = team scored LESS than threshold → NO wins
  5. Push = team scored EXACTLY threshold → contract voids (Kalshi rules)

Settlement is sport-agnostic — the existing settleGameEdges() function handles marketType='teamTotal' alongside other types.


RISK FACTORS

Risk Mitigation
Arcadia API key rotation Key is captured fresh each page load — no stale keys
Arcadia API changes format Fallback to derived team totals from spread + game total
Pinnacle blocks stealth Chromium Odds API fallback for game lines; team totals derived
Team code mismatch standardize() covers all 30 NBA teams; unresolved codes logged
Low Kalshi team total liquidity Max spread filter (20c) prevents trading illiquid contracts
Normal distribution tail error Tail confidence flag degrades far-from-anchor lines

EXPANSION ROADMAP

Sport Series Ticker Status
NBA KXNBATEAMTOTAL DEPLOYED
MLB KXMLBTEAMTOTAL Config exists, scraper ready
NHL TBD Not yet on Kalshi
NCAAB TBD Not yet on Kalshi
Soccer N/A Goals-based, different model

MLB team totals can be activated by adding 'mlb' to the combined scraper path in collectPinnacle().

Source: ~/edgeclaw/results/panel-results/nba-team-totals-research-ruling.md