<?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: WEBVOGUE Technologies</title>
    <description>The latest articles on DEV Community by WEBVOGUE Technologies (@webvogue).</description>
    <link>https://dev.to/webvogue</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%2F4122263%2F7fac9ab7-7f80-4e4d-8cdb-58cada704635.png</url>
      <title>DEV Community: WEBVOGUE Technologies</title>
      <link>https://dev.to/webvogue</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/webvogue"/>
    <language>en</language>
    <item>
      <title>I asked ChatGPT and Gemini 50 questions my buyers ask. My SaaS showed up once.</title>
      <dc:creator>WEBVOGUE Technologies</dc:creator>
      <pubDate>Fri, 23 Oct 2026 11:58:12 +0000</pubDate>
      <link>https://dev.to/webvogue/i-asked-chatgpt-and-gemini-50-questions-my-buyers-ask-my-saas-showed-up-once-e55</link>
      <guid>https://dev.to/webvogue/i-asked-chatgpt-and-gemini-50-questions-my-buyers-ask-my-saas-showed-up-once-e55</guid>
      <description>&lt;p&gt;My product was named in &lt;strong&gt;1 of 50&lt;/strong&gt; AI answers. Its biggest competitor was named in &lt;strong&gt;19&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;These weren't vanity searches. They were 25 questions a real buyer types before they've heard of any brand, each asked on ChatGPT and on Gemini. Search Console doesn't show what ChatGPT or Gemini say about you, and a rank tracker can't either. So I measured it myself.&lt;/p&gt;

&lt;p&gt;Here's what came back, what surprised me, and the code I use to turn a pile of AI answers into numbers you can track.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; the product is DropPerch, a domain-expiry monitor I run, and I measured it with CiteGaps, which I also build. All the numbers below are from that one workspace, week of 12 October 2026.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;25 buyer questions&lt;/strong&gt;, written the way people search before they know any brand. For example: &lt;em&gt;"What is the best domain expiry monitoring software for small businesses?"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No brand names in the questions.&lt;/strong&gt; "Is DropPerch good?" tells you how an AI describes you, not whether it recommends you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two engines:&lt;/strong&gt; ChatGPT and Gemini. Collected live, the way a buyer sees them, sources included. I didn't use the bare model API, because without web search it doesn't show what buyers see or which pages it cites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One week, 50 answers.&lt;/strong&gt; For each answer I kept the full text, every brand named, its position, and every cited URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Brand&lt;/th&gt;
&lt;th&gt;Share of the 50 answers that named it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DropCatch&lt;/td&gt;
&lt;td&gt;38%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ExpiredDomains.net&lt;/td&gt;
&lt;td&gt;36%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DomCop&lt;/td&gt;
&lt;td&gt;32%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UptimeRobot&lt;/td&gt;
&lt;td&gt;24%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Watchman Tower&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DropPerch (mine)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One ChatGPT answer to that small-business question listed HetrixTools, UptimeRobot and DomainMOD. DropPerch wasn't in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things I didn't expect
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. I was cited more often than I was named
&lt;/h3&gt;

&lt;p&gt;DropPerch was &lt;strong&gt;named in 2%&lt;/strong&gt; of answers but &lt;strong&gt;linked as a source in 4%&lt;/strong&gt;. So in at least one answer, the engine used my site as a source and still recommended someone else.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;mention&lt;/strong&gt; (your brand in the text) and a &lt;strong&gt;citation&lt;/strong&gt; (your URL in the sources) are different signals, and they move separately. Track both.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The most-cited source was a competitor's own website
&lt;/h3&gt;

&lt;p&gt;The single most-cited source across the week was &lt;strong&gt;domcop.com: 102 citations in 50 answers.&lt;/strong&gt; DomCop is also on the competitor list, named in 32% of answers.&lt;/p&gt;

&lt;p&gt;That changed how I think about the problem. The engines get their shortlist from a handful of pages, and one of those pages is a competitor explaining the category in its own words. If my site and the pages around it don't answer the same questions as clearly, I'm not on the shortlist.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. A forum thread was doing my competitors' marketing
&lt;/h3&gt;

