Market Overview
Real-time analytics engine — updated daily at 6:00 AM ET
Best EV Game
calculating...
Best Score
weighted rank
Top Prizes Left
games w/ jackpot intact
Avg Portfolio ROI
across all scratch-offs
FL Jackpot Pool
scratch-offs only
Powerball
$325M
Wed & Sat draw
Expected Value Ranking (EV/$ticket)
Top Prize % Remaining by Game
Scratch-Off Analyzer
Pick 2 → Powerball
Capital Asset Modeling
Correlation · Cointegration · Drawdown · Sharpe · Kelly · Bell Curves
Game ROI Correlation Matrix (top 8 games)
Measures EV return correlation between games by prize tier overlap. Higher = similar risk/reward profile. Use for portfolio diversification.
Price vs EV Scatter — All Games
Each dot = one game. Dots above the diagonal line = positive EV relative to price.
Cointegration Analysis — EV Spread by Price Tier
Cointegration indicates long-run equilibrium between prize structures. When spread reverts to mean, EV improves. This simulates pair-trading logic applied to scratch-off prize tiers.
Lottery Sharpe Ratio (Top 10 Games)
Sharpe = (EV − RiskFree) ÷ StdDev(prize distribution). Higher = better risk-adjusted return. Risk-free set to $0 (no-play baseline).
Maximum Drawdown Simulation (200 ticket sessions)
Simulated cumulative P&L over 200 ticket purchases. Drawdown = peak-to-trough decline. Helps model bankroll volatility risk.
Risk-Adjusted Metrics Table
Prize Distribution Bell Curve — Select Game
Expected Outcome Distribution — 50 Ticket Simulation
Kelly Criterion Bet Sizing
Kelly Formula: f* = (bp − q) / b
where b = net odds, p = win prob, q = 1−p
Note: Kelly fractions are typically very small in lottery — this tool uses fractional Kelly (0.1×) for realistic bankroll sizing.
Kelly Bankroll Growth Simulation
EV vs ROI Bubble Chart
Bubble size = ticket price. X = expected value. Y = ROI%. Upper-right = best plays.
Prize Tier Breakdown — Donut Charts
EV CONTRIBUTION %
PRIZES REMAINING %
Budget Optimizer
Kelly-weighted recommendations by budget allocation
Configure Session
QUICK SELECT
Budget Allocation Pie
Area Intelligence — Where to Play
Loading area recommendations...
Hot / Cold Number Engine
Historical frequency analysis with probability distributions
Number Frequency Heatmap — Last 500 Draws
Frequency Bar Chart with Probability %
🔥 HOT NUMBERS
❄ COLD NUMBERS
⚡ DUE NUMBERS
Note: "Due" is a statistical observation, not a predictor. Each draw is independent.
Number Pair Co-occurrence Frequency
Rolling Window Analysis (last 20/50/100 draws)
AI Number Suggestion Engine
Loading suggestions...
🔒
PRO FEATURE
Upgrade to PRO for AI suggestions
Scraper + API Integration
Production-ready scrapers for Florida Lottery data feeds
// LottoEdge FL — Node.js Scraper (Puppeteer + Cheerio)
// Scrapes: floridalottery.com/games/scratch-offs + draw results
// Run daily via cron: 0 6 * * * node scraper.js

const puppeteer = require('puppeteer');
const cheerio = require('cheerio');
const axios = require('axios');
const fs = require('fs');
const path = require('path');

// ── CONFIG ──────────────────────────────────────────────
const CONFIG = {
  baseUrl: 'https://floridalottery.com',
  scratchUrl: 'https://floridalottery.com/games/scratch-offs',
  drawUrl: 'https://floridalottery.com/games/draw-games',
  gameIds: [1555,1562,1590,1594,1597,1604,1606,
            1608,1618,1619,1620,1621,1622,1623,1627],
  outputDir: './data',
  delay: 1500 // ms between requests (be respectful!)
};

// ── SCRAPE SINGLE SCRATCH GAME ──────────────────────────
async function scrapeScratchGame(gameId) {
  const url = `${CONFIG.baseUrl}/games/scratch-offs/view?id=${gameId}`;
  const res = await axios.get(url, {
    headers: { 'User-Agent': 'Mozilla/5.0 (compatible; LottoEdgeBot/1.0)' }
  });
  const $ = cheerio.load(res.data);
  const prizes = [];

  // Parse prize table
  $('.odds-prizes-table tr, table tr').each((i, row) => {
    const cells = $(row).find('td');
    if (cells.length >= 3) {
      const prizeText = $(cells[0]).text().replace(/[$,]/g,'').trim();
      const oddsText = $(cells[1]).text().match(/[\d,]+$/)?.[0] || '0';
      const remText = $(cells[2]).text().trim();
      const [rem, total] = remText.split(' of ').map(x => parseInt(x.replace(/,/g,'')));
      if (isNaN(rem)) return;
      prizes.push({ prize: +prizeText, odds: +oddsText.replace(/,/g,''), rem, total });
    }
  });

  const topPrize = $('[data-top-prize], .top-prize').first().text().replace(/[$,]/g,'').trim();
  const topRemaining = $('.top-prizes-remaining').first().text().trim();
  const overallOdds = $('.overall-odds').first().text().replace('1:','').trim();

  return { gameId, topPrize: +topPrize || 0, topRemaining, overallOdds, prizes,
    scrapedAt: new Date().toISOString() };
}

