Skip to main content

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 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

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 MethodEndpointPurpose
getEventOdds()GET /v1/events/{id}/oddsGet current odds for an event
getLive()GET /v1/liveList in-play events
getScores()GET /v1/scoresLive scores

Step 1: Server-Side Polling Engine

// 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:
// 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

// 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:
/* 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; }
}
// 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 trackedPoll intervalRequests/dayMonthlyPlan
5 events15s28,800864,000Pro ($79)
5 events30s14,400432,000Pro ($79)
10 events30s28,800864,000Ultra ($149)
3 events60s4,320129,600Starter ($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

Get Your Free API Key

Start building in 5 minutes — 10,000 free requests/month

See Pricing

All plans compared side by side