<?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 checked 44 launch sites on Ahrefs' new DR4. Most of them drop to zero on 1 December.</title>
      <dc:creator>WEBVOGUE Technologies</dc:creator>
      <pubDate>Mon, 09 Nov 2026 12:59:32 +0000</pubDate>
      <link>https://dev.to/webvogue/i-checked-44-launch-sites-on-ahrefs-new-dr4-most-of-them-drop-to-zero-on-1-december-5f5l</link>
      <guid>https://dev.to/webvogue/i-checked-44-launch-sites-on-ahrefs-new-dr4-most-of-them-drop-to-zero-on-1-december-5f5l</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foyarktiayn19n57a1q9c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foyarktiayn19n57a1q9c.png" alt=" " width="751" height="786"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On 1 December 2026 Ahrefs makes &lt;strong&gt;DR4&lt;/strong&gt; its default Domain Rating. Its free API already returns both versions&lt;br&gt;
(&lt;code&gt;version=dr3&lt;/code&gt; and &lt;code&gt;version=dr4&lt;/code&gt;), so before submitting my own startup to "free launch sites for backlinks", I&lt;br&gt;
measured them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What changed:&lt;/strong&gt; DR4 gives much more weight to links from sites that get organic search traffic, and subdomains now&lt;br&gt;
get their own score. Ahrefs says DR4 tracks Google rankings about 2.5x better than DR3. The side effect: sites that&lt;br&gt;
grew their DR with "add our badge to get a dofollow link" swaps lose most of it, because the sites linking to them&lt;br&gt;
have no search traffic of their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers (Ahrefs free API, 9 November 2026)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Site&lt;/th&gt;
&lt;th&gt;DR3&lt;/th&gt;
&lt;th&gt;DR4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;fazier.com&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;td&gt;69&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pitchwall.co&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uneed.best&lt;/td&gt;
&lt;td&gt;77&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;launchigniter.com&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tinylaunch.com&lt;/td&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;openhunts.com&lt;/td&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;launchingnext.com&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;codehype.ai&lt;/td&gt;
&lt;td&gt;73&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;scrolllaunch.com&lt;/td&gt;
&lt;td&gt;73&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;open-launch.com&lt;/td&gt;
&lt;td&gt;71&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kittylaunch.com&lt;/td&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;launchstag.com&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;noonlaunch.com&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;4.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;forg.to&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;letslaunch.today&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;1.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bricklaun.ch&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;0.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;launchaf.com&lt;/td&gt;
&lt;td&gt;57&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rankcert.com&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dodolaunch.com&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For reference: producthunt.com 91 → 91, peerlist.io 78 → 72.&lt;/p&gt;

&lt;p&gt;Full table of 44 sites, with the method: &lt;a href="https://pingsold.com/ahrefs-dr4" rel="noopener noreferrer"&gt;https://pingsold.com/ahrefs-dr4&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A badge swap on a DR4-0 site is worth nothing for SEO after 1 December.&lt;/strong&gt; Plenty of boards with a DR3 in the
40s–50s are at DR4 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ones that held up have an audience&lt;/strong&gt; — real visitors, newsletters, comment sections. That's also where launch
traffic comes from, so the SEO and the launch-day value now point the same way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check DR4 before you put someone's badge on your homepage.&lt;/strong&gt; I added a DR3 vs DR4 checker for this:
&lt;a href="https://pingsold.com/domain-rating-checker" rel="noopener noreferrer"&gt;https://pingsold.com/domain-rating-checker&lt;/a&gt; (free, no sign-up).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long free queues are the real cost.&lt;/strong&gt; Many of the high-DR4 boards have free queues of 2 weeks to 4 months.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I'm building Pingsold, a startup directory ranked by traffic read from founders' own analytics, so I care about&lt;br&gt;
which numbers can be trusted — this was the same question for backlinks.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Domain Rating by Ahrefs. DR4 is Ahrefs' preview and may still change before 1 December.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>startup</category>
      <category>marketing</category>
      <category>indiehackers</category>
    </item>
    <item>
      <title>I built a startup directory ranked by verified traffic. Almost nobody connects their analytics.</title>
      <dc:creator>WEBVOGUE Technologies</dc:creator>
      <pubDate>Mon, 09 Nov 2026 11:55:32 +0000</pubDate>
      <link>https://dev.to/webvogue/i-built-a-startup-directory-ranked-by-verified-traffic-almost-nobody-connects-their-analytics-5d7b</link>
      <guid>https://dev.to/webvogue/i-built-a-startup-directory-ranked-by-verified-traffic-almost-nobody-connects-their-analytics-5d7b</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faedc4wncvxi2b40sh9xp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faedc4wncvxi2b40sh9xp.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every week my feed has another "we just hit 10k visitors" post with a cropped screenshot. Maybe it's true. There's no way to tell. So I built Pingsold: a startup directory where the ranking comes from each startup's own analytics, read-only, synced daily. Nobody can type in a number, including me, and paying never changes a rank.&lt;/p&gt;

