<?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>Forem: Слава Жуланов</title>
    <description>The latest articles on Forem by Слава Жуланов (@__747bb5a1521).</description>
    <link>https://forem.com/__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>Forem: Слава Жуланов</title>
      <link>https://forem.com/__747bb5a1521</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/__747bb5a1521"/>
    <language>en</language>
    <item>
      <title>当三个高水平钱包押向同一边：Polymarket 上的共识信号到底意味着什么</title>
      <dc:creator>Слава Жуланов</dc:creator>
      <pubDate>Tue, 26 May 2026 05:12:39 +0000</pubDate>
      <link>https://forem.com/__747bb5a1521/dang-san-ge-gao-shui-ping-qian-bao-ya-xiang-tong-bian-polymarket-shang-de-gong-shi-xin-hao-dao-di-yi-wei-zhao-shi-yao-34nl</link>
      <guid>https://forem.com/__747bb5a1521/dang-san-ge-gao-shui-ping-qian-bao-ya-xiang-tong-bian-polymarket-shang-de-gong-shi-xin-hao-dao-di-yi-wei-zhao-shi-yao-34nl</guid>
      <description>&lt;p&gt;跟踪 Polymarket 上的特定钱包几周之后，我注意到一个模式。绝大多数时候，一个高水平钱包成交只是一个信号——如果你本来就信这个钱包的判断，那它有意思，不然就是一条数据。但偶尔会出现另一种情况：&lt;strong&gt;我标记为高水平的钱包里，有三到四个，会在一天之内押向同一个市场的同一方向。&lt;/strong&gt; 不同的钱包。不同的规模。同样的方向。&lt;/p&gt;

&lt;p&gt;这种巧合比任何单一笔交易都更难用偶然来解释。所以我把它做成了 PolySignal 里独立的一种提醒类型。这篇就来说说什么是共识信号——以及它&lt;strong&gt;不是&lt;/strong&gt;什么。&lt;/p&gt;

&lt;h2&gt;
  
  
  前提
&lt;/h2&gt;

&lt;p&gt;Polymarket 完全运行在链上。每个仓位都是公开的。常年排在排行榜前列的钱包是可观察的：交易的是哪些市场、什么时候交易、规模多大、最终结算成什么样。这里没有秘密，只是人肉跟踪非常费劲。&lt;/p&gt;

&lt;p&gt;当你同时关注一小撮高水平钱包时，他们每一笔交易都携带了关于"该钱包对这件事怎么看"的一点信息。单个交易者的观点是一个数据点。值得问的问题是：&lt;em&gt;当多个相互独立的交易者押向同一边，事情会发生什么变化？&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  为什么阈值定在三
&lt;/h2&gt;

&lt;p&gt;PolySignal 的共识提醒触发条件是：用户关注的钱包里，&lt;strong&gt;至少有三个&lt;/strong&gt;，在 24 小时窗口内，押向同一个市场、同一个 outcome 的同一方向。&lt;/p&gt;

&lt;p&gt;为什么是三？&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;二是统计基线上的常态巧合。&lt;/strong&gt; 高水平钱包关注的市场互相重叠；在任何一个活跃日，排行榜里有一半的人都在那两三个热度最高的问题上有仓位。两个钱包同向，几乎只是噪声里的常态。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;三个钱包要"碰巧"同向，结构上很难。&lt;/strong&gt; 它要求三个独立判断在同一个市场、同一个窗口、同一边——而且 Polymarket 上的高水平钱包通常是专才（有人专做政治，有人专做加密，有人专做体育），所以三个钱包&lt;strong&gt;跨&lt;/strong&gt;领域汇流就更稀有。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;四个一度也试过，但过滤得太狠。&lt;/strong&gt; 我宁愿轻度多提醒，也不要错过这种事件。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这个阈值是可配置的。三是默认值，从我观察的几周看，它跑出来的模式是合理的。&lt;/p&gt;

&lt;h2&gt;
  
  
  它&lt;strong&gt;不是&lt;/strong&gt;什么
&lt;/h2&gt;

&lt;p&gt;共识信号&lt;strong&gt;不是预测&lt;/strong&gt;。不是推荐。它是一种描述：你关注的钱包里有三个、且都拥有可信的历史结算战绩，刚好在同一个市场押向同一边。过去的巧合不保证未来还会相关。这条信号告诉你的是"谁站在了哪一边"；接下来怎么做，是你自己的决定。&lt;/p&gt;

&lt;p&gt;之所以要把这句话说清楚，是因为人看到"我关注的三个钱包都押向同一边"时，本能会把它读成"这一边一定会结算正确"的证据。有时确实是。有时不是。这个模式的价值是统计意义上的，不是确定性的。&lt;/p&gt;

&lt;p&gt;我在实际跑的过程中观察到两种典型的失败模式：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;在"明显的一边"上凑齐共识。&lt;/strong&gt; 当一个市场拿到一个新闻催化、价格被推到 90 美分时，会有大量钱包在 91 美分扎堆进场，赌剩下的那一点 gap 会闭合。这时三个"高水平"钱包押向明显的那一边，&lt;strong&gt;不&lt;/strong&gt;是信号——他们跟所有人看到的是同一条新闻，只是他们恰好在你的关注列表里。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;迟到的共识。&lt;/strong&gt; 三个钱包押向一个已经走了 30 个点的市场——这告诉你的是共识&lt;strong&gt;曾经在哪&lt;/strong&gt;，而不是它接下来要去哪。当共识在行情走完之后才出现时，它的信号价值会迅速衰减。&lt;/p&gt;

