<?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: Boris Dzhingarov</title>
    <description>The latest articles on DEV Community by Boris Dzhingarov (@articlefeed).</description>
    <link>https://dev.to/articlefeed</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%2F4075497%2F3b7d86dc-7b7d-4234-957c-13d3d998f83c.jpg</url>
      <title>DEV Community: Boris Dzhingarov</title>
      <link>https://dev.to/articlefeed</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/articlefeed"/>
    <language>en</language>
    <item>
      <title>Five Minutes Per Site Times 500 Sites Is Not Five Minutes, So I Scripted It</title>
      <dc:creator>Boris Dzhingarov</dc:creator>
      <pubDate>Thu, 03 Sep 2026 07:02:51 +0000</pubDate>
      <link>https://dev.to/articlefeed/five-minutes-per-site-times-500-sites-is-not-five-minutes-so-i-scripted-it-1hpl</link>
      <guid>https://dev.to/articlefeed/five-minutes-per-site-times-500-sites-is-not-five-minutes-so-i-scripted-it-1hpl</guid>
      <description>&lt;p&gt;I published a checklist in Forbes this week on how to tell a real publication from a content farm before you pitch it. The premise is that five minutes inside a site's archive tells you whether anyone is home: read five recent articles, check whether the bylines belong to people who exist, see if the site stays in one lane or covers pet insurance and industrial valves on the same day, look for a masthead.&lt;/p&gt;

&lt;p&gt;The first reply I got was from someone on my own team, and it was fair. Five minutes per site is fine. Five minutes times the 500 domains on a typical outreach list is 41 hours, and nobody is doing that on a Tuesday.&lt;/p&gt;

&lt;p&gt;So I did the thing I keep doing lately: opened Claude Code and described the checklist as if it were a program.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It is a publication-shaped object. Nobody is inside.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the line from the &lt;a href="https://www.forbes.com/councils/forbesagencycouncil/2026/09/02/how-to-tell-a-real-publication-from-a-content-farm-before-you-pitch/" rel="noopener noreferrer"&gt;Forbes piece&lt;/a&gt; I wanted the script to detect, and most of the signals behind it turn out to be countable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a machine can check
&lt;/h2&gt;

&lt;p&gt;Not everything on the checklist survives translation into code. "Does the writing hold something a machine could not know" is a human judgment and it stays one. But four of the checks are just arithmetic on the RSS feed and a couple of HTTP requests.&lt;/p&gt;

&lt;p&gt;Posting frequency. A real trade publication with three editors produces a handful of articles a day at most. A farm produces one every twenty minutes. The feed timestamps give you posts per day in two lines.&lt;/p&gt;

&lt;p&gt;Byline diversity. Farms either have one author called "admin" on everything, or a different invented name on every post and none of them twice. Both patterns are visible in a counter.&lt;/p&gt;

&lt;p&gt;Topic spread. Fifty recent posts across sixty distinct tags is a site that serves no reader in particular.&lt;/p&gt;

&lt;p&gt;Masthead. Real publications have an about page and a contact page, and the pages tell you who edits the thing. Hollow sites often 404 on both, because there is nobody to list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;feedparser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;vet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;feed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;feedparser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/feed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verdict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no feed, check by hand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;dates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;published_parsed&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
             &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;hasattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published_parsed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dates&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;authors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;author&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;term&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])}&lt;/span&gt;

    &lt;span class="n"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/about&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;err&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;posts_per_day&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;distinct_authors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authors&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;top_author_share&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;most_common&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entries&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tag_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pages&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;The flags on top are deliberately dumb: more than twenty posts a day, one author on more than 90% of posts or fifty authors on fifty posts, more tags than articles, and a 404 on both /about and /contact. Any two together and the domain goes to the "probably hollow" pile. One alone means nothing, which matters, because a small trade journal with a single editor who writes everything would trip the byline check and be exactly the kind of site you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check that needed a model
