Skip to content

🤖 USDT (TRC20/ERC20) 转账监控机器人:技术记录

这是一个单进程常驻运行的机器人:轮询若干个 TRON / Ethereum 钱包地址的链上数据,把新发生的 USDT(TRC20/ERC20)转入转出推送到一个固定的 Telegram 群,群成员可以用指令动态增删监控地址。

需求本身不复杂,但做的过程中有几处顺序敏感的逻辑踩过坑,生产上线后也暴露了几个环境层面的问题,记录下来。

功能与不做的事

核心指令(链类型根据地址格式自动识别,T 开头 34 位是 TRC20,0x 开头 42 位是 ERC20,不需要手动指定):

  • /add <地址> [备注] — 添加监控地址
  • /remove <地址> — 移除监控地址
  • /list — 查看当前监控列表
  • /log — 查看最近的地址增删操作记录

明确不做的事:不支持多群/多租户(chat_id 全局唯一);不做权限控制(群里任何人都能 /add /remove);只认 USDT,不支持任意代币。这些不是漏做,是范围内明确划掉的非目标。

关键设计决策

1. 合约地址必须交叉验证,不能凭"看起来眼熟"

最初的技术方案里 TRC20 合约地址填错了(那串字符根本不是一个合法地址),后来用 tronscan API 核实修正,ERC20 地址也用 CoinGecko 做了交叉验证。

TRON 链上遍地都是冒充 USDT 名字和图标的假合约,地址填错不会报错——程序照常运行、照常发通知,只是监控的是假币,用户完全不会发现。换币种/换链之前,务必用独立数据源交叉验证合约地址,不要只信一个来源。

2. 先发送成功,才能标记已处理

轮询主循环里的顺序是:格式化消息 → 发送 → 发送成功才标记已处理、推进游标;发送失败直接跳出,不推进游标。

这是踩过坑之后改的:最早的版本是"先标记已处理再发送",如果发送因为 Telegram 限流失败,这笔交易已经被标记为处理过,永远不会重试,通知就静默丢失了。这个"发送确认成功后才落库"的顺序是硬约束,改动核心逻辑时不能破坏它。

3. 必须用升序(老到新)拉取,不能只拿"最新 N 笔"

TronGrid 和 Etherscan 的分页都改成了升序 + 固定单页上限。如果用降序只拿最新 N 笔,一旦某个地址在两次轮询之间的转账数超过这个上限,比较旧的那些会被永久跳过——下一轮的游标已经前进到超过它们的时间点了,不会再回头拉取。

代价是:如果一次性加入一个历史转账量巨大的地址(比如接了个交易所地址),第一轮会从很久以前的第一笔开始通知,需要好几轮才能追上当前。这是已知的、可接受的权衡,真实场景是个人/商业钱包,不会触发这个问题。

4. Telegram 限流的处理

发送逻辑会捕获 Telegram 的限流异常,按平台告知的秒数等待后重试(最多 3 次),重试耗尽则交给上面第 2 条的机制保证不丢消息、留到下一轮重试。

实测触发过一次:把一个交易所归集地址(几十笔/秒)加进监控后,瞬间打满了群消息限流,程序自动等待恢复、补发成功,没有丢消息。但长期挂着这种高频地址会持续占用群消息配额、拖慢其他地址的轮询节奏——这是 Telegram 平台的真实限制,没有代码层面的根治方法,只能不监控这类地址,或者以后做消息合并/限速发送。

5. 操作审计日志是追加式的

地址的增删操作记录单独存一张审计表,跟"当前监控列表"的表是分开的。删除一个监控地址只会从监控列表里物理删除,但审计表里的增删记录永久保留,可以用 /log 查看。要查"某个地址是谁加的",得去审计表里找最后一条对应记录,监控列表本身不带这个信息。

生产环境踩的坑

代码逻辑跑通只是第一步,实际上生产之后又暴露了几个环境层面的问题:

  • 容器 DNS 解析偶发失败:轮询链上 API 时偶尔会遇到瞬时的域名解析失败。给容器配置了多个 DNS 服务器做兜底,减少这类偶发失败。
  • Telegram 群升级为超级群后 chat_id 失效:普通群升级成超级群(supergroup)之后,chat_id 会变化,配置里原来的 chat_id 直接失效,通知发不出去。需要检测这种迁移并更新成新的 chat_id
  • 日志只记了失败,没记成功:一开始的日志策略是"正常不打印,出错才打印",结果排查问题时没法区分"整个程序没在跑"和"跑了但一直没有匹配到新交易"。后来改成成功的链上 API 请求也记日志,才能从日志里判断轮询本身是不是健康的。

这几个问题的共同点是:单元测试测不出来,只有跑在真实网络环境、真实 Telegram 服务端行为下才会暴露,所以都是上线后陆续修的。

用到的技术知识点

  • 链上数据轮询而非监听事件:TRON 用 TronGrid API,Ethereum 用 Etherscan V2 API,两边都是轮询式拉取,而不是订阅链上事件,实现和运维成本都更低,代价是有轮询间隔的延迟,可接受。
  • 游标(cursor)+ 幂等去重:每条链维护一个"已处理到哪个时间点"的游标,配合交易记录表的唯一索引做幂等去重,避免重启或重复拉取导致重复通知。
  • HTTP 429/403 退避重试:链上 API 和 Telegram API 都做了统一的退避重试封装,处理限流和临时性失败。
  • Docker 而不是裸机部署:生产服务器是 CentOS 7,系统自带 Python 2.7.5,代码用了 3.10+ 语法,直接在 CentOS 7 上装新版 Python 要编译工具链、容易踩坑,所以用 Docker 绕开系统 Python 版本的限制。
  • GitHub Actions 自动部署:push 到 main 分支后,Actions 通过 SSH 登录服务器执行 git pull && docker compose up -d --build。项目小、维护者少,目前是有意接受"push 即上生产、没有测试和人工审核"这个权衡,如果以后项目变重要,第一步应该是在部署前加一道基本的语法检查。

小结

这类"监控真实资金流水"的机器人,核心难点不在业务逻辑本身,而在两类容易被忽视的顺序敏感 bug(先标记后发送、降序拉取漏消息)和一堆只有生产环境才会暴露的环境问题(DNS、群迁移、日志盲区)。功能层面的需求已经跑通,这篇记录更多是留给以后要改动核心逻辑时的自己:改之前先看一眼这几条不变量有没有被破坏。

技术笔记