# Polymarket Unification Investigation

**Date:** 2026-02-04
**Status:** In Progress
**Goal:** Determine what's needed to add Polymarket support to the generic multi-sport stream

---

## Executive Summary

**Key Finding:** Polymarket has game-level markets for **NBA, NFL, and MLB** (but NHL appears inactive). All use the same slug pattern, making unification feasible.

**Recommendation:** Polymarket can be added to `createSportsStream()` using sport-specific slug generation, similar to how NBA does it today.

---

## Current Architecture

### Two Separate Systems

| Aspect          | NBA Stream                                        | Multi-Sport Stream                                              |
| --------------- | ------------------------------------------------- | --------------------------------------------------------------- |
| **Location**    | `lib/stream.ts` → `createConsolidatedNbaStream()` | `lib/sportsStream/stream.ts` → `createSportsStream()`           |
| **Venues**      | Kalshi + Polymarket                               | Kalshi only                                                     |
| **Output Type** | `ConsolidatedGameBooks`                           | `MoneylineGameData` → converted via `moneylineToConsolidated()` |
| **Team Names**  | NOT populated                                     | Populated from event title                                      |
| **Data Path**   | `consolidatedGames` prop directly                 | `sportsGames` Map with sport-market key                         |

### Data Flow in ValueDashboardView.tsx

```typescript
// Lines 224-234 - NBA is special-cased
const convertedMoneylineGames = useMemo(() => {
  if (selectedSport === 'nba' || selectedMarketType !== 'moneyline') {
    return []; // NBA uses consolidatedGames directly
  }
  return (currentGames as MoneylineGameData[]).map(moneylineToConsolidated);
}, [currentGames, selectedSport, selectedMarketType]);

const moneylineGamesForView = selectedSport === 'nba' ? consolidatedGames : convertedMoneylineGames;
```

---

## Polymarket API Organization

### Data Hierarchy

```
/sports (leagues)
    └── series_id → /events?series_id=X (games/matches)
                        └── event → markets[] (moneyline, spread, total, props)
```

### Key Endpoints

| Endpoint              | Purpose                           | Key Params                                                    |
| --------------------- | --------------------------------- | ------------------------------------------------------------- |
| `/sports`             | List all leagues (NBA, NFL, etc.) | -                                                             |
| `/events?series_id=X` | Get games for a league            | `series_id`, `tag_id=100639` (games only), `active`, `closed` |
| `/events/{id}`        | Get single event with all markets | -                                                             |
| `/markets?slug=X`     | Get specific market by slug       | `slug`                                                        |

### Sport Configuration (from `/sports`)

| Sport     | Series ID | Ordering | Tags                   | Slug Pattern                          |
| --------- | --------- | -------- | ---------------------- | ------------------------------------- |
| **NBA**   | 10345     | away     | 1,745,100639           | `nba-{away}-{home}-{date}`            |
| **NFL**   | 10187     | away     | 1,450,100639           | `nfl-{away}-{home}-{date}`            |
| **MLB**   | 3         | away     | 1,100639,100381        | `mlb-{away}-{home}-{date}`            |
| **NHL**   | 10346     | away     | 1,899,100639           | `nhl-{away}-{home}-{date}` (inactive) |
| **CBB**   | 10470     | away     | 1,101178,100639,101954 | `cbb-{away}-{home}-{date}`            |
| **NCAAB** | 39        | home     | 1,100149,100639        | `ncaab-...` (seasonal)                |
| **ATP**   | 10365     | home     | 1,864,100639,101232    | `atp-{player1}-{player2}-{date}`      |
| **WTA**   | 10366     | home     | 1,864,100639,102123    | `wta-{player1}-{player2}-{date}`      |

- `ordering: away` means first entity in slug is the away team
- `ordering: home` means first entity in slug is the home team (or higher-seeded player for tennis)
- `tag_id=100639` = "Games" tag (filters to game-level bets vs futures)

### Market Types (`sportsMarketType` field)