&lt;p&gt;One answer that named DropCatch, ExpiredDomains.net and DomCop, and not DropPerch, cited a discussion thread recommending DropCatch and ExpiredDomains.net. A thread an AI engine quotes is free marketing for whoever it names. A genuine, disclosed reply there can be worth more than another blog post on my own domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code: turning answers into three numbers
&lt;/h2&gt;

&lt;p&gt;Once you have the answers, you only need three numbers. Here's the scoring logic, simplified from what I run in production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;brands&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}[];&lt;/span&gt; &lt;span class="c1"&gt;// in the order the answer named them&lt;/span&gt;
  &lt;span class="nl"&gt;citedDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;                        &lt;span class="c1"&gt;// e.g. ["domcop.com", "reddit.com"]&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;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Answer&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;brand&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="c1"&gt;// Your position in each answer, or null when you weren't named.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;positions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;answers&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;a&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="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;brands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&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;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&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="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&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;cites&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;d&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// count subdomains too&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// How often you make the shortlist at all.&lt;/span&gt;
    &lt;span class="na"&gt;visibilityShare&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// How often the engine uses your own site as a source.&lt;/span&gt;
    &lt;span class="na"&gt;citationShare&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&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="o"&gt;=&amp;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;citedDomains&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cites&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Where you appear when you DO appear. Never average in the misses.&lt;/span&gt;
    &lt;span class="na"&gt;avgPosition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;positions&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;positions&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;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&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;s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;p&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;positions&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="kc"&gt;null&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;Run the same function for each competitor and you get the table above.&lt;/p&gt;

&lt;p&gt;Getting &lt;code&gt;brands&lt;/code&gt; out of free text is the fiddly part. I give each answer to an LLM with a strict JSON shape, &lt;code&gt;{"brands":[{"name": string, "sentiment": "positive"|"neutral"|"negative"|"mixed"}]}&lt;/code&gt;, and take each brand's position from the order it returns. Sentiment comes for free, and it matters: being named often but described badly is its own problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rules that keep the numbers honest
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep the question set fixed&lt;/strong&gt; week to week. Change the questions and you've changed the measurement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exclude failed answers&lt;/strong&gt; instead of counting them as "not named".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mind the sample size.&lt;/strong&gt; With 50 answers, one answer is 2 percentage points. A 3-point jump can be noise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the trend, not the week.&lt;/strong&gt; AI answers change from one session to the next. One week is a sample, not a verdict.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bonus: see ChatGPT's clicks in your analytics
&lt;/h2&gt;

&lt;p&gt;When ChatGPT links to a page, it adds &lt;code&gt;utm_source=chatgpt.com&lt;/code&gt; to the URL. That's from &lt;a href="https://help.openai.com/en/articles/12627856-publishers-and-developers-faq" rel="noopener noreferrer"&gt;OpenAI's publisher FAQ&lt;/a&gt;. In GA4, look under &lt;strong&gt;Reports → Acquisition → Traffic acquisition&lt;/strong&gt; for &lt;code&gt;chatgpt.com&lt;/code&gt; as a source. If your visibility share rises but those visits don't, the answers mention you without linking to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm doing about it
&lt;/h2&gt;

&lt;p&gt;Nothing clever. Just the list the data gave me:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Get onto the third-party pages the engines already cite:&lt;/strong&gt; the directories, comparison lists and threads in the sources, before any new content of my own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer the exact questions I'm missing.&lt;/strong&gt; A page per lost question, with the specifics a buyer needs, not a generic "features" page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A comparison page against the brand at 38%&lt;/strong&gt;, because a buyer comparing tools asks exactly that question.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reply, with disclosure, in the threads that get cited.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-run the same 50 answers every week&lt;/strong&gt; and watch the trend.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I'll post the follow-up when there's a trend worth showing, good or bad.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on your own product
&lt;/h2&gt;

&lt;p&gt;You can do all of this by hand with a spreadsheet: write 25 buyer questions, ask each one in a fresh chat, and log who's named and what's cited. Reading 50 answers a week gets old fast, which is why I automated it.&lt;/p&gt;

&lt;p&gt;If you want a quick look first, there's a &lt;a href="https://citegaps.com/free-check?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=chatgpt-50-answers" rel="noopener noreferrer"&gt;free check&lt;/a&gt;: one question, live answers from ChatGPT and Gemini, no sign-up. I also wrote up &lt;a href="https://citegaps.com/blog/ai-answer-engine-visibility" rel="noopener noreferrer"&gt;what each of these metrics means and how to read them together&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Have you checked what ChatGPT says when someone asks for a tool like yours?&lt;/strong&gt; I'm curious whether anyone else is getting cited without being named. Tell me in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>seo</category>
      <category>webdev</category>
      <category>startup</category>
    </item>
    <item>
      <title>What I learned checking 65 client domains with RDAP instead of WHOIS</title>
      <dc:creator>WEBVOGUE Technologies</dc:creator>
      <pubDate>Sat, 12 Sep 2026 15:17:20 +0000</pubDate>
      <link>https://dev.to/webvogue/what-i-learned-checking-65-client-domains-with-rdap-instead-of-whois-5cd3</link>
      <guid>https://dev.to/webvogue/what-i-learned-checking-65-client-domains-with-rdap-instead-of-whois-5cd3</guid>
      <description>&lt;p&gt;I built a domain expiry monitor, and to test it against something other than my own domains I pointed it at the "our work" pages of 30 US web design agencies — the client sites they link to publicly — and checked every domain against its registry.&lt;/p&gt;

&lt;p&gt;The results were mildly alarming, but the interesting part was everything that went wrong on the way there.&lt;/p&gt;

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

&lt;p&gt;65 client domains across 14 agencies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1 had already expired&lt;/strong&gt;, 60 days earlier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3 were inside 45 days&lt;/strong&gt;, the nearest with 3 days left&lt;/li&gt;
&lt;li&gt;the rest ran from 46 days to several years out, clustering at 3–6 months&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm not naming the agencies or the domains. Telling someone privately that their client's domain is about to lapse is useful; publishing it is exposing somebody's client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why RDAP and not WHOIS
&lt;/h2&gt;

&lt;p&gt;WHOIS returns free text in roughly as many formats as there are registrars. RDAP (RFC 7482/9083) returns JSON, over HTTPS, with a documented shape.&lt;/p&gt;

&lt;p&gt;The single nicest property: &lt;strong&gt;a 404 is an answer.&lt;/strong&gt; In RDAP it means "this domain is not registered", which is precisely the event worth alerting on. No string matching on "No match for domain".&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;res&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;domain/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/rdap+json&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;404&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="na"&gt;registered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Three things that will bite you
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The registrar's name is hidden in a jCard
&lt;/h3&gt;

&lt;p&gt;There's no &lt;code&gt;registrar&lt;/code&gt; field. The registrar is an entity with the &lt;code&gt;registrar&lt;/code&gt; role, and its human-readable name lives inside a vCard-in-JSON structure:&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;entity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entities&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;registrar&lt;/span&gt;&lt;span class="dl"&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;card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;vcardArray&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="p"&gt;[]&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;f&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fn&lt;/span&gt;&lt;span class="dl"&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;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;   &lt;span class="c1"&gt;// "GoDaddy.com, LLC"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;vcardArray[1]&lt;/code&gt; is an array of &lt;code&gt;["fn", {}, "text", "GoDaddy.com, LLC"]&lt;/code&gt; triples. That awkwardness is why so many tools show you IANA registrar ID 146 instead of a name — and "where do I renew this" is the actual operational question for anyone holding domains across several registrars.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. IANA's bootstrap does not cover every TLD
&lt;/h3&gt;

&lt;p&gt;The bootstrap file maps TLDs to RDAP servers, and &lt;code&gt;.de&lt;/code&gt;, &lt;code&gt;.co&lt;/code&gt; and &lt;code&gt;.gg&lt;/code&gt; simply aren't in it. There's no authoritative endpoint to ask.&lt;/p&gt;

