Skip to Content

calcComboOdds

⚠️

Deprecated since Toolkit v7.2.0 (still exported and working). The bets subgraph records every bet’s total odds, potential payout and payout already priced by the protocol’s rule: read a bet’s odds / settledOdds and potentialPayout / payout (or rawPotentialPayout / rawPayout) instead of re-pricing its legs on the client. useBets and getBetsReport no longer use calcComboOdds. MARGIN_APPLIED_AT is deprecated with it.

Calculates the total odds of a bet, reproducing on the client the way the protocol priced it when the bet was placed. For the minimum odds to place a new bet with, use calcMinOdds, which applies slippage on top.

Usage

import { calcComboOdds } from '@azuro-org/toolkit' const totalOdds = calcComboOdds({ odds: [ 1.5, 2 ], // leave voided legs out createdAt: bet.createdAt, })

Props

type CalcComboOddsParams = { /** the leg odds as the protocol recorded them; a voided leg must be left out by the caller */ odds: number[] /** * unix seconds, when the bet was placed - it decides whether the leg odds carry the feed's fee and * whether a single leg is priced as a combo of one * */ createdAt: number }

Return Value

string (.toFixed(ODDS_DECIMALS) called on the number)

Why a combo is not the product of its legs

The feed applies its fee to every outcome’s odds. Multiplying N legs that each carry the fee would compound it N times, which is not what the bettor is charged. A combo is therefore priced:

remove the fee from each leg → multiply → apply the fee once to the product

ceil(1.5 / 0.99) * ceil(2 / 0.99) * 0.99 = 3.05 1.5 * 2 = 3.00 ← the plain product, too low

The gap widens with every extra leg, so a long combo priced as a plain product is materially understated.

⚠️

Leave voided legs out of the array — never pass 1.0 for one. 1.0 would be de-margined to 1.02, inflating the total by about 1.8% per void. An empty array prices at 1, so a bet with nothing left standing returns its stake.

The fee has a start date

The feed did not always apply a fee. For a bet placed before MARGIN_APPLIED_AT the leg odds are raw, so a combo of them really is the plain product — and removing a fee that was never charged, then re-applying it once, would overstate the odds by roughly (1/0.99)^(legs-1).

calcComboOdds branches on createdAt for exactly that reason. Pass the bet’s real creation time; do not default it.

import { calcComboOdds, MARGIN_APPLIED_AT } from '@azuro-org/toolkit' // same legs, different eras calcComboOdds({ odds: [ 2, 1.75 ], createdAt: MARGIN_APPLIED_AT }) // '3.55...' - fee removed per leg calcComboOdds({ odds: [ 2, 1.75 ], createdAt: MARGIN_APPLIED_AT - 1 }) // '3.50...' - plain product

Singles from SINGLE_MARGIN_REMOVED_AT

The rule depends on when the bet was placed:

  • before MARGIN_APPLIED_AT the legs carry no fee, so they are multiplied as they are;
  • from MARGIN_APPLIED_AT every bet goes through the combo rule, a single leg included: the fee is removed from each leg, the result rounded up onto the two-decimal grid the protocol prices on, multiplied, and the fee taken off once - which can move a single leg’s odds off what the bettor was quoted;
  • from SINGLE_MARGIN_REMOVED_AT a single leg is its quoted odds again, while two or more legs keep the combo rule.

SINGLE_MARGIN_REMOVED_AT is a new export in Toolkit v7.2.0 (unix seconds). It is one constant for every chain, since the switch happened at one moment everywhere.

import { calcComboOdds, SINGLE_MARGIN_REMOVED_AT } from '@azuro-org/toolkit' calcComboOdds({ odds: [ 1.6789 ], createdAt: SINGLE_MARGIN_REMOVED_AT - 1 }) // '1.68...' - a single priced as a combo of one calcComboOdds({ odds: [ 1.6789 ], createdAt: SINGLE_MARGIN_REMOVED_AT }) // '1.6789...' - its quoted odds calcComboOdds({ odds: [ 2, 1.5 ], createdAt: SINGLE_MARGIN_REMOVED_AT }) // '3.05...' - two legs keep the fee rule

Reading a bet’s figures instead

For a bet that already exists, read what the subgraph recorded:

const totalOdds = bet.settledOdds ?? bet.odds // the settled odds once settled, the placed odds before const possibleWin = bet.potentialPayout // what the bet pays if it wins const payout = bet.payout // once settled: 0 if lost, the stake if canceled, the amount paid once redeemed

A voided leg is one whose selection.result === SelectionResult.Canceled. For a freebet, the pool’s payout is not what the bettor receives: the freebet contract passes the bettor only a share of it, which calcFreebetBettorShare computes. getBetsReport and the SDK’s useBets already read these figures for you.