Module 08: Prediction Markets
Prerequisites
- Wallet funded with the market's pair asset (→01), client initialized (→02)
- Understand Stable+ mechanics: Predict+ tokens are a Stable+ subtype
- Optional: hold the resolver's bond token if you want to propose or dispute outcomes (Module 14)
Next steps after a market action
- Bet on an outcome? Track shares with
getUserShares(); redeem after resolution - Bought a Predict+ token? Borrow against it (→05) and deploy elsewhere
- Created a market? Build the full creator stack via Module 13; compare to token creation
- Want to propose or dispute a result? See the deep dive in Module 14
Examples use the default main, STASIS, whose pair asset is USDG (6 decimals). On another main read "USDG" as that main's pair asset and use its
decimals(). The mains of a deployment are listed in/contracts.json(mains[]).
1. Why Prediction Markets on BASIS
BASIS prediction markets are structurally different from platforms like Polymarket. Three differences define the edge:
Payouts are not capped at $1. On Polymarket, winning shares always pay exactly $1: volume affects liquidity, not your return. On BASIS, settlement is pari-mutuel. A market resolves to a ranking of outcomes, and getTrancheBP(market) gives the share of the profit for each rank: [10000] is winner-take-all, [7000, 2000, 1000] pays the top three. A holder of a ranked outcome gets its cost back plus that rank's share of the profit (the losers' money and the seed), pro rata to shares. On a winner-take-all market a winning share gets its cost back plus its part of everything the losers paid, so a share bought at 5c can pay far more than $1 if the pot is large enough.
Instant AMM liquidity. Traditional platforms need a counterparty for every trade. BASIS uses a one-directional AMM: buys go through the AMM, sells go through the order book. Because the AMM only handles buys, no one has to deposit liquidity for it. Every market has instant fills from creation, including niche topics and off-peak hours.
Multiple independent income streams. Traditional platforms give you one role: bettor. BASIS opens at minimum three independent profit paths from a single market simultaneously: see Module 13 for full role breakdowns.
The Two Assets Every Market Creates
Every prediction market creates two completely separate assets:
| Asset | What It Is | How It Profits |
|---|---|---|
| Predict+ token | A Stable+ subtype. Market token. Price can only go up (in pair-asset units), driven by trading volume. | Volume play: can gain from volume whichever outcome wins, before fees and slippage. Trade on the DEX like any token. |
| Outcome shares | What you buy to bet on a specific result. One set per outcome. | Conviction play: stake back plus a share of the pot by rank, on resolution. |
These are not the same thing. Buying the Predict+ token is not a bet. Buying outcome shares is not token trading. They have different mechanics, different flows, and different risk profiles.
Who Benefits
| Role | How | What You Need |
|---|---|---|
| Creator | The creator fee you set on the market token (0 to 1%, 0 until set), on every bet and token trade, plus 20% of the base part of loan-side fees on it | A compelling question |
| Bettor | A share of the pot by the market's tranche table, not capped per share | Conviction on an outcome |
| Trader | Predict+ token appreciates from market volume | Read on activity levels |
| Proposer / disputer | No bounty. An uncontested proposer gets the bond back; in a dispute the side that is right also takes the loser's bond, up to twice its own | Accurate information and the resolver's bond token |
| Leveraged player | Collateralise Predict+ tokens → borrow the pair asset → buy outcome shares | Any of the above |
2. How It Works
Market Structure
- A market has a question, 2 to
MAX_OUTCOMES()outcomes (read it live from the host: 150 by default, 255 is the hard ceiling; creative market designs in Module 13), a tranche ladder (trancheBP: 1 to 10 entries, none above the outcome count, each above 0, non-increasing, summing to 10000;[10000]is winner-take-all), an end time (0 = open-ended, or at least 60 seconds ahead; it is part of the moderator approval, and the approval step refuses end times too far out), and a seed in the market's pair asset. - Sizes are set per main. Each main has its own unit of the pair asset. The seed floor is the host's
minSeed()× unit on the public host, andminSeedPublic()× unit (a creator-resolved market open to all) orminSeedPrivate()× unit (an invite-only market) on the private host; the seed is a multiple ofseedStep(10 units). The minimum bet and the market's starting depth scale the same way. Read them in one call:TrancheReader.sizesFor(host, main, n)→(unit, seedStep, minBet, perOutcome, totalPool). For example, where one unit is 1 USDG the public seed floor is 50 USDG. The seed buys the creator equal shares on every outcome: it is the creator's own stake, not pool liquidity. - At creation, every outcome can take bets at once: the creator deposits no liquidity for it. The pot grows as users buy into outcomes.
- The fee on a bet is
getTaxRate(marketToken, bettor): the 0.60% base (0.54% with an on-chain referrer) plus the creator fee the market's creator set. Market tokens carry no rewards fee. The base part is split: 10% to the vault of the market's main, 10% to operations, 15% to the bettor's referrer, 5% to that referrer's referrer, the rest (60%, plus any referral share nobody can receive: 80% with no referrer) to the buyback. A bet pays no SOCIAL share: the 15% a token trade sends to Alpha+ authors goes to the buyback. The creator fee goes to the market creator's wallet in the same transaction. Payouts are in the market's pair asset.
Buying Shares
Shares are bought via AMM: instant fill, no counterparty. Price per share reflects implied probability. As more capital flows into one outcome, shares become more expensive (lower potential upside) and other outcomes become relatively cheaper.
Selling Shares
Selling goes through the P2P order book. You list at your chosen price: 0 (follow the AMM price) or between 0.001 and 0.999, and a listing fills only while its price is at or below the current AMM price. Because resolution value can far exceed the AMM buy price (e.g., bought at 5c on a winner-take-all market, resolution value well above $1), both sides of an order book trade can be genuinely profitable: a dynamic fixed-payout platforms cannot produce.
On Resolution: Ranked Payouts
When a market resolves:
- The market resolves to a ranking of outcomes;
getTrancheBP(market)gives each rank's share of the profit. - Holders of ranked outcomes redeem: their cost back plus their rank's share of the profit, pro rata to shares. The entitlement is in the market token;
redeemsells it for the pair asset. - Outcomes outside the ranking get nothing.
Special resolution outcomes (see Module 14 for edge cases):
- INVALID (254): an empty ranking. Each bettor is refunded their average cost.
- EARLY (253): "proposed too early". Never proposed: only the disputer can raise it (
disputeRankedEarly), and the panel can vote for it (voteRankedEarly). If it wins, the dispute resets, a new round starts, the next proposal bond doubles, and betting reopens only on an open-ended market.
Resolution Lifecycle
Market end time passes (an open-ended market can be proposed at any time)
↓
The creator may propose at once, anyone else from endTime + 15 minutes:
proposeRanked(marketToken, ranking, tieMask); bond = requiredBond(marketToken, you), in the resolver's BOND_TOKEN
↓
Challenge period (PROPOSAL_PERIOD)
├── No dispute → finalizeUncontested() → the proposer gets the bond back → winners redeem
└── disputeRanked(...) or disputeRankedEarly(...), by anyone but the proposer
(bond = the proposer's bond, at most BOND_CAP) → voting period (DISPUTE_PERIOD)
↓
Authorized panel wallets vote: voteRanked / voteRankedEarly (plurality, quorum MIN_QUORUM)
↓
Veto window (VETO_PERIOD): one vetoRanked by a third party hands the decision to the BASIS team
↓
finalizeMarket() → bonds are paid in this call → winners redeem
(a tie, or fewer than MIN_QUORUM votes, opens a new voting round instead)
The challenge period is 2 hours, the voting period 24 hours, the veto window 1 hour, and the EARLY fast lane (EARLY_FAST_PERIOD) 1 hour. Read the live values: PROPOSAL_PERIOD, DISPUTE_PERIOD, VETO_PERIOD and EARLY_FAST_PERIOD from the resolver at runtime; do not hardcode. See Resolver Read Methods.
3. Actions
3a. Create a Market
client.predictionMarkets.createMarketWithMetadata(options): creates the market and registers metadata on IPFS in one call. Requires SIWE authentication.
The contract call behind it is createMarket(CreateParams, deadline, approvalSignature) on the market host (marketTrading in /contracts.json for a public market). Creation is gated while gatingEnabled() is true: deadline and approvalSignature come from the platform's market-approval step (the SDK fetches them), and the signed name, symbol, outcomes and end time must be passed unchanged, from the same wallet, before deadline. The end time is part of the moderator approval: an approval is only valid for the exact end time it was signed for, and the platform's market-approval step refuses end times too far out. A signature is good for one host and one use.
// sizes of this main (n = number of outcomes)
(unit, seedStep, minBet, perOutcome, totalPool) = TrancheReader.sizesFor(host, maintoken, n)
seedFloor = host.minSeed() * unit // a multiple of seedStep (10 units)
// the factory's creation fee for the host, in ETH
fee = FACTORY.feeWhitelist(host) ? 0 : FACTORY.feeAmount()
pairAsset.approve(host, seedAmount)
host.createMarket(CreateParams{
marketName, symbol, endTime, optionNames,
trancheBP, // [10000] = winner-take-all
maintoken, frozen, bonding, seedAmount
}, deadline, approvalSignature) { value: fee }
// the market token is in the MarketCreated(marketToken, creator, maintoken) event of the receipt
JS:
const market = await client.predictionMarkets.createMarketWithMetadata({
marketName: "Will ETH reach $10k this month?",
symbol: "ETH10K", // MUST BE CAPITALISED
endTime: BigInt(Math.floor(Date.now() / 1000) + 86400 * 30),
optionNames: ["Yes", "No"],
trancheBP: [10000], // winner-take-all; [7000, 2000, 1000] pays the top three
maintoken: client.mainTokenAddress,
seedAmount: seedFloor, // pair asset: minSeed() × unit, a multiple of seedStep (read sizesFor)
description: "ETH price prediction.",
imageUrl: "https://example.com/eth.jpg",
});
const marketToken = market.marketTokenAddress;
Python:
import time
market = client.prediction_markets.create_market_with_metadata(
market_name="Will ETH reach $10k this month?",
symbol="ETH10K",
end_time=int(time.time()) + 86400 * 30,
option_names=["Yes", "No"],
tranche_bp=[10000],
maintoken=client.main_token_address,
seed_amount=seed_floor, # pair asset: minSeed() x unit, a multiple of seedStep (read sizesFor)
)
market_token = market["market_token_address"]
| Parameter | Required | Notes |
|---|---|---|
marketName | yes | Question/title |
symbol | yes | Must be CAPITALISED (e.g., "ETH10K", not "eth10k"). An SDK rule; the contract does not check case |
endTime | yes | Unix timestamp in seconds. 0 = open-ended. If set, must be ≥ now + 60s. It is signed into the moderator approval: pass the exact end time the approval was given for |
optionNames | yes | Array of outcome names, 2 to MAX_OUTCOMES() (read it from the host) |
trancheBP | yes (the SDK defaults to [10000]) | Payout ladder: 1 to 10 entries, no more than the number of outcomes, each above 0, non-increasing, summing to 10000 |
maintoken | yes | The main the market runs on (a main from /contracts.json mains[]); bettors pay in its pair asset |
seedAmount | no | Seed in the pair asset, in that asset's decimals: at least minSeed() × unit on the public host and a multiple of seedStep (both from sizesFor). It buys equal shares on every outcome for the creator: not outcome-pool liquidity (no liquidity deposit is needed) |
description, imageUrl, website, telegram, twitterx | no | Metadata |
frozen | no | true = only whitelisted wallets can bet (buy) until unfrozen (see whitelist pattern). Needs bonding > 0 |
bonding | no | Presale size of the market token, in whole tokens (the presaleTokens of token creation) |
Fee: msg.value must equal the factory fee the host is charged (feeWhitelist(host) ? 0 : feeAmount(), 0 today). Gate errors: Approval expired, Signer unset, Approval already used, Invalid approval. A market whose main's pair asset differs from the one the host registered reverts asset mismatch. Every other failed check on create reverts with no reason (§7).
Returns: { hash, receipt, marketTokenAddress, imageUrl, metadata }
For private markets (creator-managed resolution), use client.privateMarkets.createMarketWithMetadata() instead: same parameters plus privateEvent: true restricts betting (buy) to whitelisted addresses (seed floor minSeedPrivate() × unit), false is open to all (seed floor minSeedPublic() × unit). The private host's params carry privateEvent, and it needs its own approval. See Section 4.
3b. Bet (Buy Outcome Shares)
client.predictionMarkets.buy(marketToken, outcomeId, inputToken, inputAmount, minTokens, minShares): contract buy(marketToken, outcomeId, inputToken, inputAmount, minUsdc, minShares, deadline) (after deadline it reverts Deadline). It converts the input into market tokens, takes the fee, and prices the rest at the outcome's current odds. Approves the input token to the market host.
What you can pay with. The market token, the market's pair asset or its main are taken directly. Any other BASIS token (a token on the same main, another main, another market's token) and an outside token with a hop route to the pair asset are converted first, inside the same call; a token of another pair needs a route between the two pair assets. Their sell side is your own sell: you pay that token's normal sell fee (your referral discount applies; a fee-exempt wallet pays none), then the bet fee.
Quote first, in two steps. The host's reader returns 0 for a token it does not take directly, so:
- Turn the input into a token the host quotes (an
eth_call): a token on the market's own main →SWAP.quoteCross([token, main], amount, you); a BASIS token on the market's pair asset →quoteCross([token, (its main,) pairAsset], amount, you); an outside token →HOP_EXECUTOR.quoteHop(token, pairAsset, amount). Pass your wallet as the trader so the quote matches the bet. TrancheReader.quoteShares(host, marketToken, outcomeId, step2Token, step2Amount, you)→(sharesOut, netToken).
JS:
const host = addresses.marketTrading; // addresses.privateMarket for a private market
const amount = parseUnits("5", 6); // 5 units of the pair asset (6 decimals for USDG)
const [sharesOut, netToken] = await client.marketReader.quoteShares(host, marketToken, 0, PAIR_ASSET, amount, wallet);
const minShares = sharesOut * 97n / 100n; // 3% tolerance
const minTokens = netToken * 97n / 100n;
const result = await client.predictionMarkets.buy(
marketToken,
0, // outcomeId (0-based)
PAIR_ASSET,
amount,
minTokens, // floor on the market-token amount the input converts to
minShares
);
Python:
host = addresses["marketTrading"]
amount = 5 * 10**6 # 5 USDG
shares_out, net_token = client.market_reader.quote_shares(host, market_token, 0, PAIR_ASSET, amount, wallet)
min_shares = shares_out * 97 // 100 # 3% tolerance
min_tokens = net_token * 97 // 100
result = client.prediction_markets.buy(market_token, 0, PAIR_ASSET, amount, min_tokens, min_shares)
Never derive minShares from amount × 1e18 / pricePerShare: that estimate ignores the conversion into market tokens, the fee and the bet's own price impact, so the buy reverts with no reason. Never send a minimum of 0.
| Param | Description |
|---|---|
marketToken | Market token address |
outcomeId | Outcome index, 0-based |
inputToken | The market token, the market's pair asset, its main, or another token as above |
inputAmount | Amount to spend, in the input token's decimals |
minTokens (contract minUsdc) | Floor on the market-token amount the input converts to; that amount must also reach the scaled minimum bet (sizesFor(...).minBet, in 18-decimal market tokens, before the fee) |
minShares | From quoteShares less a tolerance: always set it (see slippage tolerance →04) |
Pre-flight checks: outcomeId below getNumOutcomes() (a bad id reverts with no reason); the market is not resolved (getMarketData().resolved); betting is open, bets stop at endTime (Betting closed), and on an open-ended public market once a result was proposed (Outcome proposed); a frozen market needs the whitelist. A bet whose fee would be worth less than one raw unit of the pair asset reverts with no reason; simulate the call by eth_call near the minimum.
3c. Order Book (Sell / P2P Trade)
The order book handles peer-to-peer limit orders for outcome shares. Use it to sell shares before resolution or to buy shares at a specific price. (Advanced order book strategies for traders → Module 13.)
List a Sell Order
client.orderBook.listOrder(marketToken, outcomeId, amount, pricePerShare)
// List 100 shares of outcome 0 at a price of 0.60
const result = await client.orderBook.listOrder(
marketToken, 0, parseUnits("100", 18), parseUnits("0.6", 18)
);
result = client.order_book.list_order(
market_token, 0, 100 * 10**18, 600_000_000_000_000_000 # price 0.60
)
pricePerShare is odds × 1e18: 0 = fill at the AMM price at execution, otherwise 0.001 to 0.999. It is not a pair-asset amount. A listing fills only while its price is at or below the current AMM price.
Fill an Order
client.orderBook.buyOrder(marketToken, orderId, fill, maxCost): fill is a number of shares (at most the order's amount). maxCost is the most pair asset you will pay, fee included; above it the call reverts max cost. Take it from getBuyOrderCost(marketToken, orderId, fill).totalCostToBuyer, read with your own address as the caller (the quote uses the caller's fee rate); add headroom only for a market-priced order.
To fill several orders in one transaction use client.predictionMarkets.buyOrdersAndContract(marketToken, outcomeId, orderIds, inputToken, totalInput, minShares) with inputToken = the zero address (fill orders only; the rest of the budget stays with you) or the market's pair asset (orders, then the AMM).
Cancel an Order
client.orderBook.cancelOrder(marketToken, orderId)
Hybrid Fill (Order Book + AMM)
client.predictionMarkets.buyOrdersAndContract(marketToken, outcomeId, orderIds, inputToken, totalInput, minShares): fills the listed orders first, then buys remaining from AMM, all in one transaction. inputToken is the market's pair asset (or the zero address for orders only); a bet in any other token is a plain buy, with no order-book sweep.
A fill needs the order's price at or below the current AMM price, and you cannot fill your own order; on buyOrder both fail with no reason, while a buyOrdersAndContract sweep skips such orders (getBuyOrderAmountsOut quotes them as 0). Named reverts: empty order (listing 0 shares), dust fill, pair unit, max cost.
Order Book Reads
| Method | Returns |
|---|---|
client.orderBook.getBuyOrderCost(marketToken, orderId, fill) | { baseUsdc, buyerTax, totalCostToBuyer, netToSeller } |
client.orderBook.getBuyOrderAmountsOut(marketToken, orderId, pairAmount) | { fill, baseUsdc, buyerTax, totalCostToBuyer } |
client.predictionMarkets.getBuyOrderAmountsOut(marketToken, orderId, pairAmount) | Preview order fill from prediction module |
client.api.getMyOrders(params?) | Authenticated wallet's own orders across all markets: paginated, filterable by status / marketToken / outcomeId (→ Module 18) |
Amounts are raw pair asset of the market. Send the reads from the buyer's address: the tax in the quote is the caller's.
3d. Redeem (Collect Winnings)
client.predictionMarkets.redeem(marketToken, fractionBps, slices, minOut): after resolution, a holder of a ranked outcome sells their entitlement for the pair asset. fractionBps: 1 to 10000 of what is left. slices: 1 to 25 (more slices, less price impact). minOut: the floor on this call's total (Below minOut). A partial redeem leaves the rest pending (pendingPayout).
const [tokenPayout, pairPayout] = await client.marketReader.quoteRedeemUSDB(host, marketToken, wallet);
if (tokenPayout > 0n) {
const receipt = await client.predictionMarkets.redeem(marketToken, 10000, 1, pairPayout * 98n / 100n);
// the amount paid is in the SharesRedeemed event: (marketToken, user, payout, tokens, remaining)
}
receipt = client.prediction_markets.redeem(market_token, 10000, 1, min_out)
minOut can also come from RedemptionReader.quoteRedemption(marketToken, host.quoteRedeem(marketToken, wallet)). If none of your outcomes is ranked, the quote is 0 and there is nothing to redeem (Nothing to redeem, or a revert with no reason). An INVALID market refunds cost basis through the same call.
3e. Basic Resolution
Full resolution strategy is covered in Module 14. Here is the mechanical flow. Every tranche market resolves through the ranked calls; the old single-outcome calls revert Ranked market: use the *Ranked entrypoints.
Step 1: Discover Markets Needing Resolution
const markets = await client.api.getTokens({ isPrediction: true, limit: 100 });
const needsProposal = markets.data.filter(m => m.predictionStatus === "awaiting_proposal");
const inDispute = markets.data.filter(m => m.predictionStatus === "disputed");
predictionStatus values: "active" | "awaiting_proposal" | "proposed" | "proposed_closed" | "voting" | "veto_window" | "disputed" | "vetoed" | "awaiting_finalize" (a private market whose voting window has passed: finalize can be called) | "resolved" | "invalidated" | "unknown". awaiting_proposal opens to non-creators 15 minutes after endTime; a tie or a missing quorum reopens voting.
Step 2: Propose an Outcome
client.resolver.propose(marketToken, outcomeId) sends proposeRanked(marketToken, [outcomeId], 0). For a podium market call client.resolver.proposeRanked(marketToken, ranking, tieMask) with the outcome ids in rank order. The bond is requiredBond(marketToken, you), in the resolver's BOND_TOKEN() (sized in BOND_UNIT()), approved to the resolver: 5 units, doubled after each EARLY reset, at most 8 doublings (1,280 units), never above BOND_MAX() (8,000 units). The creator may propose from endTime, anyone else 15 minutes later (Too early to propose). There is no bounty pool.
Step 3: Finalize (Uncontested)
client.resolver.finalizeUncontested(marketToken): anyone can call after the challenge period expires with no dispute (Wait more before it is over). The proposer gets its own bond back and nothing more.
Step 4: If Disputed
client.resolver.disputeRanked(marketToken, ranking, tieMask) with a different ranking (Must be different), or disputeRankedEarly(marketToken) to say the proposal came too early. Bond: the proposer's bond, capped at BOND_CAP (500 units). The proposer cannot dispute their own proposal (Proposer cannot dispute self). Triggers the voting period.
Step 5: Vote
Only wallets on the authorized panel vote (authorizedVoter(wallet)): voteRanked(marketToken, ranking, tieMask) or voteRankedEarly(marketToken), once per round (Not an authorized voter, Voted). There is nothing to stake and no vote lock. Everyone else waits.
client.resolver.finalizeMarket(marketToken) works after the voting period and the veto window; it takes the leading ranking once MIN_QUORUM (2) votes are in, and opens a new round on a tie or a missed quorum.
Step 6: Bond Payouts
Bonds are paid inside the finalize call; there is nothing to claim. The winner gets their own bond back, plus the loser's bond up to twice the winner's own bond (so at most three times their own bond in total). Anything above that goes to insurance. If neither key wins, both bonds go to insurance.
Complete Example (JS)
const markets = await client.api.getTokens({ isPrediction: true, limit: 100 });
const market = markets.data.find(m => m.predictionStatus === "awaiting_proposal");
await client.resolver.propose(market.address, 0); // proposeRanked(market, [0], 0); posts requiredBond
// after PROPOSAL_PERIOD:
if (!(await client.resolver.isInDispute(market.address))) {
await client.resolver.finalizeUncontested(market.address); // the bond comes back
} else {
// disputed: the panel votes. After DISPUTE_PERIOD + VETO_PERIOD anyone may finalize
await client.resolver.finalizeMarket(market.address);
}
Resolver Read Methods
| SDK method | Contract | Returns |
|---|---|---|
isResolved(marketToken) | resolved | boolean |
getFinalOutcome(marketToken) | finalOutcome | First-ranked outcome; 254 = INVALID, 255 = unresolved (for the full ranking read getFinalRanking on the host) |
isInDispute(marketToken) | inDispute | boolean |
isInVeto(marketToken) | inVeto | boolean |
getCurrentRound(marketToken) | currentRound | number |
getDisputeData(marketToken) | disputes | Proposer, disputer, the ranking keys (bytes32: below 256 an outcome id or 253 / 254, otherwise decode with getRanking), the bonds, disputeStartTime |
hasProposal(marketToken) | hasProposal | boolean |
requiredBond(marketToken, wallet) | requiredBond | The proposal bond now, in BOND_TOKEN |
isVoter(wallet) | authorizedVoter | On the panel |
getVoteCount(marketToken, round, key) | voteCount | number |
getTally(marketToken, round) | tallies | Leader, leaderCount, totalVotes, tie |
hasVoted(marketToken, round, voter) | hasVoted | boolean |
getVoterChoice(marketToken, round, voter) | voterChoice | A bytes32 key |
getRanking(marketToken, key) | getRanking | The ranking and tie mask behind a key |
getConstants() | PROPOSAL_PERIOD, DISPUTE_PERIOD, VETO_PERIOD, EARLY_FAST_PERIOD, PROPOSAL_BOND, BOND_CAP, BOND_MAX, VETO_FLOOR, MIN_QUORUM, BOND_TOKEN, BOND_UNIT | Today's settings |
No view returns the phase deadlines. The proposal closes at disputes(m).disputeStartTime + PROPOSAL_PERIOD; finalize opens at disputeStartTime + DISPUTE_PERIOD + VETO_PERIOD. The creator's 15-minute head start is fixed in the contract.
4. Private Markets
Private markets use creator-managed resolution (skipping the public dispute/vote flow). The creator and their designated voters resolve the market directly.
Create: client.privateMarkets.createMarketWithMetadata(options): same parameters as public plus:
privateEvent: true: restricts betting (buy) to whitelisted addresses only (same whitelist pattern as gated tokens)
The approval is requested for the private host; trancheBP works as on a public market. Seeds: at least minSeedPublic() × unit when privateEvent is false and minSeedPrivate() × unit when it is true, always a multiple of seedStep of the pair asset.
Manage voters: client.privateMarkets.manageVoter(marketToken, voter, add): add=true adds, add=false removes. Only before the first vote; at most 11 voters including the creator, who is always a voter.
Resolution flow: After the end time (at any time on an open-ended market) each voter calls client.privateMarkets.vote(marketToken, ranking, tieMask): [outcomeId] and 0 on a winner-take-all market; an empty ranking votes INVALID. One vote each; it is final. client.privateMarkets.finalize(marketToken) can be called from firstVoteTime + VOTING_WINDOW (15 minutes); that is a minimum, not a deadline: votes count until it is called. Plurality wins. An exact tie goes to the creator's vote if it is one of the leaders, otherwise the market is INVALID; while the creator has not voted, finalize on a tie reverts with no message until the creator votes.
Important: Tell the two hosts apart with predictionType in the API response (public or private). A private (creator-managed) market resolves by its voters' vote: after awaiting_proposal it moves to voting, then awaiting_finalize once the voting window has passed, and anyone can then call finalize.
| Method | Description |
|---|---|
vote(marketToken, ranking, tieMask) | Cast the one, final ranked vote (creator and listed voters only) |
finalize(marketToken) | Finalize, from 15 minutes after the first vote |
manageVoter(marketToken, voter, add) | Add/remove voter (before the first vote only) |
togglePrivateEventBuyers(marketToken, buyers, status) | Whitelist/unwhitelist buyer addresses (contract read userCanBuyEvent) |
disableFreeze(marketToken) | Open frozen market to public |
manageWhitelist(marketToken, wallets, amount, tag, status) | Manage frozen market whitelist; amount is a cap in market tokens |
getVoters(marketToken) | The voter list |
getVoterChoice(marketToken, voter) | { key, ranking, tieMask }: contract voterKey returns a bytes32 key, decoded with getRanking(marketToken, key) |
getFirstVoteTime(marketToken) | Contract firstVoteTime |
There is no bounty on private markets either. Private markets share all prediction market and order book read methods.
5. Read Methods
Prediction Markets (client.predictionMarkets)
| Method | Returns | Notes |
|---|---|---|
getMarketData(marketToken) | MarketData struct | Includes usdc (the market's pair asset), resolved, finalOutcome (254 = INVALID, 255 = unresolved), generalPot (always 0), isPrivate, endTime |
getOutcome(marketToken, outcomeId) | Outcome struct | The outcome's state, including totalCost and circulatingShares |
getUserShares(marketToken, user, outcomeId) | bigint | Shares held, 18 decimals |
getNumOutcomes(marketToken) | bigint | Total outcome count |
getOptionNames(marketToken) | string[] | Outcome names array |
hasBettedOnMarket(marketToken, user) | boolean | |
getTrancheBP(marketToken) | number[] | The payout ladder |
getFinalRanking(marketToken) | number[] | The resolved ranking (empty = INVALID) |
quoteRedeem(marketToken, user) | bigint | The user's entitlement in market tokens |
quoteScenario(marketToken, ranking, tieMask, user) | bigint | What the user would get, in market tokens, if that ranking won |
getPendingPayout(marketToken, user) | bigint | Left after a partial redeem (contract pendingPayout) |
getInitialReserves(numOutcomes) | [bigint, bigint] | Starting depth for a market with that many outcomes; the sizes in force come from TrancheReader.sizesFor |
getBuyOrderAmountsOut(marketToken, orderId, pairAmount) | { fill, baseUsdc, buyerTax, totalCostToBuyer } | Preview order book fill |
Market Reader (client.marketReader)
Every reader call takes the market's host first: marketTrading or privateMarket from /contracts.json. "USDB" in these names means the market's pair asset.
| Method | Returns | Notes |
|---|---|---|
getAllOutcomes(host, marketToken) | OutcomeInfo[] | pricePerShare (odds × 1e18), probability (basis points), hasWon (true for every ranked outcome) |
estimateSharesOut(host, marketToken, outcomeId, pairAmount, orderIds, user) | bigint | Estimated shares from combined AMM + order book fill |
quoteShares(host, marketToken, outcomeId, inputToken, inputAmount, buyer) | [sharesOut, netToken] | The exact pre-bet quote, used for minShares |
quoteRedeemUSDB(host, marketToken, user) | [tokenPayout, usdbPayout] | What a full redeem pays now |
quoteScenarioUSDB(host, marketToken, ranking, tieMask, user) | [tokenPayout, usdbPayout] | What the user would get if that ranking won |
sizesFor(host, main, n) | [unit, seedStep, minBet, perOutcome, totalPool] | The market sizes in force on a main |
Implied probability from getAllOutcomes: Number(outcome.probability) / 100 gives percent (the value is in basis points).
Market Reader Aggregates (client.api)
// All prediction markets
const all = await client.api.getTokens({ isPrediction: true, limit: 100 });
// Filter by status
const active = all.data.filter(m => m.predictionStatus === "active");
6. Fees
| Action | Cost | Goes To |
|---|---|---|
| Market creation | The seed (pair asset, minSeed() × unit or more, a multiple of seedStep) plus the factory fee as msg.value (feeWhitelist(host) ? 0 : feeAmount(), ETH, exact; 0 today) | The seed buys equal shares on every outcome for the creator (no liquidity deposit required) |
| Buy outcome shares | getTaxRate(marketToken, bettor): the base (0.60%, 0.54% referred) plus the market creator's creator fee. A market token has no rewards fee | Base part: 10% vault of the market's main, 10% operations, 15% referrer, 5% second-level referrer, the rest to the buyback (60%, or 80% with no referrer; a bet pays no SOCIAL share). Creator fee: the market's creator |
| Predict+ token trading | The same rate | The same split |
| List order / cancel order | Free | - |
| Fill order | Buyer and seller each pay the trade fee at their own rate (buyerTax, netToSeller) | Split as above |
| Resolution proposal | requiredBond(market, you), in the resolver's bond token: 5 units, doubled after each EARLY reset, never above BOND_MAX() | Returned if nobody disputes |
| Dispute | The proposer's bond, capped at 500 units (BOND_CAP) | The winner gets their own bond back plus the loser's up to twice their own; the rest goes to insurance |
| Veto | The proposer's bond, held between 100 (VETO_FLOOR) and 500 units | Once per market, after the voting period, by anyone but the proposer and the disputer |
| Redeem | No fee; the payout is the quoted sale of your entitlement (price impact) | - |
Creator earnings: the creator fee the market's creator sets on the market token (setCreatorFee, 0 to 1%, 0 until set), on every bet and token trade, plus 20% of the base part of loan-side fees and of unwon prize money on the token. Nothing is claimed.
7. Errors
Named reverts (the resolver's strings are in Module 14 and in the list below):
| Error | Cause | Fix |
|---|---|---|
Betting closed | A bet after the market's end time (public host) | The market is closed |
Outcome proposed | A bet on an open-ended public market after a result was proposed | The market is closed |
Deadline | A bet mined after its deadline | Send it again with a later deadline |
Approval expired / Signer unset / Approval already used / Invalid approval | The creation approval is late, was used, or is for another creator, text, end time or host | Ask for a new approval and pass its signed values unchanged |
asset mismatch | createMarket on a main whose pair asset is not the host's | Use a main registered with this host |
fraction 1-10000 / slices 1-25 / Below minOut | Bad redeem arguments, or the sale returned less than minOut | Fix the arguments; re-quote minOut |
Nothing to redeem | Nothing left to redeem, or a fraction that rounds to 0 | Check quoteRedeem(market, you) first |
empty order | Listing 0 shares | List a positive amount |
dust fill / pair unit / max cost | A fill worth nothing, a pair-unit mismatch, or a fill above maxCost | Raise fill or re-quote maxCost |
hosts only | A bet in a token the host must convert, on a host the deployment did not register | A deployment error; nothing a user can fix |
Too early to propose / Already proposed / Wait more | Resolver timing | Wait for the period; read hasProposal |
Rules: not qualified | The resolver's optional proposal rule (off by default) refused the proposer | Another wallet can propose |
No proposal to dispute / Already in dispute / Dispute period over / Must be different / Proposer cannot dispute self | Resolver dispute rules | Dispute an open proposal, with another ranking, from another wallet |
Not voting / cannot vote during veto / Disputed period ended / Not an authorized voter / Voted / Bad ranking | Resolver voting rules | Only panel wallets vote, once per round, with a valid ranking |
Ranked market: use the *Ranked entrypoints | An old single-outcome resolver call | Use the ranked calls |
Reverts with no message. Most market guards are bare requires: the revert carries no text. When a create, bet, list, fill or redeem call fails with no message, one of these is false: the seed is at least the host's floor (minSeed() × unit) and a multiple of seedStep; the end time is 0 or at least 60 seconds ahead; there are 2 to MAX_OUTCOMES outcomes; trancheBP follows the ladder rules; msg.value equals the factory fee; the outcome id exists; the market is not resolved; the frozen-market whitelist admits you; the input converts to at least the scaled minBet and minTokens; the bet's fee is worth at least one raw unit of the pair asset; the shares reach minShares; the listing price is 0 or 0.001 to 0.999; you hold the shares you list; the order is active, is not yours, and its price is at or below the AMM price; you have an entitlement to redeem; on a private market, the vote and finalize rules of §4. Simulate the call (eth_call) and re-run the pre-flight reads.
8. Strategy Context
Stacking: Paths C and D
Prediction markets fit into two stacking paths. Use these as building blocks inside multi-stack strategies (full stacking reference → Module 12).
Path C: Predict+ Token Path:
pair asset → buy Predict+ token → borrow the pair asset against it
- Position held: Predict+ token (can gain from market volume whichever outcome wins, before fees and slippage)
- Pair asset recovered: about 97.4% of the input before the market's creator fee (
(1 − 0.006) × (1 − 0.0205): the 0.60% buy fee and a 10-day loan at 2% + 0.005% per day). Compute it withgetTaxRateand the main's loan terms - Risk: loan expiry + market must stay active
Path D: Outcome Bet Path:
pair asset → buy outcome shares → [wait for resolution] → pair asset (if correct)
- Position held: outcome shares
- Pair asset recovered: only on a ranked outcome, but payout is not capped per share
- Risk: you lose if the outcome is wrong
Combining C and D (the Quick Stack: see also Module 12 §5):
- Buy Predict+ tokens → borrow the pair asset
- Use the borrowed pair asset to buy outcome shares on the same market
- Two positions from one bag: token appreciating from volume, shares waiting on resolution
- Win the bet → collect winnings → repay loan → still own the Predict+ tokens
Three-stack fee reality check: After Path A (vault stake →06) → Path C (Predict+) → Path D (bet), about 94 cents of every dollar reach the bet and about 6 cents went to fees (three base fees and two loans, before any creator fee). Three stacks is the sweet spot before fees bite meaningfully.
Creator vs Bettor vs Trader
Three independent roles, each of which can earn on its own. Combining them is the real edge.
Creator (passive): Earn the creator fee you set on the market token, on every bet and trade. Create questions that generate genuine disagreement. A dead market costs gas with zero return.
Bettor (conviction): Uncapped payouts reward early conviction. Buy before consensus forms. Use the order book to exit if conviction changes: you can list shares at your own price (at or below the AMM price), and both sides of the trade can genuinely profit because of the uncapped resolution value.
Trader (volume): Buy Predict+ tokens on high-activity markets. You're betting on controversy and volume, not outcomes. Predict+ tokens are a Stable+ subtype: price can only go up, in pair-asset units, though a sale receives less than the quoted price, more so on a large sale. Hold through the market lifecycle and sell after resolution for maximum appreciation.
The maximum play: create the market + buy Predict+ tokens + bet on an outcome. Three income streams from one market. Proposing the result earns nothing beyond your bond back.
Full roles, combined strategies, and capital recycling loops → Module 13. Resolution, bonds and the veto → Module 14.
9. Up/Down Markets (Crypto Price Prediction)
A different shape of prediction market: instead of creating outcome tokens for an event, you bet on whether a tracked price will close higher or lower than where it started. Rounds are short (5 minutes to 24 hours), settled by each asset's price feed, and open to anyone.
Assets & Timeframes
One MultiPrediction contract (upDown in /contracts.json) runs every asset and every timeframe. Assets are numbered: read assetCount() and assets(i) → (priceFeed, poolToken, paused, label, main, settle, virtFloor). label is a display string; main is the main the asset's pool token trades on; settle is the asset you are paid in. The list grows when an asset is added. Every call takes (asset, tf).
Timeframes (tf enum, used in API and SDK):
tf | Slug | Round duration |
|---|---|---|
| 0 | 5m | 5 minutes |
| 1 | 15m | 15 minutes |
| 2 | 1h | 1 hour |
| 3 | 4h | 4 hours |
| 4 | 24h | 24 hours |
How A Round Works
Each timeframe runs an independent sequence of rounds back-to-back.
- Round opens: the contract reads the asset's price feed and records the current price as the round's
startPrice. - Bet window: a Bull bet wins only if the price closes strictly above the start; a Bear bet wins if it closes below. Your input is converted into the asset's pool token and added to the bull or the bear pool. A wallet can bet on one side only in a round.
- Betting closes
BET_LOCK()(15 seconds) beforeendTime. - Settlement: from
endTime, and forFINALIZE_WINDOW()(20 minutes) after it, anyone can callsettleCurrentRound(asset, tf). 5% of the losing pool is carried into the next round; winners get their stake back plus a share, by shares, of the rest of the losing pool and the seed bonus. - Cancellation: if no usable price arrives in that window, or the price ends exactly where it started, the round cannot settle. After the window anyone can call
cancelCurrentRoundAndStartNext(asset, tf)and every bet is refunded.
A cancelled round refunds the net stake (after the fee), paid in the settle asset.
Place a Bet
const a = client.updown.asset(0); // an asset index; list them with assetCount() / assets(i).label
const [shares] = await a.quoteSharesForInput(tf, 1, inputToken, amount); // side: 1 = Bull, 2 = Bear → (shares, netAmount)
await a.betBull(tf, amount, { inputToken, minShares: shares * 97n / 100n });
// contract: betBull / betBear(asset, tf, inputToken, amount, minShares, deadline)
a = client.updown.asset(0)
shares, net = a.quote_shares_for_input(tf=0, side=1, input_token=input_token, amount=amount)
a.bet_bull(tf=0, amount=amount, input_token=input_token, min_shares=shares * 97 // 100)
inputToken is the asset's settle asset (the default), its main, its pool token, or another BASIS token (quote it in two steps, as for a market bet in §3b); approve it to the contract. amount is in that token's decimals. minShares is quoteSharesForInput(asset, tf, side, inputToken, amount, user) less a tolerance (a raw quote reverts MinSharesNotMet a block later; the quote is 0 once betting is closed). side is the number 1 (Bull) or 2 (Bear). There is no minBet read: a net amount below the asset's minimum reverts InvalidAmount.
Each bet is recorded as its own event: a wallet can top up a position by betting again on the same side in the same round.
Claim Winnings (or Refund)
await a.claim(tf, roundId, { fractionBps: 10000, slices: 1, minOut });
// contract: claim(asset, tf, roundId, fractionBps, slices, minOut)
The default is the whole claim in one slice, with minOut from quoteClaimPayout(asset, tf, roundId, user) less a tolerance. It pays the settle asset. A wallet that lost, or that already claimed everything, gets NothingToClaim, not a payout of 0. A partial claim leaves the rest pending.
Settle / Cancel a Stalled Round
These are public-good actions. Anyone can call them on any expired round:
await a.settleCurrentRound(tf); // between endTime and endTime + FINALIZE_WINDOW
await a.cancelCurrentRoundAndStartNext(tf); // after endTime + FINALIZE_WINDOW: refunds all bets
Used when a round needs closing but no participant has called yet.
SDK Surface
client.updown.assets() lists the assets; client.updown.asset(keyOrIndex) returns one asset's methods:
| Method | Type | Description |
|---|---|---|
betBull(tf, amount, opts?) / betBear(tf, amount, opts?) | write | opts: inputToken, minShares. Approves the input token. Contract betBull / betBear(asset, tf, inputToken, amount, minShares, deadline) |
claim(tf, roundId, opts?) | write | opts: fractionBps, slices, minOut. Contract claim(asset, tf, roundId, fractionBps, slices, minOut) |
settleCurrentRound(tf) | write | Public settle of an ended round |
cancelCurrentRoundAndStartNext(tf) | write | Public cancel after the finalize window |
quoteSharesForInput(tf, side, inputToken, amount) | read | (shares, netAmount); 0 once betting is closed |
quoteClaimPayout(tf, roundId) | read | Claimable amount, in the settle asset |
getRound(tf, roundId?) | read | Round struct (current round if roundId omitted) |
getUserBet(tf, roundId) | read | Your bet on a round |
getAsset() | read | assets(i) |
Plus aggregate helpers on client.api:
| Method | Type | Description |
|---|---|---|
getMyUpDown() | read | Wallet's bets + claims summary across all assets/timeframes. |
getUpDownRounds(opts) | read | Paginated round list. Filter by token, tf, outcome. |
getUpDownRound(token, tf, roundId) | read | Single round detail with full bet/claim history. |
REST API Endpoints
GET /api/v1/me/updown: your active bets + claim history.GET /api/v1/updown/rounds?token=<slug>&tf=0..4: paginated rounds; take the asset slugs fromGET /api/v1/updown/assets.GET /api/v1/updown/rounds/{token}/{tf}/{roundId}: single round detail.
All require an API key (60 req/min). See Module 17 for auth + rate limit details.
Units on chain: pools, shares and stakes are pool-token amounts (18 decimals); payouts are in the asset's settle asset; prices use the feed's own decimals.
Errors
The Up/Down contract reverts with custom errors, not strings:
| Error | Why | Recovery |
|---|---|---|
BettingClosed | A bet in the last 15 seconds of a round or after its end | Bet in the next round |
AlreadyBetOnOtherSide | A second bet on the other side in the same round | One side per round |
InvalidAmount / ZeroShares | Bet too small (below the asset's minimum) | Increase the bet amount |
MinSharesNotMet | The quote moved | Quote again |
TooEarlyToSettle | Settle before endTime, or cancel before the finalize window is over | Wait |
TooLateForValidPrice / NoValidPriceInWindow / NoUpdateAfterEndTime / PriceDriftTooLarge / StalePrice / InvalidPrice | No usable feed price | Retry later, or cancel after the finalize window |
RoundAlreadySettled | Settled already | Nothing to do |
RoundNotSettled | Claim before settlement | Settle first |
NothingToClaim | A losing bet, or claimed already | Nothing to claim |
NoActiveRound | The asset or timeframe has no running round | Wait for the round to start |
InvalidAsset / InvalidTimeframe | Bad index | Read assetCount(); tf is 0 to 4 |
Paused | The contract or the asset is paused | Wait until it resumes |
The claim also has the strings fraction 1-10000, slices 1-25 and Below minOut. A bet sent after its deadline reverts Deadline.
Key Facts (Quick Reference)
| Parameter | Value |
|---|---|
| Max outcomes per market | MAX_OUTCOMES() (150 by default; read it) |
| Market sizes | Set per main: read unit with TrancheReader.sizesFor(host, main, n) |
| Seed step | seedStep = 10 units of the pair asset |
| Min seed (public host) | minSeed() × unit |
| Min seed (private host, open to all) | minSeedPublic() × unit |
| Min seed (private host, invite-only) | minSeedPrivate() × unit |
| Minimum bet | sizesFor(...).minBet (market tokens, after conversion, before the fee) |
| Creator fee | Set by the market's creator: 0 to 1%, 0 until set |
| Proposal bond | requiredBond(market, wallet): 5 bond-token units, doubling after each EARLY reset, never above BOND_MAX() |
| Dispute bond | The proposer's bond, capped at 500 units |
| Veto bond | The proposer's bond, 100 to 500 units |
| Voting rule | Authorized voter panel only; the leading ranking wins once 2 votes are in |
| Payouts | By rank, per getTrancheBP(market); [10000] = the winner takes the pot |
Error Handling
Pre-Flight Checks
Before prediction market operations:
- Seed: at least the host's floor (
minSeed()× unit;minSeedPublic()/minSeedPrivate()× unit on the private host) and a multiple ofseedStep(readsizesFor) - End time: 0 (open-ended) or ≥ now + 60 seconds, and exactly the end time in the moderator approval
- Check outcome IDs exist before betting/proposing (use
getNumOutcomes()) - For resolution: hold
requiredBondof the resolver's bond token, market in the right phase, not in veto - Check your pair-asset balance (and the bond token for resolution) before seeding or betting
- Simulate order book fills before large orders
Common Errors
See §7: one table of the strings that exist, and the checklist for reverts with no message.
Recovery Flow: Market Resolution Disputed
- Your proposal was disputed: your bond is lost only if the panel picks another ranking.
- The panel votes; you cannot vote unless your wallet is on it.
- After the voting period and the veto window anyone calls
finalizeMarket. - If your ranking wins you get your bond back plus the disputer's, up to twice your own; if it loses, your bond is forfeited.
Recovery Flow: Shares Have No Value After Resolution
- Check which outcomes are ranked:
getFinalRanking()(orgetMarketData()→finalOutcome) - If
quoteRedeem(market, you)is above 0:redeem(market, 10000, 1, minOut), payout by the market's tranche ladder - If it is 0: nothing to claim. Learn from the loss.
- Check if you also hold Predict+ tokens: these may still have value (Stable+ mechanics, price only goes up in pair-asset units; sell on the DEX →04)