Module 14: Resolution Deep Dive

What this covers: The complete dispute and resolution lifecycle for Basis prediction markets: from the moment a market ends to final payout. Includes the ranked resolver calls, bonds, private market resolution, bond economics, cross-platform arbitrage timing, and full error handling.

Prerequisites

  • Basic prediction market mechanics: market lifecycle, outcome shares, seedAmount
  • Prediction strategies: participant roles (esp. Role 5) and combined plays
  • Trading basics: buying outcome shares and Predict+ tokens, slippage protection
  • Client initialized (→02) with enough of the resolver's bond token (BOND_TOKEN()) for the bond (requiredBond(market, wallet))

Next steps


1. The Resolution Lifecycle

A prediction market moves through four phases after its end time passes. 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. Tell the hosts apart with predictionType. A non-creator can propose only 15 minutes after endTime; a tie or a missing quorum reopens voting.

Every market of the public host (marketTrading in /contracts.json) is registered with the resolver as a ranked market. The older single-outcome calls (the ones that take one uint8 outcome) revert with Ranked market: use the *Ranked entrypoints. Every call below takes a ranking: the outcome ids in finishing order. On a winner-take-all market (getTrancheBP(market) = [10000]) that is [winner]; on a podium market it is the top K places, K = the length of getTrancheBP(market) (more entries when places are tied, marked in tieMask; 0 = no ties). An empty ranking means INVALID. Check a ranking with isValidRanking(market, ranking, tieMask) on the host before sending.

Phase 1: Proposal

After endTime, the market enters "awaiting_proposal". The market's creator may propose at endTime; everyone else from endTime + 15 minutes (Too early to propose before that). An open-ended market (endTime = 0) can be proposed by anyone at any time.

// sends proposeRanked(marketToken, [outcomeId], 0)
await client.resolver.propose(marketToken, outcomeId);

// a podium market, or any ranking with ties
await client.resolver.proposeRanked(marketToken, ranking, tieMask);
  • Costs a bond of requiredBond(market, wallet) in the resolver's BOND_TOKEN (approve that token to the resolver). The bond is 5 units (PROPOSAL_BOND = 5 × BOND_UNIT), doubled after each EARLY reset, at most 8 doublings (1,280 units), never above BOND_MAX() (8,000 units). Read requiredBond(market, wallet).
  • Starts the challenge period (PROPOSAL_PERIOD: 2 hours; read it from the resolver)
  • During the challenge period, anyone except the proposer can dispute
  • If no dispute arrives by the end of the challenge period, anyone calls finalizeUncontested() and the proposer gets their bond back, and nothing more.
  • On an open-ended market a proposal also stops new bets (Outcome proposed).

The proposer cannot dispute their own proposal (Proposer cannot dispute self). A wrong proposal can only be corrected by another wallet's dispute.

Phase 2: Dispute

During the challenge period, any wallet except the proposer can file a dispute with a different ranking:

await client.resolver.disputeRanked(marketToken, ranking, tieMask);
// or, when the proposal came before the event was decided:
await client.resolver.disputeRankedEarly(marketToken);
  • Costs the proposer's bond, at most BOND_CAP (500 units), in the bond token
  • The ranking must differ from the proposal's (Must be different); a dispute restarts the clock (disputeStartTime)
  • Opens the panel's vote for DISPUTE_PERIOD (24 hours; read it from the resolver)
  • EARLY comes only from disputeRankedEarly or voteRankedEarly. INVALID is the empty ranking, open to every role.

Phase 3: Vote

Only the authorized panel votes: wallets with authorizedVoter(wallet) = true (voterCount() gives the panel size). Panel members call voteRanked or voteRankedEarly, once per round, until disputeStartTime + DISPUTE_PERIOD. Agents outside the panel cannot vote. There is no voter stake, no vote lock and no voter reward.

// panel wallets only
await client.resolver.voteRanked(marketToken, ranking, tieMask);
await client.resolver.voteRankedEarly(marketToken);

Finalization rule: plurality. The leading ranking wins once total votes reach MIN_QUORUM() (2) and there is no tie. If the quorum is missed or the vote is tied, finalizeMarket() does not revert: it opens a new voting round (currentRound goes up, event VoteReopened) and the bonds stay locked.

Phase 4: Finalization and Payout

After the voting period and the veto window, anyone can call:

// Uncontested path (challenge period elapsed, no dispute)
await client.resolver.finalizeUncontested(marketToken);

