API Reference

Two things live here, and they are priced differently on purpose. The calculators are free, no key and no account, because that math is MIT-licensed and on PyPI and you could run it yourself in a second. The board and its alerts need Extra Credit: the live fair line, the per-venue grades, and the +EV and arbitrage cases the moment they open. That part is not in the library and cannot be self-hosted.

The Teacher’s Bet API is fifteen HTTPS endpoints that compute sports-betting mathematics on caller-supplied inputs: de-vigging, expected value, Kelly sizing, arbitrage stake splits, free-bet conversion, parlay pricing, odds-format conversion, payout, hedge sizing, middles, teasers, round robins, break-even win rates and Kalshi contract fees. Each returns JSON with the result and every intermediate value. It serves no odds data and issues no picks.

Quickstart

Subscribe, sign in, and hit Copy API key in the account menu. One click, token on your clipboard. Or script it against the same endpoint:

curl -s -X POST https://teachersbet.com/api/pro/key \
  -b 'tb_session=<your session cookie>'
{"token": "...", "tier": "pro", "expires_in_days": 31,
 "usage": "send it as: Authorization: Bearer <token>"}

Then every call carries it:

curl -s https://teachersbet.com/api/devig \
  -H 'authorization: Bearer <token>' \
  -H 'content-type: application/json' \
  -d '{"ml_a": -152, "ml_b": 138}'

That gives you the fair probabilities behind a −152 / +138 market under all three de-vig methods, plus the overround it took out. Every other endpoint has the same shape. The samples below drop the authorization header for length. Every one of them needs it.

What is true of every endpoint

The fifteen endpoints

EndpointAnswers
POST /api/devigFair probabilities behind a two-sided moneyline, three ways
POST /api/evExpected value of one price against a reference line
POST /api/kellyKelly stake fraction and amount for a single bet
POST /api/kelly/simultaneousJoint Kelly stakes across bets that settle together
POST /api/arbWhether posted prices cross, and the stake split if they do
POST /api/bonusbetCash value of a stake-not-returned free bet, hedged or not
POST /api/parlayCombined odds, payout and break-even chance of a parlay
POST /api/kalshifeesKalshi taker/maker fee, total outlay and fee-adjusted break-even
POST /api/oddsconvertOne price written in all five notations, and the probability it implies
POST /api/payoutProfit and total return on a stake, and what the vig costs it
POST /api/hedgeHedge stake, the result either way, and the price of the certainty
POST /api/middleRequired hit rate and payoff grid for two prices at different numbers
POST /api/teaserWhat a teaser’s bought points have to be worth per leg
POST /api/roundrobinTickets, total risk and an exact hit-level table for a round robin
POST /api/breakevenThe win rate a price needs, and whether a record separates from it

A machine-readable description of all fifteen is served at /openapi.json (OpenAPI 3.1).

How do I remove the vig from betting odds with an API?

POST /api/devig takes both sides of a two-outcome moneyline and returns the fair, margin-free probabilities under the proportional, power and Shin methods, along with the overround it removed. This is the fair-line step every other calculation depends on.

FieldTypeReq.Meaning and units
ml_anumberyesAmerican moneyline for side A. Unitless, |ml_a| ≥ 100.
ml_bnumberyesAmerican moneyline for side B. Same constraint.
methodstringnoproportional (default), power, or shin. Selects which result lands in fair; all three are returned regardless.
curl -s https://teachersbet.com/api/devig \
  -H 'content-type: application/json' \
  -d '{"ml_a": -152, "ml_b": 138}'
{
  "inputs": {"ml_a": -152, "ml_b": 138, "method": "proportional"},
  "implied": {"a": 0.6031746031746031, "b": 0.42016806722689076},
  "overround": 0.023342670401493848,
  "fair": {"a": 0.5894160583941606, "b": 0.41058394160583944},
  "fair_all_methods": {
    "proportional": [0.5894160583941606, 0.41058394160583944],
    "power": [0.5925022242857771, 0.40749777571422285],
    "shin": [0.5915032679738437, 0.40849673202615633]
  },
  "fair_odds": {
    "a": {"decimal": 1.696594427244582, "moneyline": -143.55555555555554},
    "b": {"decimal": 2.4355555555555553, "moneyline": 143.55555555555554}
  },
  "meta": {
    "computed_at": "2026-07-24T15:04:05.123Z",
    "disclaimer": "Informational and entertainment purposes only. All outputs are calculations on user-supplied inputs, not betting advice. No guarantee of outcomes. Gamble responsibly. 1-800-GAMBLER."
  }
}

fair_all_methods is the useful field: if your edge only survives one of the three, it is an artifact of the method rather than of the market. Formulas: proportional, power, Shin. Browser version: no-vig calculator.

How do I calculate expected value with an API?

POST /api/ev de-vigs a reference line, converts your own price to net odds, and returns the expected value per dollar, under the method you chose and under all three, so method-dependence is visible rather than hidden.