| Type                   | Outcomes             | Slug Pattern                              |
| ---------------------- | -------------------- | ----------------------------------------- |
| `moneyline`            | [Team1, Team2]       | `nba-bos-hou-2026-02-04`                  |
| `spreads`              | [Favorite, Underdog] | `nba-bos-hou-2026-02-04-spread-home-6pt5` |
| `totals`               | [Over, Under]        | `nba-bos-hou-2026-02-04-total-216pt5`     |
| `first_half_moneyline` | [Team1, Team2]       | `...-1h-moneyline`                        |
| `first_half_spreads`   | [Favorite, Underdog] | `...-1h-spread-home-3pt5`                 |
| `first_half_totals`    | [Over, Under]        | `...-1h-total-111pt5`                     |
| `points`               | [Yes, No]            | `...-points-kevin-durant-24pt5`           |
| `rebounds`             | [Yes, No]            | `...-rebounds-jaylen-brown-7pt5`          |
| `assists`              | [Yes, No]            | `...-assists-derrick-white-5pt5`          |

### Querying Games by Sport

```bash
# Get all supported sports leagues
curl "https://gamma-api.polymarket.com/sports"

# Get NBA games (series_id=10345, tag=100639 for game bets only)
curl "https://gamma-api.polymarket.com/events?series_id=10345&tag_id=100639&active=true&closed=false"

# Get all markets for a specific game
curl "https://gamma-api.polymarket.com/events/192535"  # Returns 36+ markets per game
```

---

## Polymarket API Findings

### Gamma API Endpoints

- **Base URL:** `https://gamma-api.polymarket.com`
- **Sports metadata:** `/sports` (returns all leagues with series_id)
- **Events by series:** `/events?series_id=X&tag_id=100639`
- **Markets by slug:** `/markets?slug={slug}`

### Slug Pattern (Confirmed Working)

All sports use the same pattern:

```
{sport}-{awayCode}-{homeCode}-{YYYY-MM-DD}
```

Examples:

- NBA: `nba-bos-hou-2026-02-04` ✅
- NFL: `nfl-kc-phi-2025-02-09` ✅ (Super Bowl)
- MLB: `mlb-lad-tor-2025-10-31` ✅
- NHL: No recent games found (series exists but inactive since 2023)

### Market Structures (All Types Available)

**Moneyline:**

```json
{
  "slug": "nba-bos-hou-2026-02-04",
  "question": "Celtics vs. Rockets",
  "sportsMarketType": "moneyline",
  "outcomes": "[\"Celtics\", \"Rockets\"]",
  "conditionId": "0x8d25c553b41d6760febe58b0ce5c9a3cb4509e53302989f3e10bb7f5a34a2148",
  "clobTokenIds": "[\"token1...\", \"token2...\"]",
  "orderPriceMinTickSize": 0.01,
  "negRisk": false
}
```

**Spread:**

```json
{
  "slug": "nba-bos-hou-2026-02-04-spread-home-6pt5",
  "question": "Spread: Rockets (-6.5)",
  "sportsMarketType": "spreads",
  "outcomes": "[\"Rockets\", \"Celtics\"]",
  "conditionId": "0xc59679b21d854ae828b3919d8113a6b94be945d255a2113d12a17a6a62c31cc4",
  "clobTokenIds": "[\"token1...\", \"token2...\"]",
  "orderPriceMinTickSize": 0.01,
  "negRisk": false
}
```

**Total:**

```json
{
  "slug": "nba-bos-hou-2026-02-04-total-216pt5",
  "question": "Celtics vs. Rockets: O/U 216.5",
  "sportsMarketType": "totals",
  "outcomes": "[\"Over\", \"Under\"]",
  "conditionId": "0xd5075da73dc073395fac25dfacfb39d593fd6830fc42fdb957d2a88a2afc17f1",
  "clobTokenIds": "[\"token1...\", \"token2...\"]",
  "orderPriceMinTickSize": 0.01,
  "negRisk": false
}
```

All market types share identical structure - `conditionId`, `clobTokenIds`, `orderPriceMinTickSize`, `negRisk` - enabling unified streaming.

### Dashboard Data Requirements vs Polymarket Fields

