Copy-trading a Solana KOL with a 30-second entry lag and a 15-minute exit lag is not copy-trading. It's buying from them and selling to them. Most "copy bots" advertise speed they don't actually deliver, and most users never measure the gap between their fills and the wallet they're following.
The /api/v1/kol/{wallet}/timing endpoint gives you the KOL side of that equation: how long does this specific wallet hold, when in the day is it active, what percentage of positions does it close inside the first hour. Combined with your own fill timestamps, it tells you exactly where you're bleeding.
This post walks through what the endpoint returns, how to compute your own lag against it, and how to use the numbers to decide whether a KOL is worth copying at all.
The endpoint
GET /api/v1/kol/{wallet}/timing?period=30d
Returns a behavioral timing profile for a single KOL wallet over the past 7 or 30 days. The endpoint is available on every plan, including Free, and the response shape is the same on all of them:
{
"kol": { "name": "Mezoteric", "wallet": "7xKX..." },
"timing": {
"tokens_traded": 142,
"positions_closed": 128,
"avg_hold_minutes": 47.3,
"median_hold_minutes": 18.5,
"pct_closed_1h": 0.61,
"pct_closed_6h": 0.84,
"pct_closed_24h": 0.94,
"avg_buy_size_sol": 3.2,
"avg_sell_size_sol": 3.8,
"most_active_hours": [14, 15, 16, 21, 22],
"hour_distribution": { "0": 2, "1": 1, "14": 19, "15": 22, ... }
},
"period": "30d"
}
Each field is a different dimension of how this wallet trades:
avg_hold_minutes / median_hold_minutes — how long between entry and exit on average. Huge divergence (avg 47m, median 18m) means a few long holds are pulling up the mean. For copy-trade sizing, the median is usually more representative.
pct_closed_1h / 6h / 24h — cumulative distribution of hold times. A wallet with pct_closed_1h=0.61 closes 61% of positions inside the first hour. If you're copying with a 15-minute entry lag, you're already behind on two-thirds of their exits.
avg_buy_size_sol / avg_sell_size_sol — position sizing context. If avg_sell > avg_buy, the wallet is typically scaling in then exiting larger, which matters for how you model copy fills.
most_active_hours — UTC hours where the wallet posts the most trades. Use this to schedule polling frequency rather than hitting the API constantly.
hour_distribution — full 24-hour count histogram for heatmap rendering.
What "good" looks like
Different strategies produce very different timing profiles. A few archetypes to calibrate against:
Snipers — median_hold_minutes under 10, pct_closed_1h above 0.75, trades concentrated in a 4–6 hour daily window. Very hard to copy profitably unless your infrastructure is gRPC-tier and your TX sending is sub-500ms.
Swing traders — median_hold_minutes 60–240, pct_closed_6h 0.6–0.8, trades spread across more hours. The copy-trade sweet spot: their entries decay slowly enough that a 30-second lag still captures most of the move.
Bag holders — median_hold_minutes over 1440 (24h+), pct_closed_24h below 0.5. These are the easiest to copy from a latency standpoint but the hardest to profit from — their edge is usually conviction on small, slow-moving bets rather than momentum.
Bots you shouldn't copy — median_hold_minutes under 2 and pct_closed_1h above 0.95. Not a human. Infrastructure alpha, not wallet alpha. Copying these is a losing bet unless you have matching infra.
Before copy-trading any wallet, pull its timing profile and ask: is the exit window slow enough that my entry lag fits inside it?
Calculating your actual copy-trade lag
The timing endpoint tells you how the KOL trades. To measure your lag, you join it against your own fills. The KOL's trade history comes from GET /api/v1/wallet/{address}/trades (client.wallet.trades in the SDK), which needs a Pro plan or higher; a Free key gets 403 tier_required on it.
import { MadeOnSol } from "madeonsol";
const client = new MadeOnSol({ apiKey: process.env.MADEONSOL_API_KEY! });
interface MyFill {
wallet_followed: string;
token_mint: string;
side: "buy" | "sell";
filled_at: string; // ISO timestamp
}
async function measureLag(kolWallet: string, myFills: MyFill[]) {
// 1. Get the KOL's trades for the same mints
const trades = await client.wallet.trades(kolWallet, { limit: 500 });
const kolTradesByMint = new Map<string, Array<{ action: string; traded_at: string }>>();
for (const t of trades.trades) {
const arr = kolTradesByMint.get(t.token_mint) ?? [];
arr.push({ action: t.action, traded_at: t.traded_at });
kolTradesByMint.set(t.token_mint, arr);
}
// 2. For each of my fills, find the matching KOL trade and compute delta
const lags: Array<{ side: string; lag_sec: number; mint: string }> = [];
for (const mine of myFills.filter((f) => f.wallet_followed === kolWallet)) {
const kolTrades = kolTradesByMint.get(mine.token_mint) ?? [];
const match = kolTrades
.filter((k) => k.action === mine.side)
.map((k) => ({ t: new Date(k.traded_at).getTime(), ...k }))
.sort((a, b) => a.t - b.t)[0];
if (!match) continue;
const lag = (new Date(mine.filled_at).getTime() - match.t) / 1000;
lags.push({ side: mine.side, lag_sec: lag, mint: mine.token_mint });
}
// 3. Summary
const buys = lags.filter((l) => l.side === "buy").map((l) => l.lag_sec);
const sells = lags.filter((l) => l.side === "sell").map((l) => l.lag_sec);
const median = (xs: number[]) =>
xs.sort((a, b) => a - b)[Math.floor(xs.length / 2)] ?? 0;
console.log(`Median buy lag: ${median(buys).toFixed(1)}s over ${buys.length} fills`);
console.log(`Median sell lag: ${median(sells).toFixed(1)}s over ${sells.length} fills`);
// 4. Context from the timing endpoint
const { timing } = await client.kol.timing(kolWallet, { period: "30d" });
const exitWindow = (timing.median_hold_minutes ?? 0) * 60;
console.log(
`KOL median hold: ${timing.median_hold_minutes}m. Your sell lag eats ${(
(median(sells) / exitWindow) * 100
).toFixed(1)}% of their typical hold window.`
);
}
The last line is the important one. If a KOL's median hold is 18 minutes (1080 seconds) and your median sell lag is 240 seconds, you're absorbing 22% of their hold window as pure latency. On a fast-moving memecoin that can flip a +40% exit into a +10% one.
Using timing data to gate which KOLs you copy
Timing profile is the single cheapest filter for building a copy-trade whitelist. A simple rule that works in practice:
- Require
median_hold_minutes >= 15. Anything faster and infra lag eats your edge.
- Require
pct_closed_24h >= 0.70. You want wallets that actually exit, not bagholders.
- Require
tokens_traded >= 30 over 30 days. Below that the sample size is too small to trust the timing stats.
- Match your polling cadence to
most_active_hours. No point rate-limiting yourself during a wallet's off hours.
Running this filter across the KOL leaderboard typically narrows 200+ tracked wallets to 40–60 that are actually copyable at realistic infrastructure. Everyone else is either too fast (sniper bots) or too slow (bag holders) for a practical copy-trade loop.
Pairing with copytrade subscriptions
If you use POST /api/v1/copytrade/subscriptions to auto-follow KOLs, the timing endpoint is what you check before you create the rule. Two things to know about copy-trade rules:
Update, October 4, 2026: copy-trade rules now follow any valid source wallet, on Solana and on Robinhood Chain. Until October 4, 2026 a rule only fired for wallets in our tracked KOL set, and this section said so. KOL membership is now optional enrichment, not a requirement: a rule on a KOL wallet and a rule on any other wallet both deliver signals. Plan limits, the freshness check and signal, webhook and WebSocket delivery are unchanged.
- They follow any valid source wallet. It does not have to be a tracked KOL wallet; when it is one, the signal carries the KOL context as extra enrichment. Copy-trade source wallets do not count against your Wallet Tracker watchlist.
- They need a Pro plan or higher, and the plan caps how many source wallets one rule can hold (Pro 5, Ultra 50, Business 250; see pricing for the rule counts).
The rule itself doesn't know or care whether the wallet's trades are copyable at your latency, so gate on timing first. The /kol/{wallet}/timing profile exists for tracked KOL wallets (any other wallet returns 404), so for a non-KOL source wallet check its hold times another way, for example with the wallet analyzer, before you create the rule:
// Copy-trade rules need a Pro plan or higher.
async function safeSubscribe(kolWallet: string, maxLagSec: number, webhookUrl: string) {
const { timing } = await client.kol.timing(kolWallet, { period: "30d" });
if ((timing.median_hold_minutes ?? 0) * 60 < maxLagSec * 3) {
throw new Error(
`Wallet ${kolWallet} has median hold ${timing.median_hold_minutes}m, too fast for ${maxLagSec}s lag`
);
}
// Any valid source wallet works; kolWallet is a KOL here only because /kol/{wallet}/timing needs one.
const { subscription, webhook_secret } = await client.copytrade.create({
source_wallets: [kolWallet],
sizing_mode: "fixed",
sizing_amount: 0.5, // SOL per signal
only_action: "both",
delivery_mode: "webhook", // the default; "websocket" needs no webhook_url
webhook_url: webhookUrl, // required for "webhook" and "both"
});
// webhook_secret is returned once, at creation. Store it to verify signed deliveries.
return { subscription, webhook_secret };
}