<?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: AgustaON</title>
    <description>The latest articles on DEV Community by AgustaON (@agustaon).</description>
    <link>https://dev.to/agustaon</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%2F4023533%2Fc1074d80-98b1-4f85-a381-96b2ec13a2a3.gif</url>
      <title>DEV Community: AgustaON</title>
      <link>https://dev.to/agustaon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/agustaon"/>
    <language>en</language>
    <item>
      <title>Four AI APIs, four completely different citation formats. Getting the data out was harder than the analysis.</title>
      <dc:creator>AgustaON</dc:creator>
      <pubDate>Sat, 01 Aug 2026 09:03:48 +0000</pubDate>
      <link>https://dev.to/agustaon/four-ai-apis-four-completely-different-citation-formats-getting-the-data-out-was-harder-than-the-25c3</link>
      <guid>https://dev.to/agustaon/four-ai-apis-four-completely-different-citation-formats-getting-the-data-out-was-harder-than-the-25c3</guid>
      <description>&lt;p&gt;I've been running a &lt;a href="https://greaterthanservices.com/blog/ai-search-study-aesthetic-clinics" rel="noopener noreferrer"&gt;small monthly study&lt;/a&gt;: take ten buyer-intent questions from one industry, ask four AI assistants the identical question, and record every source each one actually read.&lt;/p&gt;

&lt;p&gt;The analysis is the easy bit. Set overlap, count the domains, done.&lt;/p&gt;

&lt;p&gt;Getting the citations out of four different APIs in a shape you can compare — that's where I lost a weekend. Writing it up because I couldn't find this anywhere when I needed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;All four providers will tell you which URLs they used. None of them agree on where to put that, what to call it, or even what a "URL" is.&lt;/p&gt;

&lt;p&gt;Here's roughly what each one gives you. (Shapes as of mid-2026 — they move, check the docs before you copy this.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenAI&lt;/strong&gt;, Responses API with the web search tool. Citations ride along as annotations on the text output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;urls&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="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &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;block&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &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;a&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;annotations&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;url_citation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&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;strong&gt;Anthropic&lt;/strong&gt;, Messages API with the web search tool. Search results come back as their own content block type, separate from the text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;urls&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="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;web_search_tool_result&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="k"&gt;for &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;r&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &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;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&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;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&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;strong&gt;Gemini&lt;/strong&gt;, with Google Search grounding. It's on the candidate, under grounding metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;candidates&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="nx"&gt;groundingMetadata&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;groundingChunks&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[])&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;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;web&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Perplexity&lt;/strong&gt; is the friendly one — a flat array on the response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;citations&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search_results&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="o"&gt;=&amp;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;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four different nesting depths, three different words for the same concept ("annotation", "search result", "grounding chunk"), and one of them isn't giving you the URL at all. More on that in a second.&lt;/p&gt;

&lt;p&gt;So: one adapter per provider, normalise to &lt;code&gt;{ engine, question, urls[] }&lt;/code&gt;, and never let provider-shaped data past that boundary. Obvious in hindsight. I did not do this first and regretted it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 1: Gemini's grounding URIs aren't the source
&lt;/h2&gt;

&lt;p&gt;This is the one that would have quietly wrecked my numbers.&lt;/p&gt;

&lt;p&gt;Gemini's &lt;code&gt;groundingChunks[].web.uri&lt;/code&gt; isn't the publisher's URL. It's a redirect through Google's own grounding endpoint. If you take it at face value, every single Gemini citation looks like it came from the same domain, and your "which sites does Gemini read" analysis returns exactly one site.&lt;/p&gt;

&lt;p&gt;You have to resolve it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveFinal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HEAD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;follow&lt;/span&gt;&lt;span class="dl"&gt;"&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&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;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// keep the original, flag it, move on&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;Two notes from doing this at volume. &lt;code&gt;HEAD&lt;/code&gt; is enough and much cheaper than &lt;code&gt;GET&lt;/code&gt; — you only want the final URL. And cache aggressively, because the same redirect will come up over and over and you don't want to hammer anything.&lt;/p&gt;

&lt;p&gt;I noticed this because Gemini's numbers looked absurd on the first run. Worth building a sanity check that screams when one engine's domain diversity collapses to near-zero — that's almost always an extraction bug, not a finding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 2: the same page in five costumes
&lt;/h2&gt;

