<?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: Hisashi Space</title>
    <description>The latest articles on DEV Community by Hisashi Space (@hisashispace).</description>
    <link>https://dev.to/hisashispace</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%2F3305583%2F2d37bacf-221e-40fa-ac4c-1fda4aac21ed.png</url>
      <title>DEV Community: Hisashi Space</title>
      <link>https://dev.to/hisashispace</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hisashispace"/>
    <language>en</language>
    <item>
      <title>Cloudflare split "Block AI Bots" into Search, Training and Agent. I checked all 26 of my sites through the API</title>
      <dc:creator>Hisashi Space</dc:creator>
      <pubDate>Tue, 13 Oct 2026 13:55:02 +0000</pubDate>
      <link>https://dev.to/hisashispace/cloudflare-split-block-ai-bots-into-search-training-and-agent-i-checked-all-26-of-my-sites-238h</link>
      <guid>https://dev.to/hisashispace/cloudflare-split-block-ai-bots-into-search-training-and-agent-i-checked-all-26-of-my-sites-238h</guid>
      <description>&lt;p&gt;On September 16, 2026, I got an email from Cloudflare titled "Updates to managing AI crawlers on your account."&lt;br&gt;
It said the setting for blocking AI crawlers was changing from a single switch into three categories: Search, Training and Agent.&lt;/p&gt;

&lt;p&gt;I manage 26 domains on Cloudflare.&lt;br&gt;
Instead of opening the dashboard 26 times, I pulled the settings for every domain at once through the API.&lt;br&gt;
This post covers what changed, how I checked, and how I think a site that wants to be cited in AI answers should be set up.&lt;/p&gt;
&lt;h2&gt;
  
  
  What changed
&lt;/h2&gt;

&lt;p&gt;The email listed three changes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The recommended setting for training crawlers changed from "Block" to "Disallow AI Training"&lt;/li&gt;
&lt;li&gt;Sites that had turned on "Block AI Bots" or "Managed Robots.txt" are moved to the new three-category setting automatically&lt;/li&gt;
&lt;li&gt;Sites that never touched the setting stay on "Allow," and nothing changes for them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;According to Cloudflare, "Disallow AI Training" lets search crawlers such as Applebot, Googlebot and Bingbot keep crawling for search results, while telling them not to use the content for training.&lt;/p&gt;

&lt;p&gt;Sites that are moved automatically end up with these values.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Search: Allow&lt;/li&gt;
&lt;li&gt;Training: Disallow AI Training&lt;/li&gt;
&lt;li&gt;Agent: Block only on pages with ads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;According to the email, the "Block AI Bots" switch disappears from the dashboard once the migration is done.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reading the settings through the API
&lt;/h2&gt;

&lt;p&gt;Each zone's settings can be read with &lt;code&gt;GET /zones/{zone_id}/bot_management&lt;/code&gt;.&lt;br&gt;
I listed all zones, read the settings one by one, and printed only the AI-related fields.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# CF_API_TOKEN needs the "Bot Management Read" permission&lt;/span&gt;
&lt;span class="nv"&gt;AUTH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$CF_API_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AUTH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"https://api.cloudflare.com/client/v4/zones?per_page=100"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.result[] | "\(.id) \(.name)"'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nb"&gt;id &lt;/span&gt;name&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
      &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AUTH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"https://api.cloudflare.com/client/v4/zones/&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;/bot_management"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        | jq &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; name &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'{name: $name} + (.result | {
            ai_bots_protection, ai_search, ai_training, ai_user,
            is_robots_txt_managed, cf_robots_variant, crawler_protection
          })'&lt;/span&gt;
    &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My first try used the API token I use for DNS.&lt;br&gt;
It came back with &lt;code&gt;Authentication error&lt;/code&gt; (code 10000).&lt;br&gt;
The API reference says reading this setting needs either "Bot Management Read" or "Bot Management Write."&lt;br&gt;
If you reuse a DNS token, you will hit the same wall.&lt;/p&gt;
&lt;h3&gt;
  
  
  What the fields mean
&lt;/h3&gt;

&lt;p&gt;Summarizing the API reference:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Values&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ai_bots_protection&lt;/td&gt;
&lt;td&gt;block / disabled / only_on_ad_pages&lt;/td&gt;
&lt;td&gt;Rule that blocks AI crawlers (the old "Block AI Bots")&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ai_search&lt;/td&gt;
&lt;td&gt;disabled / block / only_on_ad_pages&lt;/td&gt;
&lt;td&gt;robots.txt policy for AI search bots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ai_training&lt;/td&gt;
&lt;td&gt;disabled / disallow / block / only_on_ad_pages&lt;/td&gt;
&lt;td&gt;robots.txt policy for training bots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ai_user&lt;/td&gt;
&lt;td&gt;disabled / block / only_on_ad_pages&lt;/td&gt;
&lt;td&gt;robots.txt policy for AI assistant and agent bots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;is_robots_txt_managed&lt;/td&gt;
&lt;td&gt;true / false&lt;/td&gt;
&lt;td&gt;Whether Cloudflare writes robots.txt for you&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;crawler_protection&lt;/td&gt;
&lt;td&gt;enabled / disabled&lt;/td&gt;
&lt;td&gt;Rule that traps AI crawlers in a maze of links&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In the reference, the three new categories (ai_search, ai_training, ai_user) are described as "robots.txt policies."&lt;br&gt;
Only ai_training has a &lt;code&gt;disallow&lt;/code&gt; value.&lt;/p&gt;

&lt;p&gt;The point to keep in mind is that robots.txt is only a request.&lt;br&gt;
Cloudflare's own documentation says whether to follow robots.txt is up to each crawler, and that it is not a technical block.&lt;br&gt;
If you really want to stop a crawler, you use a rule such as AI Crawl Control.&lt;/p&gt;
&lt;h2&gt;
  
  
  Results for 26 sites
&lt;/h2&gt;

&lt;p&gt;All 26 sites came back the same.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ai_bots_protection: disabled&lt;/li&gt;
&lt;li&gt;ai_search, ai_training, ai_user: all disabled&lt;/li&gt;
&lt;li&gt;is_robots_txt_managed: false&lt;/li&gt;
&lt;li&gt;crawler_protection: disabled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, none of my sites were blocking AI crawlers.&lt;br&gt;
That matches "sites that never touched the setting stay the same" in the email.&lt;br&gt;
They are not part of the automatic migration, so this change has no effect on them.&lt;/p&gt;

&lt;p&gt;One field did differ between sites.&lt;br&gt;
&lt;code&gt;cf_robots_variant&lt;/code&gt; was &lt;code&gt;policy_only&lt;/code&gt; on 10 of the 26 sites and &lt;code&gt;off&lt;/code&gt; on the other 16.&lt;br&gt;
The reference only calls it "the type of Robots Access Control License," and I do not remember ever setting it.&lt;/p&gt;

&lt;p&gt;I fetched robots.txt from one of the &lt;code&gt;policy_only&lt;/code&gt; sites, and Cloudflare had not added any lines.&lt;br&gt;
Since is_robots_txt_managed is false, my reading is that Cloudflare is not touching robots.txt.&lt;br&gt;
I have not yet confirmed what this value actually means.&lt;/p&gt;
&lt;h2&gt;
  
  
  I allow AI crawlers explicitly in my own robots.txt
&lt;/h2&gt;

&lt;p&gt;I do not leave robots.txt to Cloudflare. I write it myself.&lt;br&gt;
AI crawlers are allowed by name (excerpt):&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: *
Allow: /

# AI crawlers: fully allowed
User-Agent: GPTBot
User-Agent: ChatGPT-User
User-Agent: OAI-SearchBot
User-Agent: ClaudeBot
User-Agent: Claude-SearchBot
User-Agent: Claude-User
User-Agent: Google-Extended
User-Agent: PerplexityBot
User-Agent: Applebot-Extended
Allow: /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason is simple: the smaller the site, the more it gains from being named in an AI answer.&lt;br&gt;
To be a candidate when someone asks "what do you recommend?", the site first has to be readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Settings for a site that wants to be cited in AI answers
&lt;/h2&gt;

&lt;p&gt;Putting this together, here is how I think a site that wants to be cited in AI answers (ChatGPT search, Perplexity and so on) should approach it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not block Search or Agent
&lt;/h3&gt;

&lt;p&gt;Blocking AI search bots means losing the chance to appear in AI search results at all.&lt;br&gt;
Agents that read a page because a user asked them to (ChatGPT-User and similar) cannot use your content in their answer if you block them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decide Training as a policy
&lt;/h3&gt;

&lt;p&gt;Being used for training and being cited in a search answer are two different things.&lt;br&gt;
If you do not want your content used for training, you can set only Training to Disallow AI Training and keep Search and Agent allowed.&lt;br&gt;
This change is exactly what makes that split selectable in the dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  If your site was migrated, check the Agent setting
&lt;/h3&gt;

&lt;p&gt;Sites that had Block AI Bots turned on are moved to "Agent: Block only on pages with ads."&lt;br&gt;
If you want AI agents to read pages that show ads, check the setting once after the migration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check that nothing else is blocking them
&lt;/h3&gt;

&lt;p&gt;Even if every Cloudflare setting is disabled, a WAF custom rule or a rate limit may still be turning AI crawlers away.&lt;br&gt;
Allowing them in robots.txt does nothing if they are stopped before they get there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Cloudflare's AI crawler setting is now split into Search, Training and Agent&lt;/li&gt;
&lt;li&gt;Only sites that were blocking before are migrated automatically. Sites that never touched it see no change&lt;/li&gt;
&lt;li&gt;The settings can be read through the &lt;code&gt;bot_management&lt;/code&gt; API, so you can check every zone at once. The token needs Bot Management Read&lt;/li&gt;
&lt;li&gt;The three new categories are robots.txt policies and are not enforced&lt;/li&gt;
&lt;li&gt;If you want to be cited in AI answers, leave Search and Agent open and decide Training as a policy&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cloudflare</category>
      <category>seo</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I run DeepSeek R1 70B from my Tokyo office. Here's the network setup.</title>
      <dc:creator>Hisashi Space</dc:creator>
      <pubDate>Sun, 11 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/hisashispace/i-run-deepseek-r1-70b-from-my-tokyo-office-heres-the-network-setup-d6n</link>
      <guid>https://dev.to/hisashispace/i-run-deepseek-r1-70b-from-my-tokyo-office-heres-the-network-setup-d6n</guid>
      <description>&lt;p&gt;I'm Hisashi. I've run my own company for 22 years, and from 2011 to 2023 I ran it while moving between countries. I still travel, and I want my tools to work the same wherever I am.&lt;/p&gt;

&lt;p&gt;This post is about how I turned a Mac in my Tokyo office into a 70B-class AI inference server I can hit from anywhere.&lt;/p&gt;




&lt;p&gt;I keep an M1 MacBook Pro stationed at my Tokyo office, running DeepSeek R1 70B and Qwen2.5 14B via Ollama. From my laptop, a café in Tokyo or a hotel room abroad, I get the same latency, the same response, the same experience.&lt;/p&gt;

&lt;p&gt;Getting it to that point taught me something. &lt;strong&gt;Installing Ollama isn't enough.&lt;/strong&gt; The moment you step outside the office, your "personal AI server" is dead unless you've fixed the network underneath it.&lt;/p&gt;

&lt;p&gt;I've optimized home networks for remote work for 15+ years, and even I was surprised by how rigorous the requirements got once "70B model accessible globally" became the bar. Four things have to be right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fast WiFi&lt;/strong&gt;: A WiFi 7 router (GL.iNet GL-BE9300, aka "Flint 3"), with 6GHz / 5GHz / 2.4GHz bands explicitly assigned to different device generations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dual-NAT elimination&lt;/strong&gt;: My ISP (au Hikari 10G, a Japanese 10-gigabit fiber service) forces a home gateway in the path. I use DMZ-host mode to make it effectively transparent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A stable VPN&lt;/strong&gt;: Tailscale, which I measured at 1.6 to 2.5× faster than Cloudflare Tunnel for this use case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Baseline security&lt;/strong&gt;: SSH key-only auth, config tracked in git, firmware current.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the overview.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Why the network is the foundation, not the model
&lt;/h2&gt;

&lt;p&gt;Getting an LLM running on Apple Silicon is trivial. &lt;code&gt;brew install ollama &amp;amp;&amp;amp; ollama run deepseek-r1:70b&lt;/code&gt; and you have a 70B model on your Mac in 10 minutes.&lt;/p&gt;

&lt;p&gt;The moment you try to use that model as "an AI server I can hit from anywhere," reality hits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can't reach it from outside. 80% of my actual use cases happen when I'm &lt;em&gt;not&lt;/em&gt; sitting at the office Mac.&lt;/li&gt;
&lt;li&gt;Connections drop. Even with Cloudflare Tunnel or similar overlays, long sessions don't stay stable.&lt;/li&gt;
&lt;li&gt;Latency is mediocre. Big responses stall and hitch.&lt;/li&gt;
&lt;li&gt;Office bandwidth suffers. AI inference traffic crowds out everything else on the same network.&lt;/li&gt;
&lt;li&gt;You become a target. Naively open SSH to the internet, and bot brute-force attempts start within hours.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are problems with Ollama. They're problems with the network underneath Ollama.&lt;/p&gt;

&lt;p&gt;If you actually want to operate an AI server seriously, you start with the network, not the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Layer 1: Fast WiFi (WiFi 7 router)
&lt;/h2&gt;

&lt;p&gt;More devices on the network means WiFi becomes a real bottleneck. My count: phone, tablet, main work Mac, AI-server Mac, printer, scanner, smart-home stuff. Easily 10 to 20 devices.&lt;/p&gt;

