폴리마켓 봇 튜토리얼 · 32장 중 9장
폴리마켓 온체인 데이터를 직접 읽어봅니다: USDC/pUSD 잔액, 아웃컴 공급량을 위한 CTF 컨트랙트 읽기, UMA Optimistic Oracle의 제안·이의 이벤트, 그리고 Polygon 트랜잭션 로그까지 코드와 함께 다룹니다.
이 장에서 다루는 내용
폴리마켓의 API는 편리하지만 최종적으로만 일관성이 맞춰집니다(eventually consistent). 진짜 기준이 되는 출처는 체인입니다. 이 장에서는 실전 봇이 자체 장부를 검증하기 위해 사용하는 온체인 읽기를 살펴봅니다: pUSD 잔액, 아웃컴 토큰 보유량, UMA 이의 이벤트, 그리고 CTF 컨트랙트 상태입니다. 대부분의 실전 봇이 수렴하는 패턴은 속도를 위한 API 우선과 정확성을 위한 주기적인 온체인 대조입니다.
- 온체인에 있는 데이터(CLOB와 대비)
- pUSD 컨트랙트 주소와 ABI
- Conditional Tokens Framework (CTF)
- UMA Optimistic Oracle: 제안 및 이의 이벤트
- Polygon 이벤트 로그 읽기 (web3.py / ethers)
- 언제 온체인을 읽고 언제 API를 신뢰할지
- 코드: 이벤트 구독으로 UMA 이의 감지
온체인에 있는 데이터(CLOB와 대비)
두 개의 상태 머신, 두 개의 진실이 있습니다.
온체인 (Polygon): pUSD 잔액, 아웃컴 토큰 보유량(토큰별 ERC-1155 공급량), allowance 승인, UMA Optimistic Oracle의 제안과 이의, 입금 및 출금 이벤트가 여기에 있습니다. 최종적으로는 정확하며, 지연은 Polygon 블록 하나(약 2초) 수준입니다.
CLOB (폴리마켓 API): 호가창, 최근 체결, 대기 중인 지정가 주문, 매칭 확인이 여기에 있습니다. 실시간이지만 최종적으로만 일관성이 맞춰집니다. 즉 ERC-1155가 정산되기 전에 매칭이 먼저 확인되며, 이로 인해 12장에서 다루는 유령 체결(phantom-fill) 문제가 발생합니다.
둘은 항상 수렴해야 합니다. 서로 다를 때는 체인이 기준입니다. CLOB만 신뢰하는 봇은 실제 상태와 어긋나게 되고, 체인만 신뢰하는 봇은 느리게 거래하게 됩니다. 실전 코드는 둘 다 사용합니다. 속도가 중요한 결정에는 CLOB를, 정확성을 위한 정기적 대조에는 체인을 사용합니다.
pUSD 컨트랙트 주소와 ABI
pUSD는 2026년 V2 마이그레이션 이후 사용되는 폴리마켓의 스테이블코인 래퍼입니다. Polygon 메인넷의 0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB에 있는 컨트랙트는 표준 ERC-20입니다.
봇에 중요한 세 가지 읽기입니다.
balanceOf(proxy)- 실제로 쓸 수 있는 pUSD입니다. 재시작할 때마다 CLOB가 보는 잔액과 비교하세요.allowance(proxy, exchange_contract)- CTF/NegRisk 익스체인지 컨트랙트가 pUSD를 사용할 수 있는지 여부입니다. 주문 매칭에 필요합니다.Transfer이벤트 구독 - 폴링 없이 입금과 출금을 감지합니다.
USDC(0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174)는 여전히 오프램프 페어로 남아 있습니다. 대부분의 봇은 pUSD 읽기만 필요하며, USDC는 입금·출금 주기 동안에만 중요합니다.
Conditional Tokens Framework (CTF)
아웃컴 지분은 Gnosis의 Conditional Tokens Framework (CTF)가 발행하는 ERC-1155 토큰입니다. Polygon의 0x4D97DCd97eC945f40cF65F87097ACe5EA0476045에 있는 CTF 컨트랙트는 position-id별 공급량을 추적합니다.
세 가지 읽기입니다.
balanceOf(proxy, position_id)- 해당 시장·아웃컴에 대해 실제로 보유한 아웃컴 토큰 수입니다.getOutcomeSlotCount(condition_id)- 아웃컴 수입니다(이진 시장은 2, 다중 아웃컴은 N).payoutNumerators,payoutDenominator- UMA가 시장을 정산할 때 설정됩니다. 이를 읽으면 CLOB UI가 갱신되기 전에 어느 쪽이 이겼는지 알 수 있습니다.
position_id는 (condition_id, outcome_index)의 해시입니다. CTF의 getPositionId 헬퍼를 사용하거나, 직접 keccak 연산을 재현해 클라이언트 측에서 계산하세요.
UMA Optimistic Oracle: 제안 및 이의 이벤트
UMA의 Optimistic Oracle (OO)은 폴리마켓의 모든 이의 정산을 처리합니다. 봇이 구독하고 싶을 만한 두 가지 이벤트가 있습니다.
ProposePrice(requester, identifier, timestamp, ancillaryData, proposer, proposedPrice)- 폴리마켓 봇이 아웃컴을 제안할 때 발생합니다.DisputePrice(requester, identifier, timestamp, ancillaryData, disputer)- 누군가 제안된 아웃컴에 이의를 제기할 때 발생합니다.
OO 컨트랙트 주소: 0xeE3Afe347D5C74317041E2618C49534dAf887c24. 폴리마켓의 requester 주소로 필터링하세요.
구독해야 하는 이유는 이렇습니다. 이의가 제기되었다는 것은 정산이 이제 다툼 상태이며 24-72시간의 UMA 투표가 필요하다는 뜻입니다. 그 기간 동안 시장은 거래가 중단될 수 있습니다. 이의가 걸린 시장에 포지션을 보유한 봇은 즉시 이를 알아야 합니다.
Polygon 이벤트 로그 읽기 (web3.py / ethers)
두 가지 참고 구현입니다.
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 });
});
지속적인 모니터링에는 HTTP 폴링 대신 WebSocket 전송(wss://...)을 사용하세요. 요청 수가 줄고 전달이 더 빠릅니다. 대부분의 유료 RPC 제공자는 최저 요금제에서도 WebSocket을 포함합니다.
언제 온체인을 읽고 언제 API를 신뢰할지
실전에서 얻은 실용적인 규칙입니다.
- API를 신뢰할 것: 실시간 호가창, 최근 체결, 본인의 대기 주문, 시장 메타데이터(slug, 질문, 종료일), 이벤트·시장 탐색.
- 체인을 신뢰할 것: 본인의 pUSD 잔액, 본인의 아웃컴 토큰 보유량, 입금 및 출금 검증, 정산 결과, 이의 상태.
- 둘 다 교차 확인할 것: 봇이 체결로 기록한 모든 금융 정보. API의 matched 이벤트와 체인의 CTF transfer를 대조해 정산을 확인하세요.
구매 후 5초 기다리기 규칙(12장)은 API의 시간 감각에 온체인 현실이 끼어드는 현상입니다. 시장가 매수 직후 제출된 GTC 매도는 CLOB가 방금 매칭했더라도 체인 확인에서는 balance: 0을 보게 됩니다.
코드: 이벤트 구독으로 UMA 이의 감지
참고 구현: 폴리마켓 관련 UMA 이의를 실시간으로 감시합니다.
from web3 import Web3
w3 = Web3(Web3.WebsocketProvider(POLYGON_WSS))
oo = w3.eth.contract(address=UMA_OO_ADDR, abi=UMA_ABI)
POLY_REQUESTER = "0x..." # 폴리마켓's UMA requester address
def on_dispute(event):
args = event["args"]
if args["requester"].lower() != POLY_REQUESTER.lower(): return
print(f"DISPUTE on 폴리마켓 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)
ancillaryData 필드는 시장 질문이 담긴 16진수로 인코딩된 JSON 형태의 텍스트입니다. 이를 디코딩하면 보유 중인 포지션과 대조할 수 있는 slug에 해당하는 식별자를 얻을 수 있습니다.