&lt;p&gt;Once you have real URLs, they still won't match each other. All of these are the same page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://www.example.com/clinic/
http://example.com/clinic
https://example.com/clinic?utm_source=chatgpt.com
https://example.com/clinic#reviews
https://example.com/Clinic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're comparing sets, five spellings of one page means five "different" sources, and your overlap number comes out lower than reality. Which, if your whole finding is &lt;em&gt;"they overlap surprisingly little"&lt;/em&gt;, is the exact direction you don't want an unforced error pointing.&lt;/p&gt;

&lt;p&gt;The canonicaliser I settled on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TRACKING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;utm_|fbclid|gclid|msclkid|ref|source$&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&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;u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^www&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &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;k&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TRACKING&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&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;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&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;Deliberately &lt;em&gt;not&lt;/em&gt; lowercasing the path — paths are case-sensitive on plenty of servers and I'd rather over-count than merge two genuinely different pages.&lt;/p&gt;

&lt;p&gt;One that made me laugh: several assistants append &lt;code&gt;?utm_source=chatgpt.com&lt;/code&gt; (or their own equivalent) to the URLs they hand back. The tools are tagging their own referral traffic. Strip it, or you'll have engine-specific duplicates of the same page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then the analysis, which really is boring
&lt;/h2&gt;

&lt;p&gt;Set overlap per question, pairwise across engines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jaccard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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;A&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;B&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&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;inter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;A&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;B&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;length&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;union&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;B&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nx"&gt;size&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;union&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;inter&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;union&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jaccard has a real flaw here: it treats a source read once exactly the same as one read by every engine on every question. I've gone back and forth on weighting it and haven't convinced myself either way. If you've got a better metric for "did these two systems consult the same evidence", I'd genuinely like to hear it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the numbers looked like
&lt;/h2&gt;

&lt;p&gt;This month's run was &lt;a href="https://greaterthanservices.com/blog/ai-search-study-aesthetic-clinics" rel="noopener noreferrer"&gt;cosmetic and aesthetic clinics&lt;/a&gt;. Ten questions, nine cities, four engines.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;214&lt;/strong&gt; different websites read across just ten questions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;75%&lt;/strong&gt; of those were read by one engine and no other&lt;/li&gt;
&lt;li&gt;Overlap between any two engines on the same question: &lt;strong&gt;7%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Exactly &lt;strong&gt;1&lt;/strong&gt; question out of ten produced a website all four read&lt;/li&gt;
&lt;li&gt;About &lt;strong&gt;13%&lt;/strong&gt; of what they read was a directory; the rest was businesses' own sites&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one flipped between industries, which I didn't expect — in &lt;a href="https://greaterthanservices.com/blog/ai-search-study-law-firms" rel="noopener noreferrer"&gt;the law firms run last month&lt;/a&gt;, directory domains dominated the top of the list.&lt;/p&gt;

&lt;p&gt;The objection I had immediately, and you probably do too: LLMs are non-deterministic, so of course two calls differ — this could be noise.&lt;/p&gt;

&lt;p&gt;So I re-ran a subset three times per engine and compared each engine against &lt;em&gt;itself&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Same engine, asked again: &lt;strong&gt;51%&lt;/strong&gt; overlap&lt;/li&gt;
&lt;li&gt;Different engines, same run: &lt;strong&gt;7%&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;About 7.5× more consistent with itself than with its competitors. There's real run-to-run variance, but it's much smaller than the gap between engines. The divergence is structural, not sampling.&lt;/p&gt;

&lt;p&gt;If you want to poke at that from the other direction, I wrote up &lt;a href="https://greaterthanservices.com/blog/is-my-website-visible-to-perplexity" rel="noopener noreferrer"&gt;whether a single assistant can even see a given site&lt;/a&gt; separately — same problem, one engine at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I can't claim
&lt;/h2&gt;

&lt;p&gt;Ten questions in one vertical is a sample, not a census. The variance control was two questions re-run three times, not the whole set. "Read" only means the URL appeared in that response's citations — I can't see weighting, or how much of the page actually mattered. And all of it is a July 2026 snapshot; these systems change under you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway for anyone building on this
&lt;/h2&gt;

&lt;p&gt;If you're writing anything that compares retrieval across providers, budget most of your time for extraction and normalisation, not analysis. My rough split ended up 80/20 in favour of the boring part.&lt;/p&gt;

&lt;p&gt;And build the sanity checks early. Both of my real bugs — the redirect one and the tracking-param one — were invisible in the output. The code ran fine. The numbers were just wrong, in a direction that happened to flatter my hypothesis. That's the kind of bug worth being paranoid about.&lt;/p&gt;

