# Method — ERC-8004 registry endpoint-gap dataset (N=577, 2026-09-15)

Companion to `dataset.csv`. Documents sampling design, provenance, budget, temporal
coverage, licensing, and explicit limitations. Written so a third party can
reproduce the collection given only the public 8004scan.io API surface.

## What this measures

**The 8004scan.io index layer as it stood on 2026-09-15.** Not the on-chain
registries directly, not the metadata authors' intent, not any downstream
buyer-agent's ability to actually reach a listed endpoint. The specific question is:
of the five endpoint fields the 8004scan `/api/v1/agents` response exposes per
record — `services`, `capabilities`, `agent_url`, `a2a_endpoint`, `mcp_server` —
how often is each null?

That is the field a buyer-agent following the ERC-8004 → 8004scan discovery path
would see, so it is the meaningful measurement even though it inherits the index's
parsing choices. The distinction matters for the C2 claim: "null on 100 %" here
means "null in 8004scan's parsed view", not "the metadata document behind the
tokenURI has no such field". See § "Limitations" for what this dataset cannot say.

## Population

The ERC-8004 identity NFTs indexed by 8004scan.io on five mainnet chains:

| Chain | `chain_id` | Total records reported by index at sampling time |
|---|---:|---:|
| Ethereum | 1 | 30,808 |
| Base | 8453 | 61,648 |
| BNB | 56 | 319,619 |
| Optimism | 10 | 531 |
| Arbitrum | 42161 | 1,419 |

BNB dominates the population by ~76 %. Testnets (Sepolia, Base Sepolia, etc.) are
excluded — mainnets only.

Population totals are read from the `total` field of the same list responses used
for sampling (see `summary.json.source.list_urls_used`).

## Sampling design

**Temporal stratification, not recency head.** For each chain: five uniformly-spaced
`created_at` anchors between the chain's oldest and newest indexed record
(`anchor_frac` = 0.00, 0.25, 0.50, 0.75, 1.00), 25 records per anchor slice,
deduplicated on `(chain_id, token_id)`. Target N per chain = 125, total target = 625.

Anchors 0.00–0.75 use forged `cursor` payloads that place the slice at the target
timestamp; anchor 1.00 uses the default `sort_order=desc` head. Both forms use
`sort_by=created_at`.

### Why stratify

A recency-only head-sample (task 0616, N=176) is biased toward the most recent
mint activity, which for ERC-8004 in 2026 is drift-heavy: many placeholder mints,
lower average `total_score` (0616 median: 0.0). Uniform temporal stratification
across the full 229-day span the index covers gives a more defensible picture of
the population. The C2 held-vs-failed comparison against 0616 is documented in
`summary.json.delta_vs_0616`.

### Cursor forging

The 8004scan API paginates by base64-encoded JSON cursors of the form
`{"v":1,"sort_by":"created_at","sort_order":"asc","scope":"...","value":"<ISO timestamp>","id":"...","total":N}`.
For asc-sort anchor slices, the sampler constructs a cursor with `value` set to the
target timestamp for that anchor (linear interpolation between the chain's oldest
and newest `created_at`) and a placeholder `id` of all zeros. The API accepts the
forged cursor and returns records at or after that timestamp. The `scope` per
chain is discovered by making a preliminary `limit=3&sort_order=asc` request and
copying its `scope` field.

This is the only way to page past the head — see § "Cursor forging is the only
paging mechanism" below.

### Deduplication

Adjacent anchor slices can overlap when a chain has few records near an anchor
timestamp (e.g. Optimism at 0.75). Rows are deduped on `(chain_id, token_id)` after
collection. This is why Optimism's final N is 77, not 125: the 0.75 anchor slice
overlapped the 0.50 slice heavily and only added 2 new unique rows after dedup.

## Collection window

- **Sampled on:** 2026-09-15 (single collection run, ~7.5 minutes wall)
- **Age of records in the final sample:** 2026-01-29 → 2026-09-15
- **Span:** 229.19 days
- **Median record age (created_at):** 2026-05-26

Per-stratum age min/max are in `summary.json.age_per_stratum` — every stratum's
records fall within a narrow band around its anchor timestamp, confirming the
cursor-forging strategy worked.

## Budget

From `summary.json.bounds`:

- **Max GET budget:** 750 → **used 613**
- **Min spacing between GETs:** 300 ms
- **Per-request timeout:** 15 s
- **Wall-time budget:** 900 s → **used 451.3 s**
- **Blocked hosts:** none