&lt;p&gt;事后看，缓解措施很直白：市场安静时提醒更有意思，市场嘈杂时提醒就没那么有意思。我考虑过加一个"当前市场在窗口内已经移动 &amp;gt; X"的抑制器，但目前我宁可把所有共识事件全推出来，让读者自己做判断。&lt;/p&gt;

&lt;h2&gt;
  
  
  它真正能捕捉到的
&lt;/h2&gt;

&lt;p&gt;共识提醒擅长捕捉的几类模式：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;早期进入还没火起来的市场。&lt;/strong&gt; 一个新市场上线，你关注的三个钱包各自独立地决定"这里值得开一个仓"。市场价格还在 50/50 附近。这类信号是信息含量最稳定的一类。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;方向上的协同转折。&lt;/strong&gt; 一个市场一直在某个价格带里震荡，某一天，你的三个钱包同时押向同一边、几乎同一时刻开仓。多半是发生了什么、他们都注意到了。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;长结算周期市场上的高水平钱包卡位。&lt;/strong&gt; 几个月才结算的市场，有时候会在结算前几周就出现高水平钱包的共识，市场其它部分要再过几周才反应过来。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;它捕捉不到的：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;钱包在&lt;em&gt;离场&lt;/em&gt;。&lt;/strong&gt; 离场和入场一样有信息量，甚至更高——但入场端的共识检测不能直接显示"大家都在拆仓"。后面我大概会单独加一种离场共识提醒。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;单一钱包的高信念交易。&lt;/strong&gt; 一个高水平钱包在几个月内下了它最大的一仓——这是行为意义上的高信念信号——属于另一类信号；PolySignal 里把它处理成单笔交易上的标签，不放进共识。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  实现
&lt;/h2&gt;

&lt;p&gt;技术好奇的同学可以看一下：检测逻辑是每条要推送的提醒上跑一次 DB 查询。对每一笔新交易，我查这位用户关注的其它钱包里，最近窗口内有没有 (市场, 方向, outcome) 完全一致的新交易。如果不同钱包计数跨过阈值，就触发共识提醒；在消息发出之前，先把一条 &lt;code&gt;SentConsensus&lt;/code&gt; 记录提交进库，这样即使发送失败也不会重复推送。&lt;/p&gt;

&lt;p&gt;代码不多——真正费心思的是阈值和窗口的选择，而不是实现本身。&lt;/p&gt;

&lt;h2&gt;
  
  
  我自己用它的方式
&lt;/h2&gt;

&lt;p&gt;每次共识提醒出现在我的 Telegram 时，我把它当成一个 flag，不是一个指令。我会问自己：&lt;em&gt;这三个钱包看到了什么、是我还没看到的？&lt;/em&gt;也许他们读了我错过的报道。也许他们在某个领域里有专业判断，市场还没定价进去。也许他们三个看的是同一个不完整的图景——那么问题就变成，我对这件事有没有自己更强的独立判断。&lt;/p&gt;

&lt;p&gt;信号是一个问题的开头。答案，是你自己的。&lt;/p&gt;

&lt;h2&gt;
  
  
  跑在哪里
&lt;/h2&gt;

&lt;p&gt;如果你看到这里、又想在自己的 Telegram 里看到共识信号：我做的 &lt;a href="https://t.me/PolySignalAlertsBot?start=zh_longread4_devto" rel="noopener noreferrer"&gt;PolySignal&lt;/a&gt; 就是为了让我不用再在 Excel 表里人肉关联那些交易。共识提醒是 Pro 档功能，bot 的其它部分免费。&lt;/p&gt;

&lt;p&gt;诚实声明：PolySignal 报告的是公开的链上活动。本服务为信息工具，不提供任何投资建议，不保管资金或私钥。Polymarket 并非在所有国家/地区可用，请自行确认。&lt;/p&gt;

</description>
      <category>polymarket</category>
      <category>crypto</category>
      <category>data</category>
    </item>
    <item>
      <title>不引入任何区块链库，用 Python 写一个链上提醒 bot</title>
      <dc:creator>Слава Жуланов</dc:creator>
      <pubDate>Mon, 25 May 2026 21:41:01 +0000</pubDate>
      <link>https://forem.com/__747bb5a1521/bu-yin-ru-ren-he-qu-kuai-lian-ku-yong-python-xie-ge-lian-shang-ti-xing-bot-a9l</link>
      <guid>https://forem.com/__747bb5a1521/bu-yin-ru-ren-he-qu-kuai-lian-ku-yong-python-xie-ge-lian-shang-ti-xing-bot-a9l</guid>
      <description>&lt;p&gt;PolySignal——我那个监控 Polymarket 的 Telegram bot——里有一个小子系统叫 &lt;code&gt;chain_watcher&lt;/code&gt;。它只做一件事：把 Polymarket API 报上来的每一笔交易，和 Polygon 链上真实发生的事情做交叉核对，一旦 API 数据对不上就通过 Sentry 报警。&lt;/p&gt;

&lt;p&gt;大约 280 行 Python，唯一依赖是 &lt;code&gt;httpx&lt;/code&gt;。没有 &lt;code&gt;web3.py&lt;/code&gt;，没有 &lt;code&gt;eth-account&lt;/code&gt;，也没有 &lt;code&gt;eth-abi&lt;/code&gt;。下面我会把"从 Python 里只用标准库 + 一个 HTTP 客户端读公开链上事件"这件事走一遍，因为 (a) 我自己确实需要做，(b) 大多数教程一上来就让你去装 SDK，但对于只读型的监听场景，SDK 完全不必。&lt;/p&gt;