&lt;p&gt;The wider point, if you take one thing away: treating &lt;a href="https://greaterthanservices.com/ai-search-optimization" rel="noopener noreferrer"&gt;"AI visibility" as a single channel&lt;/a&gt; doesn't survive contact with the data. Four engines built almost entirely separate reading lists from the same question, so it's four problems, not one.&lt;/p&gt;

&lt;p&gt;Full write-up of this month's run, with the question-by-question breakdown, is &lt;a href="https://greaterthanservices.com/blog/ai-search-study-aesthetic-clinics" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Happy to share the raw JSONL if you want to check my counts — I'd rather be corrected than cited.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Disclosure: I build &lt;a href="https://greaterthanservices.com" rel="noopener noreferrer"&gt;a tool in this space&lt;/a&gt;. The study isn't gated and there's no signup on it.)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How do you measure something that gives a different answer every time?</title>
      <dc:creator>AgustaON</dc:creator>
      <pubDate>Wed, 29 Jul 2026 06:44:55 +0000</pubDate>
      <link>https://dev.to/agustaon/how-do-you-measure-something-that-gives-a-different-answer-every-time-55m5</link>
      <guid>https://dev.to/agustaon/how-do-you-measure-something-that-gives-a-different-answer-every-time-55m5</guid>
      <description>&lt;p&gt;I had a simple-sounding question: does ChatGPT recommend this business?&lt;/p&gt;

&lt;p&gt;You'd think you just ask it. Ask ChatGPT "best personal injury law firm in NYC", see if the business is named, record yes or no.&lt;/p&gt;

&lt;p&gt;That works exactly once. Ask again an hour later and you might get a different answer. Not slightly different — potentially a completely different set of firms and a completely different set of cited sources.&lt;/p&gt;

&lt;p&gt;Which means the naive version of this measurement is worthless. You're not measuring visibility, you're sampling a distribution once and calling it a fact.&lt;/p&gt;

&lt;p&gt;This is the same problem anyone gets when they try to test an LLM-backed feature. Your normal testing instinct — same input, assert on output — just doesn't apply. So here's how I ended up designing around it, and the numbers that came out, which surprised me.&lt;/p&gt;

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

&lt;p&gt;I wanted to compare four assistants (GPT-4o, Claude Haiku 4.5, Gemini 2.5 Flash, Perplexity Sonar, all with web search on) across 10 buyer-intent questions in one vertical. Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Best personal injury law firm in New York City?"
"Top immigration lawyers in Mumbai?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each response I recorded two things: which businesses got named, and which URLs got cited. The cited sources come from each API's own citation metadata, so that part is structured — no scraping the prose.&lt;/p&gt;

&lt;p&gt;First pass, the results looked dramatic. The four assistants barely agreed on anything. Different firms, different sources, almost no overlap.&lt;/p&gt;

&lt;p&gt;Great finding. Except I couldn't publish it, because there was an obvious objection I couldn't answer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maybe they weren't disagreeing with each other. Maybe each one was just disagreeing with itself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a single assistant returns wildly different sources run to run, then "these four models cite different things" is a meaningless statement. You'd be measuring noise and calling it signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The control
&lt;/h2&gt;

&lt;p&gt;The fix is the same idea as a control group. Measure the thing you're worried about, separately, and see if it explains your result.&lt;/p&gt;

&lt;p&gt;So: re-run all 10 questions &lt;strong&gt;3× per assistant&lt;/strong&gt;, and compute two different numbers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cross-consistency&lt;/strong&gt; — how much do two &lt;em&gt;different&lt;/em&gt; assistants overlap on the same question?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-consistency&lt;/strong&gt; — how much does the &lt;em&gt;same&lt;/em&gt; assistant overlap with itself on the same question, asked again?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If those two numbers are close, the divergence is just non-determinism and I have nothing. If self-consistency is much higher, the models genuinely differ.&lt;/p&gt;

&lt;p&gt;For "overlap" I used Jaccard similarity on the sets of cited domains — intersection over union. Standard, boring, and easy to defend when someone asks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;jaccard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;setA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setB&lt;/span&gt;&lt;span class="p"&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;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;setA&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;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;setB&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;intersection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &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;x&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;intersection&lt;/span&gt;&lt;span class="o"&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;union&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;intersection&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;intersection&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;union&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;Cross-consistency for a question is the mean pairwise Jaccard across the 6 assistant pairs. Self-consistency is the mean pairwise Jaccard across the 3 runs of a single assistant. Then average across questions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;meanPairwise&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scores&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="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="o"&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;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;jaccard&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="nx"&gt;i&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="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&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="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;Two normalisation details that matter more than they sound:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Normalise to registrable domain.&lt;/strong&gt; &lt;code&gt;www.example.com/page-a&lt;/code&gt; and &lt;code&gt;example.com/page-b&lt;/code&gt; are the same source for this purpose. If you don't collapse those, you'll measure URL formatting instead of source overlap and your numbers will be garbage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decide what counts as a source and write it down.&lt;/strong&gt; I counted only domains that appeared in the API's citation metadata, not domains mentioned in prose. Defensible either way, but you have to pick one and say which.&lt;/p&gt;