// Contested path (voting and veto periods elapsed)
await client.resolver.finalizeMarket(marketToken);

A strictly leading EARLY vote that has reached the quorum may be finalized sooner, after EARLY_FAST_PERIOD (1 hour) instead of the full voting and veto wait.

Bond outcomes after finalization:

ScenarioBonds
Uncontestedthe proposer gets their own bond back
Contested, the proposal's or the dispute's ranking winsthat side gets its own bond back, plus the loser's bond up to twice its own bond (so at most three times its own bond in total); anything above that goes to insurance
Contested, a third ranking winsboth bonds go to insurance
EARLY winsif the dispute was EARLY, the disputer gets their own bond back plus half the proposer's bond (same cap) and the other half is forfeited; if EARLY won only through the votes, both bonds are forfeited. The round counter goes up, the next proposal bond doubles, and betting reopens only on an open-ended market
Vetoed, the team rules for the proposal's / the dispute's rankingthat side is paid (same cap); the veto bond is forfeited
Vetoed, the team rules for the veto's rankingthe vetoer gets their own bond plus a quarter of the losers' bonds, capped at twice their own
Vetoed, the team rules something elseevery bond is forfeited

Voters receive nothing. Nothing is claimed afterwards: bonds move inside the finalize call. Winning bettors then redeem their shares (Module 08).

Post-resolution selling note: On Basis, mass selling after resolution pushes the price up (in pair-asset units): slippage stays in the pool, so the price rises. Each sale still receives less than the quoted price, more so on a large sale. This is Stable+ mechanics in action. Patient sellers who wait through the initial sell wave exit at the highest price.


Special Outcomes

OutcomeIDWho Can Raise ItEffect
Normal0 to getNumOutcomes − 1 (at most MAX_OUTCOMES()), passed inside a rankingAnyoneStandard resolution: the tranche table pays the winners
EARLY253A dispute (disputeRankedEarly) or a panel vote (voteRankedEarly); the team on a vetoed market. Not a proposal, not a vetoResets the dispute: round increments, fresh proposal cycle begins, the next proposal bond doubles
INVALID254Anyone: the empty ranking (proposal, dispute, vote or veto)Each bettor is refunded their average cost
UNRESOLVED255InternalDefault state before any proposal

INVALID: Use when the market question is ambiguous, the event did not occur as defined, or resolution criteria are fundamentally broken. Each bettor is refunded their average cost.

EARLY: Use when the proposal came before the event was decided: effectively a do-over. Only a disputer can raise it (disputeRankedEarly), and the panel can vote for it. If EARLY wins, the market increments its round counter, the full proposal cycle restarts and the next proposal bond doubles.


Veto Mechanism

A veto opens at disputeStartTime + DISPUTE_PERIOD and lasts VETO_PERIOD (1 hour). Any wallet except the proposer and the disputer can veto once:

await client.resolver.vetoRanked(marketToken, ranking, tieMask);

Veto constraints:

  • Bond: the proposer's bond, at least VETO_FLOOR (100 units) and at most BOND_CAP (500 units), in the bond token
  • One veto per market
  • The proposer and the disputer cannot veto; the veto cannot name the proposal's or the dispute's ranking; there is no EARLY veto
  • Halts voting: the team then rules on the market (it may also rule EARLY)

2. Resolver SDK Methods

Full signatures and return types are catalogued in Module 18 SDK Reference under client.resolver. Each method sends the resolver call named beside it; the resolver's address is the resolver key of /contracts.json. No resolver call is payable.

Write Methods

propose(marketToken, outcomeId) / proposeRanked(marketToken, ranking, tieMask)

Proposes the result for a market that has passed its end time (the creator at endTime, anyone else 15 minutes later). propose sends proposeRanked(marketToken, [outcomeId], 0). Bond: requiredBond(marketToken, wallet), approved in the bond token.

ParamTypeDescription
marketTokenstringMarket token address
rankingnumber[]Outcome ids in rank order; empty = INVALID
tieMaskbigint0 = no ties

disputeRanked(marketToken, ranking, tieMask) / disputeRankedEarly(marketToken)

Disputes the proposal with a different ranking, or with EARLY. Bond: the proposer's bond, at most BOND_CAP. Not callable by the proposer. Opens the panel's vote.


voteRanked(marketToken, ranking, tieMask) / voteRankedEarly(marketToken)

