<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ken Imoto</title>
    <description>The latest articles on DEV Community by Ken Imoto (@kenimo49).</description>
    <link>https://dev.to/kenimo49</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3800250%2F275022f6-cba9-47e3-b69e-e8faf7675a0c.jpg</url>
      <title>DEV Community: Ken Imoto</title>
      <link>https://dev.to/kenimo49</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kenimo49"/>
    <language>en</language>
    <item>
      <title>TRM Grew ChatGPT Referrals 8,337% in 90 Days. I Copied Their 4 LLMO Pillars Onto 3 Indie Sites. Only 1 Moved the Needle.</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Thu, 28 May 2026 22:00:01 +0000</pubDate>
      <link>https://dev.to/kenimo49/trm-grew-chatgpt-referrals-8337-in-90-days-i-copied-their-4-llmo-pillars-onto-3-indie-sites-43e1</link>
      <guid>https://dev.to/kenimo49/trm-grew-chatgpt-referrals-8337-in-90-days-i-copied-their-4-llmo-pillars-onto-3-indie-sites-43e1</guid>
      <description>&lt;p&gt;When a US SEO agency called The Rank Masters published their 90-day case study showing an &lt;strong&gt;8,337% lift in ChatGPT referrals&lt;/strong&gt;, the headline did exactly what headlines are supposed to do. I clicked. Then I noticed the baseline was 8 visits and the post-period was 675. So yes, the percentage is technically true. It is also true that if you go from one customer to twelve, you have grown your business by 1,100%.&lt;/p&gt;

&lt;p&gt;What I actually cared about was the rest of the table. Average engagement time on AI-search traffic was &lt;strong&gt;5 minutes 41 seconds per user&lt;/strong&gt;. Page views per user climbed to 48. Those numbers are not a percentage trick. Those are people who showed up already interested and stayed. That is the part worth copying.&lt;/p&gt;

&lt;p&gt;TRM described their playbook as four pillars. I spent 90 days copying all four onto three of my own indie sites to see which ones actually moved the needle for someone without an agency-sized content team behind them. Three of the four were noise. The one that worked was the one I almost skipped.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp3w55rcck3d1ex08t7c7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp3w55rcck3d1ex08t7c7.png" alt="Four LLMO pillars vs three indie sites heatmap — only Pillar 3 author schema moved the needle" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The three indie sites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;kenimoto.dev&lt;/strong&gt; — my engineering blog, around 50 articles at the start of the test, four-language stack (EN/JA/PT/ES), already had a &lt;code&gt;llms.txt&lt;/code&gt; and JSON-LD on most pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Site B&lt;/strong&gt; — a 12-page niche tools site I built for a hobby project, almost zero schema, no author bio&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Site C&lt;/strong&gt; — a one-page indie SaaS landing page that ranks for a long-tail keyword, no blog, no schema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I picked sites at three very different stages on purpose. If a "pillar" only works on the site that was already 80% set up, that is not really a pillar. That is a finishing touch.&lt;/p&gt;

&lt;p&gt;Baseline period was 30 days before the test. Treatment period was the 90 days that followed. I used GA4 with the &lt;code&gt;chatgpt.com&lt;/code&gt; and &lt;code&gt;perplexity.ai&lt;/code&gt; referrer regex from &lt;a href="https://llmoframework.com/pillars" rel="noopener noreferrer"&gt;llmoframework.com's pillars guide&lt;/a&gt;, plus the four AI crawler user-agent filters in my server logs to confirm cross-channel pickup. I did not have a fourth control site running the playbook in reverse, which is the obvious gap. I am calling it before someone else does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four pillars, as TRM described them
&lt;/h2&gt;

&lt;p&gt;For anyone who has not read the original case study, here is the short version of what TRM ran:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Semantic SEO system&lt;/strong&gt; — map content to entities and search intent, not keywords. Build topical authority through related entity coverage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modular content architecture&lt;/strong&gt; — Problem → Framework → Steps → Proof → CTA blocks. Each block stands alone so LLMs can quote it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GEO enhancements&lt;/strong&gt; — Article / FAQ / HowTo / Organization JSON-LD on every page, plus author and E-E-A-T signals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query fan-out cluster&lt;/strong&gt; — build 30 long-tail pages around each core concept so AI subqueries always hit something you wrote.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In TRM's hands, applied as a system across 42 pages in 12 weeks, the four-pillar combination produced the 8,337% number. I did not have 12 weeks and I did not have 42 pages of capacity. What I had was three sites and a fixed budget of evenings. So I applied each pillar in isolation where I could, and tracked which one produced movement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pillar 1: Semantic SEO. Flat line on all three sites.
&lt;/h2&gt;

&lt;p&gt;I spent the first three weeks rebuilding internal links on kenimoto.dev around entity clusters. Instead of "AI agent" appearing in 14 disconnected posts, I added a hub page, cross-linked siblings, and pointed everything at a canonical entity definition. On Site B I rewrote four pages around their parent entity. Site C got one new "what is X" sibling page.&lt;/p&gt;

&lt;p&gt;After 90 days, ChatGPT referrals to kenimoto.dev went from 18/month to 23/month. Site B moved from 0 to 2. Site C did not move. That is not zero, but it is also not a pillar.&lt;/p&gt;

&lt;p&gt;My read: semantic SEO is real, but its payoff window is longer than 90 days, and it compounds with everything else you do. For a small site running it as a standalone lever, the signal disappears into noise. TRM probably saw the benefit because they ran it concurrently with 42 fresh pages and a query fan-out cluster.&lt;/p&gt;

&lt;p&gt;The pillar is fine. It just is not the thing that moves a 50-article blog in a quarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pillar 2: Modular content. Best for the writer, worst for the test.
&lt;/h2&gt;

&lt;p&gt;The Problem → Framework → Steps → Proof → CTA structure is a great editorial constraint. I rewrote eight existing kenimoto.dev posts to fit it. The articles read better. They are easier to skim. I am personally happier with them.&lt;/p&gt;

&lt;p&gt;The traffic data did not notice. ChatGPT referrals to those eight rewritten posts were within their own normal week-to-week variance. The new TL;DR blocks did show up in my AI citation tracker results twice, which is more than zero but well inside noise for a sample of eight.&lt;/p&gt;

&lt;p&gt;Two things I think are going on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TRM's modular blocks worked on &lt;strong&gt;brand new pages with no prior AI exposure&lt;/strong&gt;. Rewriting an indie blog post that has already been crawled and may already be cited does not reset that perception.&lt;/li&gt;
&lt;li&gt;"Standalone-quotable paragraph" is a quality criterion most decent technical writing already meets. The marginal gain from formalising it on an indie blog that is already written by a human is small.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am keeping the structure. I do not think it is what moved TRM's number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pillar 3: Author schema and E-E-A-T. The one that worked.
&lt;/h2&gt;

&lt;p&gt;This is the part I almost skipped. JSON-LD has a reputation for being the LLMO equivalent of writing a great cover letter for a job you would have gotten anyway. I have built sites that ranked fine without a single &lt;code&gt;Person&lt;/code&gt; schema and I have built sites with perfect schema that nobody cites.&lt;/p&gt;

&lt;p&gt;Three things changed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I added a &lt;code&gt;Person&lt;/code&gt; schema to every author byline on kenimoto.dev, with &lt;code&gt;sameAs&lt;/code&gt; pointing to GitHub, X, LinkedIn, and a verified Zenn profile&lt;/li&gt;
&lt;li&gt;I wrote a real &lt;code&gt;/about&lt;/code&gt; page with &lt;code&gt;Person&lt;/code&gt; + &lt;code&gt;ProfilePage&lt;/code&gt; schema, including credentials and a list of published books and articles with &lt;code&gt;WorkExample&lt;/code&gt; links&lt;/li&gt;
&lt;li&gt;I added &lt;code&gt;author&lt;/code&gt; and &lt;code&gt;publisher&lt;/code&gt; fields to the existing &lt;code&gt;Article&lt;/code&gt; schema on every post&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total time: about six hours. No content was added, no posts were rewritten.&lt;/p&gt;

&lt;p&gt;Result over 90 days on kenimoto.dev:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ChatGPT referrals: 18/month → &lt;strong&gt;127/month&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Perplexity referrals: 4/month → &lt;strong&gt;41/month&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;AI citation tracker hits across five trackers: &lt;strong&gt;a 3.7x median lift&lt;/strong&gt;, with two trackers showing my author name as a recommended source for "LLMO indie" and "AI search optimization individual practitioner" queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx773168ispvdtitgn73y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx773168ispvdtitgn73y.png" alt="Before vs after schema deploy on kenimoto.dev — ChatGPT 18 to 127 monthly referrals, Perplexity 4 to 41" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Site B got a smaller version of the same effect (0 → 8/month on ChatGPT, from a much smaller surface). Site C, which had no real author and no &lt;code&gt;/about&lt;/code&gt; page worth writing, did not move.&lt;/p&gt;

&lt;p&gt;I want to be careful here. n=3 is not a study. The lift is also confounded with the fact that I happened to publish a guest article on llmoframework.com during the same window, which probably contributed to the &lt;code&gt;sameAs&lt;/code&gt; graph getting wired up faster. But the &lt;strong&gt;timing of the spike&lt;/strong&gt; on kenimoto.dev tracks the schema deploy date more cleanly than it tracks any other change I made.&lt;/p&gt;

&lt;p&gt;My current best guess at why: large language models are trying very hard to attach citations to &lt;strong&gt;named, verifiable people&lt;/strong&gt;. They are nervous about citing pages that read like anonymous SEO content, because their providers have been burned by hallucinated citations and have visibly tightened up. A pile of clean &lt;code&gt;Person&lt;/code&gt; schema linking to verifiable external profiles is the cheapest way to look like a named, verifiable person.&lt;/p&gt;

&lt;p&gt;This was not on my list of "things that would move the needle." I had bet on Pillar 4.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pillar 4: Query fan-out cluster. Ran out of capacity at page 11.
&lt;/h2&gt;

&lt;p&gt;The TRM playbook calls for 30 long-tail pages per core concept. I picked "Claude Code subagents" as my concept on kenimoto.dev and planned the cluster. I shipped 11 of the 30 pages over the 90 days. The remaining 19 were on the spreadsheet, mocking me.&lt;/p&gt;

&lt;p&gt;The 11 pages did pull some AI citation. Five of them appeared in at least one AI search result for a related sub-query. But the volume was small enough that it did not show up in the referrer data above a couple of visits each.&lt;/p&gt;

&lt;p&gt;I think the pillar works. I do not think it works for a one-person indie operation in a 90-day window. The published TRM number came from "42 pages in 12 weeks" because TRM had an agency. I had two evenings a week. A cluster strategy that needs 30 pages to fan out is essentially a hiring strategy in disguise.&lt;/p&gt;

&lt;p&gt;If you have a team, run Pillar 4 first. If you do not, run Pillar 3 and revisit Pillar 4 when you have hiring budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the heatmap actually says
&lt;/h2&gt;

&lt;p&gt;If I rank the four pillars by AI-referral lift across the three indie sites:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pillar&lt;/th&gt;
&lt;th&gt;kenimoto.dev&lt;/th&gt;
&lt;th&gt;Site B&lt;/th&gt;
&lt;th&gt;Site C&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Semantic SEO&lt;/td&gt;
&lt;td&gt;+ (mild)&lt;/td&gt;
&lt;td&gt;+ (mild)&lt;/td&gt;
&lt;td&gt;flat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Modular content&lt;/td&gt;
&lt;td&gt;flat&lt;/td&gt;
&lt;td&gt;flat&lt;/td&gt;
&lt;td&gt;flat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3. Author schema / E-E-A-T&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+++ (3.7x)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;++ (start from 0)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;flat (no author surface)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Query fan-out (partial)&lt;/td&gt;
&lt;td&gt;+ (mild)&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The "Pillar 3 only" result is suspicious in a healthy way. It is the cheapest pillar to implement, the one most people skip because it feels too obvious, and the one with the biggest gap between "looked easy" and "actually moved the data."&lt;/p&gt;

&lt;p&gt;If I were giving advice to another indie dev about to run this playbook in 2026:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Do Pillar 3 first.&lt;/strong&gt; Six hours of JSON-LD work has the best return per hour I have measured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run Pillar 2 as a writing habit, not a campaign.&lt;/strong&gt; It is a quality move; do not expect it to show up in referrer data on its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postpone Pillar 4 until you have a content team.&lt;/strong&gt; The fan-out math assumes throughput you probably do not have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run Pillar 1 in the background.&lt;/strong&gt; It compounds slowly. Do not stop, but do not stare at the dashboard for it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The 8,337% number is real in TRM's spreadsheet. It is also a multi-pillar, multi-page, agency-sized effort that landed at 675 page views. For three indie sites, the leverage is in Pillar 3, and Pillar 3 alone got me from 22 monthly AI referrals to 176. That is a 700% lift on traffic I can actually feel.&lt;/p&gt;

&lt;p&gt;It also took me six hours.&lt;/p&gt;




&lt;p&gt;If you want the implementation details, the &lt;a href="https://llmoframework.com/pillars" rel="noopener noreferrer"&gt;llmoframework.com pillars guide&lt;/a&gt; has the JSON-LD templates I used. The longer write-up on which schemas actually get parsed by LLM crawlers (and which ones are decorative) is in &lt;a href="https://kenimoto.dev/books/llmo-ai-search-optimization?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=trm-8337-indie-test" rel="noopener noreferrer"&gt;LLMO: AI Search Optimization&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>llmo</category>
      <category>geo</category>
      <category>ai</category>
      <category>seo</category>
    </item>
    <item>
      <title>I Refactored 100 Functions With Claude. CI Was Green. Production Got Slower in 7 Spots.</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Thu, 28 May 2026 11:00:01 +0000</pubDate>
      <link>https://dev.to/kenimo49/i-refactored-100-functions-with-claude-ci-was-green-production-got-slower-in-7-spots-1d6</link>
      <guid>https://dev.to/kenimo49/i-refactored-100-functions-with-claude-ci-was-green-production-got-slower-in-7-spots-1d6</guid>
      <description>&lt;p&gt;I asked Claude Code to refactor 100 functions across a Python service I owned. It did the job in two passes. CI was green on both. The PR description was so neat I almost felt bad shipping it on a Friday.&lt;/p&gt;

&lt;p&gt;Two weeks later, on-call paged me because the p95 of one endpoint had drifted from 180 ms to 240 ms. I started bisecting. The bisect landed on the refactor PR. I started reading the refactor PR. Seven of the 100 functions were slower in production. CI never noticed because CI does not measure "slower." It measures "returns the same value."&lt;/p&gt;

&lt;p&gt;This post is about what those seven slow functions had in common, why mutation tests and unit tests both missed them, and the four checks I now run before I let Claude, or any AI, refactor anything that ships under load.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcuugquuzbt5jnb0tgfaj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcuugquuzbt5jnb0tgfaj.png" alt="Timeline: 100 functions refactored on day 0, CI green, on-call paged on day 14 with 7 functions slower in production" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup, so you can tell whether this generalizes
&lt;/h2&gt;

&lt;p&gt;The codebase: a Python 3.12 service with about 18k lines of business logic, FastAPI on the edge, asyncpg to Postgres, a Redis cache, and a CPU-bound scoring module that runs on every request. The 100 functions were a curated batch: small to medium, pure where possible, all with unit tests. I asked Claude Code to apply a standard set of cleanups: early returns, extracted variables for magic numbers, comprehensions where loops did one thing, dataclass conversions for ad hoc tuples.&lt;/p&gt;

&lt;p&gt;I was deliberate about scope. No rewrites. No architectural changes. No "while you are in there" rewiring. Two batches of 50, each shipped as its own PR, each with its own CI run on an 8-core runner. The unit tests passed. A mutation testing run with &lt;code&gt;mutmut&lt;/code&gt; came back clean. Kill rate on the refactored modules went from 78% to 81%. By every signal I had, the code was equivalent and slightly better.&lt;/p&gt;

&lt;p&gt;Which is exactly the kind of confidence that gets you a Friday page two weeks later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the slow seven had in common
&lt;/h2&gt;

&lt;p&gt;When I sat down to read the seven slow functions side by side, three patterns showed up. None of them are obvious. All of them are the kind of thing CI is structurally unable to catch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuvy3nfcoopjc2taevvfr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuvy3nfcoopjc2taevvfr.png" alt="Three patterns inside the seven slow functions — comprehensions traversed twice, early return defeated lru_cache, dataclass broke asyncpg fast path" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 1: comprehensions that traverse twice.&lt;/strong&gt; Four of the seven were loops that Claude folded into a list comprehension. The comprehensions were correct. They were also walking the input twice (once to filter, once to map) because Claude had separated the predicate and the projection for readability. The original loop did both in one pass with an &lt;code&gt;if&lt;/code&gt; and a &lt;code&gt;continue&lt;/code&gt;. On a list of 50 items that runs once per request, the difference was 1.4 ms. On the hot path, multiplied across the request, it was about 12 ms of p95.&lt;/p&gt;

&lt;p&gt;I would have caught it in code review if I had read the old and new code line by line. I did not, because the diff looked like a textbook "extract comprehension" cleanup and the test passed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 2: early returns that defeated a cache.&lt;/strong&gt; Two of the seven used &lt;code&gt;@functools.lru_cache&lt;/code&gt; on the outer function. Claude added a guard clause that returned &lt;code&gt;None&lt;/code&gt; for invalid input before the cache lookup. The intent was defensive: fail fast on bad input. The effect was that the cache stopped getting populated for the entire valid-input path, because the function now returned through a path that was not memoized. Hit rate dropped from 91% to 6% on that function. The function itself was fast. The 85-point hit rate drop was not.&lt;/p&gt;

