Guide

FanDuel props in JavaScript and TypeScript

Everything here is plain fetch on Node 18 or later: a small typed helper, the board, paging, one prop's history, the change feed and the live stream. Copy the helper once and every other block works.

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

1. A typed helper

It sends the key, waits out a 429 using its Retry-After, and returns the body with its meta.

api.ts
// Node 18+: fetch is built in. Keep the key on the server.
const BASE = "https://fanduelapi.com/api/v1";
const KEY = process.env.FANDUEL_API_KEY;

type Meta = { plan: string; delay_seconds: number; as_of: string; source_status: string };
type Body<T> = { data: T; meta: Meta & { has_more?: boolean; next_cursor?: string } };

export async function get<T>(path: string, params: Record<string, string | number> = {}): Promise<Body<T>> {
  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: { Authorization: `Bearer ${KEY}` } });
    if (res.status === 429) { await new Promise((r) => setTimeout(r, Number(res.headers.get("Retry-After") ?? 5) * 1000)); continue; }
    if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
    return res.json() as Promise<Body<T>>;
  }
}

2. Read the board

props.ts
type Prop = { id: string; player: string; stat: string; line: number | null; over_american: number | null; under_american: number | null; move: { line_moves: number } | null };

const { data, meta } = await get<Prop[]>("/props", { league: "NBA", stat: "points", limit: 50 });
console.log(`plan ${meta.plan}, ${meta.delay_seconds}s behind, as of ${meta.as_of}`);
for (const p of data) console.log(p.player.padEnd(24), p.line, p.over_american, p.under_american);

3. Page through everything

paging.ts
// Page through the whole board with limit and offset.
async function allProps(league: string) {
  const out: Prop[] = [];
  for (let offset = 0; ; offset += 200) {
    const page = await get<Prop[]>("/props", { league, limit: 200, offset });
    out.push(...page.data);
    if (!page.meta.has_more) return out;
  }
}

4. One prop's history

history.ts
type History = {
  prop: { id: string; player: string; stat: string };
  move: { line_moves: number; price_moves: number } | null;
  history: { observed_at: string; line: number | null; over_american: number | null }[];
};
const { data: h } = await get<History>(`/props/${data[0].id}/history`);
console.log(`${h.prop.player}: opened ${h.history[0]?.line}, now ${h.history.at(-1)?.line}, ${h.history.length} states`);

5. Follow the change feed

changes.ts
// Stay current for one request per poll: read only what changed since your cursor.
let since = "0";
setInterval(async () => {
  const page = await get<{ kind: string; player: string; previous: unknown; current: unknown }[]>("/changes", { since, limit: 500 });
  for (const c of page.data) if (c.kind === "line_move") console.log(c.player, c.previous, "->", c.current);
  since = page.meta.next_cursor ?? since;
}, 15_000);

The cursor is how a client never misses or repeats a change. See the change feed.

6. Or take the stream

stream.ts
// Or let the server push: Server-Sent Events with resume.
const res = await fetch(`${BASE}/stream?league=NBA`, { headers: { Authorization: `Bearer ${KEY}`, Accept: "text/event-stream" } });
const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += value;
  let end;
  while ((end = buffer.indexOf("\n\n")) >= 0) {
    const frame = buffer.slice(0, end); buffer = buffer.slice(end + 2);
    const data = frame.split("\n").find((l) => l.startsWith("data: "));
    if (data) console.log(JSON.parse(data.slice(6)));
  }
}

Reconnect with the last id as Last-Event-ID to resume. Production and up can receive the same events as signed webhooks instead.

Questions

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 and reads the same data five minutes behind.

Get a free API key