I kept seeing the same thing on Solana. Some wallets would buy a token early, ride it up 10x, and sell before the dump. Over and over. Not once or twice — consistently.

So I thought: what if I just copied them? Not manually refreshing Solscan like some degenerate. An actual bot that watches their wallet, detects a swap, and mirrors it on mine within seconds.

I built it. It works. And I learned a lot about why copy trading is better as a signal source than a fully automated strategy. Let me walk you through the whole thing.


Finding Wallets Worth Copying

Before writing any code, you need wallets to watch. Not every profitable wallet is worth copying. Some got lucky on one meme coin. That’s noise, not signal.

Here’s what I look for:

  • Consistent profits over 30+ trades. One big win doesn’t count.
  • Reasonable position sizes. If they’re throwing 500 SOL at micro-caps, they’re probably an insider. You can’t compete with that.
  • Diverse token picks. If all their wins are in one ecosystem, that’s too narrow.

I use tools like Birdeye, Cielo, and GMGN to find these wallets. You can also write your own on-chain scanner, but honestly, these tools save hours. Filter by win rate, total PnL, and number of trades. Then manually review the top 20 or so.

Once you’ve got a shortlist of 3-5 wallets, you’re ready to build.


Setting Up the Wallet Monitor

The core idea is simple: watch a wallet for new transactions in real time. Solana gives us onAccountChange and onLogs for this. I prefer onLogs because it catches program interactions, not just balance changes.

Here’s the basic setup:

import { Connection, PublicKey } from "@solana/web3.js";

const connection = new Connection("https://api.mainnet-beta.solana.com", {
  wsEndpoint: "wss://api.mainnet-beta.solana.com",
});

const WHALE_WALLET = new PublicKey("WhaleWalletAddressHere...");

connection.onLogs(WHALE_WALLET, async (logs) => {
  if (logs.err) return;

  console.log("New transaction detected:", logs.signature);
  await processTransaction(logs.signature);
}, "confirmed");

console.log("Watching whale wallet...");

That’s it for the listener. Every time the whale does anything on-chain, we get the transaction signature. The real work is figuring out what they did.


Parsing the Transaction

Not every transaction is a swap. The whale might be staking, transferring SOL, or interacting with some random protocol. We only care about swaps — specifically Jupiter and Raydium, since that’s where most Solana trading happens.

Here’s how I parse a transaction to detect a swap:

const JUPITER_PROGRAM = new PublicKey(
  "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"
);
const RAYDIUM_PROGRAM = new PublicKey(
  "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"
);

async function processTransaction(signature: string) {
  const tx = await connection.getParsedTransaction(signature, {
    maxSupportedTransactionVersion: 0,
  });

  if (!tx || !tx.meta) return;

  const programIds = tx.transaction.message.instructions.map((ix) => {
    if ("programId" in ix) return ix.programId.toString();
    return null;
  });

  const isJupiterSwap = programIds.includes(JUPITER_PROGRAM.toString());
  const isRaydiumSwap = programIds.includes(RAYDIUM_PROGRAM.toString());

  if (!isJupiterSwap && !isRaydiumSwap) {
    console.log("Not a swap, skipping.");
    return;
  }

  console.log("Swap detected! Parsing details...");
  const swapDetails = extractSwapDetails(tx);
  if (swapDetails) {
    await evaluateAndExecute(swapDetails);
  }
}

Extracting What They Bought

This part took me the longest to get right. Solana transactions are dense. You need to look at the token balance changes before and after the transaction to figure out what went in and what came out.

function extractSwapDetails(tx: any) {
  const preBalances = tx.meta.preTokenBalances || [];
  const postBalances = tx.meta.postTokenBalances || [];

  let tokenBought = null;
  let tokenSold = null;
  let amountBought = 0;
  let amountSold = 0;

  for (const post of postBalances) {
    const pre = preBalances.find(
      (p: any) =>
        p.accountIndex === post.accountIndex && p.mint === post.mint
    );

    const preAmount = pre
      ? parseFloat(pre.uiTokenAmount.uiAmountString || "0")
      : 0;
    const postAmount = parseFloat(
      post.uiTokenAmount.uiAmountString || "0"
    );
    const diff = postAmount - preAmount;

    if (diff > 0 && post.owner === WHALE_WALLET.toString()) {
      tokenBought = post.mint;
      amountBought = diff;
    } else if (diff < 0 && post.owner === WHALE_WALLET.toString()) {
      tokenSold = post.mint;
      amountSold = Math.abs(diff);
    }
  }

  if (!tokenBought) return null;

  return { tokenBought, tokenSold, amountBought, amountSold };
}

The key insight: compare pre and post token balances for the whale’s wallet specifically. A positive diff means they received that token (bought). A negative diff means they sent it (sold).


Position Sizing — Don’t Be Stupid

This is where most copy trading bots go wrong. The whale has 10,000 SOL. You have 5. If they buy $50,000 worth of some token, you can’t just mirror that 1:1.

I use a simple ratio:

const MY_CAPITAL_SOL = 5;
const WHALE_CAPITAL_SOL = 10000;
const MAX_TRADE_PERCENT = 0.1; // never risk more than 10% per trade

