# How scoring works

A 0–100 score with a letter grade, built from six weighted categories of individual audits (rubric v2.1). Every audit returns a finding with evidence and a recoverable point value, judged against [llms.txt v2 (Aug 2026)](https://llmstxt.org/): the report reads as a work queue, not a verdict.

The work this measures goes by several names: **generative engine optimization** (GEO), **answer engine optimization** (AEO), **LLM SEO** and **AI readiness**. Google’s own term for the on-site half is **agentic browsing**, the Lighthouse category that checks a site’s `llms.txt`, its WebMCP surface and the accessibility tree agents read, deliberately without producing a score. They describe one thing, making a site legible to the models that now answer on its behalf, and this page is how we score it. The [audits](https://llmsrank.dev/docs/audits.md) are the individual checks behind the number.

## Principles

1. **Weight by consequence, not by effort.** An element’s weight reflects how much it changes whether an AI finds, trusts, and correctly uses your content, not how hard it is to write.
2. **You are scored only on what applies to you.** An audit that doesn’t apply (no API, no volatile data, unregulated vertical) is removed and its weight redistributed, never scored as a zero. Anything the scan couldn’t verify is likewise excluded, not assumed absent. A 100 must be reachable by a simple site.
3. **Critical failures cap the total.** A flawless file that no crawler may fetch is worth zero, and the score must say so rather than average it away.
4. **Every audit is actionable.** Each returns a finding with evidence and a recoverable point value.
5. **Versioned rubric.** Scores carry a rubric version; changes are announced. See [the changelog](https://llmsrank.dev/docs/changelog.md).

## Categories and weights

The **profile** is selected by the site’s industry: regulated verticals weight compliance higher, technical sites weight agent readiness higher. The profile is stated on every report.

| Category | Generic | Regulated | Commerce | Technical |
|---|---|---|---|---|
| Retrievability & Access | 15 | 15 | 15 | 15 |
| Spec Conformance | 10 | 10 | 10 | 10 |
| Curation Quality | 25 | 20 | 25 | 20 |
| Agent Readiness | 25 | 20 | 25 | 30 |
| Compliance & Trust | 10 | 20 | 10 | 5 |
| Ecosystem Integration | 15 | 15 | 15 | 20 |

## Grade bands

| Score | Grade | Meaning |
|---|---|---|
| 90–100 | A | Reference-quality. Publishable as an example. |
| 80–89 | B | Strong. Real gaps, none structural. |
| 70–79 | C | Competent content, meaningful gaps in plumbing or curation. |
| 60–69 | D | Present but underpowered — likely a generator’s output plus edits. |
| 0–59 | F | Missing, unreachable, or a link dump. |

## Critical failures (caps)

Applied before category math. The lowest applicable cap wins, and the report leads with it.

| Condition | Cap | Why | Status |
|---|---|---|---|
| `no_file`: No llms.txt covers the URL, at its path or any ancestor up to the root | 0 | No llms.txt covers the URL: none at its path or any ancestor path, up to the root. Reported as Not Present, not as a bad score. Applied upstream: the engine only runs on a file. | Defined, not yet applied |
| `self_blocking`: robots.txt blocks the AI crawlers from the covering llms.txt | 25 | robots.txt (RFC 9309) disallows the covering llms.txt for an AI reader, through its own group or User-agent: *. A file no agent may fetch has no visibility value. The single most expensive mistake available. | Applied today |
| `unreachable`: File behind auth, JS-rendered, or a cross-host redirect chain | 40 | Most fetchers will not follow it. | Defined, not yet applied |
| `no_h1`: Not parseable as markdown / no H1 | 30 | The one element the spec requires. Downstream tooling cannot consume it. | Applied today |
| `served_as_html`: Served as text/html with markup | 50 | Parses badly; many clients reject. | Defined, not yet applied |

## The three states

A site is never docked points for an element that doesn’t apply to it. Every audit resolves to one of three states; only the first affects the score.

| State | Meaning | Effect on score |
|---|---|---|
| Scored | The gate passed. This element is relevant to your site. | Scored 0.0–1.0 and counted. |
| Not applicable | The gate failed. The element is irrelevant here. | Removed; its weight is redistributed across the applicable audits in its category, and the report lists it with the reason. |
| Not verified | The element may apply, but the scan couldn’t determine it. | Removed from scoring and flagged. Never scored as absent. |

### Weight redistribution

```
applicable = [a for a in category.audits if a.state == Scored]
effective_weight(a) = a.weight / Σ(applicable weights) × 100
category_score       = Σ effective_weight(a) × a.score
```

If every audit in a category is removed, the category drops out and its profile weight is redistributed across the remaining categories by the same formula.

**Worked example.** Curation Quality’s audit weights add up to 100, among them [Signal-to-noise](https://llmsrank.dev/docs/audits/signal-to-noise.md) (weight 16), [Link descriptions](https://llmsrank.dev/docs/audits/link-descriptions.md) (weight 16) and [Curation, not a sitemap](https://llmsrank.dev/docs/audits/curation-not-sitemap.md) (weight 8), which applies only when the sitemap lists at least 50 pages. On a smaller site C6 is Not applicable and its 8 points are redistributed: C1 now carries 16 / (100 − 8) × 100 = 17.4 points of effective weight instead of 16, and C2 gains in the same proportion.

## Computing the total

```
for each category:
    applicable = [a for a in audits if a.state == Scored]
    if applicable is empty:
        drop the category; redistribute its profile weight
    effective_weight(a) = a.weight / Σ(applicable weights) * 100
    category_score = Σ effective_weight(a) * a.score              # 0–100

live_categories = [c for c in categories if not dropped]
for c in live_categories:
    effective_profile_weight(c) = c.profile_weight / Σ(live profile weights) * 100

total = Σ category_score * effective_profile_weight / 100         # 0–100
total = min(total, lowest_applicable_cap)
```

Every report also shows the qualified count, element coverage, a Not Applicable panel, an Unverified panel, top findings sorted by recoverable points, and the profile and rubric version.

## Recoverable points

```
recoverable_points(a) = (1 − a.score) × effective_weight(a) within its category × its category's effective profile weight
```

Summed across every applicable audit, these add up to exactly 100 minus the uncapped total. **Recoverable now** is stricter: how much the displayed, capped total would move if that one audit were fixed today. When a cap binds it is 0 for almost every audit, except the audit whose fix removes the cap ([Single H1](https://llmsrank.dev/docs/audits/single-h1.md) clears `no_h1`; [AI crawler access](https://llmsrank.dev/docs/audits/ai-crawler-access.md) or [Named AI crawlers](https://llmsrank.dev/docs/audits/named-ai-crawlers.md) clears `self_blocking`).

## Versioning

This page describes rubric **v2.1**. Scores carry their rubric version, and changes are announced rather than applied silently: see the [changelog](https://llmsrank.dev/docs/changelog.md). A rescan is needed to see a grade under a newer version.