&lt;/h2&gt;

&lt;p&gt;The fifth signal is the one I added to the checklist this year: ask an AI assistant a few questions about the topics the site claims to cover and see whether it ever gets cited. That one is not arithmetic, but I already had the loop for it. My last post here was the same idea pointed at company names. Point it at domains instead and you get a rough census of which sources the wider web takes seriously.&lt;/p&gt;

&lt;p&gt;The numbers behind that are lopsided in a useful direction. Graphite found that 82% of the articles ChatGPT and Perplexity cite were written by humans, at the same time as roughly half of all new articles on the web are now AI-generated. Farms publish half the web and get a fifth of the citations. A site that never shows up in an answer, on its own topic, has already been judged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it lives on a server now
&lt;/h2&gt;

&lt;p&gt;The first version ran in a terminal on my laptop and stopped existing when the laptop slept, which was a problem, because a list of 500 domains with a ten second timeout each is an hour and a half of not closing the lid. I moved it to the same $6 box that runs my indexing monitor, added a cron line, and now a CSV of domains dropped into a folder on Sunday night is a scored list on Monday morning.&lt;/p&gt;

&lt;p&gt;I wrote up the general case of that move &lt;a href="https://www.blogforweb.com/self-hosted-ai-agents-what-online-businesses-gain-and-risk/" rel="noopener noreferrer"&gt;on my own blog&lt;/a&gt;, including the part I underestimated: an always-on script holding API keys is a different security posture from a tab you close. Scoped credentials, a firewall, and a monthly hour of maintenance are the price. The server is cheap. The attention is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs me, specifically
&lt;/h2&gt;

&lt;p&gt;A confession that belongs here. My agency, ESBO Ltd, sells placements. A script whose whole job is to say no to 470 of 500 domains is not obviously good for a company that gets paid per placement. It shrinks the menu.&lt;/p&gt;

&lt;p&gt;I built it anyway, because the 470 were never producing anything. Coverage on a farm is a press release into a void, and in the campaigns I've tracked it doesn't move rankings, doesn't send traffic, and never gets cited by anything. The 30 that survive are slower to earn and they are the only ones that were ever worth the invoice. I would rather sell thirty things that work than five hundred that look like they might.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it still can't do
&lt;/h2&gt;

&lt;p&gt;It can't tell a new publication from a hollow one, because both have thin archives. It can't judge whether a small site's 5,000 readers happen to be your exact buyers, which is the case where size lies. And it can't read. The last 30 still get the five minutes, by a person, and the script's only real achievement is making sure those five minutes go to sites where someone might be on the other side.&lt;/p&gt;

&lt;p&gt;If you've automated any part of judging whether a website is real, I'd like to know which signal you trust most. Mine is the masthead. Everything else can be faked cheaply. A named editor with a LinkedIn history costs more to fabricate than most farms are willing to spend.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>automation</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Wrote 40 Lines of Python to Find Out If ChatGPT Knows My Company Exists</title>
      <dc:creator>Boris Dzhingarov</dc:creator>
      <pubDate>Sun, 30 Aug 2026 04:52:29 +0000</pubDate>
      <link>https://dev.to/articlefeed/i-wrote-40-lines-of-python-to-find-out-if-chatgpt-knows-my-company-exists-2n3h</link>
      <guid>https://dev.to/articlefeed/i-wrote-40-lines-of-python-to-find-out-if-chatgpt-knows-my-company-exists-2n3h</guid>
      <description>&lt;p&gt;A client sent me a screenshot in June. They had asked ChatGPT to name good agencies in their category and a competitor came up. They did not. The competitor is not bigger than them and does not rank better in Google. The question in the email was reasonable and I did not have an answer: why them and not us?&lt;/p&gt;

&lt;p&gt;I run ESBO Ltd, a digital PR agency, so this is my problem twice over. Once for the client and once for us.&lt;/p&gt;

