Markets · DistributionMarket
Range markets: trade where a price lands
Ranges
2–10
1–9 boundary events (MAX_BOUNDARIES = 9)
Pays
1 USDG
per share of the winning range
Trade fee
1%
→ TreasuryVault thirds
Creator subsidy
b · ln N
LMSR's maximum loss, b ≥ 1 USDG
From threshold events to ranges
A range market with N ranges is built from N−1 ordinary yes/no Novak events — the boundaries — with ascending thresholds: boundary i is “price ≥ thresholdi at time T?”. The winning range is simply the number of boundaries that resolved TRUE. Each boundary goes through the normal resolver quorum and, if challenged, the committees — and any other market can reuse the same boundary events.
Example: “Where will NVDA be at the bell?” — 5 boundary events, 6 ranges
NVDA closes at $235.19 → 3 boundaries TRUE → range #3 ($234–236) wins and pays 1 USDG per share. Bar heights are the live prices: they always sum to 100%.
Pricing: the LMSR market maker
Let q_i be the shares outstanding in range i and b the liquidity parameter. The market maker charges according to the cost function
C(q) = b · ln( Σj eq_j / b )
pricei = eq_i / b / Σj eq_j / b (all prices sum to 1)
cost of Δ shares of i = C(q + Δ·ei) − C(q)
Buy
buy(market, range, usdgIn, minShares). 1% of usdgIn is the fee; the rest buys shares, solved in closed form so C(q + Δ) = C(q) + net. Shares round down. Reverts below minShares (slippage guard — the app uses 2%).Sell
sell(market, range, shares, minUsdgOut) pays C(q) − C(q − Δ) minus 1%. Proceeds round down. You can exit any time before close — the price just reflects what others now believe.Live example (NVDA market on testnet)
Choosing b
Larger b = deeper market (smaller price moves per trade) but a bigger subsidy for the creator.
b (USDG)
Subsidy, 6 ranges
Subsidy, 10 ranges
Feel
b (USDG)
Subsidy, 6 ranges
Subsidy, 10 ranges
Feel
b (USDG)
Subsidy, 6 ranges
Subsidy, 10 ranges
Feel
| b (USDG) | Subsidy, 6 ranges | Subsidy, 10 ranges | Feel |
|---|---|---|---|
| 100 | ≈ 179 USDG | ≈ 230 USDG | small, moves fast |
| 500 (demo markets) | ≈ 896 USDG | ≈ 1,151 USDG | balanced |
| 1,000 | ≈ 1,792 USDG | ≈ 2,303 USDG | deep |
Lifecycle
- 1
Create
creatorCreate the boundary ladder in one transaction (createEvents), approve USDG, thencreateMarket(question, boundaryIds, tradingClosesAt, b). The contract pulls⌈b·ln N⌉ + 1units and every range starts at 1/N. - 2
Trade
anyoneBuy and sell any range. Every trade emits Trade(…, pricesAfter), which the app uses for the probability history. - 3
Trading stops
automaticAttradingClosesAt— and also the moment any boundary is decided, so nobody can trade on a known outcome (the same fix as the yes/no market's fund-loss bug). - 4
Settle
keepersettle(market)reads every boundary in one call throughSettlement.getSettlements. Fees go to the TreasuryVault tagged with the boundary events; leftover subsidy goes back to the creator. - 5
Redeem
tradersredeem(market)pays each wallet once: 1 USDG per winning share, or 1/N per share if voided.
Settlement rules
Boundaries at settle
Result
Each share pays
Boundaries at settle
Result
Each share pays
Boundaries at settle
Result
Each share pays
Boundaries at settle
Result
Each share pays
| Boundaries at settle | Result | Each share pays |
|---|---|---|
| Any still Pending | settle() reverts — try again later | — |
| All Available, TRUEs form a prefix (T,T,T,F,F) | Settled: winning range = number of TRUEs | 1 USDG if in the winning range, else 0 |
| Any Voided or Expired | Voided | 1/N USDG, whatever range it is in |
| Inconsistent — a TRUE above a FALSE (T,F,T,…) | Voided | 1/N USDG |
Why it can always pay
The market keeps a reserve: the creator's subsidy plus every net trade (fees are held separately). The subsidy is exactly C(0) = b·ln N, so after any trades the reserve equals C(q). For the LMSR, C(q) ≥ max q_i ≥ average q_i — enough to pay the winning range in full, or 1/N on every share if voided. The contract doesn't rely on the proof alone: every buy and sell re-checks reserve ≥ C(q) and reverts otherwise, and rounding always favours the market. Foundry fuzz tests run random trade sequences against every outcome.
Insights: buy, hold or avoid
Each market page compares the market's price for every range with a simple model: a lognormal distribution centred on the live Chainlink spot price, using that feed's own realized volatility and the time left until T.
Model − market
Label
Meaning
Model − market
Label
Meaning
Model − market
Label
Meaning
| Model − market | Label | Meaning |
|---|---|---|
| > +5 points | Underpriced — consider buying | the model thinks this range is more likely than the price says |
| within ±5 points | Fairly priced — hold | no meaningful gap |
| < −5 points | Overpriced — consider selling / avoid | the market is paying more than the model thinks it's worth |
Create one
In the app: Markets → Create market → Price range — pick any of the 34 Chainlink stock/SGOV feeds, a time T and a liquidity; the ranges are centred on the live price. Or with the SDK:
import { NovakClient, buildPriceLadderSpecs, ladderBucketLabels, feedBySymbol, getDeployment } from "@novakoracle/sdk";
const dep = getDeployment(46630); // Robinhood Chain testnet addresses
const client = new NovakClient(publicClient, walletClient, dep);
const nvda = feedBySymbol("NVDA")!; // Chainlink feed on Robinhood Chain mainnet
const at = BigInt(Math.floor(Date.now() / 1000) + 48 * 3600);
const thresholds = [230n, 232n, 234n, 236n, 238n].map((d) => d * 10n ** 8n); // 8 decimals
// 1. Five boundary events "NVDA >= $X at T", one transaction
const specs = buildPriceLadderSpecs({ feed: nvda.address, thresholds, at, maxStaleness: 4n * 3600n });
const tx = await client.createEvents(specs, account);
const boundaryIds = await client.getCreatedEventIds(tx);
// 2. Fund the LMSR subsidy and open the market
const b = 500_000_000n; // 500 USDG (6 decimals)
const approve = await client.approveCollateralFor(dep.distributionMarket, client.distributionSubsidy(b, 6), account);
await publicClient.waitForTransactionReceipt({ hash: approve });
const mtx = await client.createDistributionMarket("Where will NVDA be at the bell?", boundaryIds, at - 60n, b, account);
const marketId = await client.getCreatedDistributionMarketId(mtx);
console.log(ladderBucketLabels(thresholds)); // ["< $230", "$230–$232", …, "≥ $238"]Contract reference
Function
Kind
Notes
Function
Kind
Notes
Function
Kind
Notes
Function
Kind
Notes
Function
Kind
Notes
Function
Kind
Notes
Function
Kind
Notes
Function
Kind
Notes
Function
Kind
Notes
| Function | Kind | Notes |
|---|---|---|
| createMarket(question, boundaryIds[], tradingClosesAt, b) | write | 1–9 ascending boundaries, all Pending; pulls the subsidy |
| buy(id, range, usdgIn, minShares) | write | fee = 1% of usdgIn |
| sell(id, range, shares, minUsdgOut) | write | fee = 1% of proceeds |
| settle(id) | write | anyone, once every boundary is decided |
| redeem(id) | write | once per wallet |
| prices(id) | view | wad, sums to 1e18 |
| quoteBuy(id, range, usdgIn) · quoteSell(id, range, shares) | view | exact contract math |
| sharesOf(id, who) · payoutOf(id, who) | view | position and redeemable amount |
| getMarket · getBoundaries · getOutstanding · marketCount · getMarketIds | view | enumeration |
Like every consumer, DistributionMarket reads outcomes only through Settlement → IEventBus — enforced by test_holdsOnlySettlementReference.