Skip to main content

Overview

In this guide, you’ll learn how to build a Telegram bot that sends real-time liquidation alerts for perpetual trading positions. This is useful for:
  • Traders who want to monitor market liquidations for trading signals
  • Risk managers tracking leverage exposure in the market
  • Analysts studying liquidation patterns and market dynamics

Prerequisites

  • A Mobula API key (Get one here)
  • A Telegram Bot Token (create via @BotFather)
  • Node.js 18+ or Bun runtime
  • Basic TypeScript knowledge

Step 1: Create Your Telegram Bot

  1. Open Telegram and search for @BotFather
  2. Send /newbot and follow the prompts
  3. Save your Bot Token (looks like 123456789:ABCdefGHIjklMNOpqrsTUVwxyz)
  4. Create a channel or group for alerts and add your bot as admin
  5. Get the Channel ID (you can use @userinfobot or check the URL)
For public channels, the ID is usually -100 followed by numbers. For private channels, you can get the ID by forwarding a message to @userinfobot.

Step 2: Set Up Your Project

mkdir liquidation-bot
cd liquidation-bot
npm init -y
npm install ws typescript @types/ws tsx
Create a tsconfig.json:
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "strict": true,
    "outDir": "./dist"
  }
}

Step 3: Build the Liquidation Bot

Create src/index.ts:
import WebSocket from 'ws';

// Configuration
const MOBULA_API_KEY = 'YOUR_MOBULA_API_KEY';
const TELEGRAM_BOT_TOKEN = 'YOUR_TELEGRAM_BOT_TOKEN';
const TELEGRAM_CHANNEL_ID = '-100XXXXXXXXXX'; // Your channel ID

// WebSocket endpoint for perps
const WS_ENDPOINT = 'wss://stream-perps-prod.mobula.io/';

// Supported chains for Lighter
const LIGHTER_CHAINS = ['lighter:301', 'lighter:304'];

interface PerpOrder {
  market: string;
  exchange: string;
  chainId: string;
  type: string;
  priceQuote: number;
  priceUSD: number;
  collateralAmountRaw: string;
  traderAddress: string;
  transactionHash: string;
  leverage: number | null;
  extra?: {
    trade?: {
      long?: boolean;
    };
  };
}

interface WebSocketMessage {
  data: PerpOrder;
  chainId: string;
  subscriptionId: string;
}

// Format USD values
function formatUsd(value: number): string {
  if (value >= 1_000_000) return `$${(value / 1_000_000).toFixed(1)}M`;
  if (value >= 1_000) return `$${(value / 1_000).toFixed(1)}K`;
  return `$${value.toFixed(2)}`;
}

// Format price
function formatPrice(price: number): string {
  if (price >= 1000) return `$${price.toFixed(2)}`;
  if (price >= 1) return `$${price.toFixed(4)}`;
  return `$${price.toFixed(6)}`;
}

// Extract symbol from market (e.g., "lighter-eth-usd" -> "ETH")
function getSymbol(market: string): string {
  const parts = market.split('-');
  return parts.length >= 2 ? parts[1].toUpperCase() : market.toUpperCase();
}

// Calculate position value
function calculateValueUsd(order: PerpOrder): number {
  const collateralUsd = Number(order.collateralAmountRaw) / 1e18;
  const leverage = order.leverage || 1;
  return collateralUsd * leverage;
}

// Get Lighter explorer URL
function getExplorerUrl(txHash: string): string {
  return `https://app.lighter.xyz/explorer/logs/${txHash}/`;
}

// Send Telegram message
async function sendTelegramMessage(text: string): Promise<void> {
  try {
    const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`;
    const response = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        chat_id: TELEGRAM_CHANNEL_ID,
        text,
        parse_mode: 'HTML',
        disable_web_page_preview: true,
      }),
    });

    if (!response.ok) {
      const error = await response.text();
      console.error('Telegram error:', error);
    }
  } catch (error) {
    console.error('Failed to send Telegram message:', error);
  }
}

// Handle liquidation event
async function handleLiquidation(order: PerpOrder): Promise<void> {
  const symbol = getSymbol(order.market);
  const valueUsd = calculateValueUsd(order);
  const price = order.priceQuote || order.priceUSD || 0;
  const explorerUrl = getExplorerUrl(order.transactionHash);

  // Determine position direction
  const isLong = order.extra?.trade?.long ?? true;
  const direction = isLong ? 'LONG' : 'SHORT';

  const message = `🔴 #${symbol} Liquidated ${direction} - ${formatUsd(valueUsd)} at ${formatPrice(price)} | <a href="${explorerUrl}">tx</a>`;

  console.log(`[LIQUIDATION] ${message}`);
  await sendTelegramMessage(message);
}

// Connect to WebSocket
function connect(): void {
  const ws = new WebSocket(WS_ENDPOINT);

  ws.on('open', () => {
    console.log('✅ Connected to Mobula Perps Stream');

    // Subscribe to liquidations
    const subscription = {
      event: 'stream',
      authorization: MOBULA_API_KEY,
      data: {
        chainIds: LIGHTER_CHAINS,
        events: ['order'],
        filters: { eq: ['type', 'LIQUIDATION'] },
      },
    };

    ws.send(JSON.stringify(subscription));
    console.log('📡 Subscribed to liquidation events');
  });

  ws.on('message', async (data: WebSocket.Data) => {
    try {
      const message: WebSocketMessage = JSON.parse(data.toString());

      // Skip non-data messages (like subscription confirmations)
      if (!message.data || !message.data.type) return;

      if (message.data.type === 'LIQUIDATION') {
        await handleLiquidation(message.data);
      }
    } catch (error) {
      console.error('Error processing message:', error);
    }
  });

  ws.on('error', (error) => {
    console.error('WebSocket error:', error);
  });

  ws.on('close', () => {
    console.log('❌ Disconnected, reconnecting in 5s...');
    setTimeout(connect, 5000);
  });

  // Keep connection alive with ping
  setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify({ event: 'ping' }));
    }
  }, 30000);
}

