Solana Tracker LogoSolana Tracker
Swap
Developers
⌘K
Affiliate
All Resources
Solana Rug Check API: Risk Scores, Factors and a Bot Gate
Data APITypeScriptJune 5, 202613 min readUpdated October 7, 2026Solana Tracker

Solana Rug Check API: Risk Scores, Factors and a Bot Gate

Use the Solana rug check API to read token risk scores, rugged flags and holder concentration, then gate tokens with a configurable MAX_RISK_SCORE.

  • data-api,
  • rugcheck,
  • risk-score,
  • security,
  • typescript
Solana Data API›Pump.fun API›

A Solana rug check API call to Solana Tracker is one request, GET /tokens/{mint}: every token response carries a risk object with a score from 1 to 10 (higher means riskier), a rugged flag, the individual risk factors, and sniper, insider, bundler, developer and top-10 holder concentration. Use the score to triage, the factors to explain, and a gate you configure to decide. The score is a screening signal, not a guarantee that a token is safe.

This guide shows the response shape, a TypeScript gate driven by MAX_RISK_SCORE, how the score is built, and the mistakes that turn a useful signal into a false sense of safety. If you want the same data without code, the Rugcheck page in the Solana Tracker terminal shows it for any mint.

What the Solana rug check API returns

The risk data lives inside the standard token info response from the Solana Data API. There is no separate risk endpoint to call. In the SDK, client.getTokenInfo(mint) returns a TokenDetailResponse whose risk field looks like this (trimmed, values illustrative, field names from the SDK 0.5.0 types and the OpenAPI spec):

{
  "risk": {
    "score": 7,
    "rugged": false,
    "jupiterVerified": false,
    "top10": 31.22,
    "dev": { "percentage": 0, "amount": 0 },
    "snipers": {
      "count": 6,
      "totalBalance": 124000000,
      "totalPercentage": 12.4,
      "wallets": [{ "wallet": "8sVT…P1iA", "balance": 41000000, "percentage": 4.1 }]
    },
    "insiders": { "count": 2, "totalBalance": 31000000, "totalPercentage": 3.1, "wallets": [] },
    "bundlers": {
      "count": 14,
      "totalBalance": 48500000,
      "totalPercentage": 4.85,
      "totalInitialBalance": 90000000,
      "totalInitialPercentage": 9,
      "wallets": []
    },
    "fees": { "totalTrading": 21.5, "totalTips": 162.1, "total": 183.6 },
    "risks": [
      {
        "name": "Top 10 Holders",
        "description": "Top 10 holders own more than 15% of the total supply",
        "level": "danger",
        "score": 5000
      },
      { "name": "Snipers", "description": "Snipers own 12.40% of the total supply.", "value": "12.40%", "level": "warning", "score": 3000 }
    ]
  }
}

The fields that matter for a rug check:

FieldMeaning
scoreFinal risk score, normalized to 1-10. Higher is riskier.
ruggedSeparate flag: no liquidity remains. Check it on its own, not only through the score.
risks[]Individual factors with name, description, level (warning or danger), an internal weight score, and sometimes a value.
snipers, insidersWallet count, total balance, percent of supply, and the wallet list.
bundlersBundler summary. It can be omitted from this response; use GET /tokens/{mint}/bundlers for the full list.
dev.percentage, top10Developer holding and top-10 concentration, both in percent of supply.
feesSOL paid in trading fees and tips, by platform, plus totals.
jupiterVerifiedWhether the token is Jupiter-verified.

The SDK normalizes two legacy details: wallet entries get an address alias next to the wire field wallet, and when the wire response omits bundlers, the SDK fills an all-zero category. That second point matters for your gate and is covered under pitfalls.

Pick the right call

Risk data appears in more places than the single-token endpoint. Choose by the question you are answering:

NeedCallRequestsNotes
Full check on one mintgetTokenInfo(mint) → GET /tokens/{mint}1Score, factors, holder groups, pools
Complete bundler listgetTokenBundlers(mint) → GET /tokens/{mint}/bundlers1Up to 500 bundler wallets with initial and current holdings
Screen a list you did not choosegetTrendingTokens('1h')1Each row is a full token object that includes risk
Pre-filter discoverysearchTokens({ maxRiskScore, maxSnipers, ... })1 per pageFilters server-side; see the token screener guide
Watch holder groups changeDatastream subscribe.snipers(), .insiders(), .bundlers()0 RESTWebSocket rooms; Premium plan or higher
Manual reviewRugchecknoneSame signals in the UI

For a trading bot, a common pattern is search with maxRiskScore to narrow the universe, getTokenInfo on each candidate right before acting, and a Datastream subscription while you hold a position.

Build a configurable risk gate in TypeScript

The gate below fetches one token, prints the factors, and returns one of three states: PASS, FAIL, or UNKNOWN. Missing data is never a pass. The thresholds come from the environment so you can tune them without a deploy.

