Toolkit v7 & SDK v8: Per-Outcome Resolution
We’re releasing Toolkit v7.0.0 and SDK v8.0.0. Both are major, breaking updates.
The protocol has dropped condition-level resolution. A Condition
is now only ever Active or Stopped, and won / lost / voided is tracked purely per outcome.
Within a single condition, outcomes can now settle independently: one Won, one Lost, one Canceled (voided, stake refunded) and one still taking bets — all at the same time. The old model could not express that.
Combo pricing was the sharpest edge of it: a voided leg was still credited into the combo’s odds, so a bet
containing one reported a possibleWin the protocol would never pay. A first fix landed in
Toolkit v6.5 / SDK v7.6; this release finishes it, because the
same figures were wrong for combos with no voided leg too — see
Combo pricing. What else this release changes is the model itself, and
with it everything that read resolution off a condition.
Several SDK changes produce no compile error. useActiveConditions / useActiveMarkets now drop
hidden conditions and outcomes by default while useConditions returns everything, a hidden market is
revealed only by an update that reports it visible, per-outcome state is no longer taken from every
socket message, a voided combo leg reports isWin: false instead of isWin: null, every
unredeemed combo reports different odds and payout figures, and useBetsSummaryBySelection returns a
differently keyed map. A green type check does not mean the migration is done — see the
SDK v8 Migration Guide.
What’s New
Toolkit v7.0.0:
ConditionStateis now exactly{ Active, Stopped }—Canceled,RemovedandResolvedare removedMarketOutcome.isWonis removed with no alias — useoutcome.state === OutcomeState.Won. It was derived from condition-levelwonOutcomeIdsand wasundefinedwhenever those were absent, so!isWonsilently rendered every outcome as lost- New
isOutcomeSettled(state)—trueforWon | Lost | Canceled,falseforActive | Stopped - New
calcComboOdds({ odds, createdAt })— a combo’s total odds, priced the way the protocol prices them, for the rules in force when the bet was placed. This is now the one place that decision lives; use it instead of multiplying leg odds or callingcalcMinOddsfor a bet that already exists - New
MARGIN_APPLIED_AT— unix seconds, when the feed started applying its fee to every outcome’s odds. Exported because reconstructing any historical combo figure needs it BetsReportEntrygained a requiredcreatedAt. Only affects code that builds entries itself and callscalcBetsReportdirectly —getBetsReportfills it in. It is required rather than optional because no default is safe: defaulting either way silently mis-prices one era of betsOutcomeResult(Won | Lost | Canceled) is now exported from the package rootBetFragment/BetsQuery:selections[].outcomenow includesresult?: OutcomeResult | null— the authoritative per-outcome settlement signal for a betBetMetaData.selections[].outcome.condition.outcomes[].resultis retypedSelectionResult→OutcomeResult, since the old type had no way to express a void- Unchanged:
OutcomeState,GameState, andConditionStatus/BetConditionStatus— the last of which is a different enum from the bets subgraph (Created | Resolved | Canceled | Paused), not the feed’sConditionState. Condition-levelwonOutcomeIdsis still served by the feed
SDK v8.0.0:
- Removed
useResolvedMarkets— useuseActiveMarketswithincludeHidden: true - Removed
BetOutcome.wonOutcomeIds - Removed
UseConditionsProps.onlyActiveOrStopped— with the reducedConditionState, its predicate matched every condition - New
includeHiddenprop onuseActiveConditionsanduseActiveMarkets, defaulting tofalse— those two hooks now drophiddenconditions and outcomes, and any condition left without a visible one, unless you opt in useConditionsis a pure data hook: it filters nothing and returns everything the feed has. Visibility can’t be decided in a fetch — the feed sends no updates for a condition that was never subscribed, so a row dropped there could never be revealed afterwards. The two “active” hooks subscribe to every condition of the game first and filter second. Theconditionsquery key lostonlyActiveOrStoppedand is now[ 'conditions', chainId, gameId, extended ], so one cache entry serves every view of a game — and entries cached by v7 are invalidateduseActiveConditions/useActiveMarketsreturnUseActiveConditionsResult/UseActiveMarketsResultinstead of a plainUseQueryResult, no longer acceptquery.select(it would run before the filter), and requireFeedSocketProvider+ConditionUpdatesProvider— both already insideAzuroSDKProvider, so only a hand-composed provider tree is affected, and it fails at runtime rather than at compile time- A hidden condition is revealed by an update reporting
hidden: false, not by any update arriving: a market the provider has parked keeps streaming odds for the rest of its life, so “it sent something” is no evidence it is being offered. Visibility is latched one way at both levels — a market that stops stays in the grid, locked, instead of reflowing the list under the bettor — andhiddenis nowboolean | undefined, whereundefinedmeans “not reported yet” rather than “revealed” useOutcomeState/useOutcomesStatetakestateandhiddenonly from updates whose condition isActive, and re-read the state endpoint otherwise: an update for an inactive condition reports every one of its outcomes asStoppedregardless of what they settled to.oddsandturnoverare real in every message and still come from all of them. Outcome visibility is latched by the SDK now, so an app keeping its own anti-flicker latch can drop ituseBets: a voided combo leg is now{ isWin: false, isLose: false, isCanceled: true }— it used to be{ isWin: null, isLose: null, isCanceled: false }, indistinguishable from still-pendinguseBets:totalOdds,possibleWin,payoutandsettledPayoutchange value for every unredeemed combo — see Combo pricinguseConditionState/useConditionsStatereturnConditionState.Stoppedinstead of the removedConditionState.Removedwhen a condition is absent from the feeduseOutcomeState/useOutcomesStatereturnOutcomeState.Stoppedinstead ofOutcomeState.Canceledwhen an outcome is absent from the feed —OutcomeState.Cancelednow strictly means voided, with money attacheduseActiveMarketno longer selects a market whose condition isActivebut all of whose outcomes are settleduseBetsSummaryBySelectionkeys its map by`${conditionId}-${outcomeId}`instead ofoutcomeId. An outcome is identified by its condition and itsoutcomeId:v5markets number their outcomes per condition from1up, so the same low ids recur in everyv5condition and collide with the idslegacymarkets take from the shared dictionaries namespace, while a re-offeredlegacymarket repeats its outcomes under a secondconditionId. Keyed byoutcomeIdalone, those selections shared one entry and each read back the sum. The map staysRecord<string, string>, so an unmigrated lookup compiles, misses, and renders nothing; this is the same keyuseOutcomesStateanduseOddsalready use
Combo pricing changes values
The feed applies its fee to every outcome’s odds. A combo is therefore priced by removing that fee from each leg, multiplying, and applying it once to the product:
ceil(1.5 / 0.99) * ceil(2 / 0.99) * 0.99 = 3.05
1.5 * 2 = 3.00 ← the plain productThe subgraph records the plain product — for a combo’s odds, settledOdds, potentialPayout and, until
the bet is redeemed, its payout. So all of those compound the fee once per leg and understate a combo
by more the more legs it has. They are also never reduced when one of its legs is voided.
From this release, useBets and getBetsReport price every unredeemed combo through
calcComboOdds. Two consequences to plan for:
totalOdds,possibleWin,payoutandsettledPayoutgo up for unredeemed combos. Anything that snapshots, caches or reconciles these numbers will see a diff. The new figures are the correct ones.- This is wider than the v6.5 / v7.6 fix, which only rebuilt a combo that had a voided leg. The compounding applies to all of them.
Three shapes are deliberately unchanged:
| Shape | Why |
|---|---|
| A redeemed bet | Its recorded payout is the amount actually paid on chain. Truth beats any reconstruction. |
A combo placed before MARGIN_APPLIED_AT | Its leg odds are raw, so the plain product already is the right price. Re-pricing it would overstate the payout by roughly (1/0.99)^(legs-1). |
| A cashed-out bet | It was paid at the price the bettor took, so its odds say nothing about what it returned. Read cashout. |
Singles are unaffected in either era: one leg means the fee is applied once already.
If you reconstruct payouts yourself, you need the bet’s creation time. The fee has a start date, so
leg odds alone are not enough to price a combo — the same legs price differently either side of
MARGIN_APPLIED_AT. Pass the real createdAt; do not default it.
The pattern to build against
Don’t gate the whole market grid on game state. Render one grid where each outcome shows its own state — Won, Lost, Refunded, or still bettable — since a game can have settled and live markets on screen at once.
The one thing that should still depend on GameState.Finished is includeHidden: while a game is active,
hidden conditions and outcomes stay hidden; once the game is finished, they are shown.
import { useActiveMarkets, useGameState } from '@azuro-org/sdk'
import { GameState, isOutcomeSettled, OutcomeState } from '@azuro-org/toolkit'
const { data: gameState } = useGameState({ gameId, initialState: game.state })
const { data: markets } = useActiveMarkets({
gameId,
includeHidden: gameState === GameState.Finished,
})
// then, per outcome:
isOutcomeSettled(outcome.state) // settled? Won | Lost | Canceled
outcome.state === OutcomeState.Won // won
outcome.state === OutcomeState.Canceled // voided - stake refundedMigration Guides
To ensure a smooth transition, please follow our migration guides:
Both cover all breaking changes and include a grep-your-call-sites section, since several of these changes are behavioural and won’t be caught by the type checker.
Questions?
If you encounter any issues during migration or have questions, reach out to us on Telegram.