// Start the bot
console.log('🚀 Starting Liquidation Alert Bot...');
connect();

Step 4: Run Your Bot

npx tsx src/index.ts
Or with Bun:
bun run src/index.ts
You should see:
🚀 Starting Liquidation Alert Bot...
✅ Connected to Mobula Perps Stream
📡 Subscribed to liquidation events

Step 5: Customize Your Alerts

Add Market Order Alerts

Extend your bot to also alert on large market orders:
// Minimum USD value to alert (e.g., $10,000)
const MIN_MARKET_ORDER_USD = 10_000;

async function handleMarketOrder(order: PerpOrder): Promise<void> {
  const valueUsd = calculateValueUsd(order);
  
  // Only alert for large orders
  if (valueUsd < MIN_MARKET_ORDER_USD) return;

  const symbol = getSymbol(order.market);
  const price = order.priceQuote || order.priceUSD || 0;
  const explorerUrl = getExplorerUrl(order.transactionHash);
  
  const isBuy = order.type === 'MARKET_BUY';
  const emoji = isBuy ? '🟢' : '🔴';
  const action = isBuy ? 'BUY' : 'SELL';

  const message = `${emoji} #${symbol} Market ${action} - ${formatUsd(valueUsd)} at ${formatPrice(price)} | <a href="${explorerUrl}">tx</a>`;

  console.log(`[MARKET] ${message}`);
  await sendTelegramMessage(message);
}

// Update the message handler
ws.on('message', async (data: WebSocket.Data) => {
  const message = JSON.parse(data.toString());
  if (!message.data?.type) return;

  switch (message.data.type) {
    case 'LIQUIDATION':
      await handleLiquidation(message.data);
      break;
    case 'MARKET_BUY':
    case 'MARKET_SELL':
      await handleMarketOrder(message.data);
      break;
  }
});

Filter by Specific Markets

// Only track BTC and ETH liquidations
const TRACKED_MARKETS = ['lighter-btc-usd', 'lighter-eth-usd'];

if (!TRACKED_MARKETS.includes(order.market)) return;

Add Rate Limiting

To avoid Telegram rate limits (30 messages/second):
const messageQueue: string[] = [];
let isProcessing = false;

async function queueTelegramMessage(text: string): Promise<void> {
  messageQueue.push(text);
  if (!isProcessing) processQueue();
}

async function processQueue(): Promise<void> {
  isProcessing = true;
  while (messageQueue.length > 0) {
    const message = messageQueue.shift()!;
    await sendTelegramMessage(message);
    await new Promise(resolve => setTimeout(resolve, 100)); // 10 msg/sec max
  }
  isProcessing = false;
}

Order Data Model

Here’s the full data structure you receive for perp orders:
interface PerpOrder {
  market: string;           // e.g., "lighter-eth-usd"
  exchange: string;         // e.g., "lighter", "gains"
  chainId: string;          // e.g., "lighter:301"
  type: string;             // MARKET_BUY, MARKET_SELL, LIQUIDATION, etc.
  priceQuote: number;       // Price in quote currency
  priceUSD: number;         // Price in USD
  baseAmountRaw: string;    // Base asset amount (wei)
  collateralAmountRaw: string; // Collateral amount (wei)
  traderAddress: string;    // Trader's wallet address
  transactionHash: string;  // Transaction hash
  tradeId: string;          // Unique trade identifier
  leverage: number | null;  // Position leverage
  takeProfitUSD: number | null;
  stopLossUSD: number | null;
  collateralAsset: string | null;
  date: string;             // ISO timestamp
  extra?: {
    trade?: {
      long?: boolean;       // Position direction
      index?: number;
      openPrice?: bigint;
    };
  };
}

Deployment

Using PM2

npm install -g pm2
pm2 start "npx tsx src/index.ts" --name liquidation-bot
pm2 save

Using Docker

FROM oven/bun:1
WORKDIR /app
COPY package*.json ./
RUN bun install
COPY . .
CMD ["bun", "run", "src/index.ts"]

Support

Need help? Reach out to our team: