Types, adapters, and the first two platform integrations
Initialize the TypeScript project, define the core data models across 7 platforms and 10 sports, create the platform adapter contract, and build the PrizePicks, Underdog, Betr Picks, Sleeper, DraftKings Pick 6, ParlayPlay, and Fliff clients.
Project initialization
Set up the pnpm project with TypeScript, Biome, and all dependencies.
Core type system
Define the constants, enums, and data models. Every platform normalizer must produce Line objects. Every engine function operates on Line[] and Slip[].
src/constants.ts
export const Sport = {
NBA: 'nba',
NFL: 'nfl',
Golf: 'golf',
Tennis: 'tennis',
MLB: 'mlb',
CFB: 'cfb',
WNBA: 'wnba',
LPGA: 'lpga',
KBO: 'kbo',
Esports: 'esports',
MMA: 'mma',
Lacrosse: 'lacrosse',
NASCAR: 'nascar',
} as const;
export type Sport = (typeof Sport)[keyof typeof Sport];
export const BetDirection = {
Over: 'Over',
Under: 'Under',
Higher: 'Higher', // Underdog rival lines
Lower: 'Lower', // Underdog rival lines
} as const;
export type BetDirection = (typeof BetDirection)[keyof typeof BetDirection];
export const Platform = {
PrizePicks: 'prizepicks',
Underdog: 'underdog',
Betr: 'betr',
Sleeper: 'sleeper',
DraftKingsPick6: 'dk-pick6',
ParlayPlay: 'parlayplay',
Fliff: 'fliff',
} as const;
export type Platform = (typeof Platform)[keyof typeof Platform];
export const BasketballProp = {
Points: 'Points',
Rebounds: 'Rebounds',
Assists: 'Assists',
Steals: 'Steals',
Blocks: 'Blocks',
Turnovers: 'Turnovers',
ThreePointMade: '3-Pointers Made',
ThreePointAttempts: '3-Pointer Attempts',
FantasyPoints: 'Fantasy Points',
PointsAssists: 'Pts+Asts',
PointsRebounds: 'Pts+Rebs',
ReboundsAssists: 'Rebs+Asts',
PointsReboundsAssists: 'Pts+Rebs+Asts',
BlocksSteals: 'Blks+Stls',
DoubleDouble: 'Double Double',
} as const;
export type BasketballProp = (typeof BasketballProp)[keyof typeof BasketballProp];
export const FootballProp = {
PassYards: 'Pass Yards',
PassCompletions: 'Completions',
RushYards: 'Rush Yards',
ReceivingYards: 'Receiving Yards',
Receptions: 'Receptions',
Interceptions: 'Interceptions',
PassYardsReceivingYards: 'Pass Yards + Receiving Yards',
CompletionsReceptions: 'Completions + Receptions',
} as const;
export type FootballProp = (typeof FootballProp)[keyof typeof FootballProp];
export const GolfProp = {
Strokes: 'Strokes',
BirdiesOrBetter: 'Birdies or Better',
BogeysOrWorse: 'Bogeys or Worse',
FewerStrokes: 'Fewer Strokes', // Rival lines (head-to-head)
} as const;
export type GolfProp = (typeof GolfProp)[keyof typeof GolfProp];
export const TennisProp = {
Aces: 'Aces',
DoubleFaults: 'Double Faults',
BreakPoints: 'Break Points',
} as const;
export type TennisProp = (typeof TennisProp)[keyof typeof TennisProp];
export const EsportsProp = {
Kills: 'Kills',
Deaths: 'Deaths',
Assists: 'Assists',
HeadshotPct: 'Headshot %',
TotalMaps: 'Total Maps',
FirstBlood: 'First Blood',
} as const;
export type EsportsProp = (typeof EsportsProp)[keyof typeof EsportsProp];
export const WNBAProp = {
// Same structure as BasketballProp — WNBA uses identical stat types
...BasketballProp,
} as const;
export const MmaProp = {
TotalRounds: 'Total Rounds',
SignificantStrikes: 'Significant Strikes',
Takedowns: 'Takedowns',
MethodOfVictory: 'Method of Victory',
} as const;
export type MmaProp = (typeof MmaProp)[keyof typeof MmaProp];
export const PayoutType = {
Mortal: 'mortal', // All-or-nothing
Insured: 'insured', // Partial payout on loss
} as const;
export type PayoutType = (typeof PayoutType)[keyof typeof PayoutType];
// Profit thresholds — minimum implied probability for +EV.
// Derived from each platform's payout structure.
export const ProfitThreshold = {
prizepicks: 0.5434,
underdog: 0.545,
betr: 0.545,
sleeper: 0.545,
'dk-pick6': 0.545,
parlayplay: 0.545,
fliff: 0.545,
} as const;
src/sports/models.ts
import type { BetDirection, Platform, PayoutType, Sport } from '../constants.js';
export interface Line {
// Identity
firstName: string;
lastName: string;
team: string;
sport: Sport;
// The prop
propType: string;
propValue: number;
betDirection: BetDirection;
// Platform-specific
platform: Platform;
platformPropId: string;
eventName: string;
// Calculated (enriched by engine)
probability?: number;
sharpProbability?: number;
ev?: number;
simProbability?: number;
// Submission metadata
identifyingInfo?: Record<string, unknown>;
}
export interface Slip {
lines: Line[];
betSize: number;
payoutType: PayoutType;
score?: number;
totalProbability?: number;
expectedPayout?: number;
}
The Line interface is the core contract. Every platform client must normalize its API response into this shape. Every engine function consumes and produces Lines.
Platform adapter interface
Every platform must implement this interface. This is the most important architectural contract.
src/platforms/adapter.ts
import type { Line, Slip } from '../sports/models.js';
import type { Sport } from '../constants.js';
export interface PlatformAdapter {
readonly name: string;
readonly supportedSports: Sport[];
/** Fetch raw prop lines from the platform and normalize to Line[] */
getLines(sport: Sport): Promise<Line[]>;
/** Submit a slip to the platform via browser automation */
submitSlip(slip: Slip): Promise<{ success: boolean; error?: string }>;
}
The original codebase used convention. This version enforces the contract with TypeScript. New platforms only need client, normalizer, and submit — no changes to the engine.
PrizePicks client
PrizePicks has an unofficial REST API. Authentication is Keycloak-based. The projections endpoint is the most accessible and does not require auth for reading.
src/platforms/prizepicks/client.ts
import { z } from 'zod';
import type { Sport } from '../../constants.js';
import type { Line } from '../../sports/models.js';
import { normalizePrizePicks } from './normalize.js';
import { logger } from '../../utils/logger.js';
const PP_BASE = 'https://api.prizepicks.com';
const PROJECTIONS_URL = `${PP_BASE}/projections`;
interface PPProjection {
id: string;
type: string;
attributes: {
adjusted_odds: number | null;
board_time: string;
description: string;
display_stat: string;
end_time: string;
flash_sale_line_score: number | null;
game_id: string;
image_url: string;
in_play: boolean;
is_promo: boolean;
league: string;
league_id: number;
line_score: number;
odds_type: string;
projection_type: string;
refundable: boolean;
start_time: string;
stat_type: string;
status: string;
tv_channel: string | null;
updated_at: string;
};
relationships: {
new_player: { data: { id: string; type: string } };
league: { data: { id: string; type: string } };
game: { data: { id: string; type: string } };
};
}
interface PPPlayer {
id: string;
type: string;
attributes: {
display_name: string;
first_name: string;
last_name: string;
image_url: string;
league: string;
league_id: number;
market: string;
name: string;
position: string;
team: string;
team_name: string;
};
}
export async function fetchPrizePicksLines(sport: Sport): Promise<Line[]> {
const leagueMap: Record<string, string> = {
nba: 'NBA',
nfl: 'NFL',
golf: 'PGA',
tennis: 'TENNIS',
mlb: 'MLB',
};
const league = leagueMap[sport];
if (!league) throw new Error(`Unsupported sport for PrizePicks: ${sport}`);
const url = `${PROJECTIONS_URL}?per_page=1000&single_stat=true&league_id=${getLeagueId(sport)}`;
logger.info({ url, sport }, 'Fetching PrizePicks projections');
const response = await fetch(url, {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`PrizePicks API error: ${response.status} ${response.statusText}`);
}
const body = await response.json();
const projections: PPProjection[] = body.data;
const included: (PPPlayer | any)[] = body.included;
const players = new Map<string, PPPlayer>();
for (const item of included) {
if (item.type === 'new_player') {
players.set(item.id, item);
}
}
return normalizePrizePicks(projections, players, sport);
}
function getLeagueId(sport: Sport): number {
const ids: Record<string, number> = {
nba: 7, nfl: 2, mlb: 3, golf: 16, tennis: 24,
};
return ids[sport] ?? 7;
}
src/platforms/prizepicks/normalize.ts
import type { Line } from '../../sports/models.js';
import type { Sport, BetDirection } from '../../constants.js';
export function normalizePrizePicks(
projections: any[],
players: Map<string, any>,
sport: Sport,
): Line[] {
const lines: Line[] = [];
for (const proj of projections) {
const attrs = proj.attributes;
if (attrs.status !== 'pre_game') continue;
const playerId = proj.relationships.new_player.data.id;
const player = players.get(playerId);
if (!player) continue;
const playerAttrs = player.attributes;
for (const direction of ['Over', 'Under'] as BetDirection[]) {
lines.push({
firstName: playerAttrs.first_name,
lastName: playerAttrs.last_name,
team: playerAttrs.team ?? 'UNK',
sport,
propType: attrs.stat_type,
propValue: attrs.line_score,
betDirection: direction,
platform: 'prizepicks',
platformPropId: proj.id,
eventName: attrs.description ?? `${playerAttrs.team_name}`,
identifyingInfo: {
league: attrs.league,
odds_type: attrs.odds_type,
projection_type: attrs.projection_type,
game_id: attrs.game_id,
},
});
}
}
return lines;
}
Underdog Fantasy client
Underdog uses an internal REST API. Auth requires a browser login flow to capture cookies.
src/platforms/underdog/client.ts
import type { Sport } from '../../constants.js';
import type { Line } from '../../sports/models.js';
import { normalizeUnderdog } from './normalize.js';
import { logger } from '../../utils/logger.js';
const UD_API = 'https://api.underdogfantasy.com';
const LINES_URL = `${UD_API}/beta/v5/over_under_lines`;
const TEAMS_URL = 'https://stats.underdogfantasy.com/v1/teams';
export async function fetchUnderdogLines(
sport: Sport,
authCookies?: string,
): Promise<Line[]> {
const headers: Record<string, string> = {
'Accept': 'application/json',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
};
if (authCookies) {
headers['Cookie'] = authCookies;
}
const [linesRes, teamsRes] = await Promise.all([
fetch(LINES_URL, { headers }),
fetch(TEAMS_URL, { headers }),
]);
if (!linesRes.ok) {
throw new Error(`Underdog API error: ${linesRes.status}. Re-auth may be needed.`);
}
const linesData = await linesRes.json();
const teamsData = await teamsRes.json();
return normalizeUnderdog(linesData, teamsData, sport);
}
src/platforms/underdog/normalize.ts
import type { Line } from '../../sports/models.js';
import type { Sport, BetDirection } from '../../constants.js';
const STAT_MAP: Record<string, string> = {
points: 'Points',
rebounds: 'Rebounds',
assists: 'Assists',
steals: 'Steals',
blocks: 'Blocks',
turnovers: 'Turnovers',
three_points_made: '3-Pointers Made',
pts_asts: 'Pts+Asts',
pts_rebs: 'Pts+Rebs',
rebs_asts: 'Rebs+Asts',
pts_rebs_asts: 'Pts+Rebs+Asts',
blk_stls: 'Blks+Stls',
fantasy_points: 'Fantasy Points',
strokes: 'Strokes',
birdies: 'Birdies',
bogeys: 'Bogeys',
pass_yards: 'Pass Yards',
rush_yards: 'Rush Yards',
receiving_yards: 'Receiving Yards',
receptions: 'Receptions',
completions: 'Completions',
};
export function normalizeUnderdog(
linesData: any,
teamsData: any,
sport: Sport,
): Line[] {
const lines: Line[] = [];
const playersById = new Map<string, any>();
for (const p of linesData.players ?? []) playersById.set(p.id, p);
const gamesById = new Map<string, any>();
for (const g of linesData.games ?? []) gamesById.set(g.id, g);
const teamsById = new Map<string, any>();
for (const t of teamsData.teams ?? []) teamsById.set(t.id, t);
const appearancesById = new Map<string, any>();
for (const a of linesData.appearances ?? []) appearancesById.set(a.id, a);
for (const ouLine of linesData.over_under_lines ?? []) {
const stat = ouLine.over_under?.appearance_stat?.stat;
const propType = STAT_MAP[stat];
if (!propType) continue;
const appearanceId = ouLine.over_under?.appearance_stat?.appearance_id;
const appearance = appearancesById.get(appearanceId);
if (!appearance) continue;
const player = playersById.get(appearance.player_id);
if (!player) continue;
if (player.sport_id?.toLowerCase() !== sport) continue;
const game = gamesById.get(appearance.match_id);
const team = teamsById.get(appearance.team_id);
let eventName = 'Unknown';
if (game) {
const away = teamsById.get(game.away_team_id);
const home = teamsById.get(game.home_team_id);
eventName = `${home?.abbr ?? '?'} vs ${away?.abbr ?? '?'}`;
}
for (const option of ouLine.options ?? []) {
const direction: BetDirection = option.choice === 'higher' ? 'Over' : 'Under';
lines.push({
firstName: player.first_name,
lastName: player.last_name,
team: team?.abbr ?? 'UNK',
sport,
propType,
propValue: parseFloat(ouLine.stat_value),
betDirection: direction,
platform: 'underdog',
platformPropId: option.over_under_line_id,
eventName,
identifyingInfo: {
overUnderId: ouLine.over_under_id,
choice: option.choice,
},
});
}
}
return lines;
}
The Underdog API version (v5) may have incremented. If v5 fails, try v6, v7, etc. Alternatively, intercept the live app's network requests to discover the current version.
Sleeper, DraftKings Pick 6, and other platforms
Five additional platform adapters beyond PrizePicks and Underdog. Each follows the same adapter pattern — client, normalizer, and submit.
Betr Picks — GraphQL Props
// Betr Picks uses a GraphQL API with JWT auth.
// Covers NFL, MLB, NBA, NHL. Supports: Points, Rebounds, Assists,
// 3-pointers, Doubles, Blocks, Steals, Turnovers, combo props.
const BETR_GQL = 'https://api.betr.app/graphql';
export async function fetchBetrLines(authToken: string): Promise<Line[]> {
const query = `{
upcomingEvents(sport: NBA) {
id
players {
id firstName lastName team
projections { statType line }
}
}
}`;
const res = await fetch(BETR_GQL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`,
},
body: JSON.stringify({ query }),
});
return normalizeBetr(await res.json());
}
Sleeper — Over/Under Pick'em
// Sleeper integrates pick'em DFS directly into their popular fantasy app.
// Over 40 million users. Supports NFL, NBA, MLB, NHL, and more.
// API-based line fetching similar to PrizePicks.
const SLEEPER_API = 'https://api.sleeper.app';
export async function fetchSleeperLines(sport: Sport): Promise<Line[]> {
const res = await fetch(`${SLEEPER_API}/picks/available/${sport}`);
if (!res.ok) throw new Error(`Sleeper API error: ${res.status}`);
const data = await res.json();
return normalizeSleeper(data, sport);
}
DraftKings Pick 6 — Pick'em Contests
// DraftKings Pick 6 is DK's pick'em product with massive liquidity.
// Supports all major sports. Uses DraftKings' existing player pool.
// Lines fetched via DraftKings API with contest-specific endpoints.
const DK_PICK6_API = 'https://api.draftkings.com/pick6';
export async function fetchDKPick6Lines(sport: Sport): Promise<Line[]> {
const res = await fetch(`${DK_PICK6_API}/projections?sport=${sport}`, {
headers: { 'Accept': 'application/json' },
});
if (!res.ok) throw new Error(`DK Pick 6 API error: ${res.status}`);
const data = await res.json();
return normalizeDKPick6(data, sport);
}
ParlayPlay — NFL Combo Workflows
// ParlayPlay focuses on NFL QB + Receiver combos.
// Uses browser automation with interactive team selection.
// Combo types: Pass Yards + Receiving Yards, Completions + Receptions.
export const NFLComboType = {
YardsCombo: 'yards', // QB Pass Yards + WR Receiving Yards
CatchCombo: 'catches', // QB Completions + WR Receptions
} as const;
export interface NFLCombo {
qb: Line; // QB prop (Pass Yards or Completions)
receiver: Line; // WR/TE prop (Receiving Yards or Receptions)
comboType: string;
teams: string[]; // e.g. ['ATL', 'JAX']
}
Fliff — Sharp Odds Aggregator
// Fliff is NOT a DFS platform — it's a sharp book used for comparison.
// Fetches odds via The Odds API filtered to the Fliff bookmaker.
// NBA markets: points, rebounds, assists, threes, double_double,
// blocks, steals, turnovers, points_rebounds_assists.
const FLIFF_MARKETS = [
'player_points', 'player_rebounds', 'player_assists',
'player_threes', 'player_double_double', 'player_blocks',
'player_steals', 'player_turnovers',
'player_points_rebounds_assists',
];
Each platform follows the same adapter pattern: client (fetch data), normalizer (convert to Line[]), and submit (browser or API). Adding a new platform requires only these three files — no changes to the engine.