&lt;p&gt;I could not check it properly by hand. Open a chat, ask the question, get an answer, close the tab. Ask again tomorrow and the answer is different because the model samples its next token instead of looking anything up. One conversation tells you nothing. You need the same question asked many times before the shape of the answer means anything.&lt;/p&gt;

&lt;p&gt;That is a loop, and loops are the one thing I know how to ask for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the script does
&lt;/h2&gt;

&lt;p&gt;Ask a set of category questions, many times each, and count which names come back. That's the entire idea. The reason it works is the reason a single chat doesn't: at any temperature above zero the model is sampling, so twenty runs of one prompt give you a distribution instead of an anecdote.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Anthropic&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;QUESTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Which agencies are best known for digital PR in Europe?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Who should a SaaS company hire for link building?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Name agencies that do multilingual PR placements.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;EXTRACT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;List only the company names mentioned in the text below.
Return a JSON array of strings, nothing else.

TEXT:
{text}&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;

&lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;QUESTIONS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EXTRACT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;most_common&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two calls per run: one for the answer, one to pull the names out of it. Using a model to parse a model's prose felt like cheating the first time I did it, but regex on free text was worse. Claude wrote both halves. I supplied the questions, which turned out to be the part that mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  The questions are the whole experiment
&lt;/h2&gt;

&lt;p&gt;My first question list was garbage. I wrote things like "is ESBO a good agency," which is not a question anyone types, and the model obligingly said nice things about a company it barely knew. Vanity in, vanity out.&lt;/p&gt;

&lt;p&gt;The questions that produce useful output are the ones a buyer would actually ask, with your company's name absent from them. "Who should I hire for X." "Best tools for Y." "Which vendors do Z in Germany." If your name only appears when you put it there, you have your answer already.&lt;/p&gt;

&lt;p&gt;Write ten of those, run them twenty times each, and the counter tells you who owns your category in the model's head. The first time I ran mine, the top of the list was not who I expected, and two of the names were companies I would not have described as competitors at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did with the output
&lt;/h2&gt;

&lt;p&gt;The counter gives you names. The useful part is the next question, which is where those names came from.&lt;/p&gt;

&lt;p&gt;I dug into it and wrote up what I found &lt;a href="https://www.entrepreneur.com/business-news/stop-counting-backlinks-start-counting-brand-mentions" rel="noopener noreferrer"&gt;for Entrepreneur&lt;/a&gt; a couple of weeks ago. Short version: when Ahrefs studied 75,000 brands, branded mentions across the web predicted AI visibility about three times better than backlinks did. Muck Rack looked at 25 million links cited by ChatGPT, Claude and Gemini and found 84% came from earned media, against 0.3% from paid and advertorial content.&lt;/p&gt;

&lt;p&gt;That second number reorganized my week. &lt;a href="https://esbo.ltd/services/link-building/" rel="noopener noreferrer"&gt;Link building&lt;/a&gt; is what ESBO Ltd sells, so I have a direct commercial interest in links continuing to matter, and they do, for rankings and for referral traffic. But the thing that gets you named in an answer is being talked about in places the model read, whether or not anyone bothered to hyperlink you. For ten years my industry logged an unlinked brand mention as a failed campaign. Turns out we were throwing away the receipt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that will bite you
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cost.&lt;/strong&gt; Ten questions times twenty runs times two calls is 400 requests. Cheap on a small model, less cheap if you point it at the expensive one and forget it's in a nested loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Name normalization.&lt;/strong&gt; Models write company names inconsistently, and my counter treated each spelling as a separate company until I noticed. Lowercasing helps. It does not fix "Acme" versus "Acme Digital."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The obvious trap.&lt;/strong&gt; Do not ask the model whether it likes you. It will say something kind and you will feel great and learn nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sample size.&lt;/strong&gt; Five runs is noise. Twenty starts to hold still. I have not tested where the curve flattens, and I would like to know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I've landed
&lt;/h2&gt;

