Before comparing two bookmakers' prices, establish that both observations describe the same market and an acceptable state of that market. A larger decimal number is a poor signal when it belongs to a different line, includes extra time, or was retained after suspension.
This article is for developers building odds archives, comparison displays and data-quality alerts from feeds they are authorized to use. The practical output is a comparison contract: a normalized record, explicit rejection reasons and a small offline checker. It validates your supplied evidence; it does not establish that an offer is currently executable.
Define the market before comparing the number
Consider two fictional records: Over 2.5 goals at 1.91 and Over 3.5 goals at 2.05. Treating the latter as an improvement compares two different outcomes. The numbers can both be valid while the comparison is invalid.
Use an explicit identity key. The following is a proposed internal model, not a claim that every provider returns these field names:
| Field | What to establish | Example mistake it prevents |
|---|---|---|
| Event namespace and ID | The same event in one provider namespace, or a reviewed cross-provider mapping | Joining two fixtures just because their team names match |
| Period | First half, regulation time or another defined period | Mixing a half-time market with a full-match market |
| Market and line | Exact proposition and threshold, with a consistent unit | Mixing totals 2.5 and 3.5 |
| Selection set | The same complete set of outcome identifiers | Treating a missing outcome as a price disappearance |
| Settlement rules | A reviewed equivalence group for the rules that affect the result | Mixing regulation-only and extra-time-inclusive outcomes |
| Event phase | A known pre-match or in-play state | Comparing a retained pre-match snapshot with an in-play observation |
A matching in-play flag does not establish equal score, match clock or game state. This example does not validate those fields; a workflow that requires synchronized game state needs additional evidence and gates.
Preserve the provider and bookmaker identifiers alongside that key. Identical raw IDs from different namespaces are not a cross-provider mapping. A settlement_rules label is only as reliable as the rule comparison behind it; copying the same label into both rows cannot establish equivalence.
These distinctions map to real feed structures. The Odds API v4 documents bookmaker and market objects, outcome prices, and point values for spread and totals outcomes. Its event-odds response also carries market-level update timestamps. Normalize the endpoint you actually use rather than copying a sample from a different operation. The Odds API v4
Rebuild state before judging completeness
A stream update is not necessarily a complete snapshot. Sportradar's Unified Odds Feed documentation says an odds_change can cover only some markets; markets omitted from that message remain unchanged. Clearing your entire market cache on each message would manufacture disappearing prices. Sportradar odds-change semantics
Separate two jobs:
- A provider adapter applies the documented snapshot, delta, recovery and status rules to a cache.
- A comparison gate evaluates the resulting normalized state.
The checker linked below performs the second job. It must receive a complete normalized observation. Setting complete_snapshot: true on a raw delta bypasses the very question the adapter must answer.
For a totals example, require both OVER and UNDER in the normalized selection set. A different market needs its own completeness definition. Preserve explicit suspension, closure and unknown state; do not fill a missing status with OPEN unless the source contract actually establishes that state. Some protocols define defaults, but a default from one protocol is not a rule for another.
When a connection is lost, retain the last observation for your archive and mark it ineligible for current comparison until the adapter establishes usable state again. Historical visibility and current eligibility are separate properties.
Keep collection time separate from price time
Record when your collector received the observation and what the source timestamp means. A fresh HTTP response may contain an old price, while an unchanged price may remain valid for a long time. An age threshold is an admission policy, not proof that a price is wrong.
Do not rewrite a price timestamp when the connection emits a heartbeat. Betfair distinguishes heartbeat messages from market changes; its stream pt is a message publish time. Connection activity alone does not establish that every cached selection was refreshed. Betfair Exchange Stream API
For a monitoring job, define and record these limits before admitting a pair:
- Maximum source age at the declared evaluation time.
- Maximum difference between the two collection timestamps.
- How missing, ambiguous, future or timezone-free timestamps are handled.
- Which source field supports the normalized price-update time.
Our fictional fixture policy uses a 30-second age limit and a two-second collection skew. Those values make the example reproducible; they are not bookmaker requirements or suitable defaults for every live feed. A production policy must fit the feed's documented timestamp semantics and the reader's decision deadline. A market-level update timestamp must remain identified as market-level evidence; it does not establish each outcome's last price change.
Evaluate stored records against the intended historical evaluation time when replaying an archive. Comparing yesterday's fixtures with your current wall clock answers a different question.
Use rejection reasons to locate the problem
These are illustrative diagnostic branches, not measured bookmaker failure rates:
| Observation | Comparison decision | Investigate next |
|---|---|---|
| Same contract, complete open state, valid prices and acceptable timestamps | Eligible for this comparison | Compare the observations while preserving their timestamps |
| Same label, different line or settlement rules | Reject this pairing | Correct the market mapping |
| One side suspended or closed | Hold current comparison | Process the status transition; preserve history |
| Outcome missing from a supposedly complete snapshot | Hold | Check adapter completeness and recovery |
| Old price time with a recent collection time | Hold if outside your declared age policy | Inspect upstream update semantics and cache state |
| Healthy heartbeat but no established usable market state | Hold | Diagnose stream state and recovery |
| Decimal price missing, non-finite or at most one | Reject the normalized input | Check parsing and odds-format conversion |
The last branch matters when ingesting multiple odds formats. Convert using a defined adapter and validate the resulting decimal representation; do not silently treat an American-odds integer as decimal odds. The Odds API exposes an explicit odds-format choice. Odds API format options
Run the offline comparison gate
Save the Python checker and fictional input cases in the same directory, then run:
python3 odds_compare.py fixtures.jsonThe supplied checker deliberately accepts only its fixture contract: regulation-time total goals at 2.5 with the documented fictional rule ID. Other markets require a reviewed extension of the contract and tests, not just replacement prices. Keep bookmaker/source provenance in your surrounding observation log.
In the executed local fixture, one of 14 pairs was eligible and 13 were held; 21 test methods passed. These are synthetic validator results, not bookmaker success or failure rates.
The input file declares a fixed evaluation time under now and the illustrative age/skew policy, so replay does not depend on when you run it. Each case contains two candidate normalized records, including deliberately incomplete or invalid records that should be held. The example contract is regulation-time total goals at 2.5, with OVER and UNDER selections and a fictional settlement-rule ID.
The checker returns eligible_for_comparison plus reasons for held pairs. Eligibility permits a data comparison under the supplied contract; it says nothing about staking, returns, liquidity or execution. Equal prices are not required.
The method and schema explain how to supply your own normalized records. The tests exercise valid comparisons and malformed or ambiguous evidence. The checker makes no network requests and includes no API adapter. Its timestamp field, price_updated_at, is supplied by your adapter; the checker cannot prove that the upstream field has the meaning you assigned to it.
Turn the contract into an operational check
An HTTP success counter belongs in transport monitoring. Keep a separate denominator for comparisons admitted by the data gate. Record attempted pairs, admitted pairs and rejection reasons, including observations you could not normalize. Otherwise, an apparently clean chart can simply be hiding the difficult inputs.
Store the mapping version, source identifiers, collection time, source timestamps, status and reasons with each decision. When an alert looks wrong, that record lets you distinguish a market-mapping defect from a stale cache or a transport failure. Do not replace unknown state with zero odds or erase the previous valid observation.
A proxy can change the transport route used by an authorized collector. It cannot establish market equivalence, fill in missing selections or prove a price is current. If the evidence is incomplete, fix the data contract before increasing request volume. For network-layer diagnosis, see proxy environment variables and timeout troubleshooting.
Method: this is an AI-assisted synthesis of the linked provider documentation and an explicitly synthetic local validation exercise. It reports no live bookmaker collection, provider performance, betting outcome or ipvolt service trial.
Join the ipvolt waitlist. Service access is not open yet. One email when access opens. Nothing else.