Panel wallets only (authorizedVoter). One vote per round, until disputeStartTime + DISPUTE_PERIOD.


finalizeUncontested(marketToken)

Finalizes a market whose proposal was not disputed within the challenge period. Anyone can call this. The proposer gets the bond back.


finalizeMarket(marketToken)

Finalizes a disputed market after the voting period and the veto window. Anyone can call it. It reopens the round when the quorum is missing or the vote is tied; it does not revert for that.


vetoRanked(marketToken, ranking, tieMask)

Vetoes a disputed market after the voting period. Bond: the proposer's bond clamped between VETO_FLOOR and BOND_CAP. One veto per market. Not callable by the proposer or the disputer; cannot name the proposal's or the dispute's ranking, or EARLY. The team then rules on the market.


Resolver Read Methods

SDK methodContractReturns
isResolved(marketToken)resolvedboolean: true once finalized
getFinalOutcome(marketToken)finalOutcomeThe first pick of a ranked result; 254 = INVALID, 255 = unresolved. For the full ranking read getFinalRanking on the host
isInDispute(marketToken)inDisputeboolean: true from a dispute until finalization
isInVeto(marketToken)inVetoboolean: true once vetoed
getCurrentRound(marketToken)currentRoundnumber: increments on EARLY and on a reopened vote
getDisputeData(marketToken)disputesproposer, disputer, vetoer, originalKey, disputedKey, vetoKey, proposerBond, disputerBond, vetoBond, disputeStartTime; the three keys are bytes32. There is no proposalEndTime and no bountyPool: the challenge period ends at disputeStartTime + PROPOSAL_PERIOD
requiredBond(marketToken, wallet)requiredBondThe proposal bond for that wallet now
hasProposal(marketToken)hasProposalboolean
isVoter(wallet)authorizedVoterboolean: whether the wallet is on the panel
getVoteCount(marketToken, round, key)voteCountVotes for one ranking key in a round
getTally(marketToken, round)talliesleader, leaderCount, totalVotes, tie
hasVoted(marketToken, round, voter)hasVotedboolean
getVoterChoice(marketToken, round, voter)voterChoiceA bytes32 key; decode it with getRanking(marketToken, key)
getRanking(marketToken, key)getRankingThe ranking and tie mask behind a key
getConstants()the constants belowOne read of every constant

The vote key of a ranking is rankingKey(ranking) when tieMask is 0.


Resolver Configuration Constants

Read the live values from the contract at runtime: do not hardcode.

GetterValue at deployDescription
PROPOSAL_PERIOD2 hoursChallenge window after a proposal. This is when disputes can be filed.
DISPUTE_PERIOD24 hoursVoting window after a dispute is raised. Despite the name, this is the voting window, not the filing window.
VETO_PERIOD1 hourVeto window after the voting period
EARLY_FAST_PERIOD1 hourFast lane for a strictly leading EARLY vote that has the quorum
PROPOSAL_BOND5 × BOND_UNITBase proposal bond
BOND_CAP500 × BOND_UNITCeiling of the dispute and veto bonds
BOND_MAX8,000 × BOND_UNITCeiling of the proposal bond
VETO_FLOOR100 × BOND_UNITFloor of the veto bond
MIN_QUORUM2Minimum votes for finalizeMarket to take the leader
BOND_TOKEN-The token every bond is posted in
BOND_UNIT-10 ** BOND_TOKEN.decimals()

Read the live values from the contract at runtime.


3. Private Market Resolution

Private markets use a completely different resolution system: the market's own voters decide, not the resolver. The predictionStatus field from the API applies to both hosts; a private market goes active → awaiting_proposal → voting → awaiting_finalize → resolved or invalidated. To identify a private market, check predictionType === "private" in the API response.

How Private Resolution Works

Private markets (host: privateMarket in /contracts.json) are resolved by their creator's panel, not by the resolver. Voters call vote(marketToken, ranking, tieMask); an empty ranking means INVALID. Each voter votes once, and the vote is final. Votes open at the end time (at creation for an open-ended market). The first vote starts the 15-minute VOTING_WINDOW; finalize() is allowed once it has passed, and votes keep counting until finalize runs. The panel holds at most 11 voters including the creator; voter changes lock after the first vote. The result is a plurality. On an exact tie the creator's vote decides: it wins if it is among the leaders; the market is INVALID if the creator voted for something else; and finalize reverts, with no reason, while the creator has not voted. The first vote freezes AMM buys on an open-ended market. An invalid market has finalOutcome 254.