&lt;p&gt;The AI-server Mac in particular downloads multi-tens-of-gigabyte model files occasionally, and during those windows every other device's WiFi degrades. It's absurd to build a "global-access AI server" while making your office WiFi unusable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the Flint 3 (GL-BE9300)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;WiFi 7 (802.11be) tri-band&lt;/li&gt;
&lt;li&gt;Independent 2.4 / 5 / 6 GHz radios, each band as its own SSID&lt;/li&gt;
&lt;li&gt;OpenWrt under the hood, so every setting is configurable via SSH + &lt;code&gt;uci&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;GL.iNet's web UI ships with Tailscale, AdGuard Home, WireGuard, and more as first-class features&lt;/li&gt;
&lt;li&gt;~$300. Pricey for a consumer router, but a fraction of an enterprise alternative.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  My three-band assignment
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;6GHz&lt;/strong&gt;: latest devices (iPhone 15/16, latest iPad, latest Macs). WPA3-only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5GHz&lt;/strong&gt;: main work devices, AI-server Mac, Apple TV. WPA2/WPA3 mixed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2.4GHz&lt;/strong&gt;: legacy IoT (scanner, printer, smart-home). WPA2.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;2.4 GHz has the longest range but the lowest throughput. By dedicating it to "legacy stuff that doesn't need speed," you push your main devices to 5/6 GHz where they get full bandwidth. Cleaner separation, less interference.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Layer 2: Defeating dual-NAT with a DMZ host
&lt;/h2&gt;

&lt;p&gt;Most Japanese fiber contracts, au Hikari 10G included, require keeping the ISP-provided HGW (home gateway router) in the path. You can't replace it; the ISP bakes authentication credentials into the device itself.&lt;/p&gt;

&lt;p&gt;That leaves you with two layers: the HGW (&lt;code&gt;192.168.0.0/24&lt;/code&gt;) and the Flint 3 (&lt;code&gt;192.168.1.0/24&lt;/code&gt;). Classic dual-NAT.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why dual-NAT is a problem
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You have to open ports twice (once per device)&lt;/li&gt;
&lt;li&gt;UPnP doesn't traverse the boundary (game console NAT-type pain)&lt;/li&gt;
&lt;li&gt;Standing up your own WireGuard / OpenVPN server inside is awkward&lt;/li&gt;
&lt;li&gt;mDNS / Bonjour gets segmented (AirPlay degrades)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The fix: DMZ host on the HGW
&lt;/h3&gt;

&lt;p&gt;In the HGW admin UI, enable "DMZ host" mode and point it at the Flint 3's WAN-side IP. Done. External packets now pass through the HGW and land on the Flint 3. The HGW becomes effectively transparent, equivalent to bridge mode in practice.&lt;/p&gt;

&lt;p&gt;You're safe because the Flint 3's OpenWrt firewall drops or rejects everything on the WAN zone by default. Even with all packets arriving, unsolicited packets get blocked. The HGW is transparent at the routing level, but firewalling happens on the Flint, where you have full control.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Layer 3: Tailscale for fast, stable, global access
&lt;/h2&gt;

&lt;p&gt;Three main ways to reach your office AI server from anywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Port-forwarding + DDNS&lt;/strong&gt;: direct exposure. Fast, but a huge attack surface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Tunnel&lt;/strong&gt;: HTTPS reverse proxy via Cloudflare's edge. Medium speed, drops long sessions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailscale&lt;/strong&gt;: WireGuard-based P2P overlay. Fast, very stable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I originally ran my server behind a Cloudflare Tunnel with a subdomain like &lt;code&gt;llm.example.com&lt;/code&gt;. It worked, but long sessions kept dropping, HTTP/2 and QUIC stalled, latency jittered.&lt;/p&gt;

&lt;p&gt;Switched to Tailscale. Measurements:&lt;/p&gt;

&lt;h3&gt;
  
  
  LAN-internal test
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;HTTP &lt;code&gt;/api/tags&lt;/code&gt; round-trip: &lt;strong&gt;Tailscale 25 ms, Cloudflare Tunnel 64 ms&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tailscale is 2.5× faster&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tethered (simulating off-site access)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;HTTP &lt;code&gt;/api/tags&lt;/code&gt; round-trip: &lt;strong&gt;Tailscale 124 ms, Cloudflare Tunnel 196 ms&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tailscale is 1.6× faster&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Tailscale connection mode: P2P direct (no relay)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tailscale wins both on LAN and off-site, and because it establishes P2P direct connections it doesn't even route through Tailscale's relay servers. Structurally lower overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Layer 4: Baseline security
&lt;/h2&gt;

&lt;p&gt;If your AI server is reachable from anywhere in the world, it's also visible to attackers from anywhere in the world. With Tailscale, you're protected by the tailnet auth boundary. With Cloudflare Tunnel, you're basically internet-facing.&lt;/p&gt;

&lt;p&gt;Three defenses are non-negotiable:&lt;/p&gt;

&lt;h3&gt;
  
  
  5-1. SSH key-only auth (password auth disabled)
&lt;/h3&gt;

&lt;p&gt;Modern macOS defaults &lt;code&gt;PasswordAuthentication&lt;/code&gt; to &lt;code&gt;no&lt;/code&gt;. On OpenWrt (the Flint 3 side), I set dropbear's &lt;code&gt;PasswordAuth&lt;/code&gt; to &lt;code&gt;off&lt;/code&gt; as well. Password brute force becomes physically impossible.&lt;/p&gt;

&lt;h3&gt;
  
  
  5-2. Git-managed configuration
&lt;/h3&gt;

&lt;p&gt;I track the Flint 3's &lt;code&gt;/etc/config/*&lt;/code&gt; files in a private GitHub repo, after sanitizing secrets (WiFi PSK, rpcd password) with &lt;code&gt;&amp;lt;REDACTED&amp;gt;&lt;/code&gt; markers. Config diffs are visible in PR form; rollback is one &lt;code&gt;git checkout&lt;/code&gt; away.&lt;/p&gt;

&lt;h3&gt;
  
  
  5-3. Firmware updates
&lt;/h3&gt;

&lt;p&gt;Both router and server firmware/OS stay current. The Flint 3 (GL.iNet) supports "Keep Settings" on firmware upgrades, so major configuration is preserved.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Before vs after
&lt;/h2&gt;

&lt;p&gt;The network rebuild changed daily experience like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global connection stability: &lt;strong&gt;before&lt;/strong&gt; "breaks every 1 to 2 hours," &lt;strong&gt;after&lt;/strong&gt; "doesn't break" (Tailscale UDP session is persistent)&lt;/li&gt;
&lt;li&gt;Ollama API latency: &lt;strong&gt;before&lt;/strong&gt; ~200ms avg, &lt;strong&gt;after&lt;/strong&gt; ~124ms avg (~40% reduction)&lt;/li&gt;
&lt;li&gt;Large file transfers: &lt;strong&gt;before&lt;/strong&gt; "stalls," &lt;strong&gt;after&lt;/strong&gt; "smooth"&lt;/li&gt;
&lt;li&gt;Dual-NAT pain: &lt;strong&gt;before&lt;/strong&gt; UPnP/mDNS broken, &lt;strong&gt;after&lt;/strong&gt; effectively gone&lt;/li&gt;
&lt;li&gt;Security: &lt;strong&gt;before&lt;/strong&gt; "weak password, SSH wide open," &lt;strong&gt;after&lt;/strong&gt; "key-only, minimal exposure"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"Getting it running" takes 10 minutes. "Operating it well" takes a few days. But once you've done it, you can hit a 70B model from anywhere in the world with the same latency you'd get sitting in front of the office Mac.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Running an AI server from a home office isn't exotic anymore.&lt;/li&gt;
&lt;li&gt;But "Ollama installed" doesn't equal "operational."&lt;/li&gt;
&lt;li&gt;Get all four layers right, WiFi, dual-NAT, VPN, security, and you have a globally-accessible inference server that actually works.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running 70B-class models locally isn't special anymore. What separates the people who actually &lt;em&gt;use&lt;/em&gt; their local model from those who just have one installed is having it "always there, from anywhere, reliably."&lt;/p&gt;

&lt;p&gt;The M1 MacBook Pro in my Tokyo office, with this setup, has become an always-on AI inference layer for me. Same experience from a café in Tokyo or a hotel abroad.&lt;/p&gt;

</description>
      <category>ollama</category>
      <category>tailscale</category>
      <category>networking</category>
      <category>homelab</category>
    </item>
    <item>
      <title>Google vs ChatGPT: which one brings people to a brand-new site first? A month of crawler logs from nine sites</title>
      <dc:creator>Hisashi Space</dc:creator>
      <pubDate>Sun, 11 Oct 2026 05:35:36 +0000</pubDate>
      <link>https://dev.to/hisashispace/google-vs-chatgpt-which-one-brings-people-to-a-brand-new-site-first-a-month-of-crawler-logs-from-a69</link>
      <guid>https://dev.to/hisashispace/google-vs-chatgpt-which-one-brings-people-to-a-brand-new-site-first-a-month-of-crawler-logs-from-a69</guid>
      <description>&lt;p&gt;When you launch a site on a brand-new domain, Google takes its time.&lt;br&gt;
It's normal to wait weeks before you show up in search results at all, and months before you rank anywhere useful.&lt;/p&gt;

&lt;p&gt;So what about ChatGPT?&lt;/p&gt;

&lt;p&gt;In the summer of 2026 we launched nine product sites, each on a fresh domain.&lt;br&gt;
When I lined up the logs, I found that while I was still waiting on Google, ChatGPT had already sent people who went on to sign up.&lt;br&gt;
And when Google traffic to some of those sites dropped to almost nothing, ChatGPT kept sending people anyway.&lt;/p&gt;

&lt;p&gt;This post lays out those logs in time order, and ends with what I'd do on a new site if I wanted ChatGPT to send people to it.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I looked at
&lt;/h2&gt;

&lt;p&gt;Each of the nine sites is a product page plus a blog (how-to and comparison articles).&lt;br&gt;
The products are all over the place: a B2B cloud service, Mac apps, tools for group messaging and bookings.&lt;br&gt;
Some sites have both Japanese and English pages, some are Japanese only.&lt;br&gt;
I'm leaving the product names out and calling them Site A to Site I.&lt;/p&gt;

&lt;p&gt;Three sources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Raw server access logs (for four of the sites, going back to the day the domain was registered)&lt;/li&gt;
&lt;li&gt;Cloudflare analytics (for the other five; nothing before September 11 is available)&lt;/li&gt;
&lt;li&gt;Google Analytics (all nine; how many people came and where from)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All times are Japan time.&lt;br&gt;
Bots were identified by their user agent and then checked against the IP ranges each company publishes, so only the real ones are counted.&lt;/p&gt;
&lt;h2&gt;
  
  
  The bots in this story
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Googlebot: collects pages for Google Search&lt;/li&gt;
&lt;li&gt;OAI-SearchBot: collects pages for ChatGPT's search feature&lt;/li&gt;
&lt;li&gt;ChatGPT-User: fetches a page on the spot when someone asks ChatGPT something&lt;/li&gt;
&lt;li&gt;GPTBot: collects pages for training OpenAI's models&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OpenAI describes each of these here:&lt;br&gt;
&lt;a href="https://developers.openai.com/api/docs/bots" rel="noopener noreferrer"&gt;OpenAI's crawlers&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The one that matters most here is ChatGPT-User.&lt;br&gt;
It doesn't crawl the web on its own schedule. It comes when a person asks ChatGPT a question.&lt;br&gt;
So a ChatGPT-User hit in your logs means that, at that moment, somebody was asking ChatGPT about something.&lt;/p&gt;
&lt;h2&gt;
  
  
  Who showed up first
&lt;/h2&gt;

&lt;p&gt;Here's Site A, from the day the domain was registered, in time order.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzmacaqpeaznxl5pc31n8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzmacaqpeaznxl5pc31n8.webp" alt="Timeline of the first bots and visitors on Site A" width="800" height="867"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Walking through it:&lt;/p&gt;

&lt;p&gt;The domain was registered in the early hours of August 29, and the SSL certificate was issued at 03:09.&lt;br&gt;
Certificate issuance goes into Certificate Transparency, a public log anyone can read.&lt;/p&gt;

&lt;p&gt;An hour later, at 04:08, four bots hit the home page within two seconds of each other.&lt;br&gt;
One of them is literally named CertSignalBot, a bot that watches those certificate logs.&lt;br&gt;
In other words, there are systems out there that pick up new domains and new certificates and start crawling them automatically.&lt;br&gt;
If you've ever looked at your backlinks in Ahrefs and found a pile of links from random overseas sites, many of those are the same kind of thing: sites that scrape new domains and list them.&lt;/p&gt;

&lt;p&gt;OAI-SearchBot, ChatGPT's search crawler, showed up 20 minutes later, at 04:27.&lt;br&gt;
Googlebot first came two days later on August 31, and Bingbot five days later on September 3.&lt;br&gt;
At that point there were no articles, no press release and no backlinks.&lt;/p&gt;

&lt;p&gt;On Site B, where I also have server logs from the start, OAI-SearchBot came about two hours after the domain was registered.&lt;br&gt;
On Site D, Googlebot came first.&lt;br&gt;
Which one comes first depends on the site, but both arrived within a few days of the domain going live.&lt;/p&gt;
&lt;h2&gt;
  
  
  Does ChatGPT find sites through Google or Bing?
&lt;/h2&gt;

&lt;p&gt;You've probably heard that ChatGPT search runs on Bing.&lt;/p&gt;

&lt;p&gt;On Sites A and B, OAI-SearchBot arrived before both Googlebot and Bingbot.&lt;br&gt;
At least at the discovery stage, ChatGPT isn't waiting for Google or Bing to index a site. It sends its own bot.&lt;/p&gt;

&lt;p&gt;What it saw to learn about the site, I can't tell you; OpenAI doesn't say.&lt;br&gt;
All the logs show is that OAI-SearchBot came 20 minutes after a certificate-watching bot did.&lt;/p&gt;

&lt;p&gt;Where ChatGPT looks for pages when it's answering a question is a separate matter, and these logs can't settle it.&lt;br&gt;
On both Site A and Site B, ChatGPT-User's first visit came after Bingbot's first visit.&lt;/p&gt;
&lt;h2&gt;
  
  
  At first it just checks the door
&lt;/h2&gt;

&lt;p&gt;One thing to watch out for.&lt;br&gt;
From August 29 to September 3, OAI-SearchBot came by one to six times a day, but all it fetched was robots.txt.&lt;br&gt;
robots.txt is the file that tells bots which parts of a site they're allowed into.&lt;br&gt;
So it was checking whether it could come in, and leaving without reading anything.&lt;/p&gt;

