<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: BlueWhale-Quant-Lab</title>
    <description>The latest articles on DEV Community by BlueWhale-Quant-Lab (@bluewhale-quant-lab).</description>
    <link>https://dev.to/bluewhale-quant-lab</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3962470%2F1edf261b-05ad-48cc-ac61-6658c360ff8e.png</url>
      <title>DEV Community: BlueWhale-Quant-Lab</title>
      <link>https://dev.to/bluewhale-quant-lab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bluewhale-quant-lab"/>
    <language>en</language>
    <item>
      <title>Polymarket CLOB WebSocket in Python — Real-Time Order Book Without Polling</title>
      <dc:creator>BlueWhale-Quant-Lab</dc:creator>
      <pubDate>Wed, 17 Jun 2026 04:52:41 +0000</pubDate>
      <link>https://dev.to/bluewhale-quant-lab/polymarket-clob-websocket-in-python-real-time-order-book-without-polling-4e6n</link>
      <guid>https://dev.to/bluewhale-quant-lab/polymarket-clob-websocket-in-python-real-time-order-book-without-polling-4e6n</guid>
      <description>&lt;p&gt;If your Polymarket bot polls &lt;code&gt;GET /book&lt;/code&gt; in a loop, your view of the market is as stale as your interval — and you'll lose to anyone using the &lt;strong&gt;WebSocket feed&lt;/strong&gt;. This tutorial builds a real-time local order book in Python from the CLOB WebSocket stream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why WebSocket beats polling
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Polling:&lt;/strong&gt; you ask every N ms → your data is up to N ms old, and you burn REST rate-limit budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSocket:&lt;/strong&gt; the server &lt;strong&gt;pushes&lt;/strong&gt; changes → your information latency drops to roughly your network round-trip.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a reactive strategy, this is the difference between trading the market that &lt;em&gt;is&lt;/em&gt; and the market that &lt;em&gt;was&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect and subscribe
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;websockets&lt;/span&gt;

&lt;span class="n"&gt;WS_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wss://ws-subscriptions-clob.polymarket.com/ws/market&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_ids&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;websockets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WS_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ping_interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ping_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assets_ids&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;market&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(Confirm the exact URL, subscription payload, and message shapes against current Polymarket docs — they change.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Maintain a local book from snapshot + deltas
&lt;/h2&gt;

&lt;p&gt;The feed typically sends an initial &lt;strong&gt;book snapshot&lt;/strong&gt; then incremental &lt;strong&gt;price-change&lt;/strong&gt; messages. Apply them to a local structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LocalBook&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;   &lt;span class="c1"&gt;# price -&amp;gt; size
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;asks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply_snapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bids&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])}&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;asks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])}&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply_delta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;changes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
            &lt;span class="n"&gt;side&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bids&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;side&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BUY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;asks&lt;/span&gt;
            &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;best_bid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bids&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;best_ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;asks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;asks&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;best_bid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;best_ask&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wire it together
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;book&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LocalBook&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;token_id&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;book&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;snapshot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply_snapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price_change&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply_delta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# react immediately on fresh state
&lt;/span&gt;        &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;on_mid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TOKEN_ID&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Measure your information lag
&lt;/h2&gt;

&lt;p&gt;Always know how fresh your book is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="n"&gt;lag_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lag_ms&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;⚠ stale: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lag_ms&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;ms behind server&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The ceiling you can't code around
&lt;/h2&gt;

&lt;p&gt;Here's the catch: your information latency can never beat your &lt;strong&gt;network round-trip to the server&lt;/strong&gt;. The CLOB WebSocket terminates in &lt;strong&gt;Amsterdam&lt;/strong&gt; — I measured ~1.2 ms from an AMS box vs ~88 ms from US-East. From the US, even a perfect WebSocket implementation is ~90 ms behind reality, because that's how long the packets take to arrive.&lt;/p&gt;

&lt;p&gt;So the prerequisite for a fast feed is a server &lt;em&gt;near&lt;/em&gt; the feed. I run mine on an Amsterdam-metro VPS: &lt;strong&gt;&lt;a href="https://www.hostkey.com/?a_aid=6a0c726869c8f" rel="noopener noreferrer"&gt;my Amsterdam VPS&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Disclosure: affiliate link, I earn a referral. It's the box behind the 1.2 ms.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Robustness checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reconnect with backoff&lt;/strong&gt; — WebSockets drop; resubscribe and re-snapshot on reconnect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detect gaps&lt;/strong&gt; — if deltas reference prices you don't have, request a fresh snapshot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heartbeat&lt;/strong&gt; — use &lt;code&gt;ping_interval&lt;/code&gt; so dead connections are detected fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One process, many assets&lt;/strong&gt; — subscribe to multiple &lt;code&gt;assets_ids&lt;/code&gt; on one socket.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;WebSocket + a maintained local book gives you near-real-time perception. But the floor on "real-time" is your distance to Amsterdam. Get the server location right, then this code makes you genuinely fast.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Code is illustrative — verify against current docs. Latency from my own 2026 tests. Not financial advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>websocket</category>
      <category>tutorial</category>
      <category>trading</category>
    </item>
    <item>
      <title>How to Benchmark API Latency to Any Endpoint (Polymarket Case Study)</title>
      <dc:creator>BlueWhale-Quant-Lab</dc:creator>
      <pubDate>Wed, 17 Jun 2026 04:51:36 +0000</pubDate>
      <link>https://dev.to/bluewhale-quant-lab/how-to-benchmark-api-latency-to-any-endpoint-polymarket-case-study-512o</link>
      <guid>https://dev.to/bluewhale-quant-lab/how-to-benchmark-api-latency-to-any-endpoint-polymarket-case-study-512o</guid>
      <description>&lt;p&gt;"Just ping it" is bad latency advice. ICMP gets deprioritized behind CDNs and tells you almost nothing about real request latency. This is how to benchmark API latency &lt;em&gt;properly&lt;/em&gt;, with a real case study: finding where Polymarket's order book lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;ping&lt;/code&gt; lies
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ping&lt;/code&gt; measures ICMP echo round-trip. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CDNs and load balancers often &lt;strong&gt;rate-limit or deprioritize ICMP&lt;/strong&gt;, so the number is noisy or misleadingly high/low.&lt;/li&gt;
&lt;li&gt;It ignores &lt;strong&gt;TLS handshake&lt;/strong&gt; cost, which dominates short HTTPS requests.&lt;/li&gt;
&lt;li&gt;It tells you nothing about &lt;strong&gt;server processing time&lt;/strong&gt; (TTFB).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For an API, measure what the API actually does: TCP connect, TLS, and time-to-first-byte.&lt;/p&gt;

&lt;h2&gt;
  
  
  A proper latency harness (Python)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;percentiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;xs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;min&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p50&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p95&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p99&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mf"&gt;0.99&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tcp_connect_ms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_connection&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ttfb_ms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HTTPSConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_default_context&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getresponse&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bench&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tcp_connect&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;percentiles&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;tcp_connect_ms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)]),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ttfb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="nf"&gt;percentiles&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;ttfb_ms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)]),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;bench&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clob.polymarket.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Read p99 and jitter, not just the average
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;average&lt;/strong&gt; is marketing. What kills a trading bot is the &lt;strong&gt;p99&lt;/strong&gt; — your latency during the volatile windows you actually trade in. Always report &lt;code&gt;min / p50 / p95 / p99 / max&lt;/code&gt;. A box with p50=1.2 ms but p99=12 ms is worse than a steady p50=3 ms box.&lt;/p&gt;

&lt;p&gt;Check jitter over time, too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ping &lt;span class="nt"&gt;-i&lt;/span&gt; 0.5 &lt;span class="nt"&gt;-c&lt;/span&gt; 600 clob.polymarket.com | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-3&lt;/span&gt;   &lt;span class="c"&gt;# watch min/avg/max/mdev spread&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The case study: where is Polymarket's CLOB?
&lt;/h2&gt;

