In this tutorial, I want to show how to create a Telegram bot (in Node.js using Telegraf) which communicates with the BNB (Binance Smart) Chain using WebSockets (e.g. GetBlock) and informs users in real-time about wallet activity. The bot can watch for balance changes, BEP-20 token transfers, NFT mints, and large BNB transactions. Users can /subscribe
to any wallet address and receive instant notifications. I will cover bot setup, WebSocket connections, event subscription, filtering logic, a subscription system, and deployment/persistence recommendations.
1. Create and Set Up Your Telegram Bot
Create a Telegram bot and set its API token through @BotFather. In Telegram, initiate a conversation with BotFather and type /newbot
. Proceed to name your bot and receive a unique token. For instance, BotFather will print a token such as 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11
. Handle this token as a password.
Save the token and place it within your environment (e.g. in a .env
file) so that your code can access it as process.env.BOT_TOKEN
. Your bot will authenticate with Telegram's API using this token.
2. Establish WebSocket Connection to BNB Chain
To follow BNB Chain events in real-time, connect via a WebSocket provider. GetBlock has BNB Smart Chain WebSocket endpoints. After signing up on GetBlock and obtaining an API key, use the endpoint:"
wss://bsc.getblock.io/mainnet/?api_key=YOUR_API_KEY
This address (with your key) gives a live WS connection to BSC. In code, you can use libraries like ethers.js or web3.js to connect:
const { Web3 } = require("web3");
const BSC_WS = "wss://bsc.getblock.io/mainnet/?api_key=YOUR_API_KEY";
const web3 = new Web3(BSC_WS);
This web3
instance now listens to blockchain data. You may also use ethers.js:
const { ethers } = require("ethers");
const provider = new ethers.providers.WebSocketProvider(BSC_WS);
Either library will let you subscribe to pending transactions and logs over the WebSocket connection.
3. Subscribing to Blockchain Events
3.1 Listening for Pending BNB Transactions
To catch all new BNB (native coin) transactions before they're mined, subscribe to the pendingTransactions
stream. Using web3.js:
async function subscribeToPending() {
const subscription = await web3.eth.subscribe('pendingTransactions');
subscription.on('data', async (txHash) => {
try {
const tx = await web3.eth.getTransaction(txHash);
// process tx.from, tx.to, tx.value, etc.
} catch (err) { console.error(err); }
});
subscription.on('error', console.error);
}
This code creates a subscription that receives new transaction hashes in the mempool. For each txHash
, we fetch the whole transaction. We can then validate tx.from
, tx.to
, and tx.value
(BNB value in Wei). For example, to trap big BNB transfers, compare tx.value
with a threshold (10 BNB) and raise an alert if crossed.
Chainstack docs illustrate this pattern, where handleNewPending
checks value
and logs large transactions only. Weβll adapt this in our bot to alert subscribers whose addresses match tx.from
or tx.to
.
3.2 Monitoring BEP-20 Token Transfers
BEP-20 (ERC-20 format) token transfers emit Transfer
events. We can subscribe to the logs for these events. Subscribing to all the logs and filtering on Transfer
signatures is one way, but better practice is to specify filters. For example, via web3.js:
const transferSig = web3.utils.sha3("Transfer(address,address,uint256)");
const logsSubscription = await web3.eth.subscribe('logs', {
topics: [transferSig]
});
logsSubscription.on('data', (log) => {
// log.address = token contract; log.topics[1]=from, [2]=to
const from = '0x' + log.topics[1].slice(26);
const to = '0x' + log.topics[2].slice(26);
// convert data (hex) to token amount
const value = web3.eth.abi.decodeParameter('uint256', log.data);
// handle transfer where from/to matches a subscriber
});
logsSubscription.on('error', console.error);
This subscribes to all BSC Transfer
events. In the callback, extract from
and to
addresses from log.topics
(the event's indexed parameters) and log.data
for the amount transferred. Then check if either address matches a watched wallet.
3.3 NFT Minting and Transfer Detection
BEP-721/BEP-1155 NFTs on BNB Chain also emit Transfer
events (same signature) on minting or transfer. We can catch these on the same logs subscription. A mint is typically a Transfer
where from
is the zero address (0x0000000000000000000000000000000000000000
) and to
is the new owner. In code:
if (from === '0x0000000000000000000000000000000000000000') {
// NFT minted to 'to'
notifyUser(tokenId, to, 'NFT minted');
}
For any NFT transfers involving a watched address (either as sender or receiver), trigger an alert. In the case of multiple token types, you may have to sort between them by contract interface or token ID ranges if there are multiple standards, but the general alert logic would be similar to BEP-20 above.
3.4 Logic for Other Events and Balances
- Balance updates. You can sometimes call
web3.eth.getBalance(address)
or watch for transactions affecting an address to infer changes. For simplicity, filtering incoming/outgoing transactions (above) handles most balance updates. - Filtering. On each event or transaction, search for if it involves any subscribed wallet address. Maintain a lookup (e.g. a JS
Set
orMap
) of addresses of interest to readily checkfrom/to
. - Multiple subscriptions. In case of lots of addresses, consider scaling out filters or using several address filter libraries. For a simple bot, we'll loop through interested addresses in callbacks.
4. Bot Commands and Subscription System
Use the Telegraf library for Telegram with Node.js. It provides a neat way to get commands and send messages. Sample setup:
const { Telegraf } = require('telegraf');
const bot = new Telegraf(process.env.BOT_TOKEN);
Implement commands to manage subscriptions:
/subscribe <address>
: User subscribes to a wallet. In the handler, save the mapping of wallet to this userβs chat ID./unsubscribe <address>
: Remove subscription./list
: (Optional) list current subscriptions.
Example command handlers:
let subscribers = {}; // mapping: address -> Set of chatIds
bot.command('subscribe', (ctx) => {
const address = ctx.message.text.split(' ')[1];
if (!web3.utils.isAddress(address)) {
return ctx.reply("Invalid address.");
}
const userId = ctx.chat.id;
subscribers[address] = subscribers[address] || new Set();
subscribers[address].add(userId);
ctx.reply(`Subscribed ${address} for alerts.`);
});
bot.command('unsubscribe', (ctx) => {
const address = ctx.message.text.split(' ')[1];
const userId = ctx.chat.id;
if (subscribers[address]) {
subscribers[address].delete(userId);
ctx.reply(`Unsubscribed from ${address}.`);
} else {
ctx.reply("No active subscription found for that address.");
}
});
This stores a simple in-memory mapping of addresses to sets of chat IDs. In a real deployment, you would persist this (e.g. a database or file) so that subscriptions survive restarts. For example, you could periodically write subscribers
to a JSON file and load it on startup.
5. Sending Real-Time Alerts
Whenever a blockchain event matches a subscription, send a Telegram message. For example, in the pending transaction listener:
const threshold = web3.utils.toWei('10', 'ether'); // 10 BNB
if (subscribers[tx.to]) {
for (const chatId of subscribers[tx.to]) {
bot.telegram.sendMessage(chatId,
`π₯ *Received*: ${web3.utils.fromWei(tx.value, 'ether')} BNB\n` +
`From: ${tx.from}\n` +
`TxHash: ${txHash}`, { parse_mode: 'Markdown' });
}
}
if (subscribers[tx.from] && tx.value >= threshold) {
for (const chatId of subscribers[tx.from]) {
bot.telegram.sendMessage(chatId,
`π *Large Transfer Out*: ${web3.utils.fromWei(tx.value, 'ether')} BNB\n` +
`To: ${tx.to}\nTxHash: ${txHash}`, { parse_mode: 'Markdown' });
}
}
Likewise, in the logs handler for token transfers:
if (subscribers[from]) {
const symbol = await getTokenSymbol(log.address); // optional helper
for (const chatId of subscribers[from]) {
bot.telegram.sendMessage(chatId,
`π» *Sent*: ${web3.utils.fromWei(value, 'ether')} ${symbol}\n` +
`To: ${to}\nContract: ${log.address}`, { parse_mode: 'Markdown' });
}
}
if (subscribers[to]) {
const symbol = await getTokenSymbol(log.address);
for (const chatId of subscribers[to]) {
bot.telegram.sendMessage(chatId,
`πΊ *Received*: ${web3.utils.fromWei(value, 'ether')} ${symbol}\n` +
`From: ${from}\nContract: ${log.address}`, { parse_mode: 'Markdown' });
}
}
These notifications use simple Markdown formatting (bold, emojis) to make activity stand out. Telegraf's bot.telegram.sendMessage(chatId, text, opts)
sends off the alert. Be aware to account for potential errors when sending (user banned bot, etc.).
6. Disconnection and Rate Limit Handling
WebSocket Reliability. WebSocket connections can be terminated. Listen for provider._websocket.on('close')
(ethers.js) or subscription error
events and attempt reconnects. Use a retry/backoff strategy. For example:
provider._websocket.on('close', (code) => {
console.error(`WS closed with code ${code}, reconnecting...`);
setTimeout(() => {
provider = new ethers.providers.WebSocketProvider(BSC_WS);
subscribeToEvents(); // re-subscribe after reconnect
}, 5000);
});
Rate Limits. GetBlock has rate limits on message count. Avoid excessive polling. Subscriptions are great because data are pushed. If you hit limits (e.g. too many subscribe requests), consider upgrading your plan or batching. Always catch Web3 provider errors (e.g. quota exceeded) and apply backoff.
7. Deployment and Persistence
To keep the bot running 24/7, deploy it on a server or a cloud instance. A few good options would be a VPS, AWS, or Heroku. For example, on Heroku you could deploy the Node.js app with a Procfile
specifying worker: node index.js
. Note that Heroku's filesystem is an ephemeral, so file-stored subscriptions won't be accessible upon a restart or dyno restart. Store subscription information in an actual database (PostgreSQL, Redis, etc.).
If you are on Heroku, remember, Heroku dynos sleep when there's no web traffic. Using a bot with long-polling on Heroku involves using webhooks (by bot.telegram.setWebhook(URL)
) or keeping a worker dyno running (which can incur expense). Alternatively, run the bot as an AWS EC2 service or a Docker container (with PM2 process management).
Finally, keep your keys safe: do not hardcode API keys or tokens into code. Use environment variables and/or a secrets manager.
Complete Example Code (Node.js with Telegraf)
// index.js
require('dotenv').config();
const { Telegraf } = require('telegraf');
const { Web3 } = require('web3');
const bot = new Telegraf(process.env.BOT_TOKEN);
const BSC_WS = `wss://bsc.getblock.io/mainnet/?api_key=${process.env.GETBLOCK_KEY}`;
const web3 = new Web3(BSC_WS);
// In-memory subscription map: address -> Set of chat IDs
let subscribers = {};
// Helper to ensure checksum and lowercase
function normalize(addr) { return web3.utils.toChecksumAddress(addr); }
// Telegram command: subscribe to address
bot.command('subscribe', (ctx) => {
const parts = ctx.message.text.split(' ');
if (parts.length < 2) return ctx.reply("Usage: /subscribe <address>");
let address = parts[1];
if (!web3.utils.isAddress(address)) return ctx.reply("Invalid address.");
address = normalize(address);
const id = ctx.chat.id;
if (!subscribers[address]) subscribers[address] = new Set();
subscribers[address].add(id);
ctx.reply(`Subscribed to wallet ${address}.`);
});
// Telegram command: unsubscribe from address
bot.command('unsubscribe', (ctx) => {
const parts = ctx.message.text.split(' ');
if (parts.length < 2) return ctx.reply("Usage: /unsubscribe <address>");
const address = normalize(parts[1]);
const id = ctx.chat.id;
if (subscribers[address]) {
subscribers[address].delete(id);
ctx.reply(`Unsubscribed from ${address}.`);
} else {
ctx.reply(`You were not subscribed to ${address}.`);
}
});
// Start bot
bot.launch();
console.log("Telegram bot started.");
// Subscribe to pending BNB transactions
web3.eth.subscribe('pendingTransactions', (err, txHash) => {
if (err) return console.error(err);
// For each pending tx, fetch details
web3.eth.getTransaction(txHash)
.then(tx => {
if (!tx) return; // might be null if it got dropped
const from = tx.from ? normalize(tx.from) : null;
const to = tx.to ? normalize(tx.to) : null;
const value = web3.utils.toBN(tx.value);
// Notify if recipient is watched
if (to && subscribers[to]) {
subscribers[to].forEach(chatId => {
bot.telegram.sendMessage(chatId,
`π₯ *Incoming* ${web3.utils.fromWei(value, 'ether')} BNB\nFrom: ${from}\nTx: [${txHash}](https://bscscan.com/tx/${txHash})`,
{ parse_mode: 'Markdown' });
});
}
// Notify if sender is watched and large transfer
const threshold = web3.utils.toBN(web3.utils.toWei('10', 'ether')); // 10 BNB
if (from && subscribers[from] && value.gte(threshold)) {
subscribers[from].forEach(chatId => {
bot.telegram.sendMessage(chatId,
`π *Large Outgoing* ${web3.utils.fromWei(value, 'ether')} BNB\nTo: ${to}\nTx: [${txHash}](https://bscscan.com/tx/${txHash})`,
{ parse_mode: 'Markdown' });
});
}
})
.catch(console.error);
});
// Subscribe to Transfer logs (BEP-20 and NFTs)
const transferSig = web3.utils.sha3("Transfer(address,address,uint256)");
web3.eth.subscribe('logs', { topics: [transferSig] }, (err, log) => {
if (err) { console.error(err); return; }
// Parse log fields
const from = '0x' + log.topics[1].slice(26);
const to = '0x' + log.topics[2].slice(26);
const value = web3.utils.toBN(log.data);
const fromNorm = normalize(from);
const toNorm = normalize(to);
// Determine token symbol (optional helper could use web3+ABI or an on-chain call)
const tokenContract = log.address;
// Send outgoing transfer alert
if (subscribers[fromNorm]) {
subscribers[fromNorm].forEach(chatId => {
bot.telegram.sendMessage(chatId,
`π» *Token Sent* (${tokenContract})\nAmount: ${web3.utils.fromWei(value, 'ether')}\nTo: ${toNorm}`,
{ parse_mode: 'Markdown' });
});
}
// Send incoming transfer alert
if (subscribers[toNorm]) {
subscribers[toNorm].forEach(chatId => {
bot.telegram.sendMessage(chatId,
`πΊ *Token Received* (${tokenContract})\nAmount: ${web3.utils.fromWei(value, 'ether')}\nFrom: ${fromNorm}`,
{ parse_mode: 'Markdown' });
});
}
});
console.log("Subscribed to blockchain events.");