# GAIP Agent Web Index: how to reproduce each figure

Public at `https://www.gaipagents.com/v1/free/agent-web-index/queries`. The script it describes is public at
`https://www.gaipagents.com/v1/free/agent-web-index/reproduce.py` (Python 3.9 or later, standard library only).
Method: section 2k of the Observatory method (`observatory-method-v1.md`). Figures are aggregates observed by
GAIP at the stated time; they assess no one. Licence: CC BY 4.0.

## What you need (all public, no key)

| Input | URL | Used for |
|---|---|---|
| The month's index | `/v1/free/agent-web-index?month=YYYY-MM` | every figure, its weekly parts and the registry-size block |
| Weekly snapshots (optional) | `/v1/free/ecosystem/snapshots?week=YYYY-Www` | cross-checking the registry walk counts (a) and (b) |

```
curl -s 'https://www.gaipagents.com/v1/free/agent-web-index?month=2026-10' > index.json
curl -s 'https://www.gaipagents.com/v1/free/ecosystem/snapshots?week=2026-W44' > w44.json
curl -s 'https://www.gaipagents.com/v1/free/agent-web-index/reproduce.py' > reproduce_agent_web_index.py
python3 reproduce_agent_web_index.py index.json --snapshot w44.json
```

The script prints a JSON report with `MATCH`, `MISMATCH` or `NOT_REPRODUCIBLE` for each figure and exits 1 if any
figure differs from what was published.

## The weekly totals format

Each of figures 1, 2, 3 and 5 has two populations: `all_agents` (the headline) and `panel` (the frozen panel
`AGENTS-COHORT-2026-10`, shown beside it). Each population has:

```
"value": {"count": 9, "of": 15, "share": 0.6},
"denominator": "plain-English statement of what is counted in 'of'",
"weekly_totals": {"numerator_by_week": {"2026-W41": 5, "2026-W42": 6},
                  "denominator_by_week": {"2026-W41": 10, "2026-W42": 12}}
```

The weekly parts split the month's hosts or agents into ISO weeks (weeks belong to the month of their Monday), so
they add up exactly to the month's count and total:

| Figure | Numerator part for week W | Denominator part for week W |
|---|---|---|
| 1 `tools_changed` | agents whose first classified tool change in the month was observed in W | not split: the published `of` (public agents GAIP watched when the copy was built) |
| 2 `terms_or_prices_changed` | hosts first read in the month in W whose declared terms, limits or x402 prices changed in the month | hosts whose declared terms were first read in the month in W |
| 3 `signed_agent_cards` | hosts whose latest agent card read in the month was in W and carried a signature | hosts whose latest agent card read in the month was in W |
| 5 `ai_preference_adoption` | agent hosts whose latest read in the month was in W and carried any AI-preference signal | agent hosts whose latest read in the month was in W |

Rules: a part is published only when every part of that figure (numerator and denominator) is at least 5;
otherwise all of that figure's parts read `{"withheld": "a weekly part is under 5"}`, so no number under 5 can be
worked out by subtraction. A week with no part is left out.

## Each figure: what the script does

1. **Agents whose declared tools changed** (`tools_changed`). Count = sum of `numerator_by_week`. Total = the
   published `of`. Then the cell rule (below) and `share = round(count / total, 4)`.
   *Not reproducible from outside:* which agents changed (the change log is public per agent at
   `/v1/free/observatory/changes`, but the all-agents count also depends on which agents were watched when the copy
   was built, which is not published agent by agent).
2. **Hosts whose declared terms, limits or prices changed** (`terms_or_prices_changed`). Count and total = sums of
   the two weekly maps; same cell rule. `of_which_x402_prices_changed` is not split by week and is checked only
   against the cell rule.
3. **Agent cards carrying a signature** (`signed_agent_cards`). As figure 2.
4. **Most frequent failure types** (`failure_types`). The script recomputes each share from the published counts
   and checks the order. *Not reproducible from outside:* the monthly counter row behind it is private (only its
   SHA-256 head is published in the daily anchor), so the counts themselves cannot be recomputed.
5. **Hosts carrying an AI-preference signal** (`ai_preference_adoption`). As figure 2. `of_which_ietf_aipref` is
   checked only against the cell rule.

**Cell rule.** A count is shown only when it and its total are at least 5; otherwise
`{"count": null, "of": <total, or null under 5>, "share": null, "withheld": "under 5"}`.

**What the weekly parts prove, and what they do not.** They let anyone check that each published figure is the
sum of its published parts under the published rules. The parts themselves come from host-level rows, which GAIP
does not publish (no public per-site list), so nobody outside GAIP can recompute the parts from raw data. When a
part is under 5 the figure is reported `NOT_REPRODUCIBLE`, not as a match.

## The registry-size block

`sample_frame.official_mcp_registry` gives four numbers, each with its own definition:

- **(a) `entries`**: server entries the Official MCP Registry listed across every page of GAIP's last complete walk
  of it, with `walk_completed_at_utc`. The walk asks for the latest version of each server, so each server name
  counts once; package-only servers count too.
- **(b) `remote`**: of (a), entries with a remote endpoint GAIP can read (a streamable-http or SSE remote with a
  public https URL), counted in the same walk.
- **(c) `watched`**: Official MCP Registry servers GAIP watches when the copy was built (active in the registry,
  inside the 50-per-host cap, not opted out).
- **(d) `read_last_7_days`**: of (c), servers with an observation attempt in the last 7 days.

`reconciles` is true when (d) <= (c) <= (b) <= (a); when it is false, `reconcile_note` says which step fails and
why (for example: GAIP watches servers admitted since the last complete walk ended, so the registry has grown since
(a) and (b) were counted). While no complete walk is recorded, (a) and (b) are null and `walk_in_progress` gives the
entries read so far: a lower bound, not the registry's size. The script checks the inequality against the
published flag and, given a weekly snapshot, compares (a) and (b) with the snapshot's
`market_coverage.registry_total_seen["official-mcp-registry"]`.

**Two numbers that are not the registry's size.** `sample_frame.panel.mcp_servers_from_official_mcp_registry`
(3,629 on 2 October 2026) is how many of the frozen panel's members came from the Official MCP Registry. The
Observatory summary's watched count by source (9,847 MCP servers on 2 October 2026) is (c). Neither is (a). When
the summary was read at 18:39 UTC on 2 October 2026 no complete walk of the Official MCP Registry had been recorded
yet, so (a) was not known from GAIP's own walk. Counts of the registry published elsewhere may count differently
(for example every version of a server rather than the latest) and are not GAIP's figures.
