Polymarket Bot Tutorial · Chapter 18 of 32
UMA dispute prediction bots on Polymarket: detect Optimistic Oracle proposals, predict dispute likelihood, exploit pre-and-post-dispute price asymmetry, and avoid disputed-market death spirals.
What this chapter covers
UMA's Optimistic Oracle resolves Polymarket markets, and disputes create price anomalies before and after they fire. Tradeable patterns exist on both sides of a dispute, but the strategy is operationally complex and has burned more bots than it has fed. This chapter is the honest playbook.
- How UMA Optimistic Oracle works
- Detecting a proposal on-chain
- Dispute predictors (volume, ambiguity, history)
- Pre-dispute price asymmetry
- Post-dispute trade setups
- When NOT to trade disputed markets
- Code: subscribe to UMA proposed/disputed events
How UMA Optimistic Oracle works
UMA's Optimistic Oracle (OO) is the dispute resolution layer for Polymarket. Every market resolution goes through OO; most are uncontested and settle automatically. The contested ones - disputes - trigger a 24-72 hour voting period during which UMA token holders decide the outcome.
The lifecycle: Polymarket's resolver proposes a price (0 = NO won, 1 = YES won). After a 2-hour challenge window, if no one disputes, the price is finalized and the CTF contract distributes payouts. If someone disputes, the market enters a voting window; UMA holders cast votes, the majority wins.
For a bot, the relevant events are ProposePrice (proposal entered, challenge window opens) and DisputePrice (dispute filed, voting period begins). Subscribe to these to track market resolution state in real time.
Detecting a proposal on-chain
The UMA OO contract on Polygon emits a ProposePrice event with the parameters (requester, identifier, timestamp, ancillaryData, proposer, proposedPrice). Filter by Polymarket's known requester address to limit to relevant proposals.
POLY_REQUESTER = "0x..." # Polymarket Adjudicator
filt = oo_contract.events.ProposePrice.create_filter(
fromBlock="latest",
argument_filters={"requester": POLY_REQUESTER}
)
for event in filt.get_new_entries():
market_id = decode_ancillary(event.args.ancillaryData)
proposed = "YES" if event.args.proposedPrice == 1e18 else "NO"
print(f"PROPOSE: market {market_id} → {proposed}")
The ancillaryData field is hex-encoded JSON describing the market question. Decoding it gives you the market identifier you can cross-reference against your open positions.
Dispute predictors (volume, ambiguity, history)
Three pre-dispute signals correlate with later actual disputes.
- Total volume: markets with > $1M in lifetime volume are disputed at 4x the rate of small markets. More capital at stake = more incentive to challenge.
- Ambiguous wording: any market with "or similar," "officially confirmed," or compound conditions (date AND specific outcome) has elevated dispute rates.
- Past disputes on the same event: if an earlier proposal was already disputed and re-proposed, the second proposal is disputed at 2-3x normal rate.
A bot can compute a "dispute probability" score from these features and avoid taking positions in markets above a threshold close to resolution.
Pre-dispute price asymmetry
In the hours before a likely dispute, the market price often shows asymmetric movement: the side that the proposer named as YES drifts down (because traders fear a dispute will flip it), the other side drifts up.
If you have a directional view on which way the dispute will resolve, this is a tradeable window. The risk: if the dispute does not happen, the asymmetry reverses when the challenge window closes uneventfully and prices snap back to the proposed direction.
Honest: most pre-dispute asymmetry trades are losing trades because most challenges resolve in favor of the original proposal. The strategy only works when you have specific information about why this dispute is likely to be sustained.
Post-dispute trade setups
After a dispute is filed, the market trades for 24-72 hours in "limbo" - known to be disputed, outcome to be voted. Two setups exist.
Convergence to UMA consensus: if the dispute resolution is signaled early (e.g. a prominent UMA voter publicly takes a side), the price moves toward that resolution. A bot watching UMA Discord / Twitter signals + price action can catch this 30-60% of the time.
Volatility farming: limbo periods have wide spreads. A patient market maker can earn the spread tax across multiple traders rotating in and out during the voting window. Inventory risk is high; size accordingly.
Both require comfort with the genuine possibility of resolution against your position. Treat dispute-period inventory as half-size at most.
When NOT to trade disputed markets
Three situations where the dispute trade is wrong by default.
- You do not have a UMA-specific view. If your only edge is "the original proposal looks correct to me," you have no edge over the original proposer - and the dispute filer thinks the opposite. The voting outcome is a coin flip you cannot predict.
- The dispute is on an ambiguous wording. UMA voters generally side with strict-reading-of-the-question. If the market said "by January 31" and the event happened February 1, UMA will resolve NO regardless of the trader population's intuition.
- You hold inventory from before the dispute. Adding to an existing position to "average down" through limbo is the classic capital-destruction pattern. Hold or exit, never add.
Code: subscribe to UMA proposed/disputed events
Reference: WebSocket subscription to UMA OO events, filtered by Polymarket requester.
from web3 import Web3
w3 = Web3(Web3.WebsocketProvider(POLYGON_WSS))
oo = w3.eth.contract(address=UMA_OO_ADDR, abi=UMA_OO_ABI)
POLY = "0x...".lower()
dispute_filter = oo.events.DisputePrice.create_filter(fromBlock="latest")
propose_filter = oo.events.ProposePrice.create_filter(fromBlock="latest")
while True:
for event in dispute_filter.get_new_entries():
if event.args.requester.lower() == POLY:
on_dispute(event)
for event in propose_filter.get_new_entries():
if event.args.requester.lower() == POLY:
on_propose(event)
time.sleep(2)
def on_dispute(event):
market_q = decode_ancillary_to_question(event.args.ancillaryData)
send_telegram(f"DISPUTE: {market_q}")
# If we hold a position in this market, alert + consider exit
if market_q in our_positions:
flag_position_for_review(market_q)
The pattern: subscribe, decode, alert. Acting on disputes algorithmically is high-risk; the bot's job is usually to surface the event to a human reviewer.