&lt;h2&gt;
  
  
  What came back
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cross-assistant overlap: ~7%.&lt;/strong&gt; Two different assistants asked the same question cited almost entirely different sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-consistency: ~51%.&lt;/strong&gt; Same assistant, same question, asked again — about half the sources repeat.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So an assistant is roughly &lt;strong&gt;7.5× more consistent with itself than with its rivals&lt;/strong&gt;. The divergence between models is real. The control did its job.&lt;/p&gt;

&lt;p&gt;But look at that self-consistency number again, because it's the more interesting one. &lt;strong&gt;Ask the same model the same question twice and half the cited sources change.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And it isn't uniform:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Assistant&lt;/th&gt;
&lt;th&gt;Sources repeating on a second ask&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Perplexity&lt;/td&gt;
&lt;td&gt;~72%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude&lt;/td&gt;
&lt;td&gt;~54%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini&lt;/td&gt;
&lt;td&gt;~42%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT&lt;/td&gt;
&lt;td&gt;~32%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Roughly two-thirds of what ChatGPT cites changes between two identical questions. Perplexity is more than twice as stable.&lt;/p&gt;

&lt;p&gt;That spread is the practically useful finding. If you're building anything that depends on retrieval consistency, your choice of provider changes how reproducible your system is by a factor of two — before you've written a line of your own code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The generalisable bit
&lt;/h2&gt;

&lt;p&gt;If you're testing anything LLM-backed, single-run assertions are lying to you. A few things I'd carry to any similar problem:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run n times and report a distribution, not a value.&lt;/strong&gt; n=3 was enough to separate signal from noise here. It won't be for everything — if your effect size is smaller, you need more runs. Sanity-check by increasing n and seeing whether your headline number moves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure your noise floor explicitly.&lt;/strong&gt; The self-consistency number &lt;em&gt;is&lt;/em&gt; the noise floor. Any difference between two things you're comparing has to clear it to mean anything. Most "we compared model A and B" posts skip this, which is why most of them are unreliable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assert on properties, not exact outputs.&lt;/strong&gt; "Response cites at least one source from an allowed list" survives non-determinism. "Response equals this string" does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pin what you can.&lt;/strong&gt; Model version, &lt;code&gt;temperature&lt;/code&gt;, whether web search is on, region. My biggest regret is not pinning model versions harder — a provider updating a model mid-study would have quietly invalidated everything, and I'd have had no way to detect it after the fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I can't claim
&lt;/h2&gt;

&lt;p&gt;Two things worth being explicit about, since this space is full of overclaiming:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is a snapshot.&lt;/strong&gt; July 2026, one vertical, 10 questions. Retrieval behaviour changes with model updates. The 32% figure for ChatGPT is not a constant of nature — it's what I measured, in that window, with that method.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More runs would be better.&lt;/strong&gt; n=3 is enough to establish that self-consistency &amp;gt;&amp;gt; cross-consistency, which is the claim I'm making. It is not enough to say Claude's 54% is meaningfully different from Gemini's 42%. Those two could easily swap places with more sampling, and I wouldn't defend that ordering.&lt;/p&gt;

&lt;p&gt;If someone wants to run it with n=20 and tell me I'm wrong, genuinely great — that's a better dataset than mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data
&lt;/h2&gt;

