TUTORIAL · 2026

Build a Crypto Whale Alert Telegram Bot in Under an Hour

April 2026 · 8 min read · Node.js + free APIs + Railway free tier deploy

The cleanest way to get crypto alerts is Telegram. No app fatigue, no notification spam, just instant DMs when something matters. In this tutorial we'll build a bot that scans famous Ethereum wallets and DMs you when they move $50k+ — using only free APIs and free hosting.

Total time: ~45 minutes. Total cost: $0. Code lives in ~120 lines of Node.js.

What you'll have at the end

A Telegram bot that sends you messages like:
"🐋 Vitalik.eth received 1,247 ETH (~$4.4M)" — with a link to Etherscan, every time a tracked wallet does something significant.

Stack

Step 1 — Create the bot via BotFather (3 min)

  1. Open Telegram, search @BotFather, start a chat.
  2. Send /newbot.
  3. Pick a display name (e.g. "MyWhaleAlerts").
  4. Pick a username ending in bot (e.g. my_whale_alerts_bot).
  5. BotFather sends you a token like 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11. Save it.

Now find your own Telegram chat ID:

  1. Search @userinfobot, send /start. It replies with your numeric ID.

Step 2 — Set up the project (5 min)

mkdir whale-bot && cd whale-bot
npm init -y
npm install dotenv

# Create .env (DO NOT commit)
echo "TELEGRAM_BOT_TOKEN=YOUR_BOT_TOKEN" > .env
echo "TELEGRAM_CHAT_ID=YOUR_CHAT_ID" >> .env
echo "ETHERSCAN_API_KEY=YOUR_OPTIONAL_KEY" >> .env
echo ".env" > .gitignore

Update package.json to add "type": "module" so we can use ESM imports.

Step 3 — The bot code (30 min)

Save this as bot.js:

import 'dotenv/config';

const TG_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
const CHAT_ID  = process.env.TELEGRAM_CHAT_ID;
const ETH_KEY  = process.env.ETHERSCAN_API_KEY || '';
const SCAN_INTERVAL_MS = 60_000;
const MIN_USD = 50_000;

const WHALES = [
    { addr: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', label: 'Vitalik.eth' },
    { addr: '0x28C6c06298d514Db089934071355E5743bf21d60', label: 'Binance 14' },
    { addr: '0xF977814e90dA44bFA03b6295A0616a897441aceC', label: 'Binance 8' },
    { addr: '0x71660c4005BA85c37ccec55d0C4493E66Fe775d3', label: 'Coinbase 1' },
    { addr: '0x503828976D22510aad0201ac7EC88293211D23Da', label: 'Coinbase 2' },
    { addr: '0x2910543Af39abA0Cd09dBb2D50200b3E800A63D2', label: 'Kraken' },
    { addr: '0x0000000000A5c50F35baeD3b2C76b6e78D03d35F', label: 'Wintermute' },
    { addr: '0x5754284f345afc66a98fbB0a0Afe71e0F007B949', label: 'Tether Treasury' },
];

const seenHashes = new Set();
let ethPrice = 3500;

async function refreshEthPrice() {
    try {
        const r = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd');
        const j = await r.json();
        if (j.ethereum?.usd) ethPrice = j.ethereum.usd;
    } catch {}
}

async function sendTelegram(text) {
    try {
        await fetch(`https://api.telegram.org/bot${TG_TOKEN}/sendMessage`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                chat_id: CHAT_ID,
                text,
                parse_mode: 'HTML',
                disable_web_page_preview: true,
            }),
        });
    } catch (e) { console.error('[tg]', e.message); }
}

async function fetchTxns(addr) {
    const keyParam = ETH_KEY ? `&apikey=${ETH_KEY}` : '';
    const url = `https://api.etherscan.io/api?module=account&action=txlist`
              + `&address=${addr}&page=1&offset=5&sort=desc${keyParam}`;
    try {
        const r = await fetch(url);
        const j = await r.json();
        if (j.status !== '1' || !Array.isArray(j.result)) return [];
        return j.result;
    } catch { return []; }
}

