<?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: Слава Жуланов</title>
    <description>The latest articles on DEV Community by Слава Жуланов (@__747bb5a1521).</description>
    <link>https://dev.to/__747bb5a1521</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3948735%2F55a3a157-887c-411a-83ae-64abbbb34b71.jpg</url>
      <title>DEV Community: Слава Жуланов</title>
      <link>https://dev.to/__747bb5a1521</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/__747bb5a1521"/>
    <language>en</language>
    <item>
      <title>Building an on-chain alerts bot in Python without any blockchain library</title>
      <dc:creator>Слава Жуланов</dc:creator>
      <pubDate>Sun, 24 May 2026 17:41:18 +0000</pubDate>
      <link>https://dev.to/__747bb5a1521/building-an-on-chain-alerts-bot-in-python-without-any-blockchain-library-3o1k</link>
      <guid>https://dev.to/__747bb5a1521/building-an-on-chain-alerts-bot-in-python-without-any-blockchain-library-3o1k</guid>
      <description>&lt;p&gt;PolySignal — my Telegram bot that watches Polymarket — has a small subsystem called &lt;code&gt;chain_watcher&lt;/code&gt;. It does one thing: cross-check every trade Polymarket's API reports against what actually happened on Polygon, and complain (via Sentry) when the API is wrong.&lt;/p&gt;

&lt;p&gt;It's about 280 lines of Python with one dependency — &lt;code&gt;httpx&lt;/code&gt;. No &lt;code&gt;web3.py&lt;/code&gt;, no &lt;code&gt;eth-account&lt;/code&gt;, no &lt;code&gt;eth-abi&lt;/code&gt;. I'm going to walk through how to read public on-chain events from Python with nothing but the standard library and an HTTP client, because (a) I needed to and (b) most tutorials reach for the SDK as the first step when you genuinely don't need it for a read-only watcher.&lt;/p&gt;

&lt;p&gt;This isn't an anti-SDK argument. If you're signing transactions, sending funds, or doing serious ABI work, use &lt;code&gt;web3.py&lt;/code&gt;. The case here is narrower: &lt;strong&gt;for reading specific events from a known contract, you can do it cleanly with &lt;code&gt;httpx&lt;/code&gt; + JSON-RPC.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What "watching the chain" actually means
&lt;/h2&gt;

&lt;p&gt;A blockchain node exposes a JSON-RPC API. You can ask it questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What block are you on?" → &lt;code&gt;eth_blockNumber&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;"Give me all logs matching this filter from blocks N to M" → &lt;code&gt;eth_getLogs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;"What's the transaction at this hash?" → &lt;code&gt;eth_getTransactionByHash&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For an alerts bot you only need the first two. You repeatedly poll the head of the chain and ask: "any logs from contract X, with this event signature, since the last block I checked?" When new logs come back, decode them and act.&lt;/p&gt;

&lt;p&gt;That's it. Forty lines for the polling loop, ten for the JSON-RPC client, twenty for the event decoder, the rest is glue.&lt;/p&gt;

&lt;h2&gt;
  
  
  The JSON-RPC client
&lt;/h2&gt;

&lt;p&gt;A node listens for HTTP POSTs to its RPC URL. Each call is a single JSON body. You don't need an SDK; you need an HTTP client and a little patience.&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;_ChainRPC&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;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;15.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&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;_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url&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;_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&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="n"&gt;timeout&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;_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;_call&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;object&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;attempt&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="mi"&gt;3&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;_id&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;try&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="k"&gt;await&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;_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&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;_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&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;jsonrpc&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;2.0&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;id&lt;/span&gt;&lt;span class="sh"&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;_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&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;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&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="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;body&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;error&lt;/span&gt;&lt;span class="sh"&gt;"&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;RuntimeError&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;RPC &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;RuntimeError&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;attempt&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;span class="k"&gt;raise&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;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&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="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unreachable&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;p&gt;That's the whole client. Two convenience methods on top:&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;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;block_number&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eth_blockNumber&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;return&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;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# the node returns hex strings
&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;get_logs&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;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from_block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topics&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&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;_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eth_getLogs&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;address&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fromBlock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_block&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;toBlock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_block&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;topics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why the retries? Public Polygon RPCs (the &lt;code&gt;polygon-rpc.com&lt;/code&gt; family and similar) return sporadic 5xx errors. A couple of half-second retries inside one tick smooths that out without stalling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filtering the events you actually want
&lt;/h2&gt;

