Technical Specification

Better v2.0

DFS prop betting automation engine. Identifies mathematically advantaged bets on Daily Fantasy Sports platforms by comparing platform lines against sharp sportsbook consensus, then scores, ranks, and optionally auto-submits optimal slip combinations.

v2.0.0 Node 22+ TypeScript Strict ES Modules ~2,650 LOC
Section 01

System Overview

+EV

Edge Detection

5

Sports

2

DFS Platforms

3

Probability Models

30s

Scan Interval

Better is a +EV (positive expected value) detection engine for Daily Fantasy Sports prop betting. It continuously fetches player prop lines from DFS platforms (PrizePicks, Underdog Fantasy), compares them against sharp sportsbook consensus odds, and identifies statistically advantaged betting opportunities across NBA, NFL, MLB, Golf, and Tennis.

When the DFS platform's implied probability diverges from what sharp books indicate, that gap represents an exploitable edge. Better quantifies that edge, generates optimal multi-leg slip combinations, applies a stochastic scoring model with penalty adjustments, and can auto-submit the best slips via browser automation with human behavior simulation.

Core Capabilities

  • Real-time +EV edge detection against sharp sportsbook consensus
  • Multi-source probability modeling (Normal CDF, Monte Carlo, sharp consensus)
  • Stochastic combination scoring with penalty adjustments for risk factors
  • Exposure management with player and stat-type caps
  • Anti-detection browser automation via Steel.dev and Playwright with human behavior simulation
  • Telegram alerts for edges, submissions, and CAPTCHA forwarding
  • Supabase persistence for P&L tracking across all bets
  • Redis caching layer with 8-hour TTL for API responses
Section 02

Technology Stack