&lt;p&gt;Full write-up with all 10 questions, every business named, every source cited, and the method in more detail:&lt;br&gt;
&lt;a href="https://greaterthanservices.com/blog/ai-search-study-law-firms" rel="noopener noreferrer"&gt;https://greaterthanservices.com/blog/ai-search-study-law-firms&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disclosure: I run &lt;a href="https://greaterthanservices.com" rel="noopener noreferrer"&gt;Greater Than Services&lt;/a&gt;, which does this measurement as a product — the variance control exists because customers were reasonably asking "how do you know this isn't random?" The method above is the answer, and it's not proprietary, so take it and run it on your own vertical if it's useful.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you've built variance controls for LLM-dependent systems I'd like to hear how you handled it — particularly how you picked n, which is the part I'm least confident I got right.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>datascience</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My company ranked #1 on Google but ChatGPT had never heard of us. Here's what I did about it.</title>
      <dc:creator>AgustaON</dc:creator>
      <pubDate>Mon, 20 Jul 2026 08:25:14 +0000</pubDate>
      <link>https://dev.to/agustaon/my-company-ranked-1-on-google-but-chatgpt-had-never-heard-of-us-heres-what-i-did-about-it-241o</link>
      <guid>https://dev.to/agustaon/my-company-ranked-1-on-google-but-chatgpt-had-never-heard-of-us-heres-what-i-did-about-it-241o</guid>
      <description>&lt;p&gt;Last month my manager forwarded me a Slack message from our founder that just said: "Ask ChatGPT what our company does. Then panic."&lt;/p&gt;

&lt;p&gt;So I did. I opened ChatGPT and typed our company name. It confidently told me about a &lt;em&gt;completely different&lt;/em&gt; company with a similar name. Then I asked it to "recommend a good [our category] tool" — the exact thing we sell — and it named three competitors. Not us. Never us.&lt;/p&gt;

&lt;p&gt;Here's the kicker: we rank #1 on Google for our main keywords. Our SEO is genuinely good. And it turns out that means almost nothing to an LLM.&lt;/p&gt;

&lt;p&gt;I went down a rabbit hole for two weeks. This is the practical version of what I learned, with the actual code that moved the needle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "good SEO" doesn't transfer to AI
&lt;/h2&gt;

&lt;p&gt;Google crawls, indexes, and ranks &lt;em&gt;pages&lt;/em&gt;. Its algorithm is built around links, keywords, freshness, and hundreds of ranking signals. You can win that game with backlinks and on-page optimization.&lt;/p&gt;

&lt;p&gt;LLMs work differently. When you ask ChatGPT or Perplexity "what's the best X," the model isn't ranking a list of pages — it's generating an answer from an internal representation of the world, sometimes augmented with live retrieval. For your business to show up, the model needs to have formed a &lt;strong&gt;clear, confident, consistent understanding&lt;/strong&gt; of what you are.&lt;/p&gt;

&lt;p&gt;That understanding comes from:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;How clearly your own site describes you&lt;/strong&gt; (in plain language, not marketing-speak)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How consistently you're described everywhere else&lt;/strong&gt; (directories, social profiles, other sites)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whether machines can parse your site at all&lt;/strong&gt; (structured data, semantic HTML)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We were failing all three, and none of them showed up in a normal SEO audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: Our homepage was written for humans who already knew us
&lt;/h2&gt;

&lt;p&gt;Our hero section said something like &lt;em&gt;"The intelligent growth platform for modern teams."&lt;/em&gt; Beautiful. Meaningless to a model. An LLM reading that has no idea what we sell.&lt;/p&gt;

&lt;p&gt;The fix was boring but effective: we rewrote the first paragraph of the homepage to be almost aggressively literal. One sentence that a stranger — or a model — could repeat back accurately:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"[Company] is a [exact product category] that helps [specific audience] do [specific outcome]."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's it. No adjectives doing load-bearing work. After the change, re-testing showed the model could actually describe us.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: No structured data
&lt;/h2&gt;

&lt;p&gt;This is the technical one and where devs can make the biggest difference fast.&lt;/p&gt;

&lt;p&gt;Most sites have zero &lt;a href="https://greaterthanservices.com/docs/schema-types" rel="noopener noreferrer"&gt;JSON-LD&lt;/a&gt; structured data. It's invisible to human visitors but it's a direct, machine-readable statement of &lt;em&gt;what this entity is&lt;/em&gt;. If you give an AI crawler explicit facts, it doesn't have to guess.&lt;/p&gt;

&lt;p&gt;Here's the minimum I'd add to any company homepage — a &lt;code&gt;&amp;lt;script type="application/ld+json"&amp;gt;&lt;/code&gt; block in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@context&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="s2"&gt;https://schema.org&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="s2"&gt;@type&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="s2"&gt;Organization&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="s2"&gt;name&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="s2"&gt;Your Company&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="s2"&gt;url&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="s2"&gt;https://yourcompany.com&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="s2"&gt;description&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="s2"&gt;A one-sentence, plain-language description of exactly what you sell and who it's for.&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="s2"&gt;sameAs&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="s2"&gt;https://www.linkedin.com/company/yourcompany&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="s2"&gt;https://twitter.com/yourcompany&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="s2"&gt;https://github.com/yourcompany&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="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sameAs&lt;/code&gt; array matters more than it looks — it links your entity to every other profile that describes you, which reinforces consistency (problem 3).&lt;/p&gt;