&lt;p&gt;A contract emits events with a known signature. For Polymarket's CTF Exchange V2, the event that matters is &lt;code&gt;OrderFilled&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderFilled(
    bytes32 indexed orderHash,
    address indexed maker,
    address indexed taker,
    uint256 side,
    uint256 tokenId,
    uint256 makerAmountFilled,
    uint256 takerAmountFilled,
    uint256 fee
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;topics&lt;/code&gt; field of an Ethereum log carries up to four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Keccak hash of the event signature (&lt;code&gt;topics[0]&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Each &lt;code&gt;indexed&lt;/code&gt; parameter, padded to 32 bytes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For &lt;code&gt;OrderFilled&lt;/code&gt;, &lt;code&gt;topics[0]&lt;/code&gt; is a known constant, and &lt;code&gt;topics[2]&lt;/code&gt; / &lt;code&gt;topics[3]&lt;/code&gt; are the maker and taker addresses (left-padded to 32 bytes). To find "any fill involving a watched wallet," I make two &lt;code&gt;eth_getLogs&lt;/code&gt; calls — one filtering by the maker topic, one by the taker:&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;for&lt;/span&gt; &lt;span class="n"&gt;filter_topics&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ORDER_FILLED_TOPIC&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="n"&gt;watched_address_topics&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;         &lt;span class="c1"&gt;# as maker
&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ORDER_FILLED_TOPIC&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="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;watched_address_topics&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;   &lt;span class="c1"&gt;# as taker
&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;raw_logs&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;rpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_logs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CONTRACT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from_block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filter_topics&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;watched_address_topics&lt;/code&gt; is a list of 32-byte left-padded addresses. The node does the heavy lifting; you receive only matching logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decoding a log
&lt;/h2&gt;

&lt;p&gt;A log looks like &lt;code&gt;{topics: [...], data: "0x...", transactionHash: "0x...", ...}&lt;/code&gt;. Topics are 32-byte hex; the &lt;code&gt;data&lt;/code&gt; field is the concatenation of all non-indexed parameters, each padded to 32 bytes. Decoding doesn't need an ABI library for a known fixed-layout event — slice the hex.&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;def&lt;/span&gt; &lt;span class="nf"&gt;decode_order_filled&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="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OrderFill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;topics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;removeprefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;64&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;i&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&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;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OrderFill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;tx_hash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transactionHash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;order_hash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;topics&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;maker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;topics&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="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;:].&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;taker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;topics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;:].&lt;/span&gt;&lt;span class="nf"&gt;lower&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="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&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;16&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="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&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="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;maker_amount_filled&lt;/span&gt;&lt;span class="o"&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;words&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="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;taker_amount_filled&lt;/span&gt;&lt;span class="o"&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;words&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. A struct-shaped tuple per event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defensive bits worth keeping
&lt;/h2&gt;

&lt;p&gt;Two production realities I built into the watcher:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Bounded block span.&lt;/strong&gt; After an RPC outage, the "from last seen block to now" span can be thousands of blocks. Public nodes reject overlong &lt;code&gt;eth_getLogs&lt;/code&gt; queries; the watcher gets stuck. So each tick caps the span at a fixed maximum (1000 blocks for me — about 30 minutes at Polygon's ~2s/block) and the watcher catches up in chunks across successive ticks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Cross-check loop, not duplicate ingestion.&lt;/strong&gt; I'm not using the chain as my primary data source; I'm using it to &lt;em&gt;verify&lt;/em&gt; the API. The watcher records on-chain fills, waits a few minutes for the API to report the same trade, and complains via Sentry if the API never did. A single buggy API response throws a signal I'll see; if the chain were silent and the API were silent, neither layer would notice.&lt;/p&gt;

&lt;p&gt;This is the part where SDK-less actually beats SDK: there's no library between me and the bytes. When something goes wrong, the trace is short.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to skip the SDK
&lt;/h2&gt;

&lt;p&gt;To be honest about the trade:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't skip if you're sending transactions.&lt;/strong&gt; Nonce management, gas estimation, signing — that's exactly what &lt;code&gt;web3.py&lt;/code&gt; is for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't skip if you need ABI introspection.&lt;/strong&gt; Dynamic ABI parsing is genuinely a library-grade problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't skip if you want one client that handles ETH + several L2s with their quirks.&lt;/strong&gt; A library normalises that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a passive watcher of a single event on a single chain, none of those apply. Eight imports, one HTTP client, ~280 lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The watcher in the wild
&lt;/h2&gt;

&lt;p&gt;This runs inside &lt;a href="https://t.me/PolySignalAlertsBot?start=longread3_devto" rel="noopener noreferrer"&gt;PolySignal&lt;/a&gt;, my Telegram bot that alerts users when top wallets on Polymarket make a trade. The &lt;code&gt;chain_watcher&lt;/code&gt; is the part that lets me sleep at night when Polymarket's API has its periodic moments.&lt;/p&gt;

&lt;p&gt;If you want to read the full module, it's about 280 lines including comments and the cross-check loop. Happy to paste a Gist or answer questions in the comments.&lt;/p&gt;

</description>
      <category>python</category>
      <category>blockchain</category>
      <category>showdev</category>
      <category>polygon</category>
    </item>
    <item>
      <title>I built a paid Telegram bot. Here's what Telegram Stars actually pay.</title>
      <dc:creator>Слава Жуланов</dc:creator>
      <pubDate>Sun, 24 May 2026 17:05:48 +0000</pubDate>
      <link>https://dev.to/__747bb5a1521/i-built-a-paid-telegram-bot-heres-what-telegram-stars-actually-pay-3fo</link>
      <guid>https://dev.to/__747bb5a1521/i-built-a-paid-telegram-bot-heres-what-telegram-stars-actually-pay-3fo</guid>
      <description>&lt;p&gt;A few months ago I launched a paid Telegram bot. No Stripe, no checkout page, no app-store account. The whole subscription runs on Telegram Stars — Telegram's own in-app virtual currency. People had questions. The most useful answer was the math, which nobody publishes openly. So here it is.&lt;/p&gt;

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

&lt;p&gt;PolySignal is a Telegram bot that alerts users when top wallets on Polymarket make a trade. It has a free tier and two paid tiers (Signal 150⭐/month, Pro 750⭐/month). Subscriptions are recurring, billed every 30 days, cancellable in two taps inside Telegram. No payment provider, no checkout flow, no shipping address fields.&lt;/p&gt;

&lt;p&gt;If you've never sold anything in Telegram: you're missing one of the cleanest payment surfaces on the internet. There's also one inconvenience hidden in the math. Both worth knowing about.&lt;/p&gt;

&lt;h2&gt;
  
  
  How users pay
&lt;/h2&gt;

&lt;p&gt;A Telegram user buys "Stars" — Telegram's in-app currency — through Apple IAP, Google IAP, or Telegram's web purchase. They cost roughly $0.02 each at retail (e.g., 75⭐ for $1.49, depending on bundle). The user spends those Stars on paid bots. From the user's perspective: tap, confirm, pay with the Apple/Google account already linked to the phone. No friction. No "I need a credit card." Zero fields to fill.&lt;/p&gt;

&lt;p&gt;This is, by some distance, the lowest-friction global subscription payment surface I've shipped on. There are users who pay for PolySignal that I'd never have reached with a Stripe checkout — same Telegram, never typed a card number, in the same flow as buying premium stickers.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the developer gets paid
&lt;/h2&gt;

&lt;p&gt;Here the math gets specific. The user pays via Apple/Google → Apple/Google take 30% → Telegram takes a cut → what remains lands in your Telegram-bot Stars balance. You then withdraw via &lt;a href="https://fragment.com/" rel="noopener noreferrer"&gt;Fragment&lt;/a&gt; (Telegram's marketplace) into TON, the network coin, and convert from there.&lt;/p&gt;

&lt;p&gt;The widely cited dev-side rate: &lt;strong&gt;about $0.013 per Star&lt;/strong&gt; — the value of one Star after Apple/Google and Telegram have taken theirs.&lt;/p&gt;

&lt;p&gt;Which means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Signal at 150⭐/month → about $1.95 net to me.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pro at 750⭐/month → about $9.75 net to me.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If those numbers look small, that's because they are. Two caveats:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The 150 / 750 prices are &lt;strong&gt;deliberately low.&lt;/strong&gt; I'm collecting conversion data on an unproven product; I'd rather see whether people convert at all before optimising revenue per user. The Stars rate is what it is, but my retail price is a knob I'll turn once I have churn data.&lt;/li&gt;
&lt;li&gt;Margin on the dev side is real: ~95% gross. There's no per-user infrastructure cost worth mentioning, no support team, no advertising spend. The question is volume, not unit economics.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The payout side: where Stars-billed gets less convenient
&lt;/h2&gt;

&lt;p&gt;Here's the part nobody mentions when they pitch you on Telegram Stars: &lt;strong&gt;how you actually withdraw the money is its own story.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fragment is the bridge from your Telegram-bot Stars balance to actual currency. It works, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Minimum withdrawal is 1,000 Stars&lt;/strong&gt; (~$13).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First-time withdrawals are delayed 21 days&lt;/strong&gt; — the Stars you earn today won't even be eligible for withdrawal for three weeks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payout is in TON, not USD.&lt;/strong&gt; Fragment sells your Stars for TON; you then convert TON → fiat through an exchange with its own KYC and fees.&lt;/li&gt;
&lt;li&gt;Fragment requires Telegram login + a connected TON wallet. Identity verification at higher transaction sizes is "not publicly documented," by which I mean: you'll discover it when you cross a threshold.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're used to Stripe — daily payouts, direct deposit, no crypto in the chain — this feels archaic. If you're used to running a crypto-native business — TON is fine, this is just another rail.&lt;/p&gt;

&lt;p&gt;The honest summary: &lt;strong&gt;convenience for the user, friction for the developer.&lt;/strong&gt; The user gets a payment so low-friction it almost doesn't feel like one. The developer gets paid through a crypto off-ramp that takes three weeks for the first round.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I did this anyway
&lt;/h2&gt;

&lt;p&gt;Three reasons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Global reach with zero compliance overhead.&lt;/strong&gt; I don't have a legal entity. I haven't talked to a payment processor. I haven't filled in a single "tell us your business model" form. Telegram (with Apple/Google) handles consumer VAT collection at the Stars-purchase point. I don't see card data. I don't see banking details. I can't be liable for things I never received.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The product lives in Telegram already.&lt;/strong&gt; PolySignal's whole user surface is Telegram messages. Asking users to leave the app to enter a credit card on a third-party page would have been a substantial conversion tax — and a meaningful chunk of my audience is in countries where Stripe doesn't even land.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The bar for "an indie founder can ship this" was unusually low.&lt;/strong&gt; Telegram's Bot API documents &lt;code&gt;createInvoiceLink&lt;/code&gt; clearly. There's a &lt;code&gt;subscription_period&lt;/code&gt; parameter for recurring. Auto-renewal is a 30-day-only fixed period (annoying — no weekly, no yearly recurring). The &lt;code&gt;successful_payment&lt;/code&gt; update fires once on the initial charge and again on every monthly renewal, with an &lt;code&gt;is_first_recurring&lt;/code&gt; flag. About 200 lines of Python total for the whole billing layer. Stripe would have been more.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell another indie founder considering this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If your product lives in Telegram already, &lt;strong&gt;Stars are the right rail.&lt;/strong&gt; The conversion gain from the in-app flow probably outweighs the worse payout mechanics.&lt;/li&gt;
&lt;li&gt;If your product lives in a web app or a mobile app, &lt;strong&gt;Stars probably isn't worth the funnel detour&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Build the withdrawal pipeline (Fragment → TON → fiat) early, before money is on the table. Discovering payout delays after you've earned $300 is unpleasant.&lt;/li&gt;
&lt;li&gt;The 30-day-only subscription period is a real product constraint. Annual plans have to be one-time charges, with manual expiry extension. Plan for it.&lt;/li&gt;
&lt;li&gt;Watch what users are actually charged. Apple/Google can adjust the user-side retail price of Stars without changing your developer rate per Star. The number you see in your Telegram dashboard is the number that matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  And, finally, the bot
&lt;/h2&gt;

&lt;p&gt;PolySignal is the bot I built around all this. It sends Telegram alerts when top wallets on Polymarket make a trade. Free tier, real-time on Signal, consensus alerts on Pro. The Telegram-Stars subscription mechanic above is the same one running in production.&lt;/p&gt;

&lt;p&gt;If you're curious to see the user experience side of "billed in Stars": &lt;a href="https://t.me/PolySignalAlertsBot?start=longread2_devto" rel="noopener noreferrer"&gt;t.me/PolySignalAlertsBot&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're building a paid Telegram bot of your own and want to compare notes — leave a comment, I'll answer.&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>indiehackers</category>
      <category>startup</category>
      <category>payments</category>
    </item>
    <item>
      <title>How I track Polymarket smart money</title>
      <dc:creator>Слава Жуланов</dc:creator>
      <pubDate>Sun, 24 May 2026 08:32:43 +0000</pubDate>
      <link>https://dev.to/__747bb5a1521/how-i-track-polymarket-smart-money-4mmm</link>
      <guid>https://dev.to/__747bb5a1521/how-i-track-polymarket-smart-money-4mmm</guid>
      <description>&lt;h2&gt;
  
  
  The interesting thing about prediction markets
&lt;/h2&gt;

&lt;p&gt;Polymarket is fully on-chain. Every position, every trade, every win and loss is public information sitting on Polygon. You can look up any user's wallet, see exactly what they've bought, what they've sold, what they've cashed out on. There are no closed funds. There are no protected portfolios.&lt;/p&gt;

&lt;p&gt;It's a remarkable property of the platform. Anywhere else — equities, forex, sports — the people who consistently win don't show you their book. On Polymarket, they have no choice.&lt;/p&gt;

&lt;p&gt;And yet, most users never look at this data.&lt;/p&gt;

&lt;p&gt;That's a small mystery I've been chasing for a few months. Below is what I learned, the signals that actually mattered, and the Telegram bot I ended up building because manually tracking wallets is impossible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: who are the sharp wallets?
&lt;/h2&gt;

&lt;p&gt;The first thing is finding wallets worth watching. Polymarket publishes a public leaderboard ranked by realized PnL over different time windows: today, this week, last 30 days, all-time.&lt;/p&gt;

&lt;p&gt;A few observations from staring at it for a while:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lifetime PnL is misleading.&lt;/strong&gt; A wallet at the top of "all-time" might have ridden one massive market and been quiet since. What you actually want is &lt;strong&gt;consistency over a recent window&lt;/strong&gt; — last 30 days is the right zoom level. Sharp wallets stay near the top across multiple time windows; flash-in-the-pan wallets sit on the all-time list and nothing else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Win rate without realized PnL is misleading too.&lt;/strong&gt; A wallet that wins 80% of the time by buying 90¢ favorites is barely coming out ahead. The honest pair is win rate &lt;strong&gt;and&lt;/strong&gt; realized PnL together. Better yet, win rate by category — most sharp Polymarket wallets are specialists. Someone who calls political markets 73% of the time may be 45% on sports.&lt;/p&gt;

&lt;p&gt;This is the first lesson: the leaderboard isn't a ranking of "best traders," it's a sampling of "people who happened to do well on the metric and window you picked." Read it as a candidate list, not a verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: what do you actually look at?
&lt;/h2&gt;

&lt;p&gt;Once you have a few candidate wallets, you click into their profile (&lt;code&gt;polymarket.com/profile/0x...&lt;/code&gt;). What's there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Closed positions&lt;/strong&gt; — every market they've resolved, with realized PnL per market.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open positions&lt;/strong&gt; — what they're currently holding, mark-to-market.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Activity feed&lt;/strong&gt; — every trade with timestamp, market, side, size and price.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is more than enough to build a real signal. The trades I learned to notice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A trade much larger than the wallet's usual size.&lt;/strong&gt; A whale doing a routine $500 trade is a routine $500 trade. The same wallet putting on its largest position of the quarter — &lt;em&gt;that's&lt;/em&gt; the moment worth knowing about. The simplest version of "conviction" is just the multiple of the wallet's recent median trade size.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A trade in a market that isn't moving yet.&lt;/strong&gt; Sharp wallets often enter days before a market starts to shift. By the time the headline lands, they're already positioned. Watching the price chart against the wallet's entry timestamp is genuinely informative.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multiple sharp wallets converging on the same side of a market.&lt;/strong&gt; When one wallet has a view, it's one signal. When five wallets you've identified as sharp all take the same side within a short window — that's a pattern. It's not a guarantee of anything, but it's hard for any one of them to fake.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Position exits.&lt;/strong&gt; "Smart money is leaving" gets less attention than "smart money is entering," but it's just as informative. A wallet that sized up then quietly unwound usually saw something.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this is novel. It's how anyone watches markets where the book is public.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: why this is unsustainable manually
&lt;/h2&gt;

&lt;p&gt;For about a month I tried to track ten wallets by hand — bookmarks, a spreadsheet, refreshing pages, an ad-hoc Telegram group with myself.&lt;/p&gt;

&lt;p&gt;It doesn't work. Polymarket trades around the clock. The signal you cared about happens while you're asleep, or at lunch, or in a meeting. By the time you refresh, the trade is hours old and the price has moved.&lt;/p&gt;

&lt;p&gt;The on-chain transparency turns into a curse: the data is public, but the only way to use it is to stare at it continuously, which no human will do. What's missing is the boring infrastructure to &lt;strong&gt;watch for you&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;PolySignal is a Telegram bot that does the watching. You pick wallets from the live leaderboard (one tap each), and it pings your Telegram the moment they trade. Each alert carries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wallet, side, outcome, market.&lt;/li&gt;
&lt;li&gt;Size and price.&lt;/li&gt;
&lt;li&gt;The wallet's track record from its closed positions: win rate, realized PnL, ROI.&lt;/li&gt;
&lt;li&gt;A "conviction" line when the trade is materially bigger than the wallet's usual size.&lt;/li&gt;
&lt;li&gt;A line confirming the data is on-chain on Polygon — the source is verifiable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's read-only. It doesn't connect to your wallet, it doesn't trade, it doesn't ask for keys. It's an information service over public data — closer in spirit to a market-data terminal than to a typical crypto-Telegram tip channel.&lt;/p&gt;

&lt;p&gt;Two features matter more than they look:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The activation moment.&lt;/strong&gt; A new user follows five wallets in one tap and immediately sees a representative alert, so they understand the product before any trade happens. The hardest part of an alert tool is the empty-watchlist day-one experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consensus alerts.&lt;/strong&gt; When three or more of the wallets you follow take the same side and outcome of the same market within a day, you get a single consolidated alert. This is the signal you can't easily get from looking at any individual profile.&lt;/p&gt;

&lt;p&gt;The free tier batches trades every 15 minutes. The paid tiers (priced cheaply on Telegram Stars while we collect conversion data) get real-time alerts, category filters, a daily digest, and custom alert rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small honest note
&lt;/h2&gt;

&lt;p&gt;I've tried to position PolySignal as what it actually is: an information tool, not advice. Polymarket isn't available in every region — that's the user's responsibility to check, and the bot says so. Past results of any wallet are past results. None of this is a prediction or a recommendation.&lt;/p&gt;

&lt;p&gt;If you find the public-on-chain-data observation interesting and you've been on Polymarket: try the bot. Tell me what's broken. The link is &lt;code&gt;t.me/PolySignalAlertsBot?start=devto&lt;/code&gt; — feedback comes to me directly.&lt;/p&gt;

</description>
      <category>polymarket</category>
      <category>crypto</category>
      <category>python</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
