A Solana token screener API is a filtered, sorted, paginated query over every indexed token, and Solana Tracker exposes it as GET /search (client.searchTokens() in the SDK). You pass filters such as minimum liquidity, 24h volume, maximum risk score, market and creation time; the API sorts server-side and returns up to 500 rows per request (100 with format: 'full') plus a cursor for the next page.
This guide builds a small screener in TypeScript with filters from environment variables, explains every parameter it uses, and covers the details that make screener output wrong in production: promoted rows, units, cursors and market names.
How the Solana token screener API works
GET /search on https://data.solanatracker.io combines an optional text query with range filters. Every filter is optional, so the same endpoint powers a symbol lookup box and a 20-condition bot screen. The Solana Data API page describes it as search with 80+ filter options; the parameters below are the ones verified against the OpenAPI spec and SDK 0.5.0 types that this guide uses or recommends.
| Parameter | Unit / values | Use |
|---|---|---|
query | text | Name, symbol or mint address |
symbol | text | Exact symbol match |
minLiquidity, maxLiquidity | USD | Pool depth |
minMarketCap, maxMarketCap | USD | Size |
minVolume_24h (also _5m, _15m, _30m, _1h, _6h, _12h) | USD | Activity in a specific window |
minVolume + volumeTimeframe | USD + 5m...24h | Same idea with the window as a parameter |
minHolders, maxHolders | count | Distribution |
maxTop10, maxDev, maxInsiders, maxSnipers | percent, 0-100 | Holder concentration |
maxBundlers, maxBundlerPercentage | count / percent | Bundled launches |
minRiskScore, maxRiskScore | 1-10, higher is riskier | Risk pre-filter |
minCreatedAt, maxCreatedAt | Unix time in ms | Token age |
market | pumpfun-amm, raydium, meteora-dlmm, ... | Where the pool trades; comma-separated for several |
launchpad | pumpfun, ... | Where the token launched |
status | graduating, graduated, default | Bonding-curve stage |
minCurvePercentage, maxCurvePercentage | percent | Curve progress for launchpad tokens |
freezeAuthority, mintAuthority | address, or null for none | Authority checks |
hasSocials, hasImage | boolean | Metadata presence |
sortBy, sortOrder | see below, asc/desc | Server-side ordering (default createdAt desc) |
limit, page, cursor | 1-500 | Pagination |
format | full | Return full token objects |
sortBy accepts liquidityUsd, marketCapUsd, priceUsd, volume, the volume_* windows, top10, dev, insiders, snipers, holders, buys, sells, totalTransactions, fees.total, fees.totalTrading, fees.totalTips, createdAt, lpBurn, curvePercentage and communityMessages.
Search vs other discovery endpoints
Search is not the only way to find tokens. Pick the endpoint that matches the question:
| Question | Endpoint | Filters you control |
|---|---|---|
| Which tokens match my rules? | GET /search | All of the above |
| What is trending right now? | GET /tokens/trending/{timeframe} | Timeframe only |
| What launched in the last minutes? | GET /tokens/latest | None |
| Who leads on volume? | GET /tokens/volume/{timeframe} | Timeframe only |
| Which venue or launchpad is busy? | GET /lighthouse | None; market-level, see the market activity guide |
| Tell me the moment a token appears | Datastream latest room | Client-side only |
A screener is the right tool when your criteria are explicit and you want them applied before data reaches you. If you want to react to every new launch, stream it instead; the Pump.fun API covers that path.
Build the screener in TypeScript
The script reads filters from the environment, validates them, pages through results with the cursor, de-duplicates by mint and prints a table. Install and run with Node.js 20.18 or newer:
npm init -y && npm pkg set type=module
npm install @solana-tracker/data-api@^0.5.0
npm install --save-dev tsx typescript @types/node
node --env-file=.env --import tsx index.ts
.env.example:
# Data API key from https://www.solanatracker.io/account/data-api
ST_API_KEY=
# Optional comma-separated market ids, e.g. pumpfun-amm,raydium. Empty = all
MARKETS=
MIN_LIQUIDITY_USD=25000
MIN_VOLUME_24H_USD=50000
# 1-10, higher = riskier
MAX_RISK_SCORE=6
# Only tokens created in the last N hours, 0 = no limit
MAX_AGE_HOURS=0
SORT_BY=volume_24h
PAGE_SIZE=20
MAX_PAGES=3
index.ts:
import { Client, DataApiError, RateLimitError, type SearchParams, type SearchResult } from '@solana-tracker/data-api';
function exit(message: string): never {
console.error(message);
process.exit(1);
}
function num(name: string, fallback: number, min: number, max: number): number {
const raw = process.env[name]?.trim();
const value = raw ? Number(raw) : fallback;
if (!Number.isFinite(value) || value < min || value > max) exit(`${name} must be a number from ${min} to ${max}`);
return value;
}
const apiKey = process.env.ST_API_KEY?.trim();
if (!apiKey) exit('Missing ST_API_KEY (create one at https://www.solanatracker.io/account/data-api)');
const SORT_FIELDS = ['liquidityUsd', 'marketCapUsd', 'volume_24h', 'volume_1h', 'holders', 'createdAt', 'totalTransactions'];
const sortBy = process.env.SORT_BY?.trim() || 'volume_24h';
if (!SORT_FIELDS.includes(sortBy)) exit(`SORT_BY must be one of ${SORT_FIELDS.join(', ')}`);
const markets = (process.env.MARKETS ?? '').split(',').map((m) => m.trim()).filter(Boolean);
const minLiquidity = num('MIN_LIQUIDITY_USD', 25_000, 0, 1e11);
const minVolume24h = num('MIN_VOLUME_24H_USD', 50_000, 0, 1e12);
const maxRiskScore = num('MAX_RISK_SCORE', 6, 0, 10);
const maxAgeHours = num('MAX_AGE_HOURS', 0, 0, 24 * 365 * 10);
const pageSize = num('PAGE_SIZE', 20, 1, 500);
const maxPages = num('MAX_PAGES', 3, 1, 20);
const now = Date.now();
const params: SearchParams = {
market: markets.length ? markets : undefined, // the SDK joins arrays with commas
minLiquidity,
minVolume_24h: minVolume24h,
maxRiskScore,
minCreatedAt: maxAgeHours > 0 ? Math.round(now - maxAgeHours * 3_600_000) : undefined, // ms
sortBy,
sortOrder: 'desc',
limit: pageSize,
};
const client = new Client({ apiKey });
// The SDK has no timeout and no retry: add both.
async function call<T>(label: string, fn: () => Promise<T>, attempts = 3): Promise<T> {
for (let attempt = 1; ; attempt++) {
let timer: NodeJS.Timeout | undefined;
try {
const timeout = new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new Error(`${label} timed out`)), 15_000);
});
return await Promise.race([fn(), timeout]);
} catch (error) {
const retryable =
error instanceof RateLimitError ||
!(error instanceof DataApiError) ||
error.status === undefined ||
error.status >= 500;
if (!retryable || attempt >= attempts) throw error;
const retryAfterMs = error instanceof RateLimitError && error.retryAfter ? error.retryAfter * 1000 : 0;
await new Promise((resolve) => setTimeout(resolve, Math.max(retryAfterMs, 500 * 2 ** attempt)));
} finally {
clearTimeout(timer);
}
}
}
// `data` can carry up to 5 promoted rows on top of the organic page: re-check the filters.
const matches = (r: SearchResult) =>
r.liquidityUsd >= minLiquidity &&
(r.volume_24h ?? 0) >= minVolume24h &&
(r.riskScore === undefined || r.riskScore <= maxRiskScore) &&
(maxAgeHours === 0 || r.createdAt >= now - maxAgeHours * 3_600_000);
const usd = (n: number | undefined) =>
typeof n === 'number' ? `$${Intl.NumberFormat('en-US', { notation: 'compact' }).format(n)}` : 'n/a';
try {
const byMint = new Map<string, SearchResult>();
let cursor: string | undefined;
let page = 1;
for (let i = 0; i < maxPages; i++) {
const res = await call('searchTokens', () => client.searchTokens(cursor ? { ...params, cursor } : { ...params, page }));
for (const row of res.data ?? []) if (row.mint && !byMint.has(row.mint) && matches(row)) byMint.set(row.mint, row);
const more = res.hasMore ?? (res.pages !== undefined && page < res.pages);
if (!more) break;
if (res.nextCursor) cursor = res.nextCursor;
else page++;
}
for (const r of byMint.values()) {
console.log(
`${(r.symbol ?? '?').padEnd(10)} ${r.mint} ${(r.market ?? '?').padEnd(12)} liq ${usd(r.liquidityUsd).padEnd(8)}` +
` vol24h ${usd(r.volume_24h).padEnd(8)} holders ${String(r.holders).padEnd(7)} risk ${r.riskScore ?? 'n/a'}`,
);
}
console.log(`Screener: ${byMint.size} tokens matched`);
} catch (error) {
const status = error instanceof DataApiError && error.status ? `HTTP ${error.status}: ` : '';
exit(`Screener failed: ${status}${error instanceof Error ? error.message : String(error)}`);
}
The companion project adds SEARCH_QUERY, SORT_ORDER, the full sortBy list, an age column and an aligned table.
Response shape
Without format, each row in data is a flat SearchResult built for tables. Trimmed from the OpenAPI example (values illustrative):
{
"status": "success",
"data": [
{
"id": "1mHo…pump_1mHo…pump",
"name": "Search Example",
"symbol": "SEARCH",
"mint": "1mHoWfm6z2rPwoYvDJnbXLBqn1Kz5uFphsuXFbNEpump",
"poolAddress": "9mHoWfm6z2rPwoYvDJnbXLBqn1Kz5uFphsuXFbNEpump",
"market": "pumpfun",
"liquidityUsd": 11448.85,
"marketCapUsd": 5598.43,
"priceUsd": 0.0000055984,
"lpBurn": 100,
"freezeAuthority": null,
"mintAuthority": null,
"status": "default",
"createdAt": 1760273456641,
"holders": 2,
"buys": 2,
"sells": 1,
"volume_24h": 286,
"top10": 5.05,
"dev": 5.05,
"insiders": 0,
"snipers": 0.32,
"bundlers": { "count": 0, "balance": 0, "percentage": 0 },
"riskScore": 10,
"fees": { "total": 0.002086, "totalTrading": 0.001966, "totalTips": 0.00012 }
}
],
"total": 13144556,
"pages": 13144556,
"page": 1,
"nextCursor": "WzE3NjAyNzM0NTY2NDEsIjltSG9…",
"hasMore": true
}
Holder fields (top10, dev, insiders, snipers) are percentages of supply, createdAt is milliseconds, and fees are in SOL. With format: 'full', each row is instead the same object GET /tokens/{mint} returns: token, pools, events, risk, buys, sells, txns and holders.
Pagination: cursor or page
The endpoint supports both, and the response tells you which to use next.
cursor | page | |
|---|---|---|
| Request | cursor= | page=2, page=3, ... |
| Deep pages | Faster, per the API reference | Slower as offsets grow |
| Stability while data changes | Resumes after the last row returned | Rows can shift between pages |
| Jump to page N | No | Yes |
| Stop condition | hasMore is false | page >= pages |
For a bot or export, use the cursor. For a UI with numbered pages, use page. Treat the cursor as opaque and send it back unchanged. It belongs to one combination of filters and sort order: when a filter changes, drop the cursor and start again.
When to use format: 'full'
format: 'full' returns full token objects, which saves a second getTokenInfo call per row when you need pools, price-change events or the complete risk object. The cost is a smaller page: the limit is capped at 100 rows. For an autocomplete box or a first-pass screen, the flat rows are enough. For a shortlist you are about to act on, request full objects or run each candidate through a dedicated rug check.
Production pitfalls
Promoted rows. The API reference notes that data can include up to 5 curated or promoted rows on top of the organic page, so data.length can reach limit + 5. Re-check your numeric filters on the client and de-duplicate by mint, as the code above does.
Market is not launchpad. market is where the pool trades today; launchpad is where the token was minted. A Pump.fun token that graduated trades on pumpfun-amm but keeps launchpad pumpfun. Use the documented values from the filter options guide; some launchpad names contain spaces, so URL-encode them if you call the endpoint without the SDK.
Units. Timestamps are milliseconds, holder filters are 0-100 percentages, fee filters are SOL, liquidity and volume are USD. A minCreatedAt in seconds silently matches everything created since 1970.
Use the volume window you mean. minVolume_24h and minVolume_1h filter different windows. Plain minVolume uses the default window unless you set volumeTimeframe.
Authority filters take a string. To require revoked authorities, pass freezeAuthority: 'null' and mintAuthority: 'null' as strings, per the API reference.
Rankings move. Search reads live, mutable data. Two identical requests a minute apart can return different rows. Store the filter set, sort, timestamp and cursor together if you need to explain a result later.
Budget requests. Each page is one REST request against your plan's monthly allowance. Cache shared screens on your backend instead of letting every client page through results, and watch usage in the Data API dashboard.
Timeouts and retries. The SDK's REST client has neither. Wrap calls, honour RateLimitError.retryAfter, and never retry a ValidationError.
From screen to action
A screener produces candidates, not decisions. A typical pipeline is: screen with maxRiskScore and holder filters, fetch the full risk object for each survivor, then subscribe to live prices only for the tokens you keep with the price WebSocket guide. If social activity is part of your criteria, the hasCoinCommunity and minCommunityMessages filters are covered in the Coin Communities quickstart.
FAQ
How many filters does the search endpoint support?
The product page lists 80+ options. This guide only uses filters that are documented in the OpenAPI spec and typed in SDK 0.5.0; the full list is in the token search reference.
Can I filter several markets at once?
Yes. Pass an array in the SDK (market: ['pumpfun-amm', 'raydium']), which it sends as a comma-separated value. The launchpad filter works the same way.
What is the maximum page size?
500 rows for the default flat format and 100 rows with format: 'full'. Promoted rows can add up to 5 more.
Should I use cursor or page pagination?
Use cursor for bots, exports and deep pagination; it continues from the last row seen. Use page when a UI needs numbered pages. Reset either one when the filters change.
Does maxRiskScore make the results safe?
No. It removes tokens whose current score is above your limit. The score is a signal; check the individual risk factors before acting.
How do I find only tokens that are about to graduate?
Filter with status: 'graduating' or a minCurvePercentage, optionally with launchpad: 'pumpfun', and sort by curvePercentage.
References
- Token search API reference
- Search market and launchpad filters
- Find new and trending Solana tokens
- Find tokens by community activity
- Risk score reference
- @solana-tracker/data-api on npm
Companion project
The full example lives at solanatracker/examples/14-solana-token-search-screener-api. It uses the same environment variables plus SEARCH_QUERY and SORT_ORDER.
cp .env.example .env && npm install && npm start