Part 1 · Attribution
Measuring AI-Originated Traffic and Revenue for a Hotel Client
A design for measuring AI-originated traffic and revenue for a hotel client — what each method captures, what it misses, what the client has to implement, and what cannot be measured at all. Companion to a working demonstration.
Contents
Part A — DESIGN
#A1. The core claim
"AI-originated traffic" is not one thing. It is several distinct events with different detection surfaces, different confidence, and — importantly — different meanings. A citation the user never clicks, an assistant fetching a page mid-session, and a human arriving from an in-app browser are not the same event and should never be summed.
This design separates them, states what each proves, tags every figure by how it was obtained, and names the things that cannot be measured at all.
#A2. What we measure
The methods below are laid out as a funnel, from "is the hotel being named at all" down to "did that turn into revenue." Each stage answers a different question, and each one is weaker or stronger than its neighbours for reasons worth understanding.
#Two capabilities underpin most of it
Rather than repeat these at every stage: almost everything rests on one or both of two capabilities on the client's side. Each can be supplied in more than one way, and the shape that supplies it changes what gets seen — not what the stages need.
Detection at the point of arrival (D). The inbound request seen and classified server-side, before any JavaScript runs, and a source value injected into the response. Running ahead of the browser is what lets it detect things a browser-based tag never will. Supplied by a CDN edge worker, a CMS plugin at origin, or framework middleware — in descending order of coverage, and the gaps are not small. A7.
Persistence to the booking click (P). The source value surviving the session and reaching the booking step with its stay parameters. Supplied by a script tag, or by a plugin server-side.
Detection and persistence are different problems, and the distinction matters more than it looks. A referral parameter can be read server-side the moment the visitor arrives; getting that value to survive until they click through to the booking engine is a separate job. What changes by installation shape is not whether both are needed, but whether one install supplies both.
#Stage 1 — Is the hotel being named at all?
The prompt panel. We run a fixed set of prompts on a schedule across the major assistants, recording whether the property appears, where in the answer, and which competitors appear alongside it.
This proves that citations for the property are occurring and shows roughly how it sits against its competitive set. What it does not do is say anything about real users. It is a sample of our own panel, not of the world, and the number of actual people who saw a citation is not recoverable from it. We do not convert one into the other.
Needs nothing from the client — we run it ourselves. Provenance: measured, of the panel. Every figure ships with its sampling frame — prompt set, cadence, locale, model versions.
#Stage 2 — Are assistants reading the site?
Edge agent classification. The classifier inspects every inbound request and identifies fetches coming from assistant infrastructure, recording two things separately: what the fetch is declared to be for, and how confident we are that it is genuine.
Purpose decides whether a fetch counts at all. Training and index crawls are asynchronous and carry no user; only a fetch made on behalf of someone mid-conversation reflects live intent, and only that enters an attribution metric ().
Confidence is recorded as three distinct states — signed, range-matched, neither — never collapsed into a single "AI traffic" count. Which state an agent lands in is not the one you would guess: the cloud browser agents that declare nothing in their user-agent are the ones carrying cryptographic proof, while the politely self-identifying fetch agents rest on an IP list no fresher than its last refresh (, ).
What this proves is narrower than it first appears: the model was exposed to the property's content. Not that it recommended it, and not that a human visited. It is never folded into session counts.
What it misses runs one way. An agent inside the user's own browser leaves no network-layer signal at all, and an operator can decline to declare itself (A4.2).
Needs . Provenance: measured — but a floor, since the misses all point one way.
Technical details
#Stage 3 — Are people arriving from assistants?
Two signals here, and the interesting thing is how differently they behave.
Mobile app browser identification reads the browser identifier on arrival. When someone taps a link inside an assistant's mobile app, that app's embedded browser identifies itself. Note this is a human reading a page — not to be confused with the embedded browser in a desktop assistant app, where an agent is driving (). This is the stronger of the two because it is deterministic — a value fixed in the app build, not something a model decides per response. Once validated against a given app version it holds until the app ships a new one. It requires a maintenance discipline: a per-app, per-version validation record, and an alert when an unrecognised pattern appears. It misses desktop entirely, and any app version we have not yet validated.
Assistant referral parameters are the desktop equivalent, and they are weaker. Verified in testing that at least one major assistant appends them, but not consistently across surfaces and answer types, and referrers are stripped or rewritten too often to lean on. The behaviour is model- and surface-dependent, and can change overnight with no notice, which is why this yields a floor rather than a count.
That instability needs a periodic re-verification job and a changelog, so a drop in reported traffic can be distinguished from a change in assistant behaviour. Without one, we will eventually tell a client their AI traffic fell when a product team shipped a link-format change.
These two share a classifier but not a confidence level, and they are never averaged together. Confidence is assessed per assistant, per signal.
Mobile app identification needs D. Referral parameters need D to read and P to carry the value forward to the booking step. Provenance: mobile app identification is measured. Referral parameters are estimated, as a floor.
Technical details
#Stage 4 — Are they trying to book?
Booking-click capture. When a visitor clicks through to the booking engine, the script records the click against the source assigned on arrival, along with the stay parameters the click carries — dates, party size, and where available a rate plan or promo code.
This is the load-bearing metric in the whole design, for a structural reason: it happens on the hotel's own domain and therefore does not depend on the booking engine cooperating at all. Everything downstream of this point does.
A click is intent, not a booking, and it is reported as such.
Its main limitation is the booking widget itself: where the search form is a cross-origin frame, browser security prevents reading its state and often the click too. Capture rate therefore varies by client and is reported per client (). The second limitation is that a booking completed by an agent inside the user's own browser fires this click from a session carrying no AI signal at all (A4.2).
Needs and , and sometimes a widget reconfiguration. Provenance: measured.
Technical detail
#Stage 5 — Did they book, and what was it worth?
Three methods, in descending confidence. Which one applies depends entirely on what the client's booking provider and property system allow, so the tier should be established during scoping rather than promised in advance.
Passthrough is the only deterministic path. The source value is appended to the booking URL and survives to the reservation record, tracing a completed booking end to end. It requires the booking provider to accept and pass through a source parameter, and providers that silently drop unknown parameters break it. Needs , , and vendor cooperation. Provenance: measured.
Reconciliation is the fallback that needs no vendor cooperation. Where the client can export reservation records with creation timestamps, click records are matched against bookings on stay dates and timing. This produces a period-level estimate with an interval — "an estimated 14 ± 5 bookings this quarter" — and it can never produce a per-booking answer, because the same dates and party size recur too often for any single match to be conclusive. It does not ship to a client until it has been calibrated against a property where passthrough already gives the true answer (). Needs , , and a scheduled reservation export. Provenance: estimated, as an interval.
Modelled revenue is the crudest tier and exists only for clients with neither of the above: the booking engine's conversion rate applied to AI-attributed intent clicks. It assumes a segment converts like the average visitor, which is an assumption we cannot test until passthrough is live somewhere. That assumption appears on the face of any report using it. Needs , , and a conversion-rate figure. Provenance: estimated, weakest tier.
Technical detail
#Provenance at a glance
Every client-facing figure carries one of these tags, and figures are never summed across tags. A measured count and an estimated interval do not add up to a bigger measured count.
- Measured: edge agent classification (a floor), mobile app browser sessions, booking clicks, passthrough bookings.
- Estimated: referral-parameter traffic (a floor), reconciled bookings (interval only), modelled revenue.
- Measured of the panel: prompt panel results, published with their sampling frame.
- Unmeasurable: see A4.
#A3. What the client has to implement
Step 0 costs the client nothing. A bot-posture audit: check whether the property is blocking the very assistant agents that generate its citations. Blanket AI blocking is common and self-defeating — it can suppress the search and user-fetch agents while doing nothing about training crawlers. The fix is a per-agent split in the site's crawler directives. This runs before any measurement, or we baseline a property that is blocking its own demand.
Prerequisite — server-side request visibility. This enables Stages 2 and 3, and the source injection that Stages 4 and 5 depend on. Its cost varies enormously and should be established during scoping rather than assumed.
The edge-worker shape gives the fullest coverage, and its cost is a ladder:
- If the property is already on a CDN with edge compute and we are given access, deploying a worker is genuinely small.
- If it is on such a CDN but the site is managed by an agency, this becomes an access or change-request conversation, which may be the slowest part of onboarding.
- If it is not behind a suitable CDN at all, the ask is a nameserver or DNS change. This is the largest single ask in the design. For a small property with an outsourced web agency, it is plausibly the thing that fails.
That last rung is where a second path matters. On a supported CMS, a plugin supplies D at origin — no DNS change, no CDN access, no agency in the loop. It is a smaller ask, not an equivalent one: it is blind to anything cached or blocked above it, so its coverage is a per-property number rather than a given (A7). Where the property runs a CMS we support, this should be weighed against the DNS conversation rather than treated as what we fall back to after it fails.
[OPEN] Where neither applies — no CDN access, no supported CMS — the last resort is
shipped server logs analysed on a lag. That loses real-time injection and source
persistence, and log availability varies by host, so it degrades Stages 4 and 5 rather
than replacing D. It needs specifying properly rather than being left as a hope.
Prerequisite — persistence to the booking click. Usually a script tag, deployed through an existing tag manager or CMS, and usually small. It is not optional for anything touching the booking step, including Stage 3's referral parameters.
In the plugin shape this is not a separate ask at all: the plugin injects the tag itself, sets the source cookie server-side, and can rewrite the booking CTA at render time. One install supplies both capabilities.
Then, additive: a scheduled reservation export from the property system or channel manager, including creation timestamps. This is what enables reconciliation.
Depends on their vendor — ask, do not assume:
- Booking engine accepting and passing through a source parameter. This enables the only deterministic revenue path.
- Where the booking widget is a cross-origin frame: reconfiguration to a redirect integration, or the engine moved to a subdomain behind the client's own CDN. The second is the better outcome, since it puts the booking request itself in front of D, and it may not be available from every vendor.
What we never ask for: a custom booking flow built against the engine's API. That puts payment-card compliance scope, availability synchronisation and conversion risk on the client's highest-value page. No hotel should accept that from an analytics vendor, and proposing it would undermine everything else we ask for.
#A4. What we cannot measure
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 or from a third-party article, 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. The prompt panel samples whether citations can occur; it cannot count how many users saw one.
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, driving the page through the browser's own debugging interface. There is no network-layer signal, and the address-class heuristic that catches cloud browser agents does not apply, because the residential address is genuine. This traffic lands silently in the human baseline.
The related case — an agent driving a browser embedded in a desktop assistant app — is
[UNVERIFIED] rather than known-invisible, and is a test we should run ().
3. Per-booking attribution. Reconciliation (Stage 5) produces a defensible interval for a period. It cannot tell you that a specific reservation came from a specific assistant, and the interface will not offer a per-reservation source field.
#A5. Confidence, and which way the errors point
Two things worth knowing before reading any number we produce.
Confidence is per assistant, not global. ChatGPT, Gemini and Perplexity each get their own validation status and their own reported figure. A UA string is deterministic — set by an app build, stable until they ship a new one. A UTM parameter is a model-side decision that can change overnight without notice. These do not belong in the same average.
Every known error runs in the same direction: we undercount AI.
- Operators evading declaration, or published IP ranges going stale — undercount.
- Agent-mediated browsing in the user's own browser — undercounts AI, overcounts direct and organic.
- Agent-completed bookings entering reconciliation as non-AI — undercount.
- Referral-parameter coverage gaps — undercount.
This is the honest direction to err in, and it means our figures are a floor. It also means the errors compound rather than cancel, which a client should be told rather than left to assume.
The corollary: our defensible claim to a client is about influence — that assistants are surfacing and retrieving this property — not about a count of completed bookings.
Technical details
#A6. What we would build first
In order, and the order is deliberate.
- Bot-posture audit. Cheap, fast, protects the client immediately, and has to precede any baseline.
- Edge classifier. One worker producing Stages 2 and 3, recording signed / range-matched / neither as separate states rather than collapsing them.
- Booking-click capture. Stage 4. First-party, and independent of whether the booking engine ever cooperates.
- Passthrough, where the vendor allows it. The deterministic tier of Stage 5.
- Reconciliation, calibrated against a passthrough client before it is shown to anyone. The estimated tier of Stage 5.
Note on ordering: 3 deliberately precedes 4, so that a client whose booking provider refuses still gets a working intent metric rather than nothing.
#A7. How this ships
#One classifier, three installation shapes
The classification logic is a pure function of the inbound request — headers, address, network class — with thin adapters per host environment. This matters beyond tidiness: the brief asks whether we would point this at a paying client's site, and real clients sit on different infrastructure. A classifier welded to one vendor answers that question badly.
CDN edge worker, deployed via a scoped client token. The full-signal shape. The client creates an API token in their CDN dashboard limited to worker deployment on their own zone; we deploy programmatically. Minutes of client action rather than an integration project. Sees every request before cache, so it captures agent fetches, browser identifiers and referral parameters completely. Worth noting that no app-store mechanism exists for this — Cloudflare Apps was discontinued in 2024 and the current marketplace runs the other direction — so the scoped token is the practical route, and its permission scopes should be verified before it is promised to anyone.
CMS plugin. The only shape with genuine one-click distribution, and plausibly where most
independent hotel sites sit — though [OPEN] we have no defensible figure for that and
should not accept anyone else's. It runs at origin, so it does see agent fetches.
Its distinguishing property is that it controls the page's code as well as its cache, which
makes it the only shape besides the edge worker that supplies both D and P from one
install. It renders the page, so it injects the script itself — no tag manager, no template
access request. It sets the source cookie via Set-Cookie rather than document.cookie,
which is what lifts the seven-day persistence ceiling described in A2. And it rewrites the
booking CTA URL at render time, so Stage 5 passthrough needs no client-side JavaScript at
all — making it immune to ad blockers and tracking prevention, which the script-tag path
is not.
Framework library. An npm package for sites built on a modern framework, hooking the request path before cache. Technically the cleanest of the three and the fastest to validate, but the addressable market among independent hotels is very small. This is a test harness that happens to also be shippable, not a route to market.
The channel that actually reaches this market is none of the three: the hospitality web agencies that build and host for many independent properties at once, where the CDN question gets answered once rather than per client. Out of scope here, but it is where distribution lives.
#The CMS plugin route — specified, not built
[OUT OF SCOPE for this trial] Recorded because it is the most plausible route to volume.
A plugin runs at origin, so it sees any request that reaches the application — including declared agent fetches. Two classes of request never reach it, and it can measure neither from the inside, because the requests it misses are precisely the ones it never receives:
- Caching. Host-level caches and page-cache drop-ins bypass the application
entirely. A CDN is the good news: Cloudflare does not cache HTML by default, so a property
on default settings still sends every HTML request to origin.
[VERIFY]before relying on it. - An upstream block. A CDN AI-crawler toggle or bot-fight rule answers the agent with a 403 before origin. A cache hit at least served the agent the content; a block did not, so the property loses the citation and the record of having lost it. This makes the Step 0 bot-posture audit in A3 a hard prerequisite in this shape rather than good practice — an edge worker sees the request it is about to block, and a plugin cannot.
Introspection cannot measure either gap, because both are defined by requests that never arrive: a plugin asked to measure its own coverage from the inside reports full coverage while a cache or a bot rule absorbs everything above it. An external probe is what makes the gap visible — fetch the property's own pages from outside as a declared agent and compare against what the plugin recorded. The gap is the coverage figure.
Cache bypass for declared agents closes most of the first blind spot: latency is irrelevant
to an automated fetch, so serving agents uncached costs nothing a visitor would notice. Two
rules, not one — a user-agent exclusion catches declared fetch agents, and a rule keyed on
the presence of a Signature-Agent header catches the cloud browser agents whose user-agent
is stock Chrome. It costs origin CPU, and it is not cloaking: the content served is
identical and only the cache path differs, which is worth stating because a property's SEO
agency will ask.
The ceiling, stated plainly: cache bypass does nothing about the second blind spot — a blocked request stays blocked, and the plugin stays unaware of it. So the plugin shape can approach the edge shape's coverage on a property whose bot posture has been audited, and can never match it overall. Plugin-derived coverage is a per-property number, not a product guarantee, and it belongs on the face of any report produced from it.
#A8. Constraint
Everything here proposes. A human approves anything that touches a client's live property. No ranking or outcome is promised in any output.
#A9. Open questions
What this design does not yet know, and what would settle each one.
- Surface identification test. Point each surface — cloud fetch agent, cloud browser agent, desktop app embedded browser, browser extension, mobile app browser — at a controlled endpoint and record exactly what arrives. Resolves and produces the per-assistant validation records Stage 3 depends on.
- Validation without a real hotel. Called out in the brief as part of the task. Needs a plan for generating or obtaining realistic traffic and server logs to test the classifier against.
- CMS share survey. What proportion of properties in the target segment run each platform? General web CMS figures are a poor proxy and hospitality-specific CMS data is not usable. Sample and classify a few hundred hotel sites in the segment; distrust any vendor figure offered without a stated sample.
- Booking widget survey. Actual distribution of integration patterns across realistic client properties — measured by sampling and classifying hotel sites in the target segment, not taken from vendor documentation. Also: which engines tolerate being proxied behind the client's own CDN zone.
- Reconciliation window length. What click-to-creation window balances recall against collision rate? Fitted per property from observed data, not fixed globally.
- Reservation export access. Which property systems permit a scheduled export with creation timestamps, and at what precision?
- Cross-device. Seen on phone, booked on desktop days later. Currently invisible to the whole design. Attempt, or declare out of scope.
- Attribution window. Lag between citation exposure and booking is likely long for travel. What window do we claim, and on what basis?
- Conversion-rate assumption in Stage 5. Needs a passthrough client before the modelled figure is defensible.
- Panel design. Prompt set size, refresh cadence, locale coverage, competitor set selection.
Part B — THE WORKING DEMONSTRATION
#B1. What it demonstrates
Three things, in descending order of how well they are currently understood anywhere:
- 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 which application it was.
- That an unverifiable claim is recorded as unverifiable rather than counted. Spoof a user agent at it and the stream shows the verification failing — which is more informative than any successful detection.
#B2. Shape
A Turborepo monorepo, two publicly deployed applications, no database.
| Package | Role |
|---|---|
packages/classifier | The pure classification function. No I/O, no framework. |
packages/events | Shared event schema, so both applications agree on the record shape. |
apps/hotel | The property site. Server-rendered, on its own domain. |
apps/stream | The live classification log, on a separate domain. |
Why the classifier is its own package. It is the only reason the monorepo earns its overhead — and it makes the portability claim visible in the repository structure rather than asserted in prose. The same module is consumed by framework middleware and by a CDN worker adapter without modification.
Why the stream is a separate application, not a route on the hotel site. Necessity rather than tidiness: traffic to the viewer would otherwise pass through the hotel's own middleware and pollute the log it is displaying.
Transport. Classification happens in middleware and is published to a realtime channel; the viewer subscribes and appends. Publishing is fire-and-forget via the platform's post-response hook, so classification adds no latency to the request being classified.
No database, deliberately. The consequence is stated rather than hidden: the stream is
ephemeral, and a visitor arriving after an interesting event has passed sees an empty feed.
Channel history or rewind-on-attach gives a short backlog without persistence and is worth
configuring; [VERIFY] the retention window available on the tier in use.
#B3. The record format is the argument
The stream does not show raw requests. It shows classifications with their evidential basis, which is the design's central discipline made visible:
ChatGPT-User · signature verified · user-triggered fetch
→ exposure, not a visit · MEASURED
Chrome/142 · residential address · no agent signal
→ human, or an agent we cannot detect · MEASURED
claims ChatGPT-User · signature absent, address outside published ranges
→ RECORDED, NOT COUNTEDThe third state is engineered on purpose. It is the one a sceptical reader will try to produce, and the one most tooling would silently count.
#B4. How it is exercised
The hotel site carries a panel of specific things to try — ask an assistant about the property and paste the URL, open the link from inside a mobile assistant app, request the page with a forged user agent — each with the classification it should produce. Every prompt is tested before publication, because a demonstration that returns silence when someone follows the instructions is worse than none.
This is also how the design's own open questions get answered: pointing each surface at a
live endpoint and reading what arrives resolves the [UNVERIFIED] in and produces the
per-assistant validation records Stage 3 depends on. Real traffic against real logs, which
is what the brief asks for and what a closed test harness cannot provide.
#B5. What it does not establish
It cannot show that citation happens. A new property with no inbound links and no index presence will not be surfaced spontaneously by any assistant. Handing over the URL triggers a fetch, which proves detection works when traffic arrives — it proves nothing about whether the property would ever be recommended. This is stated in the demonstration itself rather than left for the reader to notice, and it is consistent with Stage 1 exposure being declared unmeasurable in A4.
The revenue tiers are untested. Passthrough and reconciliation () are specified in the design and not implemented here — the first needs a cooperating booking provider, the second needs real reservation records with creation timestamps. Neither should be read as working because the detection layer beneath them is.
Coverage figures are per-property. Capture rate and cache visibility (A7) are facts about a specific site. The demonstration shows the diagnostics function; it cannot predict what they will report on a property we have not seen.
One layer built and exercised against real traffic; the rest specified with its trade-offs named.