The MC enrichment from the last release made every signal carry the market cap at the moment it fired. Useful — but read-only. You could see the MC, you couldn't filter on it.
Today MC becomes a first-class filter. Every signal surface — REST endpoints, WebSocket channels, persistent alert rules — accepts min_mc_usd and max_mc_usd so callers can scope every request and every alert rule to a specific market-cap band.
The use cases that drove this:
- "Show me only sub-$50K KOL trades — that's where the alpha lives"
- "Skip every coordination signal on tokens above $5M, those are over"
- "Auto-fire the copy-trade only when the source bought between $100K and $1M"
- "Stop sending me memecoin alerts above $10M, I only trade micro-caps"
All four are now one parameter away.
Five places it lives
1. REST API — four endpoints accept the band
GET /api/v1/kol/feed?max_mc_usd=50000 → only sub-$50K KOL trades
GET /api/v1/kol/coordination?min_mc_usd=100000 → skip micro-cap clusters
GET /api/v1/kol/first-touches?max_mc_usd=200000 → S-tier scout entries below $200K
GET /api/v1/copytrade/signals?max_mc_usd=500000 → only signals on sub-$500K tokens
/kol/feed filters at the database layer using the existing idx_kol_trades_mc_at_trade partial index — fast even on million-row scans. The other three filter post-aggregate on whichever MC concept matches the endpoint:
/kol/coordination filters on the cluster's MC at the chronologically-first KOL buy. That's the entry MC for the cluster — the cap at the moment the first KOL got in. You're filtering "where was this cluster at the start", not "where is it now".
/kol/first-touches filters on the at-touch MC stamped on the exact tx that fired the first KOL buy.
/copytrade/signals filters on the source trade's MC.
Important semantics: trades or signals with unknown MC are dropped when a band is set. The caller asked for a specific range — we don't pretend "unknown" matches. If you want the "old" behavior of including everything, just don't pass the band.
Combine with the existing filters for compound queries:
# S-tier scouts entering tokens under $50K, in the last hour
curl "https://madeonsol.com/api/v1/kol/first-touches?\
preset=scout&\
min_scout_tier=S&\
max_mc_usd=50000&\
since=2026-05-09T18:00:00Z" \
-H "Authorization: Bearer msk_..."
2. WebSocket — subscribe-time bands
The same band shows up on three WebSocket channels: kol:trades, kol:first_touch, and kol:coordination. The server-side filter drops events that miss the band before pushing them down the wire.
const ws = new WebSocket("wss://madeonsol.com/ws?token=...");
ws.onopen = () => {
ws.send(JSON.stringify({
type: "subscribe",
channels: ["kol:trades"],
filters: { min_mc_usd: 10000, max_mc_usd: 250000 },
}));
};
Bandwidth savings are real if you're subscribing to a busy channel and only care about a narrow band. The 24h API analytics on this site show power users hitting up to 17,000 WS messages per session — server-side filtering cuts that proportionally to the band's selectivity. A bot only interested in sub-$100K alpha might see a 5-10x reduction in events to process.
3. Persistent alert rules carry the band
Three user-rule tables grew min_mc_usd and max_mc_usd columns:
| Table | What rule | When the band fires |
|---|
coordination_alert_rules | Multi-KOL coordination clusters | At the triggering trade's MC |
copytrade_subscriptions | Per-wallet copy-trade rules | At the source trade's MC |
kol_first_touch_subscriptions | Per-KOL first-touch alerts | At the first-touch MC |
The signal-evaluator reads these on every trade and drops triggers that fall outside the rule's band before the alert ever fires. So instead of polling, post-filtering, and burning rate limit, you persist "alert me on KOL coordination, but only on tokens under $50K MC" as the rule itself:
import { MadeOnSol } from "madeonsol";
const client = new MadeOnSol({ apiKey: "msk_..." });
const { rule } = await client.coordinationAlerts.create({
name: "micro-cap coordination only",
min_kols: 4,
min_score: 70,
delivery_mode: "webhook",
webhook_url: "https://your-bot.example/coord",
min_mc_usd: 0,
max_mc_usd: 50000,
});
That rule will only fire when a coordination cluster forms on a token whose MC at the triggering trade was below $50K. Updates work the same way:
await client.coordinationAlerts.update(rule.id, {
max_mc_usd: 100000, // raise the ceiling
});
// or
await client.coordinationAlerts.update(rule.id, {
max_mc_usd: null, // remove the upper bound
});
4. SDKs — bumped across all 7 packages
| Package | Registry | New version |
|---|
madeonsol | npm (bare) | 2.5.0 |
madeonsol-x402 | npm | 1.6.0 |
mcp-server-madeonsol | npm | 1.6.0 |
@madeonsol/plugin-madeonsol | npm (ElizaOS) | 1.6.0 |
solana-agent-kit-plugin-madeonsol | npm | 1.6.0 |
madeonsol-x402 | PyPI | 1.6.0 |
madeonsol | crates.io (Rust) | 0.7.0 |
All min_mc_usd / max_mc_usd parameters are optional. The call signatures stay backwards-compatible — existing 1.5.x callers keep working unchanged. The new fields also live on coordinationAlerts.create/update and firstTouchSubscriptions.create/update so persistent rules carry the band end-to-end through the typed surface.
# Python
from madeonsol_x402 import MadeOnSolClient
client = MadeOnSolClient(api_key="msk_...")
result = client.kol_feed(limit=50, max_mc_usd=50_000)
# result["trades"] contains only sub-$50K trades
// Rust
use madeonsol::{Client, KolFeedParams};
let client = Client::new("msk_...");
let feed = client.kol().feed(KolFeedParams {
limit: Some(50),
max_mc_usd: Some(50_000.0),
..Default::default()
}).await?;
Why filtering separately matters
It's tempting to assume "well, the field is in the response, just filter client-side". A few reasons that's worse:
- Rate limit waste. PRO is 10K calls/day. If your client filters down 90% of returned trades, you've effectively paid 10x for the band you cared about. Server-side filtering returns only relevant trades — every call is fully useful.
- WebSocket bandwidth. Same logic, more acute. A power user subscribed to
kol:trades can see 17,000 messages a session. Filtering server-side at subscribe time cuts that to whatever fraction matches your band.
- Persistent rule semantics. If your rule says "alert me on coordination", and you post-filter on MC client-side, your audit trail in
coordination_alert_signals is full of "fired and dropped" noise. Putting the band on the rule itself means the rule's history matches what actually mattered.
Cleanup: avg_entry_mc_usd retired
While shipping the band, we also retired the avg_entry_mc_usd and entry_mc_samples fields that landed on /kol/leaderboard and /alpha/leaderboard last week. They were an experiment — "give callers a per-KOL micro-cap-vs-mid-cap signal" — that the new band subsumes. Instead of asking "what's the avg entry MC for this KOL", filter the feed by max_mc_usd=50000 and you get the actual sub-set you cared about, with the trades, sizes, and timing intact.
If you were depending on those fields, the migration is one of:
- Replicate the metric client-side — fetch
/kol/feed?kol={wallet}&limit=200, average the market_cap_usd_at_trade values yourself.
- Switch to the band — the question you were asking was probably "is this KOL playing micro-cap?" — answer it by filtering the feed and checking whether the result is materially smaller than unfiltered.
What's next
Two things on the near roadmap:
- Per-event MC band in webhook payloads. Same
min_mc_usd / max_mc_usd accepted on webhook subscription registration so the webhook fires only inside the band. (Today the band lives on coordination + first-touch + copytrade rules; we'll generalise to all webhook types.)
- MC-band presets. UI sugar like
?mc_band=nano|micro|low|mid|high mapping to standard ranges, so callers don't have to remember thresholds. Subject to actual user demand — let us know if you want it.
If you're building a compound scanner, pair the MC band with the momentum windows in Reorg-Safe Velocity Windows or the venue split in for a tighter query.