&lt;p&gt;If you sell specific products, add &lt;code&gt;Product&lt;/code&gt; schema too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@context&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="s2"&gt;https://schema.org&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="s2"&gt;@type&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="s2"&gt;Product&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="s2"&gt;name&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="s2"&gt;Product Name&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="s2"&gt;description&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="s2"&gt;What it does, in plain language.&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="s2"&gt;brand&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="s2"&gt;@type&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="s2"&gt;Brand&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="s2"&gt;name&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="s2"&gt;Your Company&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="s2"&gt;offers&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="s2"&gt;@type&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="s2"&gt;Offer&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="s2"&gt;price&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="s2"&gt;4.99&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="s2"&gt;priceCurrency&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="s2"&gt;USD&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="s2"&gt;availability&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="s2"&gt;https://schema.org/InStock&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="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For content pages, &lt;code&gt;FAQPage&lt;/code&gt; and &lt;code&gt;Article&lt;/code&gt; schema help LLMs extract answers directly. The pattern is always the same: &lt;strong&gt;stop making the machine infer, and just tell it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Validate everything with Google's Rich Results Test or Schema.org's validator before shipping — malformed JSON-LD does nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: We described ourselves five different ways
&lt;/h2&gt;

&lt;p&gt;Our website said one thing, our LinkedIn bio said another, our Crunchbase entry was three years out of date, and our Twitter bio was a joke (literally, it was a pun). To a model trying to build a confident picture, that inconsistency is noise. It'll favor a competitor whose story is the same everywhere.&lt;/p&gt;

&lt;p&gt;Fixing this wasn't code — it was a boring afternoon of copy-pasting one canonical description into every profile we own. But it measurably helped.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part where I admit I used a tool
&lt;/h2&gt;

&lt;p&gt;I could check some of this manually — read our own copy, eyeball the markup, run the schema validators. But two things were painful to do by hand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Writing correct JSON-LD for every page type&lt;/strong&gt; from scratch, matching the right schema.org vocabulary&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actually measuring&lt;/strong&gt; whether AI assistants mention us — you can't eyeball that, you have to query them with real buyer questions and check the answers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A teammate pointed me at &lt;a href="https://greaterthanservices.com" rel="noopener noreferrer"&gt;Greater Than Services&lt;/a&gt;, which runs an audit specifically for this (they split it into CRO / GEO / ASO — human, AI-search, and AI-agent readability). The useful part for me as a dev: instead of a report that says "add structured data," it generates the actual JSON-LD and meta tags for your specific site, ready to paste. It also runs the "does AI name you" test automatically across a few assistants, which is the measurement I couldn't be bothered to script myself.&lt;/p&gt;

&lt;p&gt;The free preview was enough to see our scores; the full report was a few dollars. I'm not affiliated with them — it just saved me from writing schema by hand for 6 page types, so it's earned a mention. There are other ways to do this; that's the one that worked for me. If you'd rather DIY, everything above is the manual path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 30-second test you should run right now
&lt;/h2&gt;

&lt;p&gt;Regardless of tooling, do this before you close the tab:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open ChatGPT (or Perplexity, or Gemini).&lt;/li&gt;
&lt;li&gt;Type: &lt;strong&gt;"What does [your company] sell, and who is it for?"&lt;/strong&gt; — do &lt;strong&gt;not&lt;/strong&gt; paste your URL.&lt;/li&gt;
&lt;li&gt;Read what comes back.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If it's vague, wrong, or blank, that's not a hypothetical future problem. That's real prospects, right now, asking an AI for exactly what you sell and getting pointed at someone else.&lt;/p&gt;

&lt;p&gt;We went from "ChatGPT thinks we're a different company" to "ChatGPT describes us correctly" in about two weeks, mostly with the three fixes above. The code part — structured data — was genuinely the highest-leverage thing, which is why I'm writing this on dev.to and not on LinkedIn.&lt;/p&gt;

