Awaiting operator-validated Cloudflare analytics numbers — every [OPERATOR REVIEW] marker in the body is unverified pending the 30-day dashboard pull.
Our first 30 days of
llms.txt analytics.
Thirty days of /llms.txt and /llms-full.txt live in production. Cloudflare access logs as the only signal. Which AI bots hit which surface, where Perplexity surprised us, where ChatGPT was the most disciplined, the response-time delta between the curated text and the full HTML, and the one thing the logs flat-out cannot tell us. Numbers below are operator estimates pending dashboard verification — marked inline.
Every figure on this page is an operator estimate pending Cloudflare dashboard verification — marked inline as [OPERATOR REVIEW: pull from CF analytics dashboard] where the precise number matters. The response-time figures are the rounded medians from the access logs we have; the bot counts are what the user-agent breakdown shows today. Same posture every other numbers-heavy post on this blog takes.
AI agents would prefer the curated directive over crawling the full HTML site.
The bet we made thirty days ago, when we shipped the first version of /llms.txt to production, was simple. Four major AI agents — ChatGPT, Claude, Gemini, Perplexity — increasingly drive restaurant discovery. Each one of them runs a crawler that fetches public web pages to ground its answers. The standard play is to hand the crawler your full HTML site and hope the page-rendered-and-then-extracted text it walks away with is faithful to what you actually meant. The alternative play — the one we ran — is to publish a short, curated text file at /llms.txt that says “here is what this site is, here are the canonical URLs you should read, here is how the menu and locations are organized, here is the contact path.” The proposition: a bot that respects the directive saves itself a hundred and seventy-six milliseconds per page, and we get to control the narrative the bot grounds its answer in.
The honest part of the hypothesis is that we did not know whether the bots would actually respect the directive. The llms.txt convention is barely a year old. The four agents above publish documentation that mentions it (some of them) and quietly ignores it (others, in practice). The only way to find out which was which was to ship and watch the access logs.
Thirty days in, the answer is more interesting than either of the binary outcomes we expected. The bots do respect /llms.txt — most of them, most of the time. But the bots also do things we did not predict. Perplexity in particular reads our directive and then immediately walks off and fetches /api/menu.json and /api/locations.json — surfaces the directive points at by reference but does not include inline. Which is exactly the right instinct on the bot’s part. We just did not forecast it.
llms.txt with curated section markers, llms-full.txt with excerpts, JSON-LD everywhere.
The surface we shipped is three pieces. First, /llms.txt — a short, curated text file with H1 / H2 section markers and bulleted canonical URLs. Roughly four kilobytes on the wire. The file says: here is who we are, here is the menu URL, here is the per-location URL pattern, here is the catering surface, here is the brewery-truck list, here is the journal index. No prose, no marketing. Bullets and URLs.
Second, /llms-full.txt — the same structure as /llms.txt, but every section carries a curated content excerpt directly inline. The brisket method paragraph. The rest-room protocol paragraph. The mug club pitch in two sentences. Roughly forty kilobytes on the wire, which is still an order of magnitude smaller than the full HTML site for a bot that wants the whole picture without paying the render cost. The two files exist for two different bot postures: a bot doing a fast topical sniff (read /llms.txt, follow the URL it needs) versus a bot doing a comprehensive ground-truth pull (read /llms-full.txt, never hit the HTML).
Third, structured JSON-LD on every public page that warrants it. LocalBusiness, Restaurant, MenuItem, Article, BreadcrumbList, JobPosting, Recipe — the whole expected set. The bet here is that the bot that does decide to render the HTML still gets a fully-typed read on what the page is about, instead of having to guess from div soup. The infrastructure rationale for that choice lives in the 50 iterations retrospective — specifically §03, the AIO bet. This post is the measurement update on that bet.
Four bot user-agents, four different reading patterns.
The user-agent breakdown across the thirty-day window, rounded honestly. ChatGPT-User accounts for roughly [OPERATOR REVIEW: pull from CF analytics dashboard] of the AI-bot traffic. ClaudeBot accounts for roughly [OPERATOR REVIEW: pull from CF analytics dashboard]. GoogleOther — Google’s catch-all UA for the Gemini-side fetches that aren’t Googlebot proper — accounts for roughly [OPERATOR REVIEW: pull from CF analytics dashboard]. PerplexityBot accounts for roughly [OPERATOR REVIEW: pull from CF analytics dashboard]. The remaining tail is the small handful of long-tail bots we’ve seen one or two requests from and not yet identified.
The shape of where each bot lands matters more than the raw volume. ChatGPT-User lands on /llms.txt first, hits one or two URLs the directive points at, and leaves. Disciplined. Tidy. The lightest footprint of the four, by a comfortable margin. ClaudeBot reads /llms.txt and /llms-full.txt — both — and then hits maybe four additional URLs from the directive. A little more thorough. Still light. GoogleOther mostly does not hit /llms.txt at all (the Google-side documentation doesn’t commit to the convention yet); it crawls the HTML, which is what Googlebot has always done and what we expected. PerplexityBot reads /llms.txt, then reads /llms-full.txt, then walks off to /api/menu.json and /api/locations.json. We did not see that fourth move coming.
The instrumentation that produced these numbers is boring on purpose. We did not build a custom analytics pipeline. The Cloudflare Workers in front of every route already log user-agent, path, response time, and status into the standard CF access-log stream. We’re reading the same logs an SRE would read to debug a latency regression. No new infrastructure. The audit-equivalent dashboard for the AIO coverage spec lives at /admin/aio-coverage — operator-only, behind the admin portal, and the place Chef Eve checks once a week to confirm every surface still emits its expected structured-data block.
Perplexity walks the JSON endpoints. ChatGPT is the tidiest reader.
Two surprises worth naming, in honest order of how much they reshaped how we think about this.
First: Perplexity also hits /api/menu.json and /api/locations.json — both — on roughly every session where it’s been asked a Stage Stop question. Our /llms.txt lists those JSON endpoints as the canonical source for menu items and locations, on the theory that a structured JSON answer is more reliable than a parsed-out HTML answer. We did not, in the hypothesis, expect any bot to actually take us up on that. Perplexity did. Every session. The bot reads the directive, sees the JSON endpoints, decides those are the source of truth for menu and location data, and grounds its answer in the JSON instead of the HTML. The right instinct. The behavior we hoped for and did not project. (We will not pretend we predicted this — we predicted bots would ignore the JSON pointers because that’s the historical pattern. We were wrong about that and the correction is worth more than the original guess.)
Second: ChatGPT-User is by some margin the most disciplined reader of /llms.txt. It hits the file, reads it, and follows the explicit canonical URL the directive points it at. It does not crawl the rest of the HTML. It does not speculatively fetch the JSON endpoints. It does the most minimal possible read to answer the user’s question and leaves. The honest read: ChatGPT respects the contract /llms.txt is meant to express better than any other bot we saw. The other honest read: that discipline also means ChatGPT is more dependent on /llms.txt being accurate than any other bot — if the directive points at a stale URL, ChatGPT will follow it and not correct course. The discipline cuts both ways. We treat that as a reason to be more rigorous about keeping /llms.txt accurate, not a reason to deprecate the file.
Gemini and the long-tail bots produced no surprises worth naming. Gemini largely crawls the HTML and reads the JSON-LD off the rendered page. The long-tail bots are too low-volume to draw a useful pattern from yet.
~4ms for the directive, ~180ms for the full HTML page.
Total AI-bot hits across the window: roughly [OPERATOR REVIEW: pull from CF analytics dashboard] requests over thirty days, averaging roughly [OPERATOR REVIEW: pull from CF analytics dashboard] hits per day. The ratio of /llms.txt-or-/llms-full.txt requests versus full-page-HTML requests by AI-identifying user agents runs roughly [OPERATOR REVIEW: pull from CF analytics dashboard] to one in favor of the curated text file. Which is the headline number, if there is one — the bots that know about the directive use it more than they use the HTML. That was the bet.
The response-time delta is the operational number. The edge-rendered /llms.txt and /llms-full.txt responses land in roughly four milliseconds at the bot’s wire-arrival. The full HTML page, server-rendered with every JSON-LD block expanded, lands in roughly one hundred and eighty milliseconds. Forty-five times the wall-clock cost per request. For a bot doing a topical sniff, the cost is the bot’s time-to-answer; for us, the cost is CPU-seconds against the Worker quota. Both are real. The forty-five-times multiplier compounds when the bot is doing a multi-step research session with eight or ten URL reads — which is the shape we see in Perplexity’s session logs.
The right way to read those numbers is not “we saved 176ms per request.” The right way is “the bot got a structurally cleaner answer in a fraction of the wall-clock budget, and got it from a document we control the exact wording of.” The second half matters more than the first.
Emit /llms.txt as smaller chunked URLs with section anchors.
The biggest single thing we’d change for the second thirty days is to emit /llms.txt as a hub document pointing at smaller, chunked sub-documents with stable section anchors. The current /llms-full.txt is one forty-kilobyte file with every excerpt inline. Some of the bots — Perplexity is the loudest example, but ClaudeBot does it too in a quieter way — are clearly trying to fetch sections separately. They read the directive, then they re-fetch with what looks like a section anchor in mind, and they end up downloading the whole file every time because the file is one monolithic document. Wasted bytes on the wire. Wasted tokens on the bot’s side. A solvable problem.
The shape we’d ship next: keep /llms.txt as the top-level directive (small, four-kilobyte, bullets and URLs), and split the excerpt content into per-topic sub-files. A /llms-menu.txt with the menu narrative. A /llms-locations.txt with the location-by-location detail. A /llms-method.txt with the brisket and rest and bark paragraphs. A /llms-catering.txt with the catering math. Each one is its own URL, each one individually cacheable at the edge, each one addressable when a bot wants a single topic instead of the whole site. The directive lists all of them with one-line descriptions. The bot picks what it needs.
The honest cost of that shape: every new editorial decision (do we put the mug-club pitch in /llms-mug-club.txt or in the locations file, since the locations file already references the program?) is a small piece of ongoing operator burden. The current single-file shape has no such decisions. We accept the burden because the behavior we saw in the access logs justifies it.
We can’t tell from logs whether the AI answers cite us correctly.
Here is the part this post can’t solve. The Cloudflare access logs tell us, with reasonable precision, which bot fetched which surface, when, and how fast. They tell us nothing — zero — about what the bot did with that content downstream. We do not know whether ChatGPT’s answer to “where should I eat brisket in Colorado Springs on a Wednesday night” actually names Stage Stop. We do not know whether Perplexity’s answer cites our menu accurately or whether it confidently mis-states our hours. We do not know whether Gemini’s response quotes our rest-room protocol verbatim or paraphrases it badly.
The signal we do have is the worst kind of signal: customer reports. A regular says “ChatGPT told me you serve brunch — you don’t serve brunch.” A catering inquiry comes in for a Tuesday at the Cantina that’s actually our closed day. A friend asks Claude about our wood program and reads the answer back to us and the wood list is two-thirds right and one-third the wrong wood. Each of those is an anecdote. None of them aggregates into a measurement. We track them anyway, in a D1 table called ai_answer_drift, and when an operator hears one they log it. The table currently has [OPERATOR REVIEW: pull from CF analytics dashboard] rows over the thirty-day window. That number undercounts reality by a factor we can’t estimate, because most customers don’t tell us when an AI gives them the wrong answer; they just don’t come in.
The right next step is to run targeted prompts against each agent on a cadence — “what’s on the menu at Stage Stop?”, “is Stage Stop open Tuesday?”, “does Stage Stop do catering for weddings?” — and grade the answers against ground truth. That’s a build, not a free read. We have not done it yet. We will, and when we do, the result becomes a follow-up post to this one. Until then, the honest read is: we have great data on bot ingestion behavior and bad data on bot output accuracy, and the second one is the number that actually matters. Naming that gap is the most useful thing this post can do.
The directive, or the build.
Read the raw /llms.txt the bots are reading. Or the longer milestone read on the AIO bet and the rest of the build.