Polymarket Bot Tutorial · Chapter 9 of 32
Read Polymarket on-chain data directly: USDC/pUSD balances, CTF contract reads for outcome supply, UMA Optimistic Oracle proposed/disputed events, and Polygon transaction logs - with code.
What this chapter covers
Polymarket's APIs are convenient but eventually consistent. The chain is authoritative. This chapter walks the on-chain reads that a production bot uses to verify its own bookkeeping: pUSD balances, outcome-token inventory, UMA dispute events, and CTF contract state. The pattern most production bots converge on is API-first for speed plus periodic on-chain reconciliation for correctness.
- What lives on-chain (vs in CLOB)
- pUSD contract address and ABI
- Conditional Tokens Framework (CTF)
- UMA Optimistic Oracle: proposed and disputed events
- Reading Polygon event logs (web3.py / ethers)
- When to read on-chain vs trust the API
- Code: detect a UMA dispute via event subscription
What lives on-chain (vs in CLOB)
Your bot is really watching two separate systems that each hold part of the truth: the blockchain and Polymarket's own API. It helps to know exactly what each one is authoritative for.
On-chain (Polygon): pUSD balances, outcome-token inventory (ERC-1155 supply per token), allowance approvals, UMA Optimistic Oracle proposals and disputes, deposit and withdrawal events. Eventually correct; latency is one Polygon block (~2 seconds).
CLOB (Polymarket API): order book, recent trades, pending limit orders, match acknowledgments. Real-time but eventually consistent - a match is acknowledged before the ERC-1155 settles, which produces the phantom-fill problem covered in chapter 12.
The two should always converge. When they diverge, the chain is authoritative. A bot that trusts only the CLOB will drift; a bot that trusts only the chain will trade slowly. Production code uses both: CLOB for speed-critical decisions, chain for periodic reconciliation.
pUSD contract address and ABI
pUSD is Polymarket's collateral token, introduced with the V2 platform upgrade in April 2026. The contract at 0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB on Polygon mainnet is a standard ERC-20 (its on-chain symbol is "pUSD", which you can confirm on Polygonscan).
Three reads that matter for a bot:
balanceOf(proxy)- your spendable pUSD. Compare against the CLOB's view of your balance on every restart.allowance(proxy, exchange_contract)- whether the CTF/NegRisk exchange contracts can spend your pUSD. Required for order matching.Transferevent subscription - detects deposits and withdrawals without polling.
USDC (0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174) remains the off-ramp pair. Most bots only need pUSD reads; USDC matters only during deposit/withdrawal cycles.
Conditional Tokens Framework (CTF)
Outcome shares are ERC-1155 tokens minted by Gnosis's Conditional Tokens Framework (CTF). The CTF contract on Polygon at 0x4D97DCd97eC945f40cF65F87097ACe5EA0476045 tracks per-position-id supply.
Three reads:
balanceOf(proxy, position_id)- how many outcome tokens you actually hold for that market+outcome.getOutcomeSlotCount(condition_id)- number of outcomes (2 for binary, N for multi-outcome).payoutNumerators,payoutDenominator- set when UMA resolves the market. Reading these tells you which side won before the CLOB UI updates.
The position_id is a hash of (condition_id, outcome_index). Compute it client-side via the CTF's getPositionId helper (after getConditionId → getCollectionId) or replicate the keccak math in your stack.
Three writes - the capital primitives. The same CTF contract moves collateral without the order book:
splitPosition(collateral, parentCollectionId, conditionId, partition, amount)- lock pUSD and mint a full set of outcome tokens (1 Yes + 1 No per $1).mergePositions(collateral, parentCollectionId, conditionId, partition, amount)- burn a full set back into pUSD, no resolution wait.redeemPositions(collateral, parentCollectionId, conditionId, indexSets)- after UMA resolves (thepayoutNumeratorsabove are set), exchange winning tokens for $1.00 pUSD each.
These power CTF basis arbitrage (buy Yes+No under $1 → mergePositions; split over $1 → sell both) and clean redemption - see the CTF primitives in the API reference and the basis-arb section.
UMA Optimistic Oracle: proposed and disputed events
UMA's Optimistic Oracle (OO) handles all Polymarket dispute resolution. Two events your bot may want to subscribe to.
ProposePrice(requester, identifier, timestamp, ancillaryData, proposer, proposedPrice)- fired when a Polymarket bot proposes an outcome.DisputePrice(requester, identifier, timestamp, ancillaryData, disputer)- fired when someone challenges the proposed outcome.
OO contract address: 0xeE3Afe347D5C74317041E2618C49534dAf887c24. Filter by Polymarket's requester address.
Why subscribe: a dispute means the resolution is now contested and will require a 24-72h UMA vote. During that window, the market may be paused for trading. A bot holding positions on a disputed market should know immediately.
Reading Polygon event logs (web3.py / ethers)
Two reference implementations.
Python (web3.py):
from web3 import Web3
w3 = Web3(Web3.HTTPProvider(POLYGON_RPC))
ctf = w3.eth.contract(address=CTF_ADDR, abi=CTF_ABI)
filter = ctf.events.PayoutRedemption.create_filter(fromBlock="latest")
for event in filter.get_new_entries():
print(event["args"])
Node (ethers v6):
import { ethers } from "ethers";
const p = new ethers.JsonRpcProvider(POLYGON_RPC);
const ctf = new ethers.Contract(CTF_ADDR, CTF_ABI, p);
ctf.on("PayoutRedemption", (redeemer, collateral, parentId, conditionId) => {
console.log("redemption", { redeemer, conditionId });
});
For continuous monitoring use WebSocket transport (wss://...) instead of HTTP polling - fewer requests and faster delivery. Most paid RPC providers include WebSocket on entry tiers.
When to read on-chain vs trust the API
Practical rules from production.
- Trust the API for: real-time order book, recent trades, your own pending orders, market metadata (slug, question, end date), event/market discovery.
- Trust the chain for: your own pUSD balance, your own outcome-token inventory, deposit and withdrawal verification, resolution outcomes, dispute state.
- Cross-check both for: anything financial that the bot recorded as a fill - match the API's "matched" event against the chain's CTF transfer to confirm settlement.
The 5-second-wait rule after a buy (chapter 12) is the on-chain reality intruding into API time. A GTC sell submitted immediately after a market buy will see balance: 0 from the chain check even though the CLOB matched moments ago.
Code: detect a UMA dispute via event subscription
Reference: watch Polymarket-related UMA disputes in real time.
from web3 import Web3
w3 = Web3(Web3.WebsocketProvider(POLYGON_WSS))
oo = w3.eth.contract(address=UMA_OO_ADDR, abi=UMA_ABI)
POLY_REQUESTER = "0x..." # Polymarket's UMA requester address
def on_dispute(event):
args = event["args"]
if args["requester"].lower() != POLY_REQUESTER.lower(): return
print(f"DISPUTE on Polymarket market: ancillary={args['ancillaryData'][:40]}...")
# decode ancillaryData to recover the market question
event_filter = oo.events.DisputePrice.create_filter(fromBlock="latest")
while True:
for ev in event_filter.get_new_entries():
on_dispute(ev)
time.sleep(2)
The ancillaryData field is hex-encoded JSON-ish text containing the market question. Decoding it gives you the slug-equivalent identifier to cross-reference against your open positions.