&lt;p&gt;Articles started going up on September 2, and at 00:44 on September 4 it read an article for the first time.&lt;br&gt;
It came 56 times that day.&lt;/p&gt;

&lt;p&gt;Site I shows the difference even more clearly.&lt;br&gt;
Site I's domain had existed since May, and OAI-SearchBot had been coming since May.&lt;br&gt;
But ChatGPT-User didn't show up until September 8, six days after the first articles went up.&lt;br&gt;
As long as there was nothing to read, ChatGPT never used the site in an answer.&lt;/p&gt;

&lt;p&gt;ChatGPT doesn't write a site off after one visit. It keeps coming to the door every day and starts reading once there's something there.&lt;/p&gt;
&lt;h2&gt;
  
  
  With ChatGPT, people arrive right after it reads
&lt;/h2&gt;

&lt;p&gt;The first person to come to Site A from ChatGPT arrived at 12:18 on September 4.&lt;br&gt;
That's six days after the domain was registered and two days after the first articles went up.&lt;/p&gt;

&lt;p&gt;The log shows ChatGPT-User fetching the home page, and four seconds later, a visitor arriving from chatgpt.com.&lt;/p&gt;

&lt;p&gt;The sign-ups looked the same.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9roizasjdqb8bayf98px.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9roizasjdqb8bayf98px.webp" alt="From ChatGPT reading the page to a visitor signing up" width="800" height="633"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;September 14:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;14:22:06 ChatGPT-User fetches the home page&lt;/li&gt;
&lt;li&gt;14:23:37 (91 seconds later) a visitor arrives from chatgpt.com&lt;/li&gt;
&lt;li&gt;14:23:43 they open the sign-up page&lt;/li&gt;
&lt;li&gt;14:24:15 (2 min 09 s after the fetch) sign-up complete&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;September 25:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;05:05:22 ChatGPT-User fetches the home page&lt;/li&gt;
&lt;li&gt;05:06:37 (75 seconds later) a visitor arrives from chatgpt.com&lt;/li&gt;
&lt;li&gt;05:06:42 they open the sign-up page&lt;/li&gt;
&lt;li&gt;05:07:32 (2 min 10 s after the fetch) sign-up complete&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I can't prove from the logs that the fetch and the visit came from the same person's question.&lt;br&gt;
What I can say is that this sequence really happened: ChatGPT read the page, about a minute later someone came from ChatGPT, and they signed up on the spot.&lt;/p&gt;

&lt;p&gt;As of October 9, Site A had zero sign-ups from people who came through Google Search.&lt;br&gt;
It had three from people who came through ChatGPT, including these two.&lt;/p&gt;
&lt;h2&gt;
  
  
  With Google, there are stages between reading and visiting
&lt;/h2&gt;

&lt;p&gt;Google isn't slow because its bot is slow.&lt;br&gt;
Googlebot came to Site A two days after the domain was registered, and from then on read pages dozens to hundreds of times a day.&lt;/p&gt;

&lt;p&gt;Google describes Search in three stages: crawling (collecting pages), indexing (understanding and storing them), and serving (showing them in results).&lt;br&gt;
&lt;a href="https://developers.google.com/search/docs/fundamentals/how-search-works" rel="noopener noreferrer"&gt;How Google Search works&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;People only arrive once you're in the results and someone clicks.&lt;br&gt;
For a new site, getting from "crawled" to "near the top of the results" takes time.&lt;/p&gt;

&lt;p&gt;With ChatGPT, reading the page and recommending it happen inside one question.&lt;br&gt;
Someone asks, ChatGPT reads the page right then, and puts a link in the answer.&lt;br&gt;
That's why people show up seconds to minutes after the fetch.&lt;/p&gt;
&lt;h2&gt;
  
  
  When three sites vanished from Google, ChatGPT kept sending people
&lt;/h2&gt;

&lt;p&gt;I compared visits to all nine sites over two eight-day windows: September 25 to October 2, and October 3 to October 10.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwbnc4k0phmjyapxzr0mq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwbnc4k0phmjyapxzr0mq.webp" alt="Visits from Google Search and from AI to the nine sites" width="800" height="867"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On October 3, Sites A, B and C pretty much stopped getting visitors from Google Search.&lt;br&gt;
That lines up with Google's September 2026 spam update. I wrote that part up separately:&lt;br&gt;
&lt;a href="https://dev.to/hisashispace/what-happened-to-my-one-month-old-site-during-googles-september-2026-spam-update-2347"&gt;What happened to my one-month-old site during Google's September 2026 spam update&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On those same three sites, visits from ChatGPT and other AI kept coming: 15 to 7, 14 to 13, and 11 to 7.&lt;br&gt;
Whatever Google decided about those sites, ChatGPT was reading them and recommending them on its own terms.&lt;/p&gt;

&lt;p&gt;Across all nine sites, visits from AI went from 85 to 135.&lt;br&gt;
Most of that increase was Site D, though.&lt;br&gt;
In raw numbers, Google Search still sends far more people, four to eight times as many as AI across the nine sites.&lt;br&gt;
I'm not saying ChatGPT replaces Google.&lt;br&gt;
What I am saying is that during the launch period, when Google isn't sending anyone yet, ChatGPT can.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where ChatGPT visitors landed
&lt;/h2&gt;

&lt;p&gt;I counted the first page that people coming from ChatGPT opened on the nine sites (September 1 to October 10, 257 visits):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Blog articles: 189 (74%)&lt;/li&gt;
&lt;li&gt;Home page: 48 (19%)&lt;/li&gt;
&lt;li&gt;Pricing and other pages: 20 (8%)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three out of four landed on an article.&lt;br&gt;
On Site D, a single how-to article for a group messaging app (how to message just one person in a group) brought 24 visits on its own.&lt;br&gt;
ChatGPT is picking articles as the answer to someone's question and sending them there.&lt;/p&gt;
&lt;h2&gt;
  
  
  How to get ChatGPT to send people to a new site
&lt;/h2&gt;

&lt;p&gt;Based on these logs, here's what I'd do on a new site. Call it ChatGPT SEO (some people call it LLM SEO), though it's mostly about not getting in the way and having something worth reading.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Don't block ChatGPT's bots from day one
&lt;/h3&gt;

&lt;p&gt;OAI-SearchBot checked robots.txt on the same day the domain went live.&lt;br&gt;
If you turn it away at that point, it has no reason to come back and read once you've built something.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;https://yourdomain/robots.txt&lt;/code&gt; and check that there's no &lt;code&gt;Disallow: /&lt;/code&gt; for &lt;code&gt;OAI-SearchBot&lt;/code&gt;, for &lt;code&gt;ChatGPT-User&lt;/code&gt;, or under &lt;code&gt;User-agent: *&lt;/code&gt;.&lt;br&gt;
According to OpenAI, sites that block OAI-SearchBot won't show up in ChatGPT's search answers.&lt;br&gt;
GPTBot (training) and OAI-SearchBot (search) are configured separately, so you can opt out of training and still let search in.&lt;/p&gt;

&lt;p&gt;Besides robots.txt, check that your CDN or server-side bot protection isn't blocking AI bots.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Launch once there's something to read
&lt;/h3&gt;

&lt;p&gt;On Site A, OAI-SearchBot started reading articles two days after they went up, and a person came the same day.&lt;br&gt;
On Site I, the domain existed from May, but as long as there were no articles, ChatGPT never used it in an answer.&lt;/p&gt;

&lt;p&gt;If you put a domain up with nothing but a "coming soon" page, ChatGPT just checks the door and leaves.&lt;br&gt;
Launch with your product pages plus a few dozen articles that answer your readers' questions, and you have a shot at being recommended in the first week.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Answer the questions people ask ChatGPT, one page per question
&lt;/h3&gt;

&lt;p&gt;Three out of four ChatGPT visitors landed on an article.&lt;br&gt;
And not product pitches: articles like "how to message just one person in a group chat" or "how to stop accepting responses on a Google Form", the kind of thing people ask when they're stuck.&lt;/p&gt;

&lt;p&gt;ChatGPT goes looking for a page that answers each question as it's asked.&lt;br&gt;
Turn each problem your product solves into an article phrased as the question, and each one becomes a way in.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Check it in your logs
&lt;/h3&gt;

&lt;p&gt;There are two places to see whether ChatGPT is sending people.&lt;/p&gt;

&lt;p&gt;The first is Google Analytics.&lt;br&gt;
Visitors who click a link in a ChatGPT answer usually show chatgpt.com as the referrer, and the landing URL often ends in &lt;code&gt;utm_source=chatgpt.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The second is your server access log.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;grep "OAI-SearchBot" access.log | wc -l
grep "ChatGPT-User" access.log | wc -l
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If ChatGPT-User is in your log, someone asked ChatGPT a question and ChatGPT came to read your site.&lt;br&gt;
If there's a hit with &lt;code&gt;utm_source=chatgpt.com&lt;/code&gt; right after it, that question sent you a person.&lt;/p&gt;

&lt;p&gt;Anyone can fake a user agent, so to know it's really OpenAI, check the IP against the ranges OpenAI publishes.&lt;br&gt;
The links to those lists are on the OpenAI crawler page above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;A new site takes a while to get noticed by Google and start getting search traffic.&lt;br&gt;
In the meantime, ChatGPT didn't wait for Google or Bing. It sent its own bot, started reading once there was something to read, and brought over the person who'd asked, right then.&lt;/p&gt;

&lt;p&gt;If you've just registered a domain, first check that robots.txt isn't blocking ChatGPT's bots.&lt;br&gt;
Then launch with pages that answer your readers' questions.&lt;br&gt;
While you wait on Google, ChatGPT can already be sending people.&lt;/p&gt;

</description>
      <category>chatgpt</category>
      <category>seo</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>What happened to my one-month-old site during Google's September 2026 spam update</title>
      <dc:creator>Hisashi Space</dc:creator>
      <pubDate>Sun, 11 Oct 2026 01:37:40 +0000</pubDate>
      <link>https://dev.to/hisashispace/what-happened-to-my-one-month-old-site-during-googles-september-2026-spam-update-2347</link>
      <guid>https://dev.to/hisashispace/what-happened-to-my-one-month-old-site-during-googles-september-2026-spam-update-2347</guid>
      <description>&lt;p&gt;I run a bit over ten websites. During Google's September 2026 spam update, which started on September 24, one of them basically disappeared from search. Search impressions went from a few hundred a day to almost nothing on October 3.&lt;/p&gt;

&lt;p&gt;I'm writing this up with the Search Console screens and the numbers I have, partly to keep a record and partly because I would really like to hear from anyone who's been through something similar or has a guess about what's going on. I am leaving the site's name out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;It is the marketing site for a B2B SaaS product. Here's the Performance report for the last three months, daily:&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%2Fli9yzcb40lhse8g1dblr.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%2Fli9yzcb40lhse8g1dblr.png" alt="Search Console performance report. Impressions start around Sept 7 and fall to almost zero on Oct 3" width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Daily impressions on the article pages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sept 7 to Sept 10: a big spike right after the first posts went live. Peak was 740 impressions on Sept 9, across 159 articles&lt;/li&gt;
&lt;li&gt;Sept 11 to Sept 12: dropped about 80%, then came back within roughly ten days&lt;/li&gt;
&lt;li&gt;Sept 23 to Oct 2: back up to around 480 a day, with up to 171 articles getting impressions&lt;/li&gt;
&lt;li&gt;From Oct 3: about 10 a day&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the whole site (home, pricing, comparison pages and articles), it went from 364 impressions a day (Sept 20 to Oct 2) to 12 a day (Oct 4 to Oct 7). Every page type dropped on the same day, not just the blog.&lt;/p&gt;

&lt;p&gt;Google's status dashboard lists the September 2026 spam update as starting Sept 24 and taking 13 days and 16 hours to roll out, so Oct 3 falls inside that window.&lt;/p&gt;

&lt;h2&gt;
  
  
  The site
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Domain registered: Aug 28, 2026&lt;/li&gt;
&lt;li&gt;First articles: Sept 2 (Japanese), Sept 16 (English)&lt;/li&gt;
&lt;li&gt;Articles: 471 in Japanese, 249 in English, 720 in total, about 20 a day&lt;/li&gt;
&lt;li&gt;How topics were picked: up to Sept 21 I did not check search volume for the target keyword or look at what was already ranking. After that, every article had to pass a volume check (Google Ads Keyword Planner) and a look at the current top results before going out&lt;/li&gt;
&lt;li&gt;Other pages: home, features, pricing, comparisons with competitors&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Indexing
&lt;/h2&gt;

&lt;p&gt;As of Oct 4, 701 pages were indexed and 442 weren't.&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%2F3sp7gfyfr73vnoo2e45t.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%2F3sp7gfyfr73vnoo2e45t.png" alt="Page indexing report. 701 indexed, 442 not indexed" width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's why the 442 are not indexed:&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%2Fbgdcb7tz6s2yb2qj3tyt.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%2Fbgdcb7tz6s2yb2qj3tyt.png" alt="Reasons pages aren't indexed. Discovered - currently not indexed is the largest at 147" width="800" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The biggest bucket is "Discovered - currently not indexed" at 147. I ran URL Inspection on them and none had ever been crawled.&lt;/p&gt;

&lt;p&gt;What's interesting is that the paginated blog index pages (page 2, 3, 5, 10, 20, 30) were all crawled and indexed. So Google is seeing the links to those articles, it is just not bothering to fetch them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things I checked that look fine
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Manual actions: none&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1cazmy8jjvycyulq7nra.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%2F1cazmy8jjvycyulq7nra.png" alt="Manual actions: No issues detected" width="799" height="157"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Security issues: none&lt;/li&gt;
&lt;li&gt;robots.txt, canonicals, noindex, redirects: unchanged and doing what they should&lt;/li&gt;
&lt;li&gt;No code changes on the site around the drop&lt;/li&gt;
&lt;li&gt;Searching for the product name still brings the site up. What dropped is everything else&lt;/li&gt;
&lt;li&gt;Organic visits in Google Analytics fell to 0 to 3 a day on the same day, so it's not a reporting glitch&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Backlinks
&lt;/h2&gt;

