<?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: Quentin Wendegass</title>
    <description>The latest articles on DEV Community by Quentin Wendegass (@quentin_wendegass).</description>
    <link>https://dev.to/quentin_wendegass</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4189175%2Fae69c3f9-e75f-413e-a1d4-635b8ef4335c.png</url>
      <title>DEV Community: Quentin Wendegass</title>
      <link>https://dev.to/quentin_wendegass</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/quentin_wendegass"/>
    <language>en</language>
    <item>
      <title>Quite interesting how many different bots come to your site.</title>
      <dc:creator>Quentin Wendegass</dc:creator>
      <pubDate>Tue, 03 Nov 2026 10:41:51 +0000</pubDate>
      <link>https://dev.to/quentin_wendegass/quite-interesting-how-many-different-bots-come-to-your-site-4cgi</link>
      <guid>https://dev.to/quentin_wendegass/quite-interesting-how-many-different-bots-come-to-your-site-4cgi</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/quentin_wendegass/claudebot-gptbot-chatgpt-user-which-ai-crawler-does-what-3o99" class="crayons-story__hidden-navigation-link"&gt;ClaudeBot, GPTBot, ChatGPT-User: which AI crawler does what&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/quentin_wendegass" class="crayons-avatar  crayons-avatar--l  "&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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4189175%2Fae69c3f9-e75f-413e-a1d4-635b8ef4335c.png" alt="quentin_wendegass profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/quentin_wendegass" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Quentin Wendegass
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Quentin Wendegass
                
                
              
              &lt;div id="story-author-preview-content-4968482" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/quentin_wendegass" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4189175%2Fae69c3f9-e75f-413e-a1d4-635b8ef4335c.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Quentin Wendegass&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/quentin_wendegass/claudebot-gptbot-chatgpt-user-which-ai-crawler-does-what-3o99" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Nov 3&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/quentin_wendegass/claudebot-gptbot-chatgpt-user-which-ai-crawler-does-what-3o99" id="article-link-4968482"&gt;
          ClaudeBot, GPTBot, ChatGPT-User: which AI crawler does what
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/seo"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;seo&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/agents"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;agents&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/quentin_wendegass/claudebot-gptbot-chatgpt-user-which-ai-crawler-does-what-3o99" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;3&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/quentin_wendegass/claudebot-gptbot-chatgpt-user-which-ai-crawler-does-what-3o99#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            11 min read
          &lt;/small&gt;
          
          &lt;div class="only-sidebar-menu-item trusted-visible-block print-hidden align-center"&gt;
                
                    
                
          &lt;/div&gt;

        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>ai</category>
      <category>chatgpt</category>
      <category>llm</category>
    </item>
    <item>
      <title>ClaudeBot, GPTBot, ChatGPT-User: which AI crawler does what</title>
      <dc:creator>Quentin Wendegass</dc:creator>
      <pubDate>Tue, 03 Nov 2026 10:40:14 +0000</pubDate>
      <link>https://dev.to/quentin_wendegass/claudebot-gptbot-chatgpt-user-which-ai-crawler-does-what-3o99</link>
      <guid>https://dev.to/quentin_wendegass/claudebot-gptbot-chatgpt-user-which-ai-crawler-does-what-3o99</guid>
      <description>&lt;p&gt;Open your server log and you find names like ClaudeBot, GPTBot, ChatGPT-User, OAI-SearchBot and meta-externalagent. They are AI crawlers, and they are not all doing the same thing. Most AI companies send up to three of them, each for a different job.&lt;/p&gt;

&lt;p&gt;One collects pages to train future models. One keeps the company's search index up to date. And one reads a single page right now, because a person just asked ChatGPT or Claude a question that page answers. Which one you are looking at decides what blocking it does, whether it listens to your robots.txt, and whether your analytics will ever see it. Below is what each company documents, and what actually came to our own site in five days.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqkp7aj8atcuqnnm2pfwv.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqkp7aj8atcuqnnm2pfwv.png" alt="Every AI company's crawlers by job: training, search and reading for an answer, with whether each follows robots.txt" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Every AI company's crawlers by job, as each company documents them. Checked 3 November 2026.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three jobs, three crawlers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Collecting training data.&lt;/strong&gt; GPTBot, ClaudeBot, meta-externalagent and their kind copy public pages into the data future models learn from. They crawl on their company's schedule, not because anyone asked anything. Blocking them keeps your pages out of future training sets. It does not remove anything already collected, and it does not stop the same company from finding and quoting your pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reading for its search.&lt;/strong&gt; OAI-SearchBot, Claude-SearchBot and PerplexityBot build the search index the AI service answers from, much like Googlebot does for Google. A page that is not in that index cannot turn up when someone asks. Blocking a search crawler is how you disappear from that service's answers, which is usually not what a site owner wants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reading for an answer.&lt;/strong&gt; ChatGPT-User, Claude-User and Perplexity-User request one page at the moment a person needs it: someone pasted your link, or the service decided your page answers their question. Each request is one person's question. This is the closest thing to a visit an AI service makes, and it is the one your analytics never shows (more on that below).&lt;/p&gt;

&lt;p&gt;Each job is its own name in robots.txt. OpenAI and Perplexity say plainly that every setting is independent, and Anthropic describes what blocking each of its three crawlers does on its own. Blocking GPTBot keeps ChatGPT's search and answers exactly as they were.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every AI crawler, by company
&lt;/h2&gt;

&lt;p&gt;What each company documents about its crawlers, in its own words where it matters. Each company's page is linked; that is where to look when a name changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenAI: GPTBot, OAI-SearchBot and ChatGPT-User
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;GPTBot&lt;/strong&gt; collects training data: disallowing it "indicates a site's content should not be used in training generative AI foundation models." &lt;strong&gt;OAI-SearchBot&lt;/strong&gt; builds ChatGPT's search, and "sites that are opted out of OAI-SearchBot will not be shown in ChatGPT search answers." &lt;strong&gt;ChatGPT-User&lt;/strong&gt; reads a page when someone asks ChatGPT or a custom GPT something, and "because these actions are initiated by a user, robots.txt rules may not apply." OpenAI adds that if you allow both GPTBot and OAI-SearchBot, it may use one crawl for both purposes. Changes to robots.txt take about 24 hours to reach its search. &lt;a href="https://developers.openai.com/api/docs/bots" rel="noopener noreferrer"&gt;OpenAI's crawler page&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Anthropic: ClaudeBot, Claude-SearchBot and Claude-User
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;ClaudeBot&lt;/strong&gt; collects training data. Blocking it "signals that the site's future materials should be excluded from our AI model training datasets." It also honours &lt;code&gt;Crawl-delay&lt;/code&gt;, if you want it to come less often. &lt;strong&gt;Claude-SearchBot&lt;/strong&gt; builds Claude's search, and &lt;strong&gt;Claude-User&lt;/strong&gt; reads a page when someone asks Claude a question. Anthropic says all three follow robots.txt, and that blocking Claude-User "prevents our system from retrieving your content in response to a user query." That makes it the only company of the big ones whose answer crawler listens to robots.txt, and our own test agrees: Claude asked for robots.txt one second before it read the page. ClaudeBot is also quick: on our test site it arrived within minutes of the TLS certificate being issued, before anyone had the link. &lt;a href="https://support.claude.com/en/articles/8896518-does-anthropic-crawl-data-from-the-web-and-how-can-site-owners-block-the-crawler" rel="noopener noreferrer"&gt;Anthropic's crawler page&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Google: Googlebot, Google-Extended and the Gemini crawlers
&lt;/h3&gt;

&lt;p&gt;Google has no separate AI training crawler. &lt;strong&gt;Google-Extended&lt;/strong&gt; is "a standalone product token": a name you use in robots.txt to say whether pages Googlebot already crawled may train Gemini and ground its answers. No request ever carries it, and it "does not impact a site's inclusion in Google Search." Google's page does not mention AI Overviews; they come from the ordinary search index, which &lt;strong&gt;Googlebot&lt;/strong&gt; builds. For reading on a person's behalf, Google lists &lt;strong&gt;Google-Agent&lt;/strong&gt; (agents that "browse and act on user request") and &lt;strong&gt;Google-GeminiNotebook&lt;/strong&gt; (pages someone added as sources), among other user-triggered fetchers that "generally ignore robots.txt rules." The Gemini app itself is not on that list: in our test it read the page as a bare &lt;code&gt;Google&lt;/code&gt;. &lt;a href="https://developers.google.com/crawling/docs/crawlers-fetchers/google-common-crawlers" rel="noopener noreferrer"&gt;Google's common crawlers&lt;/a&gt;, &lt;a href="https://developers.google.com/crawling/docs/crawlers-fetchers/google-user-triggered-fetchers" rel="noopener noreferrer"&gt;user-triggered fetchers&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Perplexity: PerplexityBot and Perplexity-User
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;PerplexityBot&lt;/strong&gt; builds Perplexity's search and "is not used to crawl content for AI foundation models"; Perplexity documents no training crawler. &lt;strong&gt;Perplexity-User&lt;/strong&gt; reads a page for a person's question and "generally ignores robots.txt rules." In our test, though, Perplexity answered a person's question with PerplexityBot, so you can see the search crawler's name on a request that was really for an answer. &lt;a href="https://docs.perplexity.ai/docs/resources/perplexity-crawlers" rel="noopener noreferrer"&gt;Perplexity's crawler page&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Meta: meta-externalagent, meta-webindexer and meta-externalfetcher
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;meta-externalagent&lt;/strong&gt; crawls to "train foundation AI models or to improve products by indexing content directly". &lt;strong&gt;meta-webindexer&lt;/strong&gt; improves search results in Meta AI, and allowing it "helps us cite and link to your content." &lt;strong&gt;meta-externalfetcher&lt;/strong&gt; reads individual links at a user's request, and "may bypass robots.txt rules." As with Perplexity, our test saw Meta AI answer a question with meta-webindexer. The link previews in Facebook, Instagram and WhatsApp come from a fourth name, facebookexternalhit. &lt;a href="https://developers.facebook.com/docs/sharing/webmasters/web-crawlers" rel="noopener noreferrer"&gt;Meta's crawler page&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistral, Amazon, Apple and Microsoft
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Mistral&lt;/strong&gt; has the clearest names: MistralAI-Training, MistralAI-Index and MistralAI-User. It doesn't say whether MistralAI-User follows robots.txt; in our test it read robots.txt once and then every page twice in the same second. &lt;a href="https://docs.mistral.ai/robots" rel="noopener noreferrer"&gt;Mistral's page&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Amazon&lt;/strong&gt; sends Amazonbot, which "may be used to train Amazon AI models", Amzn-SearchBot for search, and Amzn-User for questions such as Alexa's, which "may not follow all robots.txt directives." Amazonbot does more than training, so blocking it costs more than a training opt-out. &lt;a href="https://developer.amazon.com/docs/amazonbot/amazonbot.html" rel="noopener noreferrer"&gt;Amazon's page&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apple&lt;/strong&gt; crawls with Applebot for Siri, Spotlight and Safari. &lt;strong&gt;Applebot-Extended&lt;/strong&gt; is a robots.txt name like Google-Extended: it "does not crawl webpages", and disallowing it keeps your pages out of Apple's model training while they stay in search. &lt;a href="https://support.apple.com/en-us/119829" rel="noopener noreferrer"&gt;Apple's page&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Microsoft&lt;/strong&gt; documents no separate AI crawler. Copilot answers from Bing's index, which bingbot builds, so blocking bingbot removes you from Bing and Copilot at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  The ones that don't say
&lt;/h3&gt;

