Skip to Content
Developer Hub📦 Releases08/22/26 Toolkit v7 & SDK v8

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:

  • ConditionState is now exactly { Active, Stopped }Canceled, Removed and Resolved are removed
  • MarketOutcome.isWon is removed with no alias — use outcome.state === OutcomeState.Won. It was derived from condition-level wonOutcomeIds and was undefined whenever those were absent, so !isWon silently rendered every outcome as lost
  • New isOutcomeSettled(state)true for Won | Lost | Canceled, false for Active | 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 calling calcMinOdds for 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
  • BetsReportEntry gained a required createdAt. Only affects code that builds entries itself and calls calcBetsReport directly — getBetsReport fills it in. It is required rather than optional because no default is safe: defaulting either way silently mis-prices one era of bets
  • OutcomeResult (Won | Lost | Canceled) is now exported from the package root
  • BetFragment / BetsQuery: selections[].outcome now includes result?: OutcomeResult | null — the authoritative per-outcome settlement signal for a bet
  • BetMetaData.selections[].outcome.condition.outcomes[].result is retyped SelectionResultOutcomeResult, since the old type had no way to express a void
  • Unchanged: OutcomeState, GameState, and ConditionStatus / BetConditionStatus — the last of which is a different enum from the bets subgraph (Created | Resolved | Canceled | Paused), not the feed’s ConditionState. Condition-level wonOutcomeIds is still served by the feed

SDK v8.0.0:

  • Removed useResolvedMarkets — use useActiveMarkets with includeHidden: true
  • Removed BetOutcome.wonOutcomeIds
  • Removed UseConditionsProps.onlyActiveOrStopped — with the reduced ConditionState, its predicate matched every condition
  • New includeHidden prop on useActiveConditions and useActiveMarkets, defaulting to false — those two hooks now drop hidden conditions and outcomes, and any condition left without a visible one, unless you opt in
  • useConditions is 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. The conditions query key lost onlyActiveOrStopped and is now [ 'conditions', chainId, gameId, extended ], so one cache entry serves every view of a game — and entries cached by v7 are invalidated
  • useActiveConditions / useActiveMarkets return UseActiveConditionsResult / UseActiveMarketsResult instead of a plain UseQueryResult, no longer accept query.select (it would run before the filter), and require FeedSocketProvider + ConditionUpdatesProvider — both already inside AzuroSDKProvider, 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 — and hidden is now boolean | undefined, where undefined means “not reported yet” rather than “revealed”
  • useOutcomeState / useOutcomesState take state and hidden only from updates whose condition is Active, and re-read the state endpoint otherwise: an update for an inactive condition reports every one of its outcomes as Stopped regardless of what they settled to. odds and turnover are 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 it
  • useBets: 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-pending
  • useBets: totalOdds, possibleWin, payout and settledPayout change value for every unredeemed combo — see Combo pricing
  • useConditionState / useConditionsState return ConditionState.Stopped instead of the removed ConditionState.Removed when a condition is absent from the feed
  • useOutcomeState / useOutcomesState return OutcomeState.Stopped instead of OutcomeState.Canceled when an outcome is absent from the feed — OutcomeState.Canceled now strictly means voided, with money attached
  • useActiveMarket no longer selects a market whose condition is Active but all of whose outcomes are settled
  • useBetsSummaryBySelection keys its map by `${conditionId}-${outcomeId}` instead of outcomeId. An outcome is identified by its condition and its outcomeId: v5 markets number their outcomes per condition from 1 up, so the same low ids recur in every v5 condition and collide with the ids legacy markets take from the shared dictionaries namespace, while a re-offered legacy market repeats its outcomes under a second conditionId. Keyed by outcomeId alone, those selections shared one entry and each read back the sum. The map stays Record<string, string>, so an unmigrated lookup compiles, misses, and renders nothing; this is the same key useOutcomesState and useOdds already 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 product

The 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, payout and settledPayout go 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:

ShapeWhy
A redeemed betIts recorded payout is the amount actually paid on chain. Truth beats any reconstruction.
A combo placed before MARGIN_APPLIED_ATIts 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 betIt 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 refunded

Migration 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.