# Sports Odds Screen — Hyper-Detailed Spec

> Written 2026-02-17. This is the authoritative reference for what's requested,
> what's broken, and what's being built. No ambiguity. No re-asking.

---

## Table of Contents

1. [What the User Sees Today](#1-what-the-user-sees-today)
2. [What's Broken](#2-whats-broken)
3. [What's Requested](#3-whats-requested)
4. [The Odds API Integration (Rotation Numbers)](#4-the-odds-api-integration)
5. [Implementation Plan](#5-implementation-plan)
6. [File Map](#6-file-map)

---

## 1. What the User Sees Today

### Entry Point

The **Value Dashboard** view (`/app/value-dashboard`) is the primary sports screen.

**Rendered by:** `ValueDashboardView.tsx` → conditionally renders one of:

- `ConsolidatedMoneylines` (for moneylines — 1 row per game, expandable ladder)
- `TwoRowMoneylines` (alternative view — 2 rows per game, sportsbook style)
- `TwoRowSpreads` (for spreads — 2 rows per game)
- `TwoRowTotals` (for totals — 2 rows per game)

### Controls

- **Sport selector tabs:** NBA, NFL, NHL, CBB, ATP, WTA (keyboard shortcuts available)
- **Market type tabs:** Moneyline, Spread, Total (per sport)
- **Maker/Taker toggle:** Controls which side of the book is shown (best bid vs best ask)
- **View mode:** Consolidated (1-row) vs Two-Row vs Grid
- **Volume window toggles:** Show volume columns for time-windowed activity

### Moneyline Columns (ConsolidatedMoneylines — Current Default)

| #   | Column         | Format                         | Sortable | Notes             |
| --- | -------------- | ------------------------------ | -------- | ----------------- |
| 1   | Game Date      | YYYY-MM-DD via `<DateDisplay>` | Yes      | Only on away row  |
| 2   | Start Time     | HH:MM (browser local TZ)       | Yes      | Should be Vegas   |
| 3   | Away Team      | Team code (e.g., "BOS")        | Yes      | Cyan text         |
| 4   | Home Team      | Team code (e.g., "LAL")        | No       |                   |
| 5   | Away Kalshi    | Dual: `55¢ \| +122`            | No       | Clickable → trade |
| 6   | Home Kalshi    | Dual: `55¢ \| +122`            | No       | Clickable → trade |
| 7   | Away Poly      | Dual format                    | No       | Clickable → trade |
| 8   | Home Poly      | Dual format                    | No       | Clickable → trade |
| 9   | Away K Liq     | LiquidityBar (color gradient)  | No       | Dollar amount     |
| 10  | Home K Liq     | LiquidityBar                   | No       |                   |
| 11  | Away P Liq     | LiquidityBar                   | No       |                   |
| 12  | Home P Liq     | LiquidityBar                   | No       |                   |
| 13+ | Volume windows | Per configured window          | No       | Optional          |

### What's Missing from a Sportsbook Experience

A real sportsbook odds screen shows:

| What                 | Sportsbook Has       | Dashboard Has                | Gap                     |
| -------------------- | -------------------- | ---------------------------- | ----------------------- |
| **Rotation number**  | 511/512              | Nothing                      | No external data source |
| **Game time**        | "7:30 PM PT"         | "19:30" (browser local)      | Wrong TZ, no AM/PM      |
| **Sort by time**     | Default, always      | Available but not default    | UX issue                |
| **Dollar risk/win**  | "Risk $93 to win $7" | Not shown                    | Critical for taker      |
| **Moneyline odds**   | -1329                | Shown as dual `93¢ \| -1329` | Present but buried      |
| **Total $ at level** | "$74,400 risked"     | "80K contracts"              | Misleading at extremes  |
| **One-click take**   | Tap the line         | Click → modal → fill form    | Too slow                |

---

## 2. What's Broken

### BUG-001: CBB Moneylines — Today's Games Missing (P0)

**Symptom:** CBB moneyline view does not show today's games (Feb 17, 2026). Shows games from later dates instead.

**What was fixed:** `discover.ts` line 92 — UTC date filtering replaced with local date. Committed `4fe04f5`, merged via PR #4.

**What's still broken:** Despite the date filter fix, today's CBB games still don't appear. Remaining hypotheses:

1. **Kalshi doesn't have today's CBB markets open yet** — Some CBB games may not be listed until closer to tip-off. Needs live verification by logging the raw API response for `KXNCAAMBGAME` series.

2. **Ticker parsing failure** — The CBB ticker format may have edge cases where `parseMarketTicker()` returns `null`, silently dropping games. The parser expects format `KXNCAAMBGAME-{DATE}{TEAMS}-{OUTCOME}` but CBB team codes vary wildly (2-4 chars, sometimes ambiguous).

3. **Event-level fetch failure** — `stream.ts` fetches events individually to get start times and team names. If the event fetch fails for CBB games, the game data may be incomplete and filtered out downstream.

**Next step:** Add `console.log` or temporary debug output at the discovery stage to see exactly what Kalshi returns and what gets filtered out.

### BUG-002: Game Dates Showing Wrong (P1)

**Symptom:** Dates display as "March" when the game is today (Feb 17).

**Analysis:** Most likely a downstream effect of BUG-001. If today's games are filtered out, the view shows the next available games — which could be days or weeks away. The dates themselves are probably correct for those games; they're just the wrong games.

**Alternate theory:** If dates arrive in a format other than YYYY-MM-DD (e.g., ISO datetime), `DateDisplay.tsx` line 30 parses via `new Date()` which treats date-only strings as UTC midnight. In Pacific time (UTC-8), this shifts the date back one day. However, sports dates from the ticker parser ARE in YYYY-MM-DD format and hit the early return on line 25, bypassing this bug.

**Verification needed:** Inspect `game.date` values in browser DevTools to confirm what's actually rendered.

### BUG-003: Start Times Not in Las Vegas Timezone (P1)

**Symptom:** `startTimePt` variable (named for Pacific Time) is formatted using `Intl.DateTimeFormat` WITHOUT a `timeZone` parameter, defaulting to the browser's local timezone.

**Root cause:** `stream.ts` lines 1119-1123:

```typescript
const parts = new Intl.DateTimeFormat('en-US', {
  hour: '2-digit',
  minute: '2-digit',
  hour12: false,
  // MISSING: timeZone: 'America/Los_Angeles'
}).formatToParts(new Date(adjustedStartTime));
```

**Fix:** Add `timeZone: 'America/Los_Angeles'` to both the time formatter AND add a separate date formatter using the same timezone for the game date. All displayed dates and times should be Las Vegas (Pacific) time.

**Also needed:** Show "PT" label next to times so it's unambiguous. Consider 12-hour format with AM/PM for sports-bettor readability.

### BUG-004: DateDisplay UTC Shift for Non-Standard Formats (P2)

**Root cause:** `DateDisplay.tsx` line 30 uses `new Date(value)` for non-YYYY-MM-DD strings, which interprets as UTC.

**Impact:** Low for sports (YYYY-MM-DD strings hit early return). Could affect other views using ISO datetime strings.

---

## 3. What's Requested

### Priority 1: Sortable Time-Ordered Odds Screen

> "Number one thing is the time order, sortable similar to an odds screen right now."

**Requirements:**

- Default sort: chronological (next game first), by game start time
- All columns sortable (existing `useTableSort` + `SortableTh` infrastructure)
- Time column prominently displayed in Las Vegas timezone
- Sportsbook-style layout: scan-friendly, one line per team or one line per game

**Current state:** Sorting infrastructure exists (`useTableSort.ts`, `SortableTh.tsx`). Used in ConsolidatedMoneylines and TwoRowMoneylines. But default sort is not by time, and times are in browser-local timezone.

### Priority 2: Dollar Amounts / Risk-Reward Clarity

> "I don't know what I'm winning or risking... it's just how sports bettors are trained to see."

**The problem:** At 93¢, "80K liquidity" means $74,400 at risk to win $5,600. At 50¢, "80K liquidity" means $40,000 to win $40,000. The contract count is meaningless without price context. Sports bettors think in dollars risked and dollars won.

**Requirements for the odds table:**

- Show dollar risk and dollar win alongside odds
- Make it immediately clear: "Risk $X to win $Y" at each price level
- Moneyline odds already present in dual format — ensure they're prominent, not buried

**Requirements for the order book (MarketDetailView):**

- Each level shows: price, contracts, **total dollars** (price × contracts), **win dollars** ((1 - price) × contracts)
- Cumulative depth bars should reflect dollar value, not just contract count
- Sweep preview panel must show: total cost (risk), potential win, effective moneyline

### Priority 3: Book Sweep — Taker-Side One-Click Depth Execution

> "You want to one-click to make the bet, assuming you want the entire order book up to the point that book is good."

**Requirements:**

- Click an ask level in the order book → sweep all liquidity from best ask through clicked level
- Single IOC (immediate_or_cancel) limit order at clicked price for cumulative depth quantity
- Sweep confirmation panel showing: price range, total contracts, average fill price, total cost, total win, effective moneyline
- One more click to fire — "SWEEP BUY" or "SWEEP SELL"
- Success toast with fill summary

**API mechanic:** Already supported. `placeOrder()` in `kalshiApi.ts` accepts `time_in_force: 'immediate_or_cancel'`. The exchange matching engine fills at each resting level up to the limit price. Unfilled remainder is cancelled.

### Priority 4: Rotation Numbers (The Odds API)

> "We should also look at adding roto (rotation) which I believe is freely available on the-odds-api."

**What:** Rotation numbers are universal identifiers used by sportsbooks to identify specific games/bets. They're the lingua franca of sports betting — every bettor recognizes them.

**Source:** The Odds API (`api.the-odds-api.com/v4`) — free tier: 500 credits/month.

**Available data:**
| Field | Value |
|-------|-------|
| `home_rotation` | e.g., 512 |
| `away_rotation` | e.g., 511 |
| `commence_time` | ISO 8601 game start |
| `home_team` | Full name (e.g., "Los Angeles Lakers") |
| `away_team` | Full name (e.g., "Boston Celtics") |
| `bookmakers[].markets[].outcomes[]` | Odds from DraftKings, FanDuel, BetMGM, etc. |

**Supported sports:** NBA (`basketball_nba`), CBB (`basketball_ncaab`), NFL (`americanfootball_nfl`), NHL (`icehockey_nhl`)

**API call example:**

```
GET https://api.the-odds-api.com/v4/sports/basketball_ncaab/odds
  ?apiKey=KEY
  &regions=us
  &markets=h2h
  &oddsFormat=american
  &includeRotationNumbers=true
```

**Integration approach:**

1. New module: `apps/dashboard/src/lib/oddsApi.ts` — simple REST client (API key as query param, no RSA signing needed)
2. Match games via commence_time + team names to Kalshi events
3. Enrich game data with rotation numbers
4. Display rotation number as first column in the odds table
5. Cache aggressively — odds don't change second-by-second pre-game
6. Budget: ~16 calls/day on free tier, enough for periodic refreshes per sport

**Bonus:** The Odds API returns real sportsbook odds (DraftKings, FanDuel, etc.) which can be shown alongside Kalshi/Polymarket for cross-platform comparison.

### Priority 5: Las Vegas Timezone (Hard Declare)

> "Let's hard declare this Las Vegas and show the Las Vegas date and time."

**Requirements:**

- ALL game dates displayed in America/Los_Angeles timezone
- ALL start times displayed in America/Los_Angeles timezone
- Show "PT" label or "(Vegas)" indicator
- Use 12-hour format with AM/PM for sports-bettor readability
- No more browser-local timezone for sports data

### Priority 6: Enhanced Book View Navigation

> "The enhanced book view should displace the right categories so when you click on it then it shows the deeper more advanced screen."

**Requirements:**

- Clicking a market from the odds table navigates to MarketDetailView
- MarketDetailView shows: full order book with dollar amounts, sweep panel, risk/reward display
- Navigation already works: `/app/market/:ticker` route exists and renders MarketDetailView
- Need to ensure the odds table click handler navigates to this route (currently opens a PlaceOrderModal instead)

---

## 4. The Odds API Integration

### API Details

| Property         | Value                                           |
| ---------------- | ----------------------------------------------- |
| Base URL         | `https://api.the-odds-api.com/v4`               |
| Auth             | `apiKey` query parameter                        |
| Format           | JSON REST                                       |
| Rate limit       | 30 req/sec                                      |
| Free tier        | 500 credits/month                               |
| Credit cost      | 1 credit per `/odds` call per region per market |
| Rotation numbers | `includeRotationNumbers=true` param             |

### Sport Keys

| Dashboard Sport | Odds API Sport Key                             |
| --------------- | ---------------------------------------------- |
| NBA             | `basketball_nba`                               |
| CBB             | `basketball_ncaab`                             |
| NFL             | `americanfootball_nfl`                         |
| NHL             | `icehockey_nhl`                                |
| Tennis ATP      | `tennis_atp_french_open` etc. (per tournament) |

### Game Matching Strategy

Kalshi doesn't use rotation numbers. The Odds API does. To match:

1. **Primary key:** `commence_time` (within 30-minute window) + team name fuzzy match
2. **Secondary key:** Rotation number lookup after initial match
3. **Store mapping:** Cache `kalshiEventTicker → oddsApiEventId` mapping in memory

### Credit Budget (Free Tier)

500 credits/month = ~16/day.

Strategy:

- Use FREE `/events` endpoint to discover games (0 credits)
- Call `/odds` only for active sports with Kalshi markets
- 4 sports × 1 market type × 1 region = 4 credits per refresh
- Budget: ~4 refreshes per day, or more if focused on game days
- Cache responses for 15-30 minutes

### Response Shape

```typescript
interface OddsApiEvent {
  id: string;
  sport_key: string;
  sport_title: string;
  commence_time: string; // ISO 8601
  home_team: string;
  away_team: string;
  home_rotation: number | null;
  away_rotation: number | null;
  bookmakers: Array<{
    key: string; // "draftkings", "fanduel", etc.
    title: string;
    markets: Array<{
      key: string; // "h2h", "spreads", "totals"
      outcomes: Array<{
        name: string;
        price: number; // American odds
        point?: number; // For spreads/totals
      }>;
    }>;
  }>;
}
```

---

## 5. Implementation Plan

### Phase 1: Fix What's Broken (bugs) — COMPLETE

All bugs shipped and deployed to staging (`streamrift-0.2`).

| Task                                                | Status | Commit               |
| --------------------------------------------------- | ------ | -------------------- |
| BUG-001: CBB date filter + team code split          | DONE   | `4fe04f5`, `ed452b0` |
| BUG-002: Wrong dates (downstream of BUG-001)        | DONE   | Same                 |
| BUG-003: Vegas timezone on start times + 12h format | DONE   | `dfa2c02`            |
| BUG-004: DateDisplay UTC shift hardening            | DONE   | `8fa9187`            |

### Phase 2: Odds Screen Core (sortable, time-ordered) — COMPLETE

| Task                                      | Status | Commit    |
| ----------------------------------------- | ------ | --------- |
| Default sort by game time (chronological) | DONE   | `dfa2c02` |
| Consistent sort across all sport views    | DONE   | `dfa2c02` |
| Reformat start time: 12-hour + Vegas TZ   | DONE   | `dfa2c02` |
| Dollar risk/win on moneyline odds cells   | DONE   | `53181b6` |

### Phase 3: The Odds API Integration — CLIENT DONE, UI OPEN

Client, types, caching, and team matching shipped in `319ec9d`. Remaining work is wiring the data into the table UI.

| Task                                             | Status   | Commit/Notes                         |
| ------------------------------------------------ | -------- | ------------------------------------ |
| Create `oddsApi.ts` REST client                  | DONE     | `319ec9d`                            |
| Create types for Odds API responses              | DONE     | `319ec9d` (inline in oddsApi.ts)     |
| Game matching logic (commence_time + team names) | DONE     | `319ec9d` (team name fuzzy matching) |
| Cache layer (in-memory, 15-min TTL)              | DONE     | `319ec9d`                            |
| API key management (env var)                     | **OPEN** | Need `VITE_ODDS_API_KEY` in `.env`   |
| Add rotation number column to odds tables        | **OPEN** | Blocked by API key                   |
| Add sportsbook odds columns to tables            | **OPEN** | Blocked by API key                   |

### Phase 4: Order Book Enhancements — COMPLETE

All shipped in `fa3ebc5`.

| Task                                              | Status |
| ------------------------------------------------- | ------ |
| Dollar amounts on OrderBookLevel (Cost + Depth $) | DONE   |
| SweepConfirmPanel molecule                        | DONE   |
| Sweep click handler + IOC execution               | DONE   |

### Phase 5: Inline Book Panel — COMPLETE (replaced navigation approach)

Original plan was click → navigate to MarketDetailView. StreamRift rejected this UX — clicking odds must show the book **inline**, not navigate away. Implemented as inline panel sidebar on both dashboard views.

| Task                                             | Status | Commit    |
| ------------------------------------------------ | ------ | --------- |
| Inline panel on ValueDashboardView (multi-sport) | DONE   | `a2ac310` |
| Inline panel on NbaValueDashboardView (NBA-only) | DONE   | `33e430b` |
| Escape key dismisses panel                       | DONE   | `33e430b` |
| Ticker-to-game-key mapping for click routing     | DONE   | `33e430b` |

### Phase 6: Polish — OPEN

| Task                                           | Effort |
| ---------------------------------------------- | ------ |
| Hide volume columns when inline panel is open  | Small  |
| Deploy to staging + visual verify inline panel | Zero   |

---

## 6. File Map

### Files to Modify

| File                                                                           | Changes                                              |
| ------------------------------------------------------------------------------ | ---------------------------------------------------- |
| `apps/dashboard/src/lib/sportsStream/stream.ts`                                | Vegas timezone on time formatter (lines 1119-1123)   |
| `apps/dashboard/src/components/atoms/DateDisplay.tsx`                          | Harden UTC handling                                  |
| `apps/dashboard/src/components/nba-value-dashboard/ConsolidatedMoneylines.tsx` | Default time sort, dollar columns, rotation # column |
| `apps/dashboard/src/components/nba-value-dashboard/TwoRowMoneylines.tsx`       | Same as above                                        |
| `apps/dashboard/src/components/sports/TwoRowSpreads.tsx`                       | Vegas timezone, default sort                         |
| `apps/dashboard/src/components/sports/TwoRowTotals.tsx`                        | Vegas timezone, default sort                         |
| `apps/dashboard/src/components/organisms/OrderBook.tsx`                        | Pass full depth data for sweep                       |
| `apps/dashboard/src/components/molecules/OrderBookLevel.tsx`                   | Dollar amounts, win $, moneyline                     |
| `apps/dashboard/src/components/pages/MarketDetailView.tsx`                     | Sweep state, sweep handler, sweep panel              |
| `apps/dashboard/src/lib/sportsDiscovery/discover.ts`                           | Debug logging for CBB investigation                  |

### Files Created (this cycle)

| File                                                     | Purpose                             | Commit    |
| -------------------------------------------------------- | ----------------------------------- | --------- |
| `apps/dashboard/src/lib/oddsApi.ts`                      | The Odds API client + types + cache | `319ec9d` |
| `apps/dashboard/src/components/molecules/SweepPanel.tsx` | Sweep confirmation UI               | `fa3ebc5` |

### Environment Variables to Add

| Variable            | Location              | Purpose          |
| ------------------- | --------------------- | ---------------- |
| `VITE_ODDS_API_KEY` | `apps/dashboard/.env` | The Odds API key |

---

## Appendix: Existing Infrastructure We're Building On

| Piece                                     | Status          | Location                                               |
| ----------------------------------------- | --------------- | ------------------------------------------------------ |
| `useTableSort` hook                       | Working         | `apps/dashboard/src/hooks/useTableSort.ts`             |
| `SortableTh` component                    | Working         | `apps/dashboard/src/components/atoms/SortableTh.tsx`   |
| `Odds` atom (cents/american/dual)         | Working         | `apps/dashboard/src/components/atoms/Odds.tsx`         |
| `LiquidityBar` atom                       | Working         | `apps/dashboard/src/components/atoms/LiquidityBar.tsx` |
| `DateDisplay` atom                        | Fixed (BUG-004) | `apps/dashboard/src/components/atoms/DateDisplay.tsx`  |
| `oddsConversion.ts` (prob ↔ american)     | Working         | `apps/dashboard/src/lib/oddsConversion.ts`             |
| `marketTransform.ts` (price → odds)       | Working         | `apps/dashboard/src/lib/marketTransform.ts`            |
| `placeOrder()` with IOC support           | Working         | `apps/dashboard/src/lib/kalshiApi.ts:650`              |
| Sport selector (pills + keyboard)         | Working         | `SportSelector.tsx` + `useHotkeys`                     |
| Market detail route `/app/market/:ticker` | Working         | `main.tsx`, `App.tsx`                                  |

---

_This spec is the single source of truth. If it's not in here, it's not in scope._