&lt;p&gt;Some AI services document nothing we could check. &lt;strong&gt;Bytespider&lt;/strong&gt; is ByteDance's crawler; reports disagree on whether it respects robots.txt. &lt;strong&gt;xAI&lt;/strong&gt; documents no crawler for Grok, and in our test Grok's browsing agent looked exactly like a person's Chrome, from the person's own connection. &lt;strong&gt;DeepSeek&lt;/strong&gt; read our page in a full browser with an ordinary Firefox user agent. Nothing in a log line names either of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually came to our site in five days
&lt;/h2&gt;

&lt;p&gt;From 29 October to 2 November 2026 our server reported every crawler request to privatrak.com and its subdomains: 3,785 requests. 1,661 of them, 44%, carried the name of an AI crawler.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F26gbjqzsh2fr86mrtk3h.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F26gbjqzsh2fr86mrtk3h.png" alt="Requests per AI crawler name on privatrak.com over five days, split into pages, robots.txt and sitemaps, and requests for secret files" width="800" height="787"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Requests per crawler name on privatrak.com, 29 October to 2 November 2026, as our server reported them.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Training came first.&lt;/strong&gt; GPTBot requested 384 pages in five days, more than any other crawler, search engines included: Bingbot requested 297 and Googlebot 133.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Answers were the smaller part, and the most valuable.&lt;/strong&gt; ChatGPT-User requested 62 pages and Claude-User 27, each one a person whose question our page answered. 46 of ChatGPT's were for the home page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At least 635 of the 1,661 AI-named requests, 38%, were not AI crawlers at all.&lt;/strong&gt; They asked for &lt;code&gt;.env&lt;/code&gt; files, &lt;code&gt;/proc/self/environ&lt;/code&gt;, private keys and &lt;code&gt;docker-compose.yml&lt;/code&gt;: files no website shows its visitors, which scanners try in the hope of finding a leaked password. Under the names of answer crawlers there were more of these (146) than page requests (97). Perplexity-User asked for a secret file 36 times out of 40, DuckAssistBot 38 out of 42, Claude-SearchBot 47 out of 52. Nobody borrowed Meta's names, the one large company whose crawlers can't be checked against a published IP list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;llms.txt was hardly read.&lt;/strong&gt; Across every crawler, it was requested three times in five days: once each by GPTBot, Googlebot and GoogleOther. ClaudeBot, on the other hand, asked for robots.txt 30 times while requesting 19 pages.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One site, five days. The 38% is a lower bound: it counts only requests for files that are never a page, so a scanner asking for &lt;code&gt;/admin&lt;/code&gt; under ChatGPT's name counts as a page request.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What robots.txt can and can't do
&lt;/h2&gt;

&lt;p&gt;robots.txt is a request, not a lock. The training and search crawlers of the companies above say they follow it. The crawlers that read for an answer mostly say they may not, on the grounds that a person asked: OpenAI, Google, Perplexity, Meta and Amazon all say so in one form or another. Anthropic is the exception.&lt;/p&gt;

&lt;p&gt;So the choice robots.txt gives you is mostly about training. If you want to stay out of training sets but keep turning up in AI answers, block the training names and leave the rest alone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-agent: GPTBot
User-agent: ClaudeBot
User-agent: Google-Extended
User-agent: Applebot-Extended
User-agent: meta-externalagent
User-agent: MistralAI-Training
User-agent: Amazonbot
User-agent: CCBot
Disallow: /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CCBot is Common Crawl, whose public archive many models are trained on. Amazonbot is there because Amazon says it may train on what it collects, but it also feeds other Amazon products, so leave it out if that matters to you. Expect a day before a change takes effect: OpenAI and Perplexity name about 24 hours, Meta caches robots.txt for up to 24 hours and Amazon for up to 30 days.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your analytics shows none of this
&lt;/h2&gt;

&lt;p&gt;Analytics tools such as Google Analytics, Plausible or Privatrak count a visit when the page's JavaScript runs in a browser. A crawler that reads the page as text never runs it. When we &lt;a href="https://privatrak.com/blog/ai-agents-in-analytics" rel="noopener noreferrer"&gt;sent AI services to a test site&lt;/a&gt;, ChatGPT, Claude, Gemini, Perplexity, Mistral and Meta AI all read the page without running a single script. None of them reached our analytics, Google Analytics included.&lt;/p&gt;

&lt;p&gt;Our own site agrees. In the five days above, ChatGPT-User, Claude-User and the other answer crawlers requested 97 pages and left not one page view in our analytics. Search crawlers sometimes do run the script: OAI-SearchBot showed up 4 times and meta-webindexer 10 times.&lt;/p&gt;

&lt;p&gt;That is fine for training and search crawlers, which you don't want in your visitor numbers anyway. It is a real gap for the answer crawlers. Each of their requests is a person asking an AI service a question your page answers, and that is a number worth knowing. It shows whether AI services read you at all, which pages they read, and whether that turns into visitors clicking through.&lt;/p&gt;

&lt;p&gt;Only your server sees those requests. Your server log has them; so does a log at your CDN. Privatrak can count them if your server reports the requests it answers: a setting in the WordPress plugin, a Cloudflare Worker, or a few lines in Next.js or Nuxt. Your server only reports requests from known crawlers, so nothing about your visitors leaves it. The &lt;a href="https://privatrak.com/docs/ai-services" rel="noopener noreferrer"&gt;AI services&lt;/a&gt; page then shows how often ChatGPT, Claude and the others read your pages for an answer, next to the visitors they sent you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it really ClaudeBot? Check the IP address
&lt;/h2&gt;

&lt;p&gt;A user agent is a claim, and anyone can make it. Google says as much: user agent strings "can be spoofed." On our own site, at least 38% of the requests carrying an AI crawler's name were scanners (see above), borrowing a name that sites tend to let through.&lt;/p&gt;

&lt;p&gt;The one reliable check is the address the request came from. Most companies publish the IP ranges their crawlers use, and a request from outside them is not that crawler, whatever it says it is:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Company&lt;/th&gt;
&lt;th&gt;Crawlers&lt;/th&gt;
&lt;th&gt;IP lists&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;GPTBot, OAI-SearchBot, ChatGPT-User&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://openai.com/gptbot.json" rel="noopener noreferrer"&gt;gptbot.json&lt;/a&gt;, &lt;a href="https://openai.com/searchbot.json" rel="noopener noreferrer"&gt;searchbot.json&lt;/a&gt;, &lt;a href="https://openai.com/chatgpt-user.json" rel="noopener noreferrer"&gt;chatgpt-user.json&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;ClaudeBot, Claude-SearchBot, Claude-User&lt;/td&gt;
&lt;td&gt;&lt;a href="https://claude.com/crawling/bots.json" rel="noopener noreferrer"&gt;bots.json&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;Googlebot, Google-Agent, Google-GeminiNotebook&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://developers.google.com/static/search/apis/ipranges/googlebot.json" rel="noopener noreferrer"&gt;googlebot.json&lt;/a&gt;, &lt;a href="https://developers.google.com/static/crawling/ipranges/user-triggered-fetchers-google.json" rel="noopener noreferrer"&gt;user-triggered-fetchers-google.json&lt;/a&gt;, &lt;a href="https://developers.google.com/static/crawling/ipranges/user-triggered-agents.json" rel="noopener noreferrer"&gt;user-triggered-agents.json&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perplexity&lt;/td&gt;
&lt;td&gt;PerplexityBot, Perplexity-User&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.perplexity.ai/perplexitybot.json" rel="noopener noreferrer"&gt;perplexitybot.json&lt;/a&gt;, &lt;a href="https://www.perplexity.ai/perplexity-user.json" rel="noopener noreferrer"&gt;perplexity-user.json&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mistral&lt;/td&gt;
&lt;td&gt;MistralAI-Index, MistralAI-User&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://mistral.ai/mistralai-index-ips.json" rel="noopener noreferrer"&gt;mistralai-index-ips.json&lt;/a&gt;, &lt;a href="https://mistral.ai/mistralai-user-ips.json" rel="noopener noreferrer"&gt;mistralai-user-ips.json&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon&lt;/td&gt;
&lt;td&gt;Amazonbot, Amzn-SearchBot, Amzn-User&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://developer.amazon.com/amazonbot/ip-addresses/" rel="noopener noreferrer"&gt;Amazonbot&lt;/a&gt;, &lt;a href="https://developer.amazon.com/amazonbot/searchbot-ip-addresses/" rel="noopener noreferrer"&gt;Amzn-SearchBot&lt;/a&gt;, &lt;a href="https://developer.amazon.com/amazonbot/live-ip-addresses/" rel="noopener noreferrer"&gt;Amzn-User&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apple&lt;/td&gt;
&lt;td&gt;Applebot&lt;/td&gt;
&lt;td&gt;&lt;a href="https://search.developer.apple.com/applebot.json" rel="noopener noreferrer"&gt;applebot.json&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft&lt;/td&gt;
&lt;td&gt;bingbot&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.bing.com/toolbox/bingbot.json" rel="noopener noreferrer"&gt;bingbot.json&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meta&lt;/td&gt;
&lt;td&gt;meta-externalagent, meta-webindexer, meta-externalfetcher&lt;/td&gt;
&lt;td&gt;none published&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Every list was reachable on 3 November 2026. Mistral publishes none for MistralAI-Training. Google and Microsoft also document a reverse DNS check, and Bing has a &lt;a href="https://www.bing.com/toolbox/verify-bingbot" rel="noopener noreferrer"&gt;Verify Bingbot&lt;/a&gt; tool.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Meta is the one large company that publishes no list, so a request calling itself meta-externalagent can't be checked this way. The lists also change, so a firewall rule built from them needs refreshing. Anthropic warns that blocking by IP "may not work correctly or persistently guarantee an opt-out": the lists are for checking who a request is from, robots.txt is for saying what you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One AI company, up to three crawlers: one for training, one for its search, one that reads for a person's answer. Each is a separate name in robots.txt.&lt;/li&gt;
&lt;li&gt;Blocking a training crawler keeps you out of future training data and nothing else. Blocking a search crawler takes you out of that service's answers.&lt;/li&gt;
&lt;li&gt;Crawlers that read for an answer mostly don't follow robots.txt. Anthropic's does.&lt;/li&gt;
&lt;li&gt;None of the AI services we tested ran the page's JavaScript, so only your server sees them.&lt;/li&gt;
&lt;li&gt;On our site, at least 38% of the requests under AI crawler names were scanners looking for passwords, and llms.txt was requested three times in five days.&lt;/li&gt;
&lt;li&gt;A name is a claim. The published IP lists are how you check it.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://privatrak.com/blog/ai-crawlers" rel="noopener noreferrer"&gt;Privatrak blog&lt;/a&gt;. Privatrak is privacy-first product analytics that counts how often ChatGPT, Claude and Perplexity read your pages for an answer, apart from training and search crawlers. &lt;a href="https://demo.privatrak.com" rel="noopener noreferrer"&gt;Try the live demo&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>seo</category>
      <category>agents</category>
    </item>
    <item>
      <title>Over the last days I did a lot of research and testing on how to detect AI agents on your website. Long story short: It's not so easy.</title>
      <dc:creator>Quentin Wendegass</dc:creator>
      <pubDate>Tue, 27 Oct 2026 08:07:06 +0000</pubDate>
      <link>https://dev.to/quentin_wendegass/over-the-last-days-i-did-a-lot-of-research-and-testing-on-how-to-detect-ai-agents-on-your-website-120</link>
      <guid>https://dev.to/quentin_wendegass/over-the-last-days-i-did-a-lot-of-research-and-testing-on-how-to-detect-ai-agents-on-your-website-120</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/quentin_wendegass/we-sent-23-ai-agents-to-our-website-most-looked-human-3a7e" class="crayons-story__hidden-navigation-link"&gt;We sent 23 AI agents to our website. Most looked human&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/quentin_wendegass" class="crayons-avatar  crayons-avatar--l  "&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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4189175%2Fae69c3f9-e75f-413e-a1d4-635b8ef4335c.png" alt="quentin_wendegass profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/quentin_wendegass" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Quentin Wendegass
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Quentin Wendegass
                
                
              
              &lt;div id="story-author-preview-content-4926671" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/quentin_wendegass" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4189175%2Fae69c3f9-e75f-413e-a1d4-635b8ef4335c.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Quentin Wendegass&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/quentin_wendegass/we-sent-23-ai-agents-to-our-website-most-looked-human-3a7e" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 27&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/quentin_wendegass/we-sent-23-ai-agents-to-our-website-most-looked-human-3a7e" id="article-link-4926671"&gt;
          We sent 23 AI agents to our website. Most looked human
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/agents"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;agents&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/analytics"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;analytics&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/quentin_wendegass/we-sent-23-ai-agents-to-our-website-most-looked-human-3a7e" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;2&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/quentin_wendegass/we-sent-23-ai-agents-to-our-website-most-looked-human-3a7e#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            15 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>agents</category>
      <category>ai</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>We sent 23 AI agents to our website. Most looked human</title>
      <dc:creator>Quentin Wendegass</dc:creator>
      <pubDate>Tue, 27 Oct 2026 07:57:21 +0000</pubDate>
      <link>https://dev.to/quentin_wendegass/we-sent-23-ai-agents-to-our-website-most-looked-human-3a7e</link>
      <guid>https://dev.to/quentin_wendegass/we-sent-23-ai-agents-to-our-website-most-looked-human-3a7e</guid>
      <description>&lt;p&gt;AI assistants now open websites for people. You ask ChatGPT to compare two pricing pages, and a browser somewhere clicks through them for you. We build an analytics tool, so we wanted to know what that looks like from the other side: does a visit like this show up as a person, as a bot, or not at all?&lt;/p&gt;