async function scanOnce() {
    await refreshEthPrice();
    for (const whale of WHALES) {
        const txns = await fetchTxns(whale.addr);
        for (const tx of txns) {
            if (seenHashes.has(tx.hash)) continue;
            seenHashes.add(tx.hash);

            const ethVal = parseFloat(tx.value) / 1e18;
            const usdVal = ethVal * ethPrice;
            if (usdVal < MIN_USD) continue;

            const isOutgoing = tx.from.toLowerCase() === whale.addr.toLowerCase();
            const verb = isOutgoing ? '📤 sent' : '📥 received';
            const msg =
                `${whale.label} ${verb} ${ethVal.toFixed(2)} ETH ` +
                `(~$${(usdVal/1000).toFixed(0)}K)\n\n` +
                `View on Etherscan`;
            await sendTelegram(msg);
        }
        // Throttle for free-tier rate limit
        await new Promise(r => setTimeout(r, ETH_KEY ? 250 : 6000));
    }

    // Cap memory
    if (seenHashes.size > 1000) {
        const toRemove = seenHashes.size - 800;
        let removed = 0;
        for (const h of seenHashes) {
            if (removed++ >= toRemove) break;
            seenHashes.delete(h);
        }
    }
}

console.log('Whale bot starting. Scan interval:', SCAN_INTERVAL_MS, 'ms');
sendTelegram('🤖 Whale bot online. Tracking ' + WHALES.length + ' wallets.');
scanOnce();
setInterval(() => scanOnce().catch(e => console.error('[scan]', e.message)), SCAN_INTERVAL_MS);

Run it locally to test:

node bot.js

Within ~60 seconds you should get a "🤖 Whale bot online" Telegram message. After ~5–10 minutes you'll start getting actual whale alerts (assuming any of the tracked wallets moved size).

Step 4 — Deploy to Railway (free tier, 5 min)

  1. Sign up at railway.app with your GitHub.
  2. Push your bot to a private GitHub repo.
  3. Railway → New Project → Deploy from GitHub repo.
  4. Settings → Variables → add TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, ETHERSCAN_API_KEY.
  5. Railway auto-detects node bot.js; if not, set start command in Settings.
  6. Deploy. Within ~60 seconds you'll see "🤖 Whale bot online" in Telegram.

Railway gives you 500 hours/month free — way more than the 720 hours a month requires for 24/7 uptime. Use Render or Fly.io if you prefer; same pattern.

Customizations you'll want

1. Per-wallet thresholds

Vitalik moves $1M+ regularly; you might only care about $5M+. Add a per-wallet minUsd:

const WHALES = [
    { addr: '0xd8dA...96045', label: 'Vitalik.eth', minUsd: 5_000_000 },
    { addr: '0x28C6...1d60', label: 'Binance 14', minUsd: 100_000 },
    // ...
];
// Then in scanOnce:
if (usdVal < (whale.minUsd ?? MIN_USD)) continue;

2. ERC-20 token transfers

Currently the bot tracks ETH only. To track stablecoins (USDC/USDT) and other tokens, swap action=txlist with action=tokentx. Each tx now has tokenSymbol, tokenName, tokenDecimal, and the value is in token units.

3. Multi-user support

To let other people subscribe to your bot, add a Supabase database, store chat_id per user, and listen for /start commands. The full code for this is in the AlphaDesk Pro tier.

4. Solana support

Use Helius (free 100k req/month) instead of Etherscan. Their parsed-transaction endpoint decodes Solana swaps into structured JSON, much nicer than raw RPC.

Production gotchas

Want this bot fully built + deployed for you?

AlphaDesk Pro tier ships a polished version of this bot — multi-user, Supabase-backed, with admin panel and Stripe subscriptions if you want to charge for access.

Get AlphaDesk Pro →

FAQ

Can I monetize this bot?

Yes. Easiest path: free tier (3 wallets), paid tier ($5-9/mo, unlimited wallets + custom thresholds). Use Stripe Checkout. The code template for this is in AlphaDesk Pro.

How do I avoid spam alerts?

Tighten the threshold. $50k is sensitive; $250k is restrictive. Or filter by direction (only alerts on inflows, not outflows).

What if the bot dies overnight?

Railway auto-restarts crashed processes. Add a process-level error handler at the top:

process.on('uncaughtException', e => console.error('[crash]', e));
process.on('unhandledRejection', e => console.error('[unhandled]', e));

Can I add multiple chains (BSC, Polygon, Arbitrum)?

Yes. Each EVM chain has its own Etherscan-clone (BscScan, Polygonscan, Arbiscan). Same API shape, just swap the base URL. Helius for Solana.