Grading Bets Programmatically: Scores, Settlement and Edge Cases

September 25, 2026

Grading Bets Programmatically: Scores, Settlement and Edge Cases

Pricing a bet is the easy half. Telling a user whether they won it is where most integrations get complicated, because the odds and the result usually arrive from two different places.

The conventional setup is an odds feed from one vendor and a results or stats feed from another, then a reconciliation layer that matches them on team names, start times and player identities. That layer is where the bugs live. A player traded mid-season, a postponed fixture, a book that spells a team differently — each one breaks the join, and the symptom is a bet that never settles.

If the result arrives on the same object as the price, that entire layer disappears. This post covers how to do that: which fields decide whether a market can be graded, the status field people grade on by mistake, and the cases that settle silently wrong.

Everything grading needs is on the odd

Request an event and each entry in odds carries its own settlement state alongside its price:

{
  "oddID": "passing_yards-JOSH_ALLEN_1_NFL-game-ou-over",
  "opposingOddID": "passing_yards-JOSH_ALLEN_1_NFL-game-ou-under",
  "statID": "passing_yards",
  "statEntityID": "JOSH_ALLEN_1_NFL",
  "periodID": "game",
  "betTypeID": "ou",
  "sideID": "over",
  "started": true,
  "ended": true,
  "cancelled": false,
  "scoringSupported": true,
  "score": 248,
  "fairOverUnder": "231",
  "bookOverUnder": "224.5"
}

Four fields do the work:

FieldWhat it tells you
scoringSupportedWhether this market can be graded at all
started / endedWhether the bet's period has begun and finished
cancelledWhether this specific market was voided
scoreThe actual result value, once available

No second request, no name matching, no separate results product.

Check scoringSupported before anything else

Not every market can be settled from the data. scoringSupported is a boolean on each odd, and it is the first thing your grader should read.

When it is false, there is no score coming and no amount of waiting will produce one. Treat that as a known state and route those bets to whatever you do for markets you cannot auto-settle — manual review, void, or excluding them from your product in the first place. What you must not do is leave them pending forever, which is what a grader that only looks at ended will do.

Reading it up front also tells you something useful at build time: if a market category you were planning a feature around comes back with scoringSupported: false, you have found that out before you shipped the feature.

ended is not finalized

This is the mistake that costs the most.

There are two levels of status. Each odd has started and ended, which describe the odd's own period. The event has a status object describing the whole game:

"status": {
  "started": true,
  "live": false,
  "ended": true,
  "completed": true,
  "cancelled": false,
  "delayed": false,
  "finalized": true,
  "currentPeriodID": "",
  "periods": { "started": ["1q","2q","3q","4q"], "ended": ["1q","2q","3q","4q"] }
}

ended means play has stopped. finalized means the result is locked. Those are not the same moment, and the window between them is where stats get corrected — a reception reassigned, a yardage adjustment, a review that moves a touchdown.

Grade a player prop on ended and you will settle some bets on a number that changes ten minutes later. Grade on status.finalized and you settle once, correctly.

The practical rule: use ended on the odd to know a bet is no longer live, and status.finalized on the event to know it is safe to pay out. If you show users provisional results, read the first and label it as provisional; if you move money, wait for the second.

A grader, roughly

def grade(odd, event):
    if odd.get("cancelled") or event["status"].get("cancelled"):
        return "void"
    if not odd.get("scoringSupported"):
        return "manual"
    if not odd.get("ended"):
        return "pending"
    if not event["status"].get("finalized"):
        return "provisional"

    score = odd.get("score")
    if score is None:
        return "pending"

    bet_type, side = odd["betTypeID"], odd["sideID"]

    if bet_type == "ou":
        line = float(odd["bookOverUnder"])
        if score == line:
            return "push"
        won = score > line if side == "over" else score < line
        return "won" if won else "lost"

    # sp and ml follow the same shape: compare score against the line
    # (or against the opposing side's score) and branch on sideID
    ...

Two things about that sketch. The order of the guards matters — cancelled before scoring support, scoring support before ended, ended before finalized. Reversing any pair produces a bet stuck in the wrong state.

And the push check needs the line you actually took, not the consensus. bookOverUnder is the consensus line; the number your user was given came from a specific book, so store the overUnder from that book's byBookmaker entry at bet time and grade against that. A user who took 224.5 and a user who took 225 on the same market do not both push on 225.

Period markets settle early

A first-half bet is done at half time, not at the final whistle. The event's status tracks this:

"periods": { "started": ["1h","2h"], "ended": ["1h"] },
"currentPeriodID": "2h"