&lt;p&gt;The site had 52 referring domains. 40 of them came from the syndication of a single English press release (mostly US local news sites).&lt;/p&gt;

&lt;h2&gt;
  
  
  What I've done so far
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Oct 9: noindexed 132 articles whose target keyword had zero monthly searches (checked one by one in Keyword Planner)&lt;/li&gt;
&lt;li&gt;Oct 9: added category index pages and changed related posts to the neighbors in the same category. Max clicks from the home page to any article went from 7 to 6&lt;/li&gt;
&lt;li&gt;Since Oct 9: stopped publishing new articles&lt;/li&gt;
&lt;li&gt;No mass rewrites. The plan is to wait and watch until around Oct 23&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Comparing with an older site's first months
&lt;/h2&gt;

&lt;p&gt;I also run a site that's been around for many years, and I added a blog to it in March 2026. Its first months looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First four weeks or so: impressions grew steadily, about 5 to 6 a day per article&lt;/li&gt;
&lt;li&gt;Next four weeks or so: more articles were getting impressions, but total impressions flatlined. Per article it dropped to around 4&lt;/li&gt;
&lt;li&gt;Then per-article impressions recovered and it started growing. The clear upturn in Google came about two and a half months in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From Sept 23, this site was in that same "more articles showing, fewer impressions each" phase (4.7 per article down to 2.9). The difference is the older site got through it in about six weeks, while this one sank after about ten days. The older site sits on a domain with a long history. This one is a month old.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd love to hear
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The paginated blog index pages get crawled, but the articles they link to have never been fetched. How should I read that?&lt;/li&gt;
&lt;li&gt;On Sept 11 the site also dropped about 80% and came back in around ten days. This time the drop is deeper (around 97%) and it lines up with the spam update window. Is it likely to come back the same way? If you've had a whole site sink in a spam update and recover, how long did it take?&lt;/li&gt;
&lt;li&gt;Besides noindexing the zero-volume articles, is there anything I should be doing now? Anything I should definitely not do?&lt;/li&gt;
&lt;li&gt;Could a batch of links from one press release's syndication have played a part?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If there's a number or a screen that would help, let me know and I'll add what I can.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>google</category>
      <category>webdev</category>
      <category>searchconsole</category>
    </item>
    <item>
      <title>My office AI server hit 88 degrees C, so I made the room cool itself.</title>
      <dc:creator>Hisashi Space</dc:creator>
      <pubDate>Sat, 10 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/hisashispace/my-office-ai-server-hit-88degc-so-i-made-the-room-cool-itself-4ap</link>
      <guid>https://dev.to/hisashispace/my-office-ai-server-hit-88degc-so-i-made-the-room-cool-itself-4ap</guid>
      <description>&lt;p&gt;I'm Hisashi. I've run my own company for 22 years, and from 2011 to 2023 I ran it while moving between countries. I keep a Mac in my Tokyo office running as an always-on AI server.&lt;/p&gt;

&lt;p&gt;This summer in Tokyo, my office Mac started running a multi-day job at 100% GPU, and the more interesting problem became heat. This post is about something every always-on home AI server eventually faces: it gets hot, and you're not there to watch it.&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%2Fgbm8iv19h6h68re3omnj.jpeg" 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%2Fgbm8iv19h6h68re3omnj.jpeg" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is fine in an air-conditioned room in spring. It is not fine in a Tokyo summer when I'm in another country and the office aircon is off. Two things can go wrong: the chip thermally throttles and my job quietly slows down, or it just runs hotter than I'd like for weeks on end. I had no way to see either, because I was never in the room.&lt;/p&gt;

&lt;p&gt;So I built two things: a dashboard that lets me watch the machine's temperature from anywhere, and an automation that turns the room's air conditioner on when the GPU gets too hot. Total cost: zero, plus a SwitchBot hub I already owned.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Reading the sensors without sudo
&lt;/h2&gt;

&lt;p&gt;The first obstacle on Apple Silicon is that the obvious tools need root. &lt;code&gt;powermetrics&lt;/code&gt; wants sudo, and I didn't want an automation that hangs forever on a password prompt while I'm on the other side of the planet. So I found a sudo-free source for each metric.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CPU usage&lt;/strong&gt;: &lt;code&gt;top -l 1 -n 0&lt;/code&gt;, then 100 minus the idle percentage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU usage&lt;/strong&gt;: &lt;code&gt;ioreg -r -d 1 -c IOAccelerator&lt;/code&gt;. It exposes a "Device Utilization %" field with no privileges required. This is how I confirmed the GPU really was pinned at 97 to 98%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temperatures&lt;/strong&gt;: &lt;code&gt;smctemp&lt;/code&gt;, a tiny open-source SMC reader you compile with the clang already on your Mac (no Homebrew needed). &lt;code&gt;-c&lt;/code&gt; gives CPU temperature, &lt;code&gt;-g&lt;/code&gt; gives GPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fan RPM&lt;/strong&gt;: the same SMC interface. There's no dedicated flag, but dumping all keys and reading &lt;code&gt;F0Ac&lt;/code&gt; and &lt;code&gt;F1Ac&lt;/code&gt; (fan 0 and fan 1, actual RPM) works. Mine read about 5,400 and 5,760, against a ceiling near 5,350. In other words, both fans were already flat out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 15-line shell script samples all of that once a minute and appends one JSON line to a file. CPU, GPU, both temperatures, both fan speeds, plus the current job's progress. Nothing heavy: the whole sample takes a fraction of a second of CPU and never touches the GPU, so it can't interfere with the work it's measuring.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. An MRTG-style dashboard I can open from anywhere
&lt;/h2&gt;

&lt;p&gt;Old-school sysadmins will remember MRTG: a web page of rolling time-series graphs you glance at to know your machine is healthy. That's exactly what I wanted, minus the RRDtool setup.&lt;/p&gt;

&lt;p&gt;The page is one static HTML file with Chart.js bundled locally (no CDN dependency, so it works even if the office internet hiccups). It fetches the JSON log, draws three graphs (usage, temperature, fan RPM), shows the latest values as cards, and refreshes every 60 seconds. Room temperature and the air-conditioner state sit in the corner.&lt;/p&gt;

&lt;p&gt;Serving it is one line: &lt;code&gt;python3 -m http.server&lt;/code&gt;. I run both the collector and the server as launchd jobs so they survive reboots and restart themselves if they die.&lt;/p&gt;

&lt;p&gt;The part that makes it genuinely useful from a Bangkok hotel is how I reach it. The machine is on my Tailscale network, so from any of my devices I just open:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;http://m1&lt;/code&gt; from anywhere in the world, over the Tailscale tailnet.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;http://m1.local&lt;/code&gt; when I'm on the office LAN.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are plain hostnames, no port number, no IP to remember. Tailscale's MagicDNS gives me the global name; mDNS gives me the local one. I bound the server to port 80 so neither needs a &lt;code&gt;:8765&lt;/code&gt; suffix.&lt;/p&gt;

&lt;p&gt;What I see when I open it: a row of live cards (CPU and GPU usage, CPU and GPU temperature, both fan RPMs, room temperature, and the current training step), sitting above three rolling graphs (usage, temperature, fan speed). The temperature cards turn amber as they climb, so a glance from my phone tells me whether the machine is comfortable or cooking. Everything updates every 60 seconds.&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%2Fl1g47d3kxyraqq6fhn8j.jpeg" 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%2Fl1g47d3kxyraqq6fhn8j.jpeg" width="800" height="746"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Watching the graphs for an evening settled a question I'd assumed I'd have to guess at: was the machine throttling?&lt;/p&gt;

&lt;p&gt;It wasn't, yet. Job step time stayed flat while the GPU held 86 to 88°C, which means the chip was holding its clocks. Good. But the fans told the real story: they were already maxed. The cooling system had no headroom left. The only lever I had not pulled was the temperature of the air going into the machine.&lt;/p&gt;

&lt;p&gt;That reframed the whole problem. I couldn't make the Mac cool itself harder. I could only make the room cooler. And the room has an air conditioner.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Giving the room a thermostat
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fombgsmrp0xksqct20kji.jpeg" 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%2Fombgsmrp0xksqct20kji.jpeg" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The details are where you avoid an automation that drives you crazy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hysteresis.&lt;/strong&gt; Cooling turns on when the GPU stays at or above 92°C for three straight minutes, and off only when it drops to 84°C or below. The 8-degree gap stops it flapping on and off around a single threshold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A rate limit.&lt;/strong&gt; No more than one action every ten minutes, so it can't machine-gun the compressor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It only undoes its own actions.&lt;/strong&gt; The controller turns the aircon off only if it was the one that turned it on. If I switched it on myself for comfort, it leaves it alone. Infrared is fire-and-forget with no state feedback, so the controller tracks what it last commanded rather than pretending to know the real state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A floor.&lt;/strong&gt; It reads room temperature from a SwitchBot sensor and refuses to start cooling if the room is already below 24°C, so it never overcools an empty office.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It runs as one more launchd job on the same one-minute cadence, reading the same temperature log the dashboard uses. Default-off until I explicitly enabled it, so there was never a chance of it firing by surprise during testing.&lt;/p&gt;

&lt;p&gt;Right now, with the room at 26°C and the GPU at 88°C, it correctly sits idle. It's waiting for the day the Tokyo heat pushes the chip past 92, at which point the office will cool itself while I'm somewhere else entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. One mistake worth copying down
&lt;/h2&gt;

&lt;p&gt;When I first stood the dashboard up, I served the whole working directory. That directory also held the file with my SwitchBot API credentials. For about fifteen minutes, anyone on my private network could have fetched &lt;code&gt;http://m1/.env&lt;/code&gt; and read my tokens.&lt;/p&gt;

&lt;p&gt;It was tailnet-only, so the blast radius was just my own devices, but the lesson generalizes: a static file server serves &lt;em&gt;everything&lt;/em&gt; in the folder you point it at. Put only the public files (the HTML, the graph data) in the served directory, and keep secrets and scripts one level up where the server can't reach them. I moved them, confirmed the credentials now return 404, and rotated nothing because the exposure never left my own machines, but it was a clean reminder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;An always-on AI server is a small piece of infrastructure, and infrastructure you can't see is infrastructure you'll eventually lose. Two cheap moves fix that: a one-minute sampler feeding a dashboard you can open by name from anywhere, and a temperature-driven rule that lets the room take care of the machine when you're not there.&lt;/p&gt;

&lt;p&gt;The Mac in my Tokyo office now watches its own temperature, tells me about it over my tailnet, and cools its own room when the summer gets serious. I don't have to be in the office to keep it safe.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>applesilicon</category>
      <category>monitoring</category>
      <category>homelab</category>
    </item>
    <item>
      <title>SshResident at 180% CPU: why my ScanSnap iX2500 took 14 seconds to start, and the fix that wasn't code</title>
      <dc:creator>Hisashi Space</dc:creator>
      <pubDate>Fri, 09 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/hisashispace/sshresident-at-180-cpu-why-my-scansnap-ix2500-took-14-seconds-to-start-and-the-fix-that-wasnt-2nf4</link>
      <guid>https://dev.to/hisashispace/sshresident-at-180-cpu-why-my-scansnap-ix2500-took-14-seconds-to-start-and-the-fix-that-wasnt-2nf4</guid>
      <description>&lt;p&gt;I'm Hisashi. I've run my own company for 22 years, and from 2011 to 2023 I ran it while moving between countries. Running a paperless company means I scan a lot of documents, and my main scanner had developed an annoying tic: I would press Scan, and nothing would happen. The scan button just blinked, ten to fourteen times, before the rollers finally started pulling paper.&lt;/p&gt;

&lt;p&gt;The scanner is a ScanSnap iX2500, rated at 45 pages per minute. The reading speed was never the problem. The problem was the several-second stall before a single sheet moved, and my first instinct was the usual suspect: the connection. So I switched it from WiFi to USB. Exactly the same delay. Then I switched the save destination from the pop-up menu style to a fixed folder profile. Same delay. When two things you assumed were the cause turn out to change nothing, you are looking in the wrong layer.&lt;/p&gt;

&lt;p&gt;The delay wasn't in the scanner, and it wasn't on the wire. It was the Mac, doing something absurd, on every single scan.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;Press Scan, watch the button blink, wait. Worse, when I scanned twice in a row, the second scan was slower than the first. That detail matters. A slow-but-constant delay smells like a fixed handshake. A delay that grows when you repeat the operation smells like contention, something piling up and not clearing.&lt;/p&gt;

&lt;p&gt;USB versus WiFi made no difference. Quick-menu versus folder profile made no difference. So the bottleneck was not data transfer and not the scanner firmware. It was something the Mac ran between "button pressed" and "paper moving," and whatever it was, it got heavier the more I used it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule one: name the process
&lt;/h2&gt;

&lt;p&gt;Same reflex as always. Before touching anything, find the process. I streamed the unified log filtered to the ScanSnap helper processes and sampled CPU while I triggered a scan.&lt;/p&gt;

&lt;p&gt;One process lit up the instant I pressed the button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PID    %CPU  COMMAND
3798   186.6 SshResident
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SshResident&lt;/code&gt; is the resident background app that ScanSnap Home keeps running to watch the scanner. It should sit near idle. During the blink, it was pinned above 100%, peaking near 186%, meaning it had more than one full core to itself for the entire stall. When the CPU dropped back to idle, the paper started moving. The blink and the CPU spike were the same event.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cause: enumerating every app, hundreds of times
&lt;/h2&gt;

