<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Chang-Hai</title>
    <description>The latest articles on DEV Community by Chang-Hai (@changhai).</description>
    <link>https://dev.to/changhai</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%2F1491742%2F7c2395f0-c591-4139-83f9-3bc03fa36283.jpeg</url>
      <title>DEV Community: Chang-Hai</title>
      <link>https://dev.to/changhai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/changhai"/>
    <language>en</language>
    <item>
      <title>How to listen for USDT (TRC20) payments in Node.js without running a full node</title>
      <dc:creator>Chang-Hai</dc:creator>
      <pubDate>Wed, 19 Nov 2025 15:39:27 +0000</pubDate>
      <link>https://dev.to/changhai/how-to-listen-for-usdt-trc20-payments-in-nodejs-without-running-a-full-node-4g36</link>
      <guid>https://dev.to/changhai/how-to-listen-for-usdt-trc20-payments-in-nodejs-without-running-a-full-node-4g36</guid>
      <description>&lt;p&gt;Building a Telegram bot is fun until you hit the "payments" part.&lt;/p&gt;

&lt;p&gt;I recently built a bot that sells digital products, and I wanted to accept USDT on Tron (TRC20) because the fees are low and everyone uses it.&lt;/p&gt;

&lt;p&gt;I thought: "How hard can it be? I just need to check if an address received money."&lt;/p&gt;

&lt;p&gt;Turns out, it's a headache.&lt;/p&gt;

&lt;p&gt;If you are a solo dev like me, you usually have two bad options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use BitPay/Coinbase Commerce&lt;/strong&gt;: Requires KYC, business docs, and they hold your funds. (No go for me).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Self-host&lt;/strong&gt;: Query the blockchain manually.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I tried the self-host route first. Here is what I learned, and the solution I eventually built to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Hard" Way: Polling with TronWeb
&lt;/h2&gt;

&lt;p&gt;To listen for payments, you typically use the tronweb library to interact with the Tron network.&lt;/p&gt;

&lt;p&gt;You need to poll the most recent transactions or set up an event listener. Here is a simplified version of the script I initially wrote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const TronWeb = require('tronweb');
// You need an API Key from TronGrid, or you'll get rate-limited instantly.
const tronWeb = new TronWeb({
    fullHost: '[https://api.trongrid.io](https://api.trongrid.io)',
    headers: { "TRON-PRO-API-KEY": 'your-api-key' }
});

const USDT_CONTRACT = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"; // USDT Address

async function checkRecentTransactions(myWalletAddress) {
    // This is where it gets messy.
    // Public nodes often time out or return incomplete data.
    try {
        const contract = await tronWeb.contract().at(USDT_CONTRACT);
        // You have to parse the Transfer event logs
        // And handle hex conversion for amounts
        console.log("Listening for events...");

        // ... complex logic to filter events by 'to' address ...

    } catch (error) {
        console.error("RPC Error:", error);
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Problem with this approach
&lt;/h3&gt;

&lt;p&gt;It works locally, but in production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rate Limits&lt;/strong&gt;: The free TronGrid API limits you heavily. If you have 10 users checking payments, you get banned.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Missed Events&lt;/strong&gt;: Public WebSocket connections drop all the time. You need robust retry logic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Maintenance&lt;/strong&gt;: You are basically maintaining a mini-indexer just to receive $20.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The "Easy" Way: Webhooks (ChainHook)
&lt;/h2&gt;

&lt;p&gt;I got tired of debugging RPC errors at 3 AM, so I decided to wrap this entire logic into a dedicated microservice.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;ChainHook&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It's a "Set and Forget" middleware.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;You give it a wallet address (Public key only).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It monitors the chain using premium nodes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It sends &lt;strong&gt;a Webhook (POST)&lt;/strong&gt; to your bot when a payment confirms.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Integration Example
&lt;/h3&gt;

&lt;p&gt;Instead of maintaining the script above, your bot just needs one endpoint to receive the data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Your Bot's Backend (Express/Fastify/Python)
app.post('/webhook/payment', (req, res) =&amp;gt; {
  const { event, data } = req.body;

  if (event === 'payment.confirmed') {
    const { amount, from, tx_hash } = data;

    console.log(`Received ${amount} USDT from ${from}`);
    console.log(`Hash: ${tx_hash}`);

    // TODO: Auto-deliver product to user
  }

  res.status(200).send('OK');
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payload looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "event": "payment.confirmed",
  "data": {
    "network": "TRC20",
    "currency": "USDT",
    "amount": "29.99",
    "tx_hash": "89d3a...",
    "from": "TMwF...",
    "to": "YOUR_WALLET_ADDRESS",
    "timestamp": 171092834
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Looking for Beta Testers
&lt;/h2&gt;

&lt;p&gt;I built this primarily for myself and a few friends running Telegram bots, but I'm opening it up for a public beta.&lt;/p&gt;

&lt;p&gt;If you are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Building a Telegram Bot / Discord Bot&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Selling digital goods&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Hate manual verification (or fake screenshot scams)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd love for you to try it out. It's non-custodial, meaning the money goes straight to your wallet—I just act as the notifier.&lt;/p&gt;

&lt;p&gt;👉 Join the waiting list here: [&lt;a href="https://chainhook-landing.vercel.app/" rel="noopener noreferrer"&gt;https://chainhook-landing.vercel.app/&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;(Early testers will get lifetime access to the starter plan for free).&lt;/p&gt;

&lt;p&gt;Let me know in the comments if you have questions about handling Tron transactions manually, happy to help with the code too!&lt;/p&gt;

</description>
      <category>web3</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
