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.
System Overview
Edge Detection
Sports
DFS Platforms
Probability Models
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
Technology Stack
| Layer | Technology | Version | Purpose |
|---|---|---|---|
| Runtime | Node.js | ≥ 22 LTS | Event-driven execution, native fetch, ES module support |
| Language | TypeScript | 6.x (strict) | Type safety, interface contracts, compile-time checks |
| Bundler/Runner | tsx | 4.x | Zero-config TypeScript execution for development |
| HTTP | undici / native fetch | built-in | API calls to DFS platforms and odds services |
| Browser | Playwright | 1.59 | Headless browser automation for bet submission |
| Anti-Detection | ghost-cursor-playwright | 2.x | Bezier-curve mouse movement (Fitts's Law) |
| Statistics | @stdlib/stats + simple-statistics | latest | Normal CDF, mean, standard deviation, Monte Carlo |
| Database | Supabase (PostgreSQL) | 2.x client | Slip persistence, P&L tracking, analytics views |
| Cache | ioredis | 5.x | API response caching with configurable TTL |
| Config Validation | Zod | 4.x | Runtime env var validation with typed schemas |
| Logging | Pino + pino-pretty | 10.x | Structured JSON logging with colorized dev output |
| CLI | Commander | 14.x | Command parsing: analyze, monitor, fetch |
| Linting | Biome | 2.x | Unified linting and formatting (replaces ESLint + Prettier) |
| Testing | Vitest | 4.x | Unit tests for engine modules |
| Container | Docker + docker-compose | latest | Orchestrates app + Redis for deployment |
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.
Fetch
DFS lines + sharp odds
Enrich
+EV calculation
Model
Combined probability
Combine
Generate & score slips
Pick
Select with caps
Execute
Display or submit
Directory Structure
Deployment
Local with tsx
pnpm install
pnpm dev analyze -p prizepicks -s nba
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
| Script | Command | Purpose |
|---|---|---|
dev | tsx src/index.ts | Development — run TypeScript directly |
build | tsc | Compile to JavaScript in dist/ |
start | node dist/index.js | Run compiled production build |
test | vitest run | Run unit test suite |
lint | biome check . | Lint and format check |
+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.
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.
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}>.
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).
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.
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.
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.
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.
Probability Models
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.
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.
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.
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
Penalty Adjustments
| Penalty | Multiplier | Trigger |
|---|---|---|
| Under-direction bias | -5% per Under leg | Line direction is "Under" |
| Low prop value | -7.5% per low line | Value ≤ threshold (e.g., 3PM ≤ 3.5, Points ≤ 8.5) |
| Stat contention | -5% per conflict | Multiple same-team, same-stat, same-direction legs |
| Fantasy points | -5% per fantasy leg | Prop type is "Fantasy Points" |
Exposure Caps (Picker)
12% of total slips
No single player can appear in more than 12% of selected slips. Prevents overexposure to one player's variance.
10% of total slips
No single stat type can dominate the portfolio. Ensures diversification across Points, Rebounds, Assists, etc.
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.
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
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
| Sport | Key | Prop Types |
|---|---|---|
| Basketball | nba | Points, Rebounds, Assists, Steals, Blocks, Turnovers, 3PM, Fantasy Points, Pts+Asts, Pts+Rebs, Rebs+Asts, PRA, Blks+Stls |
| Football | nfl | Pass Yards, Rush Yards, Receiving Yards, Receptions |
| Baseball | mlb | Strikeouts, Hits, Home Runs |
| Golf | golf | Strokes Gained, Tournament Finish Position |
| Tennis | tennis | Aces, Games Won, Sets Won |
Odds Aggregation
The consensus module blends two tiers of odds sources with different weights reflecting their market efficiency.
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.
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.
{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.
Browser Automation
The browser layer handles authenticated bet submission with anti-detection measures. It supports two session providers with automatic fallback.
Steel.dev Cloud Browser
Cloud-hosted headless browser with built-in anti-detection, CAPTCHA solving capabilities, and optional residential proxy. Requires STEEL_API_KEY.
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
| Behavior | Technique | Parameters |
|---|---|---|
| Mouse movement | Ghost Cursor (Bezier curves + Fitts's Law) | Natural acceleration & deceleration profiles |
| Typing | Log-normal inter-keystroke timing | Common bigrams 0.6x faster, 2% thinking pauses (+300-800ms), 1% typo + correction |
| Fidgets | Random mouse drift to idle areas | Triggered between actions |
| Scrolling | Natural overshoot + correction | 10% chance of overshoot, smooth correction |
| Pre-action warmup | Scroll + fidget + simulate reading | Applied 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
Storage Layer
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)
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
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
platform | text | prizepicks | underdog |
sport | text | nba, nfl, mlb, golf, tennis |
bet_size | numeric | Dollar amount wagered |
slip_size | int | Number of legs in the slip |
total_probability | numeric | Combined probability of all legs |
score | numeric | Stochastic score at time of selection |
placed | boolean | Whether the bet was actually submitted |
result | text | win | loss | push | pending |
payout | numeric | Actual payout received |
created_at | timestamptz | When the slip was created |
Database Schema — lines
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
slip_id | UUID | Foreign key to slips |
first_name | text | Player first name |
last_name | text | Player last name |
team | text | Team abbreviation |
prop_type | text | Points, Rebounds, Assists, etc. |
prop_value | numeric | The line value (e.g., 26.5) |
bet_direction | text | Over | Under |
probability | numeric | Combined probability |
sharp_probability | numeric | Sharp market probability |
ev | numeric | Expected value edge |
result | text | hit | miss | push | pending |
actual_value | numeric | Actual 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.
Monitoring & Scanner
The LineScanner class implements continuous polling with deduplication and optional auto-submission.
Scanner Loop
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_MSor CLI flag
CLI Interface
| Command | Description | Key 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
Configuration
All configuration is managed through environment variables, validated at startup with Zod schemas. Missing required values fail fast with descriptive errors.
| Variable | Type | Default | Purpose |
|---|---|---|---|
SUPABASE_URL | URL | optional | Supabase project URL for PostgreSQL persistence |
SUPABASE_ANON_KEY | string | optional | Supabase anonymous key |
REDIS_URL | string | redis://localhost:6379 | Redis connection string for caching |
THE_ODDS_API_KEY | string | optional | The Odds API key (soft book odds) |
ODDSPAPI_KEY | string | optional | OddsPapi key (sharp book odds) |
DATAGOLF_API_KEY | string | optional | DataGolf API key (golf projections) |
STEEL_API_KEY | string | optional | Steel.dev cloud browser API key |
PRIZEPICKS_EMAIL | string | optional | PrizePicks login email |
PRIZEPICKS_PASSWORD | string | optional | PrizePicks login password |
UNDERDOG_EMAIL | string | optional | Underdog login email |
UNDERDOG_PASSWORD | string | optional | Underdog login password |
TELEGRAM_BOT_TOKEN | string | optional | Telegram bot token for alerts |
TELEGRAM_CHAT_ID | string | optional | Telegram chat ID for notifications |
LOG_LEVEL | enum | info | Pino log level (trace, debug, info, warn, error) |
DRY_RUN | boolean | true | When true, display results without submitting |
MAX_AT_RISK | number | 500 | Maximum total dollars wagered across all slips |
UNIT_SIZE | number | 10 | Dollar amount per individual slip |
SCAN_INTERVAL_MS | number | 30000 | Polling frequency for monitor mode |
AUTO_SUBMIT | boolean | false | When true, auto-place bets when edges are found |
Core Data Models
Line Interface
Slip Interface
External Integrations
| Service | Type | Data | Auth Method |
|---|---|---|---|
| PrizePicks | DFS Platform | Player prop lines | Public API (no auth) |
| Underdog Fantasy | DFS Platform | Over/Under lines | Browser session cookies |
| The Odds API | Odds Aggregator | Soft book odds (DK, FD, BetMGM) | API key |
| OddsPapi | Odds Aggregator | Sharp book odds (Pinnacle, Circa) | API key |
| Supabase | Database | Slip & line persistence, P&L views | Anon key |
| Redis | Cache | API response caching (8h TTL) | Host:port |
| Steel.dev | Cloud Browser | Anti-detect headless sessions | API key |
| Telegram | Notifications | Edge alerts, CAPTCHA forwarding | Bot token + chat ID |
| DataGolf | Sports Data | Golf projections & strokes gained | API key |
| ESPN | Sports Data | Game schedules, team info | Public |
| nba_api | Sports Data | Historical game logs (Monte Carlo) | Public |