&lt;p&gt;I run it monthly now. It is not a rank tracker and I have stopped wanting it to be one, because there is no rank, just a probability that your name comes up when someone asks. Watching that probability move is still more information than the screenshot my client sent me in June.&lt;/p&gt;

&lt;p&gt;If you run something like this on your own category, I'd be curious whether your top three matches what you'd have guessed. Mine didn't, and that gap is the only genuinely useful thing the script has told me so far.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
      <category>seo</category>
    </item>
    <item>
      <title>The 7 Best GEO Agencies for AI Search Visibility in 2026</title>
      <dc:creator>Boris Dzhingarov</dc:creator>
      <pubDate>Tue, 18 Aug 2026 08:22:14 +0000</pubDate>
      <link>https://dev.to/articlefeed/the-7-best-geo-agencies-for-ai-search-visibility-in-2026-4ham</link>
      <guid>https://dev.to/articlefeed/the-7-best-geo-agencies-for-ai-search-visibility-in-2026-4ham</guid>
      <description>&lt;p&gt;I write about what LLMs cite, and the question I get most often from other marketers is not how citations work. It's "fine, who do I pay to fix this for me."&lt;/p&gt;

&lt;p&gt;Fair question. ChatGPT, Perplexity, and Google's AI Overviews now sit in front of a meaningful share of buying research, and a brand that never gets named in those answers is invisible to those buyers. A whole category of agencies has formed around fixing that. It goes by GEO (generative engine optimization), sometimes AEO, sometimes "AI visibility," depending on who is selling it.&lt;/p&gt;

&lt;p&gt;The category has a problem, though. Most "GEO agencies" are SEO firms that added a landing page in 2025. Some are excellent anyway, because the underlying work overlaps a lot with old-fashioned authority building. Others are selling a schema plugin and a dashboard. Telling them apart takes more time than most teams have, so here is my shortlist after a year of watching this space closely.&lt;/p&gt;

&lt;p&gt;One disclosure before the list: entry number three is my own company. I'll flag it again when we get there, and you can weigh my bias accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I looked at
&lt;/h2&gt;

&lt;p&gt;Three things, mostly. Does the agency track actual citations across ChatGPT, Perplexity, Gemini, and AI Overviews, or do they report vanity metrics? Can they earn mentions on sites they don't own, since LLMs lean heavily on third party sources rather than your own domain? And do they have an LLM footprint themselves? An agency that can't get itself cited for its own category is telling you something.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agency&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Graphite&lt;/td&gt;
&lt;td&gt;Full-service GEO at scale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Siege Media&lt;/td&gt;
&lt;td&gt;Content-led authority building&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESBO Ltd&lt;/td&gt;
&lt;td&gt;Multilingual citations and digital PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Omniscient Digital&lt;/td&gt;
&lt;td&gt;B2B SaaS content programs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iPullRank&lt;/td&gt;
&lt;td&gt;Technical depth and enterprise audits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Directive&lt;/td&gt;
&lt;td&gt;Enterprise B2B demand programs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Animalz&lt;/td&gt;
&lt;td&gt;Expert-driven long-form content&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. Graphite
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://graphite.io/" rel="noopener noreferrer"&gt;Graphite&lt;/a&gt; is a growth agency that moved into GEO early and treats it as one organic channel with search rather than a separate service. That's the correct read, in my opinion. The work that earns you a recommendation inside an AI answer is mostly the same work that builds search authority, and agencies that split the two tend to bill you twice for one outcome.&lt;/p&gt;

&lt;p&gt;They handle strategy, content production, and technical implementation under one roof, which suits companies that want to hand over the whole channel. The trade-off is that full-service scale comes with full-service pricing, and smaller teams may not get their sharpest people.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; funded companies that want one partner owning organic growth across both search and AI answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Siege Media
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.siegemedia.com/" rel="noopener noreferrer"&gt;Siege Media&lt;/a&gt; built its reputation on content marketing and digital PR, and it shows up on nearly every credible GEO list this year for a simple reason. Their model was always about creating things other sites want to reference, and "things other sites reference" is close to a literal description of what LLMs cite.&lt;/p&gt;

