The instrument

Telling an agent fetch apart from a human visit.

Every page view on the demonstration hotel site is classified at the edge and published to the live stream with the evidence the classification rests on. This page is the part you can try.

Two things about that stream before you start. It shows page views only — assets, /api/* and robots.txt are not records, because one page view drags a dozen subresources behind it. And it opens showing only an assistant acting on behalf of a person; training crawlers, forged claims and ordinary human traffic are one toggle away rather than mixed in.

What this demonstrates. Whether an assistant fetched the site, which surface it came from, and whether that identity is cryptographically proven, circumstantially matched, or merely claimed. Whether a human arrived through an in-app browser. And — the case most tooling gets wrong — that an unverifiable claim is recorded as unverifiable rather than counted.

What it cannot demonstrate. That citation happens. A new property with no inbound links and no index presence will not be surfaced spontaneously by any assistant. Handing an assistant this URL triggers a fetch, which proves detection works when traffic arrives — it proves nothing about whether the property would ever be recommended.

Ask an assistant to open a page

Measured

One click each — the prompt is already written against /rooms/ocean-room, so there is nothing to copy and nothing to phrase. It asks for the rate, the bed and the footer's disclosure rather than a summary, because a summary can be answered from memory, and an answer with no fetch behind it is the failure that looks like success. Open the stream in a second window first.

Gemini and Le Chat take no prompt in a link, so those copy it and open the app — paste, then press Enter. Only Perplexity sends by itself; the rest wait for you. Grok and Le Chat will not appear in this default view — no catalogued agent and no published range respectively. Each URL carries a vsb_test tag so you can find your own fetch.

The fetch arrives from the operator's own infrastructure with a declared user-agent, and lands in the stream's default view — a real assistant reading a real page for a real person is exactly what that view is for. Identity is circumstantial via published IP range, because these agents do not sign; only Google-Agent does, and only on a subset of requests.

The two marked otherwise above are the more interesting result. Le Chat declares an identity Mistral publishes no range for. Grok is not in the agent registry at all, so it lands in the human baseline carrying an ALERT note — the second item under “what cannot be measured” below, reproducible on demand.

Arrive the way an assistant's answer sends you

Real arrivals

The launcher above tests the agent. This tests the person it sends afterwards. When an assistant prints a link in an answer, it sometimes appends its own origin to the end and sometimes does not — and whether it does is the entire desktop half of the referral registry below. These are the same page, the same browser and the same person; the only thing that changes is the parameter on the end.

Nothing here is simulated. Each is an ordinary link, and clicking it is a real navigation that reaches the hotel's edge, is classified there and publishes a genuine record — so open the stream in a second window first, and look behind Show all traffic: a human arrival is not an assistant fetch, so the default view is not where it lands. Each link carries a vsb_test tag so you can find your own.

Every one of these is ESTIMATED, and the gap is the honest part. The session is measured — someone really did arrive. Attributing it to an assistant rests on a parameter that assistant chose to append, which is a model-side decision that can change overnight with no notice. It yields a floor, never a count.

Clicking several in a row will not do what you expect. The hotel writes its source cookie only when there is not one already, so the whole session stays attributed to whichever you clicked first — deliberate, so that a visitor's second pageview cannot erase the assistant arrival that brought them. Each individual record still classifies correctly; it is the carried source that sticks. The cookie lives on the hotel's origin and this page cannot clear it, so a private window is how to start over.

Send a request your browser will not send

Simulated

Everything above this point was real. Nothing below it is. No browser will identify itself as an app build, arrive from inside an operator's published range, or attach a signature header — so these are classified as a shape: the record is worked out and handed back, and it is never published. The two are kept in separate sections for the reason the stream states plainly, that a simulated record sitting among measured ones would be indistinguishable from them.

This is where the in-app browser registry becomes something you can watch happen rather than a table of patterns. One thing it is not: sending a string we guessed proves nothing about what the app sends. Every row in that registry stays provisional until someone opens the page from inside the real app on a real device, and no button on this page can do that.

In-app browsers

The mobile half of Stage 3. Deterministic, because the string is fixed in an app build rather than decided per response by a model — and provisional, because none of these patterns has been confirmed against a live build here.

Referrers

The cases from the section above that a link cannot produce: a browser will not let a page choose the Referer header it sends.

Agent fetches

What the launcher buttons produce, without waiting for an assistant to co-operate. The addresses come from the committed range snapshot, so a row cannot claim to be inside a published range after the operator has moved it.

Claims that cannot be checked

A forged user-agent, a signature with nothing behind it, and a consumer browser on a datacentre address. Recorded in full, counted nowhere — the state most tooling does not have, and quietly counts as AI traffic instead.

Reference data, and how old it is

Range matching is only as good as the list behind it, and the failure runs one way: a stale list silently reclassifies real agents as human. Two dates matter and they are not the same — when we fetched the list, and when the operator last regenerated it. The second is the one that can hurt you.

5 of 9 operator lists are more than 60 days old. Not a bug in this deployment — the operators have not regenerated them. Range matching against a stale list undercounts, so every figure derived from it is a floor.
OperatorAgentsPrefixesOperator's list ageSource
OpenAIGPTBot21351 daysopenai.com/gptbot.json
OpenAIOAI-SearchBot35287 daysopenai.com/searchbot.json
OpenAIChatGPT-User20462 daysopenai.com/chatgpt-user.json
AnthropicClaudeBot / Claude-User / Claude-SearchBot2658 daysclaude.com/crawling/bots.json
PerplexityPerplexityBot8615 dayswww.perplexity.com/perplexitybot.json
PerplexityPerplexity-User4364 dayswww.perplexity.com/perplexity-user.json
GoogleGooglebot and common crawlers31550 daysdevelopers.google.com/static/crawling/ipranges/common-crawlers.json
GoogleGoogle special crawlers27050 daysdevelopers.google.com/static/crawling/ipranges/special-crawlers.json
GoogleGoogle user-triggered fetchers (incl. Google-Agent)49450 daysdevelopers.google.com/static/crawling/ipranges/user-triggered-fetchers-google.json

Snapshot fetched 27/08/2026, 10:00:13 am. Refresh with pnpm ranges:refresh.

In-app browser registry

This signal is deterministic — the string is fixed in an app build, not decided per response by a model — which makes it the strongest human-arrival signal in the design. The obligation that comes with it is a per-app, per-version validation record. Rows marked provisional have not been confirmed against a live app build here, and any figure derived from them is estimated rather than measured until they are.

AssistantPlatformStatusValidated againstBasis
ChatGPTiosprovisional—Pattern from third-party reports of the iOS app's embedded browser. NOT confirmed against a live app build here — run the probe from the app to promote this row.
Perplexityanyprovisional—Pattern from third-party reports. NOT confirmed against a live app build here.
Grokiosvalidated1.4.28Observed 2026-08-28 from a live build, opening a link from inside the Grok iOS app: Mozilla/5.0 (iPhone; CPU iPhone OS 26_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5.1 Mobile/15E148 Safari/604.1 GrokApp/1.4.28. Unlike the Gemini app this one keeps the full Safari token and appends GrokApp, so before the row existed it did not look anomalous at all — it classified as an ordinary human session and was absorbed into the direct baseline. That is the quieter of the two failure modes and the one A5 warns about.
Geminiiosvalidated1.2026.3270601Observed 2026-08-28 from a live build, opening a link from inside the Gemini iOS app: Mozilla/5.0 (iPhone; CPU iPhone OS 26_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 GoogleWv/1.0 (WKWebView) GeminiiOS/1.2026.3270601. Two tokens are present and only one is useful: GoogleWv is Google's shared WebView wrapper and says a Google app, not which one; GeminiiOS names the app and carries its build. Note there is no Safari or Chrome token at all, which is why this arrived classified as tooling until the row existed.
Google (Search app / Gemini surfaces)iosprovisional—GSA = Google Search App, the iOS in-app browser. Identifies the Google app, NOT specifically Gemini — an answer surface cannot be inferred from it.
Microsoft Copilotanyprovisional—Pattern from third-party reports. NOT confirmed against a live app build here.

Known gaps in this signal

  • Android Custom Tabs are indistinguishable from Chrome. An Android app that opens links in a Chrome Custom Tab sends Chrome's own user-agent with no app marker. If an assistant uses Custom Tabs rather than an embedded WebView, those arrivals land in the human baseline and cannot be recovered.
  • Desktop is not covered by this signal at all. This method is mobile-app-only. Desktop arrivals from an assistant depend on referral parameters, which are a much weaker signal (Stage 3, second half).
  • An app version we have not validated may not match. When an app ships a new build with a changed string, matching silently drops to zero. That is why an unrecognised assistant-shaped pattern raises an alert rather than being discarded.

Referral parameter registry

The desktop equivalent, and much weaker. Whether a link carries a parameter is a model-side decision that can change overnight with no notice. Every row here is provisional, which is the honest state: this yields a floor, never a count, and it needs a periodic re-verification job so a drop in reported traffic can be told apart from a change in assistant behaviour.

AssistantParameter values watchedStatusLast verified
ChatGPTchatgpt.com, chatgpt, openaiprovisionalnever
Perplexityperplexity, perplexity.aiprovisionalnever
Geminigemini, gemini.google.com, bardprovisionalnever
Copilotcopilot, bing-copilotprovisionalnever
Claudeclaude, claude.ai, anthropicprovisionalnever

What cannot be measured at all

Stated plainly, because the alternative is filling these with guesses.

  1. Real-user citation exposure. A user asks a comparison question, the assistant names the hotel from its index, and the user never clicks. Nothing touches the property's infrastructure — no log line, no referrer, no session. There is no detection path from the hotel's side.
  2. Agent-mediated browsing in the user's own browser. A browser extension acting on the user's behalf sends requests from a residential address, a real browser, and authentic device fingerprints. The address-class heuristic does not apply, because the residential address is genuine. This traffic lands silently in the human baseline — which is what the Grok button above reproduces on demand.
  3. Per-booking attribution without passthrough. Reconciliation produces a defensible interval for a period. It cannot say that a specific reservation came from a specific assistant, and no per-reservation source field appears anywhere, because a column like that would be read as fact.
Every known error runs the same direction: we undercount AI. Operators evading declaration, stale range lists, agent-mediated browsing, referral coverage gaps — all of them undercount. The errors compound rather than cancel, which means our figures are a floor. That is the honest direction to err in, and a client should be told rather than left to assume it.