&lt;p&gt;The idea still feels right to me. The numbers so far are humbling:&lt;/p&gt;

&lt;p&gt;8 startups listed. 2 verified. Both of them are mine.&lt;/p&gt;

&lt;p&gt;Six founders found Pingsold, filled in the form, got a listing page... and never connected their analytics. Which means they never appear on the leaderboard, which is the whole point of the site.&lt;/p&gt;

&lt;p&gt;Why I think they don't connect&lt;br&gt;
I went through 50+ startup launch sites this week (Product Hunt alternatives, launch boards, directories) to see what founders actually do there. Most of them ask for almost nothing: paste a URL, an AI fills the form, add a badge, done. Pingsold asks for something much bigger, and I think I was asking for it the wrong way.&lt;/p&gt;

&lt;p&gt;Trust. "Connect your Google Analytics" to a site you found yesterday is a big ask, even read-only, even with Google's OAuth verification done.&lt;br&gt;
Small numbers. Most early founders have 50–500 visitors a month. Publishing that next to a bigger startup feels bad.&lt;br&gt;
No visible payoff. A rank on a board with two verified startups isn't worth much yet. Chicken and egg.&lt;br&gt;
Timing. I asked after they'd submitted. By then they already had what they came for (a listing page and a link), and "I'll do it later" never comes.&lt;br&gt;
Effort. Finding where your API key lives, or which GA4 property is the right one, is friction at exactly the wrong moment.&lt;br&gt;
What I changed (shipped this week)&lt;br&gt;
Verification moved inside the submit flow. It's now 3 steps: Details → Verify traffic → Review &amp;amp; submit. You can still skip, but the ask comes while you're in "launch mode".&lt;br&gt;
We detect your analytics tool. After step 1, Pingsold checks your homepage for GA4, Plausible, Fathom, PostHog or DataFast and leads with it: "We found Plausible on yoursite.com — connect it in one click."&lt;br&gt;
A "show as a range" switch, right there. "1K–5K" instead of "1,240". This was the objection I underestimated.&lt;br&gt;
"Exactly what we'll read". A card showing what's public, what's only used to verify, and what we never read (individual visitors, IPs, page paths, revenue).&lt;br&gt;
Instant payoff. The moment you connect, you see your verified number and where you'd rank: "You'd be #3 of 9".&lt;br&gt;
Something worth showing off. A live badge for your own site that says "Verified on Pingsold" with your real visitors for yesterday, the last 7 days or the last 30 days, updated daily. The plain "Listed on Pingsold" badge has no number, so the badge itself is the reason to verify.&lt;br&gt;
A bit of ceremony. Confetti when you verify, when you submit, and when you go live. Silly, but launches should feel like launches.&lt;br&gt;
On the review step, skipped listings now show the muted "Not verified" label right in the preview, so you see the difference before you submit.&lt;/p&gt;

&lt;p&gt;What I'd love your take on&lt;br&gt;
Would you connect your analytics to a directory? If not, what would change your mind?&lt;br&gt;
Exact numbers or ranges: which would you pick, honestly?&lt;br&gt;
Is a verified traffic badge something you'd actually put on your site?&lt;br&gt;
If you run a startup and want to try the new flow, it's free: pingsold.com/submit. I read every reply, and I'll post the numbers in a month — whether this moved the 2 out of 8 or not.&lt;/p&gt;

</description>
      <category>startup</category>
      <category>discuss</category>
      <category>buildinpublic</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I verify a startup's traffic without trusting the founder</title>
      <dc:creator>WEBVOGUE Technologies</dc:creator>
      <pubDate>Tue, 03 Nov 2026 10:50:56 +0000</pubDate>
      <link>https://dev.to/webvogue/how-i-verify-a-startups-traffic-without-trusting-the-founder-379e</link>
      <guid>https://dev.to/webvogue/how-i-verify-a-startups-traffic-without-trusting-the-founder-379e</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmnucjledd66ziufi1q5c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmnucjledd66ziufi1q5c.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;Every week my timeline has a "we just hit 10k visitors 🚀" post with a cropped screenshot. Maybe it's true. There's no way to tell.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://pingsold.com" rel="noopener noreferrer"&gt;Pingsold&lt;/a&gt;: a startup directory ranked by traffic read straight from each startup's own analytics. Nobody types in a number, including me. Here's how the verification works, and the edge cases that made it harder than "just call the GA API".&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Read-only, or nothing
&lt;/h2&gt;

