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.
| Kind | What was observed | What to do |
|---|---|---|
honeypot | A simulated sell did not go through. Buying works, selling does not. | Never route to it. Do not display it as tradable. |
rug_confirmed | The pair's own deployer removed the liquidity from a pool that had been trading. | Nothing to trade against — the pool is gone. |
confiscation_confirmed | Holders, 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
| Kind | What was observed | What to do | Verify with |
|---|---|---|---|
no_exit | The 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.
| Kind | What was observed | What to do | Verify with |
|---|---|---|---|
deployer_flagged | The 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_rugged | The 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_delegated | The 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_funder | The 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.
| Kind | What was observed | What to do | Verify with |
|---|---|---|---|
privileged_transfer | An 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_approval | A 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_blocked | The 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_tax | Buy 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_escalation | The 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_restricted | An 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_concentrated | The 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_mint | The supply of an unverified token in the pair can still grow. | Do not hold long. | — |
upgradeable_proxy | An 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_unlocked | The 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.
| Kind | What was observed | What to do | Verify with |
|---|---|---|---|
liquidity_drop | Depth 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_trading | Volume 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_anomaly | The 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.
| Kind | What was observed | What to do | Verify with |
|---|---|---|---|
data_unconfirmed | A 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
| Kind | Where it actually appears |
|---|---|
clone_spam | A 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=25000But 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.
Spot a rug before it pulls
The fields that answer "is this pair real", and how to weigh them.
Operations payable with x402 and their prices GET
Every operation a keyless caller can pay for once its budget is spent, with the networks, assets and atomic amounts it accepts, in the x402 discovery shape. Empty where x402 is switched off. Fields are camelCase because that is the x402 wire format.