BettingLab

Build a +EV scanner with MoneyLine API in 60 lines

Marcus Hale
Marcus Hale

The MoneyLine API does the hard part — pulling odds from 100+ books, computing fair lines, deriving EV. Your job is presentation. Here's a minimal scanner.

The endpoint

GET /v1/edge?type=ev returns event-grouped EV opportunities. Each event carries an array of edges; each edge carries the bookmaker, outcome, odds, fair odds, and EV %.

A 60-line scanner

const KEY = process.env.MONEYLINE_API_KEY!;
const BASE = "https://mlapi.bet";

interface EvEdge { type: "ev"; market: string;
  ev: { bookmaker: string; outcome: string; odds: number; fairOdds: number; evPct: number }; }
interface EdgeEvent { eventId: string; leagueId: string;
  homeTeam?: string; awayTeam?: string; edges: (EvEdge | { type: string })[]; }

async function fetchEv(): Promise<EdgeEvent[]> {
  const res = await fetch(`${BASE}/v1/edge?type=ev&limit=50`, { headers: { "x-api-key": KEY } });
  if (!res.ok) throw new Error(`API ${res.status}`);
  const body = await res.json();
  return body.data ?? body;
}

function topN(events: EdgeEvent[], n = 20) {
  const flat = events.flatMap(e =>
    e.edges.filter((x): x is EvEdge => x.type === "ev").map(x => ({ event: e, edge: x }))
  );
  return flat.sort((a, b) => b.edge.ev.evPct - a.edge.ev.evPct).slice(0, n);
}

(async () => {
  const events = await fetchEv();
  for (const { event, edge } of topN(events)) {
    const matchup = event.homeTeam ? `${event.awayTeam} @ ${event.homeTeam}` : event.eventId;
    console.log(`${edge.ev.evPct.toFixed(2)}% | ${event.leagueId} | ${matchup} | ${edge.market} | ${edge.ev.outcome} @ ${edge.ev.bookmaker} ${edge.ev.odds > 0 ? "+" : ""}${edge.ev.odds}`);
  }
})();

Productionizing

Build with the same data we use.

MoneyLine API powers BettingLab's edge calculations. Free tier, 1k credits/month.

Build with the same data we use.

MoneyLine API powers BettingLab's edge calculations. Free tier, 1k credits/month.