> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fieldfunded.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a Real-Time Odds Dashboard with WebSocket

> Build a real-time odds dashboard with live updates. Poll the FieldFunded API efficiently and push odds changes to your frontend instantly.

# Build a Real-Time Odds Dashboard with Live Updates

Display live odds that update automatically without page refreshes. This guide builds a server-side polling layer that fetches odds from the FieldFunded API and pushes changes to connected browsers via WebSocket — giving your users a real-time experience.

## Architecture

```mermaid theme={null}
graph LR
    A["Browser<br/>(React)"] <-->|WebSocket<br/>live updates| B["Your Server<br/>(Node.js)"]
    B -->|HTTP Poll<br/>every 10-30s| C["FieldFunded<br/>API"]
    C -->|data| B
    B --> D["In-Memory<br/>Odds Cache"]
```

Your server polls FieldFunded at a controlled rate, diffs the odds against a local cache, and broadcasts only the changes to connected clients. This approach is more efficient than having every browser poll the API directly.

## What You'll Use

| SDK Method       | Endpoint                   | Purpose                       |
| ---------------- | -------------------------- | ----------------------------- |
| `getEventOdds()` | `GET /v1/events/{id}/odds` | Get current odds for an event |
| `getLive()`      | `GET /v1/live`             | List in-play events           |
| `getScores()`    | `GET /v1/scores`           | Live scores                   |

## Step 1: Server-Side Polling Engine

```typescript theme={null}
// server/poller.ts
import { FieldFundedSDK } from '@fieldfunded/sdk';

const ff = new FieldFundedSDK({
  apiKey: process.env.FIELDFUNDED_API_KEY!,
  baseUrl: 'https://api.fieldfunded.com/v1',
});

interface OddsSnapshot {
  eventId: string;
  markets: Map<string, Map<string, number>>; // market_id -> selection_id -> odds
  updatedAt: number;
}

const oddsCache = new Map<string, OddsSnapshot>();

export interface OddsChange {
  eventId: string;
  marketId: string;
  marketName: string;
  selectionId: string;
  selectionName: string;
  oldOdds: number;
  newOdds: number;
}

export async function pollEventOdds(eventId: string): Promise<OddsChange[]> {
  const data = await ff.getEventOdds(eventId);
  const changes: OddsChange[] = [];
  const previous = oddsCache.get(eventId);
  const newSnapshot: OddsSnapshot = {
    eventId,
    markets: new Map(),
    updatedAt: Date.now(),
  };

  for (const market of data.markets || []) {
    const selMap = new Map<string, number>();
    for (const sel of market.selections) {
      selMap.set(sel.id, sel.odds);

      // Detect changes
      if (previous) {
        const prevMarket = previous.markets.get(market.id);
        const prevOdds = prevMarket?.get(sel.id);
        if (prevOdds !== undefined && prevOdds !== sel.odds) {
          changes.push({
            eventId,
            marketId: market.id,
            marketName: market.name,
            selectionId: sel.id,
            selectionName: sel.name,
            oldOdds: prevOdds,
            newOdds: sel.odds,
          });
        }
      }
    }
    newSnapshot.markets.set(market.id, selMap);
  }

  oddsCache.set(eventId, newSnapshot);
  return changes;
}

export function getCachedOdds(eventId: string): OddsSnapshot | undefined {
  return oddsCache.get(eventId);
}
```

## Step 2: WebSocket Server

Use `ws` or `Socket.IO` to broadcast changes:

```typescript theme={null}
// server/ws.ts
import { WebSocketServer, WebSocket } from 'ws';
import { pollEventOdds, OddsChange } from './poller';

const wss = new WebSocketServer({ port: 8080 });

// Track which events each client is watching
const subscriptions = new Map<WebSocket, Set<string>>();

wss.on('connection', (ws) => {
  subscriptions.set(ws, new Set());

  ws.on('message', (raw) => {
    const msg = JSON.parse(raw.toString());

    if (msg.type === 'subscribe') {
      subscriptions.get(ws)?.add(msg.eventId);
    }

    if (msg.type === 'unsubscribe') {
      subscriptions.get(ws)?.delete(msg.eventId);
    }
  });

  ws.on('close', () => {
    subscriptions.delete(ws);
  });
});

function broadcast(eventId: string, changes: OddsChange[]) {
  for (const [ws, subs] of subscriptions) {
    if (subs.has(eventId) && ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify({ type: 'odds_update', eventId, changes }));
    }
  }
}

// Poll tracked events every 15 seconds
setInterval(async () => {
  const trackedEvents = new Set<string>();
  for (const subs of subscriptions.values()) {
    for (const id of subs) trackedEvents.add(id);
  }

  for (const eventId of trackedEvents) {
    try {
      const changes = await pollEventOdds(eventId);
      if (changes.length > 0) {
        broadcast(eventId, changes);
      }
    } catch (err) {
      console.error(`Poll failed for ${eventId}:`, err);
    }
  }
}, 15_000);
```

