A leaderboard ranks wallets by a single number. That's fine for discovery but useless for conviction. Two wallets with identical 30-day win rates can have completely different equity curves — one a smooth climb, the other an 80% drawdown saved by a single 10x at the end. If you're about to copy-trade either, that difference is the entire story.
GET /api/v1/kol/{wallet}/pnl is the endpoint that answers the shape-of-returns question. One call returns a daily equity curve, the summary statistics (profit factor, max drawdown, avg ROI, average and median hold time), every closed position in the window with per-trade ROI, and — on ULTRA — the current open positions.
This post walks through each section and the decisions it should feed.
The call
GET /api/v1/kol/{wallet}/pnl?period=7d|30d|90d|180d
Authorization: Bearer msk_...
Default period is 30d. The full range (7d → 180d) is available on every tier.
Up to 50 closed positions are returned per call, ordered by most recent. If the wallet has more than 50 closed positions in the window, you're getting the latest 50 — the summary stats still cover the full set.
Response structure
{
"kol": {
"name": "Mezoteric",
"wallet": "7xK...",
"twitter_url": "https://x.com/mezoteric",
"strategy_tag": "sniper"
},
"summary": {
"realized_pnl_sol": 312.7,
"total_volume_sol": 1846.2,
"tokens_traded": 124,
"closed_positions": 96,
"open_positions": 28,
"win_count": 61,
"loss_count": 35,
"win_rate": 0.635,
"profit_factor": 1.92,
"best_trade_pnl_sol": 48.3,
"worst_trade_pnl_sol": -22.1,
"avg_roi_pct": 47.8,
"avg_hold_minutes": 58.4,
"median_hold_minutes": 22,
"max_drawdown_sol": 41.6
},
"pnl_curve": [
{ "date": "2026-03-25", "day_pnl": 12.4, "cumulative_pnl": 12.4, "trades": 4 },
{ "date": "2026-03-26", "day_pnl": -3.1, "cumulative_pnl": 9.3, "trades": 2 },
{ "date": "2026-03-27", "day_pnl": 28.6, "cumulative_pnl": 37.9, "trades": 6 }
],
"closed_positions": [
{
"token_mint": "26s2aY...pump",
"token_symbol": "EGG",
"token_name": "Egg Season",
"buy_count": 1,
"sell_count": 3,
"bought_sol": 2.0,
"sold_sol": 9.8,
"pnl_sol": 7.8,
"roi_pct": 390,
"hold_minutes": 42,
"result": "win",
"first_trade": "2026-04-22T08:11:02Z",
"last_trade": "2026-04-22T08:53:46Z"
}
],
"period": "30d"
}
On ULTRA, an additional open_positions[] array is included — positions bought but not yet sold — each with buy_count, bought_sol, and first_buy_at.
The summary block in practice
profit_factor — the one number that matters most
Gross wins divided by gross losses. Above 1.0 means net profitable. Above 1.5 is healthy. Above 2.0 means this wallet actually knows what they're doing. Below 1.0 means they lose money net of fees — and plenty of KOLs with decent-looking win rates fall here because their losses are 3× their wins.
If you only have time to check one field, check profit factor.
win_rate — useful with sample-size caveats
Valid signal when closed_positions >= 30. Below that, a 70% win rate across 8 trades is noise. Always gate your filtering on closed_positions:
const qualified = kols.filter((k) =>
k.summary.closed_positions >= 30 &&
(k.summary.profit_factor ?? 0) >= 1.5
);
max_drawdown_sol — the risk number leaderboards hide
Computed from the cumulative_pnl timeseries: the largest peak-to-trough drop inside the window. A wallet with +300 SOL realized PnL and 15 SOL max drawdown has a very different risk profile from the same PnL with 180 SOL drawdown. The second one is one bad week away from churning your copy-trade capital.
Rough heuristic: a healthy wallet's max drawdown is under 30% of their total realized PnL. Above 50% and you're copy-trading a gambler.
avg_hold_minutes vs median_hold_minutes
When median is much smaller than mean (e.g. median 12 min, avg 58 min), the wallet is mostly scalping but occasionally holding winners longer. That's a healthy pattern. When median equals mean, they have one fixed strategy. When median > mean, a few panicked fast exits are dragging the average down — often a sign of fear-based behavior on losers.
avg_roi_pct — can mislead with outliers
Simple arithmetic mean of per-token ROI. A single 50x outlier will drag this up hard. Always read alongside best_trade_pnl_sol — if best_trade_pnl_sol / realized_pnl_sol > 0.3, their performance is overly dependent on a single trade and will likely revert.
The equity curve
pnl_curve[] is the daily rollup: day_pnl, cumulative_pnl, and the trades count that day. Three things you want from it:
1. Shape. A smooth diagonal-up curve means the wallet's edge is real. A flat line with a single vertical spike means they got lucky once and the rest is noise. A curve with 3 equal-sized up-moves is ideal — reproducible edge.
2. Activity consistency. The trades column tells you whether the wallet is active every day or clusters their activity. A wallet trading 0 days then 15 trades in one day is harder to copy (you'll miss the one-day window).
3. Recent slope. The last 5–7 days of cumulative_pnl tell you current momentum. A flat-to-down tail on a wallet with a great 30-day number is a sign they've cooled — the is_cold / is_heating_up flags on /kol/{wallet} are the boolean version of this observation.
Per-token closed positions
The closed_positions[] array gives you the last 50 completed round-trip trades. Each row has:
pnl_sol + roi_pct — realized outcome on that specific token.
hold_minutes — how long they held.
buy_count + sell_count — did they DCA in? Scale out? A position with buy_count: 4, sell_count: 5 is an active scalp-layer. 1, 1 is a straight in-out.
result — "win" or "loss", already classified for you.
Useful aggregation: group by hold_minutes bucket to see whether their wins and losses differ in holding pattern. Many KOLs have a signature: wins held long, losses cut fast. When that breaks — losers held longer than winners — the wallet is revenge-trading.
ULTRA-only: open positions
"open_positions": [
{
"token_mint": "...",
"token_symbol": "NEON",
"token_name": "Neon Street",
"buy_count": 2,
"bought_sol": 8.5,
"first_buy_at": "2026-04-21T03:14:07Z"
}
]
The current live exposure — tokens bought but not yet sold. This is load-bearing for copy-trade decisions: a wallet's closed PnL might look great, but if they're currently sitting in four tokens with 40 SOL deployed and all are down, the next print on the leaderboard will look very different.
For bots that care about "should I still be copying this wallet right now?" the open_positions block is the most actionable part of the response. It's the one thing you can't compute from the other tiers' data.
A real decision workflow
import { MadeOnSol } from "madeonsol";
const client = new MadeOnSol({ apiKey: process.env.MADEONSOL_API_KEY! });
async function shouldCopyTrade(wallet: string) {
const { summary, pnl_curve } = await client.kol.pnl({ wallet, period: "30d" });
// Gates
if (summary.closed_positions < 30) return { copy: false, reason: "sample too small" };
if ((summary.profit_factor ?? 0) < 1.5) return { copy: false, reason: "unprofitable after costs" };
if (summary.max_drawdown_sol > summary.realized_pnl_sol * 0.5) return { copy: false, reason: "excessive drawdown" };
// Recent-slope check — last 7 days vs full window
const last7 = pnl_curve.slice(-7);
const last7Pnl = last7.at(-1)!.cumulative_pnl - last7.at(0)!.cumulative_pnl;
if (last7Pnl < 0) return { copy: false, reason: "cooling off" };
return { copy: true, reason: "qualified" };
}
This is the pattern: summary for the structural checks, curve for the momentum check, open_positions (on ULTRA) for current exposure sanity.
Tier considerations