Docs v2 · testnet

Markets · DistributionMarket

Range markets: trade where a price lands

A yes/no market asks “will NVDA be above $230?”. A range market asks “where will NVDA be?” — it splits the answer into up to 10 price ranges, each with its own shares. An automated market maker (LMSR) keeps the range prices summing to 1, so the chart is the market's probability distribution. It settles on Novak's dispute layer, unchanged.

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

13%
21%
13%
27%
13%
13%
< $230$230–232$232–234$234–236$236–238≥ $238
NVDA ≥ $230 at T? TRUE
NVDA ≥ $232 at T? TRUE
NVDA ≥ $234 at T? TRUE
NVDA ≥ $236 at T? FALSE
NVDA ≥ $238 at T? FALSE

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)

Range “< $230” trades at 13%. Spending 10 USDG on it returns ≈ 71.48 shares (0.10 USDG fee) — worth 71.48 USDG if NVDA closes below $230, about 7× the stake. Average price ≈ 0.139: a little above 0.13 because buying pushes the price up.

Choosing b

Larger b = deeper market (smaller price moves per trade) but a bigger subsidy for the creator.

b (USDG)

100

Subsidy, 6 ranges

≈ 179 USDG

Subsidy, 10 ranges

≈ 230 USDG

Feel

small, moves fast

b (USDG)

500 (demo markets)

Subsidy, 6 ranges

≈ 896 USDG

Subsidy, 10 ranges

≈ 1,151 USDG

Feel

balanced

b (USDG)

1,000

Subsidy, 6 ranges

≈ 1,792 USDG

Subsidy, 10 ranges

≈ 2,303 USDG

Feel

deep

Lifecycle

  1. 1

    Create

    creator
    Create the boundary ladder in one transaction (createEvents), approve USDG, then createMarket(question, boundaryIds, tradingClosesAt, b). The contract pulls ⌈b·ln N⌉ + 1 units and every range starts at 1/N.
  2. 2

    Trade

    anyone
    Buy and sell any range. Every trade emits Trade(…, pricesAfter), which the app uses for the probability history.
  3. 3

    Trading stops

    automatic
    At tradingClosesAt — 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. 4

    Settle

    keeper
    settle(market) reads every boundary in one call through Settlement.getSettlements. Fees go to the TreasuryVault tagged with the boundary events; leftover subsidy goes back to the creator.
  5. 5

    Redeem

    traders
    redeem(market) pays each wallet once: 1 USDG per winning share, or 1/N per share if voided.

Settlement rules

Boundaries at settle

Any still Pending

Result

settle() reverts — try again later

Each share pays

—

Boundaries at settle

All Available, TRUEs form a prefix (T,T,T,F,F)

Result

Settled: winning range = number of TRUEs

Each share pays

1 USDG if in the winning range, else 0

Boundaries at settle

Any Voided or Expired

Result

Voided

Each share pays

1/N USDG, whatever range it is in

Boundaries at settle

Inconsistent — a TRUE above a FALSE (T,F,T,…)

Result

Voided

Each share pays

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

> +5 points

Label

Underpriced — consider buying

Meaning

the model thinks this range is more likely than the price says

Model − market

within ±5 points

Label

Fairly priced — hold

Meaning

no meaningful gap

Model − market

< −5 points

Label

Overpriced — consider selling / avoid

Meaning

the market is paying more than the model thinks it's worth
Informational only, not financial advice. The model ignores news, earnings and everything else traders know — that's exactly the information a market adds.

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:

create-range-market.ts
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

createMarket(question, boundaryIds[], tradingClosesAt, b)

Kind

write

Notes

1–9 ascending boundaries, all Pending; pulls the subsidy

Function

buy(id, range, usdgIn, minShares)

Kind

write

Notes

fee = 1% of usdgIn

Function

sell(id, range, shares, minUsdgOut)

Kind

write

Notes

fee = 1% of proceeds

Function

settle(id)

Kind

write

Notes

anyone, once every boundary is decided

Function

redeem(id)

Kind

write

Notes

once per wallet

Function

prices(id)

Kind

view

Notes

wad, sums to 1e18

Function

quoteBuy(id, range, usdgIn) · quoteSell(id, range, shares)

Kind

view

Notes

exact contract math

Function

sharesOf(id, who) · payoutOf(id, who)

Kind

view

Notes

position and redeemable amount

Function

getMarket · getBoundaries · getOutstanding · marketCount · getMarketIds

Kind

view

Notes

enumeration

Like every consumer, DistributionMarket reads outcomes only through Settlement → IEventBus — enforced by test_holdsOnlySettlementReference.