const KEY = process.env.MADEONSOL_KEY;
const BASE = "https://madeonsol.com/api/v1";
async function* tape(mint, params = {}) {
let cursor = null;
do {
const qs = new URLSearchParams({ limit: "200", ...params });
if (cursor) qs.set("cursor", cursor);
const res = await fetch(`${BASE}/tokens/${mint}/trades?${qs}`, {
headers: { Authorization: `Bearer ${KEY}` },
});
if (res.status === 429) {
// x-ratelimit-reset is a Unix timestamp; sleep until it instead of hammering
const reset = Number(res.headers.get("x-ratelimit-reset")) * 1000;
await new Promise((r) => setTimeout(r, Math.max(1000, reset - Date.now())));
continue;
}
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const page = await res.json();
for (const t of page.trades) yield t;
cursor = page.has_more ? page.next_cursor : null;
} while (cursor);
}
let buys = 0, sells = 0, netSol = 0;
for await (const t of tape("2P49i8PFgRKYjb8NWMBHWNY5BW7nr7G9AJeAzx6mpump")) {
if (t.action === "buy") { buys++; netSol += t.sol_amount; } else { sells++; netSol -= t.sol_amount; }
}
console.log({ buys, sells, netSol });
Step 4: Read the coverage block before you trust the numbers
The coverage object is on every response and is the honest part of the API. scope says the tape is built from the launchpad pipeline: pump.fun, LaunchLab (bonk) and bags. A mint that never went through those programs is in_scope: false and returns an empty trades array with a note, which is exactly what happened when the tape was requested for a token that trades on a Raydium pool only:
{
"mint": "3cWb3fkAuqcH1CqJdqZoy5qAAqpZpG8pxiFR68MXAcf4",
"trades": [],
"next_cursor": null,
"has_more": false,
"coverage": {
"history_start": 1775984757,
"scope": "pump.fun pipeline (pump.fun, LaunchLab/bonk, bags)",
"in_scope": false,
"note": "No persisted trades for this subject — it sits outside the pump.fun pipeline scope…"
}
}
An empty array with in_scope: false is not "no trades happened", it is "we do not index this venue". A full-chain DEX archive (Bitquery, Birdeye) is the right tool for that mint; the data-API comparison says where each provider wins. Where this tape is strongest is the part those archives see least: the bonding-curve phase before a token graduates to PumpSwap, which is where early_buyer_rank and the executed-vs-market price gap carry the signal.
Step 5: Pair the tape with the aggregates you do not want to compute yourself
Three sibling endpoints save you from rebuilding the tape into metrics:
/api/v1/tokens/{mint}/candles (PRO) returns 1-minute-derived OHLCV with buy_volume_usd, sell_volume_usd, net_volume_usd, buy_count, sell_count, volume_mev_usd and open_liquidity_usd / close_liquidity_usd per bucket. The same 4-minute-old token above already had a 1h candle: 224 trades, $10,615 volume, of which $1,572 flagged as MEV/bot volume.
/api/v1/tokens/{mint}/buyer-quality scores who is buying: the token returned score: 0, signal: "negative", bundle_buyer_count: 17, recycled_early_buyer_count: 17. Seventeen of the first buyers were bundled and had been early buyers of previous launches.
/api/v1/token/{mint} gives the snapshot: launch_cohort_size: 20, early_buyer_exit.still_holding_pct: 100, kol_activity.signal: "accumulating" with the KOL's name and buy size.
Pull the tape when you need row-level truth (who, when, at what price, with what slippage). Pull the aggregates when you need a decision in one call.
Step 6: Move to push once you are polling
If the loop in Step 2 runs on a timer, it is a polling client, and the free-tier quota (200 calls/day, 60/min) will show it. The paid tiers include WebSocket streaming on the same key: token:prices pushes per-mint price and market-cap ticks (subscribe with filters.mints; 25 mints per connection on PRO, 100 on ULTRA, 250 on BUSINESS), and token:surges pushes the moment a young token runs 3×, 6× or 8× against its launch cap. Pushed events do not count against the daily quota. The WebSocket reconnect guide covers the handshake and replay_since_seq.
FAQ
Which pump.fun tokens does the trade tape cover?
Tokens launched through pump.fun, Raydium LaunchLab (bonk) and bags, from 2026-04-12 onward. The coverage block on every response states the scope and whether the requested mint is in_scope. A mint that only ever traded on a Raydium or Orca pool returns an empty tape with in_scope: false, not a partial one.
What is the difference between price_usd and market_price_usd on a trade?
price_usd is the price that trade executed at (sol_amount / token_amount, converted at the SOL price of the moment). market_price_usd is the pool's canonical price sampled by the market-cap tracker near the same slot. The difference is the trade's slippage against the curve.
How far back does the trade history go, and where does old data live?
Postgres holds the most recent months (history.postgres_from, 2026-06-01 at the time of writing); older closed months are served from a Parquet archive. When a query reaches into it the response says so (history.archive_used: true, archive_months, and an X-Read-Source: core+archive header). Add a since bound to keep a query on the hot path.
Which tier do I need for /tokens//trades?
PRO or higher (€43/mo). A free BASIC key receives a 403 whose body names the required tier. PRO includes 10,000 calls/day and the WebSocket channels; the free tier is 200 calls/day with live feeds delayed by 5 minutes.
How do I get all trades by one wallet on a token?
Add ?wallet=<address> to the tape call. Combine with since (Unix seconds) to limit the scan; a wallet filter over the full history reads the Parquet archive as well as Postgres.
Is there a way to stream trades instead of paging?
Per-mint price and market-cap ticks stream on the token:prices WebSocket channel, and momentum events on token:surges. The full per-trade DEX firehose is a separate ULTRA/BUSINESS socket (dex_ws_url in the /stream/token response). REST paging is the right tool for backfills and audits; the socket is the right tool for a live bot.