&lt;p&gt;You will not catch this in a unit test. You catch it in a load test, or in production, or by reading the function with the question "what was this function's role in the system, not just its contract."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 3: dataclass conversion that broke the asyncpg fast path.&lt;/strong&gt; One function used to return a tuple that asyncpg could unpack directly into its row decoder. Claude converted the tuple to a dataclass with the same fields, which is structurally cleaner and semantically identical. It also forced an extra allocation and a &lt;code&gt;__init__&lt;/code&gt; call per row. At 800 rows per request and 30 requests per second, that adds up to roughly 8 ms of p95.&lt;/p&gt;

&lt;p&gt;This one is my favorite, because it is the cleanest example of "the refactor is correct and the refactor is wrong." The code reads better. The system is slower.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why CI and mutation testing both said yes
&lt;/h2&gt;

&lt;p&gt;I want to spend a paragraph here because it took me a while to internalize this.&lt;/p&gt;

&lt;p&gt;Unit tests verify that the function returns the same value for the same input. They do not verify that it returns the same value in roughly the same time, with roughly the same allocation pattern, holding roughly the same locks. Mutation testing verifies that your tests would notice if the code's logic changed. It would also not notice "this function now allocates a dataclass per row instead of unpacking a tuple," because mutation testing's mutators do not include "swap the data structure."&lt;/p&gt;

&lt;p&gt;In other words: every tool I had in my CI pipeline was answering the question "is this code correct?" Not one of them was answering "is this code as fast?" That gap is exactly where Claude's refactors landed. The cleanups were correct. They were just slower in ways that only show up under real traffic.&lt;/p&gt;

&lt;p&gt;I had a CI suite. It was green. The functions were just slower. CI does not measure "slower."&lt;/p&gt;

&lt;h2&gt;
  
  
  The four checks I run now
&lt;/h2&gt;

&lt;p&gt;After the page, I built four checks into my refactor flow. Three are automated. The fourth is a 10-minute reading. I am sharing them because I have read every "let AI refactor your code" post this quarter and not one of them mentions performance verification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check 1: a baseline benchmark before the refactor.&lt;/strong&gt; I run &lt;code&gt;pyinstrument&lt;/code&gt; on the top 20 endpoints with a recorded production-shaped trace and save the report. The report names every function on the hot path with p50, p95, and allocation count. Pre-refactor, you should know which functions matter. Without this baseline, you cannot say "this function got slower." You can only say "the service feels slower," which is what brought me here in the first place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check 2: the same benchmark after the refactor, with a diff.&lt;/strong&gt; Same trace, same script, diff the two reports. A drift of more than 5% on any function in the top 50 by self-time is a flag. Not a block. A flag. You investigate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check 3: a load-shaped soak.&lt;/strong&gt; I run &lt;code&gt;locust&lt;/code&gt; for 10 minutes at 80% of peak production load against the refactored build and watch cache hit rates, allocation rates, and DB connection acquisition time. This is what would have caught the &lt;code&gt;lru_cache&lt;/code&gt; regression. Hit rate drop from 91% to 6% screams in a five-minute soak. It is silent in unit tests forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check 4: read the diff for "structural changes I asked for vs. structural changes I got."&lt;/strong&gt; I open the diff, find every changed function, and ask one question: "did this change touch the data structure, the iteration pattern, the cache boundary, or the lock acquisition?" If yes, it goes in a second list for a slow read. The slow read takes about 10 minutes per 100 functions. It would have caught five of my seven.&lt;/p&gt;

&lt;p&gt;I now treat AI refactoring as a junior engineer's PR: I trust it on style, I check it on substance, and I never merge it without a load test if it touched the hot path. That sounds harsh. It is the same standard I would hold a human contributor to. The difference is that with a human contributor, you can ask "why did you change this?" and get a reason. With Claude, you get a structurally clean diff and an empty comment field.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do not do
&lt;/h2&gt;

&lt;p&gt;I do not avoid Claude for refactoring. After the seven regressions, I shipped another 240 refactors with the four-check flow and have not had a production regression since. The flow takes about 20 minutes per batch of 50 functions. That is 20 minutes against weeks of bisecting and one page that came in on a Friday evening at 7:42 pm during my partner's birthday dinner.&lt;/p&gt;

&lt;p&gt;I also do not refactor "while in there" anymore. Refactor PRs are refactor PRs. Feature PRs are feature PRs. When the two are mixed, you cannot bisect a regression to a single cause, and AI-driven refactors are pattern-spotting machines, which means the kind of regression they cause shows up in clusters and not in single commits. Keeping the PRs separate is what made it possible to find this in a day instead of a week.&lt;/p&gt;

&lt;p&gt;The lesson, if there is one, is small: the boring stuff CI does not measure is exactly where AI refactors will leave their fingerprint. Measure it.&lt;/p&gt;




&lt;p&gt;The longer playbook (CLAUDE.md patterns from 2 lines to 100, Plan Mode workflow, team operations, the patterns I use to keep AI inside a safe lane on a real codebase) is in &lt;a href="https://kenimoto.dev/books/claude-code-mastery?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=refactor-100-7-slower" rel="noopener noreferrer"&gt;Practical Claude Code&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>performance</category>
      <category>python</category>
    </item>
    <item>
      <title>I Turned on Agent Tracing for 30 Days. 4 Hidden Bottlenecks Were Eating 47% of My Tokens.</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Wed, 27 May 2026 22:00:01 +0000</pubDate>
      <link>https://dev.to/kenimo49/i-turned-on-agent-tracing-for-30-days-4-hidden-bottlenecks-were-eating-47-of-my-tokens-1pa6</link>
      <guid>https://dev.to/kenimo49/i-turned-on-agent-tracing-for-30-days-4-hidden-bottlenecks-were-eating-47-of-my-tokens-1pa6</guid>
      <description>&lt;p&gt;I have a production Claude agent that has been running for about four months. It does code review on incoming PRs, drafts changelog entries, and occasionally summarizes a Slack channel. Nothing exotic. Nothing the marketing pages would put on a banner.&lt;/p&gt;

&lt;p&gt;It was burning &lt;strong&gt;5.2 million tokens a month&lt;/strong&gt;. I knew that because Anthropic's invoice told me. What the invoice did not tell me was where the tokens were going. The agent's logs said "PR-1234 reviewed in 3 turns, 14k tokens." That math should not add up to 5.2M unless the agent is reviewing roughly 370 PRs a month. The team ships about 80 PRs a month.&lt;/p&gt;

&lt;p&gt;So I turned on per-call tracing for 30 days. By the end of the month I had found four bottlenecks the existing logs were structurally unable to surface. Together they were eating 47% of the monthly token bill while contributing zero new behavior. Fixing them cut the bill in half without changing what the agent does.&lt;/p&gt;

&lt;p&gt;This post is the four bottlenecks, the trace query that found each one, and the fix.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffesp09t7f01e6a4all21.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffesp09t7f01e6a4all21.png" alt="Four hidden bottlenecks ate 47% of monthly tokens — tool-call retry loop, context re-fetch, sub-agent duplication, sycophancy preamble" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I instrumented
&lt;/h2&gt;

&lt;p&gt;I am not going to claim a perfect setup. What I did was the smallest amount of tracing that gave me ground truth. Specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One span per LLM call (&lt;code&gt;anthropic.messages.create&lt;/code&gt;) with attributes &lt;code&gt;{model, input_tokens, output_tokens, cached_tokens, stop_reason}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;One span per tool call (&lt;code&gt;tool.call&lt;/code&gt;) with &lt;code&gt;{server_name, tool_name, input_size_bytes, output_size_bytes, duration_ms, error}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;One span per "agent turn" wrapping both LLM call and tool calls&lt;/li&gt;
&lt;li&gt;Trace IDs threaded through every sub-agent spawn so I could group by the originating PR&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I used OpenTelemetry with the GenAI semantic conventions (the 2026-03 revision, which is the one most exporters agree on right now). Storage was Pydantic Logfire because it groks the GenAI attributes out of the box and the free tier covered 30 days of one agent. Helicone and Langfuse work fine too; the brand of the dashboard does not matter, the per-call span does.&lt;/p&gt;

&lt;p&gt;The "per-call" is the part that matters. Aggregated metrics ("avg input_tokens by hour") would have shown me the bill going up and not why. Per-call spans let me ask "what was in the input on the 14 most expensive calls last week" and answer with one query.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottleneck 1: a tool-call retry loop nobody saw (18% of tokens)
&lt;/h2&gt;

&lt;p&gt;The first query I ran was "show me agent turns where the same tool was called more than twice in a row." This is the kind of thing you would never look at in aggregate.&lt;/p&gt;

&lt;p&gt;It came back with one offender: a Stripe API integration that was returning 429 rate-limit errors during peak hours. The Stripe MCP server had no backoff. The agent's behavior on tool error was to retry up to 7 times within a single turn. Each retry re-sent the full prompt context, because LLM calls do not have built-in idempotency. Seven retries on a 14k-token prompt is roughly 100k tokens to discover that Stripe is busy.&lt;/p&gt;

&lt;p&gt;This was happening on roughly 30% of PRs that touched payment code. None of the agent's user-facing logs mentioned the retries because the agent successfully completed the turn after Stripe came back. From the outside, everything was fine. From the trace, it was the single biggest line item in the bill: about &lt;strong&gt;18% of monthly tokens&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Fix: backoff with jitter in the Stripe MCP server, and a hard cap of 2 retries per tool per turn at the agent level. Six lines of code. The fix shipped on day 9 of the 30-day tracing window and the next invoice cycle reflected it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottleneck 2: context re-fetch on every turn (14% of tokens)
&lt;/h2&gt;

&lt;p&gt;The agent's design has it re-reading &lt;code&gt;CLAUDE.md&lt;/code&gt; at the start of every turn. This was deliberate when I built it; I wanted the agent to pick up changes to the rules without a restart. It is also approximately 4,000 tokens of context for the file alone, plus the agent generally needs 2-3 supporting files per turn (the codebase's &lt;code&gt;README.md&lt;/code&gt;, an &lt;code&gt;OWNERS.md&lt;/code&gt;, and a &lt;code&gt;style.md&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Per-call tracing showed me that the average PR review involved &lt;strong&gt;14 turns&lt;/strong&gt;, and each turn re-fetched all four context files. Total context tokens per PR review: around 56k tokens, of which maybe 8k were genuinely needed (the diff and one or two relevant source files).&lt;/p&gt;

&lt;p&gt;Fix: introduce a turn-level cache for read-only files using Anthropic's prompt-caching API (which existed but I had never bothered to wire in). The agent now reads &lt;code&gt;CLAUDE.md&lt;/code&gt; etc. once per session with &lt;code&gt;cache_control: ephemeral&lt;/code&gt;, and subsequent turns hit the cache at 10% the cost. The aggregate effect was a &lt;strong&gt;14% drop in monthly tokens&lt;/strong&gt;, with no behavior change.&lt;/p&gt;

&lt;p&gt;The trace query that found this: "group by &lt;code&gt;tool_name == read_file&lt;/code&gt; AND &lt;code&gt;input.path == 'CLAUDE.md'&lt;/code&gt;, then count per agent session." If I had been looking at the dashboard's "average tokens per turn" chart I would never have seen it because the average was hiding the multiplier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottleneck 3: sub-agent fan-out duplication (9% of tokens)
&lt;/h2&gt;

&lt;p&gt;When the agent spawns three sub-agents for code review (architect / security / performance), it was passing the full PR diff to each one independently. The diff was, on average, 3,000 tokens. Three sub-agents getting 3,000 tokens of diff = 9,000 tokens of duplicate context per PR. Over 80 PRs a month, that is 720k tokens spent re-sending the same diff.&lt;/p&gt;

&lt;p&gt;Fix: pass the diff once to a parent context, have the three sub-agents reference the same cached block. With prompt caching, the second and third sub-agents pay 10% of the input cost on the shared context. Same effect: roughly &lt;strong&gt;9% monthly drop&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The trace query: "group spans by &lt;code&gt;parent_trace_id&lt;/code&gt; and look at input similarity across child spans." Most observability tools cannot answer this out of the box; I exported the spans to a Jupyter notebook and ran a quick diff. The duplication was 92% byte-for-byte.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottleneck 4: the sycophancy preamble (6% of tokens)
&lt;/h2&gt;

&lt;p&gt;This is the cheap one and the funniest one. Across about 40% of the agent's responses, the model was opening with some variation of "You're absolutely right" or "Great question" followed by a short paraphrase of the prompt before answering. Per turn, that adds up to roughly 120 tokens of output that contributes zero information.&lt;/p&gt;

&lt;p&gt;Per turn, 120 tokens is nothing. Over a month of an agent making roughly 1,100 turns, with output tokens priced higher than input, it added up to about &lt;strong&gt;6% of monthly tokens&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Fix: a &lt;code&gt;system&lt;/code&gt; instruction that says "Do not restate the user's request or open with agreement. Begin with the answer." Output tokens per turn dropped by an average of 95 within a day.&lt;/p&gt;

&lt;p&gt;The trace query: "show me the first 200 characters of &lt;code&gt;output_text&lt;/code&gt; across all turns this week." Half of them started the same way. This is the kind of thing you only see when you can look at the actual content, not the metrics.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the dashboard could not show me
&lt;/h2&gt;

&lt;p&gt;I keep coming back to this because it took me a while to internalize.&lt;/p&gt;

&lt;p&gt;The aggregated metrics I had before (average tokens per turn, total cost per day, p95 latency) showed me that the bill was going up. They could not show me which behavior of the agent was responsible. The bottlenecks above were all "this turn cost a normal amount; there are just a lot of these turns" patterns. Aggregates hide them by design.&lt;/p&gt;

&lt;p&gt;Per-call tracing is annoying. It produces a lot of data. The Logfire UI for one month of one agent had about 1.4 million spans. You cannot read them. You can query them, which is the entire point. Every one of the four bottlenecks was a one-line trace query that I could not have asked of any aggregate dashboard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5xtdzjjcmhui6n4w4tvv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5xtdzjjcmhui6n4w4tvv.png" alt="Monthly token bill: 5.2M before tracing, 2.8M after fixing the 4 bottlenecks — 47% cheaper, same output" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I run now
&lt;/h2&gt;

&lt;p&gt;Three things I left in place after the 30 days:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Per-call spans, always on.&lt;/strong&gt; The instrumentation cost is roughly 2% in latency overhead and negligible in storage cost on the free tiers. I do not turn it off when I am "done debugging" because the next bottleneck will look exactly like these four did: silently expensive, invisible to aggregates.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A weekly trace audit.&lt;/strong&gt; Every Monday I run six saved queries (the four above plus two on tail latency and error patterns). It takes 10 minutes. It catches one new issue roughly every 6-8 weeks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A budget alert at 80% of last month.&lt;/strong&gt; If the agent's token consumption is on pace to beat last month by 20% with no design change, something is wrong. The alert fires before the invoice does.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I do not do
&lt;/h2&gt;

&lt;p&gt;I do not trust the agent's user-facing logs to tell me what it is doing. The agent's own logs are a summary. The summary is written by the same model whose behavior I am trying to measure. There is no version of that loop that is not going to flatter itself.&lt;/p&gt;

&lt;p&gt;I also do not use only token-cost metrics. The four bottlenecks above are all "I am paying for behavior I do not want." The next four will probably be "I am paying for behavior I do want but pricing has changed" or "I am paying for tail latency in a tool I depend on." Those need different queries. Per-call spans are the substrate that lets me write the next query whenever I think of it.&lt;/p&gt;

&lt;p&gt;The lesson, if there is one, is the same as it was 30 years ago for backend services: you cannot manage what you cannot see, and aggregates are not seeing, they are summarizing. AI agents are a system. Treat them like one.&lt;/p&gt;




&lt;p&gt;The longer write-up on the OpenTelemetry GenAI conventions, the per-platform tracing setup (Logfire / Helicone / Langfuse), and the W3C trace-context plumbing that connects sub-agents to their parents is in &lt;a href="https://kenimoto.dev/books/harness-engineering-guide?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=agent-tracing-30d" rel="noopener noreferrer"&gt;Observability Across Frontend and Backend&lt;/a&gt;. The harness chapter is where the budget-alert loop lives.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>observability</category>
      <category>performance</category>
    </item>
    <item>
      <title>I Wired 8 MCP Servers Into One Claude Agent. 3 Pairs Quietly Fought Over the Same Tool Name.</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Wed, 27 May 2026 11:00:00 +0000</pubDate>
      <link>https://dev.to/kenimo49/i-wired-8-mcp-servers-into-one-claude-agent-3-pairs-quietly-fought-over-the-same-tool-name-46ff</link>
      <guid>https://dev.to/kenimo49/i-wired-8-mcp-servers-into-one-claude-agent-3-pairs-quietly-fought-over-the-same-tool-name-46ff</guid>
      <description>&lt;p&gt;Eight MCP servers in one &lt;code&gt;claude_desktop_config.json&lt;/code&gt;. No error on boot. No warning on tool registration. Six days of using the agent before I noticed that "search" was sometimes hitting Brave and sometimes hitting my local filesystem, and "create_issue" had silently routed every issue I created that week into Linear when I thought I was filing them on GitHub.&lt;/p&gt;