&lt;p&gt;So we built a small test site, gave two dozen AI agents, chatbots and developer tools the same task, and recorded every request they sent and every click they made. Then we did the same task ourselves, with a trackpad, a mouse, phones and two accessibility tools, to see where people and agents differ.&lt;/p&gt;

&lt;p&gt;The short version: most agents that use a browser look exactly like a person in Chrome. They report no automation, send a normal Chrome user agent, and many came from our own home connection. Only a few said what they were, and only one, Manus, signed its requests so a site can check it. Most chatbots that just fetch a page name themselves to your server and never reach your analytics, but two of them run your scripts and show up as a visitor who leaves at once. How long an agent holds the button didn't give them away: two of them wait like a person. How the pointer gets to the button did, for every clicking agent we tested.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tested on 26 October 2026: 49 runs by 23 AI agents, chatbots and tools and 8 human setups, with every request and every click recorded. Every visitor got the same task on the same three pages. The method and its limits are at the end.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The test
&lt;/h2&gt;

&lt;p&gt;The test site was a made-up notes app with three pages: a home page, a plans page and a contact page with a form. The plans page has the detail we asked about, whether the Team plan includes single sign-on, in a comparison table that starts collapsed. Every agent got the same prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Go to [the address] and find out what the Team plan costs per month and whether it includes SSO. Then open the contact page and tell me what it asks for. Don't submit anything.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Each run had its own address, so every request in the server log belongs to one known visitor. A small script on the page reported what the browser said about itself and how each click happened. It never read what anyone typed.&lt;/p&gt;

&lt;p&gt;We tested:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI agents that use a browser:&lt;/strong&gt; ChatGPT Work's built-in browser in the ChatGPT desktop app, Claude in Chrome, Claude Cowork, Claude computer use, Grok's desktop app, Perplexity's Comet browser and Manus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools developers give their agents:&lt;/strong&gt; Vercel's agent-browser, Playwright MCP and Chrome DevTools MCP (driven by Claude Code), the open-source Browser Use, a plain Playwright script, and the same script in a Browserbase cloud browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chatbots that fetch the page:&lt;/strong&gt; ChatGPT, Claude, Gemini, Perplexity, Mistral Le Chat, Meta AI, DeepSeek, Claude Code, Jina Reader and Browserbase's hosted agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;People:&lt;/strong&gt; a Mac trackpad, a mouse, Windows, two iPhones, an Android phone, and the macOS accessibility tools Voice Control and Dwell, which click for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some we couldn't test. Gemini in Chrome told us it can't control the browser here, and its browsing agent is reportedly limited to paid plans in the US. We didn't run OpenAI's computer-use model. Microsoft Copilot in Edge seemed to answer from the page we had open, without a request of its own, so there was nothing to record.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Browser&lt;/th&gt;
&lt;th&gt;Says it's automated&lt;/th&gt;
&lt;th&gt;Came from&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT Work&lt;/td&gt;
&lt;td&gt;Chrome 156 on our Mac&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Our connection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude in Chrome&lt;/td&gt;
&lt;td&gt;Our own Chrome&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Our connection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comet, in our browser&lt;/td&gt;
&lt;td&gt;Comet (Chrome 145) on our Mac&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Our connection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comet, in the cloud&lt;/td&gt;
&lt;td&gt;Windows Chrome&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;A data centre&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grok&lt;/td&gt;
&lt;td&gt;Linux Chrome without a graphics card&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Our connection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude computer use&lt;/td&gt;
&lt;td&gt;Chrome on a Linux desktop&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Where it runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Cowork&lt;/td&gt;
&lt;td&gt;Headless Chrome in the cloud&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Anthropic's servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manus&lt;/td&gt;
&lt;td&gt;Linux Chrome in the cloud&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes, and signs its requests&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A data centre&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser Use, Playwright MCP&lt;/td&gt;
&lt;td&gt;Chrome on our Mac&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Our connection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;agent-browser, DevTools MCP, Playwright script&lt;/td&gt;
&lt;td&gt;Chrome on our Mac&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;webdriver&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Our connection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same script in Browserbase&lt;/td&gt;
&lt;td&gt;Linux Chrome&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;A data centre&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT, Claude, Gemini, Perplexity, Mistral, Meta AI&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;In the user agent&lt;/td&gt;
&lt;td&gt;Their servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jina Reader, DeepSeek&lt;/td&gt;
&lt;td&gt;Chrome, Firefox&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;A data centre&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every agent that finished the task gave the answer our test page was built to give: €9 a month, no SSO, and the four fields of the contact form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Most agents look like a person in Chrome
&lt;/h2&gt;

&lt;p&gt;Bot detection usually starts with three questions: does the browser say it's automated (&lt;code&gt;navigator.webdriver&lt;/code&gt;), does the user agent name a bot, and does the visit come from a data centre. For most agents the answer was no to all three.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ChatGPT Work&lt;/strong&gt; ran a Chrome 156 inside the ChatGPT desktop app on our Mac. It reported &lt;code&gt;webdriver: false&lt;/code&gt;, a normal Chrome user agent, our timezone, our graphics chip and our home IP address. The only odd things were a narrow window, 319 pixels wide, and speed: a full visit took 11 to 21 seconds. Google Analytics showed these visits as ordinary sessions. So did our own tool. In 2025, &lt;a href="https://simonwillison.net/2025/Aug/4/chatgpt-agents-user-agent/" rel="noopener noreferrer"&gt;Simon Willison found&lt;/a&gt; that ChatGPT's agent signed its requests so sites could verify them. In our runs, ChatGPT Work's browser sent no signature at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude in Chrome&lt;/strong&gt; runs in your own browser, so it is your browser: same version, same extensions, same IP address. There's nothing to tell apart. &lt;strong&gt;Comet&lt;/strong&gt;, when it worked in our Comet window, was the same: a Chrome 145 user agent with no mention of Comet or Perplexity, from our connection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grok&lt;/strong&gt; was the strangest. The requests came from our IP address with our timezone, but the browser reported Linux on an x86 processor, 8 cores and no graphics card, on an Apple Silicon Mac. So it wasn't our browser, but to the website it came from our connection. We don't know where that browser runs. Its user agent was plain Linux Chrome, and nothing named Grok.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude computer use&lt;/strong&gt; sees the screen through screenshots and clicks through the operating system, the way a person's mouse does. We ran it like Anthropic's reference setup, in a Linux desktop in Docker. It also reported a plain Linux Chrome with &lt;code&gt;webdriver: false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Among the developer tools, &lt;strong&gt;Browser Use&lt;/strong&gt; and &lt;strong&gt;Playwright MCP&lt;/strong&gt; look like plain Chrome too. A plain Playwright script reports &lt;code&gt;webdriver: true&lt;/code&gt;; Playwright MCP switches that off. &lt;strong&gt;A Browserbase cloud browser&lt;/strong&gt; hides it as well, but comes from a data centre, so an IP check would catch it.&lt;/p&gt;