FieldTypeReq.Meaning and units
sharp_ml_anumberyesReference line, side A. Unitless moneyline, |ML| ≥ 100.
sharp_ml_bnumberyesReference line, side B.
soft_mlnumberyesThe price being tested, at a different venue. Unitless moneyline.
sidestringnoa (default) or b: which side soft_ml is on.
methodstringnoproportional, power (default here), or shin.
capitalnumbernoBankroll, in money. Omit and stake comes back null; supply it and stake is the sized bet. Must be positive.
fractionnumbernoKelly multiplier in (0, 1]. Default 0.5 (half Kelly).
curl -s https://teachersbet.com/api/ev \
  -H 'content-type: application/json' \
  -d '{"sharp_ml_a": -140, "sharp_ml_b": 120, "soft_ml": 145, "side": "b", "method": "power"}'
{
  "inputs": {"sharp_ml_a": -140, "sharp_ml_b": 120, "soft_ml": 145, "side": "b",
             "method": "power", "capital": null, "fraction": 0.5},
  "sharp_implied": {"a": 0.5833333333333334, "b": 0.45454545454545453},
  "sharp_overround": 0.037878787878787845,
  "p_true": 0.4344352512161665,
  "soft_implied": 0.40816326530612246,
  "edge": 0.026271985910044038,
  "b": 1.45,
  "expected_value_per_dollar": 0.06436636547960783,
  "is_positive_ev": true,
  "by_method": [
    {"method": "proportional", "p_true": 0.43795620437956206, "edge": 0.029792939073439606,
     "expected_value_per_dollar": 0.07299270072992703, "is_positive_ev": true},
    {"method": "power", "p_true": 0.4344352512161665, "edge": 0.026271985910044038,
     "expected_value_per_dollar": 0.06436636547960783, "is_positive_ev": true},
    {"method": "shin", "p_true": 0.43560606060607315, "edge": 0.027442795299950695,
     "expected_value_per_dollar": 0.06723484848487926, "is_positive_ev": true}
  ],
  "robust_positive_ev": true,
  "worst_case": {"method": "power", "p_true": 0.4344352512161665, "edge": 0.026271985910044038,
                 "expected_value_per_dollar": 0.06436636547960783, "is_positive_ev": true},
  "kelly_full": 0.04439059688248814,
  "kelly_fraction": 0.02219529844124407,
  "stake": null
}

expected_value_per_dollar is money per unit staked, so 0.0644 means 6.44 cents per dollar on average over many repetitions of that exact price, not a forecast of this bet. robust_positive_ev is true only when all three methods clear zero. Formula: expected value. Browser version: EV calculator.

How do I get a Kelly stake size from an API?

POST /api/kelly returns the growth-maximizing fraction of a bankroll and the stake it implies. Supply your own probability with p_true, or let the endpoint de-vig the posted line and shift it by bias.

FieldTypeReq.Meaning and units
ml_anumberyesThe price you would take, side A. Unitless moneyline. b is derived from this side.
ml_bnumberyesThe other side, needed to de-vig the market.
capitalnumberyesBankroll, in money. Must be positive.
p_truenumbernoYour win probability for side A, a fraction in (0, 1). Overrides the de-vig path.
biasnumbernoProbability points, as a fraction, added to the de-vigged side A when p_true is absent. Default 0.
fractionnumbernoKelly multiplier in (0, 1]. Default 0.5.
methodstringnoDe-vig method, default proportional. Ignored when p_true is supplied.
curl -s https://teachersbet.com/api/kelly \
  -H 'content-type: application/json' \
  -d '{"ml_a": -152, "ml_b": 138, "p_true": 0.65, "capital": 1000, "fraction": 0.5}'
{
  "inputs": {"ml_a": -152, "ml_b": 138, "capital": 1000, "fraction": 0.5,
             "p_true": 0.65, "bias": 0, "method": "proportional"},
  "implied": {"a": 0.6031746031746031, "b": 0.42016806722689076},
  "overround": 0.023342670401493848,
  "fair": {"a": 0.5894160583941606, "b": 0.41058394160583944},
  "p_true": 0.65,
  "p_source": "user",
  "edge": 0.060583941605839464,
  "b": 0.6578947368421053,
  "expected_value_per_dollar": 0.0776315789473685,
  "kelly_full": 0.1180000000000001,
  "kelly_fraction": 0.05900000000000005,
  "stake": 59.00000000000005,
  "growth_rate_full": 0.004650544830879226,
  "growth_rate_fractional": 0.003468661481092924
}

p_source tells you where the probability came from: "user", or "devig[power] + bias" when the endpoint derived it. When the inputs carry no edge, kelly_fraction and stake come back as 0: the clamp is in the math, not in a display layer. Formula: full Kelly. Browser version: Kelly calculator.

How do I size several simultaneous bets with an API?

POST /api/kelly/simultaneous optimizes the whole slate at once by maximizing expected log wealth over every win/loss combination, instead of stacking independent per-bet Kelly stakes that together overbet the bankroll.