&lt;p&gt;这不是一篇反 SDK 的檄文。如果你要签交易、发资金、做严肃的 ABI 工作，请用 &lt;code&gt;web3.py&lt;/code&gt;。我这里的边界很窄：&lt;strong&gt;只是从一个已知合约里读特定事件，纯 &lt;code&gt;httpx&lt;/code&gt; + JSON-RPC 就能干得很干净。&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  "监听链上"到底是在干什么
&lt;/h2&gt;

&lt;p&gt;一个区块链节点对外暴露 JSON-RPC 接口。你可以向它问问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"你当前在哪个 block？" → &lt;code&gt;eth_blockNumber&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;"把符合这个 filter、区块号 N 到 M 之间的全部 log 给我" → &lt;code&gt;eth_getLogs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;"这个 hash 对应的交易长什么样？" → &lt;code&gt;eth_getTransactionByHash&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;对一个提醒 bot 而言，只需要前两个。你反复轮询链顶，问："从我上次检查过的那个 block 开始到现在，合约 X 里有没有符合这个事件签名的新 log？"如果回来有新 log，解码、推消息，就这样。&lt;/p&gt;

&lt;p&gt;四十行轮询循环，十行 JSON-RPC 客户端，二十行事件解码，剩下都是粘合逻辑。&lt;/p&gt;

&lt;h2&gt;
  
  
  JSON-RPC 客户端
&lt;/h2&gt;

&lt;p&gt;一个节点就是一个监听 HTTP POST 的 RPC URL。每次调用就是一个 JSON body。你不需要 SDK，只需要一个 HTTP 客户端和一点耐心。&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;整个客户端就是这些。上面再加两个简便方法：&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;为什么要做重试？公共 Polygon RPC（&lt;code&gt;polygon-rpc.com&lt;/code&gt; 那一类）会零零散散返回 5xx。在同一个 tick 内做两次半秒级重试，可以平滑掉这种波动，又不会把整个轮询卡住。&lt;/p&gt;

&lt;h2&gt;
  
  
  过滤你真正想要的事件
&lt;/h2&gt;

&lt;p&gt;合约触发的事件带有一个固定签名。对 Polymarket 的 CTF Exchange V2 来说，我关心的是 &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;以太坊 log 的 &lt;code&gt;topics&lt;/code&gt; 字段最多承载四个东西：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;事件签名的 Keccak 哈希（&lt;code&gt;topics[0]&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;每一个 &lt;code&gt;indexed&lt;/code&gt; 参数，左侧补齐到 32 字节。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;对 &lt;code&gt;OrderFilled&lt;/code&gt; 来说，&lt;code&gt;topics[0]&lt;/code&gt; 是一个已知常量，&lt;code&gt;topics[2]&lt;/code&gt; / &lt;code&gt;topics[3]&lt;/code&gt; 是 maker 和 taker 地址（左侧补到 32 字节）。要找"任何涉及到我关注钱包的成交"，我会发两次 &lt;code&gt;eth_getLogs&lt;/code&gt;——一次按 maker topic 过滤，一次按 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; 是一组左侧补齐到 32 字节的地址 hex。重活节点替你干，你拿到的只有命中的那些 log。&lt;/p&gt;

&lt;h2&gt;
  
  
  解码一条 log
&lt;/h2&gt;

&lt;p&gt;一条 log 大致长成 &lt;code&gt;{topics: [...], data: "0x...", transactionHash: "0x...", ...}&lt;/code&gt;。&lt;code&gt;topics&lt;/code&gt; 是若干个 32 字节 hex；&lt;code&gt;data&lt;/code&gt; 字段是所有非 indexed 参数顺序拼接的 hex，每个参数补到 32 字节。对一个布局已知、固定字段的事件来说，解码不需要任何 ABI 库——直接切 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;就这样。每个事件返回一个 struct 风格的 tuple。&lt;/p&gt;

&lt;h2&gt;
  
  
  一些值得保留的"防御性细节"
&lt;/h2&gt;

&lt;p&gt;两个我在 watcher 里专门处理过的生产现实：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. 给区块跨度设上限。&lt;/strong&gt; RPC 抖动恢复后，"从上次看过的 block 到现在"可能跨好几千个 block。公共节点会拒绝过大的 &lt;code&gt;eth_getLogs&lt;/code&gt; 请求，watcher 就会卡在那。所以每个 tick 把跨度封顶（我用的是 1000 个 block——按 Polygon 大约每 2 秒一个 block 算，差不多 30 分钟），watcher 在后续多个 tick 里分段追上来。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. 这是一个交叉核对循环，不是重复入库。&lt;/strong&gt; 我不是把链当作主数据源，而是用链来*核对*API。watcher 把链上成交记录下来，等几分钟让 API 报上同一笔交易，如果 API 一直没有，就通过 Sentry 报警。一次有 bug 的 API 响应会触发一个我能看见的信号；如果链和 API 都静默，任何一层都看不出问题，那就麻烦了。&lt;/p&gt;

&lt;p&gt;这一段恰好是"没有 SDK"反而比"有 SDK"更强的地方：我和字节之间没有任何库。出问题的时候，trace 很短。&lt;/p&gt;

&lt;h2&gt;
  
  
  什么时候&lt;strong&gt;不&lt;/strong&gt;应该跳过 SDK
&lt;/h2&gt;