// ── SCRAPE ALL GAMES ─────────────────────────────────────
async function scrapeAll() {
  console.log('[LottoEdge] Starting scrape...', new Date().toISOString());
  const results = {};

  for (const id of CONFIG.gameIds) {
    try {
      results[id] = await scrapeScratchGame(id);
      console.log(`  ✓ Game #${id} — ${results[id].prizes.length} prize tiers`);
    } catch (e) {
      console.error(`  ✗ Game #${id}:`, e.message);
    }
    await new Promise(r => setTimeout(r, CONFIG.delay));
  }

  const outPath = path.join(CONFIG.outputDir, `scratch_${Date.now()}.json`);
  fs.writeFileSync(outPath, JSON.stringify(results, null, 2));
  console.log(`[LottoEdge] Saved to ${outPath}`);
  return results;
}

// ── SCRAPE DRAW RESULTS ──────────────────────────────────
async function scrapeDrawResults() {
  const browser = await puppeteer.launch({ headless: 'new' });
  const page = await browser.newPage();
  await page.goto('https://floridalottery.com/games/draw-games/winning-numbers');
  await page.waitForSelector('.winning-numbers', { timeout: 10000 });

  const drawData = await page.evaluate(() => {
    const results = [];
    document.querySelectorAll('.draw-result-row').forEach(row => {
      const game = row.querySelector('.game-name')?.textContent;
      const date = row.querySelector('.draw-date')?.textContent;
      const nums = [...row.querySelectorAll('.ball')].map(b => +b.textContent);
      results.push({ game, date, numbers: nums });
    });
    return results;
  });

  await browser.close();
  return drawData;
}

scrapeAll().then(data => console.log('Complete', Object.keys(data).length, 'games'));
# LottoEdge FL — Python Scraper
# pip install requests beautifulsoup4 selenium pandas schedule

import requests, json, time, schedule, logging
from datetime import datetime
from bs4 import BeautifulSoup
import pandas as pd

logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')

BASE_URL = "https://floridalottery.com"
GAME_IDS = [1555,1562,1590,1594,1597,1604,1606,
            1608,1618,1619,1620,1621,1622,1623,1627]
HEADERS = {"User-Agent": "LottoEdge/1.0 (educational research)"}

def scrape_game(game_id: int) -> dict:
    url = f"{BASE_URL}/games/scratch-offs/view?id={game_id}"
    r = requests.get(url, headers=HEADERS, timeout=10)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")

    prizes = []
    for row in soup.select("table tr"):
        cells = row.find_all("td")
        if len(cells) < 3: continue
        prize_str = cells[0].get_text().replace("$","").replace(",","").strip()
        odds_str  = cells[1].get_text().replace(",","").strip()
        rem_str   = cells[2].get_text().strip()
        try:
            rem, total = [int(x.replace(",","")) for x in rem_str.split(" of ")]
            odds_val = int(odds_str.split("-in-")[-1])
            prizes.append({"prize": int(prize_str), "odds": odds_val, "rem": rem, "total": total})
        except: continue

    return {"game_id": game_id, "prizes": prizes,
            "scraped_at": datetime.utcnow().isoformat()}

def calc_ev(game: dict) -> float:
    return sum(p["prize"] * (1/p["odds"]) * (p["rem"]/p["total"]) for p in game["prizes"])

def daily_scrape():
    logging.info("Starting daily FL Lottery scrape...")
    results = {}
    for gid in GAME_IDS:
        try:
            results[gid] = scrape_game(gid)
            ev = calc_ev(results[gid])
            logging.info(f"  Game #{gid}: EV=${ev:.2f}, {len(results[gid]['prizes'])} tiers")
            time.sleep(1.5)
        except Exception as e:
            logging.error(f"  Error game #{gid}: {e}")

    fname = f"data/scratch_{int(time.time())}.json"
    with open(fname, "w") as f:
        json.dump(results, f, indent=2)
    logging.info(f"Saved {len(results)} games to {fname}")