FieldTypeReq.Meaning and units
betsarrayyes1 to 14 bet objects. The cap is deliberate: the solver enumerates 2n outcomes.
bets[].ml_anumberyesThe price taken on this bet. Unitless moneyline.
bets[].ml_bnumberyesThe other side of that market.
bets[].p_truenumbernoYour probability for that bet, a fraction in (0, 1).
bets[].biasnumbernoFractional probability shift applied to the de-vigged side A. Default 0.
bets[].labelstringnoYour own name for the bet, up to 80 characters. Echoed back.
capitalnumberyesBankroll, in money. Must be positive.
fractionnumbernoKelly multiplier in (0, 1]. Default 0.5.
methodstringnoDe-vig method for any bet without p_true. Default proportional.
curl -s https://teachersbet.com/api/kelly/simultaneous \
  -H 'content-type: application/json' \
  -d '{"bets": [{"label": "Game 1", "ml_a": -152, "ml_b": 138, "p_true": 0.65},
                {"label": "Game 2", "ml_a": 120, "ml_b": -140, "p_true": 0.5}],
       "capital": 1000, "fraction": 0.5}'
{
  "inputs": {
    "bets": [
      {"label": "Game 1", "ml_a": -152, "ml_b": 138, "p_true": 0.65, "bias": 0},
      {"label": "Game 2", "ml_a": 120, "ml_b": -140, "p_true": 0.5, "bias": 0}
    ],
    "capital": 1000, "fraction": 0.5, "method": "proportional"
  },
  "bets": [
    {"label": "Game 1", "ml_a": -152, "ml_b": 138, "b": 0.6578947368421053,
     "p_true": 0.65, "p_source": "user", "kelly_naive": 0.1180000000000001,
     "kelly_joint_full": 0.11700235330520108, "kelly_joint": 0.05850117665260054,
     "stake": 58.501176652600535},
    {"label": "Game 2", "ml_a": 120, "ml_b": -140, "b": 1.2,
     "p_true": 0.5, "p_source": "user", "kelly_naive": 0.08333333333333331,
     "kelly_joint_full": 0.08255818215898708, "kelly_joint": 0.04127909107949354,
     "stake": 41.27909107949354}
  ],
  "sum_naive": 0.20133333333333342,
  "sum_joint_full": 0.19956053546418817,
  "sum_joint": 0.09978026773209409,
  "total_stake": 99.78026773209407,
  "log_growth_full": 0.008760846117544886
}

Compare sum_naive with sum_joint_full to see what the correction is worth: 20.13% of bankroll bet-by-bet versus 19.96% jointly on two independent bets. Correlated bets move that gap a great deal further. Formula: joint Kelly.

How do I detect an arbitrage across sportsbooks with an API?

POST /api/arb takes any number of two-sided quotes, picks the best price on each side, and reports whether the inverse decimal odds sum below one, plus the stake split that equalizes the payouts if they do.

FieldTypeReq.Meaning and units
quotesarrayyes1 to 200 quote objects, one per venue.
quotes[].bookstringyesVenue name, up to 64 characters. A label only; it carries no weight in the math.
quotes[].ml_anumberyesThat venue’s price on side A. Unitless moneyline.
quotes[].ml_bnumberyesThat venue’s price on side B.
total_stakenumbernoTotal across both legs, in money. Default 100. Must be positive.
curl -s https://teachersbet.com/api/arb \
  -H 'content-type: application/json' \
  -d '{"quotes": [{"book": "Book A", "ml_a": 110, "ml_b": -105},
                  {"book": "Book B", "ml_a": -102, "ml_b": 115}],
       "total_stake": 1000}'
{
  "best": {
    "a": {"book": "Book A", "ml": 110, "decimal": 2.1},
    "b": {"book": "Book B", "ml": 115, "decimal": 2.15}
  },
  "inverse_odds_sum": 0.9413067552602437,
  "is_arb": true,
  "total_stake": 1000,
  "stake_a": 505.8823529411764,
  "stake_b": 494.1176470588235,
  "payout": 1062.3529411764705,
  "profit": 62.352941176470495,
  "return_fraction": 0.0623529411764705,
  "suspect": false,
  "same_book": false,
  "warnings": [
    "Arbs are ephemeral: lines move in seconds and the edge may be gone before both legs fill.",
    "One-sided risk: if a line moves after the first leg is placed, the position is an ordinary bet, not an arb.",
    "Books limit or ban accounts that consistently take arb prices."
  ],
  "quotes": [
    {"book": "Book A", "ml_a": 110, "ml_b": -105, "decimal_a": 2.1, "decimal_b": 1.9523809523809523},
    {"book": "Book B", "ml_a": -102, "ml_b": 115, "decimal_a": 1.9803921568627452, "decimal_b": 2.15}
  ]
}

