> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useotto.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Token Score — Methodology

> Exactly how /token-score computes its 0-100 composite, what each factor measures, and the rules for when the number is withheld instead of served.

`/token-score` returns one **0–100 composite** describing a Base token's market structure, computed
from data planes Otto already operates. It is deterministic: no language model is anywhere in the
path, so there is no narrative that can drift away from the numbers.

**The formula ships with every answer.** Each response carries a `formula` block containing every
factor's weight, the exact response field it read, and the full list of breakpoints its curve
interpolates between. Applying those breakpoints to the returned inputs reproduces the returned
sub-scores and the composite exactly. You never have to take the number on trust, and this page is a
description of that block — not a substitute for it.

## What it is not

<Warning>
  **This is not a contract-security screen.** It does not test for honeypots, mint authority, transfer
  taxes, blacklists or upgradeable proxies. A high score is **not** a statement that the contract is
  safe. That product is [`/token-security`](/acp-swarm/x402), and the two are complementary rather
  than overlapping.
</Warning>

It is also **not predictive and not a recommendation.** Every factor measures the token's market as
it stands right now. The score does not forecast price, does not rank tokens against each other, and
`band` is a plain label for where the number falls — nothing more.

## Why the key is a contract address

`/token-score` accepts a Base (chain 8453) ERC-20 contract address, `0x` followed by 40 hex
characters, and rejects a ticker symbol as malformed — uncharged.

That is a correctness rule, not a limitation. The holder plane is keyed on a bare address on a named
chain, while a symbol resolves through a canonical listing to whichever chain that asset's main
deployment lives on. Accepting a symbol would let one plane answer about a deployment on one chain
while another answered about a different one, and the composite would fuse two tokens into one score
with nothing in the response saying so. Address-keying makes all three planes provably describe the
same token. For symbol lookups, use `/token-price`.

## The seven factors

Each factor maps a raw input onto a 0–100 sub-score by linear interpolation between published
breakpoints, clamped outside the ends. The composite is the **weighted mean of the sub-scores that
resolved**, with the weights renormalised over exactly those factors.

| Factor                | Weight | Plane   | What it measures                                                                                     |
| --------------------- | ------ | ------- | ---------------------------------------------------------------------------------------------------- |
| `liquidity`           | 0.24   | pools   | USD liquidity summed across the DEX pools that quote this token                                      |
| `holderConcentration` | 0.18   | holders | Share of supply held by the ten largest holders — **inverted**, so lower concentration scores higher |
| `holderBase`          | 0.14   | holders | How many distinct addresses hold the token                                                           |
| `tradingActivity`     | 0.14   | price   | 24-hour traded volume in USD                                                                         |
| `marketMaturity`      | 0.14   | pools   | Days since the **oldest** pool holding this token was created                                        |
| `venueBreadth`        | 0.08   | pools   | How many distinct DEX pools quote this token                                                         |
| `priceStability`      | 0.08   | price   | The **absolute** size of the 24-hour price move                                                      |

**The weights are a judgement, and they are published as one.** They are not the output of a
backtest and no claim is made that they predict returns. Liquidity carries the largest single weight
because it is the only factor that bounds whether a position can be exited at all; the two holder
factors together (0.32) outweigh it because distribution is the property a market-structure score
exists to surface. They are returned on every response precisely so you can reweight the sub-scores
yourself.

### Two deliberate choices worth knowing

**Price direction is not scored.** The scored factor is the *size* of the 24-hour move, because a
score that rewarded going up would rank a pump above an established market. The signed change is
returned unscored as `context.priceChange24hPct`.

**Liquidity is read from the pools, not from the price plane.** Otto's `/token-price` publishes the
*top pool's* reserve, which is the right number for a price lookup and the wrong one here: a token
used mainly as a **quote** asset reports near nothing at the token level. Measured live on
2026-09-01, Base USDC came back with a top-pool liquidity of `0` while its matched pools held
\$26.6M. Summing the matched pools measures what this factor claims to measure. The price plane's own
figure still serves, unscored, as `context.liquidityUsd`.

The pooled figure is a **floor, not a total**: the pool search returns one page, so a token quoted
in hundreds of pools is summed over the pools it returned. `context.poolsReturned` and
`context.matchedPoolCount` publish that denominator.

## Absent factors are reported, never filled in

When a data plane does not answer, its factors come back with `present: false`, a `null` sub-score,
and an `absenceReason`. They are never defaulted to a neutral value and never quietly dropped from
the list, because a buyer could not otherwise tell a factor that was omitted from one that never
existed.

| `absenceReason`       | Meaning                                                                                                                  |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `plane_unavailable`   | That plane returned nothing at all.                                                                                      |
| `field_not_published` | The plane answered, but published no value for that field. `planeReadAt` still carries the read time — we *did* read it. |

`coverage.weightCoverage` is the share of the full weighting that actually contributed to the
number, and every present factor carries `planeReadAt`, the instant its own plane was read.

## When the composite is withheld

<Note>
  When the factors that resolved carry **less than half** the total weighting, the composite is
  withheld: `score` is `null` and `scoreWithheldReason` is `insufficient_factor_coverage`. Every
  sub-score that did resolve is still returned, with its own source and read time.
</Note>

The reason is that a "multi-factor composite" computed from a single data plane is a rename of that
plane, not a composite. The price plane alone carries 0.22 of the weight — below the floor — so a
token with neither a holder record nor an indexed pool returns its resolved sub-scores and no fused
number. The threshold is published on every response as `coverage.minWeightCoverage`.

The price plane is **required**: without it the token has no price, no liquidity and no identity, so
the call is refused **uncharged** rather than served as a partial answer.

## Score bands

`band` is a plain label for where the composite falls. It describes the *score*, not the token's
merit.

| Band        | Score   |
| ----------- | ------- |
| `very-high` | 80–100  |
| `high`      | 60–79.9 |
| `moderate`  | 40–59.9 |
| `low`       | 20–39.9 |
| `very-low`  | 0–19.9  |

## Two numbers to read carefully

**Holder concentration includes contracts.** Concentration is measured from the top holders as the
holder index reports them, and those addresses routinely include liquidity pools, burn addresses,
bridges, staking contracts and treasuries. A perfectly ordinary token can therefore look
concentrated. Read the factor as what it is — how much supply sits in a few addresses — not as a
claim about insiders.

**Turnover is returned unscored.** `context.turnover24h` divides 24-hour volume by pooled liquidity.
A very high turnover is consistent with real demand *and* with wash trading, and this data cannot
tell those apart, so you get the ratio rather than a verdict.

## Source and licensing

Every input comes from keyless public DEX data (GeckoTerminal / DexScreener) and a holder index Otto
already licenses — the same planes behind `/token-price`, `/pools-search` and `/holder-analytics`.
Nothing here is investment advice.
