Read the risk signals

Every risk signal a pair can carry, what it means, and which field lets you verify it yourself.

A score is a number someone else chose. risk_signals is the list of reasons behind it, and it is the part you can act on: each entry names one thing observed about the pair, and for almost every one of them there is another field in the same response that lets you check it yourself.

The shape

score.risk_signals arrives with every pair, in the list and in the single-pair response:

{
  "score": {
    "value": 32,
    "title": "Risky",
    "updated_at": "2026-09-20T09:14:02Z",
    "risk_signals": [
      { "kind": "lp_unlocked",         "weight": 17, "observed_at": "2026-09-20T08:40:11Z" },
      { "kind": "supply_concentrated", "weight": 24, "observed_at": "2026-09-19T22:03:57Z" },
      { "kind": "liquidity_drop",      "weight": 11, "observed_at": "2026-09-18T14:26:30Z" }
    ]
  }
}

The weights above are one snapshot, not a table: they are what those signals cost this pair at that moment. The same list reaches you as score.signals on /chains/{chain}/tokens/{address}/brief, keyed by the same kind strings.

Three rules that outlive any catalogue

The set is open. kind is a string, not a closed enum. New reasons are added as they are found, and a kind your client does not recognise arrives as the reason it is, or as unknown when it has no published spelling yet. Switch on the kinds you handle and keep a default branch — never assume the list below is complete, and never let an unrecognised kind read as "no problem".

weight is now, not always. It is the penalty this signal subtracts from the score at the moment of the response. Signals that describe market behaviour fade as the event ages; signals that describe a present condition hold their weight while the condition is true and disappear when it stops being true. A weight of 0 means the signal is disclosed to you without being applied — something else about the pair already answers it. Read the number we send; do not hardcode a table of your own.

An absent signal is not a clearance. A signal appears when something was observed. A check that could not run, or returned nothing, raises nothing — so the absence of lp_unlocked does not mean the LP is locked, it means look at security.burned_pct and security.locked_pct yourself. This is the same rule the brief states for flags: absent means not observed, never ruled out.

Verdicts — the pair is finished

These arrive with a terminal status. There is no reading of the market that changes them.

KindWhat was observedWhat to do
honeypotA simulated sell did not go through. Buying works, selling does not.Never route to it. Do not display it as tradable.
rug_confirmedThe pair's own deployer removed the liquidity from a pool that had been trading.Nothing to trade against — the pool is gone.
confiscation_confirmedHolders, or the pool itself, lost their balances to approvals they never granted.Treat every holder of the token as exposed, not just the ones already hit.

only_trustable=true on /pairs drops these for you, and verdict.status on one pair answers the same question for it as a single blocked / ok. blocked covers a terminal status on the pair or on either of its tokens.

Pool state

KindWhat was observedWhat to doVerify with
no_exitThe pair traded, held real liquidity, and now holds none. No accusation is attached — the fact stands on its own.Treat as untradable.liquidity_onchain_usd, liquidity_range

Most drained pools never get a rug_confirmed, because the wallet that took the liquidity out is not always the wallet that put it in. no_exit records the part that needs no attribution.

Who built it

These describe the wallet behind the pair, not the pair. They are history, which is exactly what makes them useful — and exactly why they say nothing about today's candle.

KindWhat was observedWhat to doVerify with
deployer_flaggedThe wallet that launched this pair carries abuse evidence — on this chain, or recorded elsewhere.Do not open a position on the strength of the chart. How strong the evidence is sits in the reputation beside it, not in a second signal.deployer.reputation.status, score, abused_launches
code_family_ruggedThe token's contract code matches a family of near-identical contracts with a rugged history.Treat the launch as a repeat of a template, not a new project.deployer.reputation
deployer_delegatedThe deployer's own account has code delegated to a contract while its token is still young.Reduce exposure. The account can now act as a contract on the token.—
abuse_cluster_funderThe wallet that paid this pair's deployer its first gas has funded a cluster of short-lived deployers whose pools did not survive.Read it as reputation one hop further out than the deployer: it describes the operator behind the key, not the key. Weakest of the four, and the one most worth ignoring when the LP is structurally locked.—

abuse_cluster_funder is the clearest case of a signal you will sometimes see at a reduced or zero weight: when the liquidity is structurally locked, evidence about a wallet two hops away no longer changes what can happen to the pool, so it is disclosed to you without being applied.

What the contract can do

Properties of the token and the pool. A chart cannot outrun any of them.