Read suspect before is_arb. It goes true above a 12% return, where a two-way market is far more likely to contain one stale price than free money. same_book flags the degenerate case of both best prices coming from one venue: negative margin, which is nearly always an error. The warnings array is always populated; the arithmetic assumes both legs fill at the quoted price, which is exactly the assumption that fails in practice. Formulas: inverse-odds sum, stake split.

How do I value a free bet or bonus bet with an API?

POST /api/bonusbet converts a stake-not-returned promotional bet into cash terms: its fair value, and, if you supply a hedge price on the other side, the stake that equalizes both outcomes and what it locks in.

FieldTypeReq.Meaning and units
bonusnumberyesFace value of the free bet, in money. Must be positive.
bonus_mlnumberyesThe price the free bet is placed at. Unitless moneyline.
hedge_mlnumbernoCash price available on the opposite side. Omit for the unhedged valuation only.
keep_stakebooleannotrue if the token returns the stake as well as the profit (a cash-like bonus). Default false.
curl -s https://teachersbet.com/api/bonusbet \
  -H 'content-type: application/json' \
  -d '{"bonus": 100, "bonus_ml": 300, "hedge_ml": -360}'
{
  "bonus": 100,
  "bonus_decimal": 4,
  "keep_stake": false,
  "win_profit": 300,
  "fair_value_rate": 0.75,
  "cash_value": 75,
  "hedged": true,
  "hedge_decimal": 1.2777777777777777,
  "hedge_stake": 234.7826086956522,
  "guaranteed_profit": 65.2173913043478,
  "conversion_rate": 0.652173913043478,
  "profit_if_bonus_wins": 65.21739130434781,
  "profit_if_hedge_wins": 65.2173913043478,
  "inputs": {"bonus": 100, "bonus_ml": 300, "hedge_ml": -360, "keep_stake": false}
}

fair_value_rate is (d − 1) / d for the bonus price: the fraction of face value a stake-not-returned token is worth at a fair hedge, which rises with the odds. conversion_rate is what this particular hedge actually realizes, and it is lower, because the hedge price carries its own vig. Both are calculations on the two prices supplied. Browser version: free bet calculator.

How do I calculate parlay odds and payout with an API?

POST /api/parlay multiplies the decimal odds of every leg into the combined price, the payout on a given stake, and the break-even probability all legs must jointly clear. Supply true_prob and it also returns the expected value.

FieldTypeReq.Meaning and units
legsarrayyes2 to 15 American moneylines, as bare numbers. Each must satisfy |ML| ≥ 100.
stakenumbernoAmount risked, in money. Default 100. Must be positive.
true_probnumbernoYour probability that every leg lands, a fraction in (0, 1). Omit and the EV fields come back null.
curl -s https://teachersbet.com/api/parlay \
  -H 'content-type: application/json' \
  -d '{"legs": [-110, -110, -110], "stake": 100}'
{
  "legs": [
    {"ml": -110, "decimal": 1.9090909090909092, "implied": 0.5238095238095238},
    {"ml": -110, "decimal": 1.9090909090909092, "implied": 0.5238095238095238},
    {"ml": -110, "decimal": 1.9090909090909092, "implied": 0.5238095238095238}
  ],
  "n_legs": 3,
  "combined_decimal": 6.9579263711495125,
  "combined_american": 595.7926371149513,
  "implied_prob": 0.1437209804556743,
  "stake": 100,
  "payout": 695.7926371149513,
  "profit": 595.7926371149513,
  "true_prob": null,
  "expected_value_per_dollar": null,
  "edge": null,
  "is_positive_ev": null,
  "inputs": {"legs": [-110, -110, -110], "stake": 100, "true_prob": null}
}

implied_prob is the vig-inclusive break-even chance: 14.37% here, against 52.38% per leg. The margin compounds the same way the odds do, which is the arithmetic the payout figure hides. Browser version: parlay calculator.

How do I calculate Kalshi trading fees with an API?

POST /api/kalshifees returns the fee on a Kalshi event-contract order with the exchange’s round-up-to-the-cent rule applied, the total outlay, the fee-adjusted break-even price, and the cost of trading out versus holding to settlement.

FieldTypeReq.Meaning and units
price_centsnumberyesContract price in cents, 1 to 99. A 50¢ contract is 50, not 0.50.
contractsintegernoOrder size, 1 to 1,000,000. Default 1. Must be a whole number.
sidestringnotaker (default, crossing the spread) or maker (a resting order).
curl -s https://teachersbet.com/api/kalshifees \
  -H 'content-type: application/json' \
  -d '{"price_cents": 50, "contracts": 100, "side": "taker"}'
{
  "inputs": {"price_cents": 50, "contracts": 100, "side": "taker"},
  "rate": 0.07,
  "fee": 1.75,
  "fee_per_contract": 0.0175,
  "cost": 50,
  "total_outlay": 51.75,
  "breakeven": {"formula": 0.5175, "effective": 0.5175},
  "roundtrip_fee": 3.5,
  "settlement_fee": 0
}

