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]

Two state machines, two truths.

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 stablecoin wrapper used since the 2026 V2 migration. The contract at 0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB on Polygon mainnet is a standard ERC-20.

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.
  • Transfer event 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 or replicate the keccak math in your stack.

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.

Frequently asked questions

Where can I find the Polymarket pUSD contract?
pUSD lives at 0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB on Polygon. It replaced USDC.e as Polymarkets canonical collateral on April 28, 2026. Polygonscan link: https://polygonscan.com/token/0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB.
What is the CTF contract on Polymarket?
CTF stands for Conditional Tokens Framework - the Gnosis-derived ERC-1155 contract that issues outcome tokens (the YES and NO shares you trade). Each Polymarket market is a CTF position with redemption logic tied to UMAs resolution. Bots rarely need to interact with CTF directly; the SDK handles it.
How do I subscribe to UMA dispute events on Polygon?
Subscribe to the UMA Optimistic Oracle V2 contract`s "ProposePrice" and "DisputePrice" events via your Polygon RPC providers WebSocket. Filter by the requesterAddress field (Polymarkets oracle adapter) to get only Polymarket-related disputes. Code samples in the chapter.
Do I need to read on-chain data if I trust the Polymarket API?
For most strategies, no. The CLOB API is canonical for order book and trade data, and the gamma API is canonical for metadata. You read on-chain when you need (a) UMA dispute alerts faster than the API surfaces them, (b) verification that a deposit actually arrived, or (c) custom analytics on outcomes/positions.
What is the latency of on-chain Polygon data vs the Polymarket API?
Polygon block time is ~2 seconds. The Polymarket API generally surfaces order book changes within hundreds of milliseconds of the on-chain match. For most signals, the API is faster than your own on-chain reads. UMA disputes are an exception - the on-chain event fires before the UI reflects the dispute.
Can I read Polymarket positions without the CLOB API?
Technically yes - read CTF balanceOf(walletAddress, positionId) for each position. Practically the CLOB API /trade/positions endpoint is faster, includes pricing, and aggregates all your positions. Only fall back to on-chain reads if you need verification or the API is down.