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();