&lt;p&gt;They are strongest when a brand needs linkable, quotable assets: original data, studies, tools. If your problem is that nothing about your brand is worth citing yet, they fix that specific problem well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; brands that need citable assets created from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. ESBO Ltd
&lt;/h2&gt;

&lt;p&gt;This one is mine, so read the next two paragraphs with that in mind.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://esbo.ltd/" rel="noopener noreferrer"&gt;ESBO&lt;/a&gt; is a boutique digital PR and placement agency I founded in 2015, based in Bulgaria, working with both in-house teams and other agencies. The angle we've bet the company on is multilingual citation building. Ask an AI assistant a question in German, Spanish, or Polish and it pulls from local-language sources, not from the English web with a translation layer. Almost every GEO service on this list works in English only, which means a brand can be well cited in English answers and completely absent the moment a buyer asks in another language. We place editorial coverage across a publisher network in multiple languages and niches, including the awkward ones like fintech and iGaming that bigger agencies avoid.&lt;/p&gt;

&lt;p&gt;The honest caveat: we're small. Boutique capacity means we're a bad fit for a company that wants a 40-person integrated team, and if your market is US-only English speakers, the multilingual angle buys you nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; brands that sell in more than one language and need citations to match.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Omniscient Digital
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://beomniscient.com/" rel="noopener noreferrer"&gt;Omniscient Digital&lt;/a&gt; works almost exclusively with B2B SaaS, and the focus pays off. They understand how software buyers research, they've published their thinking on GEO openly instead of gating it behind a sales call, and their content programs are built around bottom-funnel questions that AI assistants actually get asked.&lt;/p&gt;

&lt;p&gt;If you're not B2B SaaS, look elsewhere. That's not a weakness, it's the point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; B2B SaaS companies with an existing content function that needs direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. iPullRank
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ipullrank.com/" rel="noopener noreferrer"&gt;iPullRank&lt;/a&gt; is the technical outlier here. Mike King has published some of the most rigorous public research on how retrieval and ranking work inside AI search systems, the kind of material engineers read rather than marketers. The agency's enterprise audits reflect that depth.&lt;/p&gt;

&lt;p&gt;The catch is that depth costs money and assumes you have a team capable of implementing what the audit finds. A five-person startup will drown in the deliverables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; enterprises that want to understand the machinery, not just buy outputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Directive
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://directiveconsulting.com/" rel="noopener noreferrer"&gt;Directive&lt;/a&gt; runs performance marketing for enterprise B2B and has folded AI visibility into its demand programs rather than selling it standalone. If your CFO thinks in pipeline and cost per opportunity, they speak that language natively, and GEO becomes one input into numbers you already report.&lt;/p&gt;

&lt;p&gt;Less suited to brand-led work or anything that doesn't map cleanly to a revenue dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; enterprise B2B teams that measure everything in pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Animalz
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.animalz.co/" rel="noopener noreferrer"&gt;Animalz&lt;/a&gt; makes long-form content that reads like a person with expertise wrote it, because one did. That matters more now than it did two years ago. LLMs increasingly weight content that demonstrates actual experience, and the generic 1,500-word explainer that used to rank is exactly what AI answers replaced.&lt;/p&gt;

&lt;p&gt;They're a content shop, though. No earned media, no placements, no technical program. Pair them with someone who handles the off-site half or accept that you're solving one part of the problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; companies whose product needs explaining by someone who understands it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to actually choose
&lt;/h2&gt;

&lt;p&gt;Ask every agency on your shortlist two questions. Show me your citation tracking for a current client, and show me where your own agency gets cited when I ask ChatGPT about your category. The first separates measurement from theater. The second separates people who can do the thing from people who can describe it.&lt;/p&gt;