&lt;p&gt;Ship the JSON-LD. Fix your copy. Be consistent. The models are already answering questions about your business — you just want to be in the answer.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Ran into weird edge cases with schema for a specific page type? Drop them in the comments — happy to compare notes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>ai</category>
      <category>javascript</category>
    </item>
    <item>
      <title>We looked at where AI actually gets its answers. Your homepage barely matters.</title>
      <dc:creator>AgustaON</dc:creator>
      <pubDate>Mon, 20 Jul 2026 08:24:45 +0000</pubDate>
      <link>https://dev.to/agustaon/we-looked-at-where-ai-actually-gets-its-answers-your-homepage-barely-matters-h18</link>
      <guid>https://dev.to/agustaon/we-looked-at-where-ai-actually-gets-its-answers-your-homepage-barely-matters-h18</guid>
      <description>&lt;p&gt;If you've read anything about "getting cited by AI" in the last year, the advice was probably some combination of: add structured data, write clearer copy, fix your headings. All reasonable. We build a tool that does exactly that kind of analysis, so I'm biased toward it being useful.&lt;/p&gt;

&lt;p&gt;Then we started logging &lt;em&gt;where&lt;/em&gt; the AI answers were actually pulling from — the source types cited when an assistant answers a buying question in a given category — and the data made me rethink the priority order.&lt;/p&gt;

&lt;p&gt;Company homepages were rarely the primary source.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the citation mix actually looks like
&lt;/h2&gt;

&lt;p&gt;When you ask ChatGPT, Perplexity, Gemini, or Claude something like "best project management tool for a small agency," the model (usually with web search on) assembles an answer from retrieved documents. Log what those documents are across a category and a pattern shows up fast. Roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Review platforms&lt;/strong&gt; — G2, Capterra, Trustpilot, category-specific review sites&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Industry directories&lt;/strong&gt; — niche listings, "best X tools" databases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Editorial roundups&lt;/strong&gt; — blog posts titled "12 best X for Y in 2026"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forums and communities&lt;/strong&gt; — Reddit threads, Stack Overflow, niche forums&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brand-owned sites&lt;/strong&gt; — usually the &lt;em&gt;smallest&lt;/em&gt; slice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The exact mix varies a lot by category — developer tools skew heavily toward forums and GitHub, local services skew toward review sites and maps — but brand sites consistently underperform the intuition.&lt;/p&gt;

&lt;p&gt;This makes sense once you think about retrieval rather than ranking. The model isn't looking for the most authoritative page about &lt;em&gt;you&lt;/em&gt;. It's looking for documents that compare options in your category. Your homepage says "we're the best." A G2 category page says "here are 40 tools, ranked, with reviews." Guess which one is more useful for answering a comparison question.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable implication
&lt;/h2&gt;

&lt;p&gt;You can have perfect technical SEO, flawless JSON-LD, a beautifully clear homepage — and still be absent from the answer, because the answer was assembled from four sources you're not on.&lt;/p&gt;

&lt;p&gt;That doesn't make on-page work pointless. It makes it &lt;strong&gt;necessary but not sufficient&lt;/strong&gt;, and it changes the order you should do things in:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Be present&lt;/strong&gt; on the source types your category's answers are built from&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be parseable&lt;/strong&gt; so that when the model does encounter you, it correctly understands what you are&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most advice covers step 2 exclusively. Step 1 is unglamorous, non-technical, and does more.&lt;/p&gt;

&lt;h2&gt;
  
  
  The technical half (step 2) — what actually helps
&lt;/h2&gt;

&lt;p&gt;Since this is dev.to, here's the part you can ship this afternoon.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make your entity unambiguous
&lt;/h3&gt;

&lt;p&gt;The single highest-leverage markup is &lt;code&gt;Organization&lt;/code&gt; schema with a populated &lt;code&gt;sameAs&lt;/code&gt;. It links your site to every other profile that represents you — which is exactly how a retrieval system corroborates that scattered mentions are the same entity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@context&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="s2"&gt;https://schema.org&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="s2"&gt;@type&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="s2"&gt;Organization&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="s2"&gt;name&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="s2"&gt;Your Company&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="s2"&gt;url&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="s2"&gt;https://yourcompany.com&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="s2"&gt;description&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="s2"&gt;One plain sentence: what you sell and who it's for.&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="s2"&gt;sameAs&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="s2"&gt;https://www.g2.com/products/your-company/reviews&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="s2"&gt;https://www.crunchbase.com/organization/your-company&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="s2"&gt;https://github.com/yourcompany&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="s2"&gt;https://www.linkedin.com/company/yourcompany&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="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note what's in that &lt;code&gt;sameAs&lt;/code&gt; array: the review site and the directory. You're explicitly connecting yourself to the sources that actually get cited.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write passages that survive extraction
&lt;/h3&gt;