&lt;p&gt;A few say what they are. &lt;strong&gt;Claude Cowork&lt;/strong&gt;'s browser announced itself as &lt;code&gt;HeadlessChrome/153&lt;/code&gt; with &lt;code&gt;webdriver: true&lt;/code&gt;, from Anthropic's servers and a different IP address for almost every page. Vercel's &lt;strong&gt;agent-browser&lt;/strong&gt; and &lt;strong&gt;Chrome DevTools MCP&lt;/strong&gt; report &lt;code&gt;webdriver: true&lt;/code&gt; as well. And &lt;strong&gt;Manus&lt;/strong&gt; went furthest: it adds &lt;code&gt;Manus-User/1.0&lt;/code&gt; to its user agent and signs every page request with a &lt;code&gt;Signature-Agent: "https://api.manus.im"&lt;/code&gt; header and a cryptographic signature (the &lt;a href="https://developers.cloudflare.com/bots/reference/bot-verification/web-bot-auth/" rel="noopener noreferrer"&gt;Web Bot Auth&lt;/a&gt; standard, an &lt;a href="https://datatracker.ietf.org/doc/draft-meunier-web-bot-auth-architecture/" rel="noopener noreferrer"&gt;IETF draft&lt;/a&gt;), so a site can check that the request really came from Manus. It was the only agent in the test that did.&lt;/p&gt;

&lt;h2&gt;
  
  
  One agent, two browsers
&lt;/h2&gt;

&lt;p&gt;We gave Comet the same prompt six times. Three times it worked in our own Comet window. The other three times it used a browser in Perplexity's cloud: Windows, a server graphics card, a Los Angeles timezone and an Amazon data-centre address. When we started two tasks at once, the first ran in our browser and the second in the cloud.&lt;/p&gt;

&lt;p&gt;Asked why, Comet told us it uses the cloud browser for public pages and our own browser only when a site needs our login. That isn't what it did: it used our browser three times for the same public page. The two browsers also click differently, as the next sections show. So the same product can reach your site as a person on a home connection or as a server in a data centre.&lt;/p&gt;

&lt;h2&gt;
  
  
  Many came from our own connection
&lt;/h2&gt;

&lt;p&gt;ChatGPT Work, Claude in Chrome, Comet in our browser and Grok all reached the site from our home IP address, with our timezone, and so did every developer tool we ran on our Mac. That's worth knowing if you filter bots by IP address or by "data centre or not": for these agents, a website sees the same address as the person who sent them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agents that only read
&lt;/h2&gt;

&lt;p&gt;Asked the same question, Grok answered everything correctly, and even worked out the monthly price itself (€10.80), without a single click, scroll or mouse movement. It typed the addresses of the plans and contact pages directly and read the SSO answer out of the table that was still collapsed on screen. Manus did the same: three pages typed in 21 seconds, no click, no scroll, no movement.&lt;/p&gt;

&lt;p&gt;In analytics that's three page views, almost no time on each page, and no interaction: it looks like a visitor who left straight away. In fact it's a visitor who read everything. When we told Grok to click around, it did, and then it clicked like the other agents below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Most chatbots that fetch the page name themselves
&lt;/h2&gt;

&lt;p&gt;When you paste a link into a chatbot and ask for a summary, most don't open a browser. They fetch the page once from their own servers. No script runs, so an analytics tool that counts with a script in the page never sees the visit. Your server does, and most of these are easy to tell apart: they say who they are in the user agent, and most vendors document it (&lt;a href="https://developers.openai.com/api/docs/bots" rel="noopener noreferrer"&gt;OpenAI&lt;/a&gt;, &lt;a href="https://support.claude.com/en/articles/8896518-does-anthropic-crawl-data-from-the-web-and-how-can-site-owners-block-the-crawler" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt;, &lt;a href="https://docs.perplexity.ai/guides/bots" rel="noopener noreferrer"&gt;Perplexity&lt;/a&gt;, &lt;a href="https://developers.google.com/search/docs/crawling-indexing/google-common-crawlers" rel="noopener noreferrer"&gt;Google&lt;/a&gt;, &lt;a href="https://docs.mistral.ai/robots" rel="noopener noreferrer"&gt;Mistral&lt;/a&gt;, &lt;a href="https://developers.facebook.com/docs/sharing/webmasters/crawler" rel="noopener noreferrer"&gt;Meta&lt;/a&gt;).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;User agent&lt;/th&gt;
&lt;th&gt;Checked robots.txt first&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ChatGPT-User/1.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No. OpenAI's search crawler fetched it 33 seconds later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Claude-User/1.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes, one second before&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Google&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mistral&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;MistralAI-User/1.0&lt;/code&gt;, every page twice in the same second&lt;/td&gt;
&lt;td&gt;Yes, the first time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meta AI&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;meta-webindexer/1.1&lt;/code&gt;, the name of Meta's indexing crawler&lt;/td&gt;
&lt;td&gt;Yes, as &lt;code&gt;facebookexternalhit&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Claude-User (claude-code/…)&lt;/code&gt;, asks for Markdown first&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Perplexity&lt;/strong&gt; (&lt;code&gt;PerplexityBot/1.0&lt;/code&gt;) fetched robots.txt and then our start page instead of the address we gave it, twice. The start page of the test site is empty, so it told us it couldn't fetch the page. Another time it said the page "returned no readable content" without sending us a single request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browserbase's hosted agent&lt;/strong&gt;, asked to do the full task, didn't use a browser either. It fetched the pages with headers that imitate Chrome 146 on macOS, ten versions behind everything else in the test, and requested two pages in the same millisecond from two different IP addresses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two "fetchers" run your scripts
&lt;/h2&gt;

&lt;p&gt;Two of the chatbot tools didn't just fetch the page. They opened it in a real browser, so our script ran, and an analytics tool would count a visitor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Jina Reader&lt;/strong&gt;, a service agents use to turn a page into text, loaded it in a headless Chrome in a data centre. Its user agent said plain Linux Chrome 156; only a second version header, which said 155, showed that the user agent had been changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DeepSeek&lt;/strong&gt; opened the page in Firefox 149 on Linux, with Chinese as its language and a Hong Kong timezone, from a Huawei Cloud address in Hong Kong, and was gone within three seconds. Nothing it sent named DeepSeek or a bot.&lt;/p&gt;

&lt;p&gt;In analytics, both are a visitor who loaded one page, did nothing and left.&lt;/p&gt;

&lt;h2&gt;
  
  
  What gave the clicking agents away
&lt;/h2&gt;

&lt;p&gt;Once an agent clicks, we could compare its clicks with ours. The first thing we measured, how long the button is held, worked for most agents and failed for two:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Button held&lt;/th&gt;
&lt;th&gt;Last pointer move before the click&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Most agents&lt;/td&gt;
&lt;td&gt;0 to 13 ms&lt;/td&gt;
&lt;td&gt;1 to 63 ms before&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser Use&lt;/td&gt;
&lt;td&gt;82 to 90 ms&lt;/td&gt;
&lt;td&gt;134 to 142 ms before&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comet in our browser&lt;/td&gt;
&lt;td&gt;265 to 453 ms&lt;/td&gt;
&lt;td&gt;531 to 755 ms before&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;People with a mouse or trackpad&lt;/td&gt;
&lt;td&gt;29 to 266 ms&lt;/td&gt;
&lt;td&gt;21 ms to 2 seconds before&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Browser Use and Comet in our browser press, wait and release like a person. Comet in the cloud didn't: it held the button for 10 to 13 ms.&lt;/p&gt;

&lt;p&gt;What gave every clicking agent away was how the pointer got to the button. A person moves a hand, so the pointer arrives in many small steps; the last step before a click was 0 to 2 pixels for every one of our clicks. An agent doesn't move a hand. Before each click, the pointer appeared on the target in a single jump of 200 to 840 pixels, or it appeared there for the first time on the page, and it barely moved otherwise:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Agents&lt;/th&gt;
&lt;th&gt;People with a mouse or trackpad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The last move before a click&lt;/td&gt;
&lt;td&gt;One jump onto the target&lt;/td&gt;
&lt;td&gt;A small step, 0 to 2 pixels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Other pointer movement on the page&lt;/td&gt;
&lt;td&gt;0 to 2 movements&lt;/td&gt;
&lt;td&gt;11 to 642&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This held for all 13 kinds of clicking agent, in every run: those that click through Chrome's remote-control interface, Claude computer use, which clicks through the operating system, and the two that wait like a person.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signals that didn't hold up
&lt;/h2&gt;

&lt;p&gt;Some things that sound like good bot signals failed on our own data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How long the button is held.&lt;/strong&gt; Most agents click in a few milliseconds, but Browser Use and Comet hold the button as long as people do, and the accessibility tools below click faster than any agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clicking dead in the centre of a button.&lt;/strong&gt; ChatGPT Work did, usually within a pixel. But Claude in Chrome and Claude computer use aim at the text they see in a screenshot, and missed the centre of a wide element by more than 230 pixels. And one of us hit a link within 2 pixels of its centre with a trackpad.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A big jump of the pointer.&lt;/strong&gt; A fast swipe on a trackpad moved the pointer 701 pixels in one step. Only a jump that lands right on what is clicked next is suspicious.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pressure.&lt;/strong&gt; ChatGPT Work and Comet reported a pressure of 0 when they clicked, where a mouse reports 0.5. The Playwright tools reported 0.5.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How it navigates.&lt;/strong&gt; ChatGPT Work typed the addresses in some runs and clicked the links like a person in others, with the same browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The console trick for spotting remote control.&lt;/strong&gt; Tools that drive Chrome through its remote-control interface often listen to the browser console. Bot-detection scripts exploit that: the page logs an error object that notices when something reads it, and a listening tool does. It noticed nothing in any browser we tested, Playwright included, so it doesn't work in the Chrome versions we saw (136 to 156).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Accessibility tools look more like bots than the bots
&lt;/h2&gt;

&lt;p&gt;macOS Voice Control lets you click by saying what to click. Every one of its clicks landed exactly in the centre, with the button "held" for less than a millisecond, a pressure of 0, and no mouse movement at all. On any single signal, it looked more automated than ChatGPT Work.&lt;/p&gt;

&lt;p&gt;Dwell clicks for you when you rest the pointer on something. Its clicks were also held for 0 to 3 milliseconds.&lt;/p&gt;

&lt;p&gt;Both differ from the agents in how the pointer moves: Voice Control never moves it before clicking, and with Dwell the pointer rests on the target for about three seconds, moved there by a hand in small steps. A detection rule that relies on click timing would label people who can't use a mouse as bots. The movement tells them apart, but that's the only thing that does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for your analytics
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI agents are already in your numbers as people.&lt;/strong&gt; Short visits that read everything and click nothing, and visits that go straight to pricing, may be agents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most chatbot fetches are counted on the server.&lt;/strong&gt; They never reach a script in the page. To see how often someone asks ChatGPT, Claude or Mistral about your pages, count &lt;code&gt;ChatGPT-User&lt;/code&gt;, &lt;code&gt;Claude-User&lt;/code&gt; and &lt;code&gt;MistralAI-User&lt;/code&gt; requests on your server. Some, like DeepSeek's, show up in analytics as a one-page visit instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't count on IP addresses.&lt;/strong&gt; Most of the agents we ran came from our home connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be careful with "bot" filters.&lt;/strong&gt; If a filter catches instant, centred clicks, it also catches people using Voice Control.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What we're changing in Privatrak
&lt;/h2&gt;

