BondiCasa

The instrument

Telling an agent fetch apart from a human visit.

Every page view on this site is classified at the edge and published to a 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.

Things to try

Each one below has been run against this deployment. Open the stream — it opens in a new tab — then work through them.

1 · Just load a page

Measured

You have already done this. Your own arrival is in the stream, classified as an ordinary browser session.

expect → browser · human-visit · counts as a session
The label says “human, or an agent we cannot detect”, because an agent driving your own browser would be indistinguishable from you.

2 · Forge an agent identity

Recorded, not counted

The case a sceptical reader will try, and the one most tooling silently counts. Claim to be ChatGPT-User from an address OpenAI does not publish.

curl -s -o /dev/null -A 'Mozilla/5.0 (compatible; ChatGPT-User/1.0; +https://openai.com/bot)' \
  https://www.bondi.casa/rooms/harbour-suite

That is a page view, so it appears in the stream — under Show all traffic, since an unproven claim is not AI acting for a person. To read the whole record as JSON, send the same header to /api/whoami, which classifies its caller and returns the record in the response body. It is deliberately not published: it is a diagnostic, not a page view.

curl -s -A 'Mozilla/5.0 (compatible; ChatGPT-User/1.0; +https://openai.com/bot)' \
  https://www.bondi.casa/api/whoami | jq '.classification | {actor, identity, metric}'
expect → unverified-claim · identity verdict unverified · RECORDED_NOT_COUNTED
The evidence trail shows the range check running and failing. The claim is kept in full; it enters no metric.

3 · Ask an assistant to open this page

Measured

Paste https://www.bondi.casa/rooms/harbour-suite into ChatGPT, Claude, Perplexity or Gemini and ask it to summarise the room. The fetch arrives from the operator's own infrastructure with a declared user-agent.

expect → declared-agent · purpose user-fetch · counts as exposure, not a visit
Identity is circumstantial via published IP range for most operators, because the fetch agents do not sign. Only Google-Agent signs, and only on a subset of requests.
This is the one trial that lands in the stream's default view — a real assistant fetching a real page on behalf of a real person is exactly what that view is for.

4 · Open the link from inside an assistant's mobile app

Estimated

Ask the app about this property on a phone, then tap through. The app's embedded browser identifies itself in the user-agent.

expect → in-app-browser · counts as a session · source attributed to the app
Tagged estimated rather than measured, because our pattern for that app is still provisional — see the registry below. It becomes measured once a real arrival validates it.

5 · Watch a training crawler get excluded

Recorded, not counted

GPTBot is a training crawler: real, verifiable, and excluded from attribution on purpose, because it carries no user. Run the command and you get the other lesson first.

curl -s -o /dev/null -A 'Mozilla/5.0 (compatible; GPTBot/1.2; +https://openai.com/gptbot)' \
  https://www.bondi.casa/journal

# and to read the record itself:
curl -s -A 'Mozilla/5.0 (compatible; GPTBot/1.2; +https://openai.com/gptbot)' \
  https://www.bondi.casa/api/whoami | jq '.classification.purpose, .classification.metric'
expect → unverified-claim · purpose unknown · RECORDED_NOT_COUNTED
Not the training exclusion — your address is not in OpenAI's published GPTBot range, so the claim fails before purpose is ever consulted. Purpose is only read off an identity that survived verification.

To see the exclusion itself, use the GPTBot preset in the simulator below, which supplies an address from inside the published range. It classifies as declared-agent · training · MEASURED · counts as nothing. Measured and excluded are different states: the record does not disbelieve a verified crawler, it declines to count it.

6 · Send a signature that does not verify

Recorded, not counted

Structurally valid Web Bot Auth headers with nothing behind them.

curl -s https://www.bondi.casa/api/whoami \
  -H 'signature-agent: "https://chatgpt.com"' \
  -H 'signature-input: sig1=("@authority" "@method" "@path" "signature-agent");created=1;expires=99999999999;keyid="nope";alg="ed25519";tag="web-bot-auth"' \
  -H 'signature: sig1=:AAAA:' | jq '.classification.identity, .classification.metric'
expect → identity unverified · RECORDED_NOT_COUNTED
The verifier fetches OpenAI's real key directory, finds no key that produces a valid signature, and says so. A directory it cannot reach is reported as unavailable instead — our gap, not evidence against the caller.

7 · Click through to the booking engine

Measured

Check availability, search, and watch the booking-intent record appear. Then look at what the engine received.

expect → booking intent with stay dates, party size and the source the edge assigned
The same page demonstrates the failure case: a cross-origin widget whose state genuinely cannot be read.

Classify a request shape without sending one

The same classifier, run against inputs you type. Useful for the cases you cannot produce from a terminal — an address inside an operator's published range, for instance. Results are not published to the stream.

  • 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.

    4 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
    OpenAIGPTBot21300 daysopenai.com/gptbot.json
    OpenAIOAI-SearchBot35236 daysopenai.com/searchbot.json
    OpenAIChatGPT-User20412 daysopenai.com/chatgpt-user.json
    AnthropicClaudeBot / Claude-User / Claude-SearchBot268 daysclaude.com/crawling/bots.json
    PerplexityPerplexityBot8565 dayswww.perplexity.com/perplexitybot.json
    PerplexityPerplexity-User4313 dayswww.perplexity.com/perplexity-user.json
    GoogleGooglebot and common crawlers3150 daysdevelopers.google.com/static/crawling/ipranges/common-crawlers.json
    GoogleGoogle special crawlers2700 daysdevelopers.google.com/static/crawling/ipranges/special-crawlers.json
    GoogleGoogle user-triggered fetchers (incl. Google-Agent)4940 daysdevelopers.google.com/static/crawling/ipranges/user-triggered-fetchers-google.json

    Snapshot fetched 27/08/2026, 2:45:44 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 below that say 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
    ChatGPTiosprovisionalPattern 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.
    PerplexityanyprovisionalPattern from third-party reports. NOT confirmed against a live app build here.
    Google (Search app / Gemini surfaces)iosprovisionalGSA = Google Search App, the iOS in-app browser. Identifies the Google app, NOT specifically Gemini — an answer surface cannot be inferred from it.
    Microsoft CopilotanyprovisionalPattern 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.
    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.

    Live classification streamHow this is measured — Bondi Casa