LayerTechnologyVersionPurpose
RuntimeNode.js≥ 22 LTSEvent-driven execution, native fetch, ES module support
LanguageTypeScript6.x (strict)Type safety, interface contracts, compile-time checks
Bundler/Runnertsx4.xZero-config TypeScript execution for development
HTTPundici / native fetchbuilt-inAPI calls to DFS platforms and odds services
BrowserPlaywright1.59Headless browser automation for bet submission
Anti-Detectionghost-cursor-playwright2.xBezier-curve mouse movement (Fitts's Law)
Statistics@stdlib/stats + simple-statisticslatestNormal CDF, mean, standard deviation, Monte Carlo
DatabaseSupabase (PostgreSQL)2.x clientSlip persistence, P&L tracking, analytics views
Cacheioredis5.xAPI response caching with configurable TTL
Config ValidationZod4.xRuntime env var validation with typed schemas
LoggingPino + pino-pretty10.xStructured JSON logging with colorized dev output
CLICommander14.xCommand parsing: analyze, monitor, fetch
LintingBiome2.xUnified linting and formatting (replaces ESLint + Prettier)
TestingVitest4.xUnit tests for engine modules
ContainerDocker + docker-composelatestOrchestrates app + Redis for deployment
Section 03

Architecture

The system is organized into six distinct layers, each with a single responsibility. Data flows top-to-bottom through the pipeline, with the storage layer accessible at any point for persistence.

Layer 1

Fetch

DFS lines + sharp odds

Layer 2

Enrich

+EV calculation

Layer 3

Model

Combined probability

Layer 4

Combine

Generate & score slips

Layer 5

Pick

Select with caps

Layer 6

Execute

Display or submit

Directory Structure

Project Treesrc/ ├── index.ts # CLI entry point (commander) ├── config.ts # Zod environment validation ├── constants.ts # Sport, Platform, Prop enums │ ├── platforms/ # DFS platform adapters │ ├── adapter.ts # Abstract PlatformAdapter interface │ ├── prizepicks/ # PrizePicks client, normalizer, submitter │ └── underdog/ # Underdog client, normalizer, submitter │ ├── odds/ # Sharp odds aggregation │ ├── the-odds-api.ts # Soft books (DK, FD, BetMGM) │ ├── oddspapi.ts # Sharp books (Pinnacle, Circa) │ └── consensus.ts # Weighted multi-source consensus │ ├── engine/ # Core analysis engine │ ├── ev.ts # +EV enrichment & detection │ ├── probability.ts # CDF, Monte Carlo, combined models │ ├── combinations.ts # C(n,k) generator with filters │ ├── scoring.ts # Stochastic scoring with penalties │ └── picker.ts # Slip selection with exposure caps │ ├── sports/ # Sport-specific models │ ├── models.ts # Line & Slip interfaces │ ├── basketball.ts # NBA props & composite decomposition │ ├── football.ts # NFL props │ ├── golf.ts & tennis.ts # Sport-specific logic │ ├── browser/ # Automation layer │ ├── session.ts # Steel.dev cloud / local Playwright │ ├── stealth.ts # Human behavior simulation │ └── captcha.ts # CAPTCHA detection & Telegram relay │ ├── storage/ # Data persistence │ ├── supabase.ts # PostgreSQL via Supabase client │ ├── redis.ts # Cache-through utility │ └── schemas.sql # DDL for slips, lines, pnl_summary │ ├── monitor/ │ └── scanner.ts # Continuous polling + auto-submit │ ├── data/ # External data sources │ ├── datagolf.ts # DataGolf API (golf projections) │ ├── nba-stats.ts # NBA stats for Monte Carlo │ ├── espn.ts # ESPN hidden API │ └── historical.ts # Historical stats loader │ └── utils/ ├── logger.ts # Pino structured logging ├── csv.ts # PapaParse CSV utilities └── retry.ts # Exponential backoff

Deployment

Development

Local with tsx

pnpm install
pnpm dev analyze -p prizepicks -s nba
Production

Docker Compose

docker-compose up

Runs the app container + Redis side-by-side. The app is built with tsc and executed via node dist/index.js.

Build Pipeline

ScriptCommandPurpose
devtsx src/index.tsDevelopment — run TypeScript directly
buildtscCompile to JavaScript in dist/
startnode dist/index.jsRun compiled production build
testvitest runRun unit test suite
lintbiome check .Lint and format check
Section 04

+EV Detection Pipeline

The pipeline runs end-to-end in a single analyze invocation or on each tick of the monitor loop. Each step transforms data forward.

Step 1

Fetch DFS Lines

Pull player prop projections from PrizePicks (public API, no auth) or Underdog (session cookies). Each line includes player, team, prop type, value, and direction. Output: Line[] array.

Step 2

Fetch Sharp Odds

Query The Odds API (soft books: DraftKings, FanDuel) and OddsPapi (sharp books: Pinnacle, Circa, Bookmaker). Aggregate into consensus probabilities with 60% sharp / 40% soft weighting. Output: Map<key, {overProb, underProb, line}>.

Step 3

Enrich with +EV

For each DFS line, look up the corresponding sharp consensus. Compute EV = sharpProbability - profitThreshold. Lines with EV > 2% are flagged as edges. Profit threshold is 54.5% (from PrizePicks/Underdog payout structure).

Step 4

Combined Probability

Blend three probability sources with fixed weights: 40% sharp market odds, 30% model probability (Normal CDF), 30% simulation probability (Monte Carlo). This balances real-money market signals against internal statistical models.

Step 5

Generate Combinations

Stack-based C(n, k) generator produces all valid multi-leg combinations. Filters enforce: no duplicate players in a slip, must include players from ≥ 2 teams, no conflicting directions on same player prop.

Step 6

Score & Pick

Stochastic scoring model rolls dice weighted by probability percentile. Penalties applied for: under-direction (-5%), low prop values (-7.5%), same-team stat contention (-5%), fantasy props (-5%). Top slips selected with 12% player cap and 10% stat cap.

EV Calculation
EV = Psharp − Tprofit   where Tprofit = 0.545
Edge Threshold

Only lines with EV > 0% are considered. Lines with EV > 2% trigger a log alert. The 54.5% threshold is derived from the platform payout structure where a 2-pick slip pays 3x.

Section 05

Probability Models

Model A · Weight: 30%

Normal CDF

Statistical distribution-based probability using @stdlib/stats-base-dists-normal-cdf.

Given an expected value, line value, and standard deviation, computes the cumulative distribution function to determine the probability of the outcome exceeding (or falling below) the line.

Model B · Weight: 30%

Monte Carlo Simulation

Runs 10,000 iterations using historical game stats. Computes mean and standard deviation from recent performances, generates random normal draws via Box-Muller transform, and counts how many satisfy the direction condition.

Requires minimum 3 historical data points.

Model C · Weight: 40%

Sharp Market Consensus

Implied probabilities derived from real-money sharp sportsbook lines (Pinnacle, Circa, Bookmaker). Gets the highest weight because these lines reflect informed money and are the most market-efficient pricing available.

Consensus computed as 60% sharp books, 40% soft books.

Combined Probability
Pcombined = 0.4 × Psharp + 0.3 × Pmodel + 0.3 × Psimulation
Normal CDF (Under)
P(Under) = Φ((line − μ) / σ)   where Φ is the standard normal CDF
Monte Carlo
P(Over) = (1/N) × ∑ I(Xi > line)   where Xi ~ N(μ, σ), N = 10,000
Section 06

Scoring Algorithm

The scoring system is intentionally non-deterministic. Rather than a simple sort by probability, it uses stochastic "dice rolls" weighted by probability percentile. This produces variety in the output while still statistically favoring stronger combinations.

Score Computation

Pseudocodefor each line in combination: rank = line's percentile among all candidates rolls = max(1.5, maxRolls * percentile) maxValue = min(line.probability, 0.725) minValue = maxValue / 5 for i in 0..rolls: score += random(minValue, maxValue)

Penalty Adjustments

PenaltyMultiplierTrigger
Under-direction bias-5% per Under legLine direction is "Under"
Low prop value-7.5% per low lineValue ≤ threshold (e.g., 3PM ≤ 3.5, Points ≤ 8.5)
Stat contention-5% per conflictMultiple same-team, same-stat, same-direction legs
Fantasy points-5% per fantasy legProp type is "Fantasy Points"
Final Score
S = Sraw + (Punder + Plow + Pcontention + Pfantasy) × Sraw

Exposure Caps (Picker)

Player Cap

12% of total slips

No single player can appear in more than 12% of selected slips. Prevents overexposure to one player's variance.

Stat Cap

10% of total slips

No single stat type can dominate the portfolio. Ensures diversification across Points, Rebounds, Assists, etc.

Section 07

Platform Adapters

Each DFS platform implements the PlatformAdapter interface with three methods: getLines(), normalize(), and submit(). This adapter pattern allows new platforms to be added without modifying the engine.

Platform

PrizePicks

API: api.prizepicks.com (public, no auth)

Data format: JSON API spec with included resources

Submission: Browser automation at app.prizepicks.com

Auth: Email/password via browser login

Sports: NBA, NFL, MLB, Golf, Tennis

Platform

Underdog Fantasy

API: api.underdogfantasy.com (internal API)

Data format: Custom JSON with nested player/appearance objects

Submission: Browser automation

Auth: Session cookies from browser login

Sports: NBA, NFL, MLB, Golf

Supported Sports & Prop Types

SportKeyProp Types
BasketballnbaPoints, Rebounds, Assists, Steals, Blocks, Turnovers, 3PM, Fantasy Points, Pts+Asts, Pts+Rebs, Rebs+Asts, PRA, Blks+Stls
FootballnflPass Yards, Rush Yards, Receiving Yards, Receptions
BaseballmlbStrikeouts, Hits, Home Runs
GolfgolfStrokes Gained, Tournament Finish Position
TennistennisAces, Games Won, Sets Won
Section 08

Odds Aggregation

The consensus module blends two tiers of odds sources with different weights reflecting their market efficiency.

Tier 1 · 60% Weight

Sharp Books (OddsPapi)

Pinnacle, Circa, Bookmaker. These books accept large limits and are considered the sharpest price-setters in the market. Their lines reflect informed, professional bettor activity.

Tier 2 · 40% Weight

Soft Books (The Odds API)

DraftKings, FanDuel, BetMGM. Higher-volume recreational books. Lines are less efficient but provide additional signal and coverage for props that sharp books may not offer.

Consensus Probability
Pconsensus = 0.6 × Psharp + 0.4 × Psoft
Lookup Key Format

{Player Name}|{market_key}|{line_value} — e.g., Nikola Jokic|player_points|26.5. The market key is derived by mapping DFS prop types to standard odds API market identifiers.

Section 09

Browser Automation

The browser layer handles authenticated bet submission with anti-detection measures. It supports two session providers with automatic fallback.

Primary

Steel.dev Cloud Browser

Cloud-hosted headless browser with built-in anti-detection, CAPTCHA solving capabilities, and optional residential proxy. Requires STEEL_API_KEY.

Fallback

Local Playwright

Local Chromium with stealth configuration: randomized viewport (5 sizes), randomized user-agent, anti-fingerprinting scripts that override navigator.webdriver, chrome.runtime, plugins, and language headers.

Human Behavior Simulation

BehaviorTechniqueParameters
Mouse movementGhost Cursor (Bezier curves + Fitts's Law)Natural acceleration & deceleration profiles
TypingLog-normal inter-keystroke timingCommon bigrams 0.6x faster, 2% thinking pauses (+300-800ms), 1% typo + correction
FidgetsRandom mouse drift to idle areasTriggered between actions
ScrollingNatural overshoot + correction10% chance of overshoot, smooth correction
Pre-action warmupScroll + fidget + simulate readingApplied before critical actions (login, submit)

CAPTCHA Handling

  • Detection: Checks for reCAPTCHA v2/v3, hCaptcha, and Cloudflare Turnstile
  • Relay: Screenshots the CAPTCHA and sends it to a Telegram channel
  • Resolution: Listens for /solve <token> (token injection) or /click x y (remote click) commands
  • Timeout: 5-minute window before abandoning the submission attempt
Section 10

Storage Layer

Persistence

Supabase (PostgreSQL)

Primary data store for slips, lines, and P&L tracking. Uses the Supabase JS client for typed queries. Stores every submitted bet for historical analysis.

Tables: slips, lines

Views: pnl_summary (daily P&L aggregation)

Cache

Redis (ioredis)

Cache-through layer for API responses. Prevents redundant calls to odds APIs during frequent scanning. Default TTL: 8 hours.

Pattern: Check cache → return if hit → fetch from API if miss → write to cache → return

Database Schema — slips

ColumnTypeDescription
idUUIDPrimary key
platformtextprizepicks | underdog
sporttextnba, nfl, mlb, golf, tennis
bet_sizenumericDollar amount wagered
slip_sizeintNumber of legs in the slip
total_probabilitynumericCombined probability of all legs
scorenumericStochastic score at time of selection
placedbooleanWhether the bet was actually submitted
resulttextwin | loss | push | pending
payoutnumericActual payout received
created_attimestamptzWhen the slip was created

Database Schema — lines

ColumnTypeDescription
idUUIDPrimary key
slip_idUUIDForeign key to slips
first_nametextPlayer first name
last_nametextPlayer last name
teamtextTeam abbreviation
prop_typetextPoints, Rebounds, Assists, etc.
prop_valuenumericThe line value (e.g., 26.5)
bet_directiontextOver | Under
probabilitynumericCombined probability
sharp_probabilitynumericSharp market probability
evnumericExpected value edge
resulttexthit | miss | push | pending
actual_valuenumericActual stat line (post-game)

pnl_summary (View)

Materialized view grouping results by platform, sport, and day. Aggregates win/loss counts, total wagered, total returned, and net profit/loss for P&L reporting.

Section 11

Monitoring & Scanner

The LineScanner class implements continuous polling with deduplication and optional auto-submission.

Scanner Loop

Pseudocodewhile (running) { for each sport in sports: for each platform in [PrizePicks, Underdog]: slips = scanOnce(sport, platform) // full pipeline slips = deduplicate(slips, seenHashes) // content hash if slips.length > 0: logEdges(slips) sendTelegramAlert(slips) onEdgeDetected(callback) if AUTO_SUBMIT: for each slip: submitSlip(slip) sleep(intervalMs) // default 30s if scanCount % 10 === 0: heartbeat() // health log }

Features

  • Deduplication: Content-hash based — prevents re-submitting the same combination
  • Telegram notifications: Real-time alerts for edges found, slips submitted, and failures
  • Graceful shutdown: Handles SIGINT/SIGTERM for clean process termination
  • Heartbeat: Health log emitted every 10 scan cycles
  • Configurable interval: Default 30s, overridable via SCAN_INTERVAL_MS or CLI flag
Section 12

CLI Interface

CommandDescriptionKey Flags
better analyze One-shot: fetch lines, find +EV edges, generate optimal slips, display results -p platform (required)
-s sport (required)
--slip-size legs per slip (default: 3)
--unit-size bet amount (default: $10)
--max-risk total at risk (default: $500)
--dry-run no submission (default: true)
better monitor Continuous scan loop — polls for edges and auto-submits when AUTO_SUBMIT=true -s comma-separated sports (default: nba)
-i interval in ms (default: 30000)
better fetch Display current lines from a platform (diagnostic tool) -p platform (required)
-s sport (required)

Example Usage

Shell# Analyze PrizePicks NBA lines $ pnpm dev analyze -p prizepicks -s nba # Monitor NBA and NFL continuously $ pnpm dev monitor -s nba,nfl # Fetch Underdog NBA lines $ pnpm dev fetch -p underdog -s nba # Docker deployment (app + Redis) $ docker-compose up

Configuration

All configuration is managed through environment variables, validated at startup with Zod schemas. Missing required values fail fast with descriptive errors.

VariableTypeDefaultPurpose
SUPABASE_URLURLoptionalSupabase project URL for PostgreSQL persistence
SUPABASE_ANON_KEYstringoptionalSupabase anonymous key
REDIS_URLstringredis://localhost:6379Redis connection string for caching
THE_ODDS_API_KEYstringoptionalThe Odds API key (soft book odds)
ODDSPAPI_KEYstringoptionalOddsPapi key (sharp book odds)
DATAGOLF_API_KEYstringoptionalDataGolf API key (golf projections)
STEEL_API_KEYstringoptionalSteel.dev cloud browser API key
PRIZEPICKS_EMAILstringoptionalPrizePicks login email
PRIZEPICKS_PASSWORDstringoptionalPrizePicks login password
UNDERDOG_EMAILstringoptionalUnderdog login email
UNDERDOG_PASSWORDstringoptionalUnderdog login password
TELEGRAM_BOT_TOKENstringoptionalTelegram bot token for alerts
TELEGRAM_CHAT_IDstringoptionalTelegram chat ID for notifications
LOG_LEVELenuminfoPino log level (trace, debug, info, warn, error)
DRY_RUNbooleantrueWhen true, display results without submitting
MAX_AT_RISKnumber500Maximum total dollars wagered across all slips
UNIT_SIZEnumber10Dollar amount per individual slip
SCAN_INTERVAL_MSnumber30000Polling frequency for monitor mode
AUTO_SUBMITbooleanfalseWhen true, auto-place bets when edges are found

Core Data Models

Line Interface

TypeScriptinterface Line { firstName: string lastName: string team: string sport: Sport // "nba" | "nfl" | "mlb" | "golf" | "tennis" propType: string // "Points", "Rebounds", "3-Pointers Made", etc. propValue: number // 26.5, 8.5, etc. betDirection: BetDirection // "Over" | "Under" platform: string // "prizepicks" | "underdog" platformPropId: string // Platform-specific identifier eventName: string // "DEN @ LAL" // Enriched by engine probability?: number // Combined probability (0-1) sharpProbability?: number // Market consensus probability ev?: number // Expected value edge simProbability?: number // Monte Carlo simulation result }

Slip Interface

TypeScriptinterface Slip { lines: Line[] // 2-6 legs per slip betSize: number // Dollar amount score?: number // Stochastic score totalProbability?:number // Product of leg probabilities expectedPayout?: number // betSize * payout multiplier }

External Integrations

ServiceTypeDataAuth Method
PrizePicksDFS PlatformPlayer prop linesPublic API (no auth)
Underdog FantasyDFS PlatformOver/Under linesBrowser session cookies
The Odds APIOdds AggregatorSoft book odds (DK, FD, BetMGM)API key
OddsPapiOdds AggregatorSharp book odds (Pinnacle, Circa)API key
SupabaseDatabaseSlip & line persistence, P&L viewsAnon key
RedisCacheAPI response caching (8h TTL)Host:port
Steel.devCloud BrowserAnti-detect headless sessionsAPI key
TelegramNotificationsEdge alerts, CAPTCHA forwardingBot token + chat ID
DataGolfSports DataGolf projections & strokes gainedAPI key
ESPNSports DataGame schedules, team infoPublic
nba_apiSports DataHistorical game logs (Monte Carlo)Public