function calculatePositionSize(whaleAmountSol: number): number {
  const ratio = MY_CAPITAL_SOL / WHALE_CAPITAL_SOL;
  const scaledAmount = whaleAmountSol * ratio;
  const maxAmount = MY_CAPITAL_SOL * MAX_TRADE_PERCENT;

  return Math.min(scaledAmount, maxAmount);
}

If the whale puts in 100 SOL, I’d put in 0.05 SOL based on the ratio. But I also cap it at 10% of my balance. Never let a single trade blow up your account.


Executing the Trade via Jupiter

Once we know what to buy and how much, we hit the Jupiter API. It’s the best aggregator on Solana — good routing, decent prices, and a clean API.

import { Keypair, VersionedTransaction } from "@solana/web3.js";

const MY_WALLET = Keypair.fromSecretKey(
  Uint8Array.from(JSON.parse(process.env.PRIVATE_KEY!))
);

async function executeTrade(
  inputMint: string,
  outputMint: string,
  amountInLamports: number
) {
  const quoteUrl = `https://quote-api.jup.ag/v6/quote?inputMint=${inputMint}&outputMint=${outputMint}&amount=${amountInLamports}&slippageBps=100`;

  const quoteResponse = await fetch(quoteUrl);
  const quote = await quoteResponse.json();

  const swapResponse = await fetch("https://quote-api.jup.ag/v6/swap", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      quoteResponse: quote,
      userPublicKey: MY_WALLET.publicKey.toString(),
      wrapAndUnwrapSol: true,
    }),
  });

  const { swapTransaction } = await swapResponse.json();
  const txBuf = Buffer.from(swapTransaction, "base64");
  const transaction = VersionedTransaction.deserialize(txBuf);

  transaction.sign([MY_WALLET]);

  const txid = await connection.sendRawTransaction(transaction.serialize(), {
    skipPreflight: false,
    maxRetries: 3,
  });

  console.log("Trade executed:", txid);
  return txid;
}

I set slippage to 100 bps (1%). For low-liquidity tokens you might need more, but I’d rather miss a trade than get wrecked by slippage.


The Full Bot Loop

Here’s how everything ties together:

async function evaluateAndExecute(swap: {
  tokenBought: string;
  tokenSold: string | null;
  amountBought: number;
  amountSold: number;
}) {
  console.log(`Whale bought: ${swap.tokenBought}`);
  console.log(`Amount: ${swap.amountBought}`);

  // Skip if they're selling, not buying
  if (!swap.tokenSold) return;

  // Calculate how much we should buy
  const positionSol = calculatePositionSize(swap.amountSold);
  if (positionSol < 0.001) {
    console.log("Position too small, skipping.");
    return;
  }

  const SOL_MINT = "So11111111111111111111111111111111111111112";
  const amountLamports = Math.floor(positionSol * 1e9);

  try {
    const txid = await executeTrade(
      SOL_MINT,
      swap.tokenBought,
      amountLamports
    );
    console.log(`Copied trade! TX: ${txid}`);
  } catch (err) {
    console.error("Trade failed:", err);
  }
}

Detect. Parse. Evaluate. Execute. That’s the whole loop.


The Latency Problem

Here’s the thing nobody tells you about copy trading on Solana: latency kills your edge.

By the time you detect the whale’s transaction, parse it, hit Jupiter for a quote, and submit your own transaction, the price has already moved. On liquid tokens, maybe 0.5-1% slippage. On micro-caps with thin order books? You might be buying 5-10% higher than the whale did.

I’ve tried a few things to reduce this:

  • Use a paid RPC with websocket support. The free mainnet endpoint is slow. I use Helius or Quicknode. It makes a real difference.
  • Pre-build transaction templates. Have everything ready so you’re just swapping in the token address and amount.
  • Skip the quote step for known pairs. If you already know the route, go directly to the swap endpoint.

Even with all that, you’re still behind. On a fast-moving token, the whale’s buy itself can push the price up before you get in.


Which Wallets Are Actually Worth Copying

After running this for a few weeks, I learned something important. Not all profitable wallets make good copy targets.

Bad copy targets:

  • Wallets that trade super low-liquidity tokens. Your buy will move the price too much.
  • Wallets that make money on 1-2 huge bets. That’s luck, not skill.
  • MEV bots. They profit from transaction ordering, not direction. Copying their trades makes zero sense.

Good copy targets:

  • Wallets with 60%+ win rate over 100+ trades.
  • Traders who buy tokens with at least some liquidity (>$50k daily volume).
  • Wallets that hold positions for hours or days, not seconds. This gives you time to get a similar entry.

The longer the whale holds their position, the less latency matters to you. That’s the sweet spot.


What I Actually Do Now

I don’t run this fully automated anymore. I tried it for three weeks and the results were mixed. Some great copies, some terrible entries where I bought the top because the whale’s buy itself pumped the price.

What I do instead: I use the bot as a signal system. It alerts me when a tracked wallet makes a move. I get a notification with the token, the amount, and a link to the chart. Then I decide whether to follow.

This hybrid approach works way better. The bot handles the boring part — watching wallets 24/7. I handle the part that requires judgment — deciding if the setup still looks good at the current price.

If you’re building something like this, start with the signal approach. Get the monitoring and parsing working first. Run it for a week just logging trades without executing anything. See how the whale’s picks perform. Then decide if you trust the edge enough to automate execution.

The code works. The concept works. But the best copy traders I know still make the final call themselves.