API Reference

Eight endpoints. No key, no account, no plan. They compute betting math on numbers you send and hand back every intermediate value, so the derivation is reproducible on your side.

The Teacher’s Bet API is eight free, unauthenticated 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 and Kalshi contract fees. Each returns JSON with the result and every intermediate value. It serves no odds data and issues no picks.

Quickstart

One request, no setup:

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

That returns the fair probabilities behind a −152 / +138 market under all three de-vig methods, plus the overround. Every other endpoint follows the same shape.

What is true of every endpoint

The eight 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

A machine-readable description of all eight 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 that this endpoint does not model — check the contract’s own terms. Formula: Kalshi fees. Browser version: Kalshi fee 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).
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, and what they cost

60 requests per minute per IP across the eight endpoints combined, then 429 with retry-after: 60. There is no burst allowance and no paid tier that lifts it.

That is 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 not enough to mirror the endpoints as a backing service for someone else’s product, which is deliberate — the math is MIT-licensed and installable, so anyone who needs it at volume should run it locally rather than round-trip it. That path is faster, has no limit, and does not depend on this site staying up.

The endpoints are free and are intended to stay free. They compute; they do not read any database, and they are not a data feed.

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: the eight 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 eight operations, their parameter schemas and their response shapes in one file.

What this API does not do