← Back to Articles

Building a Copy Trading Bot with WebSocket Alerts

"The best traders have spent years learning what works. A copy trading bot lets you leverage that knowledge in real-time."

In this advanced guide, we'll build a fully functional copy trading bot that automatically mirrors trades from top traders on Jupiter Perps. When a trader you're watching opens a position, your bot will open the same position—scaled to your account size—within seconds.

Important Disclaimer

Copy trading involves significant financial risk. Past performance does not guarantee future results. Only trade with funds you can afford to lose. This guide is for educational purposes—you are responsible for testing, securing, and operating your own bot.

Architecture Overview

Before we write any code, let's understand how the system works:

Copy Trading Bot Flow

PerpsTracker WebSocket Your Bot Trade Processor Jupiter Perps API Your Position
  1. PerpsTracker WebSocket sends real-time trade alerts for wallets on your watchlist
  2. Your Bot receives and parses these alerts
  3. Trade Processor validates the trade and calculates your position size
  4. Jupiter Perps API executes the trade on your behalf
  5. Your Position mirrors the original trader's position

Prerequisites

What You'll Need:

Elite PerpsTracker subscription with API key
Node.js 18+ or Python 3.10+ installed
Solana wallet with trading funds (SOL + USDC)
Private key for your trading wallet (securely stored)
Target traders added to your PerpsTracker watchlist
Basic understanding of async programming

Project Setup

Step 1: Initialize Your Project

# Create project directory mkdir copytrader-bot cd copytrader-bot # Initialize Node.js project npm init -y # Install dependencies npm install ws @solana/web3.js @jup-ag/perps-sdk dotenv

Step 2: Create Environment Configuration

Create a .env file to store your sensitive configuration:

# .env - NEVER commit this file to git! # PerpsTracker API Key (from your account settings) PERPSTRACKER_API_KEY=your_api_key_here # Your Solana wallet private key (base58 encoded) WALLET_PRIVATE_KEY=your_private_key_here # Trading configuration MAX_POSITION_SIZE_USD=1000 COPY_RATIO=0.5 MAX_LEVERAGE=10

Security Warning

Your private key gives full access to your funds. Never share it, commit it to git, or expose it in logs. Consider using a hardware wallet or secure key management solution for production use.

Core Bot Implementation

Step 3: WebSocket Connection Handler

Create src/websocket.js to handle the WebSocket connection:

const WebSocket = require('ws'); const EventEmitter = require('events'); class AlertWebSocket extends EventEmitter { constructor(apiKey) { super(); this.apiKey = apiKey; this.ws = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = 10; } connect() { const url = `wss://perpstracker.com:5001/ws/alerts?apiKey=${this.apiKey}`; this.ws = new WebSocket(url); this.ws.on('open', () => { console.log('[WebSocket] Connected to PerpsTracker'); this.reconnectAttempts = 0; this.emit('connected'); }); this.ws.on('message', (data) => { try { const alert = JSON.parse(data); if (alert.type === 'trade') { this.emit('trade', alert.data); } } catch (err) { console.error('[WebSocket] Parse error:', err); } }); this.ws.on('close', () => { console.log('[WebSocket] Connection closed'); this.scheduleReconnect(); }); this.ws.on('error', (err) => { console.error('[WebSocket] Error:', err.message); }); this.ws.on('ping', () => { this.ws.pong(); }); } scheduleReconnect() { if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error('[WebSocket] Max reconnect attempts reached'); return; } const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000); console.log(`[WebSocket] Reconnecting in ${delay}ms...`); setTimeout(() => { this.reconnectAttempts++; this.connect(); }, delay); } } module.exports = AlertWebSocket;

Step 4: Trade Executor

Create src/executor.js to handle trade execution:

const { Connection, Keypair } = require('@solana/web3.js'); const bs58 = require('bs58'); class TradeExecutor { constructor(config) { this.config = config; this.connection = new Connection( 'https://api.mainnet-beta.solana.com' ); this.wallet = Keypair.fromSecretKey( bs58.decode(config.privateKey) ); this.pendingTrades = new Map(); } async executeCopyTrade(alert) { // Validate the trade if (!this.shouldCopyTrade(alert)) { console.log(`[Executor] Skipping trade: ${alert.marketSymbol}`); return null; } // Calculate position size const positionSize = this.calculatePositionSize(alert); console.log(`[Executor] Copying trade:`); console.log(` Market: ${alert.marketSymbol}`); console.log(` Side: ${alert.side}`); console.log(` Size: $${positionSize}`); console.log(` Leverage: ${Math.min(alert.leverage, this.config.maxLeverage)}x`); try { // Execute via Jupiter Perps API const result = await this.openPosition({ market: alert.marketSymbol, side: alert.side, sizeUsd: positionSize, leverage: Math.min(alert.leverage, this.config.maxLeverage) }); console.log(`[Executor] Trade executed: ${result.signature}`); return result; } catch (error) { console.error(`[Executor] Trade failed: ${error.message}`); throw error; } } shouldCopyTrade(alert) { // Only copy new position opens if (alert.tradeType !== 'open') { return false; } // Check if we already have a position in this market if (this.pendingTrades.has(alert.marketSymbol)) { return false; } // Validate leverage is within limits if (alert.leverage > this.config.maxLeverage * 2) { console.log(`[Executor] Leverage too high: ${alert.leverage}x`); return false; } return true; } calculatePositionSize(alert) { // Scale position based on copy ratio let size = alert.sizeUsd * this.config.copyRatio; // Cap at max position size size = Math.min(size, this.config.maxPositionSize); // Minimum $10 position size = Math.max(size, 10); return Math.round(size * 100) / 100; } async openPosition(params) { // Jupiter Perps API integration // This is a simplified example - see Jupiter docs for full implementation const response = await fetch('https://perps-api.jup.ag/v1/orders/open', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ owner: this.wallet.publicKey.toString(), marketIndex: this.getMarketIndex(params.market), side: params.side, sizeUsd: params.sizeUsd, leverage: params.leverage, collateralMint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' // USDC }) }); return response.json(); } getMarketIndex(symbol) { const markets = { 'SOL-PERP': 0, 'BTC-PERP': 1, 'ETH-PERP': 2 }; return markets[symbol] ?? 0; } } module.exports = TradeExecutor;

Step 5: Main Bot Entry Point

Create src/index.js to tie everything together:

require('dotenv').config(); const AlertWebSocket = require('./websocket'); const TradeExecutor = require('./executor'); // Load configuration const config = { apiKey: process.env.PERPSTRACKER_API_KEY, privateKey: process.env.WALLET_PRIVATE_KEY, maxPositionSize: parseFloat(process.env.MAX_POSITION_SIZE_USD) || 1000, copyRatio: parseFloat(process.env.COPY_RATIO) || 0.5, maxLeverage: parseFloat(process.env.MAX_LEVERAGE) || 10 }; // Validate configuration if (!config.apiKey || !config.privateKey) { console.error('Missing required environment variables'); process.exit(1); } // Initialize components const ws = new AlertWebSocket(config.apiKey); const executor = new TradeExecutor(config); // Track statistics let stats = { tradesReceived: 0, tradesExecuted: 0, tradesFailed: 0 }; // Handle trade alerts ws.on('trade', async (trade) => { stats.tradesReceived++; console.log(`\n[Alert] New trade from ${trade.owner.slice(0, 8)}...`); console.log(` ${trade.side.toUpperCase()} ${trade.marketSymbol}`); console.log(` Size: $${trade.sizeUsd} @ ${trade.leverage}x`); try { const result = await executor.executeCopyTrade(trade); if (result) { stats.tradesExecuted++; } } catch (error) { stats.tradesFailed++; console.error(`[Error] ${error.message}`); } }); ws.on('connected', () => { console.log('\n=== Copy Trading Bot Started ==='); console.log(`Max Position: $${config.maxPositionSize}`); console.log(`Copy Ratio: ${config.copyRatio}x`); console.log(`Max Leverage: ${config.maxLeverage}x`); console.log('Waiting for trade alerts...\n'); }); // Start the bot ws.connect(); // Print stats periodically setInterval(() => { console.log(`[Stats] Received: ${stats.tradesReceived} | Executed: ${stats.tradesExecuted} | Failed: ${stats.tradesFailed}`); }, 60000); // Handle shutdown process.on('SIGINT', () => { console.log('\nShutting down...'); process.exit(0); });

Risk Management Features

A production copy trading bot needs robust risk management. Here are essential features to implement:

Position Limits

Recommended Limits:

  • Max Single Position: 10-20% of your trading capital
  • Max Total Exposure: 50-70% of capital across all positions
  • Max Leverage: Cap at 5-10x regardless of what the trader uses
  • Daily Loss Limit: Stop copying if you lose more than 5% in a day

Trade Filtering

// Add to your shouldCopyTrade function shouldCopyTrade(alert) { // Skip very small positions (likely tests) if (alert.sizeUsd < 100) { return false; } // Skip extremely high leverage trades if (alert.leverage > 25) { return false; } // Only copy during your active hours (optional) const hour = new Date().getHours(); if (hour < 8 || hour > 22) { return false; } // Only copy from specific trusted wallets const trustedWallets = ['wallet1...', 'wallet2...']; if (!trustedWallets.includes(alert.owner)) { return false; } return true; }

Handling Position Closes

When a trader you're copying closes their position, you should close yours too. Extend your bot to handle close events:

// Track open positions const openPositions = new Map(); ws.on('trade', async (trade) => { if (trade.tradeType === 'open') { // Execute copy trade and track it const result = await executor.executeCopyTrade(trade); if (result) { openPositions.set(`${trade.owner}-${trade.marketSymbol}`, { ourPosition: result, theirEntry: trade.entryPrice }); } } if (trade.tradeType === 'close') { const key = `${trade.owner}-${trade.marketSymbol}`; if (openPositions.has(key)) { console.log(`[Executor] Closing position: ${trade.marketSymbol}`); await executor.closePosition(trade.marketSymbol); openPositions.delete(key); } } });

Testing Your Bot

Test Before Going Live!

Never deploy a copy trading bot with real funds until you've thoroughly tested it. Use these strategies to validate your bot:

Testing Strategies

  1. Paper Trading Mode: Log what trades would execute without actually executing them
  2. Minimum Size Testing: Test with $10-20 positions first
  3. Single Wallet Testing: Copy one wallet for a week before adding more
  4. Dry Run with Devnet: Test the full flow on Solana devnet
// Paper trading mode - add to your executor async executeCopyTrade(alert) { const positionSize = this.calculatePositionSize(alert); if (this.config.paperTrading) { console.log(`[PAPER] Would execute:`); console.log(` ${alert.side} ${alert.marketSymbol}`); console.log(` Size: $${positionSize}`); return { paperTrade: true }; } // Real execution... }

Monitoring and Alerts

Your bot should notify you of important events. Here's how to add Discord notifications:

async sendDiscordAlert(message) { const webhookUrl = process.env.DISCORD_WEBHOOK_URL; if (!webhookUrl) return; await fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: message, username: 'CopyTrader Bot' }) }); } // Use in your trade handler ws.on('trade', async (trade) => { const result = await executor.executeCopyTrade(trade); if (result) { await sendDiscordAlert( `Copied trade: ${trade.side} ${trade.marketSymbol} @ $${result.sizeUsd}` ); } });