&lt;p&gt;CPU percentage tells you a process is busy. It doesn't tell you what it's doing. For that, macOS ships &lt;code&gt;sample&lt;/code&gt;, which snapshots a running process's call stacks. I sampled &lt;code&gt;SshResident&lt;/code&gt; three times during the stall. All three came back with nearly all of the CPU time in the same call chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SSUIController&lt;/span&gt; &lt;span class="nf"&gt;doScanClick&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
  &lt;span class="n"&gt;isAppLaunchedByBundleID&lt;/span&gt;
    &lt;span class="n"&gt;getLaunchAppProcessIDBySignature&lt;/span&gt;
      &lt;span class="n"&gt;GetProcessInformation&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HIServices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="n"&gt;Carbon&lt;/span&gt; &lt;span class="n"&gt;Process&lt;/span&gt; &lt;span class="n"&gt;Manager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;_LSCopyApplicationInformation&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LaunchServices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what that stack means. When you press Scan, &lt;code&gt;SshResident&lt;/code&gt; needs to launch a helper app to handle the scan, and before it proceeds it polls to check whether that helper has finished launching. The way it checks is &lt;code&gt;GetProcessInformation&lt;/code&gt;, an ancient Process Manager call that does not ask about one specific app. It walks the entire list of running applications that LaunchServices is tracking and builds an information record for each one, just to find the single process it cares about. Then it does that again on the next poll. And again, hundreds of times, until the helper is up.&lt;/p&gt;

&lt;p&gt;The cost of one sweep is proportional to how many apps LaunchServices is tracking. On a normal Mac that is maybe thirty, and nobody ever notices this code. My Mac is a working machine for eleven businesses: browsers with dozens of tabs, editors, local dev servers, MCP servers, recording tools. At the moment I measured, LaunchServices was tracking about 380 applications, and the whole machine was running roughly 1,800 processes. So every poll walked 380 apps, and the launch wait fired hundreds of polls, and that product is a full core pinned for ten to fourteen seconds. The second scan was slower because it landed while the first scan's helper teardown was still churning the same list.&lt;/p&gt;

&lt;p&gt;The delay was never the scanner's limit. It was an O(number of apps) loop in the vendor's software, running head-on into a machine that runs an unusual number of apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The clever fix that backfired
&lt;/h2&gt;

&lt;p&gt;Once you know it's &lt;code&gt;GetProcessInformation&lt;/code&gt; being called in a hot poll loop, the engineer's temptation is obvious: cache it. Intercept that one function, remember each process's answer for a second or so, and let the hundreds of repeated polls hit the cache instead of rebuilding a 380-app list every time.&lt;/p&gt;

&lt;p&gt;You can inject code into another process on macOS with &lt;code&gt;DYLD_INSERT_LIBRARIES&lt;/code&gt;, and normally a hardened, notarized, signed system binary would refuse it outright. This one couldn't refuse. Its own code signature carried two entitlements, &lt;code&gt;allow-dyld-environment-variables&lt;/code&gt; and &lt;code&gt;disable-library-validation&lt;/code&gt;, precisely the two that let an unsigned or ad-hoc dylib load into it. The vendor had left the door open, presumably for its own plug-in machinery. I wrote a small interposing library that cached &lt;code&gt;GetProcessInformation&lt;/code&gt; per process with a short expiry, ad-hoc signed it, added the injection to the helper's &lt;code&gt;Info.plist&lt;/code&gt;, and re-signed the bundle while preserving its entitlements.&lt;/p&gt;

&lt;p&gt;The first version crashed the daemon on launch. The root cause is a nice trap. I had hand-declared the old &lt;code&gt;ProcessInfoRec&lt;/code&gt; struct from memory. The real 64-bit layout is not what you'd guess: it is packed to two-byte alignment, it is 72 bytes, its first pointer sits at offset 4 rather than 8, and the &lt;code&gt;processAppSpec&lt;/code&gt; field that exists in every online reference was dropped entirely in the 64-bit version because it was built on the 32-bit-only &lt;code&gt;FSSpec&lt;/code&gt; type. My natural-alignment struct put every field at the wrong offset, and my copy of a 264-byte buffer into what the caller had sized as a 70-byte one smashed its stack. I rebuilt the interposer against the SDK's real types, verified it in a standalone harness that mimicked the daemon's exact call pattern, confirmed it was safe and cut real calls by 80%, and redeployed.&lt;/p&gt;

&lt;p&gt;And scanning stopped working entirely.&lt;/p&gt;

&lt;p&gt;This is the part worth sitting with. The library was now provably correct in isolation and provably fast. But by caching the answer to "has the helper launched yet," I had blinded the daemon to the one state change it was actually polling for. It waited forever for a launch it could no longer observe. My harness reproduced the call pattern perfectly and the state transition not at all, which is exactly the class of bug a harness cannot catch. Every failed attempt left the scanner dead until I restored the original from backup, and power-cycling a scanner mid-workflow to recover from your own patch is a strong signal you are solving the wrong problem. I reverted the whole thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix that wasn't code
&lt;/h2&gt;

&lt;p&gt;Step back far enough and the real question changes. I had been trying to make the slow path fast. The better move was to delete the slow path.&lt;/p&gt;

&lt;p&gt;The delay lives entirely in the Mac-side software that orchestrates the scan. So don't let the Mac orchestrate the scan. The iX2500 has a touchscreen and, as of a 2025 firmware feature, it can save directly to a network folder over SMB with no PC involved at scan time. The scanner writes the file itself. &lt;code&gt;SshResident&lt;/code&gt; is never in the loop, because ScanSnap Home is never in the loop.&lt;/p&gt;

&lt;p&gt;The setup is boring, which is the point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Turn on File Sharing on the Mac and export the destination folder as an SMB share.&lt;/li&gt;
&lt;li&gt;Give the Mac a fixed IP so the scanner's saved destination never breaks. I put the wired connection (a CalDigit TS4 dock) on a static &lt;code&gt;192.168.1.10&lt;/code&gt;, low in the range to stay clear of the DHCP pool.&lt;/li&gt;
&lt;li&gt;On the scanner's touchscreen, register a network-folder profile pointing at &lt;code&gt;\\192.168.1.10\ScanData&lt;/code&gt; with the Mac account's credentials.&lt;/li&gt;
&lt;li&gt;Point the scanner's clock at an NTP server, because SMB authentication and file timestamps both care about time. The home router (a GL.iNet Flint 3 at &lt;code&gt;192.168.1.1&lt;/code&gt;) already answers NTP on the LAN, so that one line needs no internet at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then you pick the profile on the touchscreen and scan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Time from pressing scan to paper moving: a couple of seconds, consistently&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SshResident&lt;/code&gt; in the scan path: gone, along with the 380-app enumeration&lt;/li&gt;
&lt;li&gt;Files land straight in a local folder on the Mac, named by timestamp, ready for whatever processing I run next&lt;/li&gt;
&lt;li&gt;Mac-side scanning software touched during a scan: none&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;When the fix you assumed doesn't change the symptom, stop testing that layer. USB versus WiFi changing nothing is what told me the scanner and the wire were both innocent.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sample&lt;/code&gt; is the tool that turns "this process is busy" into "this process is calling this exact function in a loop." CPU percentage is a smoke alarm; the call stack is the fire.&lt;/li&gt;
&lt;li&gt;A delay that gets worse when you repeat the operation is contention, not a fixed cost. Chase the thing that accumulates.&lt;/li&gt;
&lt;li&gt;Injecting into a signed system binary is sometimes possible, and rarely wise. When a vendor daemon carries &lt;code&gt;allow-dyld-environment-variables&lt;/code&gt; and &lt;code&gt;disable-library-validation&lt;/code&gt;, the OS will let you in, but you are now maintaining a patch against software that fights back on every update, and a harness that reproduces the calls will still miss the state machine.&lt;/li&gt;
&lt;li&gt;The strongest fix is often the one that removes your code from the path instead of adding more. The scanner could already do the whole job by itself. I just had to stop insisting the Mac be in the middle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The meta-lesson is the same one that I keep coming back to. Depend on a machine and you owe it root-cause fixes, not rituals. Sometimes the root cause is that you were using the wrong tool for the job, and the cure is to use less software, not more.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>debugging</category>
      <category>performance</category>
      <category>hardware</category>
    </item>
    <item>
      <title>coreaudiod at 112% CPU: the sample-rate mismatch that froze Zoom on my Mac</title>
      <dc:creator>Hisashi Space</dc:creator>
      <pubDate>Thu, 08 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/hisashispace/coreaudiod-at-112-cpu-the-sample-rate-mismatch-that-froze-zoom-on-my-mac-27g9</link>
      <guid>https://dev.to/hisashispace/coreaudiod-at-112-cpu-the-sample-rate-mismatch-that-froze-zoom-on-my-mac-27g9</guid>
      <description>&lt;p&gt;I'm Hisashi. I've run my own company for 22 years, and from 2011 to 2023 I ran it while moving between countries. My work means a lot of Zoom calls, and it also means my main Mac is permanently mid-recording, mid-stream, or mid-long-running-job. Rebooting is never an option I accept without a fight.&lt;/p&gt;

&lt;p&gt;So when Zoom started freezing on launch (spinning, audio dead, the whole machine feeling sticky), I did what I always do: found the actual culprit instead of power-cycling my way past it. The culprit turned out to be embarrassingly small. One audio device was set to 44.1kHz while everything else on the system ran at 48kHz. That single mismatch had macOS's audio daemon burning a full CPU core, continuously, for weeks.&lt;/p&gt;

&lt;p&gt;If your Mac's audio feels haunted (Zoom slow to join, recording apps erroring out, UI hitching when sound plays), there's a decent chance you have the same disease. Here's the full diagnosis and the fix. Total time: about 15 minutes. Reboots required: zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;Clicking a Zoom link did nothing for a long time. The meeting window eventually appeared, but audio took even longer, and sometimes the app just sat there. Meanwhile the machine felt subtly wrong: keystrokes lagging, fans occasionally spinning up with nothing visible running.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;top&lt;/code&gt; told the real story:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PID    %CPU  COMMAND
620    112.3 coreaudiod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;coreaudiod&lt;/code&gt; is the macOS audio daemon (the process behind Core Audio; every app that plays or records sound talks to it). It should idle near 0%. Mine was pinned above 100%, meaning it had more than one full core to itself.&lt;/p&gt;

&lt;p&gt;One more detail made it clear this wasn't a fresh problem. The process had accumulated over 39 hours of CPU time. This thing had been quietly cooking for weeks, and Zoom's launch sequence just made it visible, because Zoom enumerates and opens audio devices aggressively at startup, right into the teeth of a daemon that was already drowning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule one: don't reboot
&lt;/h2&gt;

&lt;p&gt;A reboot would have "fixed" this. It also would have destroyed the diagnosis, killed everything else the machine was doing, and guaranteed the problem came back next week.&lt;/p&gt;

&lt;p&gt;macOS lets you restart a single system daemon:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;killall coreaudiod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;launchd respawns it instantly. Audio drops for two or three seconds, then reconnects. That alone brought CPU back to normal. But that's treatment, not cure. If I stopped there, the daemon would start melting again the next time the trigger condition reappeared. So the real question: what makes coreaudiod spin?&lt;/p&gt;

&lt;h2&gt;
  
  
  The cause: one device at 44.1kHz
&lt;/h2&gt;

&lt;p&gt;I'd seen this failure mode once before, with a Yamaha AG06MK2 mixer. The mixer was set to 44.1kHz, the Mac wanted 48kHz, and coreaudiod ground itself into paste doing sample-rate conversion on every audio callback. Unifying everything at 48kHz cured it permanently.&lt;/p&gt;

&lt;p&gt;The AG06 wasn't even connected this time. But the pattern generalizes, so I dumped the nominal sample rate of every device on the system. You can do this in Audio MIDI Setup (the GUI way), but I wanted it scriptable, so I asked Core Audio directly. The check is a dozen lines of Swift using &lt;code&gt;kAudioDevicePropertyNominalSampleRate&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook Pro Microphone:  48000 Hz
MacBook Pro Speakers:    44100 Hz   &amp;lt;-- there it is
Microsoft Teams Audio:   48000 Hz
SWB Audio Capture:       48000 Hz
LoomAudioDevice:         48000 Hz
ZoomAudioDevice:         48000 Hz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The built-in speakers, of all things, had drifted to 44.1kHz. Everything else, including every virtual device, sat at 48kHz. Zoom negotiates 48kHz for calls.&lt;/p&gt;

&lt;p&gt;Here's why that one line is expensive. When rates disagree, coreaudiod has to resample audio in real time between the mismatched clock domains, per stream, forever. Worse, apps that open devices at their preferred rate trigger renegotiation, and with a conferencing app in the mix you get a steady churn of format changes. Each one is cheap; a continuous stream of them, multiplied across every virtual device that mirrors system audio, is a full CPU core.&lt;/p&gt;

&lt;p&gt;The fix is one property write (same API, &lt;code&gt;AudioObjectSetPropertyData&lt;/code&gt; with 48000.0), or thirty seconds of clicking in Audio MIDI Setup. All devices at 48kHz, mismatch gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The aggravator: seven resident virtual audio drivers
&lt;/h2&gt;

&lt;p&gt;While diagnosing, I looked at &lt;code&gt;/Library/Audio/Plug-Ins/HAL/&lt;/code&gt;, the folder where apps install virtual audio devices. These are fake sound cards that apps use to capture or route system audio, and every one of them is a permanent client of coreaudiod, whether or not the app that installed it is running.&lt;/p&gt;

&lt;p&gt;I had seven:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ARK.driver
LoomAudioDevice.driver
MSTeamsAudioDevice.driver
ParrotAudioPlugin.driver
SWBAudioCapturePlugIn.driver
TVRemoteAudio.driver
ZoomAudioDevice.driver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven virtual sound cards, resident 24/7. Each multiplies the cost of every device enumeration (which Zoom does at launch) and every format renegotiation (which the 44.1k mismatch was causing constantly). The mismatch lit the fire; this pile of drivers was the accelerant.&lt;/p&gt;

