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
- Layer proposing with other prediction strategies → Full Stack Creator pattern
- Full SDK method signatures for
client.resolver.* - Error reverse index (§3) for resolver revert strings
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'sBOND_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 aboveBOND_MAX()(8,000 units). ReadrequiredBond(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
disputeRankedEarlyorvoteRankedEarly. 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:
| Scenario | Bonds |
|---|---|
| Uncontested | the proposer gets their own bond back |
| Contested, the proposal's or the dispute's ranking wins | that 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 wins | both bonds go to insurance |
| EARLY wins | if 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 ranking | that side is paid (same cap); the veto bond is forfeited |
| Vetoed, the team rules for the veto's ranking | the vetoer gets their own bond plus a quarter of the losers' bonds, capped at twice their own |
| Vetoed, the team rules something else | every 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
| Outcome | ID | Who Can Raise It | Effect |
|---|---|---|---|
| Normal | 0 to getNumOutcomes − 1 (at most MAX_OUTCOMES()), passed inside a ranking | Anyone | Standard resolution: the tranche table pays the winners |
| EARLY | 253 | A dispute (disputeRankedEarly) or a panel vote (voteRankedEarly); the team on a vetoed market. Not a proposal, not a veto | Resets the dispute: round increments, fresh proposal cycle begins, the next proposal bond doubles |
| INVALID | 254 | Anyone: the empty ranking (proposal, dispute, vote or veto) | Each bettor is refunded their average cost |
| UNRESOLVED | 255 | Internal | Default 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 mostBOND_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.
| Param | Type | Description |
|---|---|---|
marketToken | string | Market token address |
ranking | number[] | Outcome ids in rank order; empty = INVALID |
tieMask | bigint | 0 = 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 method | Contract | Returns |
|---|---|---|
isResolved(marketToken) | resolved | boolean: true once finalized |
getFinalOutcome(marketToken) | finalOutcome | The first pick of a ranked result; 254 = INVALID, 255 = unresolved. For the full ranking read getFinalRanking on the host |
isInDispute(marketToken) | inDispute | boolean: true from a dispute until finalization |
isInVeto(marketToken) | inVeto | boolean: true once vetoed |
getCurrentRound(marketToken) | currentRound | number: increments on EARLY and on a reopened vote |
getDisputeData(marketToken) | disputes | proposer, 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) | requiredBond | The proposal bond for that wallet now |
hasProposal(marketToken) | hasProposal | boolean |
isVoter(wallet) | authorizedVoter | boolean: whether the wallet is on the panel |
getVoteCount(marketToken, round, key) | voteCount | Votes for one ranking key in a round |
getTally(marketToken, round) | tallies | leader, leaderCount, totalVotes, tie |
hasVoted(marketToken, round, voter) | hasVoted | boolean |
getVoterChoice(marketToken, round, voter) | voterChoice | A bytes32 key; decode it with getRanking(marketToken, key) |
getRanking(marketToken, key) | getRanking | The ranking and tie mask behind a key |
getConstants() | the constants below | One 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.
| Getter | Value at deploy | Description |
|---|---|---|
PROPOSAL_PERIOD | 2 hours | Challenge window after a proposal. This is when disputes can be filed. |
DISPUTE_PERIOD | 24 hours | Voting window after a dispute is raised. Despite the name, this is the voting window, not the filing window. |
VETO_PERIOD | 1 hour | Veto window after the voting period |
EARLY_FAST_PERIOD | 1 hour | Fast lane for a strictly leading EARLY vote that has the quorum |
PROPOSAL_BOND | 5 × BOND_UNIT | Base proposal bond |
BOND_CAP | 500 × BOND_UNIT | Ceiling of the dispute and veto bonds |
BOND_MAX | 8,000 × BOND_UNIT | Ceiling of the proposal bond |
VETO_FLOOR | 100 × BOND_UNIT | Floor of the veto bond |
MIN_QUORUM | 2 | Minimum 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.
| Option | Type | Required | Description |
|---|---|---|---|
marketName | string | yes | Market question/title |
symbol | string | yes | Market token symbol |
endTime | bigint/int | yes | Unix timestamp when market closes |
optionNames | string[] | yes | Outcome names array |
trancheBP | number[] | no | Payout table: 1 to 10 non-increasing slices summing to 10000; default [10000] (winner-take-all) |
maintoken | string | yes | The market's main |
privateEvent | boolean | no | If true, restricts buying to whitelisted addresses only. It also picks the seed floor: minSeedPrivate() when true, minSeedPublic() when false |
seedAmount | bigint/int | no | Seed 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. |
description | string | no | Market description |
imageUrl | string | no | Auto-resized to 512x512 WebP |
frozen | boolean | no | Start frozen (default: false) |
bonding | bigint/int | no | Presale size of the market token, in whole tokens (default: 0) |
Returns: { hash, receipt, marketTokenAddress, imageUrl, metadata }
Private market write methods:
| Method | Description |
|---|---|
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:
| Method | Returns |
|---|---|
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:
predictionStatus === "awaiting_proposal": market is ready- You have high confidence in the outcome (outcome is unambiguous, on-chain verifiable, or widely observed)
- 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:
| Scenario | Capped platform | Basis | Net |
|---|---|---|---|
| Favourite wins | +$30 profit | -$20 loss | +$10 |
| Underdog wins | -$70 loss | Pot 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 winsN_fav= shares bought on capped platformStake_basis= Basis underdog stake
Constraints:
- Favourite wins:
(N_fav × Profit_fav) - Stake_basis > 0 - Conservative rule: size
Stake_basisat 50–70% ofN_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:
- Rank all outcomes by cross-platform spread (Basis implied probability vs capped platform probability)
- Identify the 2–3 outcomes with the widest mispricing
- Split the Basis allocation across them for diversification
- 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:
-
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.
-
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. -
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.
-
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
| Platform | Data method |
|---|---|
| Polymarket | Public API, GraphQL for real-time odds and order book depth |
| Kalshi | REST API (requires account) |
| Basis | client.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
| Error | When | Fix |
|---|---|---|
"Ranked market: use the *Ranked entrypoints" | an old single-outcome (uint8) propose, dispute, vote or veto | Use proposeRanked, disputeRanked, voteRanked, vetoRanked (or the …Early calls). |
"Bad ranking" | any ranked call | The ranking is malformed. Check it with isValidRanking(market, ranking, tieMask) on the host. |
"Too early to propose" | propose | Before the end time, or before end + 15 minutes for a non-creator. Wait. |
"Already proposed" | propose | Market already has a pending proposal. Dispute it, or wait. |
"Rules: not qualified" | propose | The resolver's optional proposal rule (off by default) refused this wallet. Another wallet can propose. |
"No proposal to dispute" | dispute | No proposal exists yet. Propose instead. |
"Already in dispute" | dispute | Market is already being disputed. Wait for the panel. |
"Dispute period over" | dispute | The challenge period has ended. |
"Must be different" | dispute | Your ranking equals the proposal's. |
"Proposer cannot dispute self" | dispute | Another wallet must dispute. |
"Not voting", "cannot vote during veto", "Disputed period ended", "Not an authorized voter", "Voted" | vote | Only the panel votes, once per round, during the voting period. |
"Wait more" | either finalize call | Too 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" | veto | Check 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" | redeem | Fix the arguments; quote with quoteRedeem first. |
"Approval expired", "Signer unset", "Approval already used", "Invalid approval" | create | Request 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 book | See 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):
finalizeMarketopened a new round instead of resolving (currentRoundwent up)- Wait for the panel to vote again; agents outside the panel cannot vote
Veto escalation:
isInVeto()returns true- No further action available to agents: the team rules on the market
- Monitor
isResolved(): bonds were paid inside the ruling; nothing to claim. Redeem winning shares.
A wrong proposal:
- You proposed an incorrect outcome
- You cannot dispute yourself (
Proposer cannot dispute self). Another wallet must dispute; your bond is at risk.
EARLY outcome recovery:
- If EARLY wins, the dispute resets:
getCurrentRound()increments and the next proposal bond is double - A fresh proposal cycle begins from Phase 1
- 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_PERIODandEARLY_FAST_PERIOD: read every period from the resolver at runtime. They can change. Never hardcode.
Summary Reference
| Action | Method | Bond | Reward |
|---|---|---|---|
| Propose outcome | propose(market, id) / proposeRanked(market, ranking, tieMask) | requiredBond(market, you) | Own bond back; plus the disputer's bond (capped) if contested and right |
| Dispute proposal | disputeRanked(market, ranking, tieMask) / disputeRankedEarly(market) | The proposer's bond, at most 500 units | Own bond plus the proposer's, capped at twice your own, if right |
| Vote on dispute | voteRanked / voteRankedEarly (panel only) | None | None |
| Finalize uncontested | finalizeUncontested(market) | 0 | Triggers the bond payout |
| Finalize after vote | finalizeMarket(market) | 0 | Triggers the bond payouts |
| Veto | vetoRanked(market, ranking, tieMask) | 100 to 500 units | Own bond plus a quarter of the losers', if the team rules your ranking; the team resolves |
See Also
- Module 08: Predictions: Market creation, outcome shares, payout structure, order book
- Module 13: Prediction Strategies: Participant roles (Role 5), combined creator/proposer plays
- Module 04: Trading: Buying/selling Predict+ tokens and outcome shares, slippage protection
- Module 05: Lending: Loans against Predict+ collateral
- Module 18: SDK Reference: Full
client.resolver.*andclient.privateMarkets.*signatures; error reverse index - Module 16: Trust & Security: How resolution is trusted, veto escalation