613 GETs across five chains covers the ~30 list requests needed (3 warm-up + 5
paginated anchor slices × 5 chains, some retried for pagination continuation) plus
the ~577 individual record detail lookups the analysis pass performed.

## 8004scan API quirks the sampler relies on

Both established in `docs/research/0616-registry-endpoint-gap/` and re-relied-on for
0617. Any reproduction of this dataset must know both.

### `chain` is silently ignored — use `chain_id`

`/api/v1/agents?chain=base` returns HTTP 200 with a body that looks like a filtered
list but is actually the unfiltered global listing. Only the numeric
`chain_id=8453` form filters correctly. There is no error surfaced by the API when
`chain` is sent — a scraper that trusts the "chain" name will produce a
mislabelled sample.

### Cursor forging is the only paging mechanism

The list endpoint accepts and silently ignores all of these paging parameters:

- `sort=created_at` (must be `sort_by=`)
- `order=asc` (must be `sort_order=`)
- `offset=N`
- `page=N`
- `skip=N`
- `start=N`

The API responds HTTP 200 to each but returns the same head slice. The only way to
page past the head is via the `cursor` parameter it returns in each response body
(and which this sampler forges to target specific `created_at` anchors).

Both quirks are documented in more detail in `DATA-DICTIONARY.md` § "API quirks a
third party reproducing this MUST know" and are called out because they are
themselves useful citable facts about the 8004scan public API surface — reproducing
the dataset is meaningfully harder if a third party has to rediscover them.

## Pre-pinned claims (registered before final scoring)

Three categorical claims, each with a numerical bar, registered before the final
N≥500 sample was drawn (see `summary.json.prepinned_claims`):

- **C1** *(threshold, FAILED)* — `endpoint_less_pct >= 60 AND median_total_score <= 15`.
  Actual: `endpoint_less_pct = 43.85 %`, `median_total_score = 12.02`. The
  endpoint-less rate came in well below the bar; the median-score half of C1 held
  in isolation but that is not what the claim was.

- **C2** *(categorical, HELD)* — `capabilities AND agent_url null on 100 % of the sample`.
  Actual: `capabilities_null = 100.00 %` AND `agent_url_null = 100.00 %`. Bar met on
  both. This is the headline claim carried by the post.

- **C3** *(categorical, FAILED)* — `is_endpoint_verified true on 0 records`.
  Actual: 1 record verified — Arca on Arbitrum (`chain_id=42161, token_id=10`). The
  bar was zero; a single record falsifies it.

Overall grade: **CONFIRMED** — one categorical claim (C2) held with hard zeros in
the value columns, the two other claims failed and are named. Publishing the
failed claims alongside the held one is what makes the held one credible: this is
a pre-registered result, not a post-hoc pattern-match.

## Provenance & known limitation (added by task 0624, 2026-09-16)

**This dataset is drawn from a single upstream source (`8004scan.io/api/v1/`)
that does not re-parse agent cards on any observed cadence.** Task 0623
established with a mechanical, pre-pinned rule that 8004scan's parser fires
only at mint-time (a mint-event listener that timestamps `last_parsed_at`
between 0.2 and 2.0 minutes after mint) plus a single 2026-06-06 06:03:08 to
06:05:41 UTC bulk pass across all pre-existing agents. No `?refresh=1`
mechanism, no `/refresh`, `/rescan`, or `/reparse` sub-path exists on the API.
The index has therefore not re-read any agent card in 101 days (as of
2026-09-15) and its captured columns reflect each agent's metadata as it stood
at mint (or on 2026-06-06 for pre-existing agents), NOT as those agents
currently publish.