# Schedule daily at 6 AM ET
schedule.every().day.at("06:00").do(daily_scrape)
if __name__ == "__main__":
    daily_scrape()  # run once immediately
    while True: schedule.run_pending(); time.sleep(60)
API Endpoints
MethodEndpointDescription
GET /api/gamesAll active scratch games
GET /api/games/:idGame detail + prize structure
GET /api/ev/:idExpected value calculation
GET /api/rankingsRanked game list by score
GET /api/draw/:gameDraw game history + hot/cold
GET /api/budget/:amountOptimized play plan
POST /api/simulateMonte Carlo simulation
POST /api/alertsSet prize-remaining alert
// Express.js API server — app.js
const express = require('express');
const { scrapeAll, calcEV } = require('./scraper');
const app = express();
app.use(express.json());

// Auth middleware
const auth = (req, res, next) => {
  const key = req.headers['x-api-key'];
  if (!key || !validKeys.has(key))
    return res.status(401).json({ error: 'Invalid API key' });
  next();
};

app.get('/api/games', auth, async (req, res) => {
  const data = loadLatestData();
  res.json(Object.values(data).map(g => ({
    id: g.gameId, ev: calcEV(g).toFixed(2), ...g
  })));
});

app.get('/api/budget/:amount', auth, (req, res) => {
  const b = +req.params.amount;
  res.json(optimizeBudget(b, loadLatestData()));
});

app.post('/api/simulate', auth, (req, res) => {
  const { gameId, tickets, sims } = req.body;
  res.json(monteCarlo(gameId, tickets, sims || 10000));
});

app.listen(3000, () =>
  console.log('LottoEdge API running on :3000'));
# crontab -e  — add this line for daily 6 AM ET scrape:
# 0 11 * * * cd /app && node scraper.js >> logs/scrape.log 2>&1

# GitHub Actions workflow (.github/workflows/daily-scrape.yml):
---
name: Daily FL Lottery Scrape
on:
  schedule:
    - cron: '0 11 * * *'  # 6 AM ET = 11 UTC
  workflow_dispatch:       # allow manual trigger

jobs:
  scrape:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: node scraper.js
        env:
          DB_URL: ${{ secrets.DATABASE_URL }}
          WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK }}
      - uses: actions/upload-artifact@v4
        with:
          name: scrape-data
          path: data/*.json
          retention-days: 30

# Docker Compose for full stack:
---
version: '3.8'
services:
  api:
    build: .
    ports: ["3000:3000"]
    environment:
      - DB_URL=postgresql://postgres:pass@db:5432/lottoedge
  db:
    image: postgres:16
    volumes: [pgdata:/var/lib/postgresql/data]
  scheduler:
    build: .
    command: node scheduler.js
    depends_on: [db]
volumes:
  pgdata:
// Alert system — sends Discord/Slack/Email when key events occur
const alerts = {
  // Trigger: top prize count drops (someone won!)
  topPrizeClaimed: async (gameId, prizeTier, newCount) => {
    const msg = {
      embeds: [{
        title: `🎰 TOP PRIZE CLAIMED — Game #${gameId}`,
        description: `$${prizeTier.toLocaleString()} prize won! **${newCount} remaining**`,
        color: 0xFF4D6D,
        timestamp: new Date()
      }]
    };
    await axios.post(process.env.DISCORD_WEBHOOK, msg);
  },

  // Trigger: EV crosses threshold (game becomes better value)
  evThreshold: async (gameId, ev, price) => {
    if (ev / price > 0.85) { // 85%+ return threshold
      await axios.post(process.env.SLACK_WEBHOOK, {
        text: `⚡ HIGH EV ALERT: Game #${gameId} at $${ev.toFixed(2)} EV on $${price} ticket`
      });
    }
  },

  // Daily digest email via SendGrid
  dailyDigest: async (topPlays) => {
    await sgMail.send({
      to: process.env.ALERT_EMAIL,
      from: 'alerts@lottoedge.com',
      subject: `LottoEdge Daily Brief — ${new Date().toDateString()}`,
      html: buildEmailHTML(topPlays)
    });
  }
};

// Check for changes after each scrape
async function detectChanges(prev, curr) {
  for (const [id, game] of Object.entries(curr)) {
    const p = prev[id];
    if (!p) continue;
    // Top prize claimed?
    if (p.topPrizeCount > game.topPrizeCount)
      await alerts.topPrizeClaimed(id, game.topPrize, game.topPrizeCount);
    // EV improvement?
    await alerts.evThreshold(id, calcEV(game), game.price);
  }
}
Choose Your Edge
Professional-grade lottery intelligence for serious players
All subscriptions include 7-day free trial · Cancel anytime · 18+ only · Gambling involves risk · Play responsibly