&lt;p&gt;Privatrak already &lt;a href="https://privatrak.com/docs/sessions#automated-visits" rel="noopener noreferrer"&gt;marks visits as automated&lt;/a&gt; when the browser says so (&lt;code&gt;webdriver&lt;/code&gt;) or the user agent names a headless browser or an agent like Manus. In this test that caught the Playwright script, agent-browser, DevTools MCP, Claude Cowork and Manus. ChatGPT Work, Claude in Chrome, Comet, Grok, computer use, Browser Use, Playwright MCP and the Browserbase browser were counted as people.&lt;/p&gt;

&lt;p&gt;We're adding a check in the browser for the movement pattern above: a visit is only marked when every mouse click on the page came after one jump of the pointer onto the target, with almost no other movement, so Voice Control and Dwell users stay people. Replayed on our recordings, that marks every clicking agent and none of us. The browser only sends "automated" or not, never the movements themselves. A marked visit is still counted, and you decide whether to leave automated visits out of your numbers. Agents that only read, and fetchers that run your scripts, will still look like people.&lt;/p&gt;

&lt;p&gt;For chatbots that only fetch, we're working on counting them from your server: a small addition for your site sends only the requests whose user agent names a known chatbot or crawler, and Privatrak shows which pages ChatGPT, Claude and Perplexity fetched for someone, separate from your visitors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Also seen
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visitors before anyone had the link.&lt;/strong&gt; Within minutes of the site's certificate being issued, scanners that watch the public certificate logs visited it, some claiming to be Chrome. Anthropic's crawler ClaudeBot came by twice the same morning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link previews.&lt;/strong&gt; When people opened the test links we'd sent them, WhatsApp and Facebook's preview fetchers got there first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google came back on its own.&lt;/strong&gt; Right after an agent's visit fired the Google Analytics tag, a Google fetcher (&lt;code&gt;GoogleOther&lt;/code&gt;) loaded the same page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Every user agent we saw
&lt;/h2&gt;

