Introduction
Tracking Ethereum wallet activity manually wastes precious time and opportunities. Whether you're monitoring whale movements, tracking your DeFi positions, or watching for specific transactions, constant manual checking simply doesn't scale. This guide teaches you to build an Ethereum wallet tracker bot that sends instant Telegram notifications whenever ETH moves to or from any address you specify.
By the end of this tutorial, you'll deploy a production-ready bot that monitors wallets 24/7, alerts you within seconds of any transaction, and runs reliably on minimal infrastructure. We'll use modern Web3 tools and best practices to ensure your bot stays online and responsive.
Prerequisites
Before diving in, ensure you have:
- Node.js 20+ installed (check with
node --version
) - An Infura or Alchemy API key (free tier works fine)
- A Telegram Bot token from @BotFather
- Basic familiarity with JavaScript and async/await patterns
- An Ethereum wallet address to monitor (any public address works)
Overview of the Workflow
Our bot architecture follows five core steps:
- Environment Setup - Initialize the project and install dependencies
- Web3 Connection - Connect to Ethereum via WebSocket for real-time events
- Telegram Integration - Build the bot logic to handle commands and send alerts
- Testing Pipeline - Verify functionality on testnet before mainnet
- Production Deployment - Deploy to a VPS or serverless platform
Step 1 — Environment Setup
Start by creating a new project directory and initializing it:
bashmkdir eth-wallet-tracker-bot cd eth-wallet-tracker-bot npm init -y
Install the required dependencies:
bashnpm install web3 node-telegram-bot-api dotenv winston npm install --save-dev nodemon jest
Create a .env
file to store sensitive configuration:
bashtouch .env
Add your credentials to .env
:
envINFURA_WSS_URL=wss://mainnet.infura.io/ws/v3/YOUR_PROJECT_ID TELEGRAM_BOT_TOKEN=YOUR_BOT_TOKEN_FROM_BOTFATHER MONITORED_WALLET=0x742d35Cc6634C0532925a3b844Bc9e7595f6E3D
Tip: Never commit
.env
files to version control. Add it to.gitignore
immediately.
Create the main application file:
bashtouch index.js
Step 2 — Web3 Provider & Event Filters
Setting up the Web3 connection requires careful attention to WebSocket stability. Here's our core monitoring logic:
javascriptconst Web3 = require('web3'); const TelegramBot = require('node-telegram-bot-api'); const winston = require('winston'); require('dotenv').config(); // Configure logger const logger = winston.createLogger({ level: 'info', format: winston.format.json(), transports: [ new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }), new winston.transports.Console({ format: winston.format.simple() }) ] }); // Initialize Web3 with WebSocket provider const web3 = new Web3(new Web3.providers.WebsocketProvider( process.env.INFURA_WSS_URL, { reconnect: { auto: true, delay: 5000, maxAttempts: 10, onTimeout: false } } )); // Initialize Telegram bot const bot = new TelegramBot(process.env.TELEGRAM_BOT_TOKEN, { polling: true }); // Store active chat IDs const activeChatIds = new Set(); // Monitor wallet function async function monitorWallet(address) { const normalizedAddress = address.toLowerCase(); // Subscribe to pending transactions const subscription = web3.eth.subscribe('pendingTransactions', (error, txHash) => { if (error) { logger.error('Subscription error:', error); return; } }); subscription.on('data', async (txHash) => { try { const tx = await web3.eth.getTransaction(txHash); if (!tx || !tx.to) return; const fromAddress = tx.from.toLowerCase(); const toAddress = tx.to.toLowerCase(); if (fromAddress === normalizedAddress || toAddress === normalizedAddress) { const valueInEth = web3.utils.fromWei(tx.value, 'ether'); const direction = fromAddress === normalizedAddress ? 'OUT' : 'IN'; const message = formatTransactionMessage(tx, valueInEth, direction); // Send to all active chats activeChatIds.forEach(chatId => { bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); }); logger.info(`Transaction detected: ${txHash}`); } } catch (error) { logger.error('Error processing transaction:', error); } }); } function formatTransactionMessage(tx, valueInEth, direction) { const directionEmoji = direction === 'IN' ? '📥' : '📤'; return `${directionEmoji} *${direction} Transaction Detected*\n\n` + `💰 *Amount:* ${valueInEth} ETH\n` + `📊 *Gas Price:* ${web3.utils.fromWei(tx.gasPrice, 'gwei')} Gwei\n` + `🔗 *Hash:* \`${tx.hash}\`\n` + `👤 *From:* \`${tx.from}\`\n` + `📍 *To:* \`${tx.to}\`\n\n` + `[View on Etherscan](https://etherscan.io/tx/${tx.hash})`; }
Note: WebSocket connections can drop unexpectedly. The reconnect configuration ensures your bot recovers automatically.
Step 3 — Building the Telegram Bot Logic
Now let's implement the Telegram bot commands and interaction logic:
javascript// Bot command handlers bot.onText(/\/start/, (msg) => { const chatId = msg.chat.id; activeChatIds.add(chatId); const welcomeMessage = `🤖 *Ethereum Wallet Tracker Bot*\n\n` + `I'll monitor wallet transactions and notify you instantly!\n\n` + `Currently watching: \`${process.env.MONITORED_WALLET}\`\n\n` + `Commands:\n` + `/status - Check bot status\n` + `/wallet - View monitored wallet\n` + `/stop - Stop notifications\n` + `/help - Show this message`; bot.sendMessage(chatId, welcomeMessage, { parse_mode: 'Markdown' }); logger.info(`New user started bot: ${chatId}`); }); bot.onText(/\/status/, async (msg) => { const chatId = msg.chat.id; try { const latestBlock = await web3.eth.getBlockNumber(); const balance = await web3.eth.getBalance(process.env.MONITORED_WALLET); const balanceInEth = web3.utils.fromWei(balance, 'ether'); const statusMessage = `✅ *Bot Status: Online*\n\n` + `📦 *Latest Block:* ${latestBlock}\n` + `💎 *Wallet Balance:* ${parseFloat(balanceInEth).toFixed(4)} ETH\n` + `👥 *Active Users:* ${activeChatIds.size}`; bot.sendMessage(chatId, statusMessage, { parse_mode: 'Markdown' }); } catch (error) { bot.sendMessage(chatId, '❌ Error fetching status. Please try again.'); logger.error('Status command error:', error); } }); bot.onText(/\/wallet/, (msg) => { const chatId = msg.chat.id; const walletMessage = `🔍 *Monitored Wallet*\n\n` + `Address: \`${process.env.MONITORED_WALLET}\`\n\n` + `[View on Etherscan](https://etherscan.io/address/${process.env.MONITORED_WALLET})`; bot.sendMessage(chatId, walletMessage, { parse_mode: 'Markdown' }); }); bot.onText(/\/stop/, (msg) => { const chatId = msg.chat.id; activeChatIds.delete(chatId); bot.sendMessage(chatId, '🛑 Notifications stopped. Use /start to resume.'); logger.info(`User stopped notifications: ${chatId}`); }); bot.onText(/\/help/, (msg) => { const chatId = msg.chat.id; bot.sendMessage(chatId, 'Use /start to begin tracking wallet transactions.'); }); // Error handling for polling errors bot.on('polling_error', (error) => { logger.error('Telegram polling error:', error); });
Step 4 — Testing & Debugging
Testing blockchain applications requires a methodical approach. Create a test file:
javascript// __tests__/bot.test.js const Web3 = require('web3'); describe('Ethereum Wallet Tracker Bot', () => { let web3; beforeAll(() => { web3 = new Web3(process.env.INFURA_WSS_URL); }); test('should connect to Ethereum network', async () => { const isConnected = await web3.eth.net.isListening(); expect(isConnected).toBe(true); }); test('should validate wallet address format', () => { const validAddress = '0x742d35Cc6634C0532925a3b844Bc9e7595f6E3D'; expect(Web3.utils.isAddress(validAddress)).toBe(true); }); test('should format transaction message correctly', () => { const mockTx = { hash: '0x123...', from: '0xabc...', to: '0xdef...', value: '1000000000000000000', gasPrice: '20000000000' }; const message = formatTransactionMessage(mockTx, '1.0', 'IN'); expect(message).toContain('📥'); expect(message).toContain('1.0 ETH'); }); });
Common debugging scenarios and solutions:
WebSocket Connection Drops:
javascriptweb3.currentProvider.on('error', e => { logger.error('WebSocket error:', e); }); web3.currentProvider.on('end', e => { logger.error('WebSocket connection ended:', e); // Implement reconnection logic setTimeout(() => { web3.setProvider(new Web3.providers.WebsocketProvider(process.env.INFURA_WSS_URL)); monitorWallet(process.env.MONITORED_WALLET); }, 5000); });
Rate Limiting Issues:
javascriptconst rateLimiter = { requests: 0, resetTime: Date.now() + 60000, canMakeRequest() { if (Date.now() > this.resetTime) { this.requests = 0; this.resetTime = Date.now() + 60000; } if (this.requests < 100) { this.requests++; return true; } return false; } };
Step 5 — Deploying to a VPS or Serverless Function
For production deployment, create a Dockerfile:
dockerfileFROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . RUN addgroup -g 1001 -S nodejs RUN adduser -S nodejs -u 1001 USER nodejs CMD ["node", "index.js"]
Add a health check endpoint:
javascriptconst http = require('http'); const healthServer = http.createServer((req, res) => { if (req.url === '/health') { res.writeHead(200); res.end('OK'); } }); healthServer.listen(3000);
Deploy using PM2 for process management:
bashnpm install -g pm2 pm2 start index.js --name eth-wallet-bot pm2 save pm2 startup
Best Practices & Optimizations
Implement these optimizations for production reliability:
1. Transaction Confirmation Handling:
javascriptasync function waitForConfirmation(txHash, confirmations = 12) { let currentBlock = await web3.eth.getBlockNumber(); const targetBlock = currentBlock + confirmations; return new Promise((resolve, reject) => { const checkConfirmation = setInterval(async () => { const receipt = await web3.eth.getTransactionReceipt(txHash); currentBlock = await web3.eth.getBlockNumber(); if (receipt && currentBlock >= targetBlock) { clearInterval(checkConfirmation); resolve(receipt); } }, 15000); // Check every 15 seconds }); }
2. Gas Price Optimization:
javascriptasync function getOptimalGasPrice() { const history = await web3.eth.getFeeHistory(20, 'latest', [25, 50, 75]); const baseFee = history.baseFeePerGas[history.baseFeePerGas.length - 1]; const priorityFee = history.reward[history.reward.length - 1][1]; // 50th percentile return { maxFeePerGas: baseFee + priorityFee, maxPriorityFeePerGas: priorityFee }; }
3. Memory Leak Prevention:
javascriptsetInterval(() => { // Clean up inactive chats activeChatIds.forEach(chatId => { bot.getChat(chatId).catch(() => { activeChatIds.delete(chatId); logger.info(`Removed inactive chat: ${chatId}`); }); }); }, 3600000); // Every hour
Security Considerations
Protecting your bot requires multiple security layers:
API Key Protection:
- Store all sensitive data in environment variables
- Use AWS Secrets Manager or similar for production
- Rotate API keys regularly
Wallet Security:
javascript// Never store private keys in code // Use read-only operations only const SAFE_METHODS = ['eth_getBalance', 'eth_getTransaction', 'eth_blockNumber']; function validateMethod(method) { if (!SAFE_METHODS.includes(method)) { throw new Error('Unsafe method attempted'); } }
Re-org Protection:
javascriptasync function handleReorg(blockNumber) { logger.warn(`Potential re-org detected at block ${blockNumber}`); // Re-validate recent transactions const recentTxs = await getRecentTransactions(blockNumber - 10); recentTxs.forEach(tx => validateTransaction(tx)); }
Input Validation:
javascriptfunction sanitizeAddress(address) { if (!Web3.utils.isAddress(address)) { throw new Error('Invalid Ethereum address'); } return address.toLowerCase(); }
Visual Diagram Walk-through
Imagine a flowchart with four main components:
- Ethereum Node (Infura WebSocket) continuously streams blockchain data
- Your Bot Server filters transactions matching your wallet address
- Telegram Bot API receives formatted alerts from your server
- User's Telegram App displays notifications instantly
The data flows left to right: when a transaction occurs on Ethereum, it triggers your WebSocket subscription, your bot processes it, formats a message, and sends it through Telegram's API to the user's device. This entire process typically completes in under 3 seconds.
Final Checklist
Before deploying your Ethereum wallet tracker bot, verify:
- Environment variables properly configured in
.env
- WebSocket reconnection logic implemented
- Error handling covers network failures
- Telegram bot commands respond correctly
- Rate limiting prevents API exhaustion
- Logs capture errors for debugging
- Health check endpoint returns 200 OK
- Process manager (PM2) configured for auto-restart
Frequently Asked Questions
Q: How much does running this bot cost? A: Using free tiers from Infura and running on a $5/month VPS covers most use cases. Heavy usage might require paid API plans.
Q: Can I monitor multiple wallets simultaneously?
A: Yes! Modify the monitorWallet
function to accept an array of addresses and check each transaction against all monitored wallets.
Q: How do I track ERC-20 token transfers?
A: Subscribe to contract events using web3.eth.subscribe('logs')
with the Transfer event signature and token contract address.
Q: What about tracking internal transactions? A: Internal transactions require a different approach. Consider using Etherscan's API or running your own archive node with trace capabilities.
Q: How can I reduce notification spam? A: Implement filters for minimum transaction amounts, specific addresses, or time-based rate limiting.
Conclusion
You've built a powerful Ethereum wallet tracker bot that provides real-time transaction notifications through Telegram. This foundation opens doors to advanced features like portfolio tracking, DeFi position monitoring, or whale watching systems. The WebSocket approach ensures minimal latency while the modular architecture supports easy expansion.
For deeper Telegram bot capabilities, explore the official Telegram Bot API documentation. Ready to go further? Explore more Web3 development resources at IrulMedia.
Launch your bot and never miss a wallet movement again!