| Dashboard Need   | Field                   | Polymarket Source              | Available? |
| ---------------- | ----------------------- | ------------------------------ | ---------- |
| **Moneylines**   |                         |                                |            |
| Away/Home teams  | `outcomes`              | `"[\"Celtics\", \"Rockets\"]"` | ✅         |
| Orderbook prices | `clobTokenIds`          | Token IDs for WS subscription  | ✅         |
| Tick size        | `orderPriceMinTickSize` | `0.01`                         | ✅         |
| Market ID        | `conditionId`           | Hex string                     | ✅         |
| **Spreads**      |                         |                                |            |
| Spread value     | Slug parsing            | `...-spread-home-6pt5` → 6.5   | ✅         |
| Favored team     | `outcomes[0]`           | First outcome is favorite      | ✅         |
| Orderbook prices | `clobTokenIds`          | Token IDs for WS subscription  | ✅         |
| **Totals**       |                         |                                |            |
| Strike value     | Slug parsing            | `...-total-216pt5` → 216.5     | ✅         |
| Over/Under       | `outcomes`              | `"[\"Over\", \"Under\"]"`      | ✅         |
| Orderbook prices | `clobTokenIds`          | Token IDs for WS subscription  | ✅         |

**Conclusion:** Polymarket provides ALL fields needed to populate dashboard tables for moneylines, spreads, and totals.

### How to Access Markets

**Method 1: Direct slug lookup (current approach for moneylines)**

```bash
# Know the game, construct slug, fetch market
curl "https://gamma-api.polymarket.com/markets?slug=nba-bos-hou-2026-02-04"
```

**Method 2: Event-based discovery (recommended for spreads/totals)**

```bash
# 1. Get event by slug (contains all markets)
curl "https://gamma-api.polymarket.com/events?slug=nba-bos-hou-2026-02-04"

# Response includes markets array with ALL market types:
{
  "slug": "nba-bos-hou-2026-02-04",
  "title": "Celtics vs. Rockets",
  "markets": [
    { "sportsMarketType": "moneyline", ... },
    { "sportsMarketType": "spreads", "slug": "...-spread-home-6pt5", ... },
    { "sportsMarketType": "spreads", "slug": "...-spread-home-5pt5", ... },
    { "sportsMarketType": "totals", "slug": "...-total-216pt5", ... },
    { "sportsMarketType": "totals", "slug": "...-total-215pt5", ... },
    { "sportsMarketType": "points", ... },  // player props
    ...
  ]
}
```

**Method 3: Sport-wide discovery**

```bash
# Get all active NBA games
curl "https://gamma-api.polymarket.com/events?series_id=10345&tag_id=100639&active=true&closed=false"
```

### Sports Coverage & Market Depth

| Sport     | Series ID | Status      | Moneyline | Spreads | Totals | 1H Markets | Props | Notes               |
| --------- | --------- | ----------- | --------- | ------- | ------ | ---------- | ----- | ------------------- |
| **NBA**   | 10345     | ✅ Active   | ✅        | ✅      | ✅     | ✅         | ✅    | 36 markets/game     |
| **NFL**   | 10187     | ✅ Active   | ✅        | ✅      | ✅     | ✅         | ✅    | Similar to NBA      |
| **MLB**   | 3         | ✅ Active   | ✅        | ❓      | ❓     | ❓         | ❓    | Needs verification  |
| **NHL**   | 10346     | ❌ Inactive | -         | -       | -      | -          | -     | Dead since Apr 2023 |
| **CBB**   | 10470     | ✅ Active   | ✅        | ❌      | ❌     | ❌         | ❌    | Moneyline only      |
| **NCAAB** | 39        | 🟡 Seasonal | ✅        | ❓      | ❓     | ❓         | ❓    | March Madness       |
| **ATP**   | 10365     | ✅ Active   | ✅        | ✅\*    | ✅\*   | ❌         | ❌    | Tennis-specific     |
| **WTA**   | 10366     | ✅ Active   | ✅        | ✅\*    | ✅\*   | ❌         | ❌    | Tennis-specific     |

\*Tennis uses sport-specific market types (see below)

**Tennis Market Types (`sportsMarketType`):**

- `moneyline` - match winner
- `tennis_set_handicap` - set spread (e.g., -1.5 sets)
- `tennis_first_set_winner` - first set winner
- `tennis_first_set_totals` - first set games O/U (8.5, 9.5, 10.5)
- `tennis_match_totals` - total games in match O/U (21.5, 22.5, 23.5)
- `tennis_set_totals` - total sets O/U (2.5)

