This example demonstrates how to build an interactive CLI tool that fetches real-time sports odds, allows users to select multiple bet legs, and calculates the total parlay payout and implied probability. The implementation handles American-to-Decimal odds conversion, filters for specific bookmakers (e.g., DraftKings), and prevents correlated bets (multiple bets from the same game).
// calculator.js
const fetch = require("node-fetch");
const readline = require("readline");
const API_KEY = process.env.SPORTSGAMEODDS_KEY;
const API_BASE = "https://api.sportsgameodds.com/v2";
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
// Utility functions
const americanToDecimal = (american) => {
if (american > 0) {
return american / 100 + 1;
}
return 100 / Math.abs(american) + 1;
};
const decimalToAmerican = (decimal) => {
if (decimal >= 2.0) {
return Math.round((decimal - 1) * 100);
}
return Math.round(-100 / (decimal - 1));
};
const formatOdds = (american) => {
return american > 0 ? `+${american}` : american.toString();
};
// Fetch games
async function fetchGames(league = "NBA") {
try {
const response = await fetch(`${API_BASE}/events?leagueID=${league}&finalized=false&oddsAvailable=true&limit=20`, { headers: { "x-api-key": API_KEY } });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Error fetching games:", error.message);
return { data: [] };
}
}
// Display available bets
function displayAvailableBets(events) {
const bets = [];
let betId = 1;
events.forEach((event) => {
const awayName = event.teams.away.names.long;
const homeName = event.teams.home.names.long;
const matchup = `${awayName} @ ${homeName}`;
// Get spread odds (DraftKings)
const spreadOdds = Object.values(event.odds || {}).filter((odd) => odd.betTypeID === "sp" && odd.periodID === "game" && odd.byBookmaker?.draftkings);
spreadOdds.forEach((odd) => {
const dkOdds = odd.byBookmaker.draftkings;
if (!dkOdds.available) return;
const team = odd.sideID === "home" ? homeName : awayName;
const line = dkOdds.spread || "0";
bets.push({
id: betId++,
matchup,
description: `${team} ${line}`,
odds: parseInt(dkOdds.odds),
decimalOdds: americanToDecimal(parseInt(dkOdds.odds)),
eventID: event.eventID,
oddID: odd.oddID,
betTypeID: odd.betTypeID,
sideID: odd.sideID,
spread: dkOdds.spread,
});
});
// Get moneyline odds
const mlOdds = Object.values(event.odds || {}).filter((odd) => odd.betTypeID === "ml" && odd.periodID === "game" && odd.byBookmaker?.draftkings);
mlOdds.forEach((odd) => {
const dkOdds = odd.byBookmaker.draftkings;
if (!dkOdds.available) return;
const team = odd.sideID === "home" ? homeName : awayName;
bets.push({
id: betId++,
matchup,
description: `${team} ML`,
odds: parseInt(dkOdds.odds),
decimalOdds: americanToDecimal(parseInt(dkOdds.odds)),
eventID: event.eventID,
oddID: odd.oddID,
});
});
// Get totals (over/under)
const totalOdds = Object.values(event.odds || {}).filter((odd) => odd.betTypeID === "ou" && odd.periodID === "game" && odd.byBookmaker?.draftkings);
totalOdds.forEach((odd) => {
const dkOdds = odd.byBookmaker.draftkings;
if (!dkOdds.available) return;
const side = odd.sideID === "over" ? "Over" : "Under";
bets.push({
id: betId++,
matchup,
description: `${side} ${dkOdds.overUnder}`,
odds: parseInt(dkOdds.odds),
decimalOdds: americanToDecimal(parseInt(dkOdds.odds)),
eventID: event.eventID,
oddID: odd.oddID,
betTypeID: odd.betTypeID,
sideID: odd.sideID,
overUnder: dkOdds.overUnder,
});
});
});
return bets;
}
// Calculate parlay odds
function calculateParlay(legs, stake = 100) {
if (legs.length === 0) return null;
const combinedDecimalOdds = legs.reduce((acc, leg) => acc * leg.decimalOdds, 1);
const combinedAmericanOdds = decimalToAmerican(combinedDecimalOdds);
const payout = stake * combinedDecimalOdds;
const profit = payout - stake;
return {
legs: legs.length,
combinedDecimalOdds: combinedDecimalOdds.toFixed(2),
combinedAmericanOdds: formatOdds(combinedAmericanOdds),
stake,
payout: payout.toFixed(2),
profit: profit.toFixed(2),
};
}
// Display parlay summary
function displayParlay(parlay, legs) {
console.log("\n" + "=".repeat(80));
console.log("PARLAY SUMMARY");
console.log("=".repeat(80));
console.log("\nSelected Legs:\n");
legs.forEach((leg, i) => {
console.log(`${i + 1}. ${leg.description} (${leg.matchup})`);
console.log(` Odds: ${formatOdds(leg.odds)} (${leg.decimalOdds.toFixed(2)} decimal)`);
console.log();
});
console.log("-".repeat(80));
console.log(`\nCombined Odds: ${parlay.combinedAmericanOdds} (${parlay.combinedDecimalOdds} decimal)`);
console.log(`Stake: $${parlay.stake.toFixed(2)}`);
console.log(`Potential Payout: $${parlay.payout}`);
console.log(`Potential Profit: $${parlay.profit}`);
const impliedProb = (1 / parseFloat(parlay.combinedDecimalOdds)) * 100;
console.log(`Implied Probability: ${impliedProb.toFixed(1)}%`);
console.log("\n" + "=".repeat(80));
console.log("Remember: ALL legs must win for parlay to pay out!");
console.log("=".repeat(80) + "\n");
}
async function main() {
console.log("Parlay Calculator - SportsGameOdds API\n");
const { data: events } = await fetchGames("NBA");
if (!events || events.length === 0) {
console.log("No upcoming NBA games found");
rl.close();
process.exit(1);
}
const availableBets = displayAvailableBets(events);
if (availableBets.length === 0) {
console.log("No DraftKings odds available");
rl.close();
process.exit(1);
}
console.log("Available Bets:\n");
availableBets.forEach((bet) => {
console.log(`${bet.id}. ${bet.description} ${formatOdds(bet.odds)} - ${bet.matchup}`);
});
const selectedLegs = [];
const selectLeg = () => {
rl.question('\nEnter bet ID to add to parlay (or "done" to finish): ', (answer) => {
if (answer.toLowerCase() === "done") {
if (selectedLegs.length < 2) {
console.log("\nParlay requires at least 2 legs!\n");
selectLeg();
return;
}
rl.question("\nEnter stake amount ($): ", (stakeInput) => {
const stake = parseFloat(stakeInput) || 100;
const parlay = calculateParlay(selectedLegs, stake);
displayParlay(parlay, selectedLegs);
rl.close();
});
return;
}
const betId = parseInt(answer);
const bet = availableBets.find((b) => b.id === betId);
if (!bet) { console.log("Invalid bet ID"); selectLeg(); return; }
if (selectedLegs.find((l) => l.id === bet.id)) { console.log("Bet already in parlay"); selectLeg(); return; }
if (selectedLegs.find((l) => l.eventID === bet.eventID)) { console.log("Cannot parlay multiple bets from same game"); selectLeg(); return; }
selectedLegs.push(bet);
console.log(`Added: ${bet.description} ${formatOdds(bet.odds)}`);
selectLeg();
});
};
selectLeg();
}
main();