Build a Parlay Calculator
Build a parlay calculator that combines multiple bets and calculates total odds and payout.
What You’ll Build
A JavaScript tool that:
- Fetches live odds from multiple games
- Allows selecting multiple bets (legs)
- Calculates combined parlay odds
- Shows potential payout
- Supports American and decimal odds
Perfect for: Parlay betting tools, sportsbook calculators, bet slip builders
What is a Parlay?
A parlay (or accumulator) combines multiple bets into one wager. All bets must win for the parlay to pay out, but the payout is much higher than individual bets.
Example:
- Lakers -5.5 @ -110
- Warriors ML @ -150
- Over 220.5 @ -110
Individual bets: Risk $10 each = $30 total -> Win ~$27 As parlay: Risk $30 -> Win ~$170
Prerequisites
- Node.js 18+
- SportsGameOdds API key (Get one free)
- Basic JavaScript knowledge
Complete Code
Step 1: Setup Project
mkdir parlay-calculator && cd parlay-calculator
npm init -y
npm install node-fetch@2Note: We use
node-fetch@2because v3+ is ESM-only. The@2ensures CommonJS compatibility withrequire().
Step 2: Create calculator.js
// 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) => {
// Get team names from nested structure
const awayName = event.teams.away.names.long;
const homeName = event.teams.home.names.long;
const matchup = `${awayName} @ ${homeName}`;
// Get spread odds (focusing on DraftKings for simplicity)
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 spread = dkOdds.spread || "0";
// Spread already includes + or - from API
const line = spread;
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;
}
// Multiply all decimal odds
const combinedDecimalOdds = legs.reduce((acc, leg) => acc * leg.decimalOdds, 1);
// Convert back to American
const combinedAmericanOdds = decimalToAmerican(combinedDecimalOdds);
// Calculate payout
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}`);
// Calculate implied probability
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");
}
// Interactive CLI
async function main() {
console.log("Parlay Calculator - SportsGameOdds API\n");
// Fetch games
console.log("Fetching NBA games...\n");
const { data: events } = await fetchGames("NBA");
if (events.length === 0) {
console.log("No upcoming NBA games found");
console.log("Tip: Try during NBA season (October-June) or use a different league");
rl.close();
process.exit(1);
}
const availableBets = displayAvailableBets(events);
if (availableBets.length === 0) {
console.log("No DraftKings odds available for these games");
console.log("Tip: Try a different bookmaker or check back later");
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;
}
// Check if already selected
if (selectedLegs.find((l) => l.id === bet.id)) {
console.log("Bet already in parlay");
selectLeg();
return;
}
// Check if from same game (not allowed in most parlays)
const sameGame = selectedLegs.find((l) => l.eventID === bet.eventID);
if (sameGame) {
console.log("Cannot parlay multiple bets from same game (correlated)");
selectLeg();
return;
}
selectedLegs.push(bet);
console.log(`Added: ${bet.description} ${formatOdds(bet.odds)}`);
console.log(` Current legs: ${selectedLegs.length}`);
if (selectedLegs.length >= 2) {
const preview = calculateParlay(selectedLegs, 100);
console.log(` Preview: $100 -> $${preview.payout} (${preview.combinedAmericanOdds})`);
}
selectLeg();
});
};
selectLeg();
}
main();Step 3: Run It
export SPORTSGAMEODDS_KEY=your_api_key_here
node calculator.jsExpected Output
Parlay Calculator - SportsGameOdds API
Fetching NBA games...
Available Bets:
1. Los Angeles Lakers -5.5 -110 - Boston Celtics @ Los Angeles Lakers
2. Boston Celtics +5.5 -110 - Boston Celtics @ Los Angeles Lakers
3. Los Angeles Lakers ML -220 - Boston Celtics @ Los Angeles Lakers
4. Boston Celtics ML +180 - Boston Celtics @ Los Angeles Lakers
5. Over 220.5 -110 - Boston Celtics @ Los Angeles Lakers
6. Under 220.5 -110 - Boston Celtics @ Los Angeles Lakers
7. Golden State Warriors -3.5 -110 - Phoenix Suns @ Golden State Warriors
...
Enter bet ID to add to parlay (or "done" to finish): 1
Added: Los Angeles Lakers -5.5 -110
Current legs: 1
Enter bet ID to add to parlay (or "done" to finish): 7
Added: Golden State Warriors -3.5 -110
Current legs: 2
Preview: $100 -> $364.46 (+264)
Enter bet ID to add to parlay (or "done" to finish): 13
Added: Miami Heat ML +150
Current legs: 3
Preview: $100 -> $911.16 (+811)
Enter bet ID to add to parlay (or "done" to finish): done
Enter stake amount ($): 50
================================================================================
PARLAY SUMMARY
================================================================================
Selected Legs:
1. Los Angeles Lakers -5.5 (Boston Celtics @ Los Angeles Lakers)
Odds: -110 (1.91 decimal)
2. Golden State Warriors -3.5 (Phoenix Suns @ Golden State Warriors)
Odds: -110 (1.91 decimal)
3. Miami Heat ML (Orlando Magic @ Miami Heat)
Odds: +150 (2.50 decimal)
--------------------------------------------------------------------------------
Combined Odds: +811 (9.11 decimal)
Stake: $50.00
Potential Payout: $455.58
Potential Profit: $405.58
Implied Probability: 11.0%
================================================================================
Remember: ALL legs must win for parlay to pay out!
================================================================================How It Works
1. Calculate Combined Odds
// Multiply all decimal odds
const combinedDecimalOdds = legs.reduce((acc, leg) => acc * leg.decimalOdds, 1);Example:
- Leg 1: -110 -> 1.91 decimal
- Leg 2: -110 -> 1.91 decimal
- Leg 3: +150 -> 2.50 decimal
Combined: 1.91 x 1.91 x 2.50 = 9.11 decimal (+811 American)
2. Calculate Payout
const payout = stake * combinedDecimalOdds;
const profit = payout - stake;Example:
- Stake: $50
- Combined odds: 9.11 decimal
- Payout: $50 x 9.11 = $455.58
- Profit: $455.58 - $50 = $405.58
3. Prevent Correlated Bets
const sameGame = selectedLegs.find((l) => l.eventID === bet.eventID);
if (sameGame) {
console.log("Cannot parlay bets from same game");
return;
}Why? Most sportsbooks don’t allow parlaying bets from the same game because they’re correlated (e.g., if Lakers win big, they’ll likely cover the spread too).
Parlay Odds Table
Quick reference for common parlay combinations:
| Legs | All @ -110 | Combined Odds | $100 Pays |
|---|---|---|---|
| 2 | -110, -110 | +264 | $264 |
| 3 | -110, -110, -110 | +596 | $596 |
| 4 | -110, -110, -110, -110 | +1228 | $1,228 |
| 5 | -110, -110, -110, -110, -110 | +2436 | $2,436 |
Mixed odds example (2-leg):
- -110 + +150 = +355 ($100 -> $455)
- -200 + +200 = +150 ($100 -> $250)
- +100 + +100 = +300 ($100 -> $400)
Enhancements
Add Same Game Parlays
Some books allow same-game parlays (SGP). You need to check for correlation:
function checkCorrelation(leg1, leg2) {
// Example: Can't parlay "Lakers -5.5" with "Over 220"
// If Lakers cover, game likely went over
if (leg1.eventID !== leg2.eventID) {
return false; // Different games, no correlation
}
// Check for correlated bets
const correlatedPairs = [
["sp", "ou"], // Spread and total often correlated
["ml", "ou"], // Moneyline and total often correlated
];
const pair = [leg1.betTypeID, leg2.betTypeID].sort();
return correlatedPairs.some((cp) => cp[0] === pair[0] && cp[1] === pair[1]);
}Calculate True Odds
Adjust for bookmaker vig:
function calculateTrueOdds(americanOdds) {
// Remove vig to get true probability
const decimal = americanToDecimal(americanOdds);
const impliedProb = 1 / decimal;
// Assuming ~5% vig
const trueProb = impliedProb * 0.95;
return 1 / trueProb;
}
// Use true odds for expected value calculations
const trueDecimal = calculateTrueOdds(leg.odds);Add Round Robin
Round robin creates multiple parlays from your selections:
function generateRoundRobin(legs, parlaySize) {
const combinations = [];
function combine(start, combo) {
if (combo.length === parlaySize) {
combinations.push([...combo]);
return;
}
for (let i = start; i < legs.length; i++) {
combo.push(legs[i]);
combine(i + 1, combo);
combo.pop();
}
}
combine(0, []);
return combinations;
}
// Example: 4 legs, 2-team parlays (6 combinations)
const legs = [leg1, leg2, leg3, leg4];
const roundRobin = generateRoundRobin(legs, 2);
console.log(`Creating ${roundRobin.length} 2-team parlays`);
roundRobin.forEach((combo, i) => {
const parlay = calculateParlay(combo, 10); // $10 each
console.log(`Parlay ${i + 1}: $${parlay.payout}`);
});Show Win Probability
function calculateWinProbability(legs) {
// Convert odds to probabilities (removing vig)
const probs = legs.map((leg) => {
const decimal = leg.decimalOdds;
return (1 / decimal) * 0.95; // Adjust for ~5% vig
});
// Multiply probabilities (independent events)
const combinedProb = probs.reduce((acc, prob) => acc * prob, 1);
return (combinedProb * 100).toFixed(1);
}
console.log(`Win Probability: ${calculateWinProbability(legs)}%`);Add Teaser Calculation
Teasers adjust spreads in your favor for lower payout:
function applyTeaser(legs, points = 6) {
return legs.map((leg) => {
if (leg.betTypeID === "sp") {
// Adjust spread by teaser points
const currentSpread = parseFloat(leg.spread);
const newSpread = leg.sideID === "home" ? currentSpread - points : currentSpread + points;
return {
...leg,
spread: newSpread.toString(),
odds: -110, // Standard teaser odds
decimalOdds: americanToDecimal(-110),
};
}
if (leg.betTypeID === "ou") {
// Move total in bettor's favor
const currentTotal = parseFloat(leg.overUnder);
const newTotal = leg.sideID === "over" ? currentTotal - points : currentTotal + points;
return {
...leg,
overUnder: newTotal.toString(),
odds: -110,
decimalOdds: americanToDecimal(-110),
};
}
return leg;
});
}
// 6-point teaser
const teaserLegs = applyTeaser(selectedLegs, 6);
const teaserParlay = calculateParlay(teaserLegs, 100);Troubleshooting
“Cannot parlay bets from same game”
Cause: Selected multiple bets from one game.
Solution: Most sportsbooks don’t allow this. Use “Same Game Parlay” feature if available (different odds calculation).
Calculated odds don’t match sportsbook
Causes:
- Correlation adjustments - Books adjust odds for correlated bets
- Rounding - Slight rounding differences
- Promotions - Odds boosts not reflected
Solution: Use our calculator for estimates. Always verify with actual sportsbook before placing.
Parlay odds seem too good
Remember: Parlays are -EV (negative expected value) over time. The high payouts are offset by low win probability.
Example:
- 3-leg parlay @ -110 each
- Pays: +596 ($100 -> $696)
- True odds: Each leg 52.4% -> Combined 14.4%
- Expected value: $696 x 0.144 = $100.22 (barely break-even after vig)
Best Practices
1. Limit Leg Count
Recommendation: 2-4 legs max
Why:
- Win probability drops exponentially
- 5+ leg parlays rarely hit
- Better to do multiple 2-leg parlays
| Legs | Win % (all 50/50) | Reality |
|---|---|---|
| 2 | 25% | ~22% (with vig) |
| 3 | 12.5% | ~10% |
| 4 | 6.25% | ~5% |
| 5 | 3.12% | ~2% |
2. Avoid Heavy Favorites
Problem: Adding -400 favorites doesn’t help much
Example:
- 2-leg: -110 + -110 = +264
- 2-leg: -110 + -400 = +110 (much worse!)
Better: Use favorites in straights, underdogs in parlays
3. Shop for Best Odds
Impact of small differences:
Base parlay (3 legs @ -110 each): +596
If you improve each leg by 5 cents (-105):
- 3 legs @ -105 each: +649 (+53 better!)
4. Consider Round Robins
Instead of one 4-leg parlay:
- Option A: 1x 4-leg @ +1228 (risk $100, all must win)
- Option B: 6x 2-leg @ +264 each (risk $100 total, need 2+ to profit)
Option B is safer - you can lose 1-2 legs and still profit.
Next Steps
Combine with Other Examples
- Live Odds Tracker - Monitor parlay legs in real-time
- Odds Comparison Dashboard - Find best odds for each leg
Learn More
- Understanding oddID - Decode bet identifiers
- Best Practices - Optimize your betting strategy
- Glossary - Learn all terminology
