# Parlay Calculator Example - JavaScript
URL: https://sportsgameodds.com/docs/examples/parlay-builder

Parlay Calculator Example - JavaScript [#parlay-calculator-example---javascript]

Build a parlay calculator that combines multiple bets and calculates total odds and payout.

What You'll Build [#what-youll-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? [#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 [#prerequisites]

* Node.js 18+
* SportsGameOdds API key ([Get one free](/pricing))
* Basic JavaScript knowledge

Complete Code [#complete-code]

Step 1: Setup Project [#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 [#step-2-create-calculatorjs]

```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 [#step-3-run-it]

```bash
export SPORTSGAMEODDS_KEY=your_api_key_here
node calculator.js
```

Expected Output [#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 [#how-it-works]

1. Calculate Combined Odds [#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 [#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 [#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 [#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 [#enhancements]

Add Same Game Parlays [#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 [#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 [#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 [#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 [#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 [#troubleshooting]

"Cannot parlay bets from same game" [#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 [#calculated-odds-dont-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 [#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 [#best-practices]

1. Limit Leg Count [#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 [#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 [#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 [#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 [#next-steps]

Combine with Other Examples [#combine-with-other-examples]

* **[Live Odds Tracker](/docs/examples/live-odds-tracker)** - Monitor parlay legs in real-time
* **[Odds Comparison Dashboard](/docs/examples/odds-comparison-dashboard)** - Find best odds for each leg

Learn More [#learn-more]

* **[Understanding oddID](/v2/data-types/odds)** - Decode bet identifiers
* **[Best Practices](/docs/info/best-practices)** - Optimize your betting strategy
* **[Glossary](/docs/info/glossary)** - Learn all terminology