Skip to content

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

bash
mkdir parlay-calculator && cd parlay-calculator
npm init -y
npm install node-fetch@2

Note: We use node-fetch@2 because v3+ is ESM-only. The @2 ensures CommonJS compatibility with require().

Step 2: Create calculator.js

javascript
// 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

bash
export SPORTSGAMEODDS_KEY=your_api_key_here
node calculator.js

Expected 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

javascript
// 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

javascript
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

javascript
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:

LegsAll @ -110Combined 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:

javascript
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:

javascript
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:

javascript
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

javascript
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:

javascript
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:

  1. Correlation adjustments - Books adjust odds for correlated bets
  2. Rounding - Slight rounding differences
  3. 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
LegsWin % (all 50/50)Reality
225%~22% (with vig)
312.5%~10%
46.25%~5%
53.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

Learn More