Copy trading on Solana gets talked about as if it is a single thing — find a good wallet, mirror everything it does, profit. In reality there are three distinct problems to solve: sourcing high-quality wallets to follow, getting reliable real-time signal from those wallets, and executing trades fast enough that you capture a similar entry price.
Most guides focus on the execution side. This one focuses on the signal feed — specifically, how to build a clean copy-trading signal pipeline using the Solana API Wallet Tracker endpoints. By the end you will have a TypeScript service that takes KOL wallets from the MadeOnSol leaderboard, tracks their swaps via the API, applies size filters, and emits structured buy signals ready for an execution layer.
If you want a survey of consumer copy-trading tools rather than a build guide, see our roundup of the best Solana copy trading bots.
Why API-Based Signal Beats Scraping
The naive approach to copy trading is to set up a Geyser subscription or poll an RPC node for all transactions from a list of wallets. This works, but it comes with real operational overhead: parsing raw transaction logs, handling program upgrades that change instruction formats, dealing with RPC rate limits, and storing and indexing the raw data yourself.
An API-based approach offloads all of that. The Wallet Tracker API indexes swap events, normalises them into consistent buy/sell records with token metadata, and makes them queryable via simple HTTP calls. You get action, token_mint, token_symbol, sol_amount, token_amount, and signature in a clean JSON payload — no log parsing required.
The tradeoff is a few seconds of indexing latency. For most copy strategies that is fine; you are not trying to front-run the wallet, you are trying to follow it into a position before the broader market notices.
Step 1: Sourcing KOL Wallets
The quality of your signal feed is entirely determined by the quality of the wallets you follow. Copying random on-chain addresses is noise. Copying consistently profitable traders with a track record is signal.
The MadeOnSol KOL tracker maintains a leaderboard of known KOL wallets ranked by recent PnL, win rate, and trade volume. You can browse it on-site or query it via the API to get wallet addresses programmatically.
When evaluating KOLs for copy trading, look for:
- High win rate over a meaningful sample — at least 50+ trades in the last 30 days. A 90% win rate on 5 trades is noise.
- Consistent SOL size per trade — wallets that vary their position size wildly are harder to copy; you do not know if their next buy is a $500 punt or a $50,000 conviction play.
- Short average hold time — if a KOL holds positions for weeks, copy trading becomes position management, which is a different problem.
- No obvious wash trading patterns — wallets that buy and sell the same token repeatedly in small increments are often inflating their own stats.
Once you have a list of addresses, add them to your watchlist via the API:
import { MadeOnSol } from 'madeonsol';
const client = new MadeOnSol({ apiKey: process.env.MADEONSOL_API_KEY! });
const kolWallets = [
{ address: 'WALLET_ADDRESS_1', label: 'KOL: High Win Rate 30d' },
{ address: 'WALLET_ADDRESS_2', label: 'KOL: Large Cap Focus' },
{ address: 'WALLET_ADDRESS_3', label: 'KOL: Memecoin Alpha' },
];
for (const wallet of kolWallets) {
await client.walletTracker.addToWatchlist(wallet);
console.log(`Tracking ${wallet.label}`);
}
See the API docs for watchlist limits per tier (BASIC: 10, PRO: 50, ULTRA: 100) and authentication details.
Step 2: Building the Signal Pipeline
The signal pipeline has four stages:
- Fetch — poll the trades endpoint for new buy events
- Filter — apply your minimum SOL threshold and other criteria
- Deduplicate — avoid emitting the same signal twice
- Emit — push structured signals to your execution layer or message queue
Here is a complete implementation:
// src/signal-pipeline.ts
import { MadeOnSol } from 'madeonsol';
const client = new MadeOnSol({ apiKey: process.env.MADEONSOL_API_KEY! });
interface CopySignal {
walletLabel: string;
wallet: string;
tokenMint: string;
tokenSymbol: string;
solAmount: number;
tokenAmount: number;
signature: string;
timestamp: string;
}
// Minimum SOL to treat as a real position, not a test trade
const MIN_SOL = parseFloat(process.env.MIN_SOL_THRESHOLD ?? '1.0');
// Tokens to never copy (stables, wrapped SOL, etc.)
const BLOCKED_MINTS = new Set([
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', // USDT
'So11111111111111111111111111111111111111112', // wSOL
]);
// Track seen event IDs to avoid duplicate signals
const seenEvents = new Set<string>();
// Track cursor per wallet for incremental polling
const cursors = new Map<string, string | undefined>();
async function fetchNewBuys(walletAddress: string): Promise<any[]> {
const params: any = {
wallet: walletAddress,
action: 'buy',
event_type: 'swap',
limit: 50,
};
const response = await client.walletTracker.getTrades(params);
return response.data;
}
function toSignal(event: any, label: string): CopySignal {
return {
walletLabel: label,
wallet: event.wallet,
tokenMint: event.token_mint,
tokenSymbol: event.token_symbol,
solAmount: event.sol_amount,
tokenAmount: event.token_amount,
signature: event.signature,
timestamp: event.timestamp,
};
}
async function runSignalPipeline(
onSignal: (signal: CopySignal) => Promise<void>
) {
const watchlist = await client.walletTracker.getWatchlist();
for (const entry of watchlist.data) {
const events = await fetchNewBuys(entry.address);
for (const event of events) {
// Skip already-processed events
if (seenEvents.has(event.id)) continue;
seenEvents.add(event.id);
// Size filter
if (event.sol_amount < MIN_SOL) continue;
// Blocked token filter
if (BLOCKED_MINTS.has(event.token_mint)) continue;
const signal = toSignal(event, entry.label ?? entry.address);
await onSignal(signal);
}
}
}
// Example signal handler — replace with your execution logic
async function handleSignal(signal: CopySignal) {
console.log(
`[SIGNAL] ${signal.walletLabel} bought ${signal.tokenSymbol} ` +
`for ${signal.solAmount} SOL at ${signal.timestamp}`
);
// TODO: route to your execution engine
// await executeSwap(signal.tokenMint, signal.solAmount * COPY_RATIO);
}
// Poll every 15 seconds
setInterval(() => runSignalPipeline(handleSignal), 15_000);
runSignalPipeline(handleSignal);