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.
  • 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 (after getConditionIdgetCollectionId) 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 (the payoutNumerators above 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.

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.