## Step 3: Frontend Client

```typescript theme={null}
// client/useOddsStream.ts
import { useEffect, useRef, useState, useCallback } from 'react';

interface OddsChange {
  marketId: string;
  selectionId: string;
  oldOdds: number;
  newOdds: number;
}

export function useOddsStream(eventId: string) {
  const wsRef = useRef<WebSocket | null>(null);
  const [changes, setChanges] = useState<OddsChange[]>([]);

  const connect = useCallback(() => {
    const ws = new WebSocket('ws://localhost:8080');

    ws.onopen = () => {
      ws.send(JSON.stringify({ type: 'subscribe', eventId }));
    };

    ws.onmessage = (event) => {
      const msg = JSON.parse(event.data);
      if (msg.type === 'odds_update' && msg.eventId === eventId) {
        setChanges(msg.changes);
      }
    };

    ws.onclose = () => {
      // Reconnect after 3 seconds
      setTimeout(connect, 3000);
    };

    wsRef.current = ws;
  }, [eventId]);

  useEffect(() => {
    connect();
    return () => wsRef.current?.close();
  }, [connect]);

  return changes;
}
```

## Step 4: Visual Odds Flashing

Make odds changes visible with a brief flash animation:

```css theme={null}
/* Flash green when odds increase, red when they decrease */
.odds-up {
  animation: flash-green 1s ease-out;
}

.odds-down {
  animation: flash-red 1s ease-out;
}

@keyframes flash-green {
  0% { background-color: rgba(34, 197, 94, 0.4); }
  100% { background-color: transparent; }
}

@keyframes flash-red {
  0% { background-color: rgba(239, 68, 68, 0.4); }
  100% { background-color: transparent; }
}
```

```typescript theme={null}
// OddsCell component
function OddsCell({ odds, previousOdds }: { odds: number; previousOdds?: number }) {
  const direction = previousOdds
    ? odds > previousOdds ? 'odds-up' : odds < previousOdds ? 'odds-down' : ''
    : '';

  return (
    <span key={odds} className={direction}>
      {odds.toFixed(2)}
    </span>
  );
}
```

## Rate Limit Math

The server polls FieldFunded, not the browsers. Request volume depends on how many events you track:

| Events tracked | Poll interval | Requests/day | Monthly | Plan           |
| -------------- | ------------- | ------------ | ------- | -------------- |
| 5 events       | 15s           | 28,800       | 864,000 | Pro (\$79)     |
| 5 events       | 30s           | 14,400       | 432,000 | Pro (\$79)     |
| 10 events      | 30s           | 28,800       | 864,000 | Ultra (\$149)  |
| 3 events       | 60s           | 4,320        | 129,600 | Starter (\$29) |

The key optimization: only poll events that have at least one connected viewer. When no one is watching an event, stop polling it entirely. This can reduce API usage by 80% during off-peak hours.

## Production Checklist

* [ ] Only poll events with active subscribers
* [ ] Implement exponential backoff on API errors
* [ ] Add heartbeat pings to detect stale WebSocket connections
* [ ] Log all odds changes for analytics
* [ ] Set `maxPayload` on WebSocket server to prevent memory issues

***

## Related Guides

* [Build a Line Tracker](/guides/build-line-tracker) — track odds movements over time
* [Build a Betting Platform](/guides/build-betting-platform) — full-stack architecture
* [Build a Mobile Odds App](/guides/build-mobile-odds-app) — native mobile implementation
* [FieldFunded vs The Odds API](/compare/vs-theoddsapi) — feature comparison

<CardGroup cols={2}>
  <Card title="Get Your Free API Key" icon="key" href="/quickstart">
    Start building in 5 minutes — 10,000 free requests/month
  </Card>

  <Card title="See Pricing" icon="dollar-sign" href="/compare/pricing">
    All plans compared side by side
  </Card>
</CardGroup>
