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 sends real-time trade alerts for wallets on your watchlist
- Your Bot receives and parses these alerts
- Trade Processor validates the trade and calculates your position size
- Jupiter Perps API executes the trade on your behalf
- Your Position mirrors the original trader's position
Prerequisites
What You'll Need:
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
- Paper Trading Mode: Log what trades would execute without actually executing them
- Minimum Size Testing: Test with $10-20 positions first
- Single Wallet Testing: Copy one wallet for a week before adding more
- 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:
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