Task 0624 then took the pre-registered C2 claim ("capabilities AND agent_url
null on 100 % of the sample") and source-verified it: N = 43 rows drawn from
this dataset with 2 rows per (chain, anchor-frac) bucket where available, 18
of which resolved to a valid JSON agent-card at the URL 8004scan had already
recorded in its `services[0]` field. **4 of those 18 live cards carried a
non-empty `capabilities` field** — falsifying the simple reading of C2. The 4
falsifying records are quoted verbatim in
`docs/research/0624-c2-provenance/README.md` (Otto AI on Ethereum, Normie #2613
and #1444 on Ethereum, Arca on Optimism).

**What this means for interpretation.**

- The 577 / 577 (100 %) null figure for `capabilities` and `agent_url` is
  correct **as a measurement of the 8004scan index snapshot on 2026-09-15**.
- It is *not* a correct measurement of what ERC-8004 agents publish. At least
  4 agents in the sampled subset publish a non-empty `capabilities` field at
  the exact URL 8004scan captured for them.
- The distinction matters because a buyer-agent that trusts the null value
  will not route work to those agents and will not see their advertised
  capabilities. Any downstream user of this dataset should treat the columns
  labelled `capabilities_null`, `agent_url_null`, `a2a_endpoint_null`, and
  `mcp_server_null` as **index-cached hints at unknown age**, not as ground
  truth about the underlying agent.
- What would falsify the statistic further: a source-verification pass across
  a much larger fraction of `dataset.csv` (all ~320 rows with a resolvable
  `services_first`), which would produce a rate estimate for how often the
  index diverges from the agents. 0624 did not do this — the 4 / 18 finding
  is sufficient to establish the artefact, not to size it.

## Limitations

**What this dataset cannot say.**

1. **Index vs on-chain.** This measures 8004scan's parsed view of registry
   metadata, not the metadata documents themselves. A `capabilities` field is
   known to exist inside the JSON at the tokenURI for at least 4 of the 43
   rows Task 0624 spot-checked, but not to be surfaced by the index. See
   "Provenance & known limitation" above.
2. **A single-index dependency.** If 8004scan changes its parser or its schema, a
   re-run of the same sampling design against the same population could produce a
   different result. Reproducing the dataset later means using the *same* index
   at the *same* point in time — the recorded request URLs in `summary.json` are
   the pinning mechanism.
3. **Sample is uniform-in-time, not weighted by chain population.** Ethereum, Base,
   BNB, and Arbitrum contribute 125 rows each; Optimism contributes 77. BNB is
   ~76 % of the underlying population by count. A population-weighted rate would
   put more weight on BNB, which has a higher endpoint-less rate (73.6 %) than the
   other four mainnets, and would produce a higher aggregate endpoint-less number.
   The uniform design is deliberate — it prevents one dominant chain from setting
   the shape of the whole sample.
4. **Not a callability test.** `endpoint_less` measures whether a URL is
   *published*, not whether it *answers*. `is_endpoint_verified` (1/577 true) is
   8004scan's separate probe result and is reported as-is. A full callability
   audit (fetch each published URL, record HTTP outcome, count answers) is a
   separate, non-cheap task.
5. **Testnets excluded.** Sepolia, Base Sepolia, and other testnets are not in
   this sample. Their inclusion or exclusion changes both the population size and
   likely the shape (testnet mints skew placeholder-heavy).
6. **C2 is falsifiable, not proven forever.** A next-day resample could find a
   single non-null `capabilities` or `agent_url` value and falsify the claim. The
   dataset pins the observation to a specific timestamp and sample — nothing
   more.

## License

**CC BY 4.0.** Attribution is the whole point of publishing this dataset.
Downstream users may reuse it commercially, redistribute it, and build on it,
provided they credit Onchain Agent Intel and link back to the canonical page.
That is a lower friction than CC BY-SA (share-alike) for the buyer-agent /
researcher audience we want citing this, and lower friction than a bespoke
"citation-required" license which would need explaining. The C2 statistic is not
copyrightable (it's a measurement), so the license attaches to the *dataset as
compiled and packaged* — the CSV, the JSON audit trail, the schema, and the
accompanying documentation.

## Reproduction

Given the URLs in `summary.json.source.list_urls_used` and this document:

1. Fire each list URL in order, spacing requests ≥300 ms apart, with a 15 s
   timeout each.
2. For each list response, collect records; from the paginated slices, also
   record the returned `cursor` to confirm the API is behaving.
3. For each unique `(chain_id, token_id)`, GET the detail URL
   `/api/v1/agents/{chain_id}/{token_id}` and copy the endpoint fields into the
   dataset row.
4. Deduplicate on `(chain_id, token_id)`.
5. Compute the derived columns per the definitions in `DATA-DICTIONARY.md`.

A reproduction that lands N ≥ 500 and finds `capabilities_null_pct = 100 %` AND
`agent_url_null_pct = 100 %` corroborates C2 for its own collection date. Any
non-null value in either column falsifies it.

## Companion files

- `dataset.csv` — the 577 rows
- `DATA-DICTIONARY.md` — every column, its source, its null semantics
- `summary.json` — full audit trail (URLs, cursor payloads, per-stratum age, deltas)
- `GRADE.md` — the three claims and the grade
- `dataset.jsonld` — schema.org `Dataset` metadata for the planned publication page
- `../../drafts/0617-erc8004-registry-endpoint-gap.md` — the post carrying the C2 headline
- `../../drafts/0619-dataset-ship-spec.md` — the not-yet-executed ship spec