&lt;p&gt;Founders connect Google Analytics, Plausible, Fathom, PostHog or DataFast.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Analytics&lt;/strong&gt; uses OAuth with only the &lt;code&gt;analytics.readonly&lt;/code&gt; scope (or the founder adds a service account as a Viewer). Getting Google to verify that sensitive scope took two review rounds: a data-protection section in the privacy policy and a demo video showing the scope on the consent screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The other tools&lt;/strong&gt; use API keys. Some of those tools let you create keys that can also write or manage the account, so the connector checks what the key can do and &lt;strong&gt;refuses anything that can do more than read analytics&lt;/strong&gt;. A leaked read-only key can't hurt anyone; a leaked admin key can.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We only read aggregates: visitors, pageviews, hostnames and referrer domains. Nothing about individual visitors.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The traffic has to be on &lt;em&gt;your&lt;/em&gt; domain
&lt;/h2&gt;

&lt;p&gt;The obvious way to cheat a traffic leaderboard: list a tiny product, but connect the analytics property of a big site you also run. So every sync pulls pageviews &lt;strong&gt;by hostname&lt;/strong&gt; and checks that at least 80% of them are on the listed domain.&lt;/p&gt;

&lt;p&gt;The details matter more than the threshold:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DOMAIN_MATCH_THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.8&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;PREVIEW_SUFFIXES&lt;/span&gt; &lt;span class="o"&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;.vercel.app&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;.netlify.app&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;.pages.dev&lt;/span&gt;&lt;span class="dl"&gt;"&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;ignorable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;host&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;listingDomain&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;boolean&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;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;host&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="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;\d&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="c1"&gt;// localhost, raw IPs and GA's "(not set)" aren't real traffic, so they don't count either way&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;(not set)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;localhost&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\d{1,3}(\.\d{1,3}){3}&lt;/span&gt;&lt;span class="sr"&gt;$/&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;h&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// preview deploys aren't the product, but they aren't a different site either&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;PREVIEW_SUFFIXES&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;s&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;h&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="nx"&gt;s&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="nf"&gt;registrableDomain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;listingDomain&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkDomainMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listingDomain&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;hosts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;host&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;pageviews&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;total&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;matched&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageviews&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;hosts&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="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageviews&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;pageviews&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;ignorable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listingDomain&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;pageviews&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// app.example.com and blog.example.com both belong to example.com&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;registrableDomain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;host&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="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;\d&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="nx"&gt;listingDomain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;matched&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;pageviews&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;total&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&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;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no_traffic&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;matched&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;DOMAIN_MATCH_THRESHOLD&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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="na"&gt;ok&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;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;domain_mismatch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Things I got wrong at first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Comparing hostnames exactly.&lt;/strong&gt; &lt;code&gt;app.example.com&lt;/code&gt; and &lt;code&gt;www.example.com&lt;/code&gt; are the same startup. Comparing &lt;em&gt;registrable&lt;/em&gt; domains (eTLD+1, via the Public Suffix List) fixes that, and also handles &lt;code&gt;example.co.uk&lt;/code&gt; correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Counting localhost and preview deploys.&lt;/strong&gt; A founder testing on &lt;code&gt;localhost:3000&lt;/code&gt; or a &lt;code&gt;*.vercel.app&lt;/code&gt; preview would fail the check for no good reason. Those are now neutral: they count toward neither side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checking once.&lt;/strong&gt; The check runs on every daily sync, not just at connect time, so you can't pass it once and then swap properties.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Credentials are encrypted, with key rotation
&lt;/h2&gt;

&lt;p&gt;Stored keys and refresh tokens are encrypted with AES-256-GCM. The encryption key lives in a key ring, so it can be rotated without re-encrypting everything at once. Disconnecting deletes the credentials immediately, and for Google it also revokes the token at Google.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Ranking
&lt;/h2&gt;

&lt;p&gt;Once a day, a scheduled job (Cloud Scheduler hitting a protected cron route) syncs every connection and stores a snapshot. Startups are ranked on two boards: &lt;strong&gt;top traffic&lt;/strong&gt; (30-day visitors) and &lt;strong&gt;fastest growing&lt;/strong&gt; (month over month), overall and per category. Paid options exist (faster review, a Featured slot) but they never touch the rank.&lt;/p&gt;

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

&lt;p&gt;We're only as accurate as the analytics tool. If GA counts bot traffic, so do we. Admins can exclude a startup that's clearly gaming it, but the honest answer is that verified means "this is what your analytics says", not "every visitor was a human".&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;p&gt;Next.js, TypeScript, Tailwind + shadcn/ui, Postgres (Neon) with Drizzle, Better Auth, Firebase App Hosting, Cloud Storage and Cloud Scheduler.&lt;/p&gt;




&lt;p&gt;It's live at &lt;a href="https://pingsold.com" rel="noopener noreferrer"&gt;pingsold.com&lt;/a&gt;, free to list, and the board is tiny right now (two startups, both mine). If you run something with GA4, Plausible, Fathom, PostHog or DataFast, I'd love for you to try it, and I'd really like to hear how you'd try to break the verification.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>analytics</category>
      <category>nextjs</category>
      <category>showdev</category>
    </item>
    <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>