All monetary fields are in dollars. breakeven.formula applies price + rate × price × (1 − price); breakeven.effective divides the actual rounded outlay by the contract count, so the two can differ by a fraction of a cent on small orders. settlement_fee is 0 because holding to resolution costs nothing, which makes roundtrip_fee the number that matters if you intend to trade out.

Some major events instead carry a flat 0.25% maker fee this endpoint does not model. Check the contract’s own terms. Formula: Kalshi fees. Browser version: Kalshi fee calculator.

How do I convert betting odds between formats with an API?

POST /api/oddsconvert takes one price in any of five notations and returns it in all of them, plus the probability it implies and what $100 does at it. The five notations are one number written five ways: the break-even probability, which is how often the bet has to win for the price to be a coin flip.

FieldTypeReq.Meaning and units
pricenumber or stringyesThe price, in the notation named by format. Fractional prices are strings: "10/11", "10:11" and "10-11" all parse, and a bare number in fractional form is read as net odds.
formatstringnoamerican (default), decimal, fractional, percent or cents. Percent and cents both sit strictly between 0 and 100.
curl -s https://teachersbet.com/api/oddsconvert \
  -H 'content-type: application/json' \
  -d '{"price": -110}'
{
  "inputs": {"value": -110, "format": "american"},
  "implied_prob": 0.5238095238095238,
  "decimal": 1.909090909090909,
  "moneyline": -110.00000000000001,
  "net_odds": 0.909090909090909,
  "fractional": {"numerator": 10, "denominator": 11, "exact": true},
  "cents": 52.38095238095239,
  "per_100": {"stake": 100, "profit": 90.90909090909089, "total_return": 190.9090909090909}
}

The fractional form is reduced by continued-fraction convergents, not rounded to a lookup table, so exact is true for every American price and honestly false where a decimal or percentage input can only be approximated. moneyline can come back a hair off a round number, as above: that is the round trip through a float, not a different price. implied_prob is vig-inclusive. It still carries whatever margin the venue built into the price, and removing that needs both sides of the market, which is /api/devig.

Formula: implied probability. Browser version: odds converter.

How do I calculate a bet payout with an API?

POST /api/payout returns the profit and the total return on a stake. Both, because decimal odds quote the total return while American and fractional odds quote the profit, and confusing the two is the most common payout error there is.

FieldTypeReq.Meaning and units
stakenumberyesAmount risked, above 0, in whatever currency you are thinking in.
pricenumber or stringyesThe price you are getting, in the notation named by format.
othernumber or stringnoThe price on the opposite side of the same market, same notation. Supplying it adds the vig-free payout.
formatstringnoAs for /api/oddsconvert. Default american.
methodstringnoproportional (default), power or shin: how the margin is split when other is supplied.
curl -s https://teachersbet.com/api/payout \
  -H 'content-type: application/json' \
  -d '{"stake": 100, "price": -110, "other": -105}'
{
  "stake": 100,
  "implied_prob": 0.5238095238095238,
  "decimal": 1.909090909090909,
  "net_odds": 0.909090909090909,
  "moneyline": -110.00000000000001,
  "fractional": {"numerator": 10, "denominator": 11, "exact": true},
  "profit": 90.90909090909089,
  "total_return": 190.9090909090909,
  "fair": {
    "method": "proportional",
    "implied_other": 0.5121951219512195,
    "overround": 0.03600464576074325,
    "fair_prob": 0.5056053811659194,
    "fair_decimal": 1.9778270509977824,
    "fair_profit": 97.78270509977824,
    "fair_total_return": 197.78270509977824,
    "vig_cost": 6.873614190687349
  }
}

So the vig on this pair costs this particular $100 bet $6.87 of profit. fair is null unless other is supplied, because one price cannot tell you what it would be worth without the margin. vig_cost is 0 when the two prices sum to exactly 100% and goes negative when they sum below it, which is the arbitrage condition rather than a cost. No margin is invented where the prices leave none.

Formula: net odds. Browser version: bet payout calculator.

How do I calculate a hedge stake with an API?

POST /api/hedge sizes a hedge on a bet you already hold, and prints what the position pays whichever side wins. The stake that pays the same either way divides the first bet’s total return by the hedge price, not its profit: H = S × d1 / d2. That one substitution is where most hedge arithmetic goes wrong.