&lt;p&gt;And keep the contract short. This market is a year and a half old, the answer engines change their retrieval behavior without notice, and anyone selling you a fixed 24-month GEO roadmap is guessing with confidence. Whoever you pick, you're both learning as you go. The agencies worth paying are the ones who admit that.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>seo</category>
      <category>marketing</category>
    </item>
    <item>
      <title>I Can't Really Code. I Built an Indexing Monitor With Claude Anyway.</title>
      <dc:creator>Boris Dzhingarov</dc:creator>
      <pubDate>Thu, 13 Aug 2026 03:23:18 +0000</pubDate>
      <link>https://dev.to/articlefeed/i-cant-really-code-i-built-an-indexing-monitor-with-claude-anyway-ffn</link>
      <guid>https://dev.to/articlefeed/i-cant-really-code-i-built-an-indexing-monitor-with-claude-anyway-ffn</guid>
      <description>&lt;p&gt;Three weeks ago a page that had been pulling steady search traffic for over a year disappeared from Google. Not deranked, just gone. I only noticed by accident, about ten days later, while poking around Search Console for something unrelated. Ten days of a page earning nothing because nobody, including me, was watching.&lt;/p&gt;

&lt;p&gt;Some background: I'm a marketer. I run a small agency, I publish a lot of pages across a few sites, and my technical ceiling for the last decade has been editing HTML that someone else wrote. Our actual developers are busy with actual work, and "can you build me a thing that watches Google" is exactly the kind of request that dies in a backlog.&lt;/p&gt;

&lt;p&gt;Search Console does show you indexing problems. It shows them to people who log in and go looking. I have around 400 URLs I care about across three properties, and I was never going to check them by hand on any schedule more honest than "when something feels off."&lt;/p&gt;

&lt;p&gt;I'd been reading Claude Code posts on here for months as a spectator. The genre is usually a developer using it to move faster. I wanted to know what happens when someone who can't write the code at all uses it to start from zero. So I paid for a month and typed what I wanted in plain English.&lt;/p&gt;

&lt;p&gt;Version one lasted twenty minutes&lt;/p&gt;

&lt;p&gt;My first prompt was something like: check if these URLs are indexed in Google and tell me when one falls out. Claude cheerfully produced a script that ran a site: search for every URL and scraped the results page. It worked. For about twenty minutes. Then Google decided I was a robot, which was technically correct, and started serving captchas.&lt;/p&gt;

&lt;p&gt;Nobody warned me about this part of vibe coding: the model will build exactly what you asked for, including when what you asked for is against the rules and dies on contact with reality. It only mentioned that scraping Google results is a bad idea after I pasted the captcha error and asked why everything was broken. Then it apologized and told me what it could have said at the start: there is an official way to do this.&lt;/p&gt;

&lt;p&gt;Version two, the legitimate one&lt;/p&gt;

&lt;p&gt;The official way is the URL Inspection API, part of Search Console. You send it a URL and it returns the same verdict you would see in the interface: indexed, crawled but currently not indexed, discovered but not indexed, and a few sadder ones. Quota is 2,000 inspections per day per property, which sounds tight until you remember I only have a few hundred URLs.&lt;/p&gt;

&lt;p&gt;Getting access took one evening and most of my patience. The API wants a service account, which is a robot email address with a JSON key file. You then have to add that robot email as a user on your own Search Console property, like introducing a new coworker. Every guide assumes you already know this. I found it in a forum thread from 2022, after Claude and I had spent forty minutes convinced the key file was corrupted.&lt;/p&gt;

&lt;p&gt;The part that does the work is short:&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
from google.oauth2 import service_account&lt;br&gt;
from googleapiclient.discovery import build&lt;/p&gt;