&lt;p&gt;诚实地把代价说清楚：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;要签发交易就别跳过。&lt;/strong&gt; Nonce 管理、gas 估算、签名——这就是 &lt;code&gt;web3.py&lt;/code&gt; 存在的意义。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;要做 ABI 内省就别跳过。&lt;/strong&gt; 动态 ABI 解析确实是个库级别的问题。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;要一个客户端同时跑 ETH 加好几条 L2 还要处理各自怪癖，就别跳过。&lt;/strong&gt; 库会替你把这些差异抹平。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;对一个只盯单链单事件的被动监听器来说，以上几条都不成立。八行 import，一个 HTTP 客户端，~280 行代码。&lt;/p&gt;

&lt;h2&gt;
  
  
  跑在哪里
&lt;/h2&gt;

&lt;p&gt;这套逻辑跑在 &lt;a href="https://t.me/PolySignalAlertsBot?start=zh_longread3_devto" rel="noopener noreferrer"&gt;PolySignal&lt;/a&gt; 里——这是我做的 Telegram bot，会在 Polymarket 上高水平钱包成交时给用户发提醒。&lt;code&gt;chain_watcher&lt;/code&gt; 是它里面让我能睡得着觉的那一层——尤其在 Polymarket API 偶尔抽风的时候。&lt;/p&gt;

&lt;p&gt;如果你想看完整模块，含注释和交叉核对循环大约 280 行。乐意贴 Gist，也欢迎在评论里问问题。&lt;/p&gt;

</description>
      <category>python</category>
      <category>blockchain</category>
      <category>showdev</category>
    </item>
    <item>
      <title>我做了个付费 Telegram bot。Telegram Stars 实际给开发者多少钱，我算了一笔账。</title>
      <dc:creator>Слава Жуланов</dc:creator>
      <pubDate>Mon, 25 May 2026 20:18:20 +0000</pubDate>
      <link>https://forem.com/__747bb5a1521/wo-zuo-liao-ge-fu-fei-telegram-bot-telegram-stars-shi-ji-gei-kai-fa-zhe-duo-shao-qian-wo-suan-liao-bi-zhang--3p9k</link>
      <guid>https://forem.com/__747bb5a1521/wo-zuo-liao-ge-fu-fei-telegram-bot-telegram-stars-shi-ji-gei-kai-fa-zhe-duo-shao-qian-wo-suan-liao-bi-zhang--3p9k</guid>
      <description>&lt;p&gt;几个月前我上线了一个付费 Telegram bot。没有 Stripe，没有 checkout 页面，没有任何 app store 账号。整个订阅完全跑在 Telegram Stars 上——Telegram 自己的内置虚拟货币。后来收到了挺多问题，最有意思的答案其实是那笔数学账，但没人公开写过。所以我来写。&lt;/p&gt;

&lt;h2&gt;
  
  
  背景
&lt;/h2&gt;

&lt;p&gt;PolySignal 是一个 Telegram bot，当 Polymarket 上的高水平钱包有新成交时，它会给关注者发提醒。有免费版，也有两个付费档（Signal 150⭐/月，Pro 750⭐/月）。订阅按 30 天循环计费，在 Telegram 里两下就能取消。没有支付服务商，没有收银台流程，连收货地址都不需要填。&lt;/p&gt;

&lt;p&gt;如果你从来没在 Telegram 里卖过东西：你错过的是互联网上最干净的支付界面之一。但同时，账本里也藏着一处不太方便的地方。这两件事都值得知道。&lt;/p&gt;

&lt;h2&gt;
  
  
  用户怎么付钱
&lt;/h2&gt;

&lt;p&gt;Telegram 用户先去买"Stars"——Telegram 内置虚拟货币——通过 Apple 内购、Google 内购，或 Telegram 网页内购。零售价大致是每颗 0.02 美元（比如 75⭐ 卖 1.49 美元，具体看 bundle）。然后用户拿这些 Stars 在付费 bot 里消费。在用户视角里：点一下、确认、用手机上已经绑定好的 Apple/Google 账户付款。没有摩擦。没有"我得去翻卡片"。零字段。&lt;/p&gt;

&lt;p&gt;这就是迄今我做过的产品里，全球范围内&lt;strong&gt;摩擦最低的订阅支付界面&lt;/strong&gt;。有些用户能为 PolySignal 付费，但如果让他们走 Stripe checkout，我根本接触不到这些人——同一个 Telegram，从来没输入过卡号，整个流程跟买高级表情包是同一种感觉。&lt;/p&gt;

&lt;h2&gt;
  
  
  开发者拿到多少
&lt;/h2&gt;

&lt;p&gt;这里才是具体的数学。用户通过 Apple/Google 付款 → Apple/Google 抽 30% → Telegram 再抽一刀 → 剩下的进到你 Telegram bot 的 Stars 余额。之后你用 &lt;a href="https://fragment.com/" rel="noopener noreferrer"&gt;Fragment&lt;/a&gt;（Telegram 自己的市场）把 Stars 提到 TON（TON 是 Telegram 关联的网络代币），再从 TON 换成法币。&lt;/p&gt;

&lt;p&gt;业内常引用的开发者端汇率是：&lt;strong&gt;约每颗 Star 0.013 美元&lt;/strong&gt;——这是 Apple/Google 和 Telegram 各自抽完之后，一颗 Star 在开发者手里值多少。&lt;/p&gt;