&lt;p&gt;I ran the harness from VPS boxes in five regions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Region&lt;/th&gt;
&lt;th&gt;TCP connect p50&lt;/th&gt;
&lt;th&gt;TTFB p50&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Amsterdam&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~1.4 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~6 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frankfurt&lt;/td&gt;
&lt;td&gt;~9 ms&lt;/td&gt;
&lt;td&gt;~16 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;US-East&lt;/td&gt;
&lt;td&gt;~90 ms&lt;/td&gt;
&lt;td&gt;~110 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Singapore&lt;/td&gt;
&lt;td&gt;~168 ms&lt;/td&gt;
&lt;td&gt;~195 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A ~1.4 ms TCP connect is only possible within ~100 km (fiber does ~200 km/ms RTT). So the endpoint is in &lt;strong&gt;Amsterdam&lt;/strong&gt; — proven by physics, not vibes. (Whether the matching engine is co-located vs behind an edge is a fair inference from the low TTFB, but the hosting decision is the same either way.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning the benchmark into a decision
&lt;/h2&gt;

&lt;p&gt;The whole point of benchmarking is to &lt;em&gt;act&lt;/em&gt; on it. For Polymarket, the data says: host in Amsterdam. I moved my bot to an AMS-metro VPS and the connect time went from ~90 ms to ~1.2 ms. The box I use: &lt;strong&gt;&lt;a href="https://www.hostkey.com/?a_aid=6a0c726869c8f" rel="noopener noreferrer"&gt;the Amsterdam box I use&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Disclosure: affiliate link — I earn a referral. The numbers above are from this box.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reusable checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ Measure &lt;strong&gt;TCP connect + TTFB&lt;/strong&gt;, not just ICMP.&lt;/li&gt;
&lt;li&gt;✅ Report &lt;strong&gt;percentiles&lt;/strong&gt;, especially p99.&lt;/li&gt;
&lt;li&gt;✅ Test &lt;strong&gt;jitter&lt;/strong&gt; over minutes, at different times of day.&lt;/li&gt;
&lt;li&gt;✅ Compare &lt;strong&gt;multiple regions&lt;/strong&gt; with hourly VPS boxes.&lt;/li&gt;
&lt;li&gt;✅ Convert sub-2 ms numbers into "same metro" conclusions via the fiber speed limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This harness works for any endpoint — exchanges, RPCs, your own APIs. Polymarket just happens to have a satisfying answer: Amsterdam.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Numbers from my own 2026 tests. Not financial advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>performance</category>
      <category>networking</category>
      <category>devops</category>
    </item>
    <item>
      <title>Polymarket's Price-to-Beat Has No API — Why a Co-located Server Gets It in Real Time, Not 15 Seconds</title>
      <dc:creator>BlueWhale-Quant-Lab</dc:creator>
      <pubDate>Wed, 17 Jun 2026 04:51:26 +0000</pubDate>
      <link>https://dev.to/bluewhale-quant-lab/polymarkets-price-to-beat-has-no-api-why-a-co-located-server-gets-it-in-real-time-not-15-seconds-15ko</link>
      <guid>https://dev.to/bluewhale-quant-lab/polymarkets-price-to-beat-has-no-api-why-a-co-located-server-gets-it-in-real-time-not-15-seconds-15ko</guid>
      <description>&lt;p&gt;If you automate Polymarket's crypto Up/Down markets, you hit this wall fast: &lt;strong&gt;there is no API that returns the "price to beat" (PTB)&lt;/strong&gt; — the strike/reference price that decides whether Up or Down wins. You have to &lt;em&gt;reconstruct&lt;/em&gt; it. And the speed + accuracy of that reconstruction turns out to be a &lt;strong&gt;server-location&lt;/strong&gt; problem. Let me connect the two, because almost nobody does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the price-to-beat actually is
&lt;/h2&gt;

&lt;p&gt;Each Up/Down cycle settles against one number — the PTB — captured at the &lt;strong&gt;cycle open&lt;/strong&gt; (&lt;code&gt;t_start&lt;/code&gt;). It's not stored in a Polymarket endpoint; it's taken from an upstream source at that instant:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cycle&lt;/th&gt;
&lt;th&gt;Where the PTB comes from&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5m / 15m / 4h&lt;/td&gt;
&lt;td&gt;the &lt;strong&gt;Chainlink&lt;/strong&gt; price pushed at &lt;code&gt;t_start&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1h&lt;/td&gt;
&lt;td&gt;the &lt;strong&gt;Binance 1h candle OPEN&lt;/strong&gt; at &lt;code&gt;t_start&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1d&lt;/td&gt;
&lt;td&gt;the &lt;strong&gt;Binance 1m candle CLOSE&lt;/strong&gt; at noon-ET &lt;code&gt;t_start&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(The per-cycle recipe and the noon-ET/DST boundary rules are deterministic — there's a clean open-source resolver for that part, linked at the end.)&lt;/p&gt;

&lt;p&gt;The key word is &lt;strong&gt;instant&lt;/strong&gt;. The PTB is a snapshot of an upstream feed at a precise moment. Whoever samples that feed &lt;em&gt;closest to that moment&lt;/em&gt; reconstructs the most faithful copy of the number Polymarket itself locked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two ways to get it — and why one is 15 seconds too slow
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Option A — scrape it off the market page.&lt;/strong&gt; Load the Polymarket Up/Down page, wait for it to render, parse the displayed strike. In practice that's &lt;strong&gt;~15 seconds&lt;/strong&gt; end to end (page load, JS hydration, polling until the value appears). For most of the cycle, fine. For the moment that matters — the open — it's useless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B — reconstruct it yourself at &lt;code&gt;t_start&lt;/code&gt;.&lt;/strong&gt; Sample the same upstream feed (Chainlink/Binance) at the cycle boundary and apply the recipe. You get the number &lt;em&gt;as it's being set&lt;/em&gt;, not 15 seconds after.&lt;/p&gt;

&lt;p&gt;Option B is only as good as your &lt;strong&gt;sampling delay&lt;/strong&gt; — how long after &lt;code&gt;t_start&lt;/code&gt; you actually capture the upstream value. And that delay is dominated by network latency. Which is where the server comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why co-location makes your PTB match Polymarket's
&lt;/h2&gt;

&lt;p&gt;Here's the part I verified empirically. My order/data infra sits in &lt;strong&gt;Amsterdam&lt;/strong&gt;, the same metro Polymarket's CLOB answers from (I measured ~1.2 ms to it — see my latency benchmark post). Because my box is effectively co-located with Polymarket's own slicing path, when I sample the upstream price at &lt;code&gt;t_start&lt;/code&gt;, I capture &lt;strong&gt;essentially the same tick Polymarket captured.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In my testing, the PTB I reconstruct this way matches Polymarket's locked value &lt;strong&gt;~99% of the time, and when it differs the error is under 0.2%.&lt;/strong&gt; That's not luck — it's what happens when your sampling delay (the "lock-delay") shrinks toward zero. The upstream feed (especially a Chainlink push) can &lt;em&gt;drift&lt;/em&gt; in the moments after &lt;code&gt;t_start&lt;/code&gt;; a far-away server samples late, catches drift, and reconstructs a strike that's slightly — sometimes decisively — off. A co-located server samples at the same instant and matches.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The failure mode is brutal: a late-sampled PTB can flip which side is "Up" vs "Down" on a tight cycle. Getting the strike wrong doesn't throw an error — it silently puts you on the losing side.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So "low latency" here isn't about order speed at all. It's about &lt;strong&gt;reconstructing the correct reference number in the first place.&lt;/strong&gt; Distance from the feed = drift = wrong strike.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters most at T0
&lt;/h2&gt;

&lt;p&gt;The opportunities cluster in the &lt;strong&gt;T0 window&lt;/strong&gt; — the pre-open / just-opened price-chaos period where the book is thin, quotes are stale, and the fair strike is still settling. That window is &lt;em&gt;seconds&lt;/em&gt; long. A 15-second scrape never sees it. A far-away reconstruction sees a drifted strike. Only a co-located, real-time reconstruction lets you act inside T0 with a strike you trust.&lt;/p&gt;

&lt;p&gt;That's the whole edge: &lt;strong&gt;right number, right now, while the window is open.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reconstruct, don't scrape.&lt;/strong&gt; The deterministic recipe (source/candle/field per cycle, noon-ET &amp;amp; DST boundaries, candle-close validation) is open source: &lt;strong&gt;&lt;a href="https://github.com/BlueWhale-Quant-Lab/polymarket-price-to-beat-up-down-resolver" rel="noopener noreferrer"&gt;BlueWhale's price-to-beat resolver&lt;/a&gt;&lt;/strong&gt; (MIT, runs offline).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Co-locate in Amsterdam&lt;/strong&gt; so your sampling delay → ~1 ms and your reconstructed PTB matches Polymarket's. This is the infra half of the problem; the resolver is the logic half. The box I run mine on: &lt;strong&gt;&lt;a href="https://www.hostkey.com/?a_aid=6a0c726869c8f" rel="noopener noreferrer"&gt;the Amsterdam VPS behind my ~1.2 ms / 0.2%-error setup&lt;/a&gt;&lt;/strong&gt;
&lt;em&gt;Disclosure: affiliate link — I earn a referral. It's the same box producing the numbers above.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate the candle close&lt;/strong&gt; for the daily (don't trust a 1-minute CLOSE before its &lt;code&gt;close_time&lt;/code&gt;), and treat a Chainlink push that lands too long after &lt;code&gt;t_start&lt;/code&gt; as untrusted — at that point the price has drifted and the side can flip.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Heads-up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The ~99% match / &amp;lt;0.2% error are &lt;strong&gt;my own measurements&lt;/strong&gt; from an Amsterdam box in 2026 — re-measure on yours; results depend on the feed and your sampling discipline.&lt;/li&gt;
&lt;li&gt;A correct PTB is a &lt;em&gt;data primitive&lt;/em&gt;, not a trade. It tells you the strike and the likely side — it places no orders and promises no profit.&lt;/li&gt;
&lt;li&gt;Co-location removes the latency excuse; your strategy, sizing, and risk still have to be right.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;Polymarket's price-to-beat has no API — you reconstruct it at the cycle open, and a co-located Amsterdam server lets you reconstruct it &lt;em&gt;in real time and accurate to within ~0.2%&lt;/em&gt;, instead of scraping a number that's 15 seconds stale. For T0 arbitrage, that's the difference between trading the open and reading about it afterward.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Latency/accuracy figures from my own 2026 tests. Not financial advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>trading</category>
      <category>performance</category>
      <category>crypto</category>
    </item>
    <item>
      <title>How to get the Polymarket Price To Beat at T0 - the open price the instant the window opens</title>
      <dc:creator>BlueWhale-Quant-Lab</dc:creator>
      <pubDate>Thu, 04 Jun 2026 13:03:08 +0000</pubDate>
      <link>https://dev.to/bluewhale-quant-lab/how-to-get-the-polymarket-price-to-beat-at-t0-the-open-price-the-instant-the-window-opens-4b2j</link>
      <guid>https://dev.to/bluewhale-quant-lab/how-to-get-the-polymarket-price-to-beat-at-t0-the-open-price-the-instant-the-window-opens-4b2j</guid>
      <description>&lt;h1&gt;
  
  
  How to get the Polymarket Price To Beat at T0 — the open price the instant the window opens
&lt;/h1&gt;

&lt;p&gt;Every Polymarket crypto Up/Down market resolves against one number: the &lt;strong&gt;Price To Beat (PTB)&lt;/strong&gt; — the opening reference price. Final price &amp;gt; PTB → &lt;strong&gt;Up&lt;/strong&gt; wins; below it → &lt;strong&gt;Down&lt;/strong&gt;. If you're building anything on &lt;em&gt;"BTC Up or Down"&lt;/em&gt;, the PTB is the number your whole edge depends on.&lt;/p&gt;

&lt;p&gt;And there is &lt;strong&gt;no API for it&lt;/strong&gt; on crypto markets. Worse: by the time the PTB shows up anywhere you can read it, your 5-minute window is already closing. The fix isn't to scrape it harder — it's to resolve it at &lt;strong&gt;T0&lt;/strong&gt; (the instant the window opens) from the same upstream sources Polymarket itself uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Read it off the page" comes too late
&lt;/h2&gt;

&lt;p&gt;The frontend does render the PTB and outcome — but only &lt;strong&gt;~20 seconds after the window closes&lt;/strong&gt;. That's perfect for recording results or backfilling history. It's useless for &lt;em&gt;entry&lt;/em&gt;, where you need the open price &lt;strong&gt;before&lt;/strong&gt; the window runs. Different problem, different data path.&lt;/p&gt;

&lt;p&gt;To act at the open, you have to go to the source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the open price actually comes from (it depends on the cycle)
&lt;/h2&gt;

&lt;p&gt;This is the reverse-engineered part. There is no single feed — each timeframe resolves against a different one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cycle&lt;/th&gt;
&lt;th&gt;Reference price (the PTB)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5m / 15m / 4h&lt;/td&gt;
&lt;td&gt;the &lt;strong&gt;Chainlink oracle stream&lt;/strong&gt; Polymarket resolves against — first tick at/after the window boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1h&lt;/td&gt;
&lt;td&gt;the &lt;strong&gt;Binance 1-hour candle OPEN&lt;/strong&gt; at the window start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1d&lt;/td&gt;
&lt;td&gt;the &lt;strong&gt;Binance 1-minute candle CLOSE&lt;/strong&gt; at the official noon-ET open&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use one source for all of them — say, the live Chainlink price for the daily too — and you'll be off, sometimes by hundreds of dollars, and you won't get an error. You'll just get the wrong winning side.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boundaries aren't all UTC
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;5m / 15m / 1h start on UTC grid lines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4h&lt;/strong&gt; starts on &lt;strong&gt;America/New_York&lt;/strong&gt; hours (00/04/08/12/16/20). A fixed UTC formula is wrong half the year because EST and EDT differ by an hour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1d&lt;/strong&gt; starts at &lt;strong&gt;noon ET&lt;/strong&gt; — not UTC midnight, not ET midnight. &lt;em&gt;"Up or Down on March 5"&lt;/em&gt; runs &lt;strong&gt;Mar 4 noon ET → Mar 5 noon ET&lt;/strong&gt;, and the PTB is taken at &lt;strong&gt;Mar 4 noon ET&lt;/strong&gt;. A DST-transition day is 23 or 25 hours, so the end is "noon ET + one calendar day", never "start + 86400".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Don't read the daily candle early
&lt;/h2&gt;

&lt;p&gt;The daily uses the &lt;strong&gt;close&lt;/strong&gt; of the 1-minute candle at noon ET. Binance returns that candle the moment it opens, but until it closes, &lt;code&gt;close&lt;/code&gt; is just the latest trade — not the final close:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;open_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;close_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;candle&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;now_ms&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;close_time&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;NotReady&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1m candle hasn&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t closed; CLOSE is still live&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;price_to_beat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also: Binance &lt;code&gt;startTime&lt;/code&gt; is a filter, not an exact match — always check the returned candle's &lt;code&gt;open_time&lt;/code&gt; equals the &lt;code&gt;t_start&lt;/code&gt; you asked for.&lt;/p&gt;

&lt;h2&gt;
  
  
  One async call at T0
&lt;/h2&gt;

&lt;p&gt;With the per-cycle source mapping and the Eastern-time boundaries handled, getting the open price becomes a single call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ptb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_ptb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t_start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# 5m/15m/4h -&amp;gt; first Chainlink tick at/after t_start
# 1h        -&amp;gt; Binance 1h candle OPEN
# 1d        -&amp;gt; Binance 1m candle CLOSE at noon ET (waited until final)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For 5m/15m/4h the resolver subscribes to the real-time Chainlink feed and takes the first tick at or after the boundary — the open price &lt;strong&gt;in real time, long before on-chain settlement&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two complementary approaches
&lt;/h2&gt;

&lt;p&gt;If all you need is the &lt;em&gt;settled&lt;/em&gt; result after the fact, the free page-scraper reads the rendered Outcome (and PTB) ~20s after close:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/BlueWhale-Quant-Lab/Polymarket-Crypto-UpDown-Outcome-Scraper" rel="noopener noreferrer"&gt;https://github.com/BlueWhale-Quant-Lab/Polymarket-Crypto-UpDown-Outcome-Scraper&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But for entry you need the PTB at the open, in real time, from the underlying sources — which is exactly what the T0 resolver does. I packaged the full per-cycle mapping (Chainlink stream for 5m/15m/4h, Binance open for 1h, noon-ET 1m close for 1d), the DST-aware boundaries, and the wait-for-close guard into a small async library + CLI:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://bluewhalequantlab.gumroad.com/l/polymarket-t0-price-to-beat-ptb-resolver" rel="noopener noreferrer"&gt;Polymarket T0 Price-To-Beat Resolver — get the open price the instant the window opens&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It covers BTC, ETH, SOL, XRP, DOGE across 5m / 15m / 1h / 4h / 1d, with one dependency (&lt;code&gt;aiohttp&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Good to know
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The source mapping mirrors Polymarket's own settlement logic and was checked against settled markets — in practice the resolved PTB matches the settled value to the precision the site displays. The free scraper covers the after-the-fact read completely; the T0 resolver is for when you need the number at the open.&lt;/li&gt;
&lt;li&gt;You need network access to &lt;strong&gt;Binance and Polymarket&lt;/strong&gt; — some regions block Binance, so use appropriate egress.&lt;/li&gt;
&lt;li&gt;Chainlink ticks at its own cadence; the "first tick at/after the boundary" is the open the platform uses, not a continuous price.&lt;/li&gt;
&lt;li&gt;If Polymarket ever changes a settlement source, the mapping would need updating.&lt;/li&gt;
&lt;li&gt;It returns a &lt;strong&gt;price, not a profit&lt;/strong&gt; — a data primitive, not a trading bot. No orders, no financial advice.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The PTB is reconstructable at T0 as long as you respect three things: the &lt;strong&gt;per-cycle source&lt;/strong&gt; (Chainlink vs Binance open vs Binance close), the &lt;strong&gt;noon-ET / DST boundaries&lt;/strong&gt; for 4h and 1d, and the &lt;strong&gt;wait-for-close&lt;/strong&gt; rule on the daily. Get those right and you have the opening reference price in real time — the one number the API never gives you.&lt;/p&gt;

</description>
      <category>polymarket</category>
      <category>python</category>
      <category>crypto</category>
      <category>trading</category>
    </item>
    <item>
      <title>How to read a Polymarket Up/Down outcome (and the Price To Beat) before the oracle settles</title>
      <dc:creator>BlueWhale-Quant-Lab</dc:creator>
      <pubDate>Thu, 04 Jun 2026 13:03:04 +0000</pubDate>
      <link>https://dev.to/bluewhale-quant-lab/how-to-read-a-polymarket-updown-outcome-and-the-price-to-beat-before-the-oracle-settles-3kcj</link>
      <guid>https://dev.to/bluewhale-quant-lab/how-to-read-a-polymarket-updown-outcome-and-the-price-to-beat-before-the-oracle-settles-3kcj</guid>
      <description>&lt;h1&gt;
  
  
  How to read a Polymarket Up/Down outcome (and the Price To Beat) before the oracle settles
&lt;/h1&gt;

&lt;p&gt;Polymarket runs hugely popular short-window crypto markets — &lt;em&gt;"Will BTC be Up or Down at the close?"&lt;/em&gt; — on BTC, ETH, SOL, XRP and DOGE, over 5m / 15m / 1h / 4h / 1d windows. If you automate them, two numbers decide everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Price To Beat (PTB)&lt;/strong&gt; — the opening reference price. Final price &amp;gt; PTB → &lt;strong&gt;Up&lt;/strong&gt; wins; otherwise &lt;strong&gt;Down&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outcome&lt;/strong&gt; — once the window closes, the market settles Up or Down.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the part that surprises everyone: for &lt;strong&gt;crypto&lt;/strong&gt; markets, neither number is in the public API in a way you can actually use to trade. This is where they live, and how to read the outcome about &lt;strong&gt;20 seconds after close&lt;/strong&gt; instead of waiting minutes for on-chain settlement.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API gap is real (and specific)
&lt;/h2&gt;

&lt;p&gt;Polymarket &lt;em&gt;does&lt;/em&gt; expose an opening-price endpoint — but only for non-crypto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://polymarket.com/api/equity/price-to-beat/{slug}   # stocks, FX, gold, oil
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;strong&gt;crypto&lt;/strong&gt; Up-or-Down windows there is &lt;strong&gt;no public PTB endpoint&lt;/strong&gt;. And the settled Outcome? Neither the Gamma API (&lt;code&gt;gamma-api.polymarket.com&lt;/code&gt;) nor the CLOB API (&lt;code&gt;clob.polymarket.com&lt;/code&gt;) returns it quickly — on-chain Chainlink settlement can take &lt;strong&gt;5–10 minutes&lt;/strong&gt; for a 5-minute market. By then your window is long gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The outcome isn't in the HTML, either
&lt;/h2&gt;

&lt;p&gt;The obvious move — &lt;code&gt;curl&lt;/code&gt; the event page and parse it — doesn't work:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Can you read the outcome?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;On-chain oracle (&lt;code&gt;closed&lt;/code&gt; / &lt;code&gt;outcomePrices&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Authoritative, but lags by minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw HTML (&lt;code&gt;curl&lt;/code&gt; the page)&lt;/td&gt;
&lt;td&gt;No — "Outcome: Up" isn't in the HTML, any XHR, or &lt;code&gt;__NEXT_DATA__&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Front-end render&lt;/td&gt;
&lt;td&gt;Yes — React shows it within ~20s of close ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The frontend computes the result client-side from a live oracle pipe and paints it into the DOM. It's never in a response you can fetch directly. So the reliable approach is to &lt;strong&gt;become the front-end&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Become the front-end: headless Chromium
&lt;/h2&gt;

&lt;p&gt;Load the page in headless Chromium (Playwright), let React render, then poll the DOM &lt;em&gt;inside the browser&lt;/em&gt; until the outcome text appears:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;polymarket_ptb_scraper&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PolymarketScraper&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;PolymarketScraper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;btc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;t_start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;previous_window_start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t_start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t_start&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  PTB=$&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ptb&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  final=$&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;final_price&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="c1"&gt;# BTC 1778839200: DOWN  PTB=80478.47  final=80470.32
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things make this fast and stable in production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;In-browser polling&lt;/strong&gt; — &lt;code&gt;page.wait_for_function()&lt;/code&gt; at 100ms looking for &lt;code&gt;/Outcome:\s*(Up|Down)/&lt;/code&gt;, so it resolves the instant the text renders, not on a fixed Python-side timer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SPA navigation&lt;/strong&gt; — Polymarket is Next.js, so &lt;code&gt;page.goto(next_slug)&lt;/code&gt; is a client-side route change (JS/CSS stay cached) — roughly 5× faster than &lt;code&gt;page.reload()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource interception&lt;/strong&gt; — block images/fonts/media and known analytics domains, but &lt;strong&gt;never&lt;/strong&gt; block first-party JS/CSS or React won't render.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Add backoff-retry to a deadline and a periodic browser recycle (Chromium leaks a few MB per fetch) and it stays under ~300 MB RSS for hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch across timeframes: the slug
&lt;/h2&gt;

&lt;p&gt;The scrape is the easy part. Building the &lt;strong&gt;URL&lt;/strong&gt; is where most people get stuck, because the periods don't share a slug format:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Period&lt;/th&gt;
&lt;th&gt;Slug&lt;/th&gt;
&lt;th&gt;Derivable from a timestamp?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5m / 15m / 4h&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{asset}-updown-{w}-{t_start}&lt;/code&gt; (&lt;code&gt;t_start % w == 0&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;✅ yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1h&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bitcoin-up-or-down-may-29-2026-10pm-et&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ human-readable date slug&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1d&lt;/td&gt;
&lt;td&gt;&lt;code&gt;what-price-will-bitcoin-hit-on-may-29&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ different market shape&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So a deterministic builder covers 5m/15m/4h, but &lt;strong&gt;1h and 1d need a slug-discovery step&lt;/strong&gt; (look the event up by date via the Gamma events API, then feed its slug to the scraper) — and the daily is a different price-target market type entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The free build
&lt;/h2&gt;

&lt;p&gt;The open-source version covers the &lt;strong&gt;5m / 15m&lt;/strong&gt; windows for BTC/ETH/SOL/XRP/DOGE — point it at a market, get back &lt;code&gt;outcome&lt;/code&gt;, &lt;code&gt;ptb&lt;/code&gt;, &lt;code&gt;final_price&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/BlueWhale-Quant-Lab/Polymarket-crypto-updown-PTB-Outcome-Scraper" rel="noopener noreferrer"&gt;https://github.com/BlueWhale-Quant-Lab/Polymarket-crypto-updown-PTB-Outcome-Scraper&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's a clean, dependency-light reference (Playwright only) and fully standalone — no API keys, no accounts.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you need every coin and every timeframe
&lt;/h2&gt;

&lt;p&gt;The free build stops at 5m/15m on purpose — that's the part you can derive from docs. When you hit the &lt;strong&gt;1h / 4h / 1d&lt;/strong&gt; markets you run into the slug-discovery problem above, plus the daily's noon-ET / different-market-shape edge cases. I packaged the complete version — &lt;strong&gt;all coins, all timeframes (5m / 15m / 1h / 4h / 1d)&lt;/strong&gt; with the slug discovery and the period quirks handled — here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://bluewhalequantlab.gumroad.com/l/polymarket-settled-outcome-up-or-down" rel="noopener noreferrer"&gt;Polymarket PTB &amp;amp; Outcome Scraper PRO — All Coins, All Timeframes&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Good to know
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The rendered label is an &lt;strong&gt;early indicator&lt;/strong&gt; (~20s), not the final on-chain settlement. If correctness is critical, reconcile against the oracle once it lands — the free build covers the fast read completely; the on-chain reconcile is your call.&lt;/li&gt;
&lt;li&gt;It reads &lt;strong&gt;public, already-rendered&lt;/strong&gt; content. Keep request rates modest (12 fetches/hour per window has never been rate-limited in testing) and respect Polymarket's Terms of Service.&lt;/li&gt;
&lt;li&gt;If Polymarket changes its markup, the outcome regex is the one place to update.&lt;/li&gt;
&lt;li&gt;This is a &lt;strong&gt;data primitive&lt;/strong&gt; — it reads PTB and outcome. It does not place trades or promise profit; what you build on top is yours.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;For crypto Up/Down, the API won't hand you the PTB or a timely outcome — but the front-end already computes both, and you can read them ~20s after close by rendering the page yourself. The only real complexity is the slug across timeframes; solve that and you have a clean data feed for the whole BTC/ETH/SOL/XRP/DOGE board.&lt;/p&gt;

</description>
      <category>polymarket</category>
      <category>python</category>
      <category>webscraping</category>
      <category>crypto</category>
    </item>
    <item>
      <title>Building a Low-Latency Polymarket Trading Bot in Python (py-clob-client + WebSocket)</title>
      <dc:creator>BlueWhale-Quant-Lab</dc:creator>
      <pubDate>Thu, 04 Jun 2026 06:13:16 +0000</pubDate>
      <link>https://dev.to/bluewhale-quant-lab/building-a-low-latency-polymarket-trading-bot-in-python-py-clob-client-websocket-4il8</link>
      <guid>https://dev.to/bluewhale-quant-lab/building-a-low-latency-polymarket-trading-bot-in-python-py-clob-client-websocket-4il8</guid>
      <description>&lt;p&gt;This is a hands-on guide to a &lt;strong&gt;low-latency Polymarket trading bot&lt;/strong&gt; in Python. We'll wire up &lt;code&gt;py-clob-client&lt;/code&gt; for orders, the CLOB WebSocket feed for real-time book updates, and cover the infrastructure decision that beats every code optimization combined.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WebSocket feed  ──►  strategy logic  ──►  REST place/cancel  ──►  Polygon settlement
 (perceive,            (your edge)         (act, warm conn)
  ~1.2 ms info)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two channels: &lt;strong&gt;WebSocket to perceive&lt;/strong&gt; (push, no polling lag) and &lt;strong&gt;REST to act&lt;/strong&gt; (place/cancel orders). Keep both warm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 0: host near the order book (the biggest lever)
&lt;/h2&gt;

&lt;p&gt;Polymarket's CLOB API answers from &lt;strong&gt;Amsterdam&lt;/strong&gt;. I benchmarked it: ~1.2 ms from an AMS box vs ~88 ms from US-East. Before any code, deploy your bot in an Amsterdam-metro datacenter — that single choice is worth ~90 ms, more than anything below.&lt;/p&gt;

&lt;p&gt;Sanity check from your server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"connect=%{time_connect} ttfb=%{time_starttransfer}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://clob.polymarket.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I run mine on a modest AMS VPS (bots are network-bound, so a few cores + NVMe is plenty): &lt;strong&gt;&lt;a href="https://www.hostkey.com/?a_aid=6a0c726869c8f" rel="noopener noreferrer"&gt;the Amsterdam VPS I run the bot on&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Disclosure: affiliate link, I earn a referral. It's the box producing the 1.2 ms.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: authenticate with py-clob-client
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;py_clob_client.client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ClobClient&lt;/span&gt;

&lt;span class="n"&gt;HOST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://clob.polymarket.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ClobClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PRIVATE_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chain_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;137&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Polygon
&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_or_derive_api_creds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_api_creds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Reuse this client.&lt;/strong&gt; Don't reconstruct it per order — you'd pay a fresh TCP + TLS handshake every time. One warm client = keep-alive = you pay TLS once.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2: place and cancel orders
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;py_clob_client.clob_types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OrderArgs&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;py_clob_client.order_builder.constants&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BUY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SELL&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;place&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BUY&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;token_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;token_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 3: perceive via WebSocket (no polling)
&lt;/h2&gt;

&lt;p&gt;Polling &lt;code&gt;GET /book&lt;/code&gt; makes your view as stale as your loop interval. Subscribe instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;websockets&lt;/span&gt;

&lt;span class="n"&gt;WS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wss://ws-subscriptions-clob.polymarket.com/ws/market&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on_update&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;websockets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ping_interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assets_ids&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;market&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;on_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# 'book' snapshots + 'price_change' deltas
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(Endpoints/params evolve — verify against current Polymarket docs.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: the reactive loop
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;book&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;maintain_local_book&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# apply snapshot/deltas
&lt;/span&gt;    &lt;span class="n"&gt;signal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;# YOUR edge — keep it lean
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;place&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;TOKEN_ID&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;on_update&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the network is ~1.2 ms, &lt;strong&gt;you&lt;/strong&gt; can become the bottleneck. Keep &lt;code&gt;strategy()&lt;/code&gt; tight; push logging/analytics off the hot path; avoid per-tick allocations and JSON re-parsing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: don't get throttled
&lt;/h2&gt;

&lt;p&gt;The CLOB enforces rate limits (check current docs). Rules of thumb:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Get &lt;strong&gt;data over WebSocket&lt;/strong&gt;, not REST — save your REST budget for orders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch cancels&lt;/strong&gt; when the market moves; pulling stale quotes is the priority.&lt;/li&gt;
&lt;li&gt;Handle &lt;code&gt;429&lt;/code&gt; with backoff.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Host in Amsterdam&lt;/strong&gt; (~90 ms win).&lt;/li&gt;
&lt;li&gt;Reuse a warm &lt;code&gt;py-clob-client&lt;/code&gt; (keep-alive/TLS).&lt;/li&gt;
&lt;li&gt;Perceive over &lt;strong&gt;WebSocket&lt;/strong&gt;, act over &lt;strong&gt;REST&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Keep your decision path lean.&lt;/li&gt;
&lt;li&gt;Respect rate limits.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Get step 0 right and the rest is incremental. Skip it and no amount of clever code makes you competitive.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Latency figures from my own 2026 tests; code is illustrative — verify against current docs. Not financial advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>tutorial</category>
      <category>trading</category>
      <category>performance</category>
    </item>
    <item>
      <title>I Measured Polymarket's API Latency From 5 Regions — Here's the Script and the Results</title>
      <dc:creator>BlueWhale-Quant-Lab</dc:creator>
      <pubDate>Thu, 04 Jun 2026 04:17:48 +0000</pubDate>
      <link>https://dev.to/bluewhale-quant-lab/i-measured-polymarkets-api-latency-from-5-regions-heres-the-script-and-the-results-3mjm</link>
      <guid>https://dev.to/bluewhale-quant-lab/i-measured-polymarkets-api-latency-from-5-regions-heres-the-script-and-the-results-3mjm</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — I benchmarked round-trip latency to Polymarket's CLOB order API (&lt;code&gt;clob.polymarket.com&lt;/code&gt;) from five VPS regions. Amsterdam: &lt;strong&gt;~1.2 ms&lt;/strong&gt;. US-East: &lt;strong&gt;~88 ms&lt;/strong&gt;. The script is below — run it yourself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you build trading bots, you eventually ask: &lt;em&gt;where is the order book I'm racing other bots to hit?&lt;/em&gt; For Polymarket, every Reddit answer is a guess and nobody posts numbers. So I measured it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why latency to the order book matters
&lt;/h2&gt;

&lt;p&gt;Polymarket runs an &lt;strong&gt;off-chain Central Limit Order Book (CLOB)&lt;/strong&gt; that matches orders, then settles on Polygon. When a market moves, bots race to the same fill — and your round-trip time to the CLOB is, mechanically, your place in line. Code optimization is rounding error next to &lt;em&gt;physical distance to the endpoint&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmark script
&lt;/h2&gt;

&lt;p&gt;ICMP &lt;code&gt;ping&lt;/code&gt; lies behind CDNs, so I measure the &lt;strong&gt;TCP connect&lt;/strong&gt; and &lt;strong&gt;time-to-first-byte&lt;/strong&gt; on a real HTTPS request — far more honest for a web service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;

&lt;span class="n"&gt;HOST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clob.polymarket.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;N&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tcp_connect_ms&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_connection&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ttfb_ms&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_default_context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HTTPSConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getresponse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                       &lt;span class="c1"&gt;# first byte
&lt;/span&gt;    &lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bench&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;xs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p50&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p99&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.99&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;min&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TCP connect:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;bench&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tcp_connect_ms&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TTFB       :&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;bench&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ttfb_ms&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it on whatever box you've got, then on a box in another region, and compare. That delta is the whole story.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;VPS region&lt;/th&gt;
&lt;th&gt;ICMP ping (p50)&lt;/th&gt;
&lt;th&gt;TCP connect (p50)&lt;/th&gt;
&lt;th&gt;TTFB (p50)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Amsterdam (AMS)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~1.2 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~1.4 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~6 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frankfurt&lt;/td&gt;
&lt;td&gt;~8 ms&lt;/td&gt;
&lt;td&gt;~9 ms&lt;/td&gt;
&lt;td&gt;~16 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;London&lt;/td&gt;
&lt;td&gt;~10 ms&lt;/td&gt;
&lt;td&gt;~11 ms&lt;/td&gt;
&lt;td&gt;~19 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;US-East (Virginia)&lt;/td&gt;
&lt;td&gt;~88 ms&lt;/td&gt;
&lt;td&gt;~90 ms&lt;/td&gt;
&lt;td&gt;~110 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Singapore&lt;/td&gt;
&lt;td&gt;~165 ms&lt;/td&gt;
&lt;td&gt;~168 ms&lt;/td&gt;
&lt;td&gt;~195 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What the 1.2 ms means (and what it doesn't)
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;1.2 ms&lt;/strong&gt; round trip is near-LAN. Light in fiber covers ~200 km per millisecond round-trip, so 1.2 ms means the endpoint is within ~100 km of my Amsterdam box. Across the Atlantic it's physically impossible — hence US-East's ~88 ms.&lt;/p&gt;

&lt;p&gt;Being precise about the claim:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Proven:&lt;/strong&gt; the CLOB order API's point of presence is in &lt;strong&gt;Amsterdam&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;🤔 &lt;strong&gt;Inferred:&lt;/strong&gt; that the matching engine itself is co-located (the ~6 ms TTFB on order-book requests is consistent with local matching, but I can't see inside their stack).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either way the actionable conclusion is identical: &lt;strong&gt;to minimize latency to Polymarket, host in Amsterdam.&lt;/strong&gt; US-East — the most common guess — is ~90 ms slower, which no code change recovers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I run
&lt;/h2&gt;

&lt;p&gt;My bot lives on a modest VPS in a real Amsterdam-metro datacenter — that's what produces the 1.2 ms. Specs barely matter (a Polymarket bot is network-bound, not CPU-bound); the &lt;em&gt;city&lt;/em&gt; is the entire trick. A "Europe" box that actually sits in Frankfurt costs ~8 ms instead.&lt;/p&gt;

&lt;p&gt;The provider/plan I use: &lt;strong&gt;&lt;a href="https://www.hostkey.com/?a_aid=6a0c726869c8f" rel="noopener noreferrer"&gt;HostKey — Amsterdam VPS/dedicated&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: that's an affiliate link — I earn a referral if you sign up. I pay for and use the same box; the 1.2 ms above is from it.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Spin up an hourly box in Amsterdam and one in US-East, run the script on both, and watch the ~90 ms gap. If a region labeled "EU" pings &amp;gt;5 ms, it isn't really in Amsterdam — that's your proof to look elsewhere.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Numbers are from my own 2026 tests and will drift as Polymarket scales. Re-measure before betting money on it. Not financial advice.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this was useful, I'm writing more on building low-latency Polymarket bots in Python — follow along.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>showdev</category>
      <category>trading</category>
      <category>performance</category>
    </item>
    <item>
      <title>What it takes to run a Polymarket Up/Down arbitrage bot in production</title>
      <dc:creator>BlueWhale-Quant-Lab</dc:creator>
      <pubDate>Wed, 03 Jun 2026 14:37:58 +0000</pubDate>
      <link>https://dev.to/bluewhale-quant-lab/what-it-takes-to-run-a-polymarket-updown-arbitrage-bot-in-production-59co</link>
      <guid>https://dev.to/bluewhale-quant-lab/what-it-takes-to-run-a-polymarket-updown-arbitrage-bot-in-production-59co</guid>
      <description>&lt;p&gt;A "risk-free" arbitrage on Polymarket's crypto Up/Down markets is easy to describe and hard to actually run. The edge — the cross-cycle &lt;strong&gt;sandwich&lt;/strong&gt; — is real, but the money is won or lost in execution. Here's the engineering that turns the idea into a bot that survives production, and the specific things that break if you skip them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The edge in two sentences
&lt;/h2&gt;

&lt;p&gt;Polymarket runs Up/Down markets on the same asset across cycles (5m/15m/1h/4h/1d). Two cycles that settle at the same time share one final price but have &lt;strong&gt;different strikes&lt;/strong&gt; (the per-cycle "price to beat"). Buy &lt;strong&gt;UP on the lower strike&lt;/strong&gt; and &lt;strong&gt;DOWN on the higher strike&lt;/strong&gt;: at least one leg always wins, and if the settlement price lands between the two strikes, both win. If the legs cost under $1.00 combined, that's a structural edge — on paper.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;total_cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;up_price_low_strike&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;down_price_high_strike&lt;/span&gt;
&lt;span class="n"&gt;edge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;total_cost&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;1.00&lt;/span&gt;          &lt;span class="c1"&gt;# at least one leg pays $1 -&amp;gt; can't lose if both fill
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The phrase that matters is &lt;strong&gt;"if both fill."&lt;/strong&gt; Everything below is about that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1 — direction is the whole game
&lt;/h2&gt;

&lt;p&gt;Assign the sides from the strikes: UP on the &lt;em&gt;lower&lt;/em&gt; PTB, DOWN on the &lt;em&gt;higher&lt;/em&gt;. Get it backwards and the middle band — the region you most want — loses &lt;strong&gt;both&lt;/strong&gt; legs (a "death sandwich"). So the bot's correctness hinges on a trustworthy price-to-beat per cycle, and the per-cycle rules are not uniform:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;5m/15m/4h: the Chainlink price pushed at the cycle open.&lt;/li&gt;
&lt;li&gt;1h: the Binance 1-hour candle &lt;strong&gt;Open&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;1d: the Binance 1-minute candle &lt;strong&gt;Close&lt;/strong&gt; at the &lt;strong&gt;noon-ET&lt;/strong&gt; boundary (DST-aware).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A PTB that locks a second or two late can already have drifted enough to flip which strike is "lower." So you gate on lock delay and refuse to fire on a stale strike.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2 — fire both legs, fast, together
&lt;/h2&gt;

&lt;p&gt;Two legs posted seconds apart is two chances for the market to move between them, leaving you one-sided. The fix is boring but essential: one keep-alive connection, &lt;code&gt;TCP_NODELAY&lt;/code&gt;, and &lt;code&gt;asyncio.gather&lt;/code&gt; so both orders hit the book within milliseconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;place&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;leg_up&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;place&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;leg_down&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Problem 3 — when only one leg fills
&lt;/h2&gt;

&lt;p&gt;It will happen. Now you hold a &lt;strong&gt;naked leg&lt;/strong&gt; and must top up the other side — and this is where bots quietly hemorrhage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Overbuy loop.&lt;/strong&gt; Size the top-up from the positions API and it lags (reads &lt;code&gt;0&lt;/code&gt;), so you resubmit, it fills, reads &lt;code&gt;0&lt;/code&gt; again... A real run targeted 4.29 shares and bought 37.5. Size from an &lt;strong&gt;internal fill tally&lt;/strong&gt;, never a lagging read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fee drift.&lt;/strong&gt; Fees mean the two legs never match to the exact share — use a tolerance band instead of chasing crumbs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The $1 floor.&lt;/strong&gt; Orders under ~$1 notional are rejected; a 1-share crumb loops forever on &lt;code&gt;invalid amount&lt;/code&gt;. Below the floor, let it settle.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Problem 4 — timeouts must not double-fill
&lt;/h2&gt;

&lt;p&gt;On an HTTP timeout, the naive retry resubmits the order. If the first one actually landed, you've now got double exposure. Confirm against &lt;code&gt;/data/trades&lt;/code&gt; before any resubmit; treat a timeout as "unknown," not "failed."&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 5 — the 2026-04 SDK migration
&lt;/h2&gt;

&lt;p&gt;Late April 2026, Polymarket shipped a V2 SDK that broke older bots two ways. If yours started failing then, this is why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# order_version_mismatch -&amp;gt; the wire body must be built by order_to_json_v2
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;py_clob_client_v2.order_utils.model.order_data_v2&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;order_to_json_v2&lt;/span&gt;
&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;order_to_json_v2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GTC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 'not enough balance / allowance' (with funds!) -&amp;gt; sync the cache once at startup
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_balance_allowance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;BalanceAllowanceParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asset_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AssetType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;COLLATERAL&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The old hand-built &lt;code&gt;{order, owner, orderType}&lt;/code&gt; body is dead, and the CLOB caches &lt;code&gt;balance=0&lt;/code&gt; after a deposit until you force a re-read.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;With cost &amp;lt; 1 and both legs filled, a pair can't lose. &lt;strong&gt;The risk is execution&lt;/strong&gt; — one-sided fills, slippage, the gap between your two orders, rate limits while you scan and fire. A production engine is mostly machinery for shrinking that risk: direction lock, concurrent posting, a hedge/overbuy guard, timeout idempotency, and staying under the API limits. It is not a money printer; prediction-market trading can lose money, and you should paper-test before risking real funds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The complete engine
&lt;/h2&gt;

&lt;p&gt;Everything above — three execution routes (immediate / pre-place / downstream-lock), the dual engine (arbitrage + strangle), the hedge-leg hound for one-sided fills, live Oracle + market WebSockets, timeout idempotency, and both 2026-04 SDK fixes (&lt;code&gt;order_to_json_v2&lt;/code&gt; + startup &lt;code&gt;update_balance_allowance&lt;/code&gt;) wired in — is the ready-to-run &lt;strong&gt;V25&lt;/strong&gt; build. Bilingual, configurable, fill your keys and run:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://bluewhalequantlab.gumroad.com/l/polymarket-updown-sandwich-bot-v25" rel="noopener noreferrer"&gt;https://bluewhalequantlab.gumroad.com/l/polymarket-updown-sandwich-bot-v25&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Respect that the strategy is the easy 20% and the execution is the other 80%.&lt;/p&gt;

</description>
      <category>python</category>
      <category>trading</category>
      <category>architecture</category>
      <category>crypto</category>
    </item>
    <item>
      <title>Fix Polymarket order_version_mismatch and not-enough-balance (2026-04 SDK upgrade)</title>
      <dc:creator>BlueWhale-Quant-Lab</dc:creator>
      <pubDate>Wed, 03 Jun 2026 01:16:23 +0000</pubDate>
      <link>https://dev.to/bluewhale-quant-lab/fix-polymarket-orderversionmismatch-and-not-enough-balance-2026-04-sdk-upgrade-29b</link>
      <guid>https://dev.to/bluewhale-quant-lab/fix-polymarket-orderversionmismatch-and-not-enough-balance-2026-04-sdk-upgrade-29b</guid>
      <description>&lt;p&gt;If your Polymarket Up/Down bot started throwing &lt;strong&gt;&lt;code&gt;order_version_mismatch&lt;/code&gt;&lt;/strong&gt; or &lt;strong&gt;&lt;code&gt;not enough balance / allowance&lt;/code&gt;&lt;/strong&gt; (with funds in your wallet) in late April 2026, you hit the V2 SDK upgrade. Here are both fixes, plus a minimal PTB → signal → order quickstart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1 — &lt;code&gt;order_version_mismatch&lt;/code&gt;: serialize with &lt;code&gt;order_to_json_v2&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The order wire body changed. The old hand-built JSON (&lt;code&gt;{"order": ..., "owner": ..., "orderType": ...}&lt;/code&gt;) is rejected. Serialize the signed order with &lt;code&gt;order_to_json_v2&lt;/code&gt; from &lt;strong&gt;&lt;code&gt;py_clob_client_v2&lt;/code&gt;&lt;/strong&gt; — the V2 wire adds &lt;code&gt;timestamp&lt;/code&gt;/&lt;code&gt;metadata&lt;/code&gt;/&lt;code&gt;builder&lt;/code&gt;, &lt;code&gt;postOnly&lt;/code&gt;/&lt;code&gt;deferExec&lt;/code&gt;, and an integer salt.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;py_clob_client_v2.order_utils.model.order_data_v2&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;order_to_json_v2&lt;/span&gt;
&lt;span class="n"&gt;signed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BUY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;body&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;order_to_json_v2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GTC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# POST `body` to /order with create_level_2_headers(...)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fix 2 — &lt;code&gt;not enough balance / allowance&lt;/code&gt;: sync once at startup
&lt;/h2&gt;

&lt;p&gt;After a deposit, the on-chain balance updates but the CLOB cache doesn't refresh — &lt;code&gt;get_balance_allowance&lt;/code&gt; returns &lt;code&gt;balance=0&lt;/code&gt; and orders are rejected. Force a re-read once at startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;py_clob_client_v2&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BalanceAllowanceParams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AssetType&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_balance_allowance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;BalanceAllowanceParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asset_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AssetType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;COLLATERAL&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The strategy it powers — cross-cycle sandwich
&lt;/h2&gt;

&lt;p&gt;Two Up/Down markets on the same asset settling at the same time but on different cycles (5m/15m/1h/4h/1d) have different strikes (PTB). Buy &lt;strong&gt;UP on the lower strike&lt;/strong&gt; and &lt;strong&gt;DOWN on the higher strike&lt;/strong&gt;: at least one leg always wins, and if settlement lands between the strikes, both win (~2×). Under $1.00 combined, it's a structural edge — the real risk is execution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;polymarket_updown_quickstart&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Leg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sandwich_signal&lt;/span&gt;
&lt;span class="nf"&gt;sandwich_signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Leg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;15m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ptb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;70000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;up_price&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;down_price&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.47&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="nc"&gt;Leg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;ptb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;70120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;up_price&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;down_price&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.44&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# -&amp;gt; buy UP on 15m + DOWN on 1h, total_cost 0.99, edge=True, sweet_band (70000,70120)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo (free, MIT, 8 tests, runnable demo):&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/BlueWhale-Quant-Lab/polymarket-updown-arbitrage-quickstart" rel="noopener noreferrer"&gt;https://github.com/BlueWhale-Quant-Lab/polymarket-updown-arbitrage-quickstart&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  From quickstart to a running bot
&lt;/h2&gt;

&lt;p&gt;The quickstart is the minimal shape. A real deployment also needs live price/orderbook feeds, multiple execution routes, a hedge guard for one-sided fills, timeout idempotency, and rate-limit handling. If you want the complete, ready-to-run engine with both SDK fixes wired in, there's a full V25 build — but the quickstart above stands on its own for learning the flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Post-April-2026 Polymarket: build order bodies with &lt;code&gt;order_to_json_v2&lt;/code&gt;, and call &lt;code&gt;update_balance_allowance&lt;/code&gt; once at startup. Those two lines unbreak most Up/Down bots.&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>trading</category>
      <category>crypto</category>
    </item>
    <item>
      <title>The Polymarket cross-cycle sandwich: a structural arbitrage in Up/Down markets</title>
      <dc:creator>BlueWhale-Quant-Lab</dc:creator>
      <pubDate>Tue, 02 Jun 2026 17:45:48 +0000</pubDate>
      <link>https://dev.to/bluewhale-quant-lab/the-polymarket-cross-cycle-sandwich-a-structural-arbitrage-in-updown-markets-387l</link>
      <guid>https://dev.to/bluewhale-quant-lab/the-polymarket-cross-cycle-sandwich-a-structural-arbitrage-in-updown-markets-387l</guid>
      <description>&lt;p&gt;Search "Polymarket arbitrage" and you mostly find cross-venue bots (Kalshi vs Polymarket) or the textbook buy-YES-and-NO trade. There's a different edge hiding &lt;em&gt;inside&lt;/em&gt; Polymarket's own crypto Up/Down markets — the &lt;strong&gt;cross-cycle sandwich&lt;/strong&gt;. Here's the strategy from first principles, with an open-source scanner.&lt;/p&gt;

&lt;h2&gt;
  
  
  The key observation
&lt;/h2&gt;

&lt;p&gt;Polymarket runs Up/Down markets on the same asset across cycle lengths: 5m, 15m, 1h, 4h, 1d. Two cycles of &lt;strong&gt;different lengths can end at the same wall-clock time&lt;/strong&gt; (&lt;code&gt;t_end&lt;/code&gt;). They then settle against the &lt;strong&gt;same final price &lt;code&gt;P&lt;/code&gt;&lt;/strong&gt; — but because they opened at different times, each has its own &lt;strong&gt;price to beat&lt;/strong&gt; (PTB / strike).&lt;/p&gt;

&lt;p&gt;That difference in strikes, for the same asset settling on the same &lt;code&gt;P&lt;/code&gt;, is the opportunity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade
&lt;/h2&gt;

&lt;p&gt;Two same-&lt;code&gt;t_end&lt;/code&gt; contracts with &lt;code&gt;PTB_low &amp;lt; PTB_high&lt;/code&gt;. Buy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;UP (Yes)&lt;/strong&gt; on the &lt;strong&gt;low-PTB&lt;/strong&gt; contract, and&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DOWN (No)&lt;/strong&gt; on the &lt;strong&gt;high-PTB&lt;/strong&gt; contract.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Where &lt;code&gt;P&lt;/code&gt; lands&lt;/th&gt;
&lt;th&gt;UP wins (&lt;code&gt;P&amp;gt;PTB_low&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;DOWN wins (&lt;code&gt;P&amp;lt;PTB_high&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;legs paid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;P &amp;gt; PTB_high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PTB_low &amp;lt; P &amp;lt; PTB_high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2 (sweet)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;P &amp;lt; PTB_low&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;At least one leg always pays $1/share.&lt;/strong&gt; So if the two legs cost &lt;code&gt;total_cost &amp;lt; $1.00&lt;/code&gt; per share, you collect ≥ $1 against a &amp;lt; $1 cost in every region — a structural arbitrage — and in the middle band you're paid on &lt;strong&gt;both&lt;/strong&gt; (~2×).&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: the death sandwich
&lt;/h2&gt;

&lt;p&gt;Reverse the direction (UP on the high strike, DOWN on the low) and the middle band — the region you most want — loses &lt;strong&gt;both&lt;/strong&gt; legs. Direction is everything, which is why a reliable price-to-beat is non-negotiable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5-minute pruning trick
&lt;/h2&gt;

&lt;p&gt;Only 5m cycles opening at &lt;code&gt;:10/:25/:40/:55&lt;/code&gt; end on a 15-minute boundary and can share a &lt;code&gt;t_end&lt;/code&gt; with the bigger cycles. The other ~67% can never pair — prune them before scanning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scanner
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;polymarket_sandwich&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;find_pairs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;settle_pair&lt;/span&gt;
&lt;span class="n"&gt;pairs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find_pairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contracts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# cross-cycle, pruned, direction-correct, ranked
&lt;/span&gt;&lt;span class="n"&gt;best&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_cost&lt;/span&gt;                        &lt;span class="c1"&gt;# e.g. 0.99 -&amp;gt; arbitrage
&lt;/span&gt;&lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sweet_band&lt;/span&gt;                        &lt;span class="c1"&gt;# (PTB_low, PTB_high)
&lt;/span&gt;&lt;span class="nf"&gt;settle_pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;70060&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shares&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# {'sweet': True, 'winners': 2, 'pnl': 10.1}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo (free, MIT, 13 tests, worked example):&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/BlueWhale-Quant-Lab/polymarket-cross-cycle-sandwich-arbitrage" rel="noopener noreferrer"&gt;https://github.com/BlueWhale-Quant-Lab/polymarket-cross-cycle-sandwich-arbitrage&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;cost &amp;lt; 1&lt;/code&gt; and &lt;strong&gt;both legs filled&lt;/strong&gt;, the pair can't lose. The real risk is &lt;strong&gt;execution&lt;/strong&gt; — getting one leg but not the other, the price moving between your two orders, slippage, rate limits. That's the engineering, not the payoff, and it's what the rest of the toolkit (price-to-beat, live feed, order book, execution, hedge guard, rate limiter) and the complete multi-route engine handle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Same asset, same settlement time, two different strikes → UP on the low strike, DOWN on the high strike; under $1 total and you've got a structural edge, with the band between strikes as your double. Get the direction from a real PTB, and respect that the hard part is execution.&lt;/p&gt;

</description>
      <category>python</category>
      <category>arbitrage</category>
      <category>trading</category>
      <category>crypto</category>
    </item>
    <item>
      <title>Polymarket rate limits are per 10 seconds, not per second (and 429 traps)</title>
      <dc:creator>BlueWhale-Quant-Lab</dc:creator>
      <pubDate>Tue, 02 Jun 2026 17:08:33 +0000</pubDate>
      <link>https://dev.to/bluewhale-quant-lab/polymarket-rate-limits-are-per-10-seconds-not-per-second-and-429-traps-35b2</link>
      <guid>https://dev.to/bluewhale-quant-lab/polymarket-rate-limits-are-per-10-seconds-not-per-second-and-429-traps-35b2</guid>
      <description>&lt;p&gt;If you've hit &lt;code&gt;429 Too Many Requests&lt;/code&gt; on the Polymarket APIs, the fix isn't just "slow down" — it's understanding how the limits are shaped and giving your client the right tooling. Here are the traps and a small open-source limiter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 1: the window is 10 seconds, not 1
&lt;/h2&gt;

&lt;p&gt;Polymarket's published limits use a &lt;strong&gt;10-second&lt;/strong&gt; window (trading also has a 10-minute sustained cap):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/book&lt;/code&gt;, &lt;code&gt;/price&lt;/code&gt;, &lt;code&gt;/midpoint&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;1,500 / 10s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/books&lt;/code&gt;, &lt;code&gt;/prices&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;500 / 10s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST/DELETE &lt;code&gt;/order&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;5,000 / 10s burst (120,000 / 10 min)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DELETE &lt;code&gt;/cancel-all&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;250 / 10s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So &lt;code&gt;/book&lt;/code&gt; is &lt;strong&gt;150 req/s sustained with a burst of 1,500&lt;/strong&gt; — not "1500/s", and not "150/s with no burst". A token bucket models this exactly: refill &lt;code&gt;max / window&lt;/code&gt; per second, capacity &lt;code&gt;max&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;burst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;   &lt;span class="c1"&gt;# 150 tokens/sec, cap 1500
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Trap 2: authenticated GETs throttle much earlier
&lt;/h2&gt;

&lt;p&gt;The published numbers are for public market-data endpoints. &lt;strong&gt;Authenticated&lt;/strong&gt; reads (e.g. polling a single order) get throttled far below that — around ~10/s once you're also placing and cancelling. Budget authed reads conservatively; don't assume the 1,500/10s ceiling applies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 3: a fresh connection per request
&lt;/h2&gt;

&lt;p&gt;Every call that opens a new TCP+TLS connection is slow &lt;em&gt;and&lt;/em&gt; leans harder on the limiter. Reuse one keep-alive connection (or a small pool). TCP_NODELAY and GC control are the rest of the latency story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 4: no Retry-After / backoff
&lt;/h2&gt;

&lt;p&gt;The docs say over-limit requests are "throttled rather than rejected" — but in practice you do see &lt;code&gt;429&lt;/code&gt;. Honor &lt;code&gt;Retry-After&lt;/code&gt; when present (it can be an integer &lt;strong&gt;or&lt;/strong&gt; an HTTP date), and otherwise back off exponentially with jitter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;polymarket_rate_limit&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;parse_retry_after&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;should_retry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;backoff&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;should_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;                       &lt;span class="c1"&gt;# 429 or 5xx
&lt;/span&gt;    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;parse_retry_after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The limiter
&lt;/h2&gt;

&lt;p&gt;I packaged the documented limits, a per-endpoint token bucket, the Retry-After parser, and correct backoff into a zero-dependency MIT module (injectable clock, fully tested):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;polymarket_rate_limit&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RateLimiter&lt;/span&gt;
&lt;span class="n"&gt;lim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# 0 if you may send now, else seconds to wait
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo (free, MIT, 15 tests):&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/BlueWhale-Quant-Lab/polymarket-api-rate-limit-429-handler" rel="noopener noreferrer"&gt;https://github.com/BlueWhale-Quant-Lab/polymarket-api-rate-limit-429-handler&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The speed side — keep-alive pooling, TCP_NODELAY, GC control, TLS prewarm, and a p50/p99 latency benchmark — is a complete version, but the limiter above stands on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Model the 10-second window as a token bucket, rate-limit authed GETs conservatively, reuse connections, and honor Retry-After.&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>ratelimiting</category>
      <category>trading</category>
    </item>
    <item>
      <title>How a Polymarket bot buys 37 shares when it wanted 4 (the overbuy loop)</title>
      <dc:creator>BlueWhale-Quant-Lab</dc:creator>
      <pubDate>Tue, 02 Jun 2026 13:09:33 +0000</pubDate>
      <link>https://dev.to/bluewhale-quant-lab/how-a-polymarket-bot-buys-37-shares-when-it-wanted-4-the-overbuy-loop-2kgi</link>
      <guid>https://dev.to/bluewhale-quant-lab/how-a-polymarket-bot-buys-37-shares-when-it-wanted-4-the-overbuy-loop-2kgi</guid>
      <description>&lt;p&gt;If you run a two-sided or arbitrage bot on Polymarket, you've probably hit this: one leg of a paired position fills, the other doesn't, and when you top up the missing leg your bot buys &lt;strong&gt;way&lt;/strong&gt; more than you asked for. A real run targeted 4.29 shares and ended up holding 37.5 — an 8.7× overbuy. Here's how it happens and how to stop it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The overbuy loop
&lt;/h2&gt;

&lt;p&gt;You hold a naked leg and want to top up the hedge side, so you ask "how much do I already hold?" via the positions API and buy the difference.&lt;/p&gt;

&lt;p&gt;The trap: the indexer lags. For a few seconds after a fill it still reports your old balance — often &lt;code&gt;0&lt;/code&gt;. So:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You place a GTC top-up for the full size. It fills &lt;strong&gt;instantly&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;You re-check positions → still &lt;code&gt;0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Thinking nothing filled, you place the full size &lt;strong&gt;again&lt;/strong&gt;. Fills again.&lt;/li&gt;
&lt;li&gt;Repeat until the indexer catches up — by which point you're multiples over.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The positions API is fine for reconciliation; it's the wrong thing to size a hot re-submit loop against.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: an internal tally
&lt;/h2&gt;

&lt;p&gt;Size every attempt from your own running total of fills, updated the instant an order response reports one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;internal_filled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shares&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;           &lt;span class="c1"&gt;# call with takingAmount from each order/poll response
&lt;/span&gt;    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;internal_filled&lt;/span&gt;
    &lt;span class="n"&gt;internal_filled&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;shares&lt;/span&gt;

&lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_shares&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;internal_filled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# never re-buy what you hold
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clamp at zero so an over-fill can never produce a negative "buy more".&lt;/p&gt;

&lt;h2&gt;
  
  
  Two more traps in the same path
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Fee drift.&lt;/strong&gt; Buy 8 shares; fees leave you ~7.9 received while the other leg sits at 8.4. They never reconcile to the exact share, so "keep topping up until equal" never terminates. Use a tolerance band — within ~2%, call it balanced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The $1 floor.&lt;/strong&gt; Polymarket rejects orders under ~$1 notional. A 1-share top-up at $0.53 loops on &lt;code&gt;invalid amount&lt;/code&gt;. Below the floor, leave the crumb to settle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chasing it to a fill
&lt;/h2&gt;

&lt;p&gt;Once you know the size, you still have to land it on a venue that moves. The production pattern separates two cadences: &lt;strong&gt;poll ~5s&lt;/strong&gt; to absorb fills, &lt;strong&gt;reprice ~30s&lt;/strong&gt; to nudge your bid toward a cost ceiling — chasing only the &lt;em&gt;remaining&lt;/em&gt; shares each round, and refusing to launch a second chaser for a leg that already has one (a duplicate doubles your exposure).&lt;/p&gt;

&lt;h2&gt;
  
  
  The library
&lt;/h2&gt;

&lt;p&gt;I packaged the sizing math (shortfall + tolerance, the $1 floor, overbuy-safe &lt;code&gt;remaining_to_buy&lt;/code&gt;, a cost ceiling, and a one-call &lt;code&gt;plan_hedge&lt;/code&gt;) into a zero-dependency MIT module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;polymarket_hedge_leg&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;plan_hedge&lt;/span&gt;
&lt;span class="nf"&gt;plan_hedge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;20.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;6.75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hedge_price&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.46&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary_cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.49&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_total_cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# {'need_shares': 13.25, 'ok': True, 'skip_reason': None, 'est_cost': 6.095}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo (free, MIT, 19 tests):&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/BlueWhale-Quant-Lab/polymarket-hedge-leg-overbuy-guard" rel="noopener noreferrer"&gt;https://github.com/BlueWhale-Quant-Lab/polymarket-hedge-leg-overbuy-guard&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The async hound that chases the leg to a fill — with the overbuy-safe accounting and a dedup registry — is a complete version, but the sizing math above stands on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Never size a re-submit from a lagging positions read. Keep an internal fill tally, add a tolerance band for fees, and respect the $1 floor.&lt;/p&gt;

</description>
      <category>python</category>
      <category>trading</category>
      <category>bot</category>
      <category>api</category>
    </item>
  </channel>
</rss>