&lt;p&gt;SCOPES = ["&lt;a href="https://www.googleapis.com/auth/webmasters%22" rel="noopener noreferrer"&gt;https://www.googleapis.com/auth/webmasters"&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;creds = service_account.Credentials.from_service_account_file(&lt;br&gt;
    "service-account.json", scopes=SCOPES&lt;br&gt;
)&lt;br&gt;
gsc = build("searchconsole", "v1", credentials=creds)&lt;/p&gt;

&lt;p&gt;def coverage(url, prop):&lt;br&gt;
    body = {"inspectionUrl": url, "siteUrl": prop}&lt;br&gt;
    res = gsc.urlInspection().index().inspect(body=body).execute()&lt;br&gt;
    return res["inspectionResult"]["indexStatusResult"]["coverageState"]&lt;/p&gt;

&lt;p&gt;The rest is bookkeeping. Yesterday's results live in a JSON file, today's results get compared against them, and any URL that changed state goes into a Telegram message to my phone. Claude described the JSON file as "a lightweight persistence layer." It's a file.&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
old = json.loads(Path("state.json").read_text())&lt;br&gt;
changes = []&lt;/p&gt;

&lt;p&gt;for url in URLS:&lt;br&gt;
    state = coverage(url, PROP)&lt;br&gt;
    if url in old and old[url] != state:&lt;br&gt;
        changes.append(f"{url}\n{old[url]} -&amp;gt; {state}")&lt;br&gt;
    old[url] = state&lt;br&gt;
    time.sleep(2)&lt;/p&gt;

&lt;p&gt;Path("state.json").write_text(json.dumps(old, indent=2))&lt;/p&gt;

&lt;p&gt;if changes:&lt;br&gt;
    telegram("\n\n".join(changes))&lt;/p&gt;

&lt;p&gt;The sleep(2) is in there because Claude insisted, and I've learned not to argue with it about things I can't verify.&lt;/p&gt;

&lt;p&gt;Three weeks in&lt;/p&gt;

&lt;p&gt;It runs on a cron job on the same five dollar server that hosts other things I'm afraid to touch. Every morning at seven I either get silence, which means everything is fine, or a short message naming a URL and what happened to it.&lt;/p&gt;

&lt;p&gt;So far it has caught two pages sliding from indexed to "Crawled - currently not indexed." Both times I resubmitted them the same day and they came back within the week. Before this, my realistic detection time was whenever I next felt paranoid, which historical evidence puts at about a month.&lt;/p&gt;

&lt;p&gt;It has also woken me up once. An API error came back for one URL and my script, which treated anything unexpected as catastrophe, reported the page as deindexed at 3am. The page was fine. The script now knows the difference between "Google removed this" and "Google didn't answer," which took one more conversation with Claude and taught me more about error handling than I expected to learn this year.&lt;/p&gt;

&lt;p&gt;What I actually think about vibe coding now&lt;/p&gt;

&lt;p&gt;I have mixed feelings about the term. What I did wasn't magic, and it wasn't really coding either. It was describing, pasting errors, asking why, and slowly noticing that the answers repeat. I still can't write Python from a blank file. I can now read it a little, the way you can read a menu in a language you don't speak after two weeks in the country.&lt;/p&gt;

&lt;p&gt;The uncomfortable part: I understand maybe seventy percent of what runs on my server, and that number is my ceiling, not my floor. If the auth breaks in some new way, I'm back to pasting errors and hoping. A real developer would find that unacceptable. For a marketer with 400 URLs and zero backlog priority, it beats the alternative, which was nothing.&lt;/p&gt;

&lt;p&gt;So, a question for the people here who do this properly. Where is the line where a script like this deserves real engineering? Tests, retries, an actual database. Or is duct tape the right amount of engineering for something with one user who is also the author? I genuinely can't tell, and I'd rather hear it from you than from the model that wrote the duct tape.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>seo</category>
      <category>claudecode</category>
    </item>
  </channel>
</rss>