&lt;p&gt;也就是说：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Signal 150⭐/月 → 我大约净拿到 1.95 美元。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pro 750⭐/月 → 我大约净拿到 9.75 美元。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;数字看着小，因为它就是这么小。两点说明：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;150 / 750 这两个定价是&lt;strong&gt;故意定低的&lt;/strong&gt;。产品还没被验证，我现在收集的是转化数据；在优化每用户收入之前，我更想先看看到底有没有人愿意转化。Stars 的折算率是固定的，但我自己的零售定价是个旋钮，等我手里有了留存/流失数据再去拧它。&lt;/li&gt;
&lt;li&gt;开发者侧的毛利是真实的：约 95%。没有什么值得提的人均基础设施成本，没有客服团队，没有广告预算。问题在量，不在单位经济模型。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  提现这一边：Stars 收款不那么方便的部分
&lt;/h2&gt;

&lt;p&gt;下面这部分是给 Telegram Stars 站台的人通常不会说的：&lt;strong&gt;真正把钱提出来的过程，本身就是一个故事。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fragment 是从你 Telegram bot Stars 余额到真实货币之间的桥。能用，但是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;最低提现 1,000 Stars&lt;/strong&gt;（约 13 美元）。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;首次提现要延迟 21 天&lt;/strong&gt; — 今天赚到的 Stars，要等三周后才有资格被提出来。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;结算用的是 TON，不是美元。&lt;/strong&gt; Fragment 把你的 Stars 卖成 TON；接下来你自己把 TON 通过有 KYC 和手续费的交易所换成法币。&lt;/li&gt;
&lt;li&gt;Fragment 要求 Telegram 登录 + 一个绑定的 TON 钱包。交易金额到某个阈值之后还会需要身份验证——这块"没有公开文档"，意思就是：你跨过门槛那天就会发现。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果你之前用惯了 Stripe——每日打款、直接进银行账户、链路里没有任何加密资产——这套东西会让你觉得过于古旧。但如果你本来就在做加密原生业务——TON 完全 OK，就是又一条通道而已。&lt;/p&gt;

&lt;p&gt;最诚实的总结是：&lt;strong&gt;用户端方便，开发者端不方便。&lt;/strong&gt; 用户拿到的是一个低到几乎感受不到的支付流程；开发者拿到的是一条加密 off-ramp，第一轮要走三周。&lt;/p&gt;

&lt;h2&gt;
  
  
  那我为什么还是这么做了
&lt;/h2&gt;

&lt;p&gt;三个原因：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. 全球触达 + 零合规负担。&lt;/strong&gt; 我没有公司主体。我没和任何支付服务商打过交道。我没填过一张"请告诉我们你的业务模式"的表。Telegram（连同 Apple/Google）在用户买 Stars 的那个节点上就把消费 VAT 收掉了。我看不到卡数据。我看不到银行账户信息。我也无法对从来没经过我手的东西承担责任。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. 产品本来就活在 Telegram 里。&lt;/strong&gt; PolySignal 的全部用户界面都是 Telegram 消息。让用户跳出 app、去某个第三方页面填信用卡，会是一笔相当重的转化税——而且我的目标用户里有相当一部分在 Stripe 根本进不去的国家。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. "一个独立开发者能不能做出来"这条门槛异常低。&lt;/strong&gt; Telegram 的 Bot API 把 &lt;code&gt;createInvoiceLink&lt;/code&gt; 写得很清楚。循环订阅有 &lt;code&gt;subscription_period&lt;/code&gt; 参数。自动续费固定按 30 天一档（这点烦人——没有周付、没有年付的循环订阅）。&lt;code&gt;successful_payment&lt;/code&gt; 这条 update 会在首次扣款和每个月的续费时各触发一次，并带一个 &lt;code&gt;is_first_recurring&lt;/code&gt; 标记。整个计费层加起来大约 200 行 Python。Stripe 会更多。&lt;/p&gt;

&lt;h2&gt;
  
  
  给同样在考虑这条路的独立开发者一些话
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;如果你的产品本来就活在 Telegram 里，&lt;strong&gt;Stars 是正确的通道。&lt;/strong&gt; 内嵌支付带来的转化收益，多半能盖过比较麻烦的提现机制。&lt;/li&gt;
&lt;li&gt;如果你的产品在 web app 或独立移动 app 里，&lt;strong&gt;Stars 多半不值得为它绕个支付弯子&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;提现链路（Fragment → TON → 法币）请在没钱进来之前就先打通。等钱已经在账上时才发现要等三周，体验非常糟。&lt;/li&gt;
&lt;li&gt;"只能 30 天循环"是一个真实的产品约束。年付计划必须做成一次性扣款，到期后手动延期。提前为这件事在产品上做好设计。&lt;/li&gt;
&lt;li&gt;留意用户那边实际被扣了多少。Apple/Google 可以在不改变开发者侧每 Star 折算率的前提下，单方面调整 Stars 的用户零售定价。你在 Telegram 后台看到的数字才是关键。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  最后，说回那个 bot
&lt;/h2&gt;

&lt;p&gt;PolySignal 就是我围绕这一整套折腾出来的 bot。当 Polymarket 上的高水平钱包有新成交时，它把提醒推到 Telegram。免费版有；Signal 档实时推送；Pro 档有共识提醒。文章上面讲到的整套 Telegram Stars 订阅机制，就是这个产品里正在跑的同一套。&lt;/p&gt;