**Other Sports:** 100+ leagues available (soccer, esports, cricket, etc.) via `/sports` endpoint.

---

## Existing NBA Polymarket Integration

**Location:** `apps/dashboard/src/lib/stream.ts` lines 330-419

### Key Functions

1. **Slug Generation** (`lib/polymarket/gamma.ts`):

```typescript
export function makeNbaPolySlug(opts: {
  dateYyyyMmDd: string;
  awayCode: string;
  homeCode: string;
}): string {
  return `nba-${opts.awayCode.toLowerCase()}-${opts.homeCode.toLowerCase()}-${opts.dateYyyyMmDd}`;
}
```

2. **Market Resolution** (`resolvePolyMarketInfoBySlug`):
   - Calls Gamma API to get conditionId and tokenIds
   - Returns info needed for orderbook subscription

3. **WebSocket Subscription**:
   - Uses `createPolyMarketStream()` for orderbook updates
   - Maps tokenIds to events for merging data

---

## What's Needed for Unification

### 1. Generalize Slug Generation

Create a sport-agnostic slug function:

```typescript
// Proposed: lib/polymarket/slugs.ts
export function makePolySlug(opts: {
  sport: Sport;
  awayCode: string;
  homeCode: string;
  date: string; // YYYY-MM-DD
}): string {
  const sportPrefix = POLY_SPORT_PREFIX[opts.sport]; // 'nba', 'nfl', 'mlb'
  if (!sportPrefix) return ''; // Sport not supported
  return `${sportPrefix}-${opts.awayCode.toLowerCase()}-${opts.homeCode.toLowerCase()}-${opts.date}`;
}

const POLY_SPORT_PREFIX: Partial<Record<Sport, string>> = {
  nba: 'nba',
  nfl: 'nfl',
  mlb: 'mlb',
  // NHL inactive, tennis/CBB not on Polymarket
};
```

### 2. Add Polymarket to SportsStream

Modify `createSportsStream()` to optionally include Polymarket:

```typescript
interface SportsStreamOptions {
  sport: Sport;
  marketType: MarketType;
  daysAhead?: number;
  includePolymarket?: boolean; // NEW
}
```

The stream would:

1. Discover Kalshi markets (existing)
2. For each game, generate Poly slug and resolve via Gamma API
3. Subscribe to both Kalshi WS and Poly WS
4. Merge orderbooks into unified structure

### 3. Decide on Output Type

**Option A:** Keep `MoneylineGameData` simple (single venue), create separate Poly data

- Pros: Simpler types, backward compatible
- Cons: Two separate data flows to manage

**Option B:** Extend `MoneylineGameData` to support multiple venues

- Pros: Single data structure
- Cons: Breaking change to existing consumers

**Option C:** Always output `ConsolidatedGameBooks` from multi-sport stream

- Pros: Unified with NBA, supports dual venues
- Cons: More complex conversion

### 4. Team Name Population

The NBA stream doesn't populate `awayName`/`homeName` but multi-sport stream does. When unified, ensure team names flow through.

---

## CLI Tool Created

**Location:** `apps/dashboard/scripts/explore-polymarket.ts`

```bash
# Commands available
npx ts-node apps/dashboard/scripts/explore-polymarket.ts search "Lakers"
npx ts-node apps/dashboard/scripts/explore-polymarket.ts slug "nba-bos-mia-2026-02-04"
npx ts-node apps/dashboard/scripts/explore-polymarket.ts series
npx ts-node apps/dashboard/scripts/explore-polymarket.ts events nba-2026
```

---

## Open Questions

1. ~~**Should NHL be supported?**~~ **ANSWERED:** NHL is inactive on Polymarket since 2023. Skip NHL integration.

2. ~~**What about spreads/totals?**~~ **ANSWERED:** Polymarket HAS spreads and totals with identical structure to moneylines. Full integration is possible.

3. **Rate limiting?** How many Gamma API calls are acceptable? May need caching strategy. The event-based approach (one call per game returns all markets) is more efficient than per-market-type calls.

4. **Token mapping complexity?** NBA stream maintains `polyTokenToEvent` map. For spreads/totals, need to map tokens to (eventTicker, marketType, strike).

5. **When to skip Polymarket?** Skip for: NHL (inactive), tennis, CBB. Include for: NBA, NFL, MLB.

