> ## 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.

# How the token score works

> The formula, data coverage, and limits behind Otto's 0-100 token market-structure score.

`/token-score` produces a deterministic 0–100 description of a Base token's current market structure. The response includes every raw input, sub-score, weight, breakpoint, and read time needed to reproduce the result.

<Warning>
  This is not a contract-security check. It does not test honeypots, mint authority, transfer taxes, blacklists, or upgradeable proxies. Use [`/token-security`](/acp-swarm/x402) for those checks. A high token score does not establish that a contract is safe.
</Warning>

The score does not predict returns, compare a token with its peers, or recommend buying, selling, or holding. Its `band` is only a label for the numerical result.

## Input

The endpoint accepts a Base ERC-20 contract address: `0x` followed by 40 hexadecimal characters. An invalid key is rejected uncharged.

The address is the identity shared by the price, pool, and holder reads. Symbols are not accepted because the same symbol can resolve to different deployments on different chains. Use `/token-price` for a symbol lookup.

## Formula

Each raw input is mapped to a 0–100 sub-score by linear interpolation between the breakpoints returned in `formula.factors`. Values beyond the first or last breakpoint are clamped. The composite is the weighted mean of the available sub-scores, with weights renormalized over the factors that resolved.

| Factor                | Weight | Data plane | Measurement                                                                             |
| --------------------- | -----: | ---------- | --------------------------------------------------------------------------------------- |
| `liquidity`           |   0.24 | pools      | USD liquidity summed across matching DEX pools                                          |
| `holderConcentration` |   0.18 | holders    | Supply held by the ten largest addresses, inverted so lower concentration scores higher |
| `holderBase`          |   0.14 | holders    | Number of holder addresses                                                              |
| `tradingActivity`     |   0.14 | price      | 24-hour volume in USD                                                                   |
| `marketMaturity`      |   0.14 | pools      | Days since the oldest matching pool was created                                         |
| `venueBreadth`        |   0.08 | pools      | Number of matching DEX pools                                                            |
| `priceStability`      |   0.08 | price      | Absolute size of the 24-hour price move                                                 |

The weights are a product judgement, not the result of a backtest. They travel with every response so a consumer can apply different weights to the published sub-scores.

### Liquidity and price movement

The liquidity factor sums liquidity only for returned pools whose token address matches the requested contract. The value is a **floor**, because the pool search returns one page rather than every pool in existence. `context.poolsReturned` and `context.matchedPoolCount` show the coverage. The separate `context.liquidityUsd` field is the price plane's top-pool value and is not scored.

Price direction is also not scored. `priceStability` uses the absolute 24-hour move so a rapid rise does not receive a higher score merely for moving upward. The signed move remains available in `context.priceChange24hPct`.

## Missing data and withholding

Every factor remains in the response even when its input is missing:

| `absenceReason`       | Meaning                                                    |
| --------------------- | ---------------------------------------------------------- |
| `plane_unavailable`   | The underlying price, pool, or holder read did not answer. |
| `field_not_published` | The read answered but did not publish that field.          |

An absent factor has `present: false`, `score: null`, its nominal weight, and its data plane. A factor from a successful read also carries `planeReadAt`. No missing factor is replaced with a neutral value.

`coverage.weightCoverage` is the share of nominal weight that resolved. If it is below `0.5`, the endpoint returns:

```json theme={null}
{
  "score": null,
  "band": null,
  "scoreWithheldReason": "insufficient_factor_coverage"
}
```

The available sub-scores still return. A complete pool plane carries 0.46 of the weight, the holder plane 0.32, and the price plane 0.22. The 0.5 floor prevents any one plane from producing the composite; complete data from any two planes clears it. No data plane is unconditionally required.

## Bands

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

## Two fields that need context

**Holder concentration includes contracts.** Top addresses can include liquidity pools, burn addresses, bridges, staking contracts, and treasuries. The factor measures concentration; it does not identify insiders.

**Turnover is returned without a score.** `context.turnover24h` divides 24-hour volume by pooled liquidity. High turnover can reflect demand or wash trading, and these inputs do not distinguish the two.

Inputs come from public DEX data and Otto's licensed holder-data provider. The response's `formula` block is the authority for the exact calculation used on that read.