&lt;p&gt;如果你想从用户视角感受一下"完全用 Stars 计费"是什么样：&lt;a href="https://t.me/PolySignalAlertsBot?start=zh_longread2_devto" rel="noopener noreferrer"&gt;t.me/PolySignalAlertsBot&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;如果你也在做付费 Telegram bot，想交换一下经验——评论区留言，我会回。&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>indiehackers</category>
      <category>startup</category>
    </item>
    <item>
      <title>我是怎么跟踪 Polymarket 上的聪明钱的</title>
      <dc:creator>Слава Жуланов</dc:creator>
      <pubDate>Mon, 25 May 2026 19:57:16 +0000</pubDate>
      <link>https://forem.com/__747bb5a1521/wo-shi-zen-yao-gen-zong-polymarket-shang-de-cong-ming-qian-de-42be</link>
      <guid>https://forem.com/__747bb5a1521/wo-shi-zen-yao-gen-zong-polymarket-shang-de-cong-ming-qian-de-42be</guid>
      <description>&lt;h2&gt;
  
  
  预测市场最有意思的地方
&lt;/h2&gt;

&lt;p&gt;Polymarket 完全运行在链上。每一笔仓位、每一笔成交、每一次盈亏，都是公开信息，静静地躺在 Polygon 上。任何人的钱包你都能查：买了什么、卖了什么、最后从哪个市场拿走多少钱。这里没有封闭的基金，没有"受保护的"投资组合。&lt;/p&gt;

&lt;p&gt;这是个非常少见的属性。换到别处——股票、外汇、体育——那些真正常年赢钱的人是绝对不会把账本摊给你看的。在 Polymarket 上，他们没得选。&lt;/p&gt;

&lt;p&gt;但绝大多数用户，从来没认真看过这堆数据。&lt;/p&gt;

&lt;p&gt;我盯了这件事好几个月，下面是我学到的东西、真正有信号意义的几类信号，以及最后我自己写出来的那个 Telegram bot——因为人肉跟踪钱包这件事根本做不下去。&lt;/p&gt;

&lt;h2&gt;
  
  
  第一步：哪些是高水平钱包？
&lt;/h2&gt;

&lt;p&gt;第一步是先找出值得关注的钱包。Polymarket 自己有一个公开排行榜，按已实现盈亏（realized PnL）排序，可以切换今日、本周、近 30 天、历史全部几个时间窗。&lt;/p&gt;

&lt;p&gt;盯着这个榜看了一段时间之后，有几点观察：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;历史总盈亏会骗人。&lt;/strong&gt; 历史榜首位的钱包，可能是靠一个超大市场吃了一波，之后就再没动静。你真正想看的是&lt;strong&gt;最近一段时间的稳定性&lt;/strong&gt;——"近 30 天"是合适的窗口。优质钱包在多个时间窗里都靠前；昙花一现的钱包只在历史榜上有名字。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;只看胜率、不看已实现盈亏也会骗人。&lt;/strong&gt; 一个钱包靠买 90 美分的大热门赢 80%，账面上其实没赚到什么。诚实的看法是&lt;strong&gt;胜率和已实现盈亏一起看&lt;/strong&gt;。更进一步，按市场类别拆分胜率——Polymarket 上的优质钱包多数是专才。某个钱包政治类市场胜率 73%，到了体育类可能只有 45%。&lt;/p&gt;

&lt;p&gt;这是第一个教训：排行榜不是"最强交易者排行"，它只是"按你选的指标和时间窗，恰好排得靠前的人"的一份样本。把它当候选名单来读，不要当成定论。&lt;/p&gt;

&lt;h2&gt;
  
  
  第二步：进去要看什么？
&lt;/h2&gt;

&lt;p&gt;锁定几个候选钱包之后，就进到它的资料页（&lt;code&gt;polymarket.com/profile/0x...&lt;/code&gt;）。里面有：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;已结仓位&lt;/strong&gt; — 每个结算完的市场，对应的已实现盈亏。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;持仓中&lt;/strong&gt; — 当前还在持有的仓位，按当前市价标记。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;活动流&lt;/strong&gt; — 每一笔交易，带时间戳、市场、方向、规模、价格。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这点信息已经足够搭出真正有用的信号。我慢慢留意的几类交易是：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;远超该钱包平时规模的一笔交易。&lt;/strong&gt; 一个鲸鱼做一笔 500 美元的常规交易，那就是常规交易。但同一个钱包突然下了它这个季度最大的一笔仓位——&lt;em&gt;这个时刻&lt;/em&gt;才是值得关注的。最朴素版本的"信念度"就是：当前交易规模相对该钱包近期中位数交易规模的倍数。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;进入了一个还没开始动的市场。&lt;/strong&gt; 优质钱包经常在市场开始波动前的几天就进场。等到新闻打到台面上时，他们已经站好位了。把价格曲线和钱包进场时间戳叠在一起看，信息量是真实存在的。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;多个高水平钱包同时押向同一个市场的同一方向。&lt;/strong&gt; 单一钱包有一个观点，那是一个信号。但你识别出来的五个高水平钱包，在很短窗口里全押向同一边——这就是一个模式。这不代表任何确定性，但他们五个里没有人能单独伪造这种共振。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;离场。&lt;/strong&gt; "聪明钱在进场"比"聪明钱在离场"得到的关注多得多，但两者的信息含量其实差不多。一个先放大仓位、后又悄悄拆掉的钱包，多半是看见了什么。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这些观察都不新鲜，公开账本类的市场里，看市的人本来就这么看。&lt;/p&gt;

&lt;h2&gt;
  
  
  第三步：为什么手动跟根本撑不下去
&lt;/h2&gt;

&lt;p&gt;我用一个多月时间试着人肉跟踪十个钱包——浏览器书签、Excel 表、刷新页面、自己跟自己开的 Telegram 群。&lt;/p&gt;

&lt;p&gt;这件事不可持续。Polymarket 是 24 小时不停的。你想看的那个信号，往往出现在你睡觉、吃饭、开会的时候。等你下次刷新页面，那笔交易已经过去几个小时，价格也走完了。&lt;/p&gt;

&lt;p&gt;链上透明性反而成了一种诅咒：数据是公开的，但唯一用得起来的方式是不停盯着它看，而没有任何一个真人能这么干。缺的，是那种枯燥的、&lt;strong&gt;替你盯着&lt;/strong&gt;的基础设施。&lt;/p&gt;

&lt;h2&gt;
  
  
  我做了什么
&lt;/h2&gt;

&lt;p&gt;PolySignal 是一个 Telegram bot，负责替你盯。你从实时排行榜里挑钱包（每个一键加入），它就在这些钱包成交的那一刻把消息推到你的 Telegram。每条提醒里有：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;钱包、方向、对应的 outcome、市场。&lt;/li&gt;
&lt;li&gt;仓位规模和成交价格。&lt;/li&gt;
&lt;li&gt;这个钱包从已结仓位算出的历史表现：胜率、已实现盈亏、ROI。&lt;/li&gt;
&lt;li&gt;当本笔交易明显大于该钱包平时规模时，附一行"信念度"标签。&lt;/li&gt;
&lt;li&gt;一行说明数据来源是 Polygon 上的链上记录——可验证。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;它是只读的。它不连接你的钱包，不下任何单，也不要求私钥。它是建立在公开数据之上的信息工具——气质上更接近一台行情终端，而不是常见的 Telegram 喊单频道。&lt;/p&gt;

&lt;p&gt;有两个功能比看上去更重要：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;启动那一刻。&lt;/strong&gt; 新用户一键关注五个钱包，立刻就能看到一条样例提醒，让人在任何真实交易发生之前就理解了产品是怎么用的。提醒类工具最难处理的就是"刚加完关注，钱包还没动"的第一天体验。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;共识提醒。&lt;/strong&gt; 当你关注的钱包里有三个或更多，在一天之内押向同一个市场的同一方向、同一个结果，你只会收到一条合并好的提醒。这是你从任何单个钱包资料页里都不容易拼出来的信号。&lt;/p&gt;

&lt;p&gt;免费版每 15 分钟批量推一次。付费版（用 Telegram Stars 计费，前期价格定得很低，方便我先收集转化数据）能拿到实时提醒、按类别过滤、每日摘要，以及自定义提醒规则。&lt;/p&gt;

&lt;h2&gt;
  
  
  一点诚实声明
&lt;/h2&gt;

&lt;p&gt;我一直试着按 PolySignal 实际的样子来定位它：一个信息工具，不是任何形式的建议。Polymarket 并非在所有国家/地区都可用——这一点请使用者自行确认，bot 里也写明了。任何钱包的过去结果只是过去结果。这里没有预测、没有推荐。&lt;/p&gt;

&lt;p&gt;如果你觉得"链上数据全部公开"这件事本身有意思，又恰好在 Polymarket 上有交易：试试这个 bot，告诉我哪里不好用。地址是 &lt;a href="https://t.me/PolySignalAlertsBot?start=zh_longread1_devto" rel="noopener noreferrer"&gt;t.me/PolySignalAlertsBot&lt;/a&gt;——反馈直接到我手上。&lt;/p&gt;

</description>
      <category>polymarket</category>
      <category>crypto</category>
      <category>data</category>
    </item>
    <item>
      <title>When three sharp wallets agree: what consensus signals on Polymarket actually mean</title>
      <dc:creator>Слава Жуланов</dc:creator>
      <pubDate>Mon, 25 May 2026 06:46:31 +0000</pubDate>
      <link>https://forem.com/__747bb5a1521/when-three-sharp-wallets-agree-what-consensus-signals-on-polymarket-actually-mean-19ki</link>
      <guid>https://forem.com/__747bb5a1521/when-three-sharp-wallets-agree-what-consensus-signals-on-polymarket-actually-mean-19ki</guid>
      <description>&lt;p&gt;A few weeks into watching specific wallets on Polymarket, I noticed a pattern. Most of the time, a sharp wallet making a trade is one signal — interesting if you already trust that wallet's read, otherwise just data. Occasionally something else happens: &lt;strong&gt;three or four of the wallets I'd identified as sharp would all take the same side of the same market within a day.&lt;/strong&gt; Different wallets. Different sizes. Same direction.&lt;/p&gt;

&lt;p&gt;That coincidence is harder to explain away than any single trade. So I built it into PolySignal as its own alert type. This piece is about what consensus signals are — and what they aren't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The premise
&lt;/h2&gt;

&lt;p&gt;Polymarket is fully on-chain. Every position is public. The wallets that consistently top the leaderboard are observable: which markets they trade, when, in what size, with what eventual outcome. None of this is secret; it's just labour-intensive to track manually.&lt;/p&gt;

&lt;p&gt;When you watch a handful of sharp wallets, each individual trade carries some information about that wallet's view. One trader's view is one data point. The question worth asking is: &lt;em&gt;what changes when independent traders converge?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three wallets is the threshold I picked
&lt;/h2&gt;

&lt;p&gt;The PolySignal consensus alert fires when &lt;strong&gt;at least three&lt;/strong&gt; of a user's followed wallets have taken the same side, in the same outcome, of the same market, within a 24-hour window.&lt;/p&gt;