FieldTypeReq.Meaning and units
stakenumberyesThe original bet, above 0.
ml_takennumberyesThe American price you took.
ml_hedgenumberyesThe American price now available on the other side.
modestringnoequal (default) pays the same either way; breakeven returns the original stake and no more; partial takes partial_pct of the equal stake.
partial_pctnumbernoPercent of the equal-profit stake, 0 to 1000. Used only in partial mode. Default 100.
ml_nownumbernoThe current American price on the side you hold. Supplying it adds the expected-cost half, since a fair probability needs both current prices.
methodstringnoDe-vig method for that fair probability. Default proportional.
curl -s https://teachersbet.com/api/hedge \
  -H 'content-type: application/json' \
  -d '{"stake": 100, "ml_taken": 150, "ml_hedge": -130, "ml_now": 140}'
{
  "decimal_taken": 2.5,
  "decimal_hedge": 1.7692307692307692,
  "original_return": 250,
  "hedge_stake": 141.30434782608697,
  "equal_profit_stake": 141.30434782608697,
  "stake_back_stake": 130,
  "if_original_wins": 8.695652173913032,
  "if_hedge_wins": 8.695652173913047,
  "swing": 1.4210854715202004e-14,
  "worst_case": 8.695652173913032,
  "total_staked": 241.30434782608697,
  "same_either_way": true,
  "cost": {
    "fair_prob_original": 0.4243542435424355,
    "fair_prob_hedge": 0.5756457564575646,
    "expected_cost": -2.6070912883041837,
    "cost_per_dollar_hedged": -0.01845018450184499,
    "expected_value_unhedged": 6.0885608856088735,
    "expected_value_hedged": 8.695652173913057,
    "hedge_priced_above_fair": true
  }
}

$141.30 locks $8.70 either way. swing is the distance between the two outcomes and same_either_way is true below half a cent of it, which is why a float residue of 1e-14 still counts as locked. expected_cost is the hedge leg’s own expected value with the sign flipped, so a positive number is what buying the certainty costs. Here it is negative: at +140 against −130 the two current prices cross, so the hedge is priced above fair value and is not a cost at all. The API reports that rather than flooring the figure at zero.

Formula: hedge stake. Browser version: hedge calculator.

How do I calculate a middle bet with an API?

POST /api/middle prices two opposite-side bets taken at different numbers, so there is a window where both tickets win. The payoff grid has three cells rather than the two an arbitrage has, and everything follows from sigma, the combined implied probability of the pair.

FieldTypeReq.Meaning and units
ml_1numberyesOne side, at its number, as an American price.
ml_2numberyesThe opposite side, at a different number.
stakenumbernoTotal across both sides, split for an equal payout. Default 100.
hit_probnumbernoYour own estimate of how often the window lands, as a fraction in [0, 1). Supplying it adds expected value.
curl -s https://teachersbet.com/api/middle \
  -H 'content-type: application/json' \
  -d '{"ml_1": -110, "ml_2": -110, "stake": 100, "hit_prob": 0.06}'
{
  "decimal_1": 1.9090909090909092,
  "decimal_2": 1.9090909090909092,
  "inverse_odds_sum": 1.0476190476190477,
  "required_hit_rate": 0.04761904761904767,
  "hold": 0.045454545454545504,
  "stake_1": 50,
  "stake_2": 50,
  "payout_one_side": 95.45454545454545,
  "if_miss": -4.545454545454547,
  "if_hit": 90.9090909090909,
  "is_arb": false,
  "expected_value": {
    "expected_value": 1.181818181818187,
    "expected_value_per_dollar": 0.01181818181818187,
    "edge_vs_required": 0.012380952380952326
  }
}

required_hit_rate is sigma − 1, the overround on the two prices: the rate at which the window has to land for the position to return exactly $0. It is not the hold. The hold is (sigma − 1) / sigma, the share of every dollar risked, and it is what a miss actually costs: 4.55% here, not 4.76%. Both are returned so neither has to be inferred from the other. When is_arb is true the pair sums below 100%, both sides already clear, and the window is a bonus rather than the point. An estimate sitting exactly on the required rate returns an expected value of 0.

Formula: required hit rate. Browser version: middle calculator.

How do I calculate teaser odds with an API?

POST /api/teaser answers the only part of a teaser that is arithmetic: what the bought points have to be worth for the quoted price to break even. Whether they are worth that is a question about where football margins land, and this endpoint does not answer it.

FieldTypeReq.Meaning and units
pricenumberyesThe quoted American price for the whole teaser ticket.
legsintegeryesNumber of legs, 2 to 8.
base_pricenumberyesOne leg at its untouched number, before the tease.
pointsnumbernoPoints bought per leg, up to 60. Adds the per-point average.
alt_pricesarraynoThe straight price at each teased number: one price applied to every leg, or exactly one per leg. Adds the only apples-to-apples comparison of a teaser price.
p_legnumbernoYour own estimate of each leg’s win rate after the tease, in (0, 1). Adds expected value.
curl -s https://teachersbet.com/api/teaser \
  -H 'content-type: application/json' \
  -d '{"price": -120, "legs": 2, "base_price": -110, "points": 6}'
{
  "decimal": 1.8333333333333335,
  "decimal_base": 1.9090909090909092,
  "breakeven_ticket": 0.5454545454545454,
  "breakeven_per_leg": 0.7385489458759964,
  "breakeven_base_leg": 0.5238095238095238,
  "points_must_buy": 0.21473942206647256,
  "per_point": 0.03578990367774543,
  "parlay_decimal": 3.6446280991735542,
  "payout_given_up": 0.4969765684051398,
  "vs_alternate": null,
  "expected_value": null
}