&lt;p&gt;For reference, here is the user agent each one sent, copied from our server log. Only the rows marked "Yes" ran the page's scripts, so only those can reach an analytics tool that counts with a script. The agents that drive a browser, ChatGPT's agent included, send an ordinary Chrome user agent; only Claude Cowork, agent-browser and Manus give themselves away in it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;User agent&lt;/th&gt;
&lt;th&gt;Runs your scripts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT Work (ChatGPT's agent)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude in Chrome (your own browser)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comet, in your browser&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comet, in Perplexity's cloud&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grok&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude computer use&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Cowork&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.8010.12 Safari/537.36&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manus&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36; Manus-User/1.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser Use&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright MCP, DevTools MCP&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vercel agent-browser&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.0.0 Safari/537.36&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jina Reader&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT, reading a link&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; ChatGPT-User/1.0; +https://openai.com/bot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude, reading a link&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Claude-User/1.0; +claude-user@anthropic.com)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Claude-User (claude-code/2.1.309; +https://support.anthropic.com/)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Google&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perplexity&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; PerplexityBot/1.0; +https://perplexity.ai/perplexitybot)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mistral Le Chat&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; MistralAI-User/1.0; +https://docs.mistral.ai/robots)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meta AI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;meta-webindexer/1.1 (+https://developers.facebook.com/docs/sharing/webmasters/crawler)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How we tested
&lt;/h2&gt;

&lt;p&gt;The test site ran on our own server, with three static pages. The server log recorded every request with all its headers. A script on each page reported what the browser said about itself (user agent, &lt;code&gt;webdriver&lt;/code&gt;, window size, timezone, graphics) and how each click happened: how long the button was held, how the pointer moved before it, and how many movements there were in total. It never read what was typed into the form.&lt;/p&gt;

&lt;p&gt;Each agent ran in its own way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ChatGPT Work, Grok, Cowork, Comet, Manus and the chatbots were run by hand in their own apps or websites, on free plans where possible.&lt;/li&gt;
&lt;li&gt;Claude in Chrome ran in our own Chrome.&lt;/li&gt;
&lt;li&gt;Claude computer use ran on Claude Opus 5.5 with the computer-use toolset, in a Linux desktop in Docker that clicks with &lt;code&gt;xdotool&lt;/code&gt;, like Anthropic's reference setup.&lt;/li&gt;
&lt;li&gt;agent-browser, Playwright MCP and DevTools MCP were driven by Claude Code on our Mac; Browser Use ran on our Mac with Claude Sonnet 4.6 and its default settings.&lt;/li&gt;
&lt;li&gt;The Playwright script ran in Chrome on our Mac and in a Browserbase cloud browser. Jina Reader was called without an account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Limits to keep in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's one day and one site: one to six runs per agent, and ten runs by people, most of them by us.&lt;/li&gt;
&lt;li&gt;Agents change quickly. A new version can click differently tomorrow.&lt;/li&gt;
&lt;li&gt;An agent that moves the pointer along a path in small steps would look like a person. None of the agents we tested did.&lt;/li&gt;
&lt;li&gt;Where Grok's browser runs, and what makes Comet choose a browser, is what we observed, not something we could confirm.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>agents</category>
      <category>analytics</category>
    </item>
    <item>
      <title>What it took to get our MCP server into Claude's connector directory</title>
      <dc:creator>Quentin Wendegass</dc:creator>
      <pubDate>Thu, 22 Oct 2026 09:52:18 +0000</pubDate>
      <link>https://dev.to/quentin_wendegass/what-it-took-to-get-our-mcp-server-into-claudes-connector-directory-28c0</link>
      <guid>https://dev.to/quentin_wendegass/what-it-took-to-get-our-mcp-server-into-claudes-connector-directory-28c0</guid>
      <description>&lt;p&gt;&lt;em&gt;An automated review approved Privatrak for Anthropic's connector directory about an hour after we submitted it. Here's what the submission involved, what tripped us up, and what we'd do again.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Our MCP server is now listed in Claude's connector directory. An automated review approved it in about an hour. We build &lt;a href="https://privatrak.com" rel="noopener noreferrer"&gt;Privatrak&lt;/a&gt;, a cookieless analytics tool, and MCP (Model Context Protocol) is the standard way to plug a tool like it into an AI assistant. Once connected, Claude can answer your analytics questions in plain language or set up the tracking for you, such as funnels, dashboards and which clicks count as which feature.&lt;/p&gt;

&lt;p&gt;This is what getting it into the directory actually involved, in the order we ran into it. Most of it applies to any remote MCP server, not just analytics.&lt;/p&gt;

&lt;h2&gt;
  
  
  An AI reviewed it, not a person
&lt;/h2&gt;

&lt;p&gt;The approval email said the submission "was reviewed and approved as a community connector by an automated review of the listing." Anthropic's &lt;a href="https://claude.com/docs/connectors/verification" rel="noopener noreferrer"&gt;verification page&lt;/a&gt; explains what that means. Every submission is scanned automatically for policy compliance and listed as a &lt;strong&gt;Community&lt;/strong&gt; connector by default. Listings that look highly useful to Claude users can be escalated to &lt;strong&gt;Verified&lt;/strong&gt; review, which is slower and where "reviewers run a functional test of each tool." That escalation is decided automatically too, and there's nothing to apply for.&lt;/p&gt;

&lt;p&gt;The label changes less than you might think. Once connected, a Community connector works exactly like a Verified one, it shows up in search, and Claude can suggest it in a chat when it fits. The difference is the label, and a reminder before someone connects that it "hasn't been reviewed in depth." You can't apply for Verified. Anthropic picks listings that look "highly useful to Claude users", and its developer portal ranks every listing by how many people use it. So our bet is that usage is what counts.&lt;/p&gt;

&lt;p&gt;That changes how you write the submission. The first thing to read your tool descriptions is an automated check against Anthropic's policy, and the strictest rules there are about text that steers Claude into doing something the user didn't ask for (more on this below). The test account and test plan the form asks for may be looked at too, but the only review that says it runs every tool is the Verified one. So they have to work without you there to explain them.&lt;/p&gt;

&lt;p&gt;A few months ago this looked very different. Submissions went through a Google Form and a manual queue, and developers &lt;a href="https://www.reddit.com/r/mcp/comments/1tgubv7/how_long_does_claude_mcp_server_approval_take/" rel="noopener noreferrer"&gt;reported waiting months&lt;/a&gt; without hearing back. Anthropic has since moved submissions to a portal with an automated check, and some submissions still get a review by a person, with times that depend on the queue. We don't know why ours went through in an hour. Our guess is that a clean submission, with OAuth, honest tool labels and a full test plan, gives the automated check nothing to send on to a person.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we needed OAuth sign-in
&lt;/h2&gt;

&lt;p&gt;Claude on the web, the desktop app and the mobile apps connect to a remote server &lt;strong&gt;by URL&lt;/strong&gt;. For most users there is nowhere to paste an API key (at the time of writing, a beta that accepts one is rolling out to some customers), so a server that only accepts keys locks most people out. And the directory requires OAuth anyway.&lt;/p&gt;

&lt;p&gt;So we built sign-in with OAuth, the same flow as any "Sign in with..." button. Claude sends you to our login page, you approve access, and Claude gets a token for our server. Here's what we learned from making it work with the apps people actually use.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude introduces itself with a web address.&lt;/strong&gt; Claude doesn't register with your server in advance. Its ID is a URL that points to a small file describing Claude (a &lt;em&gt;Client ID Metadata Document&lt;/em&gt;). You fetch that file and check that the address you send people back to after sign-in is listed in it. Cursor and Codex instead register themselves on the fly (&lt;em&gt;dynamic client registration&lt;/em&gt;), so you need both if you want to support them too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code picks a random port.&lt;/strong&gt; After sign-in, it expects to be sent back to &lt;code&gt;http://localhost/callback&lt;/code&gt;, on whatever port it happened to open. The OAuth rules for desktop apps (RFC 8252) say to ignore the port for localhost addresses, and you have to, or Claude Code's sign-in fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't trust the name an app gives itself.&lt;/strong&gt; An app that registers on the fly can call itself "Claude". Our approval screen only shows a name as checked when the file describing the app and the address it returns to are on the same domain, such as claude.ai. Otherwise it shows the domain next to the name and marks it as not checked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The approval screen is where people decide.&lt;/strong&gt; Ours asks which projects the connection may see, and whether it may only read or also make changes. Those two choices match the two kinds of API key we already had, so an AI connection can never do more than a key could.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the submission form, all of this was one line. Behind that line was the biggest piece of work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checkbox that made us stop
&lt;/h2&gt;

&lt;p&gt;Every tool comes with a short description that tells Claude what it does and when to use it. Claude reads these descriptions almost like part of its instructions, which is why the directory checks them closely. The last step of the form has seven boxes to tick. Six were simple. This one wasn't.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tool descriptions contain no instructions about model behavior, other tools, or external instruction sources, and no hidden or encoded text.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read literally, ours break it twice. First, some tool descriptions mention other tools, such as "use the ID you got from &lt;code&gt;start_setup_run&lt;/code&gt;". Second, the general instructions our server gives Claude tell it how to behave, such as "quote counts with every percentage", so that it says "2 of 4 visitors" instead of just "50%". We wanted to keep both, because they make the tools work better.&lt;/p&gt;

&lt;p&gt;So we read the &lt;a href="https://support.claude.com/en/articles/13145358-anthropic-software-directory-policy" rel="noopener noreferrer"&gt;policy&lt;/a&gt; the checkbox summarises. Its actual rules are narrower.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Descriptions must say precisely what a tool does and when to use it, and match what it does.&lt;/li&gt;
&lt;li&gt;A server must not push Claude toward calling &lt;strong&gt;other&lt;/strong&gt; software the user didn't ask for, or interfere with other servers' tools.&lt;/li&gt;
&lt;li&gt;It must not tell Claude to fetch behavioural instructions from external sources.&lt;/li&gt;
&lt;li&gt;No hidden, obfuscated or encoded instructions. In the policy's words, "all behavioral guidance must be human-readable and clearly presented."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anthropic's &lt;a href="https://claude.com/docs/connectors/building/review-criteria" rel="noopener noreferrer"&gt;pre-submission checklist&lt;/a&gt; is more blunt. "Describe what the tool does, and don't tell Claude how to behave." But the most useful part is the last rejection reason on its list, descriptions that "tell Claude to behave in ways unrelated to the tool's function."&lt;/p&gt;

&lt;p&gt;So guidance about what your own tools do is allowed. What isn't allowed is guidance that pulls Claude away from what the user asked, toward someone else's tools, or that is hidden. Referring to your own tools, telling the model how to read your own numbers, and pointing it at your own documentation as reference material are all fine, as long as it's in plain sight.&lt;/p&gt;

&lt;p&gt;We ticked the box and explained it in the notes field instead of hoping nobody would notice. We listed which tools our descriptions refer to, why the usage guidance is there, and that our two documentation tools only read our own public documentation, from a fixed list of pages.&lt;/p&gt;

&lt;p&gt;The automated review approved it, so that reading seems to hold. If you're building a server, write descriptions as if a reviewer will read every one, because one will, and the first one is automated. Name only your own tools, keep all guidance visible, and if a sentence would read as manipulation out of context, rewrite it.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Claude asks before using a tool
&lt;/h2&gt;

&lt;p&gt;The directory requires three labels on every tool, a &lt;code&gt;title&lt;/code&gt; and two hints called &lt;code&gt;readOnlyHint&lt;/code&gt; and &lt;code&gt;destructiveHint&lt;/code&gt;. They decide when Claude asks for permission. According to the checklist, read-only tools "can run without per-call confirmation, and destructive tools always prompt." Get them wrong and people either have to confirm every harmless lookup, or aren't asked before something is deleted.&lt;/p&gt;

&lt;p&gt;Why does the directory check them so closely? The &lt;a href="https://modelcontextprotocol.io/specification/2025-11-25/server/tools" rel="noopener noreferrer"&gt;MCP specification&lt;/a&gt; says clients must treat these hints as untrusted unless they come from a trusted server, and yet Claude uses them to decide when to ask. Our guess is that the review is what earns a server that trust.&lt;/p&gt;

&lt;p&gt;Watch out for one thing. ** if you leave &lt;code&gt;destructiveHint&lt;/code&gt; unset on a write tool, the protocol assumes it is destructive. ** That is a safe default, but it describes a "create dashboard" tool wrongly. So we set both hints explicitly on every write tool.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating something is not destructive, and running it twice gives you two of them.&lt;/li&gt;
&lt;li&gt;Updating, renaming, deleting and undoing are destructive, and running them twice does no more than running them once.&lt;/li&gt;
&lt;li&gt;Some tools need a closer look. In Privatrak, a &lt;em&gt;click problem&lt;/em&gt; is something people click that doesn't work. Marking one as fixed is destructive, and doing it twice is not the same as once, because it moves the date from which the problem counts as happening again. Marking it as ignored twice really changes nothing the second time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setting up a test account
&lt;/h2&gt;

&lt;p&gt;Test credentials are required, and the checklist says they "must be for a fully populated account." For analytics that matters more than for most tools, because an empty project answers every question with zero.&lt;/p&gt;

&lt;p&gt;This part was easy for us, because we already run a &lt;a href="https://demo.privatrak.com" rel="noopener noreferrer"&gt;public demo&lt;/a&gt; with a sample shop and its traffic. We created a dedicated account on the Pro plan, with a project holding a month of the same kind of shop traffic, with visitors from search and campaigns, product pages, a checkout funnel, user journeys and a few broken buttons for the click-problem tools. One detail almost tripped us up. Our funnels only count from the day they were created, so a funnel added on the day of submission would have shown nothing. Everything in the review project had to look as if it had been there for the whole month.&lt;/p&gt;

&lt;p&gt;Give the account a plain email and password. One team &lt;a href="https://dev.to/eugeniya_ivanova_4a58eadc/my-mcp-integration-got-rejected-almost-nothing-in-the-server-had-to-change-npb"&gt;was rejected&lt;/a&gt; because their review account used Google sign-in with a code sent to a phone, which the reviewer couldn't get past. Their server was fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing a test plan
&lt;/h2&gt;

&lt;p&gt;The form asks how to test the server. Our first draft was a handful of example prompts, and they were useless. Five nice prompts exercise maybe a dozen tools. They show the server works, not that every tool does.&lt;/p&gt;

&lt;p&gt;So we wrote a plan of 15 prompts that together call all 44 tools, each with the tools it triggers and the answer we got, so the reviewer can check rather than guess. Here's step 4.&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="p"&gt;4.&lt;/span&gt; "Which properties can I filter by, and which page paths exist? Then chart
   page views and clicks per day from 2026-10-13 to 2026-10-19 and compare
   them with the week before."
   Tools: list_property_keys, list_property_values, run_insights_query.
   Expect: 162 page views (+25.6%), 53 clicks (+23.3%).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fixed dates matter here. "Last week" means something different every day the reviewer opens the plan.&lt;/p&gt;

&lt;p&gt;The harder part is the write tools. A reviewer has to create, change and delete things to test them, on an account other reviewers may use after them. If the plan leaves a mess, the second run fails.&lt;/p&gt;

&lt;p&gt;This is where a design decision from months earlier paid off. In Privatrak, every change an assistant makes belongs to a &lt;a href="https://privatrak.com/docs/ai-setup#undo" rel="noopener noreferrer"&gt;&lt;strong&gt;setup run&lt;/strong&gt;&lt;/a&gt;, and a setup run can be undone in one step. Undo puts back exactly what the run changed. If someone has changed the same thing since, it skips that item and says so instead of overwriting their work. The undo tool first shows what it would do, and only then does it.&lt;/p&gt;

&lt;p&gt;So the last step of the plan cleans up after everything else.&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="p"&gt;15.&lt;/span&gt; "List the setup runs and show what 'Review test' changed. Then undo it:
    dry run first, then for real."
    Tools: list_setup_runs, get_setup_run, undo_setup_run.
    Expect: 15 changes listed; the undo reverts all 15 with nothing skipped.
    The project is back to its original state.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We ran the whole plan ourselves through Claude, signed in as the review account, before submitting. The form's Test &amp;amp; launch step asks you to confirm you tested every tool, and this was how we could say yes. Every tool passed, and the undo left the project exactly as it was. The plan can be run as often as anyone likes.&lt;/p&gt;

&lt;p&gt;We don't know whether the automated review looked at it. We'd write it again anyway. It's what a Verified reviewer would follow, and it was the only way we knew that all 44 tools work through Claude itself, not just in our own tests.&lt;/p&gt;

&lt;p&gt;We built setup runs for customers, not for reviewers. Letting an AI change your analytics setup is only reasonable if you can take it all back. But if your server has write tools and no way to undo them, think about how a reviewer is supposed to test them. And then about how your users are supposed to trust them. We &lt;a href="https://privatrak.com/blog/mcp-server-undo" rel="noopener noreferrer"&gt;wrote up how our undo works in a separate article&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small things that cost us an afternoon
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code took 25 seconds to connect.&lt;/strong&gt; The cause was one default setting. The Go SDK we use tells every client that our list of tools may change while it's connected (&lt;code&gt;listChanged: true&lt;/code&gt;). Claude Code waited for an update that never came, then gave up after about 25 seconds. Our tool list never changes, so we turned the setting off (&lt;code&gt;listChanged: false&lt;/code&gt;) and Claude Code now connects right away.&lt;/li&gt;
&lt;li&gt;** Claude's desktop app won't send your API key in the usual place. ** You can add your own connector there with extra request headers, but not the &lt;code&gt;Authorization&lt;/code&gt; header, where API keys normally go. So we also accept the key in an &lt;code&gt;X-API-Key&lt;/code&gt; header, which our API already used. People who prefer a key over signing in can still connect.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What about the official MCP Registry?
&lt;/h2&gt;

&lt;p&gt;One thing confused us at the start. Besides Anthropic's directory there is the &lt;strong&gt;official MCP Registry&lt;/strong&gt; (registry.modelcontextprotocol.io), a public list of MCP servers run by the people behind the protocol. The two are separate. The registry wants proof that you own the name you publish under, and Anthropic's form never mentions the registry at all.&lt;/p&gt;

&lt;p&gt;We did both. To publish as &lt;code&gt;com.privatrak/analytics&lt;/code&gt; in the registry, we had to prove we control &lt;code&gt;privatrak.com&lt;/code&gt;. You do that by adding a DNS record with a public key to your domain, then signing in with the matching private key using the registry's &lt;code&gt;mcp-publisher&lt;/code&gt; tool. The one snag was that the tool needs the private key as hex, and the command in the instructions to get it didn't work with our version of OpenSSL. This one works with any version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl pkey &lt;span class="nt"&gt;-in&lt;/span&gt; key.pem &lt;span class="nt"&gt;-outform&lt;/span&gt; DER | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 32 | xxd &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The registry entry itself is a small &lt;code&gt;server.json&lt;/code&gt; with a name, a title, a one-line description, your website, an icon and your server's address. Write the description for someone deciding whether to install you, not for search engines. Ours says "Ask your website analytics in plain language and set up funnels, features and dashboards."&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist for your submission
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Anthropic's &lt;a href="https://claude.com/docs/connectors/building/review-criteria" rel="noopener noreferrer"&gt;pre-submission checklist&lt;/a&gt; open next to the form. It spells out what the policy only implies.&lt;/li&gt;
&lt;li&gt;Sign-in with OAuth that works for Claude (Client ID Metadata Documents) and for Cursor and Codex (dynamic registration), and ignores the port on localhost addresses.&lt;/li&gt;
&lt;li&gt;The current MCP transport, Streamable HTTP. If your tool list never changes, set &lt;code&gt;listChanged: false&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;readOnlyHint&lt;/code&gt; and &lt;code&gt;destructiveHint&lt;/code&gt; on every tool, set deliberately.&lt;/li&gt;
&lt;li&gt;Tool descriptions that refer only to your own tools, with all guidance in plain sight. Explain anything borderline in the notes.&lt;/li&gt;
&lt;li&gt;A test account with realistic data, and dates in the test plan that don't move.&lt;/li&gt;
&lt;li&gt;A test plan that calls every tool, with expected answers, and cleans up after itself.&lt;/li&gt;
&lt;li&gt;Optionally, an entry in the official MCP Registry, via a DNS record on your domain.&lt;/li&gt;
&lt;li&gt;A public privacy policy, linked in the submission.&lt;/li&gt;
&lt;li&gt;A review account with a plain email and password, no Google sign-in or phone codes.&lt;/li&gt;
&lt;li&gt;Patience with later changes. A new description or new screenshots are reviewed by a person, and that can take weeks while your listing stays live.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to see what this looks like from the user's side, Privatrak is &lt;a href="https://claude.ai/directory/privatrak" rel="noopener noreferrer"&gt;in the directory&lt;/a&gt; now as a Community connector, and &lt;a href="https://privatrak.com/docs/ai-assistant" rel="noopener noreferrer"&gt;our docs&lt;/a&gt; show how to connect it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>claude</category>
      <category>mcp</category>
    </item>
    <item>
      <title>We gave our MCP server an undo button</title>
      <dc:creator>Quentin Wendegass</dc:creator>
      <pubDate>Wed, 21 Oct 2026 03:56:08 +0000</pubDate>
      <link>https://dev.to/quentin_wendegass/we-gave-our-mcp-server-an-undo-button-2mah</link>
      <guid>https://dev.to/quentin_wendegass/we-gave-our-mcp-server-an-undo-button-2mah</guid>
      <description>&lt;p&gt;&lt;em&gt;Letting an AI agent change your product's settings is only reasonable if you can take it all back. So every change our MCP server makes goes into a run that can be undone in one step. This is how it works, and the decisions that turned out to be hard.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In July 2025, Replit's AI agent deleted a company's production database during a code freeze, then told the user the data couldn't be recovered. It could. Replit had a rollback, and the user restored everything from a checkpoint. The agent just &lt;a href="https://incidentdatabase.ai/cite/1152/" rel="noopener noreferrer"&gt;didn't know the feature existed&lt;/a&gt;. Two lessons from that story shaped what we built: the agent has to know that undo exists, and people have to be able to undo without asking the agent.&lt;/p&gt;

&lt;p&gt;We build &lt;a href="https://privatrak.com" rel="noopener noreferrer"&gt;Privatrak&lt;/a&gt;, a cookieless analytics tool. Its MCP server lets Claude, Cursor or Codex do more than read your analytics. They can set them up: feature rules that name what people click, funnels, dashboards, and marking click problems as fixed. 17 of its 44 tools change something.&lt;/p&gt;

&lt;p&gt;Most MCP servers with write access rely on the client asking before each call. We wanted one more thing: after an agent has made twenty changes, you see all of them in one list and undo them in one step, without losing what you or a colleague changed in between.&lt;/p&gt;

&lt;h2&gt;
  
  
  Asking first isn't enough
&lt;/h2&gt;

&lt;p&gt;Claude asks before it runs a tool marked destructive. That's good, and we set our tool annotations carefully for it. But it depends on the server labelling its tools honestly, which is why the MCP specification tells clients to treat those labels as untrusted unless the server is trusted. And it protects one call at a time, while an agent's work isn't one call.&lt;/p&gt;

&lt;p&gt;Setting up a project takes a dozen or twenty writes: rules, a funnel built on those rules, a dashboard whose widgets query them. Nobody reads the fifteenth confirmation as carefully as the first. Security people call this confirmation fatigue, and OWASP's list of risks for AI agents includes it. And the mistakes in a setup rarely show in a single call. They show later, when a dashboard counts the wrong thing.&lt;/p&gt;

&lt;p&gt;What you need then isn't another confirmation. You need to see everything the agent did as one unit, and to take that unit back. Developers have that for code in version control. Product settings usually have nothing like it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Undo has to live in the server
&lt;/h2&gt;

&lt;p&gt;Coding agents already have undo. Claude Code and Cursor keep checkpoints you can go back to. But both only cover files the agent edited. &lt;a href="https://code.claude.com/docs/en/checkpointing" rel="noopener noreferrer"&gt;Claude Code's docs&lt;/a&gt; say changes made through commands aren't tracked, and &lt;a href="https://docs.cursor.com/agent/chat/checkpoints" rel="noopener noreferrer"&gt;Cursor's&lt;/a&gt; say restoring a checkpoint doesn't undo a database migration the agent ran. Once an agent calls an API, the client can't take it back. Only the service behind the API can.&lt;/p&gt;

&lt;p&gt;MCP doesn't help here either. The &lt;a href="https://modelcontextprotocol.io/specification/2025-11-25/server/tools" rel="noopener noreferrer"&gt;specification&lt;/a&gt; asks clients to keep a person in the loop and to confirm tool calls, but it has nothing for transactions or rollback. So if you want undo for what an agent does in your product, you build it into your MCP server. That's what the rest of this article is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  One task, one run
&lt;/h2&gt;

&lt;p&gt;The unit of undo is a &lt;strong&gt;setup run&lt;/strong&gt;. An agent starts one at the beginning of a task with &lt;code&gt;start_setup_run&lt;/code&gt; and a label such as "Checkout tracking", then passes its id on every write. The run answers the question "what did the assistant do?"&lt;/p&gt;

&lt;p&gt;Two rules make it reliable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nothing happens outside a run.&lt;/strong&gt; A write tool called without a run id starts a run itself and returns its id. Even an agent that ignores the instructions leaves a trail that can be undone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runs that changed nothing don't clutter the list.&lt;/strong&gt; If the API rejects a write, the tool deletes the run it had just started for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the API, a write joins a run through a header, &lt;code&gt;X-Setup-Run-ID&lt;/code&gt;, not a body field. DELETE has no body, and all fourteen write operations should join the same way. A write without the header takes exactly the code path it always took, so adding runs changed nothing that already worked.&lt;/p&gt;

&lt;p&gt;The runs also show up in the dashboard under Settings, as &lt;strong&gt;AI assistant changes&lt;/strong&gt;. The person who connected the assistant doesn't need the assistant to undo what it did.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdnlzi3u7fnn9i7rth9y4.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdnlzi3u7fnn9i7rth9y4.png" alt="The AI assistant changes page in Privatrak's settings, listing the run " width="800" height="263"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Record before and after, in the same transaction
&lt;/h2&gt;

&lt;p&gt;Every write that belongs to a run is saved together with a record of what it changed: the state before and the state after. Both are saved in one database transaction, so either both happen or neither does. The run is locked meanwhile, so nobody can undo it halfway through a write.&lt;/p&gt;

&lt;p&gt;The recorded state is the whole definition: a rule's name and conditions, a funnel's steps, a dashboard with its widgets. Everything undo needs to rebuild it. The database enforces the shape: a creation has no "before", a deletion has no "after".&lt;/p&gt;

&lt;p&gt;One detail cost us more time than the rest. Undo later compares these records with what is in the database now. If the same thing can be saved in two ways that mean the same, the comparison fails, and every undo looks like a conflict. So every write saves its input in exactly one form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Undo walks backwards
&lt;/h2&gt;

&lt;p&gt;Undo replays the run's changes newest first, in one transaction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Created&lt;/strong&gt; becomes deleted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Updated&lt;/strong&gt; gets back the fields the run changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deleted&lt;/strong&gt; is recreated, with its original id.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Newest first matters. An agent that created a rule and then renamed it has to unwind the rename before the creation. And if the run created something and later deleted it itself, there is nothing to do for it at all.&lt;/p&gt;

&lt;p&gt;The original id matters more than it sounds. Dashboards, funnels and links refer to things by id. A recreated rule even gets its original creation time back, because the order of rules decides which name the journey view shows for a click. Undo should leave the project reading exactly as before, not just something equivalent.&lt;/p&gt;

&lt;p&gt;Afterwards the run is marked as undone and its report is stored. Asking again returns the same report, and a write that tries to join an undone run is refused.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F29ebsofwen5xxh72fp25.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F29ebsofwen5xxh72fp25.png" alt="Left, the agent's four changes from oldest to newest. Right, what undo does with each, from the newest up: the dashboard is deleted, the renamed funnel is skipped, the rule's old name is put back, the rule is deleted" width="800" height="420"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The undo starts with the agent's last change and works back to its first. The funnel you renamed stays, and everything else goes.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Never overwrite someone else's work
&lt;/h2&gt;

&lt;p&gt;This was the hardest decision. Between the agent's run and the undo, people keep working. Someone renames a widget the agent made, or fixes a rule's condition. Undo must not destroy that.&lt;/p&gt;

&lt;p&gt;So every change is checked before it is reverted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Undoing a creation requires the thing to still be exactly what the run created.&lt;/li&gt;
&lt;li&gt;Undoing an update requires every field the run changed to still hold the run's value. Fields the run didn't touch may have changed since. They are none of its business.&lt;/li&gt;
&lt;li&gt;A dashboard the run created that now holds someone else's widgets is not deleted.&lt;/li&gt;
&lt;li&gt;Recreating something that would go over the plan's limits is skipped, not forced through.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a check fails, that one change is skipped and reported with the reason, and the rest of the run is still undone. We considered stopping the whole undo instead. But then one widget someone renamed would keep an agent's entire setup in place. Skipping and reporting gives people the undo they asked for, minus exactly the things they have since made their own.&lt;/p&gt;

&lt;p&gt;What counts as someone's work was a decision too. Moving or resizing a widget doesn't count. Someone who drags an agent's widget aside hasn't made it theirs, and deleting it destroys nothing they did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The preview is the real undo, rolled back
&lt;/h2&gt;

&lt;p&gt;You want to see an undo before it happens. The MCP tool &lt;code&gt;undo_setup_run&lt;/code&gt; does a dry run unless told otherwise, and the server's instructions tell the agent to show you the result first. In the dashboard, the dialog says "This is what undoing will do. Nothing has been changed yet." before the button.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fphelzbtc3s4d52yfdr8s.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fphelzbtc3s4d52yfdr8s.png" alt="The " width="800" height="594"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The preview isn't a separate prediction. It runs the actual undo, every check and every write, inside the transaction, and then rolls the transaction back. A prediction written as separate code drifts away from what undo really does, and then the preview is wrong exactly when it matters. One code path can't disagree with itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Say what can't come back
&lt;/h2&gt;

&lt;p&gt;Some things can't be undone exactly, and pretending otherwise is worse than saying so.&lt;/p&gt;

&lt;p&gt;In Privatrak, a funnel counts from the day it is created. The per-visitor data that could recount earlier days is deleted every night, for privacy. That has two consequences for undo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Undoing a funnel the agent created also deletes the days it has counted since. The preview says how many.&lt;/li&gt;
&lt;li&gt;A funnel the agent deleted comes back with its name and steps, but it counts again from the undo. Its old days are gone, and the report says so.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule we took from this: when undo can't be exact, the preview has to say what will be lost, before anyone confirms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Undo is the last line, not the only one
&lt;/h2&gt;

&lt;p&gt;Undo is what makes write access reasonable, but it sits behind other guards:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tools go through our own API.&lt;/strong&gt; Each tool sends an ordinary request to our existing API, with the same authentication, validation, plan limits and project scoping as any other client. There is no second path to the data, so there is no second set of rules to get wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write access is a fixed list.&lt;/strong&gt; A read-only connection can reach a fixed list of API endpoints. Write access adds exactly the endpoints the write tools need, and nothing else. Tests fail if the tools and the lists disagree, in either direction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;People choose at sign-in.&lt;/strong&gt; Which projects the connection may see, and whether it may only read or also make changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every call names its project.&lt;/strong&gt; The server keeps no "current project" that one chat could switch for another.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The hints are honest.&lt;/strong&gt; Creating isn't destructive. Updating, deleting and undoing are, so Claude asks before them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When we submitted Privatrak to &lt;a href="https://claude.ai/directory/privatrak" rel="noopener noreferrer"&gt;Claude's connector directory&lt;/a&gt;, the undo made one more thing possible: a test plan that calls all 44 tools on a shared review account and ends by undoing everything it changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you build this yourself
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Pick the unit: one task, one run. Make every write join a run, even when the agent forgets to start one.&lt;/li&gt;
&lt;li&gt;Record the whole state before and after, in the same transaction as the write, saved in one fixed form so you can compare it later.&lt;/li&gt;
&lt;li&gt;Undo newest first, in one transaction, and recreate deleted things with their original ids.&lt;/li&gt;
&lt;li&gt;Check every change for conflicts. Skip and report; never force, and never give up on the whole run.&lt;/li&gt;
&lt;li&gt;Make the preview the real undo, rolled back.&lt;/li&gt;
&lt;li&gt;Say what can't come back, before it is lost.&lt;/li&gt;
&lt;li&gt;Put undo where people are: in the tool for the agent, and in your product's UI for everyone else.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Privatrak's MCP server works with Claude, Cursor and Codex. &lt;a href="https://privatrak.com/docs/ai-assistant" rel="noopener noreferrer"&gt;Our docs&lt;/a&gt; show how to connect it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>mcp</category>
      <category>automation</category>
    </item>
    <item>
      <title>Our analytics counted US visitors twice every evening. The bug was the word "day"</title>
      <dc:creator>Quentin Wendegass</dc:creator>
      <pubDate>Fri, 16 Oct 2026 15:36:28 +0000</pubDate>
      <link>https://dev.to/quentin_wendegass/our-analytics-counted-us-visitors-twice-every-evening-the-bug-was-the-word-day-21b1</link>
      <guid>https://dev.to/quentin_wendegass/our-analytics-counted-us-visitors-twice-every-evening-the-bug-was-the-word-day-21b1</guid>
      <description>&lt;p&gt;&lt;em&gt;A tester in the US told us the numbers looked wrong. The cause was a single assumption about what a day is, and we now suspect it hides in a lot more software than analytics.&lt;/em&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzy4sh3f1iar5xbs3c4gt.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzy4sh3f1iar5xbs3c4gt.png" alt="The same two page views, counted with the day cut at midnight UTC and at the visitor’s own midnight." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We build &lt;a href="https://privatrak.com" rel="noopener noreferrer"&gt;Privatrak&lt;/a&gt;, a cookieless analytics tool. In September one of our testers, who lives in the US, told us something was off. His data looked weird. There were more visits every day than there should have been.&lt;/p&gt;

&lt;p&gt;He was right. For visitors in the US we were counting some people twice, every day, around dinner time. The reason is obvious in hindsight and was invisible before: we had never decided what a "day" is.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we count people without a cookie
&lt;/h2&gt;

&lt;p&gt;A cookie is a label stuck on a browser. Every page view carries it, so counting unique visitors is easy.&lt;/p&gt;

&lt;p&gt;We don't store anything on the visitor's device, so we compute an identifier on the server from things every request already contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;visitor_id = HMAC(daily_key, IP address + browser + site)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same person on the same device produces the same identifier on every page view, so we count them once. We never keep the raw IP address or browser string, only the hash.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;daily_key&lt;/code&gt; is what keeps this private. If it never changed, the identifier would be a stable fingerprint that follows someone for months, which is exactly what cookie rules exist to prevent. So the key changes once a day. After the change, yesterday's identifiers can't be matched to today's.&lt;/p&gt;

&lt;p&gt;What we hadn't thought about: "once a day" means "at some clock time". We had picked midnight UTC without realising it was a choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Midnight UTC is not midnight
&lt;/h2&gt;

&lt;p&gt;Midnight UTC arrives at very different times of day depending on where your visitors are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your visitors are in&lt;/th&gt;
&lt;th&gt;Midnight UTC is, for them&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Los Angeles&lt;/td&gt;
&lt;td&gt;4–5 pm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New York&lt;/td&gt;
&lt;td&gt;7–8 pm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;São Paulo&lt;/td&gt;
&lt;td&gt;9 pm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;London&lt;/td&gt;
&lt;td&gt;midnight–1 am&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Berlin&lt;/td&gt;
&lt;td&gt;1–2 am&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dubai&lt;/td&gt;
&lt;td&gt;4 am&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mumbai&lt;/td&gt;
&lt;td&gt;5:30 am&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Singapore&lt;/td&gt;
&lt;td&gt;8 am&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokyo&lt;/td&gt;
&lt;td&gt;9 am&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sydney&lt;/td&gt;
&lt;td&gt;10–11 am&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;(The ranges are the two sides of daylight saving time.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For a European audience that moment is the middle of the night, so almost nobody is browsing and almost nobody gets split. For a US audience it lands in the early evening. Someone reads your pricing page at 7:50 pm, goes to dinner and comes back at 8:10 pm. The key changed at 8:00 (in winter it is 7:00). Same person, new identifier, two visitors.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhx0hrsbfpiozndc853o4.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhx0hrsbfpiozndc853o4.png" alt="An illustrative day for an evening-heavy site in New York. The shape is invented, not measured." width="800" height="420"&gt;&lt;/a&gt;&lt;br&gt;
That is why a tester in the US saw more visits than expected, and why a tester in Europe would never have noticed. We can't give you a percentage, because it depends on how long your visitors stay, how many come back the same evening and when your traffic peaks. What we can say is what happened after the fix: the difference was big, and his funnels improved as well.&lt;/p&gt;

&lt;p&gt;The funnels make sense once you see it. A funnel follows one visitor from step to step, so someone who looked at the pricing page at 7:50 pm and signed up at 8:10 pm was two visitors, and neither of them ever completed the funnel. Once the day was cut in the right place, his funnel numbers also matched what the insights in the dashboard showed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this affects
&lt;/h2&gt;

&lt;p&gt;Not everyone. It takes distance: if your visitors live close to the timezone where the day is cut, almost nobody is browsing when it turns over, and nobody gets split. It bites when visitors are far from that boundary. For a product like ours, where customers and visitors can be in every timezone, any single fixed boundary can land in somebody's evening, so one boundary for everyone can't be right.&lt;/p&gt;

&lt;p&gt;A site whose visitors are mostly in one region can fix it by choosing that region's timezone. A site with a truly worldwide audience can't make it go away, because some part of the world is always in the evening when the day turns over. The best you can do is cut the day where your own traffic is lowest.&lt;/p&gt;

&lt;h2&gt;
  
  
  It wasn't only visitors
&lt;/h2&gt;

&lt;p&gt;When we looked, the visitor key was only one of two places that used the UTC day. The other was every daily bucket in the reports. We had two separate pieces of code that each decided independently that a day starts at midnight UTC. They happened to agree with each other, so the mistake didn't show up as a disagreement anywhere.&lt;/p&gt;

&lt;p&gt;For a New York project, the bar labelled "Tuesday" actually held Monday 8 pm to Tuesday 8 pm. So the problem wasn't just a few extra visitors. Every day in the report was shifted. A newsletter sent at 9 pm on Monday would have appeared to work on Tuesday.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: decide once, in one place
&lt;/h2&gt;

&lt;p&gt;We gave every project a timezone and made it decide both things: the moment the key changes and the edges of every daily bucket. Now there is only one value, so the two can't disagree.&lt;/p&gt;

&lt;p&gt;It looked like a small change. It wasn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A counted day is frozen.&lt;/strong&gt; We delete the raw rows of a day once we've counted it, for privacy. So when someone changes a project's timezone, we can't re-count old days on the new boundary; the rows that would answer it are gone. A timezone change applies from then on, and the first day after a change is longer or shorter than 24 hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Daylight saving makes days 23 or 25 hours long.&lt;/strong&gt; An hourly chart has 25 buckets on the day the clocks go back and 23 when they go forward. We label each bucket with the UTC offset at that moment, so there's one bucket per real hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is not only an analytics bug
&lt;/h2&gt;

&lt;p&gt;Once we'd seen it, we started spotting the same bug elsewhere. Plenty of software never decides whose day it is or when things reset, and just uses whatever clock the server has. Places we'd look:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Daily quotas and rate limits&lt;/strong&gt; that reset at midnight UTC, so users elsewhere get their fresh allowance at an odd hour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaks and daily goals.&lt;/strong&gt; A streak that breaks because the server's midnight arrived mid-evening is a classic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Today" in dashboards and reports,&lt;/strong&gt; especially when you compare it with another system's day, such as your payment provider's.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily budgets and billing periods&lt;/strong&gt; that pace or reset by an account's day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled digests and cron jobs.&lt;/strong&gt; "Every day at 9 am" in whose 9 am?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything that rotates an identifier on a schedule:&lt;/strong&gt; session IDs, rate-limit buckets, A/B test bucket seeds. If it changes at a fixed time, it splits whoever is mid-action, exactly like ours did.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daylight saving,&lt;/strong&gt; for everything that assumes a day has 24 hours.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What we'd do differently
&lt;/h2&gt;

&lt;p&gt;The first time &lt;code&gt;DATE(timestamp)&lt;/code&gt; or "reset at midnight" shows up in code, ask whose midnight it is. We didn't, and we found out when a tester in another timezone noticed.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>security</category>
      <category>analytics</category>
    </item>
  </channel>
</rss>
