The instinct on most APIs is to call them in a for loop. You have 30 token mints to look up; you write for (const mint of mints) await client.token.get(mint); and ship it. It works in dev. Then production traffic shows up and your 30-row scanner is firing 30 HTTP requests every 5 seconds, you breach the burst limit on the third batch, and your bot starts 429-ing.
There's a better path on MadeOnSol for the two endpoints where this comes up most: full token lookup and buyer-quality scoring. Both have batch variants that accept up to 50 mints per request, run all the underlying queries in parallel, and return the same per-mint shape you'd get from the singular endpoint.
This post walks through both, with worked examples for a sniper bot that needs to evaluate every new mint from a stream within seconds.
POST /api/v1/token/batch — full per-mint data, 50 at a time
Request:
curl -X POST https://madeonsol.com/api/v1/token/batch \
-H "Authorization: Bearer msk_..." \
-H "Content-Type: application/json" \
-d '{ "mints": ["4k3Dyj...", "9bTpRf...", "2eY8Ww..."] }'
Response:
{
"tokens": [
{
"mint": "4k3Dyj...",
"price_usd": 0.00029,
"market_cap": 287000,
"vwap_price_usd": 0.000288,
"fdv_usd": 290000,
"liquidity_usd": 14200,
"primary_dex": "pumpswap",
"volume_24h_usd": 412000,
"trades_24h": 1820,
"first_seen_at": "2026-05-14T08:11:00Z",
"age_seconds": 12660,
"is_blacklisted": false,
"blacklist_category": null,
"deployer": {
"wallet": "Gv7n...",
"tier": "elite",
"bonding_rate": 0.41
},
"kol_activity": {
"buying_kols": 6,
"selling_kols": 1,
"net_flow_sol": 18.4,
"signal": "accumulating",
"top_buyers": [
{ "name": "@alpha", "sol_amount": 4.2 },
{ "name": "@beta", "sol_amount": 3.8 }
]
},
"mc_change_pct": { "5m": 8.2, "15m": 22.1, "1h": 41.7 },
"volume_usd": { "5m": 4200, "15m": 18200, "1h": 81000 },
"mev_volume_pct":{ "5m": 12.1, "15m": 18.4, "1h": 22.7 }
}
],
"count": 3
}
Identical to the single-mint GET /v1/token/{mint} shape, just in an array. ULTRA tier additionally gets KOL wallet addresses inside kol_activity.top_buyers. Up to 50 mints per call.
What runs in parallel under the hood
A single-mint call kicks off seven concurrent operations: mc-tracker enrichment, dex-stream price/volume, SOL price, RPC supply lookup, deployer join, KOL trade history, and first-seen + blacklist lookups. A batch call fans the same seven out, but the database queries are batched with IN (...) rather than executed N times, and the per-mint HTTP/RPC calls fan out in parallel:
- Deployer / first-seen / blacklist / KOL trades — one Postgres round-trip each for all 50 mints (vs 50 round-trips). The
is_blacklisted flag returned here is the same sniper safety guard we detail in our token blacklist API guide, which filters stablecoins, wrapped SOL, LSTs, and known rugs.
- dex-stream
/price/{mint} — 50 parallel local HTTP calls (vs 50 serial).
- RPC supply lookup — 50 parallel RPC calls.
- mc-tracker enrichment — one batched query for all 50.
End-to-end, a 50-mint batch typically completes in 200-400ms — comparable to a single-mint call, not 50× the time. From the rate-limit perspective, it costs you one request, not 50.
POST /api/v1/tokens/batch/buyer-quality — quality scores with LRU caching
Buyer-quality scoring is its own batch endpoint, separate from /token/batch, because the underlying compute is different (joining early-buyer tables, computing the weighted score) and because it benefits enormously from caching — every score is good for 5 minutes after computation.
Request:
curl -X POST https://madeonsol.com/api/v1/tokens/batch/buyer-quality \
-H "Authorization: Bearer msk_..." \
-H "Content-Type: application/json" \
-d '{ "mints": ["4k3Dyj...", "9bTpRf...", "2eY8Ww..."] }'
Response:
{
"tokens": [
{
"mint": "4k3Dyj...",
"score": 78,
"confidence": "high",
"signal": "positive",
"breakdown": {
"alpha_wallet_count": 3,
"kol_count": 2,
"bundle_buyer_count": 1,
"avg_historical_win_rate": 62.4,
"bot_dominated": false
},
"cached_at": "2026-05-14T11:38:42Z"
},
{
"mint": "9bTpRf...",
"score": 50,
"confidence": "low",
"signal": "neutral",
"breakdown": { ... },
"note": "Insufficient buyer data"
}
],
"count": 3,
"cache_hits": 1
}
cache_hits reports how many mints in the batch were served from cache. The cache is shared with the single-mint GET /v1/tokens/{mint}/buyer-quality endpoint and TTL'd at 5 minutes — if you'd called GET on a mint a minute ago, calling the batch on the same mint now returns instantly from memory.
For tokens with no early-buyer data yet, the response includes "note": "Insufficient buyer data" and a default neutral score of 50. Branch on note to skip these in your scoring logic.
For any mint where the batch score warrants a closer look, the cap-table endpoint returns the full per-wallet breakdown behind that single number — win rates, bundle flags, and KOL identities for every early buyer.
A worked example: sniper bot per-mint pre-filter
A sniper bot subscribing to the dex:trades firehose sees thousands of fresh mints per hour. Most are dust. The filter pipeline typically wants two cheap checks before deciding whether to act:
- Is the token old enough to have any meaningful data? (We track first-seen.)
- Is the early-buyer cohort high-quality? (Buyer-quality score.)
Step 2 is exactly what the buyer-quality batch is for. Group every fresh mint from the last 30 seconds into one call:
import { MadeOnSol } from "madeonsol";
const client = new MadeOnSol({ apiKey: process.env.MADEONSOL_API_KEY! });
// Collect fresh mints from your DEX firehose subscription...
const freshMints: string[] = pollFromQueue();
if (freshMints.length === 0) return;
// One API call instead of N
const { tokens, cache_hits } = await client.tokens.batchBuyerQuality({ mints: freshMints });
const qualifying = tokens.filter(
(t) => t.score >= 60 && t.confidence !== "low" && t.signal === "positive"
);
console.log(`${qualifying.length}/${tokens.length} mints qualify, ${cache_hits} cache hits`);