KindWhat was observedWhat to doVerify with
privileged_transferAn address with privileges over the token can move the pool's balance without holding an allowance.Do not enter. The exit does not belong to you.deployer.address
forged_approvalA holder carries an approval to the deployer that the holder never signed. The capability above has been used.Exit. Being untouched so far is not a state, it is an ordering.holders (holders_unreliable shows as a missing holder block)
sell_side_blockedThe sell side is measurably blocked, without a terminal verdict having been recorded.Test a small sell before sizing up, or stay out.sell_tax, flags: honeypot
high_taxBuy tax, sell tax or the pool fee is high enough to eat a normal target, or the sell side costs materially more than the buy side.Subtract it from every target before deciding.buy_tax, sell_tax, fee
tax_escalationThe sell tax rose sharply between two observations.The level matters less than the movement: someone is turning the dial now.sell_tax across two polls
transfer_restrictedAn unverified token in the pair can blacklist, freeze or pause a holder.Your exit is conditional on someone else's list.security, token verification state
supply_concentratedThe deployer still holds a large share of its own token outside the pool.Size for the overhang. This is the supply that gets sold into you.holders.top10_pct
owner_can_mintThe supply of an unverified token in the pair can still grow.Do not hold long.—
upgradeable_proxyAn unverified token's code can be replaced by its admin.Do not hold long. Today's behaviour is not a promise about tomorrow's.—
lp_unlockedThe LP was measured, and less than half of it is burned or locked.The liquidity can leave.security.burned_pct, security.locked_pct, earliest_unlock_at

On lp_unlocked, read Spot a rug before it pulls for how the four security fields relate: burned beats locked, locked-with-a-date beats unlocked, and a lock expiring tomorrow is not a lock.

What the market did

These describe events rather than properties, so their weight falls as the event ages and rises when it repeats. One is noise. Three, or one that keeps returning, is a shape.

KindWhat was observedWhat to doVerify with
liquidity_dropDepth fell well below where it recently sat — in one step, or as a slow bleed below its 24-hour peak.The slow shape is the one that hides. Compare against the peak, not against yesterday.liquidity_usd, liquidity_range
wash_tradingVolume that moves no price, from few makers.The volume you are looking at is not depth you can exit through./pairs/{address}/trades — count distinct maker_address; txn.buyers vs txn.count
price_anomalyThe price here is not a price anyone paid: a swap moved the pool further than its own execution justified, or the pool disagrees with what the same tokens fetch elsewhere.Do not anchor on the last price, and check depth before sending a market order. It measures the size of the move, not anyone's intent — a thin pool produces it honestly./pairs/{address}/candles, the same token's other pairs

What we could not measure

Not claims about the pair — statements about our own coverage. They still belong in your decision, because trading on a figure we could not confirm is your risk, not ours.

KindWhat was observedWhat to doVerify with
data_unconfirmedA figure on this pair could not be confirmed on chain: the reserves, or a trustworthy USD price for one of the two tokens.Treat liquidity and price as indications, not values. Do not compute PnL from this response.liquidity_onchain_usd, missing price fields

In the vocabulary, not on a pair

KindWhere it actually appears
clone_spamA deployer-level finding. It reaches you through deployer.reputation, never in a pair's risk_signals.

Using it

Set your own threshold, server-side. min_score and max_score filter the list before it is sent, which is cheaper than filtering after:

curl -G 'https://api.preview.avee.tech/api/v1/chains/bsc/pairs' \
  -d min_score=70 \
  -d only_trustable=true \
  -d min_liquidity_usd=25000

But do not let the score be the whole gate. The bands (title) are a summary, and a summary loses the difference between one small reason and one serious one. Keep an explicit block list and check it whatever the score says:

const NEVER = new Set([
  'honeypot', 'rug_confirmed', 'confiscation_confirmed',
  'no_exit', 'privileged_transfer', 'forged_approval', 'sell_side_blocked',
]);

function tradable(pair) {
  if (pair.status === 'scam' || pair.status === 'unresolved') return false;

  for (const s of pair.score?.risk_signals ?? []) {
    if (NEVER.has(s.kind)) return false;
    if (s.kind === 'unknown') return false;   // a reason we have not published yet is still a reason
  }
  return pair.score?.value >= YOUR_THRESHOLD;
}

Read the reasons when the number moves. Sorted by current weight, the list is an ordered explanation:

curl -s 'https://api.preview.avee.tech/api/v1/chains/bsc/pairs/0x…' \
  | jq '.score | {value, title,
        why: [.risk_signals[] | {kind, weight, observed_at}] | sort_by(-.weight)}'

What these signals are not

They are a record of what was observed, with the field that carries the observation named next to each one. They are not a prediction, not a rating, and not financial advice. A pair with no signals is a pair nothing has been recorded against — which is a different sentence from "this pair is safe", and the difference is the whole point of publishing the reasons instead of only the number.

On this page