Install and run (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=
# Mint to check
TOKEN_MINT=
# Fail above this score (1-10, higher = riskier)
MAX_RISK_SCORE=6
# Fail when snipers or insiders hold more than this percent of supply
MAX_SNIPER_PCT=20
MAX_INSIDER_PCT=20

index.ts:

import { Client, DataApiError, RateLimitError, type TokenDetailResponse } from '@solana-tracker/data-api';

type Risk = TokenDetailResponse['risk'];
type Verdict = { status: 'PASS' | 'FAIL' | 'UNKNOWN'; reasons: string[] };
type Factor = { name: string; level: string; detail: string };

function exit(message: string): never {
  console.error(message);
  process.exit(1);
}

function numberFromEnv(name: string, fallback: number, max: number): number {
  const raw = process.env[name]?.trim();
  const value = raw ? Number(raw) : fallback;
  if (!Number.isFinite(value) || value < 0 || value > max) exit(`${name} must be a number from 0 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 mint = process.env.TOKEN_MINT?.trim() ?? '';
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(mint)) exit('Set TOKEN_MINT to a base58 mint address');

const policy = {
  maxScore: numberFromEnv('MAX_RISK_SCORE', 6, 10),
  maxSniperPct: numberFromEnv('MAX_SNIPER_PCT', 20, 100),
  maxInsiderPct: numberFromEnv('MAX_INSIDER_PCT', 20, 100),
};

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);
    }
  }
}

// Guides document factors as objects; the OpenAPI schema lists strings. Accept both.
function factors(risks: unknown): Factor[] {
  if (!Array.isArray(risks)) return [];
  return risks.map((entry: unknown) => {
    if (typeof entry === 'string') return { name: entry, level: 'unknown', detail: '' };
    const f = (entry ?? {}) as { name?: string; level?: string; description?: string; value?: string | number };
    return { name: f.name ?? 'unnamed', level: f.level ?? 'unknown', detail: String(f.value ?? f.description ?? '') };
  });
}

function evaluate(risk: Risk | undefined): Verdict {
  if (!risk || typeof risk.score !== 'number' || !Number.isFinite(risk.score)) {
    return { status: 'UNKNOWN', reasons: ['no risk score in the response'] };
  }
  const reasons: string[] = [];
  if (risk.rugged) reasons.push('rugged flag is set');
  if (risk.score > policy.maxScore) reasons.push(`score ${risk.score} > ${policy.maxScore}`);
  if ((risk.snipers?.totalPercentage ?? 0) > policy.maxSniperPct) reasons.push(`snipers ${risk.snipers.totalPercentage.toFixed(1)}%`);
  if ((risk.insiders?.totalPercentage ?? 0) > policy.maxInsiderPct) reasons.push(`insiders ${risk.insiders.totalPercentage.toFixed(1)}%`);
  for (const f of factors(risk.risks)) if (f.level === 'danger') reasons.push(`danger: ${f.name}`);
  return { status: reasons.length ? 'FAIL' : 'PASS', reasons };
}

try {
  const info = await call('getTokenInfo', () => client.getTokenInfo(mint));
  const bundlers = await call('getTokenBundlers', () => client.getTokenBundlers(mint)).catch(() => undefined);
  const { risk } = info;

  console.log(`${info.token?.symbol ?? '?'}  score ${risk?.score ?? 'n/a'}/10  rugged: ${risk?.rugged ? 'yes' : 'no'}`);
  console.log(`snipers ${risk?.snipers?.totalPercentage ?? 'n/a'}%  insiders ${risk?.insiders?.totalPercentage ?? 'n/a'}%`);
  console.log(`dev ${risk?.dev?.percentage ?? 'n/a'}%  top10 ${risk?.top10 ?? 'n/a'}%  bundlers ${bundlers ? `${bundlers.percentage}% (${bundlers.total} wallets)` : 'unavailable'}`);
  for (const f of factors(risk?.risks)) console.log(`  [${f.level}] ${f.name} ${f.detail}`);

  const verdict = evaluate(risk);
  console.log(`Risk gate (MAX_RISK_SCORE=${policy.maxScore}): ${verdict.status}`);
  for (const reason of verdict.reasons) console.log(`  - ${reason}`);
  process.exitCode = verdict.status === 'PASS' ? 0 : 2;
} catch (error) {
  const status = error instanceof DataApiError && error.status ? `HTTP ${error.status}: ` : '';
  exit(`Rug check failed: ${status}${error instanceof Error ? error.message : String(error)}`);
}

The process exits with 0 on PASS, 2 on FAIL or UNKNOWN, and 1 on configuration or request errors, so a shell script or CI job can branch on the result. The companion project splits the same logic into modules, prints aligned tables, and screens the 1h trending list when TOKEN_MINT is empty.

How the score is built

Each factor carries an internal weight. The API sums the weights of the factors that apply and normalizes the total to the final 1-10 score, so the per-factor score values (for example 5000) are relative strength, not the user-facing number. The risk score reference lists every factor. A few representative ones:

FactorLevelWeight
Rugged (no liquidity remains)danger20000
Freeze authority enableddanger7500
Very low liquiditydanger7500
Top 10 holders own more than 15%danger5000
LP not burned or not lockeddanger4000
Mint authority enableddanger2500
Snipers hold more than 10%warning3000
No social media linkswarning2000

Sniper, insider, bundler and developer factors scale with the share of supply those groups hold; bundlers also scale with wallet count. Meteora curve tokens get a dynamic-fee factor once fees pass 5%.

The docs describe score bands roughly as 1-3 low, 4-6 medium, 7-8 high and 9-10 critical. Treat those as labels for a UI, not as a policy. Your threshold depends on what a false negative costs you: a portfolio display can show everything with a badge, while an auto-buy bot might want a low MAX_RISK_SCORE and danger factors blocking.

Why a score is a signal, not a guarantee

A risk score summarizes what the indexer observed at one moment. It cannot see intent, off-chain agreements, or a deployer's next transaction. A token with a score of 2 can still lose its liquidity tomorrow, and a token with a score of 8 can be a long-lived project whose authority settings simply have not been revoked.

Use the score to decide where to spend attention. Use the factors to explain the decision to a user. Keep rugged as its own check, because it describes the current state of liquidity rather than a probability.

Production pitfalls

Missing data is not a pass. If risk or risk.score is missing, return UNKNOWN and treat it like a fail in automated flows. The gate above never substitutes zero.

Do not trust a zero bundler count from the token response. The OpenAPI spec notes that risk.bundlers may be omitted even when the bundlers endpoint returns wallets, and the SDK then fills an all-zero category. Call getTokenBundlers when bundling matters to your decision, and show "unavailable" when that call fails.

Type the factor list defensively. The guides show risks as objects with name, level and score; the OpenAPI schema types them as strings. Normalizing both shapes costs a few lines and prevents a crash.

Do not assume an integer score. The OpenAPI schema says integer, but the token-safety guide shows a decimal example (7.4). Compare with > against a number and format for display.

Holder groups overlap. The same wallet can be counted as a sniper and an insider. Never add sniper, insider, bundler and developer percentages into one "suspicious supply" total.

Snapshots go stale. Liquidity, authorities and holdings change after your check. Re-check right before any action that moves funds, cache results for seconds rather than hours, and record the policy values and timestamp with every verdict so you can explain it later.

Handle rate limits and timeouts yourself. The SDK's REST client has no timeout and does not retry. RateLimitError.retryAfter gives seconds to wait; ValidationError (for example, a malformed mint) should never be retried.

Keep watching after the first check

A one-time check answers "is this acceptable now". To learn when it stops being acceptable, subscribe to the Datastream safety rooms for the mint: ds.subscribe.snipers(mint), ds.subscribe.insiders(mint) and ds.subscribe.bundlers(mint) push holder-group changes, and ds.subscribe.token(mint).top10() tracks concentration. Datastream is included on Premium and higher plans; manage keys in the Data API dashboard.

For liquidity pulls specifically, follow LP additions and removals with the liquidity events guide. For launch-time behavior, the Pump.fun first buyers guide shows who bought first and what they still hold; the Pump.fun API page covers the launchpad endpoints.

FAQ

Is a higher risk score better or worse?

Worse. The score runs from 1 to 10 and higher means more reported risk. Read the factor list to see why a token scored the way it did.

What does MAX_RISK_SCORE do in the example?

It is your policy threshold: any token with risk.score above it fails the gate. It is not an API parameter on the token endpoint. On the search endpoint, maxRiskScore is a real filter that applies the same idea server-side.

Does passing the gate mean a token is safe?

No. It means the observed fields passed your configured policy at the moment of the request. Liquidity, authorities and holdings can change minutes later.

Why does the bundler count differ between the token response and the bundlers endpoint?

The token response can omit the bundler summary, and the SDK replaces an omitted summary with zeros for compatibility. The dedicated GET /tokens/{mint}/bundlers endpoint returns up to 500 bundler wallets and is the source to trust.

How many requests does a rug check cost?

One request for getTokenInfo, plus one if you also call getTokenBundlers. Screening the trending list costs one request for up to the whole list, because each row already includes risk.

Can I check risk without writing code?

Yes. Open Rugcheck and enter a mint. It shows the risk score, holder distribution, liquidity locks and mint and freeze authority for any Solana token.

References

  • Read Solana token risk scores and safety signals
  • Risk score reference
  • Get token information
  • Get token bundlers
  • Find early token buyers, snipers, and bundlers
  • Monitor token risk signals over WebSocket
  • @solana-tracker/data-api on npm

Companion project

The full example lives at solanatracker/examples/12-check-solana-token-rug-risk-api. It reads the same environment variables, adds BLOCK_DANGER_FACTORS, and screens the trending list when no mint is set.

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