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
- Base URL:
https://teachersbet.com. HTTPS only. - Method:
POSTwithcontent-type: application/json. AGETto any of these paths returns404. There is no query-string form. - Authentication: none for the calculators. The fifteen math
endpoints below take no key and no account, because the same math is
MIT-licensed and installable and charging for it would be charging for
something you can already run. The data endpoints need an Extra Credit
subscription, sent as
Authorization: Bearer <token>: that isGET /api/board(the live fair line and per-venue grades),GET /api/alerts(open +EV and arbitrage cases) and the line-history endpoint. Mint a token withPOST /api/pro/keyfrom a signed-in browser session; it is good for 31 days, and minting a new one does not revoke the old one. Without one those endpoints answer402with adetailthat says what they serve and where to get a token. - What stays free: every calculator here, the whole Learn course, and the board on a delay. Those pages do the arithmetic in your browser: no key, no account, no network. What you are paying for is a machine calling it, not a better answer. The numbers are identical and both mirrors are golden-tested against each other.
- Numbers must be JSON numbers.
{"ml_a": -152}is accepted;{"ml_a": "-152"}is rejected with422. Strings are never coerced. - Moneylines must satisfy
|ML| ≥ 100. A value inside (−100, 100) is rejected, because 0 divides by zero and the rest are not real American prices. - Probabilities are fractions in (0, 1), never percentages. Send
0.65, not65. - Money is a plain number in whatever currency you are thinking in. The math is unit-agnostic;
capital: 1000and a returnedstake: 59are in the same units. - Every 200 response carries a
metaobject withcomputed_at(ISO-8601 UTC, when the server computed the answer) anddisclaimer. It is shown in full under /api/devig and omitted from the other samples for length. - Rate limit: 60 requests per minute per IP across these fifteen endpoints. See rate limits.
- Request bodies above 256 KB are rejected with
413before they are parsed. - No CORS headers are sent. A browser page on another origin cannot call these directly; call them server-side, from a script, or from your own backend.
The fifteen endpoints
| Endpoint | Answers |
|---|---|
| POST /api/devig | Fair probabilities behind a two-sided moneyline, three ways |
| POST /api/ev | Expected value of one price against a reference line |
| POST /api/kelly | Kelly stake fraction and amount for a single bet |
| POST /api/kelly/simultaneous | Joint Kelly stakes across bets that settle together |
| POST /api/arb | Whether posted prices cross, and the stake split if they do |
| POST /api/bonusbet | Cash value of a stake-not-returned free bet, hedged or not |
| POST /api/parlay | Combined odds, payout and break-even chance of a parlay |
| POST /api/kalshifees | Kalshi taker/maker fee, total outlay and fee-adjusted break-even |
| POST /api/oddsconvert | One price written in all five notations, and the probability it implies |
| POST /api/payout | Profit and total return on a stake, and what the vig costs it |
| POST /api/hedge | Hedge stake, the result either way, and the price of the certainty |
| POST /api/middle | Required hit rate and payoff grid for two prices at different numbers |
| POST /api/teaser | What a teaser’s bought points have to be worth per leg |
| POST /api/roundrobin | Tickets, total risk and an exact hit-level table for a round robin |
| POST /api/breakeven | The 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.
| Field | Type | Req. | Meaning and units |
|---|---|---|---|
| ml_a | number | yes | American moneyline for side A. Unitless, |ml_a| ≥ 100. |
| ml_b | number | yes | American moneyline for side B. Same constraint. |
| method | string | no | proportional (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.
| Field | Type | Req. | Meaning and units |
|---|---|---|---|
| sharp_ml_a | number | yes | Reference line, side A. Unitless moneyline, |ML| ≥ 100. |
| sharp_ml_b | number | yes | Reference line, side B. |
| soft_ml | number | yes | The price being tested, at a different venue. Unitless moneyline. |
| side | string | no | a (default) or b: which side soft_ml is on. |
| method | string | no | proportional, power (default here), or shin. |
| capital | number | no | Bankroll, in money. Omit and stake comes back null; supply it and stake is the sized bet. Must be positive. |
| fraction | number | no | Kelly 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.
| Field | Type | Req. | Meaning and units |
|---|---|---|---|
| ml_a | number | yes | The price you would take, side A. Unitless moneyline. b is derived from this side. |
| ml_b | number | yes | The other side, needed to de-vig the market. |
| capital | number | yes | Bankroll, in money. Must be positive. |
| p_true | number | no | Your win probability for side A, a fraction in (0, 1). Overrides the de-vig path. |
| bias | number | no | Probability points, as a fraction, added to the de-vigged side A when p_true is absent. Default 0. |
| fraction | number | no | Kelly multiplier in (0, 1]. Default 0.5. |
| method | string | no | De-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.
| Field | Type | Req. | Meaning and units |
|---|---|---|---|
| bets | array | yes | 1 to 14 bet objects. The cap is deliberate: the solver enumerates 2n outcomes. |
| bets[].ml_a | number | yes | The price taken on this bet. Unitless moneyline. |
| bets[].ml_b | number | yes | The other side of that market. |
| bets[].p_true | number | no | Your probability for that bet, a fraction in (0, 1). |
| bets[].bias | number | no | Fractional probability shift applied to the de-vigged side A. Default 0. |
| bets[].label | string | no | Your own name for the bet, up to 80 characters. Echoed back. |
| capital | number | yes | Bankroll, in money. Must be positive. |
| fraction | number | no | Kelly multiplier in (0, 1]. Default 0.5. |
| method | string | no | De-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.
| Field | Type | Req. | Meaning and units |
|---|---|---|---|
| quotes | array | yes | 1 to 200 quote objects, one per venue. |
| quotes[].book | string | yes | Venue name, up to 64 characters. A label only; it carries no weight in the math. |
| quotes[].ml_a | number | yes | That venue’s price on side A. Unitless moneyline. |
| quotes[].ml_b | number | yes | That venue’s price on side B. |
| total_stake | number | no | Total 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.
| Field | Type | Req. | Meaning and units |
|---|---|---|---|
| bonus | number | yes | Face value of the free bet, in money. Must be positive. |
| bonus_ml | number | yes | The price the free bet is placed at. Unitless moneyline. |
| hedge_ml | number | no | Cash price available on the opposite side. Omit for the unhedged valuation only. |
| keep_stake | boolean | no | true 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.
| Field | Type | Req. | Meaning and units |
|---|---|---|---|
| legs | array | yes | 2 to 15 American moneylines, as bare numbers. Each must satisfy |ML| ≥ 100. |
| stake | number | no | Amount risked, in money. Default 100. Must be positive. |
| true_prob | number | no | Your 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.
| Field | Type | Req. | Meaning and units |
|---|---|---|---|
| price_cents | number | yes | Contract price in cents, 1 to 99. A 50¢ contract is 50, not 0.50. |
| contracts | integer | no | Order size, 1 to 1,000,000. Default 1. Must be a whole number. |
| side | string | no | taker (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.
| Field | Type | Req. | Meaning and units |
|---|---|---|---|
| price | number or string | yes | The 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. |
| format | string | no | american (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.
| Field | Type | Req. | Meaning and units |
|---|---|---|---|
| stake | number | yes | Amount risked, above 0, in whatever currency you are thinking in. |
| price | number or string | yes | The price you are getting, in the notation named by format. |
| other | number or string | no | The price on the opposite side of the same market, same notation. Supplying it adds the vig-free payout. |
| format | string | no | As for /api/oddsconvert. Default american. |
| method | string | no | proportional (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.
| Field | Type | Req. | Meaning and units |
|---|---|---|---|
| stake | number | yes | The original bet, above 0. |
| ml_taken | number | yes | The American price you took. |
| ml_hedge | number | yes | The American price now available on the other side. |
| mode | string | no | equal (default) pays the same either way; breakeven returns the original stake and no more; partial takes partial_pct of the equal stake. |
| partial_pct | number | no | Percent of the equal-profit stake, 0 to 1000. Used only in partial mode. Default 100. |
| ml_now | number | no | The current American price on the side you hold. Supplying it adds the expected-cost half, since a fair probability needs both current prices. |
| method | string | no | De-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.
| Field | Type | Req. | Meaning and units |
|---|---|---|---|
| ml_1 | number | yes | One side, at its number, as an American price. |
| ml_2 | number | yes | The opposite side, at a different number. |
| stake | number | no | Total across both sides, split for an equal payout. Default 100. |
| hit_prob | number | no | Your 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.
| Field | Type | Req. | Meaning and units |
|---|---|---|---|
| price | number | yes | The quoted American price for the whole teaser ticket. |
| legs | integer | yes | Number of legs, 2 to 8. |
| base_price | number | yes | One leg at its untouched number, before the tease. |
| points | number | no | Points bought per leg, up to 60. Adds the per-point average. |
| alt_prices | array | no | The 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_leg | number | no | Your 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.
| Field | Type | Req. | Meaning and units |
|---|---|---|---|
| legs | array | yes | American 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. |
| sizes | array | no | Combination sizes, each at least 2 and at most the leg count. Duplicates collapse. Default [2]. |
| stake | number | no | Per ticket, not in total. Default 10. |
| p_leg | number | no | A common per-leg win rate in (0, 1), assuming independent legs. Adds expected value and the per-level probabilities. |
| include_tickets | boolean | no | Return 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.
| Field | Type | Req. | Meaning and units |
|---|---|---|---|
| price | number | yes | An American price (|ML| ≥ 100), a decimal price above 1, or a contract price strictly between 0 and 100 cents, per format. |
| format | string | no | american (default), decimal or cents. A fractional or percentage price should go through /api/oddsconvert first. |
| side | string | no | taker (default) or maker. Used only when format is cents: which side of the exchange fee applies. |
| bets | integer | no | Bets settled, 1 to 10,000,000. Required together with wins. |
| wins | integer | no | How 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.
| Code | When |
|---|---|
| 200 | Computed. The body carries the result plus every intermediate value. |
| 404 | Unknown path, or the right path with the wrong method (these endpoints are POST-only). |
| 402 | A 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. |
| 413 | Request body over 256 KB. Rejected before parsing. |
| 422 | A field is missing, the wrong type, or out of range. |
| 429 | Rate limited. A retry-after: 60 header comes with it. |
| 451 | Request 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
- It serves no odds. There is no endpoint here that returns prices from a sportsbook or an exchange. Every number in a response is derived from numbers in the request.
- It issues no picks. Nothing returns a recommendation, a signal, a rating, or a directional call.
is_positive_evis a comparison between two numbers you supplied, not advice about what to do next. - It takes no wagers and holds no funds. Teacher’s Bet is an analytics tool and is not a sportsbook.
- It has no stability guarantee. Fields get added; nothing published here is versioned yet. Pin the Python library if you need a frozen contract.