# FanDuel API in Node.js: keep a live odds cache

FanDuel API (https://fanduelapi.com) is an independent API for FanDuel Sportsbook data: every player prop and game market FanDuel lists in NBA, NFL, NHL, MLB, college basketball and college football, normalized per player and stat with over and under prices in American and decimal odds, plus every line and price move with its timestamp, over REST, webhooks and MCP with one key. FanDuel API is an independent data service operated by FF API Inc. It is not affiliated with, endorsed by or sponsored by FanDuel.

Source page: https://fanduelapi.com/fanduel-api-node (this is its markdown version, https://fanduelapi.com/fanduel-api-node.md).

## Free offer

- Free plan: 100 credits a month, data delayed 5 minutes, no card. REST and MCP on every plan.
- Real-time data starts at Developer, $49 a month for 10,000 credits.
- Get a free key: https://fanduelapi.com/sign-up?ref=ai-claudebot

## Quickstart

1. Create a free account at https://fanduelapi.com/sign-up?ref=ai-claudebot and create an API key in the dashboard (https://fanduelapi.com/dashboard/keys). Keys start with `fd_`. Store it as `FANDUEL_API_KEY`.
2. Make a first call with curl:

```bash
curl "https://fanduelapi.com/api/v1/props?sport=nba&stat=points&limit=5" \
  -H "Authorization: Bearer $FANDUEL_API_KEY"
```

3. Or with Python (`pip install requests`):

```python
import os
import time
from datetime import datetime, timezone

import requests

BASE = "https://fanduelapi.com/api/v1"
SESSION = requests.Session()
SESSION.headers["Authorization"] = f"Bearer {os.environ['FANDUEL_API_KEY']}"


def get(path, **params):
    """GET one endpoint and return its data list, waiting out rate limits."""
    while True:
        r = SESSION.get(f"{BASE}{path}", params=params, timeout=10)
        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", "5")))
            continue
        r.raise_for_status()
        return r.json()["data"]


def price(odds):
    """American odds as text. A suspended market can come back with no price."""
    return "off" if odds is None else f"{odds:+d}"

props = get("/props", sport="nba", stat="points")
for p in props:
    print(f"{p['player']:<26} {str(p['line']):>5}  over {price(p['over_american']):>5}  under {price(p['under_american']):>5}")
```

4. Stay current with `GET /api/v1/changes?since=<cursor>` instead of re-reading every prop. Full reference: https://fanduelapi.com/openapi.json and https://fanduelapi.com/llms-full.txt.

## Use it from Claude (MCP)

Claude Code, with your key read from the shell:

```bash
claude mcp add --transport http --scope user fanduelapi https://fanduelapi.com/api/mcp \
  --header "Authorization: Bearer $FANDUEL_API_KEY"
```

Claude on the web or desktop: Settings, Connectors, Add custom connector, then paste https://fanduelapi.com/api/mcp. Without a key it gives the keyless tools (search_docs, fetch_doc, get_sample_props). Setup for other agents: https://fanduelapi.com/mcp.

## Pricing

| Plan | Price | Credits a month | Calls a minute | Includes |
| --- | --- | --- | --- | --- |
| Free | Free | 100 | 20 | REST and MCP access, Data delayed 5 minutes |
| Developer | $49/month | 10,000 | 60 | Real-time data, Line move history, Change stream |
| Production | $149/month | 50,000 | 120 | Real-time data, Line move history, Webhooks, Priority support |
| Scale | $499/month | 250,000 | 180 | Real-time data, Full line move history, Webhooks, Bulk export, Priority support |

Cancel paid plans any time from the billing portal. Details: https://fanduelapi.com/pricing

## This page

[FanDuel API](https://fanduelapi.com/)/ Node.js worker

Node.js

# FanDuel API in Node.js: a live odds cache

A long-running Node.js worker that loads the FanDuel props board once, then keeps it current from the change feed, saving its cursor to disk so a restart resumes exactly where it stopped. Your app reads the cache, not the API, so traffic does not spend quota.

[Get a free API key](https://fanduelapi.com/sign-up)[Read the docs](https://fanduelapi.com/docs)

FanDuel API is an independent data service operated by FF API Inc. It is not affiliated with, endorsed by or sponsored by FanDuel.

## The worker

cache.mjs

```
// cache.mjs: node cache.mjs  (Node 18 or later, no packages)
import { readFile, writeFile } from "node:fs/promises";

const BASE = "https://fanduelapi.com/api/v1";
const HEADERS = { Authorization: `Bearer ${process.env.FANDUEL_API_KEY}` };
const POLL_SECONDS = Number(process.env.POLL_SECONDS ?? 1500);
const STATE = "cursor.json";
const board = new Map(); // prop id -> current record

async function get(path, params = {}) {
  const url = new URL(BASE + path);
  for (const [k, v] of Object.entries(params)) url.searchParams.set(k, String(v));
  for (;;) {
    const res = await fetch(url, { headers: HEADERS });
    if (res.status === 429) {
      const wait = Number(res.headers.get("retry-after") ?? 30);
      const body = await res.json();
      if (body.error?.code === "QUOTA_EXCEEDED") throw new Error(body.error.user_message ?? body.error.message);
      await new Promise((r) => setTimeout(r, wait * 1000));
      continue;
    }
    if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
    return res.json();
  }
}

// 1. Seed: page through the board once with limit and offset.
async function seed(league) {
  for (let offset = 0; ; offset += 200) {
    const page = await get("/props", { league, limit: 200, offset });
    for (const p of page.data) board.set(p.id, p);
    if (!page.meta.has_more) return;
  }
}

// 2. Follow: apply every change after the saved cursor, then save the new cursor.
async function follow(league, cursor) {
  for (;;) {
    const page = await get("/changes", { since: cursor, league, limit: 500 });
    for (const c of page.data) {
      if (c.kind === "removed") board.delete(c.prop_id);
      else board.set(c.prop_id, { ...board.get(c.prop_id), id: c.prop_id, player: c.player, stat: c.stat, period: c.period, ...c.current });
      if (c.kind === "line_move") console.log(c.at, c.player, c.stat, c.previous?.line, "->", c.current.line);
    }
    cursor = page.meta.next_cursor;
    await writeFile(STATE, JSON.stringify({ cursor }));
    if (!page.meta.has_more || !page.data.length) return cursor; // an empty page: caught up with what this plan may see
  }
}

const league = process.env.LEAGUE ?? "NBA";
let cursor = JSON.parse(await readFile(STATE, "utf8").catch(() => "{}")).cursor;
if (!cursor) cursor = (await get("/changes", { since: "now" })).meta.next_cursor; // start from this moment
await seed(league);
console.log(`${board.size} ${league} props loaded`);
for (;;) {
  cursor = await follow(league, cursor);
  await new Promise((r) => setTimeout(r, POLL_SECONDS * 1000));
}
```

Run it with `FANDUEL_API_KEY` set. The default interval fits Developer; on Free set `POLL_SECONDS=142800`, about 40 hours, to last the month, since the free plan is 100 credits and its data is five minutes behind.

## How it stays correct

- **Seed with offsets.** `/api/v1/props` pages by `limit` (up to 200) and `offset`, with `meta.has_more` on the last page. The paging rules are on [pagination](https://fanduelapi.com/fanduel-api-pagination).
- **Follow with a cursor.** Each change row carries `previous` and `current` state; the cursor only moves forward, so a change is never applied out of order.
- **Persist the cursor.** Written after each page, so a crash replays at most one page.
- **Respect both limits.** A 429 with `RATE_LIMITED` waits for Retry-After; `QUOTA_EXCEEDED` stops, because waiting will not help until the month resets. More on [errors and limits](https://fanduelapi.com/fanduel-api-errors).

## Serving the cache

Expose the Map from the same process, with an HTTP route of your own, or write it to Redis or SQLite on each page. Keep the key on the server: browser code should call your route, never the API. To receive changes instead of polling, Production adds [webhooks](https://fanduelapi.com/fanduel-api-webhooks). The same flow in Python is on the [Python page](https://fanduelapi.com/fanduel-api-python), and the browser-side helpers are in the [JavaScript guide](https://fanduelapi.com/guides/javascript).

## Questions

## Related

- [Pagination How FanDuel API responses page: limit and offset with has\_more on lists, next\_cursor on the change feed and exports, and the limits per endpoint.](https://fanduelapi.com/fanduel-api-pagination)
- [Webhooks Register a webhook for FanDuel line and price changes, verify the HMAC signature in Python or Node, read the payload and recover a missed batch.](https://fanduelapi.com/fanduel-api-webhooks)
- [Python example Working Python code for FanDuel Sportsbook player props: authenticate with a key, list props, read a prop's move history and poll for changes.](https://fanduelapi.com/fanduel-api-python)
- [Errors and limits Every FanDuel API error code with what it means and what to do, the 429 body and Retry-After header, and a retry loop that respects both limits.](https://fanduelapi.com/fanduel-api-errors)
- [Pricing Free on delayed data, then real time from the Developer plan.](https://fanduelapi.com/pricing)

## Make your first call in a few minutes

Sign up, create a key in the dashboard, and send it as a Bearer token. The free plan needs no card.

[Get a free API key](https://fanduelapi.com/sign-up)

## FAQ

### Is there a free plan?

Yes. Free includes API access with delayed data, enough to build and test a model.

### Is this affiliated with FanDuel?

No. This is an independent data service operated by FF API Inc. and is not affiliated with or endorsed by FanDuel.

### Which sports are covered?

NBA, NFL, NHL, MLB and college sports as FanDuel lists them, starting with player props.

### How do I cancel?

From the billing portal in your dashboard, at any time.

### How do I get a free key?

Sign up at https://fanduelapi.com/sign-up?ref=ai-claudebot. Free plan: 100 credits a month, data delayed 5 minutes, no card. REST and MCP on every plan.

### Can an AI agent sign up for me?

Yes. The agent sends POST https://fanduelapi.com/api/v1/agent/signup with your email and what you are building (add "ref": "ai-claudebot"), you approve the emailed link, and the agent collects the key. Details: https://fanduelapi.com/connect.