Read periodID on the odd, then check whether that period appears in status.periods.ended. A 1h market whose period has ended is settleable while the game is still live — which matters if your product tells users where they stand mid-game rather than only afterwards.

The periods reference lists every periodID, and it carries a warning worth repeating: full-game points are always present by the time an event finalizes, but breakdown periods appear most of the time rather than always. Null-check whichever period you depend on.

The three-way trap

This one mis-settles quietly, and only on soccer.

A two-outcome grader assumes every market has a winner and a loser. Three-way moneylines do not. The ml3way bet type carries six sides:

sideIDWins whenOpposite side
homeHome winsaway+draw
awayAway winshome+draw
drawThe match is drawnnot_draw
home+drawHome wins or drawsaway
away+drawAway wins or drawshome
not_drawAny result except a drawdraw

Look at the right-hand column. The opposite of home is away+draw, not away. A grader that pairs home against away will settle both as losers on a drawn match, which is wrong twice over on the same fixture.

This is also why you should follow opposingOddID rather than constructing the opposite oddID yourself. You would not guess away+draw. The same pairing matters when you are pricing rather than grading, which we cover in why devigging one book isn't a fair price.

The Premier League page shows where three-way lines appear in soccer coverage. They sit on the reg period rather than game, because regulation-time result and final result are different propositions once extra time exists.

Cancelled, postponed, and the difference

cancelled appears in two places and means two things. On the event, the fixture is off. On an individual odd, that market was voided while the rest of the event stands — a player scratched from the lineup voids their props without touching the game lines.

Grade both. A grader that only checks event-level cancellation will leave a scratched player's props pending until someone notices.

Postponement is a separate problem, and a more annoying one: a rescheduled fixture arrives as a new event rather than a revived one, so a bet stored against the original event ID will never settle against the new game. We wrote that up separately in postponed, delayed and cancelled events, including how to spot the pairing.

Why this is one integration rather than two

The argument for settling from the same feed is not that it is cheaper, though it is. It is that there is no join to get wrong.

When odds and results come from separate vendors you are matching on strings — team names, player names, timestamps — and every one of those is a place where a mid-season trade or a different spelling breaks a settlement silently. Here the oddID that priced the bet is the oddID that carries the score. The key is the same on both sides because there is only one side.

Scores and settlement data ship on every plan, including the free tier, so you can build and test a full grading path before paying anything. Bet tracking and settlement covers the wider workflow, the events endpoint covers the request itself, and the odds data type reference lists every field used above.

Frequently asked questions

How do I know whether a market can be graded?
Read scoringSupported on the odd. When it is false there is no score coming, so route those bets to manual review or exclude them rather than leaving them pending. Checking it before anything else also tells you at build time whether a market category you are planning a feature around can be settled at all.
What is the difference between ended and finalized?
ended means play has stopped. finalized means the result is locked. Stats get corrected in the window between them — a reception reassigned, a yardage adjustment, a review. Use ended on the odd to know a bet is no longer live, and status.finalized on the event before you pay anything out.
Do I need a separate results or stats API to settle bets?
No. Scores and settlement state arrive on the same odd object that carried the price, keyed by the same oddID. That removes the reconciliation layer entirely — there is no matching on team names, player names or start times, because there is only one source.
Which line should I grade an over/under against?
The line your user actually took, not the consensus. bookOverUnder is the consensus number; store the overUnder from the specific book's byBookmaker entry at bet time and grade against that. A user who took 224.5 and a user who took 225 on the same market do not both push on 225.
How do I settle a first-half or quarter bet before the game ends?
Read periodID on the odd and check whether that period appears in the event's status.periods.ended array. A 1h market whose period has ended is settleable while the game is still live. Note that breakdown periods appear most of the time rather than always, so null-check whichever one you depend on.
Why do three-way soccer markets settle wrong?
Because the opposite of home on an ml3way market is away+draw, not away. A grader that pairs home against away settles both as losers on a drawn match. The bet type carries six sides — home, away, draw, home+draw, away+draw and not_draw — so follow opposingOddID rather than constructing the opposite oddID yourself.
What happens to bets on a postponed game?
A rescheduled fixture arrives as a new event rather than a revived one, so a bet stored against the original event ID will never settle against the new game. That needs handling explicitly rather than assumed, and it is a different case from cancellation.
Can a single market be voided without the whole event?
Yes. cancelled appears on the event and on each individual odd. A player scratched from the lineup voids their props while the game lines stand, so a grader that only checks event-level cancellation will leave those props pending until someone notices.

Price it and settle it from one feed

Odds, scores and settlement state on the same oddID, on every plan including the free tier.