The ticket needs 54.55%; two equal, independent legs therefore need 73.85% each; a −110 leg is already 52.38%. So the six points have to add 21.47 probability points per leg, an average of 3.58 per point. Average is the operative word: the points beside 3 and 7 are worth several times the ones on either side of them. independent is doing real work too, since a tease off one game’s key numbers is correlated and breaks the product rule in both directions.

Formula: teaser break-even. Browser version: teaser calculator.

How do I calculate a round robin with an API?

POST /api/roundrobin builds every combination of a set of legs as a separate parlay and returns what the slip risks, what each hit level pays, and how often each lands. The stake is per ticket, which is the single biggest surprise on a round-robin slip: three legs by 2s at $10 risks $30, not $10.

FieldTypeReq.Meaning and units
legsarrayyesAmerican prices, 3 to 9 of them. Two legs make a single combination, which is an ordinary parlay. Nine is the ceiling because every outcome is enumerated exactly.
sizesarraynoCombination sizes, each at least 2 and at most the leg count. Duplicates collapse. Default [2].
stakenumbernoPer ticket, not in total. Default 10.
p_legnumbernoA common per-leg win rate in (0, 1), assuming independent legs. Adds expected value and the per-level probabilities.
include_ticketsbooleannoReturn the full ticket list, up to 502 rows at nine legs. Default false; by_size always reports the shape.
curl -s https://teachersbet.com/api/roundrobin \
  -H 'content-type: application/json' \
  -d '{"legs": [-110, 120, -150], "sizes": [2], "stake": 10, "p_leg": 0.55}'
{
  "n_legs": 3,
  "decimals": [1.9090909090909092, 2.2, 1.6666666666666667],
  "n_tickets": 3,
  "by_size": [{"size": 2, "tickets": 3}],
  "stake_per_ticket": 10,
  "total_risk": 30,
  "min_decimal": 3.1818181818181825,
  "max_decimal": 4.200000000000001,
  "uniform_payout": false,
  "levels": [
    {"correct": 0, "tickets_cashing_max": 0, "return_min": 0, "net_min": -30, "probability": 0.091125},
    {"correct": 1, "tickets_cashing_max": 0, "return_min": 0, "net_min": -30, "probability": 0.334125},
    {"correct": 2, "tickets_cashing_max": 1, "return_min": 31.818181818181827, "net_min": 1.8181818181818272, "probability": 0.40837500000000004},
    {"correct": 3, "tickets_cashing_max": 3, "return_min": 110.48484848484851, "net_min": 80.48484848484851, "probability": 0.16637500000000004}
  ],
  "legs_to_clear_risk": 2,
  "expected_value": {
    "expected_value": 3.4216666666666704,
    "expected_value_per_dollar": 0.11405555555555568,
    "profit_probability": 0.5747500000000001
  }
}

Each levels row is exact, not sampled: all 2n possible sets of winning legs are enumerated, which is why nine legs is the cap. Rows are abridged above; each also carries worlds, tickets_cashing_min, return_max, return_mean and net_max. legs_to_clear_risk is the fewest correct legs at which every winning combination returns more than the slip risked.

Expected value is additive across tickets, so per dollar risked a round robin is exactly the stake-weighted average of the parlays inside it. Combining adds no edge; it changes the spread of outcomes. The per-level probabilities assume independent legs at a common rate, which legs from the same game are not.

Formula: round-robin tickets. Browser version: round robin calculator.

How do I calculate a break-even win rate with an API?

POST /api/breakeven does two things. It converts a price into the win rate it needs, and, given a settled record, it tests that record against the rate. The second half is the one most tools skip: a win rate is a sample mean of a coin flip, and a sample mean is not a skill level.

FieldTypeReq.Meaning and units
pricenumberyesAn American price (|ML| ≥ 100), a decimal price above 1, or a contract price strictly between 0 and 100 cents, per format.
formatstringnoamerican (default), decimal or cents. A fractional or percentage price should go through /api/oddsconvert first.
sidestringnotaker (default) or maker. Used only when format is cents: which side of the exchange fee applies.
betsintegernoBets settled, 1 to 10,000,000. Required together with wins.
winsintegernoHow many of those won, 0 to bets. Required together with bets.
curl -s https://teachersbet.com/api/breakeven \
  -H 'content-type: application/json' \
  -d '{"price": -110, "bets": 100, "wins": 55}'
{
  "breakeven_win_rate": 0.5238095238095238,
  "wins_per_100": 52.38095238095239,
  "decimal": 1.909090909090909,
  "fee_rate": null,
  "profit_if_win": 90.90909090909089,
  "record": {
    "rate": 0.55,
    "breakeven": 0.5238095238095238,
    "edge": 0.02619047619047621,
    "standard_error": 0.04994327848429293,
    "z": 0.5244044240850761,
    "p_two_sided": 0.5999972800889763,
    "separated": false,
    "wilson_95": {"low": 0.45244602995194916, "high": 0.6438546202256804},
    "bets_to_detect": 2847.484128515327
  }
}