Production Deployment Checklist

Before Going Live:

Test thoroughly in paper trading mode for at least 1 week
Test with minimum position sizes ($10-20) for another week
Set up monitoring and alerting (Discord, Telegram, etc.)
Implement all risk management features
Store private keys securely (environment variables, not code)
Set up automatic restart on failure (PM2, systemd, etc.)
Keep logs for debugging and analysis
Have a kill switch to stop all trading immediately
Document your configuration and recovery procedures

Common Issues and Solutions

Issue Solution
WebSocket disconnects frequently Implement exponential backoff reconnection; respond to ping messages
Trades execute too slowly Use priority fees; optimize your RPC endpoint
Position sizes are wrong Double-check your copy ratio calculation; add logging
Missing some trades Check that traders are on your watchlist; verify API key permissions
Bot crashes overnight Use process manager (PM2); add error handling for all async operations

Need Help?

Join our Discord community to connect with other developers building trading bots. Check out the API documentation for complete endpoint references.

Next Steps

You now have a foundation for a copy trading bot. Here are ways to enhance it:

  • Add performance tracking: Track PnL per copied trader
  • Implement smart filtering: Only copy trades that match certain patterns
  • Build a dashboard: Web interface to monitor and control the bot
  • Add webhooks: Use webhooks as a backup notification method
  • Multi-account support: Scale to copy across multiple wallets
← Back to Articles