Solana Tracker LogoSolana Tracker
Swap
Developers
⌘K
Affiliate
All Resources
Solana Token Screener API: Filters, Sorting and Pagination
Data APITypeScriptJune 7, 202612 min readUpdated October 7, 2026Solana Tracker

Solana Token Screener API: Filters, Sorting and Pagination

Build a Solana token screener API client in TypeScript: searchTokens filters for liquidity, volume, age and risk, plus sorting and cursor pagination.

  • data-api,
  • search,
  • screener,
  • token-discovery,
  • typescript
Solana Data API›Pump.fun API›

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.

ParameterUnit / valuesUse
querytextName, symbol or mint address
symboltextExact symbol match
minLiquidity, maxLiquidityUSDPool depth
minMarketCap, maxMarketCapUSDSize
minVolume_24h (also _5m, _15m, _30m, _1h, _6h, _12h)USDActivity in a specific window
minVolume + volumeTimeframeUSD + 5m...24hSame idea with the window as a parameter
minHolders, maxHolderscountDistribution
maxTop10, maxDev, maxInsiders, maxSniperspercent, 0-100Holder concentration
maxBundlers, maxBundlerPercentagecount / percentBundled launches
minRiskScore, maxRiskScore1-10, higher is riskierRisk pre-filter
minCreatedAt, maxCreatedAtUnix time in msToken age
marketpumpfun-amm, raydium, meteora-dlmm, ...Where the pool trades; comma-separated for several
launchpadpumpfun, ...Where the token launched
statusgraduating, graduated, defaultBonding-curve stage
minCurvePercentage, maxCurvePercentagepercentCurve progress for launchpad tokens
freezeAuthority, mintAuthorityaddress, or null for noneAuthority checks
hasSocials, hasImagebooleanMetadata presence
sortBy, sortOrdersee below, asc/descServer-side ordering (default createdAt desc)
limit, page, cursor1-500Pagination
formatfullReturn 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:

QuestionEndpointFilters you control
Which tokens match my rules?GET /searchAll of the above
What is trending right now?GET /tokens/trending/{timeframe}Timeframe only
What launched in the last minutes?GET /tokens/latestNone
Who leads on volume?GET /tokens/volume/{timeframe}Timeframe only
Which venue or launchpad is busy?GET /lighthouseNone; market-level, see the market activity guide
Tell me the moment a token appearsDatastream latest roomClient-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.

cursorpage
Requestcursor=page=2, page=3, ...
Deep pagesFaster, per the API referenceSlower as offsets grow
Stability while data changesResumes after the last row returnedRows can shift between pages
Jump to page NNoYes
Stop conditionhasMore is falsepage >= 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
View source on GitHub›Run in StackBlitz›

Runnable Node.js project — clone from GitHub, add keys to .env, then npm start. Use a private local environment for credentials. Check the companion package version before running.

Related Guides

Solana Liquidity Events API: LP History and Live Stream
Data API

Solana Liquidity Events API: LP History and Live Stream

Read more
Jupiter DCA API: Track Recurring Orders and Live Fills
Data API

Jupiter DCA API: Track Recurring Orders and Live Fills

Read more
Solana Market Activity API: DEX and Launchpad Stats
Data API

Solana Market Activity API: DEX and Launchpad Stats

Read more

Products

  • Data API
  • Pump.fun API
  • Solana RPC
  • Dedicated Nodes
  • Yellowstone gRPC
  • Raptor Swap API
  • Enterprise

Trading

  • Swap
  • Latest Tokens
  • Trending
  • Top Gainers
  • Memescope
  • Whale Watch
  • KOL Tracker

Tools

  • Wallet Tracker
  • Rugcheck
  • PnL Leaderboard
  • KOLScan
  • Axiom Leaderboard
  • Photon Leaderboard
  • Bloom Leaderboard
  • FOMO Leaderboard
  • GMGN Leaderboard
  • Pump.fun App Leaderboard
  • Terminal Leaderboard
  • Platform Compare
  • My Positions
  • Teams

Resources

  • Developer Guides
  • Blog
  • Documentation
  • API Reference
  • Status
  • Affiliate Program — 25% recurring, uncapped
Solana TrackerSolana Tracker© 2026
Terms of ServicePrivacy PolicyContact