55 of 100 at −110 is 2.6 points over break-even and separated: false: the 95% Wilson interval runs from 45.2% to 64.4%, which is entirely consistent with having no edge at all. bets_to_detect says a rate that size needs about 2,850 settled bets before a test can see it at 95% confidence and 80% power, and it is null at or below break-even, where there is no edge to detect. The interval, not the point estimate, is the result.

The standard error is taken under the null hypothesis, sqrt(q(1−q)/n), since q is the rate being tested against rather than an estimate from the sample. A record is both halves: sending bets without wins is a 422, not a zero-win sample, because a missing count must never read as a confident verdict from no data.

On a prediction-market contract the fee moves the requirement: {"price": 50, "format": "cents"} returns 0.5175, above the 50¢ price, with fee_rate: 0.07 and decimal: null.

Formula: break-even win rate. Browser version: break-even win rate calculator.

Status codes and errors

Errors are JSON, never HTML, and always carry a detail string that names the offending field and states the rule it broke.

CodeWhen
200Computed. The body carries the result plus every intermediate value.
404Unknown path, or the right path with the wrong method (these endpoints are POST-only).
402A data endpoint (board, alerts, history) without an Extra Credit token. The detail names what the endpoint serves and where to get a token. The calculators never answer this.
413Request body over 256 KB. Rejected before parsing.
422A field is missing, the wrong type, or out of range.
429Rate limited. A retry-after: 60 header comes with it.
451Request from a restricted region. See geographic restriction.
$ curl -s https://teachersbet.com/api/devig \
    -H 'content-type: application/json' -d '{"ml_a": -50, "ml_b": 138}'
{"detail": "ml_a must be <= -100 or >= 100"}

$ curl -s https://teachersbet.com/api/devig \
    -H 'content-type: application/json' -d '{"ml_a": "-152", "ml_b": 138}'
{"detail": "ml_a must be a number"}

$ curl -s https://teachersbet.com/api/kelly \
    -H 'content-type: application/json' -d '{"ml_a": -152, "ml_b": 138}'
{"detail": "capital is required"}

A 422 body carries detail and nothing else; validation stops at the first failing field, so fix them one at a time. A 422 is usually resolvable without reading this page.

Rate limits

60 requests per minute per IP across the fifteen calculator endpoints combined, then 429 with retry-after: 60. There is no burst allowance and no plan lifts it. The data endpoints (board, alerts, line history) sit in a smaller bucket of their own, because each one analyses the whole board per call.

A real limit, but a generous one for the intended uses: a script pricing a slate, a notebook checking a derivation, a backend computing a stake. It is deliberately not enough to mirror the endpoints as a backing service for someone else’s product. The math is MIT-licensed and installable, so anyone needing it at volume should run it locally. That path is faster, has no limit, and does not depend on this site staying up.

These endpoints compute. They read no database and they are not a data feed, which is why the limit is the same for everyone who has access at all.

Building something on this?

The advice above is honest and it stays: if you need the arithmetic at volume, install the library and run it locally. It is MIT, it is the same code the site runs, and it will beat any network call. Nothing about that changes.

What the library does not contain is the fair line: the de-vigged, weighted, leave-one-out consensus across venues, and the refusal rules that decide when it declines to print a number at all. That part is computed here, from licensed feeds, and our own house rules keep the fitted weights private while publishing every result.

Two plain facts about it. The board API is the paid product: GET /api/board answers 402 without an Extra Credit key, because the fair line computed live is the thing the subscription pays for. The same board is free in the browser, two hours behind, and every math endpoint on this page stays open with no key. And there is no business plan to point you at, because quoting one before we can support it would be a promise our uptime record has not earned yet. So if you are building on the fair line rather than on the calculators, write and say what you are building. We would rather know who depends on this than find out from the logs, and knowing is what would let a properly supported tier exist later.

Geographic restriction

Requests from Washington State receive 451 Unavailable For Legal Reasons on the API. RCW 9.46.240 makes internet transmission of betting odds a presumptive felony, and whether an odds-arithmetic API falls inside that statute is unsettled. Rather than resolve an open legal question in our own favour, the surface is gated pending counsel. This documentation page, the formula sheet, the Learn course and the calculators are never gated.

A note on crawling and indexing

robots.txt allows crawlers across the public site and closes only the account, billing and admin paths. It does not blanket-disallow /api/. It does not need to: all fifteen endpoints are POST-only, so a crawler issuing a GET gets {"detail": "not found"} and a 404. There is no document at those URLs to index.

This page is the indexable surface, and /openapi.json is the machine-readable one. If you are an agent or a retrieval system, the OpenAPI document is the thing to fetch: it describes all fifteen operations, their parameter schemas and their response shapes in one file.

What this API does not do