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 Line Movement Tracker That Spots Sharp Money

Track how odds change over time to detect sharp money movement and steam moves. This guide uses the FieldFunded SDK to poll odds, store historical snapshots, and alert on significant shifts.

What You’ll Use

SDK MethodEndpointPurpose
getEvents()GET /v1/eventsList upcoming events with odds
getEventOdds()GET /v1/events/{id}/oddsGet current odds for an event
getEvent()GET /v1/events/{id}Get event detail with all markets
getLive()GET /v1/liveTrack live events

Architecture

Poll getEventOdds() every N minutes

Store snapshot in database (timestamp, event_id, market, odds)

Compare with previous snapshot

If change > threshold → Alert (Discord webhook, email)

Step 1: Set Up

npm install @fieldfunded/sdk better-sqlite3 node-cron
import { FieldFundedSDK } from '@fieldfunded/sdk';
import Database from 'better-sqlite3';
import cron from 'node-cron';

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

const db = new Database('odds_history.db');

// Create snapshots table
db.exec(`
  CREATE TABLE IF NOT EXISTS odds_snapshots (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
    event_id TEXT,
    home_team TEXT,
    away_team TEXT,
    market_name TEXT,
    selection_name TEXT,
    odds REAL,
    UNIQUE(timestamp, event_id, market_name, selection_name)
  )
`);

Step 2: Snapshot Odds

async function snapshotOdds(sport: string = 'soccer') {
  const events = await client.getEvents({
    sport,
    status: 'prematch',
    starts_within: '24h',
  });

  for (const event of events.events.slice(0, 20)) {
    const detail = await client.getEvent(event.id);

    if (!detail.markets) continue;

    // Track main markets (1x2, Over/Under, BTTS)
    const mainMarkets = detail.markets.filter((m: any) =>
      ['Match Winner', '1X2', 'Over/Under 2.5', 'Both Teams to Score'].includes(m.name)
    );

    const insert = db.prepare(`
      INSERT OR IGNORE INTO odds_snapshots
      (event_id, home_team, away_team, market_name, selection_name, odds)
      VALUES (?, ?, ?, ?, ?, ?)
    `);

    for (const market of mainMarkets) {
      for (const selection of market.selections) {
        insert.run(
          event.id,
          event.home_team,
          event.away_team,
          market.name,
          selection.name,
          selection.odds
        );
      }
    }
  }
}

Step 3: Detect Movement

function detectMovement(threshold: number = 0.05) {
  const movements = db.prepare(`
    SELECT
      s1.event_id,
      s1.home_team,
      s1.away_team,
      s1.market_name,
      s1.selection_name,
      s1.odds as current_odds,
      s2.odds as previous_odds,
      (s1.odds - s2.odds) as change,
      ABS(s1.odds - s2.odds) / s2.odds as pct_change
    FROM odds_snapshots s1
    JOIN odds_snapshots s2 ON
      s1.event_id = s2.event_id AND
      s1.market_name = s2.market_name AND
      s1.selection_name = s2.selection_name AND
      s1.id > s2.id
    WHERE ABS(s1.odds - s2.odds) / s2.odds > ?
    ORDER BY s1.timestamp DESC
    LIMIT 20
  `).all(threshold);

  return movements;
}

Step 4: Alert via Discord Webhook

async function sendAlert(movement: any) {
  const direction = movement.change > 0 ? '📈' : '📉';
  const color = movement.change > 0 ? 0x00FF00 : 0xFF0000;

  await fetch(process.env.DISCORD_WEBHOOK_URL!, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      embeds: [{
        title: `${direction} Line Movement Detected`,
        color,
        fields: [
          { name: 'Match', value: `${movement.home_team} vs ${movement.away_team}`, inline: false },
          { name: 'Market', value: `${movement.market_name}${movement.selection_name}`, inline: false },
          { name: 'Previous', value: `${movement.previous_odds}`, inline: true },
          { name: 'Current', value: `${movement.current_odds}`, inline: true },
          { name: 'Change', value: `${(movement.pct_change * 100).toFixed(1)}%`, inline: true },
        ],
      }],
    }),
  });
}

Step 5: Schedule

// Snapshot every 10 minutes (144 req/day for 20 events ≈ 4,320/month)
cron.schedule('*/10 * * * *', async () => {
  await snapshotOdds('soccer');
  const movements = detectMovement(0.05); // 5% threshold

  for (const m of movements) {
    await sendAlert(m);
  }
});

Rate Limit Math

Polling frequencyEvents trackedRequests/dayMonthly (free tier?)
Every 10 min20 events~288~8,640 ✅
Every 5 min20 events~576~17,280 (Starter)
Every 2 min50 events~3,600~108,000 (Pro)
The free tier (10K/month) supports tracking 20 events every 10 minutes.

Events API Reference

See events endpoint filters →

Get Your Free API Key

Start tracking lines today — 10,000 free requests/month