Momentum is the dominant signal in memecoin trading. Tokens either keep going up after they break out, or they fade — there isn't much middle ground on a fast-moving pump.fun chart. So the question every scanner is trying to answer reduces to: "how fast is the market cap moving, and over what window?"
MadeOnSol exposes that as a first-class field on every relevant endpoint. mc_change_pct is a five-window object — 5m, 15m, 1h, 2h, 4h — populated from CONFIRMED-commitment swap snapshots stored in mc-tracker. The same fields appear on /v1/token/{mint}, /v1/token/batch, /v1/tokens (scanner), /v1/kol/coordination, and /v1/kol/tokens/{mint} — anywhere a token-level response shape makes sense.
This post explains where the numbers come from, why CONFIRMED commitment matters here, what each window is actually good for, and a worked-example scanner that uses three of them in combination.
What the response looks like
{
"token": {
"mint": "4k3Dyj...",
"market_cap_usd": 412_000,
"vwap_price_usd": 0.000412,
"mc_change_pct": {
"5m": 8.2,
"15m": 22.1,
"1h": 41.7,
"2h": 39.2,
"4h": 12.8
},
"volume_usd": {
"5m": 4200,
"15m": 18_200,
"1h": 81_000,
"2h": 142_000,
"4h": 290_000
},
"mev_volume_pct": {
"5m": 12.1,
"15m": 18.4,
"1h": 22.7,
"2h": 27.1,
"4h": 31.9
},
"history_age_seconds": 14_400
}
}
Window keys are only present when the token has been tracked long enough — history_age_seconds reports how much coverage you have (capped at 4 hours 5 minutes, the buffer's max retention). A token tracked for 23 minutes will have populated 5m and 15m but not 1h, 2h, or 4h.
volume_usd is organic-only (MEV-stripped) USD volume in each window. mev_volume_pct is the MEV share of total volume in the window — useful for sanity-checking whether the momentum is bot-driven or trader-driven (see the MEV stripping post).
Where the snapshots come from
mc-tracker maintains a per-token history ring buffer in memory. Every 60 seconds (rate-limited so memory stays bounded), it appends a tuple of {timestamp, market_cap_usd, cumulative_organic_volume_usd, cumulative_mev_volume_usd} for each actively-tracked token. The ring trims past 4h 5m. On every DB write, the closest snapshot to each window's lookback point is materialized into the mc_usd_5m_ago, mc_usd_15m_ago, etc. columns in token_prices.
So the math behind mc_change_pct.1h is simply:
(market_cap_usd_now - mc_usd_1h_ago) / mc_usd_1h_ago * 100
The interesting design choice: snapshots are appended only when swaps occur, and only on CONFIRMED-commitment swaps (single-confirmation Solana finality). PROCESSED-commitment swaps are intentionally excluded.
Why CONFIRMED matters
Solana's swap firehose can be subscribed at three commitment levels:
- PROCESSED — the leader has executed it but hasn't been confirmed. Possible reorg.
- CONFIRMED — confirmed by enough validators that single-confirmation finality holds.
- FINALIZED — finalized via supermajority root. Takes ~12.8s after CONFIRMED.
For latency-critical sniper feeds we publish PROCESSED swaps (the WS dex:trades channel runs on this). For anything you're going to compute statistics from — market cap, volume, velocity windows — PROCESSED is the wrong layer because a reorg'd swap means a mc_change_pct.5m value of +47% retroactively becomes +38%. Statistics on retconned data are bad statistics.
CONFIRMED swaps are ~250-500ms behind PROCESSED on Solana mainnet today (slot times are 400ms, single-confirmation is a slot or two later). That extra 250ms is worth it for stability. By the time a velocity window value appears in mc_change_pct.5m, it's not going to change.
The exception is 5m immediately after first tracking. If mc-tracker started seeing the token 4.5 minutes ago, the 5m snapshot might be at 4m30s rather than exactly 5m back. The change percentage is still honest — it's the percentage move over the available history — but the window label is an approximation for the first few minutes. After ~6 minutes of tracking, the 5m window settles to exact.
What each window is for
The five windows are not redundant. Each one captures a different question.
5m: "Is it moving right now?" Use as an entry trigger when stacked with a fundamental filter (KOL buying, deployer alert, scout fire). On its own, 5m noise is severe — pump.fun tokens routinely flicker ±10% on a 5m window. Pair with at least a 15m filter to confirm.
15m: "Has the move been sustained for at least one cycle?" This is the workhorse window for entry signals. If 15m and 5m agree (both positive, both above your threshold), the token is in a real breakout, not noise.
1h: "Is this a coordinated push or a sustained breakout?" When 1h is strong (above 50%) but 5m is fading (under 5%), the move is exhausting. When all three (5m, 15m, 1h) are strong, you've got a real run forming.
2h: "Have late entrants had time to chase yet?" Tokens that are still up 30%+ on 2h have absorbed at least one cycle of profit-taking and recovered. That's a meaningfully different signal than just being up 30% on 1h.
4h: "Is the move multi-cycle?" 4h is the longest window we expose. If mc_change_pct.4h is still positive while mc_change_pct.1h has rolled over, you're seeing a mature uptrend with a normal pullback — different from a one-cycle pump.
A worked example: stacked momentum filter
The single best filter we've seen for catching real breakouts is a three-window agreement:
- 5m and 15m both positive (current momentum is alive)
- 1h above your threshold (the move is sustained, not a one-candle spike)
- Organic volume in the last hour above the floor that matters to your size
In one API call:
import { MadeOnSol } from "madeonsol";
const client = new MadeOnSol({ apiKey: process.env.MADEONSOL_API_KEY! });
async function stackedMomentum() {
const { tokens } = await client.tokens.list({
min_mc: 100_000,
max_mc: 5_000_000,
min_liq: 5000,
active_h: 2,
mc_change_1h_min_pct: 25,
min_volume_1h_usd: 20_000,
max_mev_share_pct: 30,
sort: "last_trade_desc",
limit: 100,
});
// Server-side filtering covered 1h; do 5m + 15m client-side from the returned shape.
return tokens.filter((t) => {
const c = t.mc_change_5m_pct ?? 0;
const f = t.mc_change_1h_pct ?? 0;
return c > 2 && f > 25;
});
}
The /v1/tokens scanner endpoint surfaces mc_change_5m_pct and mc_change_1h_pct directly on the response row, so the second filter is local. For the full five-window object (including 15m, 2h, 4h), call /v1/token/{mint} per row to drill in, or batch up to 50 mints at once with /v1/token/batch.
Stacking with KOL data
The strongest pattern combines velocity windows with KOL flow. If a token is up 30% on 1h AND multiple high-winrate KOLs are net-buying right now, that's a meaningfully tighter signal than either alone:
const [velocityHits, hotKolTokens] = await Promise.all([
client.tokens.list({
min_mc: 100_000, max_mc: 2_000_000,
mc_change_1h_min_pct: 30,
min_volume_1h_usd: 25_000,
max_mev_share_pct: 25,
limit: 50,
}),
client.kol.tokens.hot({
period: "1h",
min_kols: 3,
min_avg_winrate: 60,
limit: 50,
}),
]);
const hotMints = new Set(hotKolTokens.hot_tokens.map((t) => t.token_mint));
const intersection = velocityHits.tokens.filter((t) => hotMints.has(t.mint));
The intersection is much smaller than either list alone — and historically much higher-conviction. Velocity tells you the move is real; KOL flow tells you smart money is participating.
Caveats and edge cases
- History gates window availability. A token tracked for 12 minutes will only have
5m populated. Don't crash your scanner when a window key is missing.
mc_change_pct can be null inside an existing window. This happens when mc_usd_*_ago was zero (e.g., the token went through a period of no priced trades). The window key is present but its value is null. Handle both cases.
- History resets on idle prune. If a token has no swap activity for 48 hours, mc-tracker prunes it from memory. When it comes back, history starts fresh — your window may temporarily disappear even though the token has been around for weeks.