&lt;p&gt;Some of these names tell you nothing, and this folder is a common hiding spot for abandoned software. Don't guess; verify. &lt;code&gt;codesign&lt;/code&gt; tells you who actually shipped each one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codesign &lt;span class="nt"&gt;-dv&lt;/span&gt; &lt;span class="nt"&gt;--verbose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2 /Library/Audio/Plug-Ins/HAL/TVRemoteAudio.driver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The census results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ARK.driver&lt;/strong&gt;: Rogue Amoeba (the audio engine behind Loopback, Audio Hijack, SoundSource). Keeping it; I use their tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LoomAudioDevice / ZoomAudioDevice / SWBAudioCapture&lt;/strong&gt;: screen-recording and conferencing tools I actually use. Kept.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ParrotAudioPlugin.driver&lt;/strong&gt;: this one looked suspicious (unfamiliar name, sitting in a third-party folder), but the signature chain is Apple's own OS platform signing, not a Developer ID. It's a genuine macOS system component. Leave it alone; it would come back with the next OS update anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TVRemoteAudio.driver&lt;/strong&gt;: TeamViewer's remote-audio driver. I use TeamViewer occasionally but never its audio streaming. Dead weight. Removed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MSTeamsAudioDevice.driver&lt;/strong&gt;: Microsoft Teams. I only ever join Teams calls when someone else hosts one, and the browser version handles mic, camera, and screen share fine without installing anything. So I uninstalled the desktop app entirely. Note that uninstalling Teams leaves this driver behind; you have to delete it from the HAL folder yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two drivers gone (backed up first, always), then one more &lt;code&gt;sudo killall coreaudiod&lt;/code&gt; to make the daemon reload its plugin list. Down to five residents, all accounted for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;coreaudiod: 112% CPU before, 0.4% after, and it stays there&lt;/li&gt;
&lt;li&gt;Zoom: clicking a meeting link now puts the window on screen in about 2 seconds&lt;/li&gt;
&lt;li&gt;Loom and other recording tools: device enumeration is lighter, so startup errors from enumeration timeouts should drop too&lt;/li&gt;
&lt;li&gt;Reboots: zero&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;When a Mac feels sick, name the process before you touch anything. &lt;code&gt;top -o cpu&lt;/code&gt; costs nothing. "Reboot it" is a diagnosis you pay for later.&lt;/li&gt;
&lt;li&gt;Audio weirdness on macOS (conferencing apps slow to join, recording apps failing, hitching when sound plays) should trigger one reflex: check that every device in Audio MIDI Setup shows the same sample rate. 48kHz is the sane default; one device at 44.1kHz can eat a core.&lt;/li&gt;
&lt;li&gt;System daemons can be restarted individually with &lt;code&gt;sudo killall &amp;lt;daemon&amp;gt;&lt;/code&gt;. launchd brings them back in seconds. This solves a large class of "haunted Mac" problems without a reboot.&lt;/li&gt;
&lt;li&gt;Audit &lt;code&gt;/Library/Audio/Plug-Ins/HAL/&lt;/code&gt; once in a while. Every app you've ever installed for calls or screen recording may have left a virtual sound card there, and they all tax the audio daemon forever. &lt;code&gt;codesign -dv&lt;/code&gt; identifies the vendor when the name doesn't.&lt;/li&gt;
&lt;li&gt;The accumulated CPU time column in &lt;code&gt;ps&lt;/code&gt; is underrated. 39 hours on a daemon that should idle told me this was chronic, not acute, before I knew anything else.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The meta-lesson is the same one that I keep coming back to. A machine you depend on deserves root-cause fixes, not rituals. The reboot ritual works just often enough to stop people from ever learning what was actually wrong.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>debugging</category>
      <category>audio</category>
      <category>zoom</category>
    </item>
    <item>
      <title>Mac keyboard not working? Cmd+Tab dead and input switching broken? Check for a password field hidden in a background app</title>
      <dc:creator>Hisashi Space</dc:creator>
      <pubDate>Wed, 07 Oct 2026 05:14:07 +0000</pubDate>
      <link>https://dev.to/hisashispace/mac-keyboard-not-working-cmdtab-dead-and-input-switching-broken-check-for-a-password-field-2g7i</link>
      <guid>https://dev.to/hisashispace/mac-keyboard-not-working-cmdtab-dead-and-input-switching-broken-check-for-a-password-field-2g7i</guid>
      <description>&lt;p&gt;One morning, three things happened on my Mac at the same time:&lt;/p&gt;

&lt;p&gt;・Cmd+Tab would not switch apps&lt;br&gt;
・Switching between Japanese and English input behaved strangely&lt;br&gt;
・The mouse and the other keys worked normally&lt;/p&gt;

&lt;p&gt;A reboot might have fixed it.&lt;br&gt;
But my Mac is always recording or running something long, so I don't reboot lightly.&lt;br&gt;
When I tracked it down, the cause was a password field in an app hidden in the background.&lt;/p&gt;
&lt;h2&gt;
  
  
  The cause: macOS Secure Input
&lt;/h2&gt;

&lt;p&gt;macOS has a feature called Secure Input.&lt;br&gt;
While the cursor is in a password field, other apps are not allowed to read keystrokes.&lt;br&gt;
It exists so nothing can snoop on your password, and that's the right call.&lt;/p&gt;

&lt;p&gt;The catch is that it applies to the whole Mac.&lt;br&gt;
While Secure Input is on, apps that work by reading keystrokes (Cmd+Tab switchers, input method helpers, key remappers) stop receiving keys.&lt;br&gt;
They still see modifier keys like Cmd and Shift, but not the actual key presses.&lt;/p&gt;

&lt;p&gt;And if the cursor stays in a password field, Secure Input stays on, even when that app's window is hidden behind others.&lt;/p&gt;
&lt;h2&gt;
  
  
  What happened on my Mac
&lt;/h2&gt;

&lt;p&gt;The first time, it was a browser password manager extension.&lt;br&gt;
The password manager's Safari extension turned Secure Input on and failed to turn it off, and Cmd+Tab died.&lt;br&gt;
Quitting Safari didn't fix it. Locking and unlocking the screen didn't either.&lt;/p&gt;

&lt;p&gt;The second time, it was a browser window I was driving with automation.&lt;br&gt;
A browser running in the background got stuck on a login page with the cursor in the password field.&lt;br&gt;
The window was off screen, so I couldn't see it.&lt;br&gt;
Cmd+Tab and input switching stayed broken from the middle of the night until morning.&lt;/p&gt;
&lt;h2&gt;
  
  
  Check whether Secure Input is on
&lt;/h2&gt;

&lt;p&gt;Run this in Terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ioreg &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; 0 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; SecureInput
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the &lt;code&gt;kCGSSessionSecureInputPID&lt;/code&gt; line shows a number other than 0, Secure Input is on.&lt;br&gt;
If nothing comes back, or the number is 0, it's off.&lt;/p&gt;
&lt;h2&gt;
  
  
  The trap: the app it points to may not be the culprit
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;kCGSSessionSecureInputPID&lt;/code&gt; shows a process ID.&lt;br&gt;
It's tempting to read it as "this app is the culprit". That's the trap.&lt;/p&gt;

&lt;p&gt;The number is only the process that last turned Secure Input on.&lt;br&gt;
It isn't necessarily the app holding it now, and it can even be the ID of an app that has already quit.&lt;/p&gt;

&lt;p&gt;The second time, the ID pointed to an unrelated app.&lt;br&gt;
Restarting that app did nothing.&lt;br&gt;
The real culprit was the password field in that off screen browser window.&lt;/p&gt;
&lt;h2&gt;
  
  
  How to find the real culprit
&lt;/h2&gt;

&lt;p&gt;Instead of trusting the number, check these in order:&lt;/p&gt;

&lt;p&gt;・Is any open app sitting with its cursor in a password field? Include windows that are behind others or off screen&lt;br&gt;
・Is a password manager running, including browser extensions?&lt;br&gt;
・Is a browser automation tool stuck on a login page?&lt;/p&gt;

&lt;p&gt;With macOS Accessibility, you can check which element each app currently has focused, and whether it's a password field, without moving the cursor.&lt;br&gt;
That's how I found the password field in the off screen window.&lt;/p&gt;
&lt;h2&gt;
  
  
  How to fix it
&lt;/h2&gt;

&lt;p&gt;Once you know the culprit, you don't need to reboot.&lt;/p&gt;

&lt;p&gt;・If an app has its cursor in a password field, move the cursor out of it or close that window&lt;br&gt;
・If it's a password manager extension, quit just that extension's process. You don't have to quit the browser&lt;/p&gt;

&lt;p&gt;The first time, quitting only the Safari extension's process turned Secure Input off on the spot, and Cmd+Tab came back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# For the Bitwarden Safari extension (it starts again on its own when needed)&lt;/span&gt;
pkill &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"Bitwarden.app/Contents/PlugIns/safari.appex"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  If you automate a browser
&lt;/h2&gt;

&lt;p&gt;When you drive a browser with automation, a job can stop on a login page and leave the cursor in a password field.&lt;br&gt;
Even when you can't see the window, it affects keyboard input for the whole Mac.&lt;/p&gt;

&lt;p&gt;On my setup, automation now always moves the cursor out of password fields when it finishes.&lt;br&gt;
And when the automation hits a login page, it leaves the field alone and hands it to a person.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;・If Cmd+Tab or input switching suddenly stops working, suspect Secure Input first&lt;br&gt;
・Confirm it with &lt;code&gt;ioreg -l -w 0 | grep -i SecureInput&lt;/code&gt;&lt;br&gt;
・The process ID it shows may not be the culprit&lt;br&gt;
・Look for password fields in hidden or off screen windows, password manager extensions, and automated browsers&lt;br&gt;
・Once you find the culprit, you can fix it without a reboot&lt;/p&gt;

</description>
      <category>macos</category>
      <category>mac</category>
      <category>troubleshooting</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I control 10+ browsers with AI agents from one terminal. Here is how</title>
      <dc:creator>Hisashi Space</dc:creator>
      <pubDate>Sun, 04 Oct 2026 07:47:08 +0000</pubDate>
      <link>https://dev.to/hisashispace/i-control-10-browsers-with-ai-agents-from-one-terminal-here-is-how-5ek0</link>
      <guid>https://dev.to/hisashispace/i-control-10-browsers-with-ai-agents-from-one-terminal-here-is-how-5ek0</guid>
      <description>&lt;p&gt;I run my business alone, and a lot of my day is browser work. Checking payouts in Stripe. Pulling rankings from Search Console. Filling in forms on directories and launch sites. Posting to a dozen places.&lt;/p&gt;

&lt;p&gt;These days I hand most of that to Claude Code. The hard part was never the AI. It was the browser: how do you let an AI agent use a website you're already logged in to, without it getting in your way or getting you flagged?&lt;/p&gt;

&lt;p&gt;You've probably seen demos like "I had an AI agent order Uber Eats through the browser." To me, that misses the point. I can order food myself, faster. One task done by an agent instead of me saves almost nothing.&lt;/p&gt;

&lt;p&gt;What matters is how many jobs can run in parallel while I do something else. That's the only reason I want an agent in the browser at all.&lt;/p&gt;

&lt;p&gt;I tried the usual ways first. Each one broke in a way I didn't expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Claude's Chrome extension
&lt;/h2&gt;

&lt;p&gt;The easy answer is Claude's own extension for Chrome. My logins are already there, so the agent can start right away.&lt;/p&gt;

&lt;p&gt;But with the extension, I give instructions browser by browser. That's fine for one task. It's not what I wanted for running several jobs side by side.&lt;/p&gt;

&lt;p&gt;I wanted to control everything from the terminal. One place where I ask for the work, and the agents do it in whichever services they need, at the same time.&lt;/p&gt;

&lt;p&gt;Two more things bothered me:&lt;/p&gt;

&lt;p&gt;・I was handing over the whole browser. Every tab, every account, every service. There's no way to say "only Stripe".&lt;br&gt;
・It once connected to a different browser than the one I thought, then asked me to sign in to a service I was already signed in to. Everything looked fine on my side, so it took a while to figure out why.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Browser automation tools with a separate browser
&lt;/h2&gt;

&lt;p&gt;This is how most AI agent browser automation works: start Chrome with special settings and connect to it.&lt;/p&gt;

&lt;p&gt;What I ran into:&lt;/p&gt;

&lt;p&gt;・It's a fresh browser, so there are no logins. Sign in again, type the two-factor code again, for every service.&lt;br&gt;
・Sites notice. One startup directory returned "Bot access denied" when I submitted through an automation tool. After that, even my own clicks on the Submit button stopped going through.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Letting it watch the screen
&lt;/h2&gt;

&lt;p&gt;The agent takes a screenshot, picks a spot, clicks and types. It can drive anything.&lt;/p&gt;

&lt;p&gt;But it drives the screen I'm looking at. While it works, my mouse and keyboard belong to it, and there is only one screen, so there's no second task running beside the first.&lt;/p&gt;

&lt;p&gt;That's the part that bothered me most. The whole point of handing work to an agent is that I keep working. If the agent takes my focus while it works, I'm just sitting and watching. From an efficiency point of view, that defeats the purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually wanted
&lt;/h2&gt;

&lt;p&gt;After all that, my list was short:&lt;/p&gt;

&lt;p&gt;・I sign in myself. The agent never sees a password.&lt;br&gt;
・I give the agent one service, not my whole browser.&lt;br&gt;
・Nothing jumps in front of what I'm doing.&lt;br&gt;
・Several jobs can run at the same time.&lt;/p&gt;

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

&lt;p&gt;So I built Kagemusha. It turns any website into its own Mac app. A Stripe app, a Search Console app, a GitHub app. Each one is a separate browser with its own sign-in.&lt;/p&gt;

&lt;p&gt;I sign in to each app once, the normal way, with my password manager or a passkey. Then, app by app, I decide whether an AI agent is allowed to operate it. A new app starts with that switched off.&lt;/p&gt;

&lt;p&gt;When the agent works, it works inside that one app, in the background. My screen stays mine. I can show the window if I want to watch, or keep it hidden.&lt;/p&gt;

&lt;p&gt;Getting this part right took a lot of trial and error. Windows kept coming to the front while I was typing. Even bringing one forward for a moment took my keyboard for almost three seconds, and my keystrokes went into the wrong place. I kept going until the agent could open, read and fill in pages without ever stepping in front of what I was doing.&lt;/p&gt;

&lt;p&gt;Because each app is its own browser, I can run several at once. When I checked the other day, I had 24 of these apps open at once on my MacBook Pro (M4 Max, 128 GB of memory), using about 23 GB in total, while the same Mac was running around 20 local development servers. That was one observation on one machine, not a stress test.&lt;/p&gt;