&lt;p&gt;It turns out MCP, as of the 2026-03 spec, has no built-in namespace for tool names. Two servers can register &lt;code&gt;list_files&lt;/code&gt; and the client (Claude in my case) will use whatever map it built last. There is no collision detection. There is no warning. There is a registry that quietly overwrites.&lt;/p&gt;

&lt;p&gt;This post is what I found when I sat down and audited the 8-server registration on day six, what each silent collision actually did, and the three-line config change that has kept me at zero collisions for six weeks since.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy6x4uuh1ibu2s22q6jwp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy6x4uuh1ibu2s22q6jwp.png" alt="Three pairs of MCP servers fighting over the same tool name — search, create_issue, list_files" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The 8 servers and why each one was there
&lt;/h2&gt;

&lt;p&gt;For context, this is not a stunt setup. Each server earned its slot for a real task I run weekly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;brave-search&lt;/code&gt; — web search for fact-checking&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;filesystem&lt;/code&gt; — read/write inside an Obsidian vault&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;github&lt;/code&gt; — issue and PR ops on my own repos&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;linear&lt;/code&gt; — issue and project ops on a client repo&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;s3&lt;/code&gt; — read access to a private logs bucket&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;freee&lt;/code&gt; — tax/expense ops (Japanese accounting service)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;slack&lt;/code&gt; — read-only on two channels for catch-up summaries&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;postgres&lt;/code&gt; — read-only on a personal analytics DB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eight servers, totalling 87 tools when Claude finished registering them. Around 4,400 tokens of tool descriptions in the system prompt, which is its own problem (separate post). The thing I want to talk about is the names.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three collisions
&lt;/h2&gt;

&lt;p&gt;When I dumped the registered tool list and grouped by name, three pairs had collided. Two of them I could have predicted in retrospect. The third I would not have, and it is the one that scared me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collision 1: &lt;code&gt;search&lt;/code&gt;.&lt;/strong&gt; Both &lt;code&gt;brave-search&lt;/code&gt; and &lt;code&gt;filesystem&lt;/code&gt; registered a tool named &lt;code&gt;search&lt;/code&gt;. The Brave one takes a query and returns web results. The filesystem one takes a query and greps the Obsidian vault. They have completely different argument schemas. Claude was choosing based on which definition got loaded last on boot, which in turn depended on file order in the config (alphabetical, then filesystem won). When I asked "search for the latest Anthropic safety paper," Claude ran a regex over my Obsidian vault and confidently told me there was no result. That was the bug that started the audit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collision 2: &lt;code&gt;create_issue&lt;/code&gt;.&lt;/strong&gt; Both &lt;code&gt;github&lt;/code&gt; and &lt;code&gt;linear&lt;/code&gt; registered &lt;code&gt;create_issue&lt;/code&gt;. Same name, same overall shape (title, body, labels), incompatible everything else (&lt;code&gt;linear&lt;/code&gt; wants a &lt;code&gt;teamId&lt;/code&gt;, &lt;code&gt;github&lt;/code&gt; wants &lt;code&gt;owner&lt;/code&gt; and &lt;code&gt;repo&lt;/code&gt;). When I asked Claude to "open an issue about the asyncpg regression," it called the second-loaded one, which was Linear. The issue went into a client project where it did not belong, with a body that mentioned my private Postgres schema. I closed it quickly. The fact that I had to is the point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collision 3: &lt;code&gt;list_files&lt;/code&gt;.&lt;/strong&gt; Both &lt;code&gt;filesystem&lt;/code&gt; and &lt;code&gt;s3&lt;/code&gt; registered &lt;code&gt;list_files&lt;/code&gt;. When I asked Claude to "list the files in the inbox folder," it ran the s3 version, listed every object in the bucket prefix &lt;code&gt;inbox/&lt;/code&gt;, and stuffed about 31,000 tokens of file metadata into the context. The session was effectively burned. I had to start a new one. The bucket has roughly 40k objects in it. The local &lt;code&gt;inbox/&lt;/code&gt; directory has 12.&lt;/p&gt;

&lt;p&gt;None of these throw an error. The MCP client (Claude Desktop / Claude Code) sees a flat tool registry. Last write wins. Period.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the spec lets this happen
&lt;/h2&gt;

&lt;p&gt;I went and re-read the &lt;a href="https://modelcontextprotocol.io/specification" rel="noopener noreferrer"&gt;Model Context Protocol spec&lt;/a&gt; (2026-03-26 revision) to confirm I was not missing something. I was not. The &lt;code&gt;tools/list&lt;/code&gt; response from a server returns tool names as flat strings. There is no &lt;code&gt;namespace&lt;/code&gt; field. There is no &lt;code&gt;server_id&lt;/code&gt; qualifier. The client is expected to flatten the tool lists from all servers into a single map. The spec does not say what to do on collision because, in the spec's mental model, a collision is a configuration problem.&lt;/p&gt;

&lt;p&gt;That is technically correct and operationally insufficient. Anyone wiring more than two MCP servers will hit a collision eventually because the names that show up are exactly the names you would pick yourself: &lt;code&gt;search&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;, &lt;code&gt;get&lt;/code&gt;, &lt;code&gt;create&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;. They are not safe by accident.&lt;/p&gt;