&lt;p&gt;Retrieval works on chunks, not pages. A paragraph that only makes sense after reading the three above it is a bad chunk. A self-contained paragraph that answers one question is a good one.&lt;/p&gt;

&lt;p&gt;Practically, that means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Bad: requires context to be useful --&amp;gt;&lt;/span&gt;
&lt;span class="gu"&gt;## How it works&lt;/span&gt;
It integrates seamlessly and handles this automatically for you.

&lt;span class="c"&gt;&amp;lt;!-- Good: survives being ripped out of context --&amp;gt;&lt;/span&gt;
&lt;span class="gu"&gt;## How does [Product] handle rate limiting?&lt;/span&gt;
[Product] applies a token bucket per API key, defaulting to 100
requests per minute. Limits are configurable per plan, and 429
responses include a Retry-After header.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second one can be lifted directly into an answer with attribution. The first can't be lifted at all.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FAQPage&lt;/code&gt; schema formalizes this — it's a machine-readable declaration of "here is a question and here is its self-contained answer":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@context&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="s2"&gt;https://schema.org&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="s2"&gt;@type&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="s2"&gt;FAQPage&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="s2"&gt;mainEntity&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="s2"&gt;@type&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="s2"&gt;Question&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="s2"&gt;name&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="s2"&gt;How does the API handle rate limiting?&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="s2"&gt;acceptedAnswer&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="s2"&gt;@type&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="s2"&gt;Answer&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="s2"&gt;text&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="s2"&gt;A token bucket per API key, defaulting to 100 requests per minute, configurable per plan. 429 responses include a Retry-After header.&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="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Don't block the crawlers you want
&lt;/h3&gt;

&lt;p&gt;Easy to get wrong, especially if someone added a blanket rule during a scraping panic. Check your &lt;code&gt;robots.txt&lt;/code&gt; for the AI crawlers you actually want indexing you (&lt;code&gt;GPTBot&lt;/code&gt;, &lt;code&gt;PerplexityBot&lt;/code&gt;, &lt;code&gt;ClaudeBot&lt;/code&gt;, &lt;code&gt;Google-Extended&lt;/code&gt;, among others). Blocking them is a legitimate choice — just make sure it's a &lt;em&gt;choice&lt;/em&gt; and not a leftover.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparison pages are retrieval bait
&lt;/h3&gt;

&lt;p&gt;If the cited documents are mostly comparison content, one thing you control is publishing genuinely useful comparison content yourself. An honest "X vs Y" page — including where you lose — is far more likely to be retrieved for a comparison query than a features page. It also tends to be the highest-intent traffic you'll get.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest caveats
&lt;/h2&gt;

&lt;p&gt;Two things worth saying plainly, because a lot of content in this space overclaims:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nobody can guarantee AI citations.&lt;/strong&gt; You can improve the signals that correlate with being retrieved and understood. You cannot control what any model outputs next week. Anyone selling you a promised ranking in ChatGPT is selling you something they can't deliver.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is a moving target.&lt;/strong&gt; Retrieval behavior differs across engines and changes with model updates. What I described is a snapshot, not a law. Measure your own category rather than trusting a generic list — including this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check your own category
&lt;/h2&gt;

&lt;p&gt;Free version, takes ten minutes: open each of the four assistants, ask three or four buying questions a customer in your category would ask, and note which domains get cited. Not which companies get named — which &lt;em&gt;domains get linked&lt;/em&gt;. Tally them. The pattern usually appears within a dozen questions.&lt;/p&gt;

&lt;p&gt;If you'd rather not do it by hand, &lt;a href="https://greaterthanservices.com" rel="noopener noreferrer"&gt;Greater Than Services&lt;/a&gt; (what I work on) runs those queries across ChatGPT, Perplexity, Gemini and Claude and gives you the source-type breakdown for your specific category, plus the schema fixes as copy-paste code. Free preview, no signup. But the manual method above genuinely works and costs nothing — the point of this post is the finding, not the tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Your homepage's job is to be unambiguous when a model encounters it. Getting &lt;em&gt;encountered&lt;/em&gt; mostly happens elsewhere — on the review sites, directories, roundups and forums that AI answers are actually assembled from.&lt;/p&gt;

&lt;p&gt;Fix the markup. Then go get listed.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Curious what the citation mix looks like in other categories — if you run the manual check for yours, drop what you find in the comments. Especially interested in whether dev-tool categories really are as forum-heavy as they look.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>ai</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
