Copy-trading KOL wallets is one of the most popular strategies in Solana trading. Instead of manually watching wallet trackers, you can build a bot that automatically mirrors trades from top-performing wallets the moment they happen.
This tutorial shows you how to build a KOL copy-trading bot using the MadeOnSol API, which tracks 946 curated KOL wallets in real-time with PnL data, coordination signals, and deployer enrichment.
What you'll build
A TypeScript bot that:
- Polls the MadeOnSol KOL feed for new buy trades
- Filters by KOL performance (win rate, PnL)
- Checks if multiple KOLs are buying the same token (coordination signal)
- Executes a swap via Jupiter when conditions are met
- Optionally receives instant alerts via webhooks
Prerequisites
- Node.js 18+
- A Solana wallet with SOL for trading
- A MadeOnSol API key (get one free — 100 requests/day)
- Basic TypeScript knowledge
Step 1: Set up the project
mkdir kol-copy-bot && cd kol-copy-bot
npm init -y
npm install madeonsol @solana/web3.js
Create a .env file:
MADEONSOL_API_KEY=msk_your_key_here
SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
WALLET_PRIVATE_KEY=your_base58_private_key
MAX_SOL_PER_TRADE=0.1
Step 2: Initialize the MadeOnSol client
// src/client.ts
import { MadeOnSol } from "madeonsol";
export const client = new MadeOnSol({
apiKey: process.env.MADEONSOL_API_KEY!,
});
The SDK auto-detects the msk_ prefix and authenticates directly with MadeOnSol's API. No RapidAPI account needed.
Step 3: Fetch the KOL leaderboard
Before copy-trading, you want to know which KOLs are actually profitable. The leaderboard endpoint ranks wallets by realized PnL:
// src/find-kols.ts
import { client } from "./client";
async function findTopKols() {
// Get top KOLs by PnL over the last 7 days
const { leaderboard } = await client.kol.leaderboard({ period: "7d" });
// Filter for consistent performers
const profitable = leaderboard.filter(
(kol) => kol.win_rate && kol.win_rate > 0.5 && kol.trade_count > 10
);
console.log(`Found ${profitable.length} profitable KOLs:`);
for (const kol of profitable.slice(0, 10)) {
console.log(
` ${kol.kol_name || kol.wallet} — PnL: ${kol.total_pnl_usd.toFixed(0)} USD, Win rate: ${(kol.win_rate! * 100).toFixed(0)}%`
);
}
return profitable.map((k) => k.wallet);
}
Step 4: Monitor the KOL feed for buy signals
The KOL feed returns trades within seconds of on-chain confirmation. Poll it to catch new buys:
// src/monitor.ts
import { client } from "./client";
const SEEN_SIGNATURES = new Set<string>();
async function checkForBuys(watchlist: string[]) {
const { trades } = await client.kol.feed({
limit: 50,
action: "buy",
});
const newBuys = trades.filter(
(t) =>
!SEEN_SIGNATURES.has(t.signature) &&
watchlist.includes(t.wallet)
);
for (const trade of newBuys) {
SEEN_SIGNATURES.add(trade.signature);
console.log(
`[BUY] ${trade.kol_name} bought ${trade.token_symbol} for ${trade.sol_amount} SOL`
);
}
return newBuys;
}
Step 5: Check coordination signals
The most powerful signal is when multiple KOLs buy the same token. The coordination endpoint detects this automatically:
// src/coordination.ts
import { client } from "./client";
async function getCoordinationSignals() {
const { tokens } = await client.kol.coordination({
period: "1h", // Look at the last hour
min_kols: 3, // At least 3 KOLs buying
});
// Only accumulating tokens (net positive flow)
const accumulating = tokens.filter((t) => t.signal === "accumulating");
for (const token of accumulating) {
console.log(
`[COORDINATION] ${token.token_symbol}: ${token.kol_count} KOLs buying, ` +
`${token.total_sol_volume.toFixed(1)} SOL volume`
);
}
return accumulating;
}
When a token appears in both the KOL feed and coordination signals, that's a strong entry signal.
Step 6: Execute a swap via Jupiter
When your bot detects a buy signal, execute the trade using Jupiter's swap API:
// src/swap.ts
import { Connection, Keypair, VersionedTransaction } from "@solana/web3.js";
const connection = new Connection(process.env.SOLANA_RPC_URL!);
const wallet = Keypair.fromSecretKey(/* decode your private key */);
async function executeBuy(tokenMint: string, solAmount: number) {
// Get Jupiter quote
const quoteUrl = `https://quote-api.jup.ag/v6/quote?inputMint=So11111111111111111111111111111111111111112&outputMint=${tokenMint}&amount=${Math.floor(solAmount * 1e9)}&slippageBps=500`;
const quote = await fetch(quoteUrl).then((r) => r.json());
// Get swap transaction
const swapRes = await fetch("https://quote-api.jup.ag/v6/swap", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
quoteResponse: quote,
userPublicKey: wallet.publicKey.toBase58(),
}),
});
const { swapTransaction } = await swapRes.json();
// Sign and send
const tx = VersionedTransaction.deserialize(
Buffer.from(swapTransaction, "base64")
);
tx.sign([wallet]);
const sig = await connection.sendTransaction(tx);
console.log(`[SWAP] Bought ${tokenMint} — tx: ${sig}`);
}
Step 7: Use webhooks for instant alerts (Pro/Ultra)
Instead of polling, you can set up webhooks to receive push notifications the moment a KOL trades:
// src/webhook.ts
import { client } from "./client";
async function setupWebhook() {
const { webhook } = await client.webhooks.create({
url: "https://your-server.com/kol-webhook",
events: ["kol:trade"],
filters: { min_sol: 1, action: "buy" },
});
console.log(`Webhook created: ${webhook.id}`);
console.log(`Secret (save this): ${webhook.secret}`);
}
Your webhook endpoint receives payloads like:
{
"event": "kol:trade",
"data": {
"kol_name": "ansem",
"wallet": "7xKX...",
"action": "buy",
"token_symbol": "BONK",
"sol_amount": 5.2,
"token_mint": "DezX..."
}
}
Step 8: Put it all together
// src/index.ts
import { client } from "./client";
const MAX_SOL = parseFloat(process.env.MAX_SOL_PER_TRADE || "0.1");
const POLL_INTERVAL = 15_000; // 15 seconds
async function main() {
// Find top performers to copy
const { leaderboard } = await client.kol.leaderboard({ period: "7d" });
const watchlist = leaderboard
.filter((k) => k.win_rate && k.win_rate > 0.55)
.map((k) => k.wallet);
console.log(`Watching ${watchlist.length} KOLs...`);
const seen = new Set<string>();
setInterval(async () => {
try {
const { trades } = await client.kol.feed({ limit: 20, action: "buy" });
for (const trade of trades) {
if (seen.has(trade.signature)) continue;
seen.add(trade.signature);
if (!watchlist.includes(trade.wallet)) continue;
// Check coordination — is anyone else buying this?
const { tokens } = await client.kol.coordination({
period: "1h",
min_kols: 2,
});
const isCoordinated = tokens.some(
(t) => t.mint === trade.mint && t.signal === "accumulating"
);
if (isCoordinated) {
console.log(
`[SIGNAL] ${trade.kol_name} + ${tokens.find((t) => t.mint === trade.mint)?.kol_count} others buying ${trade.token_symbol}`
);
// executeBuy(trade.mint, MAX_SOL);
}
}
} catch (err) {
console.error("Poll error:", err);
}
}, POLL_INTERVAL);
}
main();
API pricing and rate limits
| Tier | Price | Daily limit | Best for |
|---|
| Free | $0/mo | 100 requests | Testing and prototyping |
| Pro | $49/mo | 10,000 requests | Live trading bots |
| Ultra | $199/mo | 100,000 requests | Multi-strategy, DEX streaming |
Get a free API key at madeonsol.com/developer to start building.
What's next
- Add stop-loss logic by tracking token price after entry
- Use the deployer alerts endpoint to catch tokens from elite deployers before KOLs buy
- Set up webhooks instead of polling for lower latency
- Add a Telegram bot integration to notify you before executing trades
- Explore the DEX trade stream (Ultra tier) for real-time all-market data
FAQ
How many KOL wallets does MadeOnSol track?
MadeOnSol tracks 946 curated Solana KOL wallets including well-known traders, fund managers, and alpha callers. The list is actively maintained to add new performers and remove inactive wallets.
What's the latency on the KOL feed?
Trades appear in the API within seconds of on-chain confirmation. For sub-second latency, use webhooks (Pro) or WebSocket streaming (Pro/Ultra) instead of polling.
Can I use the free tier for a live trading bot?
The free tier (100 requests/day) is good for testing but not for live trading. A polling bot making one request every 15 seconds would need ~5,760 requests/day. The Pro tier (10,000/day) covers this comfortably.
Is this different from just tracking wallets on Birdeye or Solscan?
Yes. MadeOnSol provides structured API data with KOL names, PnL statistics, win rates, coordination signals (multiple KOLs buying the same token), and deployer enrichment — all in a single API call. Wallet explorers show raw transactions without this context.