6. **Spread/total slug parsing:** Need to extract strike values from slugs like `...-spread-home-6pt5` → 6.5 and `...-total-216pt5` → 216.5.

---

## Next Steps

1. **[x] Verify MLB/NFL market structure** - ✅ Confirmed identical structure (conditionId, clobTokenIds, orderPriceMinTickSize, negRisk)

2. **[x] Investigate Polymarket spreads/totals** - ✅ Available with same structure as moneylines

3. **[ ] Create generalized slug generator** - `makePolySlug(sport, away, home, date)` for base slug

4. **[ ] Create market-specific slug builders:**
   - `makePolySpreadSlug(baseSlug, strike, homeOrAway)` → `nba-bos-hou-2026-02-04-spread-home-6pt5`
   - `makePolyTotalSlug(baseSlug, strike)` → `nba-bos-hou-2026-02-04-total-216pt5`

5. **[ ] Update event-based market resolution:**
   - Fetch event by slug: `/events?slug=nba-bos-hou-2026-02-04`
   - Parse all markets from response (moneyline, spreads, totals)
   - Extract strike values from slugs

6. **[ ] Prototype Polymarket addition to sportsStream** - Start with NBA spreads/totals since we have working moneyline integration

7. **[ ] Decide on output type** - Option A, B, or C above

8. **[ ] Add team name population to NBA** - Fix the missing `awayName`/`homeName` in NBA stream

---

## Files to Reference

| File                                      | Purpose                                      |
| ----------------------------------------- | -------------------------------------------- |
| `lib/stream.ts`                           | NBA consolidated stream with Polymarket      |
| `lib/sportsStream/stream.ts`              | Multi-sport stream (Kalshi only)             |
| `lib/sportsStream/types.ts`               | GameData types + `moneylineToConsolidated()` |
| `lib/polymarket/gamma.ts`                 | Gamma API client + `makeNbaPolySlug()`       |
| `lib/polymarket/marketStream.ts`          | Polymarket WebSocket client                  |
| `lib/nbaConsolidated/types.ts`            | `ConsolidatedGameBooks` type                 |
| `components/pages/ValueDashboardView.tsx` | Where data flows diverge (line 232)          |
| `scripts/explore-polymarket.ts`           | CLI tool for investigation                   |

---

## Related Code Snippets

### How NBA resolves Polymarket markets (stream.ts ~345-390)

```typescript
const slugsByEvent: Array<{ eventTicker: string; slug: string }> = [];
for (const [eventTicker, g] of gamesByEvent.entries()) {
  slugsByEvent.push({
    eventTicker,
    slug: makeNbaPolySlug({
      dateYyyyMmDd: g.date,
      awayCode: g.awayCode,
      homeCode: g.homeCode,
    }),
  });
}

const infos = await Promise.all(
  slugsByEvent.map(async ({ eventTicker, slug }) => {
    const info = await resolvePolyMarketInfoBySlug({ slug, useRelay });
    return { eventTicker, info };
  })
);
```

### How multi-sport builds games (sportsStream/stream.ts ~241-263)

```typescript
if (marketType === 'moneyline') {
  gameData = buildMoneylineGame(event, eventBooks);
}
// ... apply start time and team names
gamesByEvent.set(eventTicker, gameData);
```

---

## Session Context

- Started investigating why NBA Moneylines shows team codes instead of names
- Discovered NBA uses separate data pipeline from other sports
- NBA is unique because it includes Polymarket integration
- Investigated Polymarket API to see what other sports are supported
- Found NBA, NFL, MLB all use same slug pattern
- NHL appears inactive on Polymarket
- **2026-02-04:** Deep dive into Polymarket API organization. Discovered spreads/totals ARE available (not just moneylines). Documented full API hierarchy and querying patterns.

---

## References

- [Polymarket Gamma API Overview](https://docs.polymarket.com/developers/gamma-markets-api/overview)
- [Get Sports Metadata](https://docs.polymarket.com/api-reference/sports/get-sports-metadata-information)
- [Get Events](https://docs.polymarket.com/developers/gamma-markets-api/get-events)
- [Fetch Markets Guide](https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide)
- [Gamma Structure](https://docs.polymarket.com/developers/gamma-markets-api/gamma-structure)
