Polymarket Bot Tutorial · 32개 챕터 중 8번째
봇을 위한 Polymarket CLOB API: order book 스냅샷용 REST endpoint, 실시간 업데이트용 WebSocket subscription, bids/asks 파싱, mid-price와 depth 계산, 코드 예제.
이 챕터에서 다루는 내용
CLOB API는 주문이 서명되고, 전송되고, 매칭되며, order book이 존재하는 곳입니다. Polymarket에는 두 세대의 SDK가 있습니다. 사용 중단된 v1과 현재의 v2입니다. 이 챕터는 v2만 다루며, 2026년에 배포할 봇에는 v1이 들어가서는 안 됩니다. 여기서는 REST 스냅샷 경로, WebSocket 업데이트 채널, 새 빌더들이 자주 실수하는 파싱 세부사항, 그리고 장시간 실행되는 봇이 몇 시간 만에 sync에서 벗어나지 않도록 하는 reconnect 로직까지 살펴봅니다.
- CLOB v1 vs v2 (v2 사용)
- Order book REST snapshot
- WebSocket subscriptions: market and user channels
- Parsing bids/asks/depth
- Computing mid-price and best-bid/ask
- Maker fees, taker fees, rebates
- Code: connect WS and process price-change events
- Reconnect and gap-handling
CLOB v1 vs v2 (v2 사용)
Polymarket은 두 세대의 SDK를 유지합니다. v1(@polymarket/clob-client on npm, py-clob-client <0.30)은 사용 중단되었고 2024년에 추가된 여러 order type이 없습니다. v2(@polymarket/clob-client-v2 Node v1.0.6, Python용 py-clob-client 0.34.6+)가 현재 표준입니다.
구체적인 차이는 세 가지입니다. v2는 다중 outcome market을 위한 negRisk flag를 지원합니다. 이는 2024년 말 NegRisk exchange가 출시된 이후 필수입니다. v2는 WebSocket message shape에 대한 TypeScript type을 제공합니다. v1은 any를 반환합니다. v2는 2025년 8월의 Gnosis Safe signature flow를 기본적으로 처리합니다. v1은 커스텀 signing glue가 필요합니다.
이 챕터의 나머지는 전부 v2를 전제로 작성되었습니다. 오래된 튜토리얼에서 v1 코드를 보게 되면, 반증되기 전까지는 깨진 것으로 간주하세요. 특히 NegRisk market에 대한 order placement는 v1에서 잘못된 exchange contract로 조용히 라우팅될 수 있습니다.
Order book REST snapshot
REST snapshot endpoint는 특정 시점의 단일 token에 대한 전체 book을 반환합니다.
GET https://clob.polymarket.com/book?token_id=<ERC1155_TOKEN_ID>
Response shape:
{
"market": "0x...",
"asset_id": "5413...",
"timestamp": "1715600000000",
"hash": "0x...",
"bids": [{"price":"0.45","size":"120"}, {"price":"0.44","size":"380"}, ...],
"asks": [{"price":"0.47","size":"85"}, {"price":"0.48","size":"210"}, ...]
}
가격은 소수점 2~3자리 문자열이며, size는 share 수를 나타내는 문자열입니다(달러가 아님). bids는 높은 가격에서 낮은 가격 순으로, asks는 낮은 가격에서 높은 가격 순으로 정렬됩니다. hash는 중복 제거 marker입니다. 변하지 않은 book을 반복 조회하면 같은 hash가 반환되므로, 봇은 처리를 건너뛸 수 있습니다.
REST snapshot은 일회성 조회(진입 판단 시 price check)에 적합합니다. 지속적인 모니터링에는 아래 WebSocket channel을 사용하세요.
WebSocket subscriptions: market and user channels
중요한 WebSocket channel은 두 개입니다.
Market channel: wss://ws-subscriptions-clob.polymarket.com/ws/market. 하나 또는 여러 token에 subscribe하면, order-book updates가 발생하는 대로 받게 됩니다.
{"type":"Market","markets":["0xCondId1","0xCondId2"]}
message는 변경이 있을 때마다 도착합니다. type에는 book(전체 snapshot), price_change(delta), tick_size_change(드묾), last_trade_price(가장 최근 fill)이 포함됩니다.
User channel: wss://ws-subscriptions-clob.polymarket.com/ws/user. 인증이 필요하며, fill, partial fill, cancellation 등 자신의 order event를 받습니다.
{"type":"User","auth":{"apiKey":"...","secret":"...","passphrase":"..."}}
User channel은 fill을 감지하는 가장 깔끔한 방법입니다. orders REST endpoint를 polling하면 비용이 더 들고, poll 사이의 state change를 놓칠 수 있습니다. WebSocket은 matcher가 이를 승인하는 순간 event를 바로 push합니다.
Parsing bids/asks/depth
order book은 aggregated size를 가진 price level 목록입니다. 제대로 맞춰야 하는 parsing convention이 두 가지 있습니다.
Order direction: bids는 buy order입니다(누군가 이 가격에 BUY하려는 것). YOUR bot이 sell할 때는 bid를 hit합니다. 봇이 buy할 때는 ask를 lift합니다. Polymarket UI는 같은 방향을 표시합니다. 일부 다른 exchange는 이를 반대로 표시합니다.
Sorting: bids는 내림차순으로 도착합니다(가장 좋은 bid가 먼저). asks는 오름차순으로 도착합니다(가장 좋은 ask가 먼저). best bid는 bids[0], best ask는 asks[0]입니다. 주의: public WebSocket은 때때로 pre-sorted되지 않은 partial book update를 보냅니다. merge 후에는 항상 방어적으로 다시 정렬하세요.
한 level의 depth는 실제로 거래 가능한 달러 가치입니다: price * size. top-5-level depth는 흔한 liquidity metric입니다: sum(b.price * b.size for b in bids[:5]). top-5 depth가 $100 미만이면 book은 illiquid하며 대부분의 전략 가정이 깨집니다.
Computing mid-price and best-bid/ask
봇이 필요로 하는 파생 price point는 세 가지입니다.
- Best bid / best ask:
bids[0].price와asks[0].price. 실제로 거래할 수 있는 가격이며, 1 share 기준입니다. - Mid-price:
(best_bid + best_ask) / 2. spread의 수학적 중앙값입니다. valuation에는 유용하지만, 실제로는 mid에서 거래하지 않습니다. - VWAP price for size N: cumulative size가 N에 도달할 때까지 book을 따라가며, size-weighted average price를 반환합니다. 더 깊은 level까지 밀고 들어가는 것을 반영한, 지금 당장 N shares를 BUY하는 실제 비용입니다.
edge case: bid 또는 ask 한쪽이 비어 있는 경우(파는 사람이 없거나 사는 사람이 없음) book이 one-sided라는 뜻입니다. Polymarket의 market structure에서는 보통 해소되었거나 거의 해소된 market에서 이런 일이 발생하며, 한쪽은 0.999이고 loser side에는 아무도 liquidity를 제공하지 않을 때 나타납니다. best-bid = 0 또는 best-ask = 1은 "do not trade" 신호로 취급하세요.
Maker fees, taker fees, rebates
Polymarket은 역사의 대부분 동안 거래 수수료를 전혀 받지 않았습니다. 이는 2026년에 바뀌었습니다. 연초에 15분 단위 암호화폐 market에서 수수료가 도입되었고, 2026년 3월 30일 Sports로 확대되었으며, 이후 대부분의 카테고리로 적용되었습니다. 아직도 Polymarket이 수수료가 없다고 주장하는 튜토리얼은 모두 낡은 것이며, 고빈도 전략에서 이를 놓치면 조용히 잠식당합니다. 2026년 중반 기준으로 이 모델이 실제로 어떻게 작동하는지 설명하겠습니다.
먼저 모든 거래의 두 측면입니다. maker는 book에 대기하는 지정가 주문을 올려 그대로 기다리는 쪽이고, taker는 이미 존재하는 유동성에 대해 즉시 체결되는 주문을 보내는 쪽입니다. maker는 여전히 수수료가 0이며 그 위에 rebate까지 받습니다. 수수료를 내는 것은 taker뿐입니다.
taker fee는 고정 비율이 아닙니다. 주문 규모와 가격 모두에 달려 있는 곡선을 따릅니다.
fee = shares × feeRate × price × (1 - price)
price × (1 - price) 항은 가격 0.50(진정한 동전 던지기 market)에서 최대가 되고 0이나 1로 갈수록 줄어듭니다. 즉, 가장 불확실한 market에서 가장 높은 수수료를 내고, 거의 결정된 market에서는 거의 내지 않습니다. feeRate는 카테고리별로 정해져 있습니다.
- Crypto: feeRate 0.07(가장 높음, 실효 정점 약 1.8%), maker rebate 20%.
- Sports: feeRate 0.03(정점 약 0.75%), maker rebate 25%.
- Finance, Politics, Tech, Mentions: feeRate 0.04, maker rebate 25%.
- Economics, Culture, Weather, 일반: feeRate 0.05, maker rebate 25%.
- Geopolitics 및 대규모 세계 이벤트: 0, 여전히 수수료 무료.
계산 예시입니다. 봇이 암호화폐 market에서 가격 0.50일 때 100 shares를 가져간다고 합시다. 수수료는 100 × 0.07 × 0.50 × (1 - 0.50) = 100 × 0.07 × 0.25 = $1.75입니다. 같은 100 shares를 0.90에서 가져가면, 가격이 불확실한 중앙에서 멀기 때문에 수수료는 100 × 0.07 × 0.90 × 0.10 = $0.63으로 떨어집니다. 봇에게 교훈은 분명합니다. 변동성이 크고 거의 균형을 이룬 암호화폐·스포츠 market에서 유동성을 가져가는 것이 수수료가 가장 큽니다. 그러니 바로 그곳에서 수수료를 내기보다 maker로 quote하여 rebate를 받는 편이 훨씬 유리합니다.
명시적인 수수료에 더해, bid-ask spread를 넘을 때마다 그 spread를 냅니다. spread는 최우선 매수가와 최우선 매도가의 차이이며, taker로 진입하고 청산하는 전략에서는 그 차이가 수수료에 더해지는 실제 비용입니다. 일반적인 book에서는 round-trip당 1~3센트, illiquid한 book에서는 더 크다고 가정하세요. NegRisk market(multi-outcome exchange)은 동일한 수수료 모델을 쓰지만 별도의 contract에서 정산되므로 reward가 따로 누적됩니다. Chapter 19에서는 maker rebate를 받는 것 자체가 부수 효과가 아니라 전략이 되는 liquidity-rewards farming을 다룹니다.
Code: connect WS and process price-change events
최소한의 Node 예제입니다. 연결하고, subscribe하고, 한 token에 대한 모든 price-change event를 로그로 남깁니다.
import WebSocket from "ws";
const ws = new WebSocket("wss://ws-subscriptions-clob.polymarket.com/ws/market");
ws.on("open", () => {
ws.send(JSON.stringify({ type: "Market", markets: ["<CONDITION_ID>"] }));
});
ws.on("message", (data) => {
const msg = JSON.parse(data.toString());
if (msg.event_type === "price_change") {
console.log("price_change", msg.asset_id, msg.changes);
} else if (msg.event_type === "book") {
console.log("book snapshot", msg.bids?.[0], msg.asks?.[0]);
}
});
ws.on("close", () => console.log("closed"));
ws.on("error", (e) => console.error("err", e.message));
WebSocket connection 하나당 약 30개 token까지는 무난하게 subscribe할 수 있습니다. 그 이상이면 여러 connection으로 나누세요. 서버가 가끔 큰 subscription을 오류 없이 떨어뜨리기 때문에, 조용히 stale book을 읽는 문제가 생길 수 있습니다.
Reconnect and gap-handling
장시간 실행되는 WebSocket connection은 끊길 수 있습니다. Cloudflare는 몇 시간마다 connection을 순환시키고, network도 순간적으로 흔들리며, Polymarket도 때때로 배포를 진행합니다. 이를 전제로 설계하세요.
Reconnect strategy: close 또는 error가 발생하면 jitter를 섞어 min(2^attempt, 30)초 대기한 뒤 다시 subscribe합니다. reconnect 후 첫 성공 message가 도착하면 attempt counter를 초기화합니다.
gap handling은 reconnect 속도보다 더 중요합니다. WebSocket이 끊겨 있는 동안 book은 움직였습니다. reconnect할 때마다 subscribe한 모든 token의 REST snapshot을 다시 가져와 reconcile하세요. book이 의미 있게 움직인 open position은 state 재확인이 필요하고, exit이 발동해야 할 수도 있으며, alarm은 stale할 수 있습니다. "30초 동안 book update를 놓쳤다"는 장시간 실행 봇의 조용한 치명타입니다. 봇은 stale state 위에서 계속 실행되며, 더 이상 존재하지 않는 가격에 order를 넣게 됩니다.
방어적 패턴: WebSocket 상태와 무관하게 subscribe한 book을 1분마다 snapshot하고, WS는 snapshot polling 위에 얹는 빠른 경로로 취급하세요.










