# Method

Everything below was run on 2 September 2026. Every figure quoted in the
accompanying report is produced by `scripts/analyze.py` and `scripts/farm_facts.py`
and stored in `numbers.json`; no number was transcribed by hand.

## 1. The question set

380 buyer-intent software and service categories (`scripts/cats300.py`, which
extends the 100-category list in `scripts/cats.py`). Categories were written
before any results were seen, chosen to span mainstream B2B software, developer
infrastructure, and vertical industry software, and never revised afterwards.

Each category was put to the model as one prompt (`scripts/orlib.py`):

    What are the best {category} in 2026? Answer ONLY with a JSON array of
    exactly 5 objects, ranked best first, each {"name": "<product name>",
    "domain": "<official website domain, no www or https>"}. No other text.

## 2. The models

Two web-grounded stacks, both reached natively through OpenRouter, both of which
return the list of URLs they retrieved:

- `perplexity/sonar`
- `perplexity/sonar-pro`

380 categories x 2 models = 760 calls. All 760 returned a parseable answer.
Total OpenRouter spend for the run, measured as the difference in account usage
before and after: **$23.77**.

Google Gemini was dropped. On OpenRouter, grounding a Gemini model requires the
`:online` suffix, which routes the query through OpenRouter's own web-search
plugin rather than Google's native grounding; the citations returned would
describe that plugin, not Google. No native grounded Google route returning
citations was available on the account, so **only Perplexity was measured** and
no claim in the report extends beyond it.

The two Perplexity tiers turned out to share a retrieval layer (see the report's
limitations section): they returned a byte-identical citation list in 289 of 380
categories. They are therefore reported as one search stack measured twice, not
as two independent stacks.

## 3. Domain enrichment

`scripts/enrich.py`:

- **Popularity.** Tranco top-1M daily list `K9QPW`, generated 2026-09-01,
  downloaded from tranco-list.eu. A cited domain is "unranked" when its
  registrable domain does not appear in that list at all. Registrable domain is
  approximated as the last two labels of the host, which is accurate for the
  .com/.org/.io-dominated set here and slightly over-merges a handful of
  country-code hosts.
- **Age.** Wayback Machine CDX API, first capture with HTTP status 200 for the
  domain (`matchType=domain`). "Never archived" means the CDX API returned no
  such capture.

## 4. Liveness of recommended vendor homepages

Every distinct domain the models supplied as a product's official homepage
(1,502 of them) was checked twice:

1. directly from the research host (`scripts/enrich.py`), and
2. through a rotating proxy, a fresh IP per request (`scripts/recheck_vendors.py`).

DNS was always resolved locally, since a proxy cannot change whether a name
exists. `scripts/merge_vendors.py` then merges the two passes conservatively: a
domain counts as reachable if **either** path reached it, so neither a blocked
research IP nor a blocked datacentre proxy IP is mistaken for a dead site. 32
domains were blocked on the proxy but fine directly and 3 the other way round;
both are corrected by the merge.

HTTP 403 and 429 are treated as bot-blocking, not as evidence of a dead site,
and are excluded from every "gone" count. Only absent DNS, no response on either
path, 404/410 and 5xx count as gone.

`scripts/redirect_check.py` follows every homepage that redirects to a different
registrable domain and tests whether the destination page still mentions the
recommended product. This is a weak test — an acquirer's landing page often does
not repeat the acquired product's name — so it is published as raw data
(`redirect_check.json`) and the report names only cases verified by hand.

## 5. Fetching

All web fetches other than the OpenRouter API calls went out through a rotating
proxy (`scripts/proxyfetch.py`; the proxy URL is read from a local file that is
not part of this dataset). The User-Agent throughout was

    Mozilla/5.0 (compatible; TrellnerResearchBot/1.0; +https://trellner.com/research)

Concurrency was capped at 16, timeouts at 10-45s.

## 6. Site-level evidence

Every page named in the report was re-fetched at write time
(`scripts/refetch_evidence.py`), with status, final URL and fetch time recorded
in a manifest. Page titles, meta descriptions, JSON-LD `ItemList` rankings,
whois creation dates and sitemap page counts are extracted from those saved
copies by `scripts/farm_facts.py`. Sitemap counts are of unique `<loc>` entries
across all sub-sitemaps.

## 7. Exclusions and known limits

- One prompt wording only. An earlier pilot found the shortlist is sensitive to
  the adjective used ("best" vs "most popular"); the citation-source mix was far
  more stable, but this run does not measure that.
- One run per category per model; no repeat sampling.
- Tranco rank is a popularity proxy, not a quality measure. It is used here only
  to separate the widely-visited web from the rest, and every claim about a
  specific site rests on that site's own pages, not on its rank.
- Registrable-domain grouping merges subdomains, so a citation to a vendor's
  documentation subdomain counts toward the vendor's domain.
- The three brands identified as one operation share a Cloudflare nameserver
  pair (`pam.ns.cloudflare.com`, `sean.ns.cloudflare.com`), a registrar
  (NameCheap), a page template, and a six-post blog on each site whose only
  subject is the other brands in the set plus a fourth, `zipdo.co`, on the same
  nameserver pair. That is strong circumstantial evidence of common control, not
  proof of ownership, and the report says so.
- `scripts/proxyfetch.py` fetches with TLS verification disabled, so a site with
  a broken certificate is not recorded as unreachable.