Private Market Methods

createMarketWithMetadata(options) Creates a private prediction market and registers IPFS metadata in one call. Requires SIWE authentication. For full token creation and prediction creation options, see those modules. On chain it sends createMarket(CreateParams, deadline, approvalSignature) on the private host: payable (the factory's creation fee) and, while gatingEnabled() is true, signed by the moderator. The signature covers the name, symbol, outcomes and the exact end time; the platform's market-approval step refuses end times too far out.

OptionTypeRequiredDescription
marketNamestringyesMarket question/title
symbolstringyesMarket token symbol
endTimebigint/intyesUnix timestamp when market closes
optionNamesstring[]yesOutcome names array
trancheBPnumber[]noPayout table: 1 to 10 non-increasing slices summing to 10000; default [10000] (winner-take-all)
maintokenstringyesThe market's main
privateEventbooleannoIf true, restricts buying to whitelisted addresses only. It also picks the seed floor: minSeedPrivate() when true, minSeedPublic() when false
seedAmountbigint/intnoSeed in the main's pair asset, the creator's own stake in the market (default: 0). The floor is minSeedPublic() / minSeedPrivate() × the main's unit, in steps of seedStep: set per main, read TrancheReader.sizesFor(host, main, n). It is not deposited liquidity. See Module 08 for seed rules.
descriptionstringnoMarket description
imageUrlstringnoAuto-resized to 512x512 WebP
frozenbooleannoStart frozen (default: false)
bondingbigint/intnoPresale size of the market token, in whole tokens (default: 0)

Returns: { hash, receipt, marketTokenAddress, imageUrl, metadata }


Private market write methods:

MethodDescription
vote(marketToken, ranking, tieMask)Cast the one, final ranked vote (creator and listed voters only); empty ranking = INVALID
finalize(marketToken)Finalize, from firstVoteTime + VOTING_WINDOW (15 minutes after the first vote)
manageVoter(marketToken, voter, add)Add (add=true) or remove (add=false) a voter. Only before the first vote; at most 11 voters including the creator
togglePrivateEventBuyers(marketToken, buyers, status)Whitelist (status=true) or unwhitelist (status=false) buyer addresses. buyers is an address array.
disableFreeze(marketToken)Open market to public
manageWhitelist(marketToken, wallets, amount, tag, status)Add/remove wallets from frozen market whitelist. amount = the cap in market tokens per wallet, tag = label.

Private market read methods:

MethodReturns
getMarketData(marketToken)Market data struct
getNumOutcomes(marketToken)bigint/int
getOutcome(marketToken, outcomeId)Outcome struct
getUserShares(marketToken, user, outcomeId)bigint/int
hasBettedOnMarket(marketToken, user)boolean
getBuyOrderCost(marketToken, orderId, fill)Cost to buy an order
getBuyOrderAmountsOut(marketToken, orderId, amount)Amounts out for an amount of the market's pair asset
getMarketOrders(marketToken, orderId)Order details (contract marketOrders)
getNextOrderId(marketToken)bigint/int (contract nextOrderId)
canUserBuy(marketToken, user)boolean: private event buyer check (contract userCanBuyEvent)
isMarketVoter(marketToken, voter)boolean
getVoters(marketToken)address[]: the panel
getVoterChoice(marketToken, voter){ key, ranking, tieMask }: contract voterKey returns a bytes32 key, decoded with getRanking(marketToken, key)
getRanking(marketToken, key)The ranking and tie mask behind a key
getFirstVoteTime(marketToken)bigint/int: timestamp of first vote (contract firstVoteTime)
getFinalKey(marketToken)The winning key (contract finalKey)
getTrancheBP(marketToken)The payout table
getFinalRanking(marketToken)The final ranking
quoteScenario(marketToken, ranking, tieMask, user)Market tokens a ranking would pay the user
quoteRedeem(marketToken, user)Market tokens the user can redeem now
getPendingPayout(marketToken, user)Contract pendingPayout
VOTING_WINDOW()The voting window (15 minutes)
getInitialReserves(numOutcomes)Starting depth for a market with that many outcomes; the sizes in force come from TrancheReader.sizesFor

No view returns the current tally: read getVoters, each voter's key and getFirstVoteTime, and apply the rule above.


4. Bond Economics

Proposing and disputing are bonded actions; nothing pays beyond the bonds, and voting is for panel wallets only. For integration with other prediction plays (e.g., Full Stack Creator), see Module 13.

When to Propose

Economics of proposing:

  • Cost: requiredBond(market, you) in the bond token (locked for the challenge period) + gas
  • Reward (uncontested): exactly your bond back
  • Risk: if someone disputes and the panel confirms their ranking, you lose your bond to them (they take up to twice their own; the rest goes to insurance)

When to propose:

  1. predictionStatus === "awaiting_proposal": market is ready
  2. You have high confidence in the outcome (outcome is unambiguous, on-chain verifiable, or widely observed)
  3. You need the market resolved: for example, you hold winning shares

Timing: the creator may propose at the end time, anyone else 15 minutes later.

Dispute Economics

  • Cost: the proposer's bond, at most 500 units, + gas
  • If your ranking wins: your own bond back, plus the proposer's bond up to twice your own
  • If a third ranking wins: both bonds go to insurance
  • If the proposal wins: you lose your bond to the proposer

Never dispute without evidence. You are putting your bond against the proposer's. The expected value is negative if you are guessing.

Veto Economics

A veto costs at least 100 bond units (at most 500) and, only if the team rules your ranking, returns your bond plus a quarter of the losers' bonds, capped at twice your own. It is a last resort, not a profit source.

The Panel

Only wallets on the panel (authorizedVoter) vote. Agents outside it cannot vote and earn nothing from votes.


5. Cross-Platform Arbitrage

This section covers the Basis prediction arb engine and how resolution timing creates specific execution windows. For combining arb with layered prediction strategies and capital stacking, see Modules 13 and 12.

The Structural Edge

On a winner-take-all market (getTrancheBP(market) = [10000]) the winners split the whole pot (all money from all outcomes), with no $1 cap; a podium market splits it by its tranche table across several ranks (Module 08). Read getTrancheBP before sizing. Every traditional prediction platform (Polymarket, Kalshi, Manifold) caps winning payouts at $1 per share. This difference is architectural and permanent.

The strategy: Use capped platforms for NO signals and price discovery. Use Basis for YES execution with uncapped upside.

Binary Market Arb

Setup: A binary market exists on both Basis and a capped platform. Favourite at 70% on the capped platform.

Position:

  • Buy YES on the favourite at the capped platform (e.g., 100 shares at 70c = $70 outlay; profit if favourite wins: $30)
  • Buy YES on the underdog at Basis (sized below the $30 profit margin, e.g., $20)

Outcomes:

ScenarioCapped platformBasisNet
Favourite wins+$30 profit-$20 loss+$10
Underdog wins-$70 lossPot split (e.g., $200+)+$110+

Total capital: $90. Can profit in both scenarios, before fees (winner-take-all markets; other ladders split by rank). The underdog scenario pays asymmetrically well because underdog winners split all the favourite's money. The Basis payout is sold through the AMM in slices when you redeem: quote it with TrancheReader.quoteRedeemUSDB(host, market, wallet) before relying on it.

Sizing Framework

Factor in trade fees and position sizing before executing either leg.

Variables:

  • P_fav = favourite YES price on capped platform (e.g., 0.70)
  • Profit_fav = 1 - P_fav = profit per share if favourite wins
  • N_fav = shares bought on capped platform
  • Stake_basis = Basis underdog stake

Constraints:

  • Favourite wins: (N_fav × Profit_fav) - Stake_basis > 0
  • Conservative rule: size Stake_basis at 50–70% of N_fav × Profit_fav

Factor in fees: a Basis bet pays getTaxRate(marketToken, you) once, on the way in: the 0.60% base (0.54% with a referrer) plus the market creator's fee. Include capped platform fees in net edge calculations.

Multi-Outcome Markets

A 10-outcome market provides 9 potential underdog arb entry points instead of 1. Arb agents should:

  1. Rank all outcomes by cross-platform spread (Basis implied probability vs capped platform probability)
  2. Identify the 2–3 outcomes with the widest mispricing
  3. Split the Basis allocation across them for diversification
  4. Maintain hedge sizing on the capped platform side for each leg

The volume flywheel: Underdog arb volume on Basis inflates the pot, which makes the favourite side more attractive (bigger pot + cheaper favourite shares). Favourite volume then makes underdogs more valuable. Each wave reinforces the other.

The Self-Correcting Mechanism

As arb volume flows into underdog outcomes on Basis, the underdog's implied probability rises. If enough volume accumulates, the "underdog" on Basis becomes the favourite, while the capped platform still prices it as the underdog. The arb flips: now buy the Basis "underdog" (capped platform favourite) on Basis and hedge with a NO on the capped platform.

Agents should monitor cross-platform implied probabilities continuously and rebalance when the spread exceeds ~5%.

Resolution Timing Plays

Resolution creates specific windows:

  1. Pre-resolution: Prices converge as informed participants position ahead of the known outcome. The arb spread narrows. Exit or reduce Basis positions if the outcome is highly certain and the pot premium no longer justifies the capital allocation.

  2. Resolution announcement window: Once the outcome is known (but before the market is finalized on-chain), the proposal window opens. Agents with live positions can propose to get the market resolved (15 minutes after the end time unless you created it): the bond (requiredBond) comes back when nobody disputes.

  3. Post-resolution: Redeem winning shares on Basis. Collect Polymarket winnings. The 24-hour post-resolution sell dynamic on Basis (price rises as sellers exit) means patient exit timing is rewarded.

  4. Disputed markets: A dispute restarts the clock: the panel votes for DISPUTE_PERIOD, then the veto window runs. Only panel wallets vote; holders wait for the result.

Data Sources

PlatformData method
PolymarketPublic API, GraphQL for real-time odds and order book depth
KalshiREST API (requires account)
Basisclient.api.getTokens(), client.marketReader.getAllOutcomes(), client.api.getOrders(), client.api.getPulse(): see Module 10 Portfolio/Info and Module 17 API reference

Agent Execution Flow

1. Identify matching markets across platforms 2. Compare implied probabilities: find spread 3. Determine which side is underpriced on Basis 4. Size position: Basis stake < capped platform profit margin × 50-70% 5. Execute both legs near-simultaneously to avoid slippage divergence 6. Monitor pot growth and odds movement 7. Rebalance or add to position if spread exceeds 5% 8. On resolution: propose outcome, redeem Basis winnings (`redeem(market, 10000, 1, minOut)`), collect capped platform winnings

Risk Factors

  • Pot dilution: More volume entering your outcome reduces your per-share payout. Monitor outcome share distribution.
  • Timing risk: Stale quotes between platforms erode edge. Execute both legs simultaneously.
  • Fee drag: the Basis bet fee (getTaxRate) + capped platform fees. Calculate net edge, not gross.
  • Liquidity mismatch: Thin liquidity on either side causes slippage. Use tranches for large orders.
  • Resolution mismatch: Verify both platforms resolve on identical criteria before entering. A market that resolves INVALID on Basis but normally on Polymarket destroys the hedge.
  • Settlement currency: Basis bets settle in the market's pair asset (USDG on the default main). Check that the other platform settles in a dollar too, or price the conversion into the edge.

6. Error Handling

For the complete error reverse index including error → module routing, see Module 18 §3.

Pre-Flight Checks

Before any resolution action, run these checks:

Before proposing:

// 1. Market must be past end time and awaiting proposal
//    (the creator may propose at endTime, anyone else from endTime + 15 minutes)
const market = await client.api.getToken(marketToken);
if (market.predictionStatus !== "awaiting_proposal") throw new Error("Not ready for proposal");
if (await client.resolver.hasProposal(marketToken)) throw new Error("Already proposed");

// 2. Check your bond-token balance (and allowance to the resolver)
const bond = await client.resolver.requiredBond(marketToken, walletAddress);
const { BOND_TOKEN } = await client.resolver.getConstants();
const balance = await client.getBalance(walletAddress, BOND_TOKEN);
if (balance < bond) throw new Error("Insufficient bond token for the bond");

// 3. Verify the outcome IDs (a host view: the reader has no getNumOutcomes)
const n = await client.predictionMarkets.getNumOutcomes(marketToken);
if (outcomeId < 0 || outcomeId > Number(n) - 1) throw new Error("Invalid outcome ID");
// A ranked proposal has getTrancheBP(market).length entries (more with ties):
// check it with isValidRanking(marketToken, ranking, tieMask) on the host.

Before disputing:

// 1. Market must have an existing proposal
const disputeData = await client.resolver.getDisputeData(marketToken);
if (!disputeData.proposer) throw new Error("No proposal to dispute");

// 2. You are not the proposer, and your ranking differs from the proposal's
if (disputeData.proposer.toLowerCase() === walletAddress.toLowerCase()) throw new Error("Proposer cannot dispute self");

// 3. Challenge period must still be active
const { PROPOSAL_PERIOD } = await client.resolver.getConstants();
if (Date.now() / 1000 >= Number(disputeData.disputeStartTime) + Number(PROPOSAL_PERIOD)) throw new Error("Dispute period over");

// 4. Market must not already be in dispute
const inDispute = await client.resolver.isInDispute(marketToken);
if (inDispute) throw new Error("Already in dispute");

Before voting (panel wallets only):

// 1. Market must be in dispute/voting phase
const inDispute = await client.resolver.isInDispute(marketToken);
if (!inDispute) throw new Error("Not in voting phase");

// 2. Market must not be in veto
const inVeto = await client.resolver.isInVeto(marketToken);
if (inVeto) throw new Error("In veto phase: cannot vote");

// 3. Must be on the panel
if (!(await client.resolver.isVoter(walletAddress))) throw new Error("Only the authorized panel votes");

// 4. Must not have already voted in this round
const round = await client.resolver.getCurrentRound(marketToken);
const voted = await client.resolver.hasVoted(marketToken, round, walletAddress);
if (voted) throw new Error("Already voted in this round");

Before finalizing:

// Uncontested: now >= disputeStartTime + PROPOSAL_PERIOD
// Contested:   now >= disputeStartTime + DISPUTE_PERIOD + VETO_PERIOD
// Earlier calls revert "Wait more".
const round = await client.resolver.getCurrentRound(marketToken);
const t = await client.resolver.getTally(marketToken, round);   // leader, leaderCount, totalVotes, tie
// finalizeMarket does not revert on the tally: below MIN_QUORUM or tied, it opens a new round

Common Errors and Fixes

ErrorWhenFix
"Ranked market: use the *Ranked entrypoints"an old single-outcome (uint8) propose, dispute, vote or vetoUse proposeRanked, disputeRanked, voteRanked, vetoRanked (or the …Early calls).
"Bad ranking"any ranked callThe ranking is malformed. Check it with isValidRanking(market, ranking, tieMask) on the host.
"Too early to propose"proposeBefore the end time, or before end + 15 minutes for a non-creator. Wait.
"Already proposed"proposeMarket already has a pending proposal. Dispute it, or wait.
"Rules: not qualified"proposeThe resolver's optional proposal rule (off by default) refused this wallet. Another wallet can propose.
"No proposal to dispute"disputeNo proposal exists yet. Propose instead.
"Already in dispute"disputeMarket is already being disputed. Wait for the panel.
"Dispute period over"disputeThe challenge period has ended.
"Must be different"disputeYour ranking equals the proposal's.
"Proposer cannot dispute self"disputeAnother wallet must dispute.
"Not voting", "cannot vote during veto", "Disputed period ended", "Not an authorized voter", "Voted"voteOnly the panel votes, once per round, during the voting period.
"Wait more"either finalize callToo early. Wait for the periods above.
"Invalid market", "Already resolved", "Veto only allowed on disputed/voting markets", "Veto only allowed once", "Already vetoed", "Cant veto propose dispute outcome", "Veto window not started yet", "Party to the dispute cannot veto", "Cannot veto back to the proposal"vetoCheck the veto rules in §1.
"Betting closed", "Outcome proposed"a bet on the public host after the end time or after a proposal (the private host reverts with no reason)The market no longer takes bets.
"fraction 1-10000", "Nothing to redeem", "slices 1-25", "Below minOut"redeemFix the arguments; quote with quoteRedeem first.
"Approval expired", "Signer unset", "Approval already used", "Invalid approval"createRequest a fresh moderator approval. It covers the name, symbol, outcomes and end time: pass them unchanged.
"empty order", "pair unit", "dust fill", "max cost"order bookSee Module 08.
(no reason string)market already resolved; minShares not met; seed below the floor or not a step of seedStep; bad endTime (it must be 0, or at least 1 minute ahead); filling your own order with buyOrder (a buyOrdersAndContract sweep skips it)These checks revert with no reason string. Check the state first.

Recovery Flows

Stuck on finalization (tie or missed quorum):

  1. finalizeMarket opened a new round instead of resolving (currentRound went up)
  2. Wait for the panel to vote again; agents outside the panel cannot vote

Veto escalation:

  1. isInVeto() returns true
  2. No further action available to agents: the team rules on the market
  3. Monitor isResolved(): bonds were paid inside the ruling; nothing to claim. Redeem winning shares.

A wrong proposal:

  1. You proposed an incorrect outcome
  2. You cannot dispute yourself (Proposer cannot dispute self). Another wallet must dispute; your bond is at risk.

EARLY outcome recovery:

  1. If EARLY wins, the dispute resets: getCurrentRound() increments and the next proposal bond is double
  2. A fresh proposal cycle begins from Phase 1
  3. Re-engage the proposal flow from the beginning

7. Worked Example: Full Resolver Workflow

import { BasisClient } from 'basis-sdk-js';

async function resolverWorkflow() {
  const apiDomain = "<the host you are using: the value you pass as apiDomain>";
  const client = await BasisClient.create({
    privateKey: process.env.BASIS_PRIVATE_KEY,
    apiDomain,
  });
  const wallet = client.walletClient.account.address;

  // 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");
  console.log(`Found ${needsProposal.length} markets needing proposals`);

  if (needsProposal.length === 0) return;

  const market = needsProposal[0];
  const marketToken = market.address;

  // 2. Check the market's outcomes to determine which won
  // Fetch the host address dynamically: do not hardcode (see Module 17 for contracts.json).
  // This example handles public markets: their host is the marketTrading key.
  const addresses = await fetch(`${apiDomain}/contracts.json`).then(r => r.json());
  const host = addresses.marketTrading;
  const outcomes = await client.marketReader.getAllOutcomes(host, marketToken);
  for (const o of outcomes) {
    const prob = Number(o.probability) / 100; // basis points → percent
    console.log(`  Outcome ${o.outcomeId}: "${o.name}": ${prob.toFixed(1)}%`);
  }

  // 3. Pre-flight: the bond, in the resolver's bond token
  const bond = await client.resolver.requiredBond(marketToken, wallet);
  console.log("Bond required:", bond);

  // 4. Propose the winning outcome (sends proposeRanked(market, [id], 0))
  const winningOutcomeId = 0; // ← your determination of which outcome won
  const proposeResult = await client.resolver.propose(marketToken, winningOutcomeId);
  console.log("Proposed outcome:", winningOutcomeId, "tx:", proposeResult.hash);

  // 5. Read the challenge period end time
  const disputeData = await client.resolver.getDisputeData(marketToken);
  const { PROPOSAL_PERIOD } = await client.resolver.getConstants();
  console.log("Challenge period ends:", new Date((Number(disputeData.disputeStartTime) + Number(PROPOSAL_PERIOD)) * 1000));

  // 6. Wait for the challenge period (read PROPOSAL_PERIOD at runtime, never hardcode)
  // await sleep(Number(PROPOSAL_PERIOD) * 1000);

  // 7a. Uncontested path
  if (!(await client.resolver.isInDispute(marketToken))) {
    const finalizeResult = await client.resolver.finalizeUncontested(marketToken);
    console.log("Finalized uncontested. Bond returned. Tx:", finalizeResult.hash);
  } else {
    // 7b. A dispute was filed. Only the panel votes.
    // After DISPUTE_PERIOD + VETO_PERIOD anyone may call finalizeMarket.
    // await sleep(...);
    const result = await client.resolver.finalizeMarket(marketToken);
    console.log("finalizeMarket sent (it reopens the round on a tie or a missed quorum):", result.hash);
  }
}

resolverWorkflow().catch(console.error);

Key timing reminders:

  • PROPOSAL_PERIOD (challenge window), DISPUTE_PERIOD (voting window), VETO_PERIOD and EARLY_FAST_PERIOD: read every period from the resolver at runtime. They can change. Never hardcode.

Summary Reference

ActionMethodBondReward
Propose outcomepropose(market, id) / proposeRanked(market, ranking, tieMask)requiredBond(market, you)Own bond back; plus the disputer's bond (capped) if contested and right
Dispute proposaldisputeRanked(market, ranking, tieMask) / disputeRankedEarly(market)The proposer's bond, at most 500 unitsOwn bond plus the proposer's, capped at twice your own, if right
Vote on disputevoteRanked / voteRankedEarly (panel only)NoneNone
Finalize uncontestedfinalizeUncontested(market)0Triggers the bond payout
Finalize after votefinalizeMarket(market)0Triggers the bond payouts
VetovetoRanked(market, ranking, tieMask)100 to 500 unitsOwn bond plus a quarter of the losers', if the team rules your ranking; the team resolves

See Also