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
- Cache for 60-300 seconds — EV moves slowly relative to API rate limits.
- Filter on
minEdgeserver-side (?minEdge=2) to skip noise. - Persist seen-bets so you don't re-alert the same edge twice.