&lt;p&gt;What changed in practice: I stopped babysitting the browser. I ask for last week's Search Console drops, this month's Stripe refunds and a draft post, and they come back while I keep working on something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to find it
&lt;/h2&gt;

&lt;p&gt;I built it for my own work first. If you want to look at it, it's here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://kagemusha.app/" rel="noopener noreferrer"&gt;https://kagemusha.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd like to hear how others are handling this. If you let an agent use your browser, which of these walls did you hit?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>automation</category>
      <category>claude</category>
    </item>
    <item>
      <title>Regex couldn't sort my sales emails. A yes/no judgment model could</title>
      <dc:creator>Hisashi Space</dc:creator>
      <pubDate>Fri, 02 Oct 2026 08:08:58 +0000</pubDate>
      <link>https://dev.to/hisashispace/regex-couldnt-sort-my-sales-emails-a-yesno-judgment-model-could-5e2j</link>
      <guid>https://dev.to/hisashispace/regex-couldnt-sort-my-sales-emails-a-yesno-judgment-model-could-5e2j</guid>
      <description>&lt;p&gt;After a press release, the contact address of one of my products started filling up with sales pitches: webinar invites, free whitepapers, trade show booths, agencies offering to help. I run more than ten products on my own, so I didn't have time to sort these by hand, and my Gmail filters and regex weren't holding.&lt;/p&gt;

&lt;p&gt;This post is about what replaced them: a judgment model that answers yes/no questions about each email, and the rules I put around it. On October 2, 2026, it took the inbox from 92 messages to 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why rules broke
&lt;/h2&gt;

&lt;p&gt;Four kinds of mail were landing in the same inbox:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;replies people had written to us (these need an answer)&lt;/li&gt;
&lt;li&gt;auto-replies from contact forms (we reach out to companies through their website forms)&lt;/li&gt;
&lt;li&gt;pitches, newsletters and event invites&lt;/li&gt;
&lt;li&gt;bounces&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same phrases show up across kinds. "Thank you for your inquiry" appears in auto-replies and in thoughtful human replies. Filtering on "webinar" catches real inquiries that mention one.&lt;/p&gt;

&lt;p&gt;Some pitches don't even come from the sender. A common trick, mostly from English-speaking companies, is to write the pitch in a Google Doc and share it with you, so the email arrives from Google. Another is signing you up for a Zoom webinar, so the invite comes from Zoom's shared address. You can't send Google or Zoom to spam without losing the shares and invites you actually need.&lt;/p&gt;

&lt;p&gt;A wording rule also hurt us once. We treated "we'll pass this time" as a refusal and stopped contacting that company for good. It says "this time." A regex can't tell "this time" from "never" reliably.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model: Jev
&lt;/h2&gt;

&lt;p&gt;I used Jev from TypeSafe AI. It's what TypeSafe calls a System One model (the name comes from Kahneman's fast, intuitive System 1). It reads natural language like an LLM does, but it doesn't generate text. You send a &lt;code&gt;state&lt;/code&gt; and a set of typed questions, and it returns typed answers with probabilities. No JSON to coax out of a chat model, no parsing.&lt;/p&gt;

&lt;p&gt;There are three question types:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Asks&lt;/th&gt;
&lt;th&gt;Returns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;choice&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pick one option from a list&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;choice&lt;/code&gt;, per-option &lt;code&gt;probabilities&lt;/code&gt;, &lt;code&gt;confidence&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;score&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;place the state on 2 to 10 described levels&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;score&lt;/code&gt; (can land between levels), per-level &lt;code&gt;probabilities&lt;/code&gt;, &lt;code&gt;confidence&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;noul&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes or no&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;noul&lt;/code&gt;, the probability of yes (0 to 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few properties mattered for this job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All questions in one request are evaluated against the same state, in parallel and independently. One answer doesn't leak into another.&lt;/li&gt;
&lt;li&gt;The docs say most queries come back in about 100 ms, and adding questions barely changes that.&lt;/li&gt;
&lt;li&gt;The question id is never sent to the model. Only &lt;code&gt;instructions&lt;/code&gt; (and optional &lt;code&gt;criteria&lt;/code&gt;) are, so the edge cases go in the instructions.&lt;/li&gt;
&lt;li&gt;Pricing at the time of writing (Jev 1.13) is $0.042 per million input tokens, output free. Rate limits are 100K tokens/s and 40 requests/s, and you get a 429 past either.&lt;/li&gt;
&lt;li&gt;English is the primary training language. Other languages, Japanese included, work but less accurately. My inbox is mostly Japanese, which turned out to matter for thresholds.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Five questions, one call
&lt;/h2&gt;

&lt;p&gt;Each email becomes one request with five &lt;code&gt;noul&lt;/code&gt; questions. Most of the cost is sending the body, so you put all questions in a single call instead of one call per question.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.typesafe.ai/v1/systemone &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TYPESAFE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; @- &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
{
  "model": "jev-latest",
  "state": "From: ...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;Subject: ...&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;Body...",
  "questions": {
    "human": { "type": "noul", "instructions": "A person wrote this reply individually. Auto-replies, promotions and newsletters don't count." },
    "needReply": { "type": "noul", "instructions": "It would be appropriate, socially or commercially, for us to reply to this email." },
    "counterSales": { "type": "noul", "instructions": "The sender is trying to sell us their own product, service or seminar." },
    "meetingRequest": { "type": "noul", "instructions": "The sender wants to set up a meeting, call or visit." },
    "stopRequest": { "type": "noul", "instructions": "The sender clearly asks us not to contact them again. Declining just this one time doesn't count." }
  }
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last instruction is the direct fix for the "this time" mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same input, slightly different answers
&lt;/h2&gt;

&lt;p&gt;The probabilities wobble between calls. In an earlier test on a different task, I sent the same three questions about the same 69 items three times. Average spread was 0.013 to 0.041 per question, the worst was 0.15, and only 2 to 11 of the 69 items got identical values all three times.&lt;/p&gt;

&lt;p&gt;So anything near a threshold can flip if you ask again. I ask twice and only act when both answers land on the same side of the line. If they disagree, a person looks at it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;askRepeated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;QUESTIONS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;agreed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;gateNoul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;LINES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;])));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;act&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unsure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;gateNoul&lt;/code&gt; maps a probability to yes, no or "don't decide", and &lt;code&gt;agreed&lt;/code&gt; only returns &lt;code&gt;auto&lt;/code&gt; when every run reached the same verdict. Both live in a small wrapper I share across my products.&lt;/p&gt;

&lt;h2&gt;
  
  
  Draw the lines from your own mail
&lt;/h2&gt;

&lt;p&gt;My wrapper's default for a single noul is 0.9 for yes and 0.1 for no. On 26 real Japanese emails, that left nearly everything undecided. The measured values looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"should we reply": real replies 0.61 to 0.93, pitches and auto-replies 0.05 to 0.49&lt;/li&gt;
&lt;li&gt;"stop contacting us": the highest of the 26 was 0.19&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So each question got its own line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LINES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;human&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;yes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;no&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;needReply&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;yes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;no&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.599&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;counterSales&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;yes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;no&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.599&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;meetingRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;yes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;no&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.599&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;stopRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;yes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;no&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// can't be undone, so the bar is higher&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things I learned the hard way. Changing the set of questions in a request moves the numbers for the others, so the lines belong to that set of questions and need re-measuring when you add one. And &lt;code&gt;jev-latest&lt;/code&gt; is an alias that moves when a new version ships, so if you tuned thresholds on a version, pin it (&lt;code&gt;jev-1.13.0&lt;/code&gt;) and re-measure before moving. The response's &lt;code&gt;model&lt;/code&gt; field tells you which version answered, which is worth logging.&lt;/p&gt;

&lt;h2&gt;
  
  
  What never goes to the model
&lt;/h2&gt;

&lt;p&gt;If code can decide it, code decides it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bounces are recognizable from the sender (mailer-daemon, postmaster)&lt;/li&gt;
&lt;li&gt;anything about invoices, payments or contracts stays in the inbox regardless of the model&lt;/li&gt;
&lt;li&gt;if the call fails or returns nothing, the email goes to a person, never to "fine, archive it"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one matters most. "Couldn't judge" and "judged as harmless" aren't the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;

&lt;p&gt;On October 2, 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;about 70 pitches and invites moved to "no reply needed"&lt;/li&gt;
&lt;li&gt;about 15 form auto-replies filed away&lt;/li&gt;
&lt;li&gt;3 bounces&lt;/li&gt;
&lt;li&gt;2 left in the inbox: a Google Workspace invoice and one reply I needed to answer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It now runs every five minutes, with judged message IDs cached so the same email isn't paid for twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Wording rules break when the same words show up across different kinds of mail, and platform-borne pitches (Google Docs shares, Zoom invites) can't be filtered by sender at all.&lt;/li&gt;
&lt;li&gt;Put every question for one email in one call.&lt;/li&gt;
&lt;li&gt;Ask twice, act only on agreement, send disagreements to a person.&lt;/li&gt;
&lt;li&gt;Measure thresholds on your own data, especially outside English, and raise the bar for anything irreversible.&lt;/li&gt;
&lt;li&gt;Pin the model version you measured against.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TypeSafe AI docs:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.typesafe.ai/introduction" rel="noopener noreferrer"&gt;https://docs.typesafe.ai/introduction&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The longer story, including setup steps, is on my blog:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hisashi.space/blog/jev-inbox-triage" rel="noopener noreferrer"&gt;https://hisashi.space/blog/jev-inbox-triage&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>email</category>
      <category>typescript</category>
      <category>automation</category>
    </item>
    <item>
      <title>Traffic from ChatGPT jumped! Is it because its crawlers now run JavaScript?</title>
      <dc:creator>Hisashi Space</dc:creator>
      <pubDate>Wed, 30 Sep 2026 18:03:30 +0000</pubDate>
      <link>https://dev.to/hisashispace/traffic-from-chatgpt-jumped-is-it-because-its-crawlers-now-run-javascript-5961</link>
      <guid>https://dev.to/hisashispace/traffic-from-chatgpt-jumped-is-it-because-its-crawlers-now-run-javascript-5961</guid>
      <description>&lt;p&gt;On September 25, 2026, early in the morning Japan time, OpenAI's crawlers changed how they read my sites. I saw it in the server logs. OAI-SearchBot (search) and GPTBot (training) stopped just fetching HTML and started running JavaScript and rendering pages.&lt;/p&gt;

&lt;p&gt;The accepted wisdom has been that AI crawlers don't run JavaScript. The Vercel and MERJ study from December 2024 said none of OpenAI's three bots executed it. What I'm seeing is a change from that.&lt;/p&gt;

&lt;p&gt;This post has the numbers from the logs first. After that comes a list of what I would do now if I wanted ChatGPT to mention a site. I couldn't find any announcement from OpenAI, so where I'm guessing, I say so.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;The main site is &lt;a class="mentioned-user" href="https://dev.to/soho"&gt;@soho&lt;/a&gt; (atsoho.com), a Japanese freelance marketplace built on Next.js with the App Router. A few of my product sites run on the same server. I did not trust the user agent alone. Every request was checked against OpenAI's published IP ranges (gptbot.json, searchbot.json, chatgpt-user.json), and anything outside them was dropped as a fake.&lt;/p&gt;

&lt;h3&gt;
  
  
  Requests per day on atsoho.com
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;OAI-SearchBot&lt;/th&gt;
&lt;th&gt;GPTBot&lt;/th&gt;
&lt;th&gt;ChatGPT-User&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sep 10&lt;/td&gt;
&lt;td&gt;1,108&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;td&gt;13,538&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sep 22&lt;/td&gt;
&lt;td&gt;1,253&lt;/td&gt;
&lt;td&gt;83&lt;/td&gt;
&lt;td&gt;657&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sep 24&lt;/td&gt;
&lt;td&gt;7,082&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;911&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sep 25&lt;/td&gt;
&lt;td&gt;41,444&lt;/td&gt;
&lt;td&gt;28,484&lt;/td&gt;
&lt;td&gt;1,645&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sep 28&lt;/td&gt;
&lt;td&gt;14,339&lt;/td&gt;
&lt;td&gt;152,009&lt;/td&gt;
&lt;td&gt;1,071&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;OAI-SearchBot jumped at 4 a.m. JST on September 25, and GPTBot took off at noon the same day. Until September 24 GPTBot came from one IP a day. From September 25 it came from 36 to 131.&lt;/p&gt;

&lt;h3&gt;
  
  
  Most of the new traffic was prefetch
&lt;/h3&gt;

&lt;p&gt;When I looked inside, almost all of the extra requests were Next.js link prefetches, the ones with &lt;code&gt;?_rsc=&lt;/code&gt; plus five random characters at the end. That string is not in the HTML. It only shows up when the page is rendered and its JavaScript runs.&lt;/p&gt;

&lt;p&gt;・September 22: 80% of OAI-SearchBot requests were plain HTML fetches, 2% were prefetches&lt;br&gt;
・September 28: 84% of OAI-SearchBot requests and 97% of GPTBot requests were prefetches&lt;/p&gt;

&lt;p&gt;So 150,000 requests a day does not mean 150,000 pages read. Most of it is what a rendered page fires off on its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many pages actually got rendered
&lt;/h3&gt;

&lt;p&gt;Each prefetch carries the page that triggered it in the Referer. Counting distinct Referers tells you how many pages the bot rendered.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Rendered by OAI-SearchBot&lt;/th&gt;
&lt;th&gt;of which job pages&lt;/th&gt;
&lt;th&gt;Rendered by GPTBot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sep 18&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sep 25&lt;/td&gt;
&lt;td&gt;861&lt;/td&gt;
&lt;td&gt;108&lt;/td&gt;
&lt;td&gt;1,994&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sep 28&lt;/td&gt;
&lt;td&gt;539&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;4,395&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;About 10x for search, and well over 100x for training.&lt;/p&gt;

&lt;h3&gt;
  
  
  They're rendering HTML they already have
&lt;/h3&gt;

&lt;p&gt;Of the 4,309 pages GPTBot rendered on September 28, only 28% had been fetched from my server by anyone that day. My HTML is not cached at the CDN, so the CDN did not serve it either. My read is that OpenAI re-renders HTML it stored earlier in its own headless browser, and the only thing that reaches my server is the prefetches that fire during that render.&lt;/p&gt;

&lt;p&gt;OpenAI's own docs say that if a site allows both bots, it may use the results from one crawl for both purposes to avoid duplicate crawling. GPTBot rendering what OAI-SearchBot fetched fits that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this happening everywhere?
&lt;/h3&gt;

&lt;p&gt;On the same server, all four Next.js sites that have articles saw OAI-SearchBot go up 5 to 15x on September 24 and 25. They run different Next.js versions and deploy differently, and I did not change anything on my side that week. The change came from OpenAI.&lt;/p&gt;

&lt;p&gt;But on Cloudflare Radar, OpenAI's share of all AI bot traffic stayed flat through September. It looks like rendering is only switched on for some sites.&lt;/p&gt;

&lt;p&gt;It reaches static sites too. On one of my sites that doesn't use Next.js, OAI-SearchBot pulled images and CSS on September 25, which is what rendering looks like. A static page does not fire prefetches, though, so the request count barely moves. The spike on the Next.js sites is mostly prefetch amplification.&lt;/p&gt;

&lt;h3&gt;
  
  
  What else changed at the same time
&lt;/h3&gt;

&lt;p&gt;Around the same time, visits from ChatGPT to &lt;a class="mentioned-user" href="https://dev.to/soho"&gt;@soho&lt;/a&gt;'s job pages went up, and daily signups rose about 1.7x. The number of people coming from ChatGPT did not change. Where they landed did. It used to be mostly blog posts, and from about September 25 it was mostly job pages. That lines up with job pages rendered by OAI-SearchBot going from 2 a day to 36 to 108. I cannot prove one caused the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do if you want ChatGPT to mention you
&lt;/h2&gt;

&lt;p&gt;These six come from the logs above and from OpenAI's crawler docs.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Let OAI-SearchBot in
&lt;/h3&gt;

&lt;p&gt;OpenAI's docs say sites that opt out of OAI-SearchBot will not be shown in ChatGPT search answers, though they can still appear as navigational links. They recommend allowing OAI-SearchBot in robots.txt and allowing requests from their published IP ranges.&lt;/p&gt;

&lt;p&gt;Three things to check:&lt;/p&gt;

&lt;p&gt;・robots.txt: make sure OAI-SearchBot isn't disallowed&lt;br&gt;
・WAF and bot protection: Cloudflare bot rules or your own rate limits might be blocking OpenAI's IPs. My own server was returning 403 to some of their requests on September 22 to 24&lt;br&gt;
・IP lists: OpenAI updates them. This time gptbot.json changed on September 22 and chatgpt-user.json on September 25. If you keep an allowlist by hand, pull it from the list URLs instead&lt;/p&gt;

&lt;p&gt;If you do not want your content used for training, you can block GPTBot alone and keep search.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Put the content in the first HTML
&lt;/h3&gt;

&lt;p&gt;OpenAI has started rendering, but only for some sites, it seems. Everywhere else, only the initial HTML gets read. And even where rendering happens, it starts from stored HTML.&lt;/p&gt;

&lt;p&gt;Anything you want quoted, like the body, title, prices and dates, is safest in HTML that's readable without running JavaScript. In Next.js that means server rendering or static generation. On any site, avoid loading the main content from an API after the page opens.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Link to the pages you want read
&lt;/h3&gt;

&lt;p&gt;The bots were reading prefetches inside rendered pages. The more internal links point at a page, the more chances it has to be read.&lt;/p&gt;

&lt;p&gt;Google works the same way. One of my other product sites has 520 articles, and only 2.5% were indexed. The article list was paginated and Google never went past page 1, while 632 product pages linked to each other 37 times each and to the articles zero times. About 90% of Google's crawls went to those product pages.&lt;/p&gt;

&lt;p&gt;・Do not bury articles deep in pagination&lt;br&gt;
・Link related pages to each other, like a product page and the articles about that product&lt;br&gt;
・Do not stop at the sitemap. Pages that only exist in the sitemap get pushed back&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Don't block Next.js prefetches
&lt;/h3&gt;

&lt;p&gt;Next.js has a check that turns prefetching off for known bots. The list has Google, Bing and others, but not OpenAI's bots (I checked the source in Next.js 14.2 and 16). So when an OpenAI bot renders a page, the prefetches go out as usual, lots of them.&lt;/p&gt;

&lt;p&gt;I wouldn't block &lt;code&gt;?_rsc=&lt;/code&gt; in robots.txt or answer it with 429. A prefetch response contains the linked page's content, so for a rendering crawler it's one of the ways it reads your pages. Get the rule slightly wrong and you can block the pages themselves.&lt;/p&gt;

&lt;p&gt;If server load becomes a problem, turn prefetch off only for the navigation links that appear on every page (&lt;code&gt;prefetch={false}&lt;/code&gt;). That cuts the traffic and keeps the content and normal links intact. On my site most prefetches came from the category links in the global nav.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Tell machines how fresh things are
&lt;/h3&gt;

&lt;p&gt;On &lt;a class="mentioned-user" href="https://dev.to/soho"&gt;@soho&lt;/a&gt;, ChatGPT sent people to job posts that had already closed. I cannot tell from outside how old ChatGPT thinks the information is. To avoid sending people to stale pages, put the published date, updated date and whether it's still open in both the structured data (JSON-LD) and the page itself. For job posts, one way is to mark only open posts as JobPosting and give closed ones a datePublished.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. llms.txt can wait
&lt;/h3&gt;

&lt;p&gt;I put llms.txt on &lt;a class="mentioned-user" href="https://dev.to/soho"&gt;@soho&lt;/a&gt; on September 20. OpenAI's bots read it once every few days. So far it does not look like it decides whether ChatGPT mentions you. It does not hurt, but I wouldn't put it ahead of 1 to 5.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check this on your own site
&lt;/h2&gt;

&lt;p&gt;If you have server logs (nginx or similar), you can check all of this yourself.&lt;/p&gt;

&lt;p&gt;・Identify the bots: match on the user agent (OAI-SearchBot, GPTBot, ChatGPT-User) and check the IP against OpenAI's three lists. An IP that is not listed is a fake&lt;br&gt;
・Is it rendering: look for &lt;code&gt;_rsc=&lt;/code&gt; in the URL (Next.js). On a static site, see whether the bot is fetching images and CSS&lt;br&gt;
・Pages rendered: count distinct Referers on the &lt;code&gt;_rsc=&lt;/code&gt; requests. Don't judge by request counts&lt;br&gt;
・Traffic from ChatGPT: ChatGPT adds &lt;code&gt;utm_source=chatgpt.com&lt;/code&gt; to its links. The Referer misses some of it, so count on the request URL&lt;br&gt;
・Compare with everyone else: pull daily shares per AI bot from the Cloudflare Radar API&lt;/p&gt;

&lt;p&gt;If a site is served only from Cloudflare, there are no server logs. Cloudflare's GraphQL Analytics API (httpRequestsAdaptiveGroups filtered by user agent) gives you counts by day and path, even on the free plan.&lt;/p&gt;

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

&lt;p&gt;・How OpenAI picks which sites to render&lt;br&gt;
・When and how it fetches the HTML it renders&lt;br&gt;
・How much rendering actually affects whether ChatGPT mentions a site&lt;/p&gt;

&lt;p&gt;I'll add to this post as I find out.&lt;/p&gt;

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

&lt;p&gt;・OpenAI, Overview of OpenAI Crawlers&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developers.openai.com/api/docs/bots" rel="noopener noreferrer"&gt;https://developers.openai.com/api/docs/bots&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;・Vercel, The rise of the AI crawler (December 2024)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://vercel.com/blog/the-rise-of-the-ai-crawler" rel="noopener noreferrer"&gt;https://vercel.com/blog/the-rise-of-the-ai-crawler&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;・Next.js, Prefetching&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nextjs.org/docs/app/guides/prefetching" rel="noopener noreferrer"&gt;https://nextjs.org/docs/app/guides/prefetching&lt;/a&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>nextjs</category>
      <category>chatgpt</category>
      <category>webdev</category>
    </item>
    <item>
      <title>X lead gen ads are back: how to collect names and emails without sending anyone off X</title>
      <dc:creator>Hisashi Space</dc:creator>
      <pubDate>Tue, 29 Sep 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/hisashispace/x-lead-gen-ads-are-back-how-to-collect-names-and-emails-without-sending-anyone-off-x-4c3o</link>
      <guid>https://dev.to/hisashispace/x-lead-gen-ads-are-back-how-to-collect-names-and-emails-without-sending-anyone-off-x-4c3o</guid>
      <description>&lt;p&gt;Can you collect leads with X ads? Names and email addresses, straight from the ad?&lt;/p&gt;

&lt;p&gt;Yes. X launched Lead Gen Ads on August 26, 2026.&lt;br&gt;
Tap the ad, and a form opens inside X. People fill it in without leaving the app, and you get the leads as a CSV or sent to your CRM.&lt;/p&gt;

&lt;p&gt;This isn't the first time. Twitter had lead generation cards from 2013, and stopped showing them in March 2017. Nine years later, lead capture is back on X in a new form.&lt;/p&gt;

&lt;p&gt;What follows is based on X's own setup guide for advertisers (August 2026).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd64m8egcvoq8dr5ir6v5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd64m8egcvoq8dr5ir6v5.webp" alt="How X lead gen ads work" width="800" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What people see
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The ad shows up in their timeline&lt;/li&gt;
&lt;li&gt;They tap the button (such as "Learn More") and a form opens inside X&lt;/li&gt;
&lt;li&gt;They enter their name and email and submit&lt;/li&gt;
&lt;li&gt;A final page appears. You can put a button there that takes them to your site&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You build it in the opposite order: the form first, then the ad.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Build the lead form
&lt;/h2&gt;

&lt;p&gt;In Ads Manager, open &lt;strong&gt;Leads center&lt;/strong&gt; from the left menu and click &lt;strong&gt;New form&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content tab&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;・Name: the form's internal name&lt;br&gt;
・Introduction (optional): a headline and description shown above the form. The description can hold extra consent text&lt;br&gt;
・Purpose: what the form is for. This only shows when Introduction is turned off&lt;br&gt;
・Fields: what you ask people to fill in. Email is on and required by default&lt;br&gt;
・Privacy policy: your company name and a link to your privacy policy. The URL is reviewed before the ad can run&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fields you can add&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;・Contact: email, phone number, X handle, street address, city, state or province, country, post or zip code&lt;br&gt;
・User: full name, first name, last name, date of birth, gender&lt;br&gt;
・Work: company name, job title, work email, work phone number&lt;/p&gt;

&lt;p&gt;The guide notes that the available fields may change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final page tab&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;・Headline&lt;br&gt;
・Description&lt;br&gt;
・Website button (optional): the URL and the button label&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create the campaign
&lt;/h2&gt;

&lt;p&gt;On the campaign form, pick &lt;strong&gt;Leads&lt;/strong&gt; as the objective and name the campaign.&lt;/p&gt;

&lt;p&gt;You can also attach a lead form to ads with other objectives, such as Reach or Video Views, by choosing the lead form as the ad's destination.&lt;br&gt;
In that case, delivery is optimized for the objective of that campaign, not for leads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Set up the ad group
&lt;/h2&gt;

&lt;p&gt;・Ad group name&lt;br&gt;
・Budget (daily budget; a total cap is optional)&lt;br&gt;
・Start time (end time is optional)&lt;br&gt;
・Demographics (age targeting is 18 and over only)&lt;br&gt;
・Placements: home timeline, search results, profiles, replies&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Create the ad
&lt;/h2&gt;

&lt;p&gt;・Ad name and post text&lt;br&gt;
・The lead form to attach&lt;br&gt;
・Call to action&lt;br&gt;
・Image or video&lt;/p&gt;

&lt;p&gt;The call to action is one of: Book Now, Buy Tickets, Contact Us, Donate, Download, Get Offer, Learn More, Shop Now, Sign Up, Watch Now.&lt;/p&gt;

&lt;p&gt;You can also build the ad in &lt;strong&gt;Composer&lt;/strong&gt;. Choose &lt;strong&gt;Lead form&lt;/strong&gt; as the destination, then pick the form and the call to action.&lt;br&gt;
Posts made in Composer can also go into Takeover campaigns (Timeline Takeover and Timeline Reach only).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Review and launch
&lt;/h2&gt;

&lt;p&gt;Choose a funding source, check the campaign, and click &lt;strong&gt;Launch&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reporting
&lt;/h2&gt;

&lt;p&gt;Ads Manager shows the usual metrics plus lead metrics: Leads and Cost per lead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting the leads out
&lt;/h2&gt;

&lt;p&gt;Three ways:&lt;/p&gt;

&lt;p&gt;・Download from Leads center as CSV or XLSX&lt;br&gt;
・Connect a CRM (HubSpot, Salesforce, Zapier): pick the form and paste the webhook URL from your CRM&lt;br&gt;
・Pull them through the Ads API&lt;/p&gt;

&lt;h2&gt;
  
  
  Four things to sort out before you launch
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Leads are only available for 90 days after they're submitted.&lt;/strong&gt;&lt;br&gt;
Connect your CRM or Zapier before the ad goes live, or decide who downloads them and how often.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your privacy policy URL gets reviewed.&lt;/strong&gt;&lt;br&gt;
If your site doesn't have a privacy policy page, the ad won't run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Age targeting is 18+ only.&lt;/strong&gt;&lt;br&gt;
You can't use it to reach anyone younger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Email is required by default.&lt;/strong&gt;&lt;br&gt;
In the guide, the personal email field is switched on and required from the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Yes, you can collect names and email addresses with X ads alone.&lt;br&gt;
Build the form, run a campaign with the Leads objective, and take the leads out as CSV or into your CRM.&lt;/p&gt;

&lt;p&gt;Have a privacy policy page and a place for the leads to land before you start, and nothing will stop you halfway.&lt;/p&gt;

</description>
      <category>marketing</category>
      <category>advertising</category>
      <category>startup</category>
      <category>saas</category>
    </item>
  </channel>
</rss>