&lt;p&gt;Why three?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two is the modal coincidence.&lt;/strong&gt; Sharp wallets watch overlapping markets; on any active day, half the leaderboard has positions in the two or three most-trafficked questions. Two wallets agreeing is barely above baseline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three is structurally hard to coordinate accidentally.&lt;/strong&gt; It requires three independent reads to land on the same side, in the same window, on the same market. Sharp Polymarket wallets are also specialised — some focus on politics, others on crypto, others on sports — so three converging &lt;em&gt;across&lt;/em&gt; specialties is rarer still.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Four was tempting, but it filtered too aggressively.&lt;/strong&gt; I'd rather over-alert lightly than miss the event entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That threshold is configurable. Three is the launch default and it has produced sensible patterns in the few weeks of observation I have.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it isn't
&lt;/h2&gt;

&lt;p&gt;A consensus signal is &lt;strong&gt;not a prediction.&lt;/strong&gt; It's not a recommendation. It's a description: three wallets with strong closed-market track records have just taken the same side of one market. Past coincidence does not guarantee future correlation. The signal is information about who's positioned where; what you do with it is your own decision.&lt;/p&gt;

&lt;p&gt;This matters because the temptation when you see "three of your wallets agree" is to read it as evidence the side will resolve correctly. Sometimes it does. Sometimes it doesn't. The pattern's value is statistical, not deterministic.&lt;/p&gt;

&lt;p&gt;Two failure modes I've watched in real time:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consensus on the obvious side.&lt;/strong&gt; When a market gets a news catalyst that pushes prices to 90¢, a lot of wallets will pile in at 91¢ expecting the gap to close. Three "sharp" wallets converging on the obvious side after the news isn't a signal — it's the same news everyone else read, expressed by people who happen to be on your watchlist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Late-arriving consensus.&lt;/strong&gt; Three wallets converging on a market that's already moved 30 points is information about where the consensus &lt;em&gt;was&lt;/em&gt;, not where it's going next. The signal value of consensus drops sharply when it arrives after the move.&lt;/p&gt;

&lt;p&gt;The mitigations are obvious in hindsight: the alert is more interesting when the market is quiet, and less interesting when it's noisy. I've thought about adding a "market is currently moving &amp;gt; X over the window" suppressor; for now I'd rather show all consensus events and let readers exercise their own judgement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually catches
&lt;/h2&gt;

&lt;p&gt;The patterns the consensus alert catches well:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Early entries into not-yet-popular markets.&lt;/strong&gt; A new market launches; three of your watched wallets each independently decide it's worth a position. The market is still 50/50 in price terms. This is the signal class with the most consistent information value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coordinated direction shifts.&lt;/strong&gt; A market has been trading in a band; one day, three of your wallets all open positions on the same side at the same time. Something changed that they all noticed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sharp-wallet positions on long-resolution questions.&lt;/strong&gt; Markets that resolve in months sometimes show consensus from sharp wallets weeks before the rest of the market catches on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The patterns it doesn't catch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wallets &lt;em&gt;exiting&lt;/em&gt; a market.&lt;/strong&gt; Exits are as informative as entries, often more so, but consensus on the entry side doesn't surface "everyone unwinding." I'll probably add a separate exit-consensus alert eventually.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-wallet conviction trades.&lt;/strong&gt; A single sharp wallet putting on its largest position in months — the signal of behavioural conviction — is its own thing; PolySignal handles it as a per-trade tag, not as consensus.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The mechanics
&lt;/h2&gt;

&lt;p&gt;For the technically curious: detection is one DB query per delivered trade alert. For each new trade I check the user's followed wallets and look for other recent trades that share (market, side, outcome) within the window. If the distinct-wallet count crosses the threshold, the consensus alert fires; a &lt;code&gt;SentConsensus&lt;/code&gt; row is committed before the message is sent so a failed delivery never produces a duplicate.&lt;/p&gt;

&lt;p&gt;It's a small piece of code — most of the cleverness is in the choice of threshold and window, not in the implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The frame I actually use
&lt;/h2&gt;

&lt;p&gt;When a consensus alert lands in my Telegram, I treat it as a flag, not an instruction. The question I ask is &lt;em&gt;what do these three wallets see that I don't yet?&lt;/em&gt; Maybe they read a story I missed. Maybe they have domain expertise the market hasn't priced in. Maybe they're all reading the same incomplete picture — in which case the question becomes whether I have a stronger read of my own.&lt;/p&gt;

&lt;p&gt;The signal is the start of a question. The answer is your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this runs
&lt;/h2&gt;

&lt;p&gt;If you've read this far and want to see consensus signals in your own Telegram: I built &lt;a href="https://t.me/PolySignalAlertsBot?start=longread4_devto" rel="noopener noreferrer"&gt;PolySignal&lt;/a&gt; precisely so I'd stop manually correlating trades in a spreadsheet. The consensus alert is a Pro-tier feature; the rest of the bot is free.&lt;/p&gt;

&lt;p&gt;Honest disclosure: PolySignal reports on public on-chain activity. It is an information service, not financial advice. Polymarket isn't available in every region — check yours.&lt;/p&gt;

</description>
      <category>polymarket</category>
      <category>crypto</category>
      <category>data</category>
      <category>showdev</category>
    </item>
    <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://forem.com/__747bb5a1521/building-an-on-chain-alerts-bot-in-python-without-any-blockchain-library-3o1k</link>
      <guid>https://forem.com/__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://forem.com/__747bb5a1521/i-built-a-paid-telegram-bot-heres-what-telegram-stars-actually-pay-3fo</link>
      <guid>https://forem.com/__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://forem.com/__747bb5a1521/how-i-track-polymarket-smart-money-4mmm</link>
      <guid>https://forem.com/__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>