&lt;p&gt;The right response is to say so. "The registry publishes no expiry date" is a fact about the registry, not about the domain, and it's more useful to a user than a number you inferred from somewhere else. Any tool that confidently shows an expiry date for a &lt;code&gt;.de&lt;/code&gt; is guessing.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Public suffixes
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;cartwright.co.uk&lt;/code&gt; naively reduces to &lt;code&gt;co.uk&lt;/code&gt;. Query that and you get the registry's own record, then cheerfully report the registry's expiry date as your user's domain expiry. The full Public Suffix List is thousands of entries; even a small hardcoded set of &lt;code&gt;co.uk&lt;/code&gt;, &lt;code&gt;com.au&lt;/code&gt;, &lt;code&gt;co.nz&lt;/code&gt; and friends prevents the embarrassing version of this bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that nearly discredited the whole thing
&lt;/h2&gt;

&lt;p&gt;My first extractor took every &lt;code&gt;href&lt;/code&gt; on the page. For one agency it confidently reported &lt;code&gt;licdn.com&lt;/code&gt;, &lt;code&gt;hotjar.com&lt;/code&gt;, &lt;code&gt;addtoany.com&lt;/code&gt; and &lt;code&gt;gmpg.org&lt;/code&gt; as their clients.&lt;/p&gt;

&lt;p&gt;All four came from the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;: &lt;code&gt;&amp;lt;link rel="preconnect"&amp;gt;&lt;/code&gt; hints and the XFN profile link WordPress writes into every page.&lt;/p&gt;

&lt;p&gt;I started adding them to a blocklist, then stopped — every new analytics vendor adds another entry, and the blocklist can never be finished. The structural rule can:&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="c1"&gt;// Only anchors. A person clicks anchors, and only anchors point at clients.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;re&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&amp;lt;a&lt;/span&gt;&lt;span class="se"&gt;\b[^&lt;/span&gt;&lt;span class="sr"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;?\b&lt;/span&gt;&lt;span class="sr"&gt;href&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*=&lt;/span&gt;&lt;span class="se"&gt;\s&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="se"&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="se"&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="se"&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="se"&gt;([^\s&lt;/span&gt;&lt;span class="sr"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="se"&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;/gi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line, and the entire category of failure disappears.&lt;/p&gt;

&lt;p&gt;A related one: an agency's &lt;em&gt;second&lt;/em&gt; domain isn't the agency's client. One firm's site was at &lt;code&gt;atendesigngroup.com&lt;/code&gt; and they also owned &lt;code&gt;aten.io&lt;/code&gt;; another at &lt;code&gt;openmediafoundation.org&lt;/code&gt; owned &lt;code&gt;open.media&lt;/code&gt;. Matching only the exact domain you crawled isn't enough — comparing the leading label catches both.&lt;/p&gt;

&lt;h2&gt;
  
  
  About a third of sites hard-block automated requests
&lt;/h2&gt;

&lt;p&gt;This one changed the architecture. I wrote the fetcher with &lt;code&gt;fetch&lt;/code&gt;, and got &lt;strong&gt;403 on every single site&lt;/strong&gt;, including with a complete set of genuine browser headers. Not a Cloudflare JS challenge you can wait out — a flat 403, identical in headless Chrome and in a real headed one. It's blocking by IP reputation.&lt;/p&gt;

&lt;p&gt;I switched to driving a real browser, which fixed it for about two thirds of sites. That turned out to matter for a second reason I hadn't considered: &lt;strong&gt;most agency portfolios render their client grid client-side&lt;/strong&gt;, in Webflow or React. Even a successful &lt;code&gt;fetch&lt;/code&gt; would have returned markup with no client links in it, and I'd have spent a day blaming my extractor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The unglamorous finding
&lt;/h2&gt;

&lt;p&gt;Only &lt;strong&gt;14 of 30&lt;/strong&gt; agency sites linked three or more client domains at all. The rest show client work as screenshots with no anchor.&lt;/p&gt;

&lt;p&gt;That's a lot of backlinks being left on the floor.&lt;/p&gt;




&lt;p&gt;If you want to look at what I built with this, it's at &lt;a href="https://dropperch.com" rel="noopener noreferrer"&gt;dropperch.com&lt;/a&gt; — free tier monitors domains. But the RDAP notes above are the genuinely reusable part, and they're yours whether or not you ever look at it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>api</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