&lt;p&gt;There is a &lt;a href="https://github.com/modelcontextprotocol/specification/issues" rel="noopener noreferrer"&gt;pending proposal (#287)&lt;/a&gt; to add namespace prefixes server-side, dated around early 2026, but as of writing it has not landed and the client implementations have not picked it up. So this is an "until further notice" problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I added
&lt;/h2&gt;

&lt;p&gt;Three lines in my agent config. Not pretty. Effective.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"brave-search"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool_prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"brave_"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"filesystem"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool_prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fs_"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"github"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool_prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gh_"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"linear"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool_prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"linear_"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"s3"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool_prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3_"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"freee"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool_prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"freee_"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"slack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool_prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"slack_"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"postgres"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool_prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pg_"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tool_prefix&lt;/code&gt; is a client-side feature in the build of Claude Code I am running (added in the 2026-04 release; check your version). It rewrites every tool name from a server to &lt;code&gt;{prefix}{tool_name}&lt;/code&gt; before registering. Now &lt;code&gt;search&lt;/code&gt; becomes &lt;code&gt;brave_search&lt;/code&gt; and &lt;code&gt;fs_search&lt;/code&gt;, &lt;code&gt;create_issue&lt;/code&gt; becomes &lt;code&gt;gh_create_issue&lt;/code&gt; and &lt;code&gt;linear_create_issue&lt;/code&gt;, and the registry has 87 unique names.&lt;/p&gt;

&lt;p&gt;If your client does not have this feature, the same thing works at the server side: fork the server, prefix the names at the source. Uglier, same result.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8y0tvrfgfq801exdqlgl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8y0tvrfgfq801exdqlgl.png" alt="Before vs after the 3-line tool_prefix rule — 22% wrong-server hit rate dropped to 0 over 6 weeks" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measure now
&lt;/h2&gt;

&lt;p&gt;I added two checks to my agent boot:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Collision scan.&lt;/strong&gt; On startup, after all servers register, walk the tool list and assert no duplicates. Fail the boot if a duplicate exists. Three lines of code. It would have caught my problem on day one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tool-call attribution log.&lt;/strong&gt; Every tool call gets logged with &lt;code&gt;{server_name, tool_name, args_summary}&lt;/code&gt;. When something feels wrong, I can grep one day of transcripts and see whether &lt;code&gt;search&lt;/code&gt; calls went to Brave or filesystem. This is also what I used to measure the 22% wrong-server rate before the prefix change. Without attribution logging, you cannot know whether you have this problem.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The attribution log lives in &lt;code&gt;~/.claude/agent-tool-calls.jsonl&lt;/code&gt; for me. Six weeks of it is about 14 MB and has caught one other subtle bug (a freee server returning data for the wrong fiscal year) that had nothing to do with name collisions. The investment paid for itself twice in six weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do not do
&lt;/h2&gt;

&lt;p&gt;I do not run any MCP server with a generic tool name like &lt;code&gt;search&lt;/code&gt; or &lt;code&gt;list&lt;/code&gt; un-prefixed, ever, even if it is the only server registered. The cost of prefixing is around 4 tokens per tool in the description. The cost of a silent collision when you add a second server six months later is one production-shaped incident.&lt;/p&gt;

&lt;p&gt;I also do not trust client implementations to add collision warnings on my behalf. The MCP client market is moving fast. Today's "the client warns you on duplicate" feature is tomorrow's "we removed that warning because it was too noisy in this other workflow." The boot-time assertion lives in my repo. It will outlast any specific client.&lt;/p&gt;

&lt;p&gt;The lesson, if there is one, is the same as it always is with protocols that started as Just Wire Two Things Together: as soon as you have eight of anything, the assumptions the protocol made when there were two are the things that quietly bite.&lt;/p&gt;




&lt;p&gt;The longer version of this story (the OWASP MCP Top 10 in production, the file-upload workaround chain, the 55k-token system-prompt diet I am running on the same 8-server config) is in &lt;a href="https://kenimoto.dev/books/mcp-security-practice?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=8-mcp-tool-collision" rel="noopener noreferrer"&gt;Practical MCP Security&lt;/a&gt;. Chapter 6 is the auth and tool-registration audit playbook I run on every new server now.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>claudecode</category>
      <category>ai</category>
      <category>devtools</category>
    </item>
    <item>
      <title>I Benchmarked 5 Voice AI Stacks. Only 2 Stayed Under 300ms.</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Tue, 26 May 2026 22:00:00 +0000</pubDate>
      <link>https://dev.to/kenimo49/i-benchmarked-5-voice-ai-stacks-only-2-stayed-under-300ms-2bka</link>
      <guid>https://dev.to/kenimo49/i-benchmarked-5-voice-ai-stacks-only-2-stayed-under-300ms-2bka</guid>
      <description>&lt;p&gt;I kept reading that voice AI agents respond in under 300ms. AssemblyAI says it, Vapi says it, every Realtime API launch post says it. So I built five stacks, dropped a stopwatch into each pipeline, and ran the same one-minute conversation through all of them.&lt;/p&gt;

&lt;p&gt;Three of the five never came close.&lt;/p&gt;

&lt;p&gt;The other two were the ones I had quietly assumed were "marketing numbers." Turns out the marketing was right and my hand-stitched pipelines were the problem.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsy6rm5igxynw7obh7oct.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsy6rm5igxynw7obh7oct.png" alt="Bar chart of P95 latency for five voice AI stacks against the 300ms cliff. OpenAI Realtime (281ms) and LiveKit + Gemini Live (295ms) are the only stacks under the threshold." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The three cliffs nobody puts on the slide
&lt;/h2&gt;

&lt;p&gt;Before the numbers, the perception model. Voice latency does not degrade smoothly. It falls off cliffs. AssemblyAI, Vapi, and Retell all converge on roughly the same three thresholds, and after a week of user testing I now believe them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;th&gt;What the user does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0-300ms&lt;/td&gt;
&lt;td&gt;Talks normally, never thinks about the AI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;300-500ms&lt;/td&gt;
&lt;td&gt;Senses a pause, tolerates it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500-800ms&lt;/td&gt;
&lt;td&gt;Talks over the AI ("can you hear me?")&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;800-1500ms&lt;/td&gt;
&lt;td&gt;Repeats the question&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1500ms+&lt;/td&gt;
&lt;td&gt;Treats the call like an international line, gives up&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;300ms is the first cliff. Above it, the user starts noticing a machine. Above 500ms they start fighting the turn-taking model and your STT keeps resetting because they keep talking over. By 800ms, half my testers said "hello? hello?" — the universal "is this thing on" sound. I have not lived a more humbling week of code review than watching that on playback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the 300ms budget goes
&lt;/h2&gt;

&lt;p&gt;If you want to know why three of my five stacks failed, look at the budget math. A cascaded pipeline has to fit four serial things into 300ms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;STT&lt;/strong&gt; (speech-to-text): 80-300ms depending on model and VAD design&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM TTFT&lt;/strong&gt; (time to first token): 100-500ms depending on model size, context length, and cold-start&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTS TTFB&lt;/strong&gt; (time to first byte of audio): 75-300ms depending on the vocoder&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network round-trip&lt;/strong&gt;: 50-200ms, capped by the speed of light and your colo choice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add the &lt;em&gt;fastest&lt;/em&gt; number in every row and you get 305ms. Add the typical number and you get over a second. The "anatomy of latency" punchline is that a cascade is mathematically allergic to 300ms unless every component lives next to every other component.&lt;/p&gt;

&lt;p&gt;Voice-to-voice end-to-end models cheat by collapsing STT + LLM + TTS into a single forward pass over an audio token stream. There is no second hop. There is no TTS warmup. There is no inter-service hand-off. That is the whole game, and it is also why the two stacks that won are the two stacks I wrote the least code for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five stacks
&lt;/h2&gt;

&lt;p&gt;I wanted a real comparison, not a "look at my favorite vendor" post. Same one-minute customer-support script. Same WebRTC ingress (Daily.co for everything except OpenAI Realtime, which uses its own). Same prompt. Same client machine, US-East. Ten turns per stack, 50 measurements per stack. I report P50, P95, and P99 because averages lie in a way that voice users physically feel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack 1 — OpenAI Realtime API.&lt;/strong&gt; &lt;code&gt;gpt-4o-realtime&lt;/code&gt; over the official WebRTC endpoint. Voice-in, voice-out, no glue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack 2 — Deepgram + Claude + ElevenLabs cascade.&lt;/strong&gt; Deepgram Nova-3 for STT, Claude Sonnet 4.6 for the LLM, ElevenLabs Turbo v2.5 for TTS. The "best-of-breed" cascade you would draw on a whiteboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack 3 — Local Edge (Whisper + Llama + Coqui).&lt;/strong&gt; Whisper Large v3 Turbo, Llama 3.3 70B local on a single H100, Coqui XTTS for TTS. Network round-trip: 0ms. This is the "privacy and sovereignty" answer that Hacker News loves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack 4 — LiveKit Agents + Gemini 2.0 Flash Live.&lt;/strong&gt; LiveKit's agents framework as the media plane, Google's native-audio Gemini Live for the brain. Also voice-to-voice end-to-end, but through a different SDK.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack 5 — Pipecat + Claude + Cartesia.&lt;/strong&gt; Pipecat as the orchestrator, Claude Sonnet 4.6 for the LLM, Cartesia Sonic for the TTS. A more opinionated cascade with a faster TTS than ElevenLabs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stack&lt;/th&gt;
&lt;th&gt;P50&lt;/th&gt;
&lt;th&gt;P95&lt;/th&gt;
&lt;th&gt;P99&lt;/th&gt;
&lt;th&gt;Under 300ms?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. OpenAI Realtime (voice-to-voice)&lt;/td&gt;
&lt;td&gt;232ms&lt;/td&gt;
&lt;td&gt;281ms&lt;/td&gt;
&lt;td&gt;320ms&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Deepgram + Claude + ElevenLabs&lt;/td&gt;
&lt;td&gt;480ms&lt;/td&gt;
&lt;td&gt;624ms&lt;/td&gt;
&lt;td&gt;780ms&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Whisper + Llama 70B + Coqui (local)&lt;/td&gt;
&lt;td&gt;870ms&lt;/td&gt;
&lt;td&gt;980ms&lt;/td&gt;
&lt;td&gt;1,210ms&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. LiveKit + Gemini Live (voice-to-voice)&lt;/td&gt;
&lt;td&gt;250ms&lt;/td&gt;
&lt;td&gt;295ms&lt;/td&gt;
&lt;td&gt;360ms&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Pipecat + Claude + Cartesia&lt;/td&gt;
&lt;td&gt;410ms&lt;/td&gt;
&lt;td&gt;540ms&lt;/td&gt;
&lt;td&gt;670ms&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Stack 1 and Stack 4 are the only two that stayed under 300ms at P95. Both are voice-to-voice. Both ship a single forward pass instead of a relay race. Stack 5 is what a careful cascade looks like (Cartesia's TTS is genuinely fast: 90ms TTFB) and it still cannot beat the cliff because LLM TTFT plus inter-service hops eat the budget.&lt;/p&gt;

&lt;p&gt;Stack 3 is the painful one. I had hoped local would at least beat the cascade because of zero network. It does, sometimes. But Llama 3.3 70B is not small, and "no network" does not save you when LLM TTFT alone is 600ms on commodity GPU. The honest read on edge AI is that today's realistic edge win is &lt;em&gt;smaller&lt;/em&gt; models (Qwen2.5 1.5B class), not full-fat 70B local. A 70B model on local hardware is the worst of both worlds: you pay for the GPU and still miss the cliff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why voice-to-voice wins (today)
&lt;/h2&gt;

&lt;p&gt;Three reasons, in decreasing order of how much they shocked me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One — no TTFT-then-TTFB stacking.&lt;/strong&gt; In a cascade, you wait for the LLM's first token, then start the TTS, which has its own first-byte latency. Voice-to-voice emits audio tokens directly. There is no second warmup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two — no hand-off serialization.&lt;/strong&gt; Deepgram → Claude → ElevenLabs is three separate API endpoints. Even if each is fast, you pay TLS, connection pooling, and frame-buffer overhead three times. Pipecat helps but does not erase it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three — VAD-aware turn-taking.&lt;/strong&gt; The voice-to-voice models do their own endpoint detection from the audio stream. Cascades have to wait for a VAD signal to commit the STT output, then send it. That commitment delay is invisible in benchmarks that start measuring from "user stops speaking" — but the user does not know when they "officially" stopped. They feel it as silence.&lt;/p&gt;

&lt;p&gt;The cheap way to hit 300ms in May 2026 is to skip writing the pipeline. Most of my latency was my code.&lt;/p&gt;

&lt;h2&gt;
  
  
  When edge AI catches up
&lt;/h2&gt;

&lt;p&gt;Edge is the right answer for the right shape of problem — local-only privacy, no-network kiosks, offline robotics. It is not yet the right answer for "I want a sub-300ms cloud agent." Whisper v3 Turbo posts a real-time factor north of 1000x and 1.5B-class LLMs can return first tokens in 200ms on CPU. That combination — small model, fast STT, local TTS — can hit 300-350ms total. The 70B-on-H100 path I tested in Stack 3 cannot.&lt;/p&gt;

&lt;p&gt;The other path is hybrid: edge STT, cloud LLM, cloud TTS. You skip the network round trip on the longest synchronous step (capturing audio frames) and you keep cloud-grade model quality for the brain. 350-500ms is realistic, sub-300ms cloud cascade is not.&lt;/p&gt;

&lt;p&gt;For more on the perception side (how to make a 500ms agent &lt;em&gt;feel&lt;/em&gt; like a 300ms agent), I wrote a companion piece on &lt;a href="https://dev.to/kenimo49/voice-perception-hacks-i-kept-the-pipeline-at-540ms-and-users-still-said-instant-3oki"&gt;perception hacks for voice AI&lt;/a&gt;. Filler audio, micro-confirmations, and progressive token playback can buy you a cliff's worth of perceived speed. They do not make the cliff move.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would build today
&lt;/h2&gt;

&lt;p&gt;If I were starting a voice agent in May 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Greenfield consumer product&lt;/strong&gt; — OpenAI Realtime or Gemini Live, direct. Stop sooner than you think you should and just ship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need Claude in the loop&lt;/strong&gt; — Pipecat + Claude + Cartesia. You will live at 500-600ms P95. Plan your filler strategy now, not later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy or air-gap requirement&lt;/strong&gt; — Whisper Turbo + Qwen2.5 1.5B + local TTS. Aim for 350ms TTFB. Forget 70B local until the next GPU generation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise telephony&lt;/strong&gt; — Hybrid: edge STT, cloud voice-to-voice for the brain. The PSTN codec layer already kills your latency advantage, so optimize for quality of turn-taking instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The deepest mistake I made was assuming "300ms" was a property of the &lt;em&gt;model&lt;/em&gt; I picked. It is a property of the &lt;em&gt;architecture&lt;/em&gt; I picked. The model just decides how comfortable the architecture is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/kenimo49/your-voice-agent-has-300ms-before-users-bail-here-are-the-3-latency-cliffs-that-decide-everything-2g2k"&gt;Your Voice Agent Has 300ms Before Users Bail&lt;/a&gt; — same cliff, different angle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the full latency anatomy, perception model, and the edge AI chapter that informed Stack 3, the research is packaged in a book.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://kenimoto.dev/books/voice-ai-300ms-ux?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=five-voice-ai-stacks-300ms" rel="noopener noreferrer"&gt;Voice AI 300ms UX: Design and Engineer the Conversation Cliff&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webrtc</category>
      <category>performance</category>
      <category>llm</category>
    </item>
    <item>
      <title>5 AI Crawlers Hit My Sites 14,300 Times in 30 Days. Here's What Their User-Agents Told Me About LLMO.</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Tue, 26 May 2026 11:00:01 +0000</pubDate>
      <link>https://dev.to/kenimo49/5-ai-crawlers-hit-my-sites-14300-times-in-30-days-heres-what-their-user-agents-told-me-about-4okh</link>
      <guid>https://dev.to/kenimo49/5-ai-crawlers-hit-my-sites-14300-times-in-30-days-heres-what-their-user-agents-told-me-about-4okh</guid>
      <description>&lt;p&gt;I thought &lt;code&gt;robots.txt&lt;/code&gt; was the boundary. Three lines of &lt;code&gt;Disallow:&lt;/code&gt; and I'd told the AI bots where they could and couldn't go. Done. I went back to writing posts about LLMO measurement, citation rates, and AI referral traffic in GA4.&lt;/p&gt;

&lt;p&gt;Then I opened the access logs for three of my sites and the picture I had in my head collapsed.&lt;/p&gt;

&lt;p&gt;This is what I learned reading thirty days of raw server logs from &lt;code&gt;kenimoto.dev&lt;/code&gt;, &lt;code&gt;kaoriq.com&lt;/code&gt;, and &lt;code&gt;llmoframework.com&lt;/code&gt;. Five User-Agent strings dominated everything. The traffic patterns each one created told me more about my LLMO standing than any GA4 dashboard had.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhntrqra5ov9oqlvv52ey.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhntrqra5ov9oqlvv52ey.png" alt="Bar chart of 5 top AI crawler hits over 30 days: GPTBot 4,212; ClaudeBot 3,108; PerplexityBot 2,790; OAI-SearchBot 2,043; Google-Extended 1,387. Together 94.7% of AI traffic." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I started reading logs in the first place
&lt;/h2&gt;

&lt;p&gt;Most LLMO measurement advice tells you to track the &lt;em&gt;outbound&lt;/em&gt; side: did ChatGPT cite me, did Perplexity link to me, did Google AI Overviews show me. That's the citation side.&lt;/p&gt;

&lt;p&gt;The other side, where AI services actually pull HTML from my server, is invisible in GA4. AI crawlers don't fire JavaScript. They don't trigger gtag. They show up in raw HTTP access logs and nowhere else.&lt;/p&gt;

&lt;p&gt;I'd been writing LLMO posts for months and had never once looked at the side of the funnel I could actually control. So I exported 30 days of logs from Cloudflare (&lt;code&gt;kenimoto.dev&lt;/code&gt;, &lt;code&gt;kaoriq.com&lt;/code&gt;) and Vercel (&lt;code&gt;llmoframework.com&lt;/code&gt;), grepped for known AI User-Agents, and started counting.&lt;/p&gt;

&lt;p&gt;The total: &lt;strong&gt;14,300 AI crawler hits across three sites in 30 days.&lt;/strong&gt; Roughly 477 hits per day per site. More than I expected. Less than I think it should be in another six months.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5 crawlers that hit me most
&lt;/h2&gt;

&lt;p&gt;Here's the ranked list. Hits are deduplicated by &lt;code&gt;(timestamp, path, IP)&lt;/code&gt; so cache retries don't inflate the count.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rank&lt;/th&gt;
&lt;th&gt;User-Agent&lt;/th&gt;
&lt;th&gt;30-day hits&lt;/th&gt;
&lt;th&gt;Operator&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GPTBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4,212&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;Training data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ClaudeBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3,108&lt;/td&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;Training + retrieval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PerplexityBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2,790&lt;/td&gt;
&lt;td&gt;Perplexity&lt;/td&gt;
&lt;td&gt;Answer index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OAI-SearchBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2,043&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;ChatGPT search citations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Google-Extended&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,387&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;Gemini training&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Five User-Agents. 13,540 hits. That's 94.7% of all AI traffic. The remaining 5.3% was a long tail: &lt;code&gt;Bytespider&lt;/code&gt;, &lt;code&gt;Applebot-Extended&lt;/code&gt;, &lt;code&gt;Meta-ExternalAgent&lt;/code&gt;, &lt;code&gt;Amazonbot&lt;/code&gt;, &lt;code&gt;cohere-ai&lt;/code&gt;, a smattering of &lt;code&gt;Claude-User&lt;/code&gt;, and two hits from something that called itself &lt;code&gt;anthropic-ai&lt;/code&gt; (the old UA Anthropic supposedly retired).&lt;/p&gt;

&lt;p&gt;Before you read too much into the order: this is &lt;em&gt;my&lt;/em&gt; data, three small sites, mostly English/Japanese tech content. Your ranking will look different. The shape of it (a handful of bots accounting for most hits, OpenAI and Anthropic at the top) is probably the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each one is actually doing
&lt;/h2&gt;

&lt;p&gt;The reason rank order matters less than the &lt;em&gt;purpose&lt;/em&gt; of each bot is that the three buckets behave completely differently in LLMO terms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Training crawlers&lt;/strong&gt; read your content to potentially update model weights. They show up consistently, follow &lt;code&gt;robots.txt&lt;/code&gt; (usually), and don't care about your content being "fresh." &lt;code&gt;GPTBot&lt;/code&gt;, &lt;code&gt;Google-Extended&lt;/code&gt;, &lt;code&gt;Bytespider&lt;/code&gt;, &lt;code&gt;Applebot-Extended&lt;/code&gt;, and &lt;code&gt;anthropic-ai&lt;/code&gt; (legacy) fall here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retrieval crawlers&lt;/strong&gt; index your content so it can be cited in real-time answers. They re-fetch popular pages, follow &lt;code&gt;Last-Modified&lt;/code&gt;, and have a measurable crawl-to-refer ratio. &lt;code&gt;OAI-SearchBot&lt;/code&gt;, &lt;code&gt;PerplexityBot&lt;/code&gt;, &lt;code&gt;Claude-SearchBot&lt;/code&gt; (newer, independently controllable from &lt;code&gt;ClaudeBot&lt;/code&gt;), and &lt;code&gt;GoogleOther&lt;/code&gt; belong here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User-initiated fetches&lt;/strong&gt; happen when a human pastes your URL into ChatGPT or asks Claude to read it. These are &lt;code&gt;ChatGPT-User&lt;/code&gt;, &lt;code&gt;Perplexity-User&lt;/code&gt;, and &lt;code&gt;Claude-User&lt;/code&gt;. They don't follow &lt;code&gt;robots.txt&lt;/code&gt; (per &lt;a href="https://developers.openai.com/api/docs/bots" rel="noopener noreferrer"&gt;OpenAI's revised crawler docs&lt;/a&gt;, because they're user actions, not crawls).&lt;/p&gt;

&lt;p&gt;I had been treating all of these as the same animal. They are not. If your goal is "get cited in ChatGPT Search," &lt;code&gt;OAI-SearchBot&lt;/code&gt; hits matter and &lt;code&gt;GPTBot&lt;/code&gt; hits are basically noise. If your goal is "be in the training set of the next Claude," it's exactly inverted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who actually obeys robots.txt
&lt;/h2&gt;

&lt;p&gt;Here's the part that flipped my view of &lt;code&gt;robots.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On &lt;code&gt;kenimoto.dev&lt;/code&gt;, I had a &lt;code&gt;Disallow: /api/&lt;/code&gt; rule. Over 30 days:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GPTBot&lt;/code&gt;: 0 hits to &lt;code&gt;/api/&lt;/code&gt;. Compliant.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Google-Extended&lt;/code&gt;: 0 hits to &lt;code&gt;/api/&lt;/code&gt;. Compliant.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ClaudeBot&lt;/code&gt;: 0 hits to &lt;code&gt;/api/&lt;/code&gt;. Compliant.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;OAI-SearchBot&lt;/code&gt;: 3 hits to &lt;code&gt;/api/&lt;/code&gt;. Borderline. Possibly cached before the rule, possibly the &lt;a href="https://ppc.land/openai-revises-chatgpt-crawler-documentation-with-significant-policy-changes/" rel="noopener noreferrer"&gt;revised compliance language&lt;/a&gt; is doing something subtle.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PerplexityBot&lt;/code&gt;: 41 hits to &lt;code&gt;/api/&lt;/code&gt; in one 90-second burst. Not compliant on this run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Forty-one hits is not a sample of one. The 90-second burst pattern matched a &lt;a href="https://www.appearonai.com/insights/ai-crawler-configuration-robots-txt-guide" rel="noopener noreferrer"&gt;public report&lt;/a&gt; where Perplexity was observed ignoring &lt;code&gt;User-agent: PerplexityBot&lt;/code&gt; blocks when answering an active user query. The behavior makes more sense if you think of &lt;code&gt;PerplexityBot&lt;/code&gt; as straddling the retrieval/user-initiated line: it acts like a retrieval crawler on the calm days, and a user-initiated fetch when somebody is waiting on an answer.&lt;/p&gt;

&lt;p&gt;The takeaway I wrote down: &lt;strong&gt;&lt;code&gt;robots.txt&lt;/code&gt; is a self-reported boundary&lt;/strong&gt;. Three of five top crawlers honored it cleanly on my data. One was iffy. One did whatever it wanted when a human was on the other end. Plan accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three LLMO signals you can derive from this
&lt;/h2&gt;

&lt;p&gt;The reason I'm writing this down is that crawler hit data is a measurable LLMO signal, and I haven't seen it discussed much next to the usual citation-rate metrics. Three things I now look at every week:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Crawler diversity.&lt;/strong&gt; If only &lt;code&gt;GPTBot&lt;/code&gt; hits your site and nothing else, your retrieval surface is OpenAI-only. You're invisible to Claude, Perplexity, and Gemini's retrieval paths even if you're cited in ChatGPT. A healthy crawler-diversity score is at least three of the five top User-Agents hitting you regularly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Retrieval-to-training ratio.&lt;/strong&gt; If you sum retrieval-side hits (&lt;code&gt;OAI-SearchBot&lt;/code&gt; + &lt;code&gt;PerplexityBot&lt;/code&gt; + &lt;code&gt;Claude-SearchBot&lt;/code&gt; + &lt;code&gt;GoogleOther&lt;/code&gt;) and divide by training-side hits (&lt;code&gt;GPTBot&lt;/code&gt; + &lt;code&gt;Google-Extended&lt;/code&gt; + &lt;code&gt;anthropic-ai&lt;/code&gt;), you get a number that tells you whether the AI ecosystem thinks of you as "content to be learned from" or "content to be cited right now." Mine sits at 0.81. Anything below 0.5 means your content isn't fresh enough to be retrieved in real time. Anything above 1.5 means you're being actively used in answers (good) but probably plateauing as training material (worth noticing).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;llms.txt&lt;/code&gt; fetch rate.&lt;/strong&gt; Of the five top crawlers, only &lt;code&gt;PerplexityBot&lt;/code&gt; and &lt;code&gt;ClaudeBot&lt;/code&gt; fetched &lt;code&gt;/llms.txt&lt;/code&gt; on my sites during the 30-day window. &lt;code&gt;GPTBot&lt;/code&gt;, &lt;code&gt;OAI-SearchBot&lt;/code&gt;, and &lt;code&gt;Google-Extended&lt;/code&gt; never did. This roughly matches what other operators have observed and is a load-bearing detail when you're deciding whether &lt;code&gt;llms.txt&lt;/code&gt; is worth maintaining. (Short answer: yes, but mostly for the two crawlers that read it.) The &lt;a href="https://llmoframework.com/retrieval-signals/" rel="noopener noreferrer"&gt;llmoframework.com retrieval signals page&lt;/a&gt; goes deeper on this.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to actually pull this data
&lt;/h2&gt;

&lt;p&gt;This is the part I always wanted to read and never quite found, so:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare (free plan).&lt;/strong&gt; The AI Crawl Control dashboard (formerly AI Audit, &lt;a href="https://developers.cloudflare.com/ai-crawl-control/" rel="noopener noreferrer"&gt;docs here&lt;/a&gt;) shows top AI crawler User-Agents out of the box. For raw logs, you need Logpush, which is paid. On free, the easiest substitute is enabling "AI Audit" + filtering Analytics by known AI User-Agents. Free won't give you per-request paths but it gives you counts and trends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vercel.&lt;/strong&gt; Project → Logs → filter by &lt;code&gt;User-Agent contains "Bot"&lt;/code&gt;. Vercel keeps 30 days of edge logs on the Pro plan. On Hobby, you get less, and you'll want to forward to a log drain if you're serious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Netlify / self-hosted Nginx.&lt;/strong&gt; Just &lt;code&gt;grep&lt;/code&gt; the access log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"GPTBot|ClaudeBot|PerplexityBot|OAI-SearchBot|Google-Extended"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /var/log/nginx/access.log &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $14}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you crawler counts. Add &lt;code&gt;awk '{print $7}'&lt;/code&gt; instead of &lt;code&gt;$14&lt;/code&gt; to get the URL ranking. The exact field number depends on your log format; check with &lt;code&gt;awk '{print NF}'&lt;/code&gt; on one line to count.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed after looking at all this
&lt;/h2&gt;

&lt;p&gt;Three concrete changes after the 30-day window:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I split my &lt;code&gt;robots.txt&lt;/code&gt; to allow &lt;code&gt;OAI-SearchBot&lt;/code&gt; and &lt;code&gt;Claude-SearchBot&lt;/code&gt; (retrieval, good for citations) while keeping &lt;code&gt;Disallow: /api/&lt;/code&gt; strict for &lt;code&gt;GPTBot&lt;/code&gt; (training, no upside for me on those endpoints).&lt;/li&gt;
&lt;li&gt;I added a &lt;code&gt;Last-Modified&lt;/code&gt; header to every blog post route, because retrieval crawlers use it to decide re-fetch frequency and Vercel wasn't sending one by default.&lt;/li&gt;
&lt;li&gt;I started tracking the retrieval-to-training ratio weekly in a spreadsheet. Two weeks in, the only useful insight is that the number is stable. That just means my crawler diet isn't lurching around week to week, but it's a baseline I didn't have before.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I expected the logs to confirm what I already believed about LLMO. They mostly didn't. Citation isn't the only signal worth watching. Who's pulling your pages is a separate question, and the answer is written in plain text in a log file you probably already have.&lt;/p&gt;

&lt;p&gt;If you want the full measurement frame (citation tracking, GA4 referrals, and server-log crawler analysis as parts of one system) the book is here: &lt;a href="https://kenimoto.dev/books/llmo-ai-search-optimization?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=five-ai-crawlers-30-days" rel="noopener noreferrer"&gt;LLMO: AI Search Optimization&lt;/a&gt;. Chapter 10 is the measurement chapter; this post is basically the missing seventh KPI it didn't have room for.&lt;/p&gt;

</description>
      <category>llmo</category>
      <category>ai</category>
      <category>seo</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Added a 4th Agent That Audits My Other Agents. It Caught My Strategist Procrastinating for 3 Weeks.</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Mon, 25 May 2026 22:00:01 +0000</pubDate>
      <link>https://dev.to/kenimo49/i-added-a-4th-agent-that-audits-my-other-agents-it-caught-my-strategist-procrastinating-for-3-cg</link>
      <guid>https://dev.to/kenimo49/i-added-a-4th-agent-that-audits-my-other-agents-it-caught-my-strategist-procrastinating-for-3-cg</guid>
      <description>&lt;p&gt;I built a three-layer agent harness and called it "autonomous." Observer collected the data. Strategist picked the theme. Marketer wrote the article. They all followed &lt;code&gt;strategy.md&lt;/code&gt;, the file that holds my rules. The cron fired every Monday at 09:00 and the articles showed up by lunch. I felt very clever about it.&lt;/p&gt;

&lt;p&gt;Then I read my own Strategist logs across three weeks and noticed something. The same retreat criterion ("if Reaction rate stays under 1% for four consecutive weeks, revise the strategy") had been deferred three weeks in a row. Each week the Strategist wrote "data insufficient, observe next week" and moved on. The rule existed. The data existed. The rule never fired.&lt;/p&gt;

&lt;p&gt;The three-layer harness couldn't catch this because the three layers were doing exactly what &lt;code&gt;strategy.md&lt;/code&gt; told them to do. The bug was not in the agents. The bug was in the rules themselves, and nothing in the harness was paid to look at the rules.&lt;/p&gt;

&lt;p&gt;I added a 4th layer called Evolver. On its first real proposal it filed a diff against the exact rule my Strategist had been hiding behind.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftd7iyludqh22ddpyltj0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftd7iyludqh22ddpyltj0.png" alt="Four-layer agent harness diagram: Observer, Strategist, Marketer follow strategy.md; Evolver audits and rewrites the strategy.md itself." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The three layers were not the autonomous part
&lt;/h2&gt;

&lt;p&gt;The architecture I had been calling autonomous looked like this. Observer ran daily and dumped GA4 numbers into &lt;code&gt;article-performance.jsonl&lt;/code&gt;. Strategist ran every Monday morning, read &lt;code&gt;strategy.md&lt;/code&gt;, and picked five themes for the week. Marketer turned each theme into an article and queued it for publishing. Three roles, three cron jobs, predictable behavior.&lt;/p&gt;

&lt;p&gt;The trick that made this fast was that I had taken WebSearch away from Strategist on purpose. A Strategist with WebSearch wandered for twenty minutes per run and started picking themes that matched recent news instead of themes that matched my actual content library. Stripping WebSearch dropped the cycle from twenty minutes to three. That post was about making Strategist faster. This one is about making it accountable.&lt;/p&gt;

&lt;p&gt;The thing none of those three layers could do was rewrite &lt;code&gt;strategy.md&lt;/code&gt;. They read it every Monday and obeyed it. If the rule was wrong, they obeyed a wrong rule. The only way to change the rule was for me, the human, to notice during weekly review that a rule needed updating. And I was the bottleneck. I had not been paying attention to the retreat criteria for at least three weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the procrastination looked like in the logs
&lt;/h2&gt;

&lt;p&gt;I am going to quote my own Strategist logs because the pattern is more honest when you see it in the original.&lt;/p&gt;

&lt;p&gt;From the log dated three weeks before I added the Evolver:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Reaction rate continues at 0% for the majority of articles. Title strategy has shifted to first-person and numerical framing. Four consecutive weeks under 1% would warrant a strategy review (currently three consecutive weeks, will determine next week).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The next week:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Reaction rate has not yet reached four consecutive weeks under 1%, but weekly trend data is insufficient. Observe next week.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the entire failure mode in two sentences. The rule said "four consecutive weeks." The Strategist had three consecutive weeks of data under 1%. Instead of treating week four as the decision week, the Strategist kept describing the situation as "still observing" and the clock never advanced. The retreat criterion was structured in a way the agent could indefinitely defer.&lt;/p&gt;

&lt;p&gt;When I went and computed the actual numbers from &lt;code&gt;article-performance.jsonl&lt;/code&gt; myself, the picture was even uglier. Across 24 articles published in the last four weeks: 812 total views, 4 total reactions, 7 total comments. Reaction rate: 0.49%. Half the threshold. Engagement rate (reactions plus comments): 1.35%. The rule should have triggered weeks ago. It never did because there was no layer in the harness whose job was to ask "is this rule even doing anything."&lt;/p&gt;

&lt;h2&gt;
  
  
  The 4th layer: what an Evolver is
&lt;/h2&gt;

&lt;p&gt;So I added a 4th cron job. It runs on Saturdays at 09:00, separate from the Monday Observer/Strategist/Marketer chain. Unlike the other three, it has WebSearch enabled. Its job is not to write articles. Its job is to read the strategy file, read the last few weeks of decision logs, and propose diffs against &lt;code&gt;strategy.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each proposal is one file: &lt;code&gt;domains/&amp;lt;name&amp;gt;/data/evolution/EVO-NNNN.md&lt;/code&gt;. The Evolver fills in five sections.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Observation — what it saw in the data&lt;/li&gt;
&lt;li&gt;Proposal — the rule change in plain prose&lt;/li&gt;
&lt;li&gt;Rationale — internal data and external references that justify the change&lt;/li&gt;
&lt;li&gt;Expected impact — what should improve if applied&lt;/li&gt;
&lt;li&gt;Diff — a literal &lt;code&gt;diff&lt;/code&gt; block against &lt;code&gt;strategy.md&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The diff block is the load-bearing part. The Evolver does not just write English suggestions. It writes the exact patch that would land in the repo. A small CLI called &lt;code&gt;harness-evolve.sh&lt;/code&gt; knows how to extract the diff block, run &lt;code&gt;git apply --check&lt;/code&gt;, and commit it with the proposal as the body. No LLM is involved in the apply step. The LLM proposes, the shell applies.&lt;/p&gt;

&lt;p&gt;That separation is on purpose. The proposal is creative. The apply is mechanical. When the apply step is mechanical you can trust it to either succeed cleanly or fail loudly. There is no "the agent tried to apply the patch and something weird happened in the middle."&lt;/p&gt;

&lt;h2&gt;
  
  
  EVO-0003 caught my Strategist procrastinating
&lt;/h2&gt;

&lt;p&gt;The Evolver's third real proposal, &lt;code&gt;EVO-0003&lt;/code&gt;, was the one I described above. The proposal is on disk and I am reading it back as I write this.&lt;/p&gt;

&lt;p&gt;The observation section quoted both of my Strategist logs, the "three consecutive weeks, will determine next week" one and the "data insufficient, observe next week" one. Then it computed the engagement rate from &lt;code&gt;article-performance.jsonl&lt;/code&gt; and showed that the threshold had been breached for at least four weeks. Then it argued that the original rule was bad in three ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The formula was not specified. Was "Reaction rate" per-article or aggregate? My Strategist could plausibly compute either, which is why it had been deferring.&lt;/li&gt;
&lt;li&gt;The trigger condition "four consecutive weeks" was ambiguous when weekly data was thin.&lt;/li&gt;
&lt;li&gt;The action on trigger ("propose a title and angle revision") was abstract enough that the Strategist could fulfill it with a single sentence and move on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The proposal replaced the rule with this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Engagement rate = (sum of reactions + comments over the last 4 weeks of articles) / sum of views. The Strategist must compute this every week and log it. If under 1.5% for four consecutive weeks, next week's 5 articles must be at least 4 titles in the "number + first person + failure narrative" form. Abstract titles are forbidden.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is a 20-line patch. The diff is below the prose in the proposal file. I approved it via &lt;code&gt;/harness-evolve approve EVO-0003&lt;/code&gt; at 14:04 on a Tuesday afternoon. The shell ran &lt;code&gt;git apply --index&lt;/code&gt; against &lt;code&gt;strategy.md&lt;/code&gt;, made the commit, updated the proposal's frontmatter to &lt;code&gt;status: applied&lt;/code&gt;, and sent me a Telegram note. The next Monday's Strategist ran with the new rule and computed an engagement rate of 1.35% in the log without prompting. The "data insufficient" sentence stopped appearing.&lt;/p&gt;

&lt;p&gt;The thing I want to be honest about is that the Strategist hadn't been malicious. It hadn't been broken either. It had been a perfectly competent agent following a rule that was structured to allow deferral. That is a failure of the rule. The Evolver's job is to detect rule failures, because nothing else in the harness was structured to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Safety boundary, because Self-Evolving Agents are not toys
&lt;/h2&gt;

&lt;p&gt;The minute you say "an agent that rewrites the harness," somebody in your head should be raising their hand and asking what stops it from rewriting itself into a paperclip optimizer. Several things, on purpose.&lt;/p&gt;

&lt;p&gt;The Evolver cannot touch the kinds of decisions that have to remain mine. Adding or removing a domain. Switching languages. Changing the quality bar for writing. Anything involving licensing, author identity, or security. The &lt;code&gt;.env&lt;/code&gt; file, the credentials directory, the publish triggers. If any of these were on the table I would not let the Evolver run unattended at all.&lt;/p&gt;

&lt;p&gt;Inside the territory it can touch, three numeric limits keep it from running away.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Diff size cap: 20 lines per proposal. A proposal larger than that has to be split or escalated.&lt;/li&gt;
&lt;li&gt;Two proposals per week per domain. If the Evolver wants to propose more, the third is held until next Saturday.&lt;/li&gt;
&lt;li&gt;Three consecutive rejects on the same theme triggers an automatic mute. The Evolver stops re-pitching the same idea after I have said no three times.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last one is the part I think is undersold in the broader "self-improving agent" literature. The interesting signal in a &lt;code&gt;reject&lt;/code&gt; log is not the proposal, it is the reason. "MCP is still the main revenue genre, we cannot drop it" is the kind of business context that has never been written into &lt;code&gt;strategy.md&lt;/code&gt;. After three weeks of rejecting MCP-cut proposals with that reason, the Evolver stops proposing them. Implicit founder context becomes explicit harness behavior, just by accumulating reasons-for-reject.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need before adding a 4th layer
&lt;/h2&gt;

&lt;p&gt;I think there are three real prerequisites before adding an Evolver-style layer to your own setup. Without them, the 4th layer is just noise.&lt;/p&gt;

&lt;p&gt;First, the three existing layers have to produce decision logs that another agent can read. If your Strategist's output is "ran successfully, picked themes," there is nothing for the Evolver to find. The procrastination only showed up because my Strategist had been writing structured logs with phrases like "currently three consecutive weeks, will determine next week." Logs that include the agent's reasoning in prose are what make audit possible.&lt;/p&gt;

&lt;p&gt;Second, the rules themselves have to be in version control as text. &lt;code&gt;strategy.md&lt;/code&gt; is a checked-in markdown file because the Evolver needs to produce a diff block that &lt;code&gt;git apply&lt;/code&gt; can land. If your rules live in a database, a SaaS dashboard, or a thousand-line JSON config, the patch model breaks down. Plain markdown in git is the cheap path.&lt;/p&gt;

&lt;p&gt;Third, you need a human approval channel that does not require the human to read the whole proposal every time. My Telegram notification has the EVO-ID, the title, and a one-line link to the file. I open the file only when the title makes me curious. Most of the time I either approve fast or reject with a short reason. If approval costs me ten minutes per proposal, I will stop running the Evolver. If it costs me thirty seconds, I will run it indefinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about not adding a 4th layer
&lt;/h2&gt;

&lt;p&gt;If you do not want a 4th layer, you can absolutely get most of the benefit by running a weekly human review with a specific question. Not "how are the agents doing." That is what I had been doing, and it did not catch the procrastination. The specific question is: "did any retreat criterion in &lt;code&gt;strategy.md&lt;/code&gt; actually fire this week, and if not, why not."&lt;/p&gt;

&lt;p&gt;Sit with that question for ten minutes per Friday. You will catch what I was missing for three weeks. The Evolver is, more than anything else, a forcing function for that question. It does not have to be an agent. It can be a calendar reminder.&lt;/p&gt;

&lt;p&gt;I happen to like running it as an agent because the proposal artifacts pile up in version control and become a record of how my rules have evolved. &lt;code&gt;EVO-0001&lt;/code&gt; through &lt;code&gt;EVO-0004&lt;/code&gt; form a small history of "things I thought were good ideas, things I thought were bad ideas, and why." That history is useful when I am writing next year's &lt;code&gt;strategy.md&lt;/code&gt; from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I have not built yet
&lt;/h2&gt;

&lt;p&gt;The current Evolver only audits one domain at a time. Across my four domains (devto, qiita, zenn, kenimoto-dev) I have written different versions of &lt;code&gt;strategy.md&lt;/code&gt; for each, and most of them have similarly structured retreat criteria. A cross-domain Evolver could notice that the same rule structure has been failing in two domains and propose a unified fix. I have not built it. It is on the list.&lt;/p&gt;

&lt;p&gt;The other thing on the list is the obvious recursion question. Who audits the Evolver. The current answer is "I do, every approve/reject is a human signal." The longer answer is "I do not know yet." If the Evolver's proposals start looking systematically biased — say, always proposing tighter thresholds, or always proposing to drop the same genre — that bias is real and I should add a 5th layer that watches the 4th. I have not seen it yet. I might not until EVO-0050 or so. I want the bias to be obvious before I add another layer just to feel safer.&lt;/p&gt;

&lt;p&gt;For now: three agents that follow rules, one agent that audits the rules, and one human who approves the audit. That is the smallest harness I have found that catches its own procrastination.&lt;/p&gt;




&lt;p&gt;If you want the full Harness Engineering picture — the 6 building blocks, the AGENTS.md/CLAUDE.md/hooks patterns, and the Self-Evolving Agent chapter that grounds this article — that is in the book.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://kenimoto.dev/books/harness-engineering-guide?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=evolver-caught-strategist" rel="noopener noreferrer"&gt;Harness Engineering: From Using AI to Controlling AI&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>claudecode</category>
      <category>llm</category>
    </item>
    <item>
      <title>I Told Claude Code to Do TDD. It Wrote the Test AFTER the Code 6 Out of 10 Times.</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Mon, 25 May 2026 11:00:00 +0000</pubDate>
      <link>https://dev.to/kenimo49/i-told-claude-code-to-do-tdd-it-wrote-the-test-after-the-code-6-out-of-10-times-31kd</link>
      <guid>https://dev.to/kenimo49/i-told-claude-code-to-do-tdd-it-wrote-the-test-after-the-code-6-out-of-10-times-31kd</guid>
      <description>&lt;p&gt;My CLAUDE.md had a section called &lt;code&gt;## TDD First&lt;/code&gt;. Six lines. Very clear. I had spent twenty minutes drafting it. Then I ran a 30-day audit of my own commits and discovered that across the features I had asked Claude Code to TDD, the test file was committed &lt;em&gt;after&lt;/em&gt; the source file 6 out of 10 times.&lt;/p&gt;

&lt;p&gt;Not "the test failed first, then I fixed it." The test file did not exist at the moment the source file got committed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fms1yprjyrsiij2nkhfze.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fms1yprjyrsiij2nkhfze.png" alt="A side-by-side panel: before the PreToolUse hook, 6 of 10 features had the test written after the source. After the hook, 10 of 10 had the test first." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the story of how I caught it, why it kept happening, and the two-part fix (prompt plus a PreToolUse hook) that finally pushed Claude into a real red-green-refactor cycle. It is the third installment in what is becoming an accidental series on Claude doing things confidently and wrong. The first was Claude &lt;a href="https://dev.to/kenimo49/i-caught-claude-hiding-my-bug-three-times-here-are-the-10-debugging-prompts-that-stopped-it-3l1h"&gt;hiding bugs three times in a row&lt;/a&gt;. The second was refusing to write specs until the code went sideways three times. This one is about TDD, and the pattern is identical: the model agrees, the model proceeds, the model skips the part of the prompt that would cost it tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 30-day audit
&lt;/h2&gt;

&lt;p&gt;The audit was accidental. I had been writing about debugging habits and wanted to see whether my own commit history was consistent with what I was preaching. So I pulled &lt;code&gt;git log --name-only --pretty=format:'%h %ai %s'&lt;/code&gt; for the last 30 days on a project I had been driving with Claude Code, and grouped the commits by feature. Ten features. For each one, I noted the timestamp of the first commit that touched the source file, and the timestamp of the first commit that touched its test file.&lt;/p&gt;

&lt;p&gt;Six features out of ten had the source file committed first. The gap ranged from 90 seconds to 23 minutes. In two cases the test file was committed in the same commit as a later round of fixes, after the source had already been shipped to a feature branch. In one case there was no test file at all, only a &lt;code&gt;# TODO: add tests&lt;/code&gt; next to the function.&lt;/p&gt;

&lt;p&gt;I had been telling Claude "TDD this" every single time. I had a &lt;code&gt;## TDD First&lt;/code&gt; section in CLAUDE.md. I had even pasted the red-green-refactor sequence at the top of the prompt for the more complex features. And six times out of ten, it had cheerfully written the implementation, then either written the test afterward or skipped it entirely.&lt;/p&gt;

&lt;p&gt;I am not blaming the model for being lazy. The model was doing exactly what it was trained to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why next-token prediction defaults to implementation-first
&lt;/h2&gt;

&lt;p&gt;This is the part that took me a while to actually understand. The model is not deciding "I will do TDD" or "I will not do TDD" the way a human engineer might decide. It is predicting the next most plausible token given the context. And in its training data, the overwhelming majority of "user asks for feature X" responses look like &lt;em&gt;here is the function that does X&lt;/em&gt;, optionally followed by &lt;em&gt;and here is a test&lt;/em&gt;. The "test first, then implementation, with the test failing in between" sequence is rare in public repositories because humans rarely commit the red phase as its own commit. We commit the green phase. So the model never built a strong prior for the red-first ordering.&lt;/p&gt;

&lt;p&gt;Several people in the Claude Code community have pointed at the same thing. The &lt;a href="https://www.aihero.dev/skill-test-driven-development-claude-code" rel="noopener noreferrer"&gt;aihero.dev TDD skill writeup&lt;/a&gt; puts it as: when the test writer and the implementer share the same context window, the implementer's thinking leaks into the test writer's, and you get tests that conveniently pass on the first run. That is not TDD. That is "tests retrofitted to pass." The &lt;a href="https://alexop.dev/posts/custom-tdd-workflow-claude-code-vue/" rel="noopener noreferrer"&gt;alexop.dev red-green-refactor loop post&lt;/a&gt; argues that the only reliable fix is to force the cycle from outside the model, with hooks or skills that the agent cannot override mid-stride.&lt;/p&gt;

&lt;p&gt;The other thing I keep seeing in community writeups, including the &lt;a href="https://docs.bswen.com/blog/2026-03-25-tdd-skill-claude-code/" rel="noopener noreferrer"&gt;BSWEN Claude Code TDD skill walkthrough&lt;/a&gt;, is the same Anthropic guidance I had been ignoring: Claude will sometimes alter the test to make it pass rather than fix the implementation. Committing the test before the implementation gives you a diff to look at if that happens. I was not doing that either.&lt;/p&gt;

&lt;p&gt;So the model had a weak prior for test-first, and I had a weak workflow that did nothing to compensate. Six out of ten makes a lot of sense in retrospect. The surprising thing is that it was as low as six.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tried first that did not work
&lt;/h2&gt;

&lt;p&gt;Before the hook, I tried prompt engineering harder. This is what most people try, and it gets you most of the way without getting you there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 1 — &lt;code&gt;## TDD First&lt;/code&gt; in CLAUDE.md.&lt;/strong&gt; Already had this. Six out of ten ignored it. The header was too generic; the model saw it as a vibe, not a constraint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 2 — explicit red-phase instruction in the prompt.&lt;/strong&gt; I started pasting "Write a failing test for [feature] in &lt;code&gt;tests/X_test.py&lt;/code&gt;. Do not write the implementation yet. Run the test and confirm it fails before proceeding." This got me to maybe 8 out of 10. Better, but 2 out of 10 I would catch it cheating, usually by writing the test in a way that mocked out the part that would actually have failed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 3 — separate prompts for red and green.&lt;/strong&gt; Two messages. First message: write the failing test, stop, run it, show me the failure. Second message, only after I had eyeballed the failure: now write the implementation. This was the first time I got something that smelled like real TDD. The problem was that it required me to physically be at the keyboard for two turns, and if I context-switched away mid-feature, the next Claude session would happily merge the two steps back into one.&lt;/p&gt;

&lt;p&gt;The lesson from Attempt 3 is that prompts are advice. The model can ignore advice. To get TDD enforced, I needed something the model could not ignore. That something is a hook.&lt;/p&gt;

&lt;h2&gt;
  
  
  The PreToolUse hook that broke the loop
&lt;/h2&gt;

&lt;p&gt;Claude Code's hook system lets you intercept tool calls before they execute. A PreToolUse hook on Write or Edit gets the file path the model is about to touch. If the model is trying to write to &lt;code&gt;src/foo.py&lt;/code&gt; and there is no &lt;code&gt;tests/foo_test.py&lt;/code&gt; that currently fails, the hook can exit 2, which Claude Code treats as "this tool call is denied, here is the reason, try again."&lt;/p&gt;

&lt;p&gt;This is the smallest version that worked for me, on a Python project with pytest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Write|Edit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"python3 .claude/hooks/require-failing-test.py"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script reads the file path from the tool call payload, maps &lt;code&gt;src/X.py&lt;/code&gt; to &lt;code&gt;tests/X_test.py&lt;/code&gt;, checks the test file exists, runs &lt;code&gt;pytest tests/X_test.py --no-header -q&lt;/code&gt;, and exits 2 if pytest exits 0. If the test does not yet exist or the test currently fails, the hook lets the edit through. If the test exists and is already passing, the hook blocks the edit with a message like &lt;em&gt;"a failing test must exist in tests/X_test.py before src/X.py can be modified. Write the failing test first."&lt;/em&gt; That message lands in the model's next-turn context. It does not have a choice.&lt;/p&gt;

&lt;p&gt;There are edge cases. The test file might pass for the wrong reason; the hook does not catch that. The mapping from source to test path is project-specific; mine is hardcoded. And I have an escape hatch, a magic comment &lt;code&gt;# tdd-bypass: refactor&lt;/code&gt; on the first line, for refactor commits where you genuinely want to edit without a new failing test, because refactor is supposed to preserve behavior, not add it. The hook respects the escape hatch, but it logs every use of it to a file I review at the end of the week. The first week, my escape-hatch log had 22 entries. The second week it had 4. That number going down is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the 30-day rerun looked like
&lt;/h2&gt;

&lt;p&gt;I ran the same audit 30 days after the hook went in. Same project, same kind of features, same prompt style. The numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test file committed first: &lt;strong&gt;9 of 10&lt;/strong&gt; (up from 4 of 10)&lt;/li&gt;
&lt;li&gt;Test file committed in same commit as source, but written first per the file-modification timestamps: 1 of 10&lt;/li&gt;
&lt;li&gt;Test file committed after source: 0 of 10&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The single feature where the test went in the same commit as the source was a 12-line config helper that I had legitimately bypassed with the magic comment. So in terms of TDD being followed when the rule applied, the number is 10 of 10.&lt;/p&gt;

&lt;p&gt;I do not want to claim that the hook turned Claude into a disciplined TDD practitioner. It did not. The model still writes implementations that look suspicious from a "test was designed around the implementation" perspective some of the time. What the hook gives me is &lt;em&gt;ordering&lt;/em&gt;: a failing test must exist before the source can be touched. That alone closes the loop where Claude was retrofitting tests around code that was already shaping the test's assertions. The Anthropic guidance on this, captured by several community writeups including the &lt;a href="https://www.datacamp.com/tutorial/claude-code-best-practices" rel="noopener noreferrer"&gt;DataCamp best practices roundup&lt;/a&gt;, is that ordering is the load-bearing constraint and everything else is bonus.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to skip TDD entirely
&lt;/h2&gt;

&lt;p&gt;This is the part I should have figured out before instrumenting any of this. There are tasks where TDD is the wrong tool. Refactors that should be a no-op behaviorally. One-off scripts I am going to throw away in 20 minutes. Pure data migrations. UI tweaks where the test would just be a snapshot of itself. Forcing TDD on these tasks does not make the code better; it makes the workflow heavier with no payoff.&lt;/p&gt;

&lt;p&gt;The escape hatch exists for these. The week-end review of the escape-hatch log is where I notice if I am abusing it. "I bypassed TDD because the test was hard to write" is a smell. "I bypassed TDD because the code was a snapshot test of CSS class names" is fine. The audit, not the rule, is what keeps the workflow honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;My CLAUDE.md still says &lt;code&gt;## TDD First&lt;/code&gt;. I left it there for vibes. It was never going to be the part that did the work. The hook is the part that does the work, and the audit is the part that decides whether the hook is still tuned right.&lt;/p&gt;

&lt;p&gt;If you want the full picture of how to layer prompts vs hooks vs MCP servers (when to use which layer for which kind of rule), I wrote it down in &lt;a href="https://kenimoto.dev/books/claude-code-mastery?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=claude-tdd-6-of-10" rel="noopener noreferrer"&gt;Practical Claude Code&lt;/a&gt;. The hooks chapter is the one I keep coming back to.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/FlorianBruniaux/claude-code-ultimate-guide/blob/main/guide/workflows/tdd-with-claude.md" rel="noopener noreferrer"&gt;TDD with Claude Code (FlorianBruniaux/claude-code-ultimate-guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bswen.com/blog/2026-03-25-tdd-skill-claude-code/" rel="noopener noreferrer"&gt;How to Implement TDD with Claude Code TDD Skill (BSWEN, Mar 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.aihero.dev/skill-test-driven-development-claude-code" rel="noopener noreferrer"&gt;My Skill Makes Claude Code GREAT At TDD (aihero.dev)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://alexop.dev/posts/custom-tdd-workflow-claude-code-vue/" rel="noopener noreferrer"&gt;Forcing Claude Code to TDD: an agentic red-green-refactor loop (alexop.dev)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.datacamp.com/tutorial/claude-code-best-practices" rel="noopener noreferrer"&gt;Claude Code Best Practices: Planning, Context Transfer, TDD (DataCamp)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>tdd</category>
      <category>hooks</category>
    </item>
    <item>
      <title>Your New Domain's First Week of GA4 Is a Lie: 4 Days of Raw Data from a Launch</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Sat, 23 May 2026 13:00:01 +0000</pubDate>
      <link>https://dev.to/kenimo49/your-new-domains-first-week-of-ga4-is-a-lie-4-days-of-raw-data-from-a-launch-47pi</link>
      <guid>https://dev.to/kenimo49/your-new-domains-first-week-of-ga4-is-a-lie-4-days-of-raw-data-from-a-launch-47pi</guid>
      <description>&lt;p&gt;Four days after registering a new domain, I opened GA4 and saw &lt;strong&gt;65 page views / 34 users / 9 countries&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For a brief, build-in-public moment, I almost cheered. Then I looked at the breakdown. The US had 17 sessions averaging &lt;strong&gt;4.9 seconds&lt;/strong&gt; of session duration. France, Poland, South Korea, India, Singapore: each between &lt;strong&gt;0 and 1.4 seconds&lt;/strong&gt;. Japan alone sat at &lt;strong&gt;751 seconds (over 12 minutes)&lt;/strong&gt;: an outlier so loud it should be illegal.&lt;/p&gt;

&lt;p&gt;The domain is &lt;a href="https://kaoriq.com" rel="noopener noreferrer"&gt;kaoriq.com&lt;/a&gt;, registered on 2026-05-02, a personality-quiz × fragrance e-commerce site I'm building. As of today (May 5), it has fewer than 20 articles. Doing the back-of-the-envelope math, that page-view distribution is physically impossible to come from real humans.&lt;/p&gt;

&lt;p&gt;This post walks through how I read the first week of GA4 data on a new domain as &lt;strong&gt;"me + a crawler army"&lt;/strong&gt;, with the actual numbers exposed. For anyone running GA4 on a new project, or anyone who registered a domain this weekend.&lt;/p&gt;

&lt;h2&gt;
  
  
  The raw data: past 14 days (4 days of real activity)
&lt;/h2&gt;

&lt;p&gt;Numbers first, no spin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overall&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sessions&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Page Views&lt;/td&gt;
&lt;td&gt;65&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total Users&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New Users&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg Session Duration&lt;/td&gt;
&lt;td&gt;104.1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bounce Rate&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;80%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;By Country&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Country&lt;/th&gt;
&lt;th&gt;Sessions&lt;/th&gt;
&lt;th&gt;PV&lt;/th&gt;
&lt;th&gt;Avg Duration (s)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Japan&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;751.0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;United States&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;4.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canada&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;France&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poland&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;South Korea&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(not set)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;India&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Singapore&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Daily&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Sessions&lt;/th&gt;
&lt;th&gt;PV&lt;/th&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2026-05-02 (registration day)&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-05-03&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-05-04&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-05-05&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At a glance, "not bad for week one" is a tempting read. But this dataset contains a &lt;strong&gt;751-second Japanese reader&lt;/strong&gt; living next door to &lt;strong&gt;9 countries averaging zero seconds&lt;/strong&gt;. The middle is missing. That gap is the whole tell.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five signals, beaten in parallel
&lt;/h2&gt;

&lt;p&gt;I never call bot traffic on a single signal. To avoid false positives, I always cross-check five axes at once.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Bot pattern&lt;/th&gt;
&lt;th&gt;Human pattern&lt;/th&gt;
&lt;th&gt;kaoriq actual&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Session duration&lt;/td&gt;
&lt;td&gt;0–5 s&lt;/td&gt;
&lt;td&gt;30 s – several min&lt;/td&gt;
&lt;td&gt;US 4.9s, FR 1.4s, KR 0s&lt;/td&gt;
&lt;td&gt;Bot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bounce rate&lt;/td&gt;
&lt;td&gt;90–100%&lt;/td&gt;
&lt;td&gt;40–70%&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;td&gt;Bot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PV / Session&lt;/td&gt;
&lt;td&gt;1.0 (one page, gone)&lt;/td&gt;
&lt;td&gt;1.5–3.0&lt;/td&gt;
&lt;td&gt;US: 17/17 = 1.0&lt;/td&gt;
&lt;td&gt;Bot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Geographic anomaly&lt;/td&gt;
&lt;td&gt;Random countries unrelated to content&lt;/td&gt;
&lt;td&gt;Concentrated in target geo&lt;/td&gt;
&lt;td&gt;EN/JA only, yet PL/IN/SG&lt;/td&gt;
&lt;td&gt;Bot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time-series spike&lt;/td&gt;
&lt;td&gt;Massive day-one for new domains&lt;/td&gt;
&lt;td&gt;Gradual ramp&lt;/td&gt;
&lt;td&gt;40 PV on day of registration&lt;/td&gt;
&lt;td&gt;Bot&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why a single signal lies
&lt;/h3&gt;

&lt;p&gt;"80% bounce, must all be bots, right?" Not so fast.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Duration alone&lt;/strong&gt;: A reader who tabs your post and walks away for lunch racks up 30+ minutes. Indistinguishable from "deeply engaged" or "abandoned tab."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounce rate alone&lt;/strong&gt;: A landing page that perfectly answers the question gets a 100% bounce from satisfied humans. Excellence and bots both score the same.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geography alone&lt;/strong&gt;: A viral overseas tweet legitimately produces multi-country traffic. Weak on its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You only get to call "bot" with confidence when &lt;strong&gt;all five signals lean the same direction simultaneously&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bimodal distribution was the smoking gun
&lt;/h2&gt;

&lt;p&gt;The real reason this verdict held in kaoriq's case is the &lt;strong&gt;shape of the duration distribution&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Japan: 5 sessions / &lt;strong&gt;751 s&lt;/strong&gt; average&lt;/li&gt;
&lt;li&gt;Everywhere else: &lt;strong&gt;0–5 s&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the traffic were genuinely human, session duration should spread &lt;strong&gt;more evenly across the 20–120 second band&lt;/strong&gt;: "bounced after the title (10s)," "read the lede (40s)," "made it to the end (180s)" forming a gradient.&lt;/p&gt;

&lt;p&gt;But kaoriq's distribution is &lt;strong&gt;bimodal&lt;/strong&gt; with the middle scooped out. The honest reading: only "me (long sessions, testing the site)" and "crawlers (instant exits)" exist. Nothing in between.&lt;/p&gt;

&lt;p&gt;Conversely, a healthy distribution would look like "Japan 100 sessions / 60s, US 50 sessions / 45s, Canada 20 sessions / 30s": durations spread normally. That'd be a real human traffic signature.&lt;/p&gt;

&lt;h2&gt;
  
  
  So how many real humans were there?
&lt;/h2&gt;

&lt;p&gt;After all that beating, my estimate breaks down as:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Estimated sessions&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Me, testing the site&lt;/td&gt;
&lt;td&gt;4–5&lt;/td&gt;
&lt;td&gt;Most of Japan's 5 sessions, source of the 751s average&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crawlers (Googlebot / Bingbot / GPTBot / ClaudeBot / AhrefsBot, etc.)&lt;/td&gt;
&lt;td&gt;27–30&lt;/td&gt;
&lt;td&gt;US 17, plus the zero-second Europe &amp;amp; Asia rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Actual organic human traffic&lt;/td&gt;
&lt;td&gt;2–5&lt;/td&gt;
&lt;td&gt;The remainder of Japan + a couple of US sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Of 37 sessions, &lt;strong&gt;at most 5 were real humans&lt;/strong&gt;. That's the reality of week one for a new domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why GA4 doesn't filter this for you
&lt;/h2&gt;

&lt;p&gt;GA4 has a &lt;strong&gt;"known bots and spiders" auto-exclusion&lt;/strong&gt; based on the &lt;a href="https://www.iab.com/guidelines/iab-abc-international-spiders-bots-list/" rel="noopener noreferrer"&gt;IAB/ABC Spiders &amp;amp; Bots list&lt;/a&gt;. It catches classical crawlers but misses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript-executing crawlers&lt;/strong&gt;: GPTBot, ClaudeBot, PerplexityBot. These new generative-AI crawlers run JS, so the GA4 tag fires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO-tool crawlers&lt;/strong&gt;: AhrefsBot, SemrushBot, MozBot. High frequency, and they swarm new domains the moment they're discovered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headless-browser scrapers&lt;/strong&gt;: Custom Puppeteer or Playwright bots are indistinguishable from a real Chrome session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The week after a new domain registration is when this crawler army discovers the new IP.&lt;/strong&gt; It calms down within 7–10 days as DNS propagates. But if you take week-one GA4 at face value, you'll make bad decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three annotations every new-project dashboard needs
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use "Engaged Sessions" as your primary metric.&lt;/strong&gt; GA4 defines an engaged session as: ≥10s duration OR ≥2 PV OR a conversion event. Most of the bot army gets filtered here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always view session duration &lt;em&gt;split by country&lt;/em&gt;.&lt;/strong&gt; Looking at any single metric (sessions, PV) without the geo filter lets the crawler army masquerade as success.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat the first 30 days as a "noise phase."&lt;/strong&gt; Real numbers only appear after social funnels, SEO, and content depth all line up.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Closing: look at your own GA4 with this lens
&lt;/h2&gt;

&lt;p&gt;A new domain's GA4 lies for the first 1–2 weeks. If your country breakdown is full of zero-second sessions from the US, Eastern Europe, and Southeast Asia: that's the crawler parade, not humans falling in love with your content.&lt;/p&gt;

&lt;p&gt;The procedure is simple: &lt;strong&gt;beat with five signals → suspect bimodal distributions → swap the primary metric to Engaged Sessions&lt;/strong&gt;. Doing this saves you from being whipsawed by early data.&lt;/p&gt;

&lt;p&gt;Doubting GA4 is, in the end, a discipline for not making expensive mistakes. Beat the data before the data beats you.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;This post is based on real data&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Site: &lt;a href="https://kaoriq.com" rel="noopener noreferrer"&gt;kaoriq.com&lt;/a&gt; (domain registered 2026-05-02, built with Astro v6 + Tailwind v4)&lt;/li&gt;
&lt;li&gt;Period analyzed: 2026-04-22 → 2026-05-05 (4 days of actual activity)&lt;/li&gt;
&lt;li&gt;Data source: GA4 Data API v1beta via Service Account&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you want the full LLMO playbook (how to think about AI crawlers, citations, and the measurement layer underneath the GA4 narrative):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://kenimoto.dev/books/llmo-ai-search-optimization?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=ga4-new-domain-lie" rel="noopener noreferrer"&gt;LLMO: AI Search Optimization for Engineers&lt;/a&gt;&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>webdev</category>
      <category>llmo</category>
      <category>seo</category>
    </item>
    <item>
      <title>I Stacked 4 More Context Layers on Top of RAG. Sonnet Got 12% Better. Haiku Got 14% Worse.</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Fri, 22 May 2026 13:00:01 +0000</pubDate>
      <link>https://dev.to/kenimo49/i-stacked-4-more-context-layers-on-top-of-rag-sonnet-got-12-better-haiku-got-14-worse-3h4d</link>
      <guid>https://dev.to/kenimo49/i-stacked-4-more-context-layers-on-top-of-rag-sonnet-got-12-better-haiku-got-14-worse-3h4d</guid>
      <description>&lt;p&gt;I read a post about "Full Context Engineering" and immediately added four more layers to my RAG pipeline. Structured output instructions. Hierarchical document layout. Role definition. Few-shot examples. The whole buffet.&lt;/p&gt;

&lt;p&gt;The improvement on Claude Sonnet was 12%.&lt;/p&gt;

&lt;p&gt;The improvement on Claude Haiku was minus 14%.&lt;/p&gt;

&lt;p&gt;I had just spent two weeks building scaffolding to make my smaller model worse at its job. If you have ever wallpapered a room and stepped back to discover you covered up the light switch, you know the feeling.&lt;/p&gt;

&lt;p&gt;This post is about what those numbers actually mean for the way you spend your context engineering effort in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I was measuring
&lt;/h2&gt;

&lt;p&gt;I was running a benchmark against my own book corpus for a previous experiment (the cheap-model post). The same scoring rubric: factual accuracy, hallucination rate, specificity, and honesty on a 0 to 15 scale.&lt;/p&gt;

&lt;p&gt;The configurations were a ladder. Each rung adds one more thing on top of the previous one.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;System prompt only&lt;/strong&gt;: the bare baseline. No retrieval, nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System + RAG&lt;/strong&gt;: vector search over a curated corpus, top documents injected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full Context Engineering&lt;/strong&gt;: RAG + structured output instructions + hierarchical layout + role definition + few-shot examples.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What I expected: a smooth upward curve. What I got was a curve that leaned forward and then fell over.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb6xjktcrby9e3jwkz8tw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb6xjktcrby9e3jwkz8tw.png" alt="Full CE vs RAG: Score deltas across Sonnet and Haiku" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Sonnet, total score (out of 15):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;th&gt;Delta from previous&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;System only&lt;/td&gt;
&lt;td&gt;8.8&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System + RAG&lt;/td&gt;
&lt;td&gt;10.2&lt;/td&gt;
&lt;td&gt;+1.4 (+16%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full Context Engineering&lt;/td&gt;
&lt;td&gt;11.4&lt;/td&gt;
&lt;td&gt;+1.2 (+12%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Claude Haiku, total score (out of 15):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;th&gt;Delta from previous&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;System only&lt;/td&gt;
&lt;td&gt;3.7&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System + RAG&lt;/td&gt;
&lt;td&gt;11.8&lt;/td&gt;
&lt;td&gt;+8.1 (+219%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full Context Engineering&lt;/td&gt;
&lt;td&gt;10.1&lt;/td&gt;
&lt;td&gt;-1.7 (-14%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two findings I did not expect.&lt;/p&gt;

&lt;p&gt;First: RAG is doing almost all of the work. On Sonnet, RAG closed 88% of the gap between baseline and the fully tricked-out pipeline (1.4 of the total 2.6 point improvement). On Haiku, RAG over-shot the final number entirely.&lt;/p&gt;

&lt;p&gt;Second: stacking more on top of RAG is not free. On Haiku, it actively made things worse. The hallucination score went from 1.7 to 0.5. The honesty score went from 1.3 to 0.5. The model started confidently making things up that it had previously hedged on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this happens
&lt;/h2&gt;

&lt;p&gt;I have a hypothesis that I think survives contact with reality.&lt;/p&gt;

&lt;p&gt;A small model has limited working memory. RAG hands it the right facts. Once those facts are in front of it, the marginal returns from extra structure are small. But the marginal cost of extra context is not small. Every paragraph of role definition, every few-shot example, every "here is how to format the output" block competes with the retrieved documents for the model's attention.&lt;/p&gt;

&lt;p&gt;For Sonnet, the working memory is wide enough that the extras land in unused space. For Haiku, the extras shove the actually-useful retrieved context off to the edge of the window. The model still sees it. It just stops trusting it.&lt;/p&gt;

&lt;p&gt;This is the same finding that recent research on long-context behavior keeps surfacing. Studies on instruction-following at high context fill report that for most frontier models in 2026, quality starts to degrade measurably at 60 to 70 percent context fill, and falls off a cliff around 90 percent. The cliff is steeper for smaller models.&lt;/p&gt;

&lt;p&gt;The Pareto principle applies to context engineering with embarrassing accuracy. RAG is the 20 percent of effort that produces 80 percent of the result. Everything you stack on top of it is the long tail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 2026 reality I almost forgot to mention
&lt;/h2&gt;

&lt;p&gt;When I ran the original experiment, I was on Sonnet 4 and Haiku 3 with a 200K context window. As of this writing, Sonnet 4.6 has &lt;a href="https://www.anthropic.com/news/claude-sonnet-4-6" rel="noopener noreferrer"&gt;a 1M token context window at standard pricing&lt;/a&gt; and prompt caching cuts the cost of repeated context by 90 percent.&lt;/p&gt;

&lt;p&gt;This changes the math, but not in the direction you might think.&lt;/p&gt;

&lt;p&gt;A 1M context window does not magically make stacked context cheaper to design. The model still has to pay attention to the right thing. The cliff at 60 to 70 percent fill is a percentage, not an absolute. A bigger window just means you can write more bad context before you fall off it.&lt;/p&gt;

&lt;p&gt;Prompt caching helps if your stacked layers are static. The role definition, the few-shot examples, the structured output instructions: those parts cache cleanly. But that only saves money. It does not save quality. If your Haiku result was minus 14%, prompt caching makes minus 14% cheaper. That is not the win you wanted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing nobody told me about Skills
&lt;/h2&gt;

&lt;p&gt;Anthropic's Skills feature is interesting in this light. Skills are reusable context bundles that load on demand. The right way to think about them is not "more context, all the time" but "the right context, just in time."&lt;/p&gt;

&lt;p&gt;That is the failure mode my Full CE experiment ran into. I was packing every layer into every request. Skills point at the alternative: keep the system prompt small, retrieve the relevant skill, and let the rest stay out of the window. It is the same lesson as RAG, applied one level up. Selective beats throwing everything in.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do now
&lt;/h2&gt;

&lt;p&gt;If you take only one thing from this post, take this: the order of operations matters more than the number of operations.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build the retrieval first. Get RAG working with a clean corpus, decent embeddings, and a relevance threshold. This is your 80%.&lt;/li&gt;
&lt;li&gt;Run a benchmark. Real benchmark, on real questions, scored by a real rubric. Not vibes.&lt;/li&gt;
&lt;li&gt;Add one layer at a time. Structured output, then hierarchical layout, then role definition. Re-benchmark after each.&lt;/li&gt;
&lt;li&gt;If the score goes down, take that layer out. Do not assume the layer is good and your benchmark is bad. The benchmark is right more often than you think.&lt;/li&gt;
&lt;li&gt;Try the same ladder on a smaller model. The thing that helps Sonnet may hurt Haiku. Knowing which side of the line you are on saves you money.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This sounds obvious. It is not what most teams do. Most teams read a blog post about Context Engineering, add four layers in one weekend, and never measure whether the layers actually helped.&lt;/p&gt;

&lt;h2&gt;
  
  
  The chef and the kitchen, revisited
&lt;/h2&gt;

&lt;p&gt;In an earlier post I wrote that the model is the chef and the context is the kitchen. I want to extend that.&lt;/p&gt;

&lt;p&gt;Adding more context layers is like installing more kitchen equipment. A second oven. A pasta machine. A sous-vide. None of them make the chef worse at cooking pasta. But if the pasta machine takes up the counter space where the chef was chopping vegetables, the dinner gets worse anyway.&lt;/p&gt;

&lt;p&gt;The chef does not need every appliance. The chef needs the right ingredients within reach.&lt;/p&gt;

&lt;p&gt;Before you read the next breathless post about Full Context Engineering and start adding layers, run the experiment. Measure RAG alone. Measure RAG plus one thing. Find the layer that earns its keep, and leave the rest in the catalog.&lt;/p&gt;

&lt;p&gt;The answer is almost always: do RAG well first. Everything else is decoration. Decoration that, on a small model, can flip the sign on your accuracy score and leave you wondering why.&lt;/p&gt;

&lt;p&gt;The next time someone says "Context Engineering," what I want to say back is: please define which 20 percent of context you mean. The other 80 has a good chance of making things worse.&lt;/p&gt;




&lt;p&gt;The full Context Engineering system (five strategies, the RAG benchmarks behind these numbers, MCP server design, and the Agentic RAG implementation) is in &lt;a href="https://kenimoto.dev/books/context-engineering?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=full-ce-rag-12-haiku-14" rel="noopener noreferrer"&gt;Turning LLMs from Liars into Experts: Context Engineering in Practice&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>llm</category>
      <category>contextengineering</category>
    </item>
    <item>
      <title>OpenClaw Hit 250K Stars Faster Than React. I Spent 24 Hours Trying to Like It.</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Fri, 22 May 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/kenimo49/openclaw-hit-250k-stars-faster-than-react-i-spent-24-hours-trying-to-like-it-1pe2</link>
      <guid>https://dev.to/kenimo49/openclaw-hit-250k-stars-faster-than-react-i-spent-24-hours-trying-to-like-it-1pe2</guid>
      <description>&lt;p&gt;I switched my entire dev setup from Claude Code to OpenClaw on a Tuesday morning. By 11am I was googling "how to remove openclaw". By 6pm I had written a SOUL.md file longer than the actual feature I was shipping.&lt;/p&gt;

&lt;p&gt;This post is about that day. About what broke, what didn't, and what 24 hours of working in the terminal agent that is now technically the most-starred open-source project in GitHub history bought me.&lt;/p&gt;

&lt;p&gt;Yes, I am the engineer who wrote about Claude Code Skills three weeks ago and called the workflow pattern "settled for at least a year." Then OpenClaw passed React's all-time star count in 60 days, Peter Steinberger announced he was joining OpenAI to ship agents to everyone, and the launch tweet went past 4 million views. Settled, apparently, was a one-month forecast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers I had to verify before believing them
&lt;/h2&gt;

&lt;p&gt;Let me get the facts out of the way, because half of what people quote on Twitter about OpenClaw is wrong by a factor of two.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenClaw crossed 250,000 GitHub stars on March 3, 2026, surpassing React for the all-time most-starred software repository&lt;/li&gt;
&lt;li&gt;60 days from launch to 250K. React took roughly a decade&lt;/li&gt;
&lt;li&gt;60K stars in the first 72 hours. That part is the one nobody actually believes the first time&lt;/li&gt;
&lt;li&gt;Peter Steinberger announced on February 14, 2026 that he is joining OpenAI to work on agents, with OpenClaw moving to a foundation to stay open and independent&lt;/li&gt;
&lt;li&gt;One mid-sized refactor session in my test run consumed 920K tokens, which on Claude 4.5 Sonnet billing came out to about USD 8.30&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Hacker News thread when it crossed React was the most upvoted submission of the week. The top comment was "this is either the best thing that happened to dev tools in five years or the most expensive way to learn what &lt;code&gt;--yolo&lt;/code&gt; does."&lt;/p&gt;

&lt;p&gt;It is, somehow, both.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup, and the part I underestimated
&lt;/h2&gt;

&lt;p&gt;Installation took less than a minute.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.openclaw.dev | sh
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-ant-...
openclaw
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first surprise: OpenClaw asked me which model I wanted as default. I had four serious choices, plus Ollama for local models.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw &lt;span class="nt"&gt;--model&lt;/span&gt; claude-4.5-sonnet
openclaw &lt;span class="nt"&gt;--model&lt;/span&gt; gpt-4o
openclaw &lt;span class="nt"&gt;--model&lt;/span&gt; gemini-2.5-pro
openclaw &lt;span class="nt"&gt;--model&lt;/span&gt; ollama/devstral:24b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code has a backend model. OpenClaw has a backend model dropdown. That is not a small UX difference when you are trying to land a refactor for less than ten dollars.&lt;/p&gt;

&lt;p&gt;The second surprise: when I ran my first command, the agent asked me where the SOUL.md file was. I did not have one. It happily generated a default. The default was generic enough that I closed the session, opened my editor, and started writing my own. That is when the day quietly stopped being a benchmark and started being a personality test.&lt;/p&gt;

&lt;h2&gt;
  
  
  SOUL.md is the part nobody warned me about
&lt;/h2&gt;

&lt;p&gt;Here is the SOUL.md I ended the day with, after rewriting it three times.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# SOUL.md&lt;/span&gt;
You are a senior backend engineer with strong opinions and short patience for code
that talks more than it does.
&lt;span class="p"&gt;
-&lt;/span&gt; Prefer Python over TypeScript when both fit. We're not building a frontend here.
&lt;span class="p"&gt;-&lt;/span&gt; Never add a feature without a test. If the test would take more than 10 minutes
  to write, ask first instead of writing it.
&lt;span class="p"&gt;-&lt;/span&gt; Performance matters but readability matters more. We're a four-person team, not
  Google.
&lt;span class="p"&gt;-&lt;/span&gt; Do not write conversational filler. "Sure, I'll do that" is not output. Output
  is the diff.
&lt;span class="p"&gt;-&lt;/span&gt; When in doubt, ask. Don't guess. Guessing once cost us a weekend.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The thing the docs do not tell you: SOUL.md is not a config file. It is a contract. CLAUDE.md tells Claude Code what the project is. SOUL.md tells OpenClaw who the agent is. They are two different shapes of the same trust problem, and the day I figured that out was the day OpenClaw stopped feeling worse than Claude Code and started feeling different.&lt;/p&gt;

&lt;p&gt;I had a Claude Code session open in another window all day for a sanity check. By 4pm I noticed my CLAUDE.md was 312 lines and my SOUL.md was 14. The SOUL.md was doing more work per line.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Gateway, and why my LGPD-anxious teammate cared
&lt;/h2&gt;

&lt;p&gt;OpenClaw routes every LLM call through a local process called the Gateway.&lt;/p&gt;

&lt;p&gt;The Gateway sits on your machine. Your prompts and code do not pass through an OpenClaw-operated cloud relay on the way to Anthropic, OpenAI, or whoever. They go straight from your laptop to the model provider you picked.&lt;/p&gt;

&lt;p&gt;Claude Code does not have an equivalent intermediary, but it also does not need one because Anthropic is the only provider. The moment you have multi-provider support, you either need a relay (vendor lock-in risk) or a local gateway (the OpenClaw choice).&lt;/p&gt;

&lt;p&gt;A teammate of mine who lives in Brazil and spends meaningful time worrying about LGPD compliance pinged me at lunch to ask what the network diagram looked like. He liked what I sent him. That conversation alone might be worth the day.&lt;/p&gt;

&lt;h2&gt;
  
  
  ClawHub vs Claude Code Skills
&lt;/h2&gt;

&lt;p&gt;Claude Code Skills are markdown files plus optional resources, distributed however you distribute markdown. ClawHub is an npm-style package marketplace for OpenClaw skills.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw skills search &lt;span class="s2"&gt;"docker"&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install&lt;/span&gt; @clawhub/docker-manager
openclaw skills list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ClawHub had several thousand skills the day I tried it. The numbers Steinberger throws around at conferences are higher and probably accurate, but the count moves fast enough that any specific figure is wrong by the time you publish it.&lt;/p&gt;

&lt;p&gt;Two real differences I felt:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;ClawHub skills are JavaScript. They run in a sandbox but can request shell exec privileges. That makes them more capable than Claude Code Skills and more dangerous. The ClawHavoc incident in March of 2026 saw 341 malicious skills caught, which is a real cost of an open marketplace&lt;/li&gt;
&lt;li&gt;Claude Code Skills are simpler to author. I wrote a Skill in 20 minutes my first time. The equivalent ClawHub skill took me about 90 minutes because I had to learn the SDK conventions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you are an individual developer wanting to share a workflow, Skills are easier. If you are a team wanting a versioned, packaged, audited tool, ClawHub is better. They are not competing for the same problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 3pm moment where I almost stopped
&lt;/h2&gt;

&lt;p&gt;I asked OpenClaw to update some Python 3.8 code to 3.11 across a small repo, run the test suite, and report back.&lt;/p&gt;

&lt;p&gt;It did. The session ate 920K tokens, took about 14 minutes, found three places where my colleague had used the walrus operator wrong, and quietly fixed them. I checked the diff. It was right.&lt;/p&gt;

&lt;p&gt;Claude Code does the same thing. I have run the same prompt against it many times.&lt;/p&gt;

&lt;p&gt;The difference was not the output. The difference was that Claude Code is in my muscle memory. I have typed &lt;code&gt;claude&lt;/code&gt; three times a day for a year. When I typed &lt;code&gt;openclaw&lt;/code&gt; and waited the extra 1.2 seconds for the cold start, my fingers reached for &lt;code&gt;claude&lt;/code&gt; instead. Three times.&lt;/p&gt;

&lt;p&gt;That is the part nobody writes about. Switching costs are not just config. They are reflexes. By 3pm I had written half a SOUL.md, almost given up, made coffee, and come back. By 6pm I was OK again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would actually use each one for
&lt;/h2&gt;

&lt;p&gt;I built this matrix during the second coffee.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;OpenClaw&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Locked into Anthropic models?&lt;/td&gt;
&lt;td&gt;No, multi-provider&lt;/td&gt;
&lt;td&gt;Yes, Anthropic only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local model option&lt;/td&gt;
&lt;td&gt;Ollama&lt;/td&gt;
&lt;td&gt;None official&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skill distribution&lt;/td&gt;
&lt;td&gt;ClawHub package marketplace&lt;/td&gt;
&lt;td&gt;Markdown files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personality file&lt;/td&gt;
&lt;td&gt;SOUL.md (who is the agent)&lt;/td&gt;
&lt;td&gt;CLAUDE.md (what is the project)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network architecture&lt;/td&gt;
&lt;td&gt;Local Gateway, no relay&lt;/td&gt;
&lt;td&gt;Direct to Anthropic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maturity&lt;/td&gt;
&lt;td&gt;60 days old, foundation forming&lt;/td&gt;
&lt;td&gt;18 months, Anthropic-stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best at&lt;/td&gt;
&lt;td&gt;Multi-model teams, regulated environments&lt;/td&gt;
&lt;td&gt;Anthropic-first dev shops, simplicity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If your team is Anthropic-only and your CLAUDE.md is already 200 lines, do not switch. Claude Code is fine. The Skills you wrote are still fine. The pattern works.&lt;/p&gt;

&lt;p&gt;If your team is multi-provider, or your compliance team has questions about where prompts travel, or you want a backend model dropdown, OpenClaw is worth a Tuesday.&lt;/p&gt;

&lt;p&gt;I am still on Claude Code as my default. I have OpenClaw aliased to a separate command for the cases where I want to try a different model on the same prompt without paying for two SaaS subscriptions worth of context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes next
&lt;/h2&gt;

&lt;p&gt;OpenClaw moving to a foundation while Steinberger joins OpenAI is the part I am watching most closely. Foundations are how open-source projects survive their founders. They are also how projects ossify. The first six months of governance under the OpenClaw Foundation will tell you whether the project is going to be Linux or Helm.&lt;/p&gt;

&lt;p&gt;If you used to argue Claude Code vs Codex was a binary, OpenClaw is the answer that was supposed to be impossible: a third option that is not produced by an LLM lab. The economics of that are interesting. The next twelve months are going to teach us whether neutral, cross-provider, foundation-governed AI tooling is sustainable, or whether it gets quietly absorbed.&lt;/p&gt;

&lt;p&gt;I am betting on sustainable. I have also been wrong about agents in roughly all of the previous quarters, so adjust accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this all costs to know
&lt;/h2&gt;

&lt;p&gt;If you take one thing from my Tuesday, take this. OpenClaw and Claude Code are not competitors. They are two answers to the same question: what should the AI inside your terminal be allowed to do without asking you first? SOUL.md and CLAUDE.md are different shapes of the same trust contract. The team that wrote each chose differently because they had different assumptions about who was sitting in front of the screen.&lt;/p&gt;

&lt;p&gt;The right tool is the one whose assumptions match yours. Pick on assumptions, not stars.&lt;/p&gt;




&lt;p&gt;If you want the harness-engineering frame on this (CLAUDE.md tiers, hooks, sub-agents, how to think about the shell around the prompt, not just the prompt), this is the reference:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://kenimoto.dev/books/harness-engineering-guide?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=openclaw-vs-claudecode" rel="noopener noreferrer"&gt;Harness Engineering: A Practitioner's Guide&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>agents</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Is AI Actually Citing Your Site? How to Measure What Google Rankings Can't</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Thu, 21 May 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/kenimo49/is-ai-actually-citing-your-site-how-to-measure-what-google-rankings-cant-1cnm</link>
      <guid>https://dev.to/kenimo49/is-ai-actually-citing-your-site-how-to-measure-what-google-rankings-cant-1cnm</guid>
      <description>&lt;p&gt;I've spent the past few weeks writing about LLMO: how to get cited by AI search engines, which content structures work, what Princeton's GEO study says about visibility. All useful stuff. One problem: I had no idea whether any of it was actually working.&lt;/p&gt;

&lt;p&gt;I was like a chef who obsesses over recipes but never tastes the food. My Google Search Console was immaculate. My LLMO measurement setup? I was literally typing "does ChatGPT know about my site" into ChatGPT and refreshing the page like a teenager checking if their crush liked their post.&lt;/p&gt;

&lt;p&gt;Measuring LLMO is a genuinely hard problem, and most people aren't doing it at all. Here's what I've built: three measurement layers, from "costs nothing" to "costs you a Saturday afternoon of Python."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fww81qxyfq200ipf30nfu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fww81qxyfq200ipf30nfu.png" alt="SEO KPIs vs LLMO KPIs: ranking position disappears, citation becomes the metric" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Measurement Gap
&lt;/h2&gt;

&lt;p&gt;In SEO, measurement is a solved problem. Google Search Console shows rankings, impressions, clicks, and CTR for free, updated daily. Ahrefs adds backlink data. SEMrush gives you keyword tracking. Everything is visible.&lt;/p&gt;

&lt;p&gt;In LLMO, almost nothing is visible out of the box.&lt;/p&gt;

&lt;p&gt;There's no "AI Search Console." ChatGPT doesn't send a weekly email saying "You were cited 47 times!" Perplexity has no creator dashboard. The shift: SEO had rankings (1st through 100th position). LLMO has a binary outcome. You're either cited or you're not. And nobody is telling you which.&lt;/p&gt;

&lt;p&gt;This gap isn't just an inconvenience. You can't improve what you can't measure, and right now, most content creators are optimizing for AI visibility while flying blind.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxsqdm16d4c11twqezz6y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxsqdm16d4c11twqezz6y.png" alt="Three layers of LLMO measurement: GA4, manual protocol, Python automation" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: GA4 AI Referral Traffic (Free, 5 Minutes)
&lt;/h2&gt;

&lt;p&gt;The easiest measurement you can set up today is tracking AI referral traffic in Google Analytics 4. When an AI search engine cites your site with a clickable link and someone clicks it, GA4 records the source.&lt;/p&gt;

&lt;p&gt;Here is the regex pattern I use in a custom channel group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chatgpt\.com|perplexity\.ai|claude\.ai|gemini\.google\.com|copilot\.microsoft\.com|deepseek\.com|you\.com|meta\.ai|poe\.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go to &lt;strong&gt;Admin → Channel Groups → Create&lt;/strong&gt;, add a new channel with this regex as the session source filter, and name it "AI Search." You'll immediately see aggregated traffic from all AI platforms in one view.&lt;/p&gt;

&lt;p&gt;A few things to know:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ChatGPT plays nicely.&lt;/strong&gt; Since late 2025, ChatGPT appends &lt;code&gt;utm_source=chatgpt.com&lt;/code&gt; to outbound links. ChatGPT traffic shows up cleanly as &lt;code&gt;chatgpt.com / referral&lt;/code&gt; in GA4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Perplexity is decent.&lt;/strong&gt; Traffic appears as &lt;code&gt;perplexity.ai / referral&lt;/code&gt;, though without UTM tags. Still trackable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free-tier ChatGPT is a black hole.&lt;/strong&gt; Free users often don't send referrer data due to privacy settings. Their clicks show up as "Direct," indistinguishable from someone typing your URL manually. Your GA4 numbers are a floor, not a ceiling.&lt;/p&gt;

&lt;p&gt;The conversion story is where this gets interesting. Industry data from 2026 shows AI referral traffic converts at 8-12%, compared to 2-3% for traditional Google organic. People who arrive via AI search have already done their research. The AI did it for them. They are further along in the decision process.&lt;/p&gt;

&lt;p&gt;I started tracking three weeks ago. My AI referral traffic is still small (single digits daily), but the conversion rate is 3x my organic average. Small sample, but a signal worth watching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: The "Ask Five AIs" Protocol (Free, 30 Min/Month)
&lt;/h2&gt;

&lt;p&gt;GA4 tells you who clicked through. It does not tell you whether AI is &lt;em&gt;mentioning&lt;/em&gt; you without linking, or whether it is mentioning you at all.&lt;/p&gt;

&lt;p&gt;For that, you need to ask directly. I run this on the first Monday of every month:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1.&lt;/strong&gt; Write 10-15 prompts related to your niche. Mine include "What are the best resources for AI search optimization?", "How do I get my site cited by ChatGPT?", and "LLMO vs SEO differences."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2.&lt;/strong&gt; Run each prompt on five platforms: ChatGPT, Perplexity, Gemini, Claude, and Copilot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3.&lt;/strong&gt; Record four things per prompt per platform:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mentioned? (Yes / No)&lt;/li&gt;
&lt;li&gt;Context (recommendation / comparison / neutral / negative)&lt;/li&gt;
&lt;li&gt;Accuracy of information&lt;/li&gt;
&lt;li&gt;URL provided?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 4.&lt;/strong&gt; Calculate your citation rate. 15 prompts x 5 platforms = 75 checks. Mentioned 20 times? That's 26.7%.&lt;/p&gt;

&lt;p&gt;This takes about 30 minutes with a spreadsheet. It's manual and tedious, and also the most reliable method that exists today. Automated tools can approximate this, but they can't replicate the nuance of "was that mention positive or just a passing reference?"&lt;/p&gt;

&lt;p&gt;One caveat: LLM responses are non-deterministic. The same prompt can produce different answers on different days. A single check isn't statistically significant. That is why I track the monthly trend, not individual data points. Three months of data starts showing real patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: Automate It With Python (One Saturday)
&lt;/h2&gt;

&lt;p&gt;If you're an engineer, you can automate the manual protocol with API calls. Hit the OpenAI and Anthropic APIs with your query set, check whether your brand appears in the response, and log results as a time series.&lt;/p&gt;

&lt;p&gt;The core logic is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;BRAND_VARIANTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-site.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Your Brand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yourbrand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;CHECK_QUERIES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Best tools for [your category]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How to solve [problem you address]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[Your brand] vs [competitor]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_openai&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;
    &lt;span class="n"&gt;mentioned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;BRAND_VARIANTS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;platform&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ChatGPT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mentioned&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;mentioned&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Extend this for Claude and Perplexity, run weekly via cron, dump to CSV. You get a time series of your AI visibility score for about $0.50/week.&lt;/p&gt;

&lt;p&gt;The payoff: instead of "I think LLMO is working," you can say "my visibility went from 12% to 28% after I added structured data." Numbers beat feelings.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Available in May 2026
&lt;/h2&gt;

&lt;p&gt;If building your own tools isn't your thing, several commercial platforms now track AI citations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Otterly.ai&lt;/strong&gt; is the fastest-growing option, with 10,000+ users since launching in October 2024. It monitors your brand across ChatGPT, Perplexity, Google AI Overviews, and Copilot. Keyword-level citation tracking, competitor benchmarking, and clean dashboards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Profound&lt;/strong&gt; sits at the enterprise end. Their published case study with Ramp, where they went from 3.2% to 22.2% AI visibility in one month, is the kind of result that gets budget approved. If you're a larger organization, this is where you'll probably land.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Peec AI&lt;/strong&gt; focuses on brand mention analysis across LLM outputs. Beyond whether you're cited, it tracks how: what sentiment surrounds your mentions, which prompt patterns trigger citations.&lt;/p&gt;

&lt;p&gt;My honest take: for individual creators and small teams, the manual protocol plus a basic Python script gives you 80% of the insight at 0% of the cost. Commercial tools become worthwhile when you're tracking dozens of keywords across multiple brands and need team dashboards.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Crawler Signal You're Probably Ignoring
&lt;/h2&gt;

&lt;p&gt;Here's a measurement angle most people miss: AI crawler logs.&lt;/p&gt;

&lt;p&gt;Your server access logs already record which AI systems are visiting your content. GPTBot (OpenAI), ClaudeBot (Anthropic), PerplexityBot, Google-Extended. They all identify themselves in the User-Agent string.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"GPTBot|ClaudeBot|PerplexityBot|Google-Extended"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /var/log/nginx/access.log | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $7}'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pages that get crawled frequently are more likely to appear in AI responses. Pages that never get crawled are invisible. It is an indirect signal, but useful for finding content that AI systems are skipping entirely.&lt;/p&gt;

&lt;p&gt;I checked my own logs and found that &lt;code&gt;/blog/&lt;/code&gt; pages get crawled 15x more than my &lt;code&gt;/about/&lt;/code&gt; page. Not shocking, but the gap was wider than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a Measurement Habit
&lt;/h2&gt;

&lt;p&gt;Measurement without action is just data hoarding. Here is the cycle I run:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weekly (10 min):&lt;/strong&gt; Check GA4 AI referral dashboard. Note spikes or drops. Compare week-over-week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monthly (30 min):&lt;/strong&gt; Run the five-platform manual protocol. Calculate citation rate. Scan crawler logs for new patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quarterly (1 hour):&lt;/strong&gt; Full review. Update query set. Compare citation rate trends. Check whether content changes produced measurable results.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://llmoframework.com" rel="noopener noreferrer"&gt;LLMO Framework&lt;/a&gt; provides a structured approach to KPI design if you want a more formal methodology. I reference it when deciding which metrics matter most at different growth stages.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Punchline
&lt;/h2&gt;

&lt;p&gt;I started measuring my LLMO visibility three weeks ago. My citation rate across five platforms is 14%. Not great. Not terrible. But the important part is that I &lt;em&gt;know&lt;/em&gt; the number, and three months from now I'll know whether it went up or down.&lt;/p&gt;

&lt;p&gt;The SEO world figured out measurement twenty years ago. The LLMO world is still in its "checking rankings by Googling yourself" era. The people who build measurement infrastructure now will have a compounding advantage over those who keep guessing.&lt;/p&gt;

&lt;p&gt;If you're still typing your brand name into ChatGPT and squinting at the output, I get it. I was doing the same thing last month. But now I have a spreadsheet, a cron job, and a regex filter in GA4. Less romantic, more informative. I'll take that trade.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.1clickreport.com/blog/track-ai-traffic-ga4-chatgpt-perplexity-claude" rel="noopener noreferrer"&gt;How to Track AI Traffic in GA4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ziptie.dev/blog/best-llmo-tools/" rel="noopener noreferrer"&gt;Best LLMO Tools in 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2311.09735" rel="noopener noreferrer"&gt;GEO: Generative Engine Optimization&lt;/a&gt; by Aggarwal et al., Princeton / ACM SIGKDD 2024&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://llmoframework.com" rel="noopener noreferrer"&gt;LLMO Framework&lt;/a&gt;: KPI design and implementation guide&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The full playbook (llms.txt patterns, JSON-LD examples, citation-rate KPIs, and ChatGPT/Perplexity/Brave comparison) is in &lt;a href="https://kenimoto.dev/books/llmo-ai-search-optimization?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=measure-ai-citations-kpi" rel="noopener noreferrer"&gt;LLMO Practical Guide: Why ChatGPT Ignores Your Website&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>llmo</category>
      <category>seo</category>
      <category>analytics</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
