<?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: WhoisFreaks</title>
    <description>The latest articles on DEV Community by WhoisFreaks (whoisfreaks).</description>
    <link>https://dev.to/whoisfreaks</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%2Forganization%2Fprofile_image%2F13715%2F493dcbec-3dd4-4ad0-9641-28dd0fed3109.png</url>
      <title>DEV Community: WhoisFreaks</title>
      <link>https://dev.to/whoisfreaks</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/whoisfreaks"/>
    <language>en</language>
    <item>
      <title>How I Built a Subdomain Takeover Scanner in Python (The Dangerous Ones Don't Resolve)</title>
      <dc:creator>Sameer Sheikh</dc:creator>
      <pubDate>Thu, 17 Sep 2026 13:15:27 +0000</pubDate>
      <link>https://dev.to/whoisfreaks/how-i-built-a-subdomain-takeover-scanner-in-python-the-dangerous-ones-dont-resolve-4hho</link>
      <guid>https://dev.to/whoisfreaks/how-i-built-a-subdomain-takeover-scanner-in-python-the-dangerous-ones-dont-resolve-4hho</guid>
      <description>&lt;p&gt;Every subdomain takeover scanner I've used starts from the same place: enumerate what resolves, then check what it points at. That ordering is backwards. The subdomain you need to worry about is the one that stopped resolving eight months ago, when somebody deleted a Heroku app and left the CNAME behind.&lt;/p&gt;

&lt;p&gt;A live scanner never sees it. It's not in your wordlist hits, it's not in your cert transparency pull, and it doesn't answer DNS. It sits there until someone re-registers &lt;code&gt;checkout-staging-v2&lt;/code&gt; on Heroku and starts serving whatever they want from &lt;code&gt;checkout-staging.yourbank.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So I built the scanner the other way round. Pull the historical subdomain set first, including records that went dark. Then resolve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The key insight
&lt;/h2&gt;

&lt;p&gt;Subdomain takeover has two halves, and most tooling only does one well. The DNS half is easy: follow the CNAME, see if the target is gone. The discovery half is where everything falls apart, because brute-force and passive DNS both bias hard toward what's alive right now. Certificate transparency helps, but only for names somebody bothered to issue a cert for, and plenty of internal staging hosts never got one.&lt;/p&gt;

&lt;p&gt;What I wanted was a list of every hostname that has ever existed under a domain, with dates attached, so I could sort by "last seen" and start at the bottom. That's exactly what the WhoisFreaks Subdomains API returns. Each record carries &lt;code&gt;first_seen&lt;/code&gt;, &lt;code&gt;last_seen&lt;/code&gt;, and &lt;code&gt;inactive_from&lt;/code&gt;, plus a &lt;code&gt;status&lt;/code&gt; filter you can set to &lt;code&gt;inactive&lt;/code&gt; to get only the dead ones. Full details in the &lt;a href="https://whoisfreaks.com/documentation/subdomains-api" rel="noopener noreferrer"&gt;WhoisFreaks Subdomains API docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The dates are the part that matters. A subdomain last seen in March that still has a CNAME in your zone file is a strong lead, not a curiosity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/WhoisFreaks/wf-subdomain-takeover.git
&lt;span class="nb"&gt;cd &lt;/span&gt;wf-subdomain-takeover
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt          &lt;span class="c"&gt;# requests, dnspython&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;WHOISFREAKS_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your_key"&lt;/span&gt;
python takeover_scan.py example.com &lt;span class="nt"&gt;--csv&lt;/span&gt; results.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Free tier is 500 credits, which is plenty for a few domains. Two dependencies, no Docker, no config file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Pull the full subdomain history
&lt;/h2&gt;

&lt;p&gt;The endpoint is &lt;code&gt;GET /v1.0/subdomains&lt;/code&gt; and it pages. &lt;code&gt;total_pages&lt;/code&gt; comes back on the first response, so you loop until you've drained it. I added a rate limiter because the free tier caps requests per minute and I'd rather sleep 6 seconds than eat a 429.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;API_BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.whoisfreaks.com/v1.0/subdomains&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_subdomains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_pages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rpm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Page through /v1.0/subdomains and return every record.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;limiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rpm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;max_pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apiKey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;

        &lt;span class="n"&gt;limiter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;API_BASE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[!] page &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; returned HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subdomains&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;total_pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;total_pages&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each record looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subdomain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"staging-api.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"first_seen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2021-08-04"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"last_seen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-11-19"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inactive_from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-11-20"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass &lt;code&gt;--status inactive&lt;/code&gt; and you get only the records the index has marked decommissioned. On the domain I tested that filter was exactly right, so it's a reasonable shortlist. I'd still pull everything and resolve it yourself, partly because DNS is cheap and mostly because you want the live hosts in the same table to compare against.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Walk the CNAME chain properly
&lt;/h2&gt;

&lt;p&gt;This is where I lost the most time. A single &lt;code&gt;resolve(host, "CNAME")&lt;/code&gt; isn't enough, because chains nest. &lt;code&gt;blog.example.com&lt;/code&gt; → &lt;code&gt;example.netlify.app&lt;/code&gt; → &lt;code&gt;apex-loadbalancer.netlify.com&lt;/code&gt; is three hops, and the interesting answer lives at the end.&lt;/p&gt;

&lt;p&gt;You also have to separate outcomes that look identical from the outside: no CNAME at all, a CNAME pointing somewhere alive, a CNAME pointing at a name that NXDOMAINs, and a lookup that simply failed. Only the third is a dangling record.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resolve_chain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Walk CNAMEs until an A record, NXDOMAIN, or the depth cap.

    Returns (chain, resolves_to_address, final_target_nxdomain, state) where
    state is live / nxdomain / no_records / error.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CNAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NoAnswer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;                      &lt;span class="c1"&gt;# no CNAME here, try A below
&lt;/span&gt;        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NXDOMAIN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nxdomain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NoNameservers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;live&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NXDOMAIN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nxdomain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NoAnswer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no_records&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NoNameservers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;bool(chain)&lt;/code&gt; is doing real work in those NXDOMAIN branches. If the chain is empty, the host itself just doesn't exist and that's boring. If the chain has entries, something in your zone is pointing at a name that no longer exists, and that's the whole game.&lt;/p&gt;

&lt;p&gt;That fourth return value isn't how I wrote it the first time. The original collapsed &lt;code&gt;no_records&lt;/code&gt; and &lt;code&gt;error&lt;/code&gt; into the same "didn't resolve" boolean, which seemed fine until I counted results on a real domain. More on that below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Fingerprint the service
&lt;/h2&gt;

&lt;p&gt;A dangling CNAME only matters if the service on the other end lets a stranger claim the hostname. So I keep a table modelled on the classifications in &lt;a href="https://github.com/EdOverflow/can-i-take-over-xyz" rel="noopener noreferrer"&gt;can-i-take-over-xyz&lt;/a&gt;, with three values instead of a boolean.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;FINGERPRINTS&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS S3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cnames&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3.amazonaws.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3-website&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bodies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NoSuchBucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The specified bucket does not exist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vulnerable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bucket name must still be free in the same region&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GitHub Pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cnames&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;github.io&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;githubusercontent.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bodies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;There isn&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t a GitHub Pages site here&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vulnerable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;edge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GitHub has required domain verification since 2021; older orgs can still be exposed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Zendesk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cnames&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zendesk.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bodies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Help Center Closed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;this help center no longer exists&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vulnerable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Zendesk blocks re-registration of released subdomains&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;# ...17 services total in the repo
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;"no"&lt;/code&gt; on Zendesk is the reason for the three-way split. &lt;code&gt;help.figma.com&lt;/code&gt; CNAMEs to &lt;code&gt;figma.zendesk.com&lt;/code&gt;, and a naive scanner that treats every known-service CNAME as a hit will flag it. Nobody's taking that over. Flagging it trains people to ignore your tool, which is worse than not having one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Confirm over HTTP
&lt;/h2&gt;

&lt;p&gt;DNS gets you a lead. The HTTP body gets you proof. Wildcards are why: &lt;code&gt;*.github.io&lt;/code&gt; resolves, so a deleted GitHub Pages site never NXDOMAINs and the DNS layer tells you nothing. You have to ask the service.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcu68nim7nu09d5utuqkq.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%2Fcu68nim7nu09d5utuqkq.png" alt="The DNS view and the HTTP view of the same hostname side by side. DNS reports state=live; HTTP returns 404 with " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I checked both of these against live endpoints while writing this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wf-takeover-demo-9xk22.s3.amazonaws.com  -&amp;gt; HTTP 404, "NoSuchBucket"
wf-takeover-demo-9xk22.github.io         -&amp;gt; HTTP 404, "There isn't a GitHub Pages site here"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;probe_http&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allow_redirects&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;20000&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;needle&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bodies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;needle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;needle&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note it tries HTTPS first and falls back to HTTP. A lot of abandoned hosts have a dead cert, and if you only speak HTTPS you'll miss them entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Score it
&lt;/h2&gt;

&lt;p&gt;Four verdicts, ordered by how much of your evening they deserve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target_nxdomain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DANGLING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;matched_body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FINGERPRINT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vulnerable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REVIEW&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resolves&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REVIEW&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vulnerable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DANGLING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FINGERPRINT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REVIEW&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; (service blocks third-party claims)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;finding&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DANGLING&lt;/code&gt; means the CNAME target is gone. &lt;code&gt;FINGERPRINT&lt;/code&gt; means the service is serving its "nothing here" page and the service is claimable. &lt;code&gt;REVIEW&lt;/code&gt; is everything suspicious that I'm not willing to call. The exit code follows suit: 2 for dangling, 1 for fingerprint, 0 for clean, so you can drop it straight into CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Prove it actually fires
&lt;/h2&gt;

&lt;p&gt;A detector nobody has seen fire is just a script that prints &lt;code&gt;OK&lt;/code&gt;. So there's a &lt;code&gt;--selftest&lt;/code&gt; mode that points the whole pipeline at two hostnames where nothing is registered, on services anyone can check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python takeover_scan.py &lt;span class="nt"&gt;--selftest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[*] self-test: probing unclaimed hostnames on live services

  PASS  wf-takeover-demo-9xk22.s3.amazonaws.com
        service  AWS S3  (expected AWS S3)
        verdict  FINGERPRINT  (expected FINGERPRINT)
        dns      state=live  resolves=True  target_nxdomain=False
        http     404  matched=NoSuchBucket

  PASS  wf-takeover-demo-9xk22.github.io
        service  GitHub Pages  (expected GitHub Pages)
        verdict  FINGERPRINT  (expected FINGERPRINT)
        dns      state=live  resolves=True  target_nxdomain=False
        http     404  matched=There isn't a GitHub Pages site here

[*] 2/2 passed.
    Both hosts resolve through a service wildcard, so DNS alone says nothing.
    The response body is what flags them.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the &lt;code&gt;dns&lt;/code&gt; line on both. &lt;code&gt;state=live&lt;/code&gt;, &lt;code&gt;target_nxdomain=False&lt;/code&gt;. If you only walked CNAMEs you'd call both of these clean and move on. That's the wildcard problem in two lines of output, and it's why the HTTP stage isn't optional.&lt;/p&gt;

&lt;p&gt;It needs no API key, which also makes it the fastest way to tell whether a fingerprint has gone stale. Services rewrite their error pages without announcing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real results
&lt;/h2&gt;

&lt;p&gt;I pointed it at python.org. Public infrastructure, over a decade of history, and a surface wide enough to be interesting without being someone's private estate.&lt;/p&gt;

&lt;p&gt;The first thing it prints is the part I nearly got wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[*] python.org: 113 subdomain records across 2 page(s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two pages. My first run used &lt;code&gt;--max-pages 1&lt;/code&gt; and cheerfully scanned 100 of the 113 hostnames without complaining about the other 13. Page size is 100. Drain the pages or you're auditing a prefix of your attack surface and calling it a scan.&lt;/p&gt;

&lt;p&gt;The full run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[*] python.org: 113 subdomain records across 2 page(s)
[*] scanning 113 hostnames with 12 workers
    100/113

HOST                       VERDICT   SERVICE         CNAME TARGET
------------------------------------------------------------------------------
blog.python.org            OK        GitHub Pages    python.github.io
blog-ja.python.org         OK        -               ghs46.google.com
devguide.python.org        OK        Read the Docs   readthedocs.io
discuss.python.org         OK        -               python1.hosted-by-discourse.com
docs.python.org            OK        Fastly          dualstack.python.map.fastly.net
education.python.org       OK        Heroku          desolate-laurel-pfbo73zva...herokudns.com
hg.python.org              OK        -               map-lb.nyc1.psf.io
peps.python.org            OK        Fastly          python.map.fastly.net
status.python.org          OK        -               2p66nmmycsj3.stspg-customer.com
translations.python.org    OK        GitHub Pages    python-docs-translations.github.io
ukpatl.uk.python.org       OK        Netlify         glistening-kashata-2a8d56.netlify.app
wwww.python.org            OK        -               -
...

OK=113
[*] wrote python-results.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean. All 113.&lt;/p&gt;

&lt;p&gt;The CSV is where the shape shows up. 54 of the 113 records carry a CNAME, 59 are bare A records. Sixty-seven resolve, and 46 don't. The index spans 2025-09-28 to 2026-09-13, about a year of history for a page and a bit of API calls.&lt;/p&gt;

&lt;p&gt;Then I checked that 46 against what the index itself claims:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python takeover_scan.py python.org --status inactive --show-all

[*] python.org: 43 subdomain records across 1 page(s)
OK=43
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Forty-three flagged inactive, and all 43 are in my non-resolving set. Zero false positives. The three my scan counted as dead but the index didn't were my bug, not its: &lt;code&gt;_dmarc.python.org&lt;/code&gt; is a TXT-only record that was never going to answer an A query, and &lt;code&gt;es.python.org&lt;/code&gt; and &lt;code&gt;comunidad.es.python.org&lt;/code&gt; both resolved fine when I retried them. I'd been counting "my lookup failed" as "this host is gone."&lt;/p&gt;

&lt;p&gt;That's the bug I flagged back in Step 2. One overloaded boolean was hiding three different situations, and it only showed up because a number I could check disagreed with a number someone else had computed. The scanner reports &lt;code&gt;dns_state&lt;/code&gt; now.&lt;/p&gt;

&lt;p&gt;Twenty-five of those hosts matched a service in the fingerprint table: 11 Fastly, 7 GitHub Pages, 5 Read the Docs, 1 Heroku, 1 Netlify. Every one came back &lt;code&gt;OK&lt;/code&gt;, because in every case the service is actively serving the site. That's the table doing its actual job, which is filtering rather than finding.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F061k5779ug1s93jdwxyu.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%2F061k5779ug1s93jdwxyu.png" alt="Funnel of the python.org scan: 113 subdomain records, 54 carrying a CNAME, 25 pointing at a fingerprinted service, 0 flagged for takeover." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Look at &lt;code&gt;education.python.org&lt;/code&gt; for a second. It CNAMEs to &lt;code&gt;desolate-laurel-pfbo73zvavjs5gdidi59tus8.herokudns.com&lt;/code&gt;, and it's live and healthy right now. If that Heroku app were ever deleted without removing the DNS record, that single row is the entire premise of this post, sitting there in a live scan. Nothing is wrong with it today. It's just the shape of the thing you're looking for.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Paging is not optional and the tool won't warn you. &lt;code&gt;--max-pages 1&lt;/code&gt; returned 100 of 113 hosts and printed no complaint. Thirteen hostnames silently outside the scan is exactly the kind of gap that makes a clean result meaningless.&lt;/li&gt;
&lt;li&gt;Forty-three of the 113 records are genuinely decommissioned, and the index's own &lt;code&gt;inactive&lt;/code&gt; flag matched all 43 exactly. I'd expected that flag to lag reality and it didn't. Good remediation is invisible from the outside, and it's a third of what a historical index contains.&lt;/li&gt;
&lt;li&gt;"My DNS lookup failed" and "this host is gone" are different claims, and I'd been conflating them. A TXT-only record like &lt;code&gt;_dmarc&lt;/code&gt; never answers an A query. A timeout is a timeout. Three hosts landed in my dead pile that didn't belong there, which on a bigger estate would be a steady trickle of noise nobody chases.&lt;/li&gt;
&lt;li&gt;Only 54 of 113 carry a CNAME at all. The rest are bare A records and structurally can't be taken over this way. Your real exposure is usually a fraction of your subdomain count, worth knowing before a long list sends anyone into a panic.&lt;/li&gt;
&lt;li&gt;Twenty-five hosts matched a fingerprinted service and every single one came back &lt;code&gt;OK&lt;/code&gt;, because the service is actively serving them. The table earns its place by filtering, not by finding. A scanner that flagged all 25 would be useless by Tuesday.&lt;/li&gt;
&lt;li&gt;Ten hostnames CNAME to &lt;code&gt;ghs46.google.com&lt;/code&gt;, the Blogger custom-domain host, and my table doesn't cover it. Seven more targets are also unrecognized, including &lt;code&gt;python1.hosted-by-discourse.com&lt;/code&gt; and the Statuspage and PSF load-balancer names. I've left them out rather than guess at body strings I haven't verified, which is the honest failure mode: the scanner tells you it doesn't know, instead of pretending.&lt;/li&gt;
&lt;li&gt;Wildcard DNS is why the HTTP check isn't optional. &lt;code&gt;*.github.io&lt;/code&gt; answers for names that were never registered, so the DNS signal is flat and only the response body tells you anything.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;education.python.org&lt;/code&gt; points at a live Heroku app. Nothing's wrong with it. But that one row is the whole vulnerability class in miniature, and the only difference between it and a finding is whether somebody deletes the app and forgets the DNS.&lt;/li&gt;
&lt;li&gt;Every record came back &lt;code&gt;OK&lt;/code&gt;, and I'll admit I wanted one embarrassing finding to write up. Does a scanner that finds nothing still earn its keep? Only in the sense a smoke alarm does on the days it stays quiet, which is most of them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Going further
&lt;/h2&gt;

&lt;p&gt;A few directions I'd take this next.&lt;/p&gt;

&lt;p&gt;Run it on a schedule against your own zones and diff the CSV, because the useful signal is a host moving from &lt;code&gt;OK&lt;/code&gt; to &lt;code&gt;DANGLING&lt;/code&gt;, not the state on any single day. GitHub Actions on a weekly cron does the job in about fifteen lines.&lt;/p&gt;

&lt;p&gt;Widen the fingerprint table. Seventeen services covers the common ground, and the &lt;code&gt;can-i-take-over-xyz&lt;/code&gt; list is longer than that. The table is plain dicts specifically so adding one is a two-minute pull request.&lt;/p&gt;

&lt;p&gt;Feed the &lt;code&gt;DANGLING&lt;/code&gt; rows into whatever you use for tickets. A finding that sits in a CSV nobody opens isn't a finding.&lt;/p&gt;

&lt;p&gt;And if you'd rather not run the enumeration side yourself, the &lt;a href="https://whoisfreaks.com/products/subdomains-api" rel="noopener noreferrer"&gt;WhoisFreaks Subdomains API&lt;/a&gt; keeps the historical index with first-seen and last-seen dates so you only maintain the DNS logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full source code
&lt;/h2&gt;

&lt;p&gt;Everything's on GitHub: &lt;a href="https://github.com/WhoisFreaks/wf-subdomain-takeover" rel="noopener noreferrer"&gt;github.com/WhoisFreaks/wf-subdomain-takeover&lt;/a&gt;. MIT, one file, two dependencies. The repo includes the full seventeen-service fingerprint table, a sample &lt;code&gt;domains.txt&lt;/code&gt;, and the GitHub Actions workflow for the scheduled diff.&lt;/p&gt;

&lt;p&gt;If you find a fingerprint that's wrong or missing, open an issue. Service error pages change more often than you'd think, and a stale fingerprint is a false negative.&lt;/p&gt;




</description>
      <category>python</category>
      <category>security</category>
      <category>cybersecurity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Monthly Insights Radar: Newly Registered Domain Analytics for August 2026</title>
      <dc:creator>Sameer Sheikh</dc:creator>
      <pubDate>Thu, 10 Sep 2026 11:04:47 +0000</pubDate>
      <link>https://dev.to/whoisfreaks/monthly-insights-radar-newly-registered-domain-analytics-for-august-2026-k96</link>
      <guid>https://dev.to/whoisfreaks/monthly-insights-radar-newly-registered-domain-analytics-for-august-2026-k96</guid>
      <description>&lt;h2&gt;
  
  
  Quick Answer
&lt;/h2&gt;

&lt;p&gt;August 2026 recorded 11,640,735 newly registered domains, up 19.5% from July and the first month above eleven million in this series. Germany's .de extension supplied roughly 30% of that growth, but 66% of .de volume landed inside a single five-day batch window. Adjusted for that batch, underlying market growth was closer to 11%. Two other events shape the month: a market-wide ingestion gap on August 8, and a two-window registration campaign visible in .xyz, GMO Internet and Japan.&lt;/p&gt;

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

&lt;p&gt;The Newly Registered Domain (NRD) landscape in August 2026 recorded &lt;strong&gt;11,640,735 total registrations&lt;/strong&gt;, a &lt;strong&gt;+19.5% expansion&lt;/strong&gt; month over month from July's 9,742,358, and the first reading the series has posted above eleven million. Roughly &lt;strong&gt;30%&lt;/strong&gt; of that growth traces to a single extension.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The .de Deluge&lt;/strong&gt; lifted Germany's country-code namespace from &lt;strong&gt;119,703&lt;/strong&gt; registrations to &lt;strong&gt;690,812&lt;/strong&gt; (&lt;strong&gt;+571,109, +477.1%&lt;/strong&gt;) and from rank &lt;strong&gt;#16&lt;/strong&gt; to rank &lt;strong&gt;#3&lt;/strong&gt;. The daily chart makes clear what kind of event this was. A baseline running &lt;strong&gt;~3.3k to ~31.7k/day&lt;/strong&gt; and averaging &lt;strong&gt;~9,000/day&lt;/strong&gt; was broken by a flat &lt;strong&gt;~90k/day&lt;/strong&gt; plateau across &lt;strong&gt;August 13 to 17&lt;/strong&gt;, five days that on their own carry &lt;strong&gt;66%&lt;/strong&gt; of the extension's monthly volume, with four German registrars entering the Top-20 simultaneously to process it. Strip the plateau out and .de lands near &lt;strong&gt;234,000&lt;/strong&gt;, a &lt;strong&gt;~9,000/day&lt;/strong&gt; run rate: still more than double July's, but nowhere near the podium. Every dependent statistic in this report, from the ccTLD split to the TLD pie, should be read with that qualification attached.&lt;/p&gt;

&lt;p&gt;The rest of the month was genuinely strong, and two further patterns shape it. &lt;strong&gt;The August 8 Blackout&lt;/strong&gt; cut aggregate volume to a month-low &lt;strong&gt;~126,000&lt;/strong&gt; on a Saturday, roughly &lt;strong&gt;one-third&lt;/strong&gt; of the daily average, with &lt;strong&gt;.com&lt;/strong&gt; at &lt;strong&gt;~17.5k&lt;/strong&gt;, the &lt;strong&gt;United States&lt;/strong&gt; at &lt;strong&gt;~16.9k&lt;/strong&gt;, &lt;strong&gt;GoDaddy&lt;/strong&gt; at &lt;strong&gt;~7.0k&lt;/strong&gt; and &lt;strong&gt;Namecheap&lt;/strong&gt; at &lt;strong&gt;~5.8k&lt;/strong&gt;, all month lows. An &lt;strong&gt;August 9&lt;/strong&gt; rebound then carried the market to &lt;strong&gt;~551,000&lt;/strong&gt; and .com to its monthly high of &lt;strong&gt;~189.4k&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Separately, &lt;strong&gt;the Twin-Window Campaign&lt;/strong&gt; shows up identically in three places. &lt;strong&gt;.xyz&lt;/strong&gt; (&lt;strong&gt;+26.0%&lt;/strong&gt; to &lt;strong&gt;946,600&lt;/strong&gt;), &lt;strong&gt;GMO Internet Group&lt;/strong&gt; (&lt;strong&gt;+30.4%&lt;/strong&gt; to &lt;strong&gt;933,005&lt;/strong&gt;, reclaiming rank &lt;strong&gt;#3&lt;/strong&gt;) and &lt;strong&gt;Japan&lt;/strong&gt; all ran hot from &lt;strong&gt;August 5 to 10&lt;/strong&gt; and again from &lt;strong&gt;August 18 to 22&lt;/strong&gt;, then collapsed to near nothing for the final nine days.&lt;/p&gt;

&lt;p&gt;On the competitive board, &lt;strong&gt;Namecheap&lt;/strong&gt; closed to within &lt;strong&gt;50,204&lt;/strong&gt; registrations of &lt;strong&gt;GoDaddy&lt;/strong&gt;, the narrowest top-two gap the series has recorded and tighter than July's 79,432, while &lt;strong&gt;Iceland&lt;/strong&gt; faded &lt;strong&gt;-25.7%&lt;/strong&gt; to &lt;strong&gt;550,237&lt;/strong&gt;. Data integrity held where it matters most: registrar coverage stayed at &lt;strong&gt;97.0% "Cleaned"&lt;/strong&gt; and address coverage slipped to &lt;strong&gt;50.6%&lt;/strong&gt;, while usable contact data nearly doubled to &lt;strong&gt;14.8%&lt;/strong&gt; of records. That last move is an artifact of the .de cohort arriving with contact fields intact, not a change in redaction policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Highlights
&lt;/h2&gt;

&lt;h3&gt;
  
  
  TLD and Market Dynamics
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The .de Deluge:&lt;/strong&gt; &lt;strong&gt;.de&lt;/strong&gt; rose &lt;strong&gt;+477.1%&lt;/strong&gt; from &lt;strong&gt;119,703&lt;/strong&gt; to &lt;strong&gt;690,812&lt;/strong&gt; registrations and from rank &lt;strong&gt;#16&lt;/strong&gt; to &lt;strong&gt;#3&lt;/strong&gt;, the largest single-month move in the history of this series. It is not organic demand. Roughly &lt;strong&gt;456,700&lt;/strong&gt; registrations landed across the flat &lt;strong&gt;August 13 to 17&lt;/strong&gt; plateau, &lt;strong&gt;66%&lt;/strong&gt; of the monthly total in five days, against a surrounding baseline averaging &lt;strong&gt;~9k/day&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The August 8 Blackout:&lt;/strong&gt; Aggregate "Newly" volume fell to &lt;strong&gt;~126,000&lt;/strong&gt; on &lt;strong&gt;August 8&lt;/strong&gt; against a monthly average of &lt;strong&gt;~375,500&lt;/strong&gt;. Month lows were recorded simultaneously by &lt;strong&gt;.com&lt;/strong&gt;, &lt;strong&gt;.top&lt;/strong&gt;, the &lt;strong&gt;United States&lt;/strong&gt;, &lt;strong&gt;Iceland&lt;/strong&gt;, &lt;strong&gt;China&lt;/strong&gt;, &lt;strong&gt;Germany&lt;/strong&gt;, &lt;strong&gt;GoDaddy&lt;/strong&gt;, &lt;strong&gt;Namecheap&lt;/strong&gt;, &lt;strong&gt;Dynadot&lt;/strong&gt; and &lt;strong&gt;Spaceship&lt;/strong&gt;: ten of the fifteen category charts. Only the five batch-driven or campaign-driven series (&lt;strong&gt;.de&lt;/strong&gt;, &lt;strong&gt;.cn&lt;/strong&gt;, &lt;strong&gt;.xyz&lt;/strong&gt;, &lt;strong&gt;Japan&lt;/strong&gt; and &lt;strong&gt;GMO Internet&lt;/strong&gt;) show no month low on that date, making this the broadest single-day data event the series has seen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Twin-Window Campaign:&lt;/strong&gt; &lt;strong&gt;.xyz&lt;/strong&gt; advanced &lt;strong&gt;+26.0%&lt;/strong&gt; to &lt;strong&gt;946,600&lt;/strong&gt; registrations on two discrete bursts, &lt;strong&gt;August 5 to 10&lt;/strong&gt; and &lt;strong&gt;August 18 to 22&lt;/strong&gt;, together &lt;strong&gt;72%&lt;/strong&gt; of its monthly volume. After the second window the extension fell to a &lt;strong&gt;~0.5k to 14k&lt;/strong&gt; floor for the final nine days. Its registrar and country mirrors, &lt;strong&gt;GMO Internet Group&lt;/strong&gt; and &lt;strong&gt;Japan&lt;/strong&gt;, trace the same two windows almost day for day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.cn's One-Day Jump:&lt;/strong&gt; &lt;strong&gt;.cn&lt;/strong&gt; gained &lt;strong&gt;+64.4%&lt;/strong&gt; to &lt;strong&gt;348,187&lt;/strong&gt; registrations and rose from rank &lt;strong&gt;#8&lt;/strong&gt; to &lt;strong&gt;#5&lt;/strong&gt;. Roughly &lt;strong&gt;83,700&lt;/strong&gt; of that, &lt;strong&gt;24%&lt;/strong&gt; of the month, arrived on &lt;strong&gt;August 18&lt;/strong&gt; alone against a &lt;strong&gt;~3k to 17k&lt;/strong&gt; baseline. .cn is one of three Top-5 extensions that did not dip on August 8, alongside &lt;strong&gt;.de&lt;/strong&gt; and &lt;strong&gt;.xyz&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ccTLD Rebalance:&lt;/strong&gt; Country-code space expanded to &lt;strong&gt;27.4%&lt;/strong&gt; of all NRDs (&lt;strong&gt;3,188,479&lt;/strong&gt;, &lt;strong&gt;+72.4%&lt;/strong&gt;) while gTLDs eased to &lt;strong&gt;72.6%&lt;/strong&gt; (&lt;strong&gt;8,452,256&lt;/strong&gt;, &lt;strong&gt;+7.1%&lt;/strong&gt;), an &lt;strong&gt;8.4-point&lt;/strong&gt; swing and the largest in the series. The move is led by .de's advance and by &lt;strong&gt;.cn +64.4%&lt;/strong&gt;, so the headline shift is concentrated in a handful of extensions rather than a broad regional adoption signal.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Geographic and Registrar Trends
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Tightest Top-Two Yet:&lt;/strong&gt; &lt;strong&gt;GoDaddy&lt;/strong&gt; added just &lt;strong&gt;+0.3%&lt;/strong&gt; to &lt;strong&gt;1,192,684&lt;/strong&gt; while &lt;strong&gt;Namecheap&lt;/strong&gt; rose &lt;strong&gt;+3.0%&lt;/strong&gt; to &lt;strong&gt;1,142,480&lt;/strong&gt;, compressing the leader's margin to &lt;strong&gt;50,204 registrations&lt;/strong&gt;, under &lt;strong&gt;4.3%&lt;/strong&gt; of GoDaddy's volume and the narrowest gap the series has recorded. Their combined share of Top-20 registrar volume fell to &lt;strong&gt;29.0%&lt;/strong&gt; from July's 32.0%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The German Registrar Cohort:&lt;/strong&gt; Four registrars entered the Top-20 at once: &lt;strong&gt;Domainers Registrar AG&lt;/strong&gt; (&lt;strong&gt;199,755&lt;/strong&gt;, rank #11), &lt;strong&gt;EliteDomains GmbH&lt;/strong&gt; (&lt;strong&gt;132,790&lt;/strong&gt;, #17), &lt;strong&gt;Nomino GmbH&lt;/strong&gt; (&lt;strong&gt;124,856&lt;/strong&gt;, #19) and &lt;strong&gt;InternetX GmbH&lt;/strong&gt; (&lt;strong&gt;116,865&lt;/strong&gt;, #20). Their combined &lt;strong&gt;574,266&lt;/strong&gt; registrations are &lt;strong&gt;7.1%&lt;/strong&gt; of Top-20 registrar volume. All four are German-market names and all four appear in the same month as the .de Deluge, which is the clearest available evidence that the event ran through a coordinated registrar channel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GMO Reclaims the Podium:&lt;/strong&gt; &lt;strong&gt;GMO Internet Group (onamae.com)&lt;/strong&gt; rose &lt;strong&gt;+30.4%&lt;/strong&gt; to &lt;strong&gt;933,005&lt;/strong&gt; registrations and moved from rank &lt;strong&gt;#4&lt;/strong&gt; to &lt;strong&gt;#3&lt;/strong&gt;, with &lt;strong&gt;75%&lt;/strong&gt; of its volume inside the two Twin-Window blocks and a month-end floor near &lt;strong&gt;~1.8k/day&lt;/strong&gt;. Its country mirror diverged: &lt;strong&gt;Japan&lt;/strong&gt; eased &lt;strong&gt;-5.5%&lt;/strong&gt; to &lt;strong&gt;509,410&lt;/strong&gt;, so a growing share of GMO's registrations is no longer resolving to a Japanese registrant address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Iceland Fade:&lt;/strong&gt; &lt;strong&gt;Iceland&lt;/strong&gt; fell &lt;strong&gt;-25.7%&lt;/strong&gt; from &lt;strong&gt;740,960&lt;/strong&gt; to &lt;strong&gt;550,237 registrations&lt;/strong&gt;, its weakest reading in months, and its pie share compressed from &lt;strong&gt;16.1%&lt;/strong&gt; to &lt;strong&gt;10.7%&lt;/strong&gt;. The daily chart shows a clean downward drift, roughly &lt;strong&gt;22k to 28k/day&lt;/strong&gt; in the first week against &lt;strong&gt;13k to 16k&lt;/strong&gt; in the last, rather than a single break. That makes this the most orderly decline of any large geography this month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Germany's Arrival and Broad Asian Recovery:&lt;/strong&gt; &lt;strong&gt;Germany&lt;/strong&gt; climbed &lt;strong&gt;+186.2%&lt;/strong&gt; to &lt;strong&gt;247,133&lt;/strong&gt; registrations and rank &lt;strong&gt;#5&lt;/strong&gt;, entering the Top-5 country pie for the first time in the series. Alongside it, &lt;strong&gt;China&lt;/strong&gt; rebounded &lt;strong&gt;+34.0%&lt;/strong&gt; to &lt;strong&gt;403,409&lt;/strong&gt;, &lt;strong&gt;Hong Kong&lt;/strong&gt; rose &lt;strong&gt;+46.9%&lt;/strong&gt; and &lt;strong&gt;South Korea&lt;/strong&gt; &lt;strong&gt;+12.8%&lt;/strong&gt;, reversing July's Asian correction, while &lt;strong&gt;Portugal&lt;/strong&gt; (&lt;strong&gt;67,688&lt;/strong&gt;), &lt;strong&gt;Russia&lt;/strong&gt; (&lt;strong&gt;43,809&lt;/strong&gt;) and &lt;strong&gt;Indonesia&lt;/strong&gt; (&lt;strong&gt;31,951&lt;/strong&gt;) entered the Top-20 as &lt;strong&gt;Poland&lt;/strong&gt;, &lt;strong&gt;Australia&lt;/strong&gt; and &lt;strong&gt;Italy&lt;/strong&gt; dropped out.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Data Quality and Intelligence
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Contact Coverage Nearly Doubles:&lt;/strong&gt; Usable contact details jumped to &lt;strong&gt;14.8%&lt;/strong&gt; of records (&lt;strong&gt;1,717,788&lt;/strong&gt;) from July's 7.6%, a &lt;strong&gt;+980,160&lt;/strong&gt; absolute increase and by far the largest single-month move this dimension has recorded. The "Newly_Cleaned" daily line locates the cause precisely: it spikes to &lt;strong&gt;~213k&lt;/strong&gt; on &lt;strong&gt;August 4&lt;/strong&gt; and holds &lt;strong&gt;~106k to 120k&lt;/strong&gt; across &lt;strong&gt;August 13 to 17&lt;/strong&gt;, exactly the .de Deluge windows. This is a composition effect from one cohort, not a change in redaction behavior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registrar Coverage Holds:&lt;/strong&gt; Registrar metadata registered &lt;strong&gt;97.0% "Cleaned"&lt;/strong&gt; (&lt;strong&gt;11,296,406&lt;/strong&gt; of 11,640,735 records) against July's 97.1%, a &lt;strong&gt;0.1-point&lt;/strong&gt; change that leaves registrar-based segmentation the most reliable analytical lens in the dataset. That matters more than usual in a month whose headline number depends on correctly attributing a batch event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Address Coverage Slips Below the Coin Flip Margin:&lt;/strong&gt; Usable address data eased to &lt;strong&gt;50.6%&lt;/strong&gt; of records (&lt;strong&gt;5,886,108&lt;/strong&gt; cleaned, &lt;strong&gt;5,754,627&lt;/strong&gt; redacted) from July's 53.4%, a &lt;strong&gt;2.8-point&lt;/strong&gt; decline that continues the give-back begun last month and leaves geo-segmentation with almost no margin above an even split.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Top 20 TLDs - Full Period
&lt;/h2&gt;

&lt;p&gt;The TLD market in August 2026 grew hard and unevenly. The Top-20 aggregate expanded &lt;strong&gt;+19.5%&lt;/strong&gt; to &lt;strong&gt;8,733,934&lt;/strong&gt; registrations, but the distribution of that growth is lopsided in a way no previous month matches. &lt;strong&gt;.de&lt;/strong&gt; supplies &lt;strong&gt;571,109&lt;/strong&gt; of the &lt;strong&gt;1,425,635&lt;/strong&gt; increase, or &lt;strong&gt;40%&lt;/strong&gt;, from a standing start at rank #16. Beneath the anomaly the board is healthier than July's: &lt;strong&gt;.com&lt;/strong&gt; returned to growth, &lt;strong&gt;.xyz&lt;/strong&gt; recovered most of its June peak, &lt;strong&gt;.cn&lt;/strong&gt; and &lt;strong&gt;.vip&lt;/strong&gt; advanced, and the bottom of the roster turned over toward country-code and regional extensions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparison with July 2026
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Newly added in August (entered Top-20)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;.co&lt;/strong&gt; to 139,743 (re-entry after a one-month absence, ranked #14)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.in&lt;/strong&gt; to 106,027 (re-entry after a one-month absence, ranked #19)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.asia&lt;/strong&gt; to 101,623 (new entry, ranked #20)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Removed in August (dropped out of Top-20)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;.icu&lt;/strong&gt; was 95,353 in July (ranked #18), not in August Top-20&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.br&lt;/strong&gt; was 94,868 in July (ranked #19), not in August Top-20&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.click&lt;/strong&gt; was 91,157 in July (ranked #20), not in August Top-20&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Biggest UPs (July to August)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;.de&lt;/strong&gt;: 119,703 to 690,812 (&lt;strong&gt;+571,109, +477.1%&lt;/strong&gt;) | rank 16 to 3&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.cn&lt;/strong&gt;: 211,764 to 348,187 (&lt;strong&gt;+136,423, +64.4%&lt;/strong&gt;) | rank 8 to 5&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.vip&lt;/strong&gt;: 108,138 to 137,209 (&lt;strong&gt;+29,071, +26.9%&lt;/strong&gt;) | rank 17 to 15&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.xyz&lt;/strong&gt;: 751,496 to 946,600 (&lt;strong&gt;+195,104, +26.0%&lt;/strong&gt;) | rank 2 to 2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.site&lt;/strong&gt;: 157,060 to 185,346 (&lt;strong&gt;+28,286, +18.0%&lt;/strong&gt;) | rank 9 to 10&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.ru&lt;/strong&gt;: 147,654 to 170,151 (&lt;strong&gt;+22,497, +15.2%&lt;/strong&gt;) | rank 11 to 11&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.com&lt;/strong&gt;: 3,379,656 to 3,780,477 (&lt;strong&gt;+400,821, +11.9%&lt;/strong&gt;) | rank 1 to 1&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.net&lt;/strong&gt;: 213,811 to 233,700 (&lt;strong&gt;+19,889, +9.3%&lt;/strong&gt;) | rank 7 to 9&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Biggest DOWNs (July to August)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;.shop&lt;/strong&gt;: 374,120 to 293,099 (&lt;strong&gt;-81,021, -21.7%&lt;/strong&gt;) | rank 4 to 6&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.info&lt;/strong&gt;: 139,141 to 129,274 (&lt;strong&gt;-9,867, -7.1%&lt;/strong&gt;) | rank 12 to 17&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.app&lt;/strong&gt;: 122,459 to 118,324 (&lt;strong&gt;-4,135, -3.4%&lt;/strong&gt;) | rank 15 to 18&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.cc&lt;/strong&gt;: 149,392 to 147,272 (&lt;strong&gt;-2,120, -1.4%&lt;/strong&gt;) | rank 10 to 12&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.online&lt;/strong&gt;: 242,883 to 242,838 (&lt;strong&gt;-45, -0.0%&lt;/strong&gt;) | rank 6 to 8&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Leaderboard (absolute counts)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcl9u6fvnxfj44j0gj9gs.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcl9u6fvnxfj44j0gj9gs.webp" alt="Newly Registered Domains by TLD" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this says:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;.com&lt;/strong&gt; recovered &lt;strong&gt;+11.9%&lt;/strong&gt; to &lt;strong&gt;3,780,477 registrations&lt;/strong&gt;, reversing July's softening and setting a new high for the series. The recovery is broad-based rather than event-driven. The second half of the month ran a visibly higher floor (&lt;strong&gt;~140k to 160k/day&lt;/strong&gt;) than the first (&lt;strong&gt;~85k to 131k/day&lt;/strong&gt;), with the only anomalous readings being the August 8 trough and the August 9 rebound.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The .de Deluge&lt;/strong&gt; is the month's dominant fact and its largest interpretive risk. At &lt;strong&gt;690,812&lt;/strong&gt; registrations .de arrives at rank &lt;strong&gt;#3&lt;/strong&gt; from outside the top fifteen. But &lt;strong&gt;66%&lt;/strong&gt; of that volume landed on five days, the surrounding baseline averages &lt;strong&gt;~9k/day&lt;/strong&gt;, and four German registrars appeared in the Top-20 in the same month. Treat the placement as the product of a batch or promotional event. Model September from the &lt;strong&gt;~6k to 9k&lt;/strong&gt; post-plateau floor, not from the monthly total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.xyz&lt;/strong&gt; regained &lt;strong&gt;+26.0%&lt;/strong&gt; to &lt;strong&gt;946,600&lt;/strong&gt; registrations and holds rank &lt;strong&gt;#2&lt;/strong&gt;, retaining the runner-up slot it has occupied since June. Its own shape is the &lt;strong&gt;Twin-Window Campaign&lt;/strong&gt;: roughly &lt;strong&gt;72%&lt;/strong&gt; of volume inside &lt;strong&gt;August 5 to 10&lt;/strong&gt; and &lt;strong&gt;August 18 to 22&lt;/strong&gt;, then a &lt;strong&gt;~0.5k to 14k&lt;/strong&gt; floor from &lt;strong&gt;August 23&lt;/strong&gt; onward. This is the second consecutive month in which .xyz has posted a large headline number on top of a collapsing exit run rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.top held its ground&lt;/strong&gt; at &lt;strong&gt;427,684&lt;/strong&gt; (&lt;strong&gt;+9.2%&lt;/strong&gt;), keeping a place in the Top-5 pie. Its daily chart is the choppiest in the set: three separate one-day peaks near &lt;strong&gt;~29k&lt;/strong&gt; (August 9, August 21, August 26) against a &lt;strong&gt;~6k to 17k&lt;/strong&gt; baseline, and two collapses to &lt;strong&gt;~3.6k&lt;/strong&gt; and &lt;strong&gt;~1.5k&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The bottom of the roster rotated toward country-code and regional space.&lt;/strong&gt; &lt;strong&gt;.icu&lt;/strong&gt;, &lt;strong&gt;.br&lt;/strong&gt; and &lt;strong&gt;.click&lt;/strong&gt; exited while &lt;strong&gt;.co&lt;/strong&gt;, &lt;strong&gt;.in&lt;/strong&gt; and &lt;strong&gt;.asia&lt;/strong&gt; entered. The only meaningful decliner among established extensions was &lt;strong&gt;.shop&lt;/strong&gt;, whose &lt;strong&gt;-21.7%&lt;/strong&gt; retreat to &lt;strong&gt;293,099&lt;/strong&gt; ends the two-month &lt;strong&gt;.shop Ascent&lt;/strong&gt; that defined June and July.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Share of Top 5 TLDs - Pie View
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcysmip8tcwwvydoq78dz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcysmip8tcwwvydoq78dz.webp" alt="Top 5 TLDs: Pie View" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;top five TLDs together contribute 70.9%&lt;/strong&gt; of all newly registered domains in August 2026 within the Top-20 cohort. "Others" (ranks #6 to #20) make up the remaining &lt;strong&gt;29.1%&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;.com&lt;/strong&gt; captures &lt;strong&gt;43.3%&lt;/strong&gt; of Top-20 TLD volume, down from July's 46.2% despite growing &lt;strong&gt;+11.9%&lt;/strong&gt; in absolute terms, because the cohort around it expanded faster. It remains roughly &lt;strong&gt;4.0 times&lt;/strong&gt; the size of the next extension.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.xyz&lt;/strong&gt; holds &lt;strong&gt;10.8%&lt;/strong&gt;, up slightly from July's 10.3%, confirming that the extension's structural gain since June has held even as its month-end run rate has not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.de&lt;/strong&gt; takes &lt;strong&gt;7.9%&lt;/strong&gt;, up from &lt;strong&gt;1.6%&lt;/strong&gt; in July. This is the single most dramatic share movement the series has recorded, and also the least durable. On the post-plateau daily floor .de would occupy roughly &lt;strong&gt;3%&lt;/strong&gt; of the cohort.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.top&lt;/strong&gt; follows at &lt;strong&gt;4.9%&lt;/strong&gt;, down from 5.4%, a share loss produced entirely by cohort growth rather than by any weakness in its own &lt;strong&gt;+9.2%&lt;/strong&gt; advance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.cn&lt;/strong&gt; rounds out the top five at &lt;strong&gt;4.0%&lt;/strong&gt;, up from 2.9%, entering the pie for the first time since the spring on the strength of a single &lt;strong&gt;August 18&lt;/strong&gt; event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-tail market presence:&lt;/strong&gt; The &lt;strong&gt;"Others"&lt;/strong&gt; bucket at &lt;strong&gt;29.1%&lt;/strong&gt; was effectively flat against July's 29.5%, and &lt;strong&gt;.shop&lt;/strong&gt;, which held 5.1% of the pie last month, fell out of the Top-5 entirely. Concentration therefore rose this month, but it rose because of one anomalous extension. On an event-adjusted basis the long tail is broadly where July left it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Daily Dynamics - Top 5 TLDs
&lt;/h3&gt;

&lt;h4&gt;
  
  
  .com
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi0d6tdpt39fp9mb7vpqb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi0d6tdpt39fp9mb7vpqb.webp" alt=".com tld daily dynamics" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High-floor operating band (~85k to 160k):&lt;/strong&gt; .com traded coherently all month, with first-half weekdays clustered between &lt;strong&gt;~108k and ~131k&lt;/strong&gt; and second-half weekdays stepping up to &lt;strong&gt;~140k to 160k&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly high on August 9 (~189.4k):&lt;/strong&gt; The peak is the rebound day immediately after the blackout, roughly &lt;strong&gt;1.2 times&lt;/strong&gt; the extension's normal ceiling. The strongest non-anomalous reading came in at &lt;strong&gt;~160.0k on August 26&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The August 8 Blackout (~17.5k):&lt;/strong&gt; .com's month low is roughly &lt;strong&gt;one-fifth&lt;/strong&gt; of its weekend floor and &lt;strong&gt;one-seventh&lt;/strong&gt; of a normal weekday, a break no demand pattern produces and the deepest relative shortfall .com has recorded in the series.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean weekly cyclical troughs on days 2 to 3, 16 to 17, 23 to 24 and 30 to 31:&lt;/strong&gt; Setting the blackout aside, each Sunday and Monday pair dropped into the &lt;strong&gt;~85k to 107k&lt;/strong&gt; band before recovering. The month closed near &lt;strong&gt;~106k&lt;/strong&gt; with a materially higher weekday run rate than it opened.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  .xyz
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frq5baicbrr4q5riiy06a.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frq5baicbrr4q5riiy06a.webp" alt=".xyz tld daily dynamics" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two elevated windows and a collapse (~0.5k to 98.7k):&lt;/strong&gt; .xyz ran &lt;strong&gt;~55k to 98.7k/day&lt;/strong&gt; across &lt;strong&gt;August 5 to 10&lt;/strong&gt;, cooled to a &lt;strong&gt;~15k to 29k&lt;/strong&gt; middle band, ran hot again at &lt;strong&gt;~56k to 66.6k&lt;/strong&gt; across &lt;strong&gt;August 18 to 22&lt;/strong&gt;, then fell to &lt;strong&gt;~0.5k to 14k&lt;/strong&gt; for the final nine days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly high on August 9 (~98.7k):&lt;/strong&gt; The peak sits inside the first window rather than standing alone, with &lt;strong&gt;~79.6k on August 7&lt;/strong&gt; and &lt;strong&gt;~72.4k on August 5&lt;/strong&gt; beside it. This is a sustained campaign block, not a one-day burst.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concentration:&lt;/strong&gt; The two windows together carry roughly &lt;strong&gt;72%&lt;/strong&gt; of .xyz's &lt;strong&gt;946,600&lt;/strong&gt; registrations across &lt;strong&gt;11 of 31 days&lt;/strong&gt;, which is how the extension posts &lt;strong&gt;+26.0%&lt;/strong&gt; growth while exiting the month below &lt;strong&gt;5k/day&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Month low on August 25 (~0.5k):&lt;/strong&gt; The floor after the second window is lower than anything .xyz recorded in July, and the final week held &lt;strong&gt;~4.0k to 8.2k&lt;/strong&gt;. If sustained, that run rate implies a very large September decline.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  .de
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp6n2micnewrj6nkvg7i8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp6n2micnewrj6nkvg7i8.webp" alt=".de tld daily dynamics" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One batch plateau against a flat baseline (~3.3k to 95.1k):&lt;/strong&gt; Across the &lt;strong&gt;26&lt;/strong&gt; non-plateau days .de traded between &lt;strong&gt;~3.3k and ~31.7k&lt;/strong&gt;, averaging &lt;strong&gt;~9,000/day&lt;/strong&gt;. The five plateau days are the entire story of its rank #3 placement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly high on August 13 (~95.1k):&lt;/strong&gt; The plateau opens at its peak, roughly &lt;strong&gt;ten times&lt;/strong&gt; the surrounding baseline, and no reading outside &lt;strong&gt;August 13 to 17&lt;/strong&gt; exceeds &lt;strong&gt;~31.7k&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The August 13 to 17 plateau (~89.7k to 95.1k):&lt;/strong&gt; Five consecutive days within a &lt;strong&gt;~5.4k&lt;/strong&gt; range. That flatness is itself diagnostic, since organic daily demand does not hold within 6% for five days at ten times its own baseline. These five days contribute roughly &lt;strong&gt;456,700&lt;/strong&gt; registrations, or &lt;strong&gt;66%&lt;/strong&gt; of the monthly total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complete reversion at the close:&lt;/strong&gt; From &lt;strong&gt;August 25&lt;/strong&gt; onward .de held &lt;strong&gt;~5.6k to 8.7k&lt;/strong&gt;, closing the month at &lt;strong&gt;~6.6k&lt;/strong&gt;, below its own August 1 reading. Setting the plateau aside, the extension's underlying run rate is &lt;strong&gt;~9,000/day&lt;/strong&gt; against July's &lt;strong&gt;~3,900/day&lt;/strong&gt;: real growth, but a fraction of the headline.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  .top
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh699z69kj1es288agax6.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh699z69kj1es288agax6.webp" alt=".top tld daily dynamics" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Choppy mid-volume profile with no weekly cadence (~1.5k to 29.3k):&lt;/strong&gt; .top alternated between a &lt;strong&gt;~6k to 17k&lt;/strong&gt; baseline and repeated isolated jumps, with no seven-day rhythm visible anywhere in the series.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three near-equal peaks (~26.5k to 29.3k):&lt;/strong&gt; The monthly high came on &lt;strong&gt;August 26 (~29.3k)&lt;/strong&gt;, effectively tied with &lt;strong&gt;August 9 (~29.1k)&lt;/strong&gt; and followed by &lt;strong&gt;August 21 (~26.5k)&lt;/strong&gt;, a roughly weekly cadence of one-day bursts rather than a sustained elevation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two separate collapses:&lt;/strong&gt; .top hit &lt;strong&gt;~3.6k on August 8&lt;/strong&gt; with the rest of the market, then &lt;strong&gt;~1.5k on August 25&lt;/strong&gt;, a Tuesday on which no other chart in the set shows a shortfall. That extension-specific gap was immediately followed by its monthly high.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mid-month soft patch:&lt;/strong&gt; &lt;strong&gt;August 11 to 17&lt;/strong&gt; held a subdued &lt;strong&gt;~6.1k to 8.5k&lt;/strong&gt;, the weakest sustained stretch of the period. The month closed near &lt;strong&gt;~11.0k&lt;/strong&gt;, roughly in line with its own baseline.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  .cn
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz4de48c2j7syvhdlkrja.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz4de48c2j7syvhdlkrja.webp" alt=".cn tld daily dynamics" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Low, spiky baseline (~3.0k to 16.9k, excluding one day):&lt;/strong&gt; For thirty of thirty-one days .cn traded in a narrow low band, with typical readings between &lt;strong&gt;~4k and ~13k&lt;/strong&gt; and no weekly pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The August 18 spike (~83.7k):&lt;/strong&gt; The monthly high sits at roughly &lt;strong&gt;six times&lt;/strong&gt; the surrounding baseline and delivers &lt;strong&gt;24%&lt;/strong&gt; of .cn's monthly volume in a single day, collapsing to &lt;strong&gt;~10.4k&lt;/strong&gt; the next day. This is the same date on which &lt;strong&gt;.xyz&lt;/strong&gt;, &lt;strong&gt;GMO Internet&lt;/strong&gt; and &lt;strong&gt;Japan&lt;/strong&gt; all opened their second window, but at a far greater relative amplitude.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No August 8 shortfall:&lt;/strong&gt; Along with &lt;strong&gt;.de&lt;/strong&gt; and &lt;strong&gt;.xyz&lt;/strong&gt;, .cn read &lt;strong&gt;~12.3k on August 8&lt;/strong&gt;, above its own monthly baseline, placing it outside the blackout entirely and pointing to a separate ingestion path for the extension.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firmer second half:&lt;/strong&gt; After &lt;strong&gt;August 19&lt;/strong&gt; .cn held &lt;strong&gt;~7.2k to 16.9k&lt;/strong&gt; against a first-half range nearer &lt;strong&gt;~3k to 12k&lt;/strong&gt;, a modest step-up that underpins perhaps a third of its &lt;strong&gt;+64.4%&lt;/strong&gt; monthly gain on non-anomalous volume.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Top 20 Countries - Full Period
&lt;/h2&gt;

&lt;p&gt;The country landscape in August 2026 expanded &lt;strong&gt;+11.3%&lt;/strong&gt; to &lt;strong&gt;5,133,486&lt;/strong&gt; registrations, the mildest cohort growth of the three dimensions and a useful check on the TLD numbers. If the .de Deluge had been driven by identifiable German registrants, the country cohort would have grown far faster. Instead &lt;strong&gt;Germany&lt;/strong&gt; added &lt;strong&gt;160,782&lt;/strong&gt; registrations against .de's &lt;strong&gt;+571,109&lt;/strong&gt;, meaning roughly &lt;strong&gt;72%&lt;/strong&gt; of the .de event did not resolve to a German registrant address. Beneath the leaders, Asian markets reversed July's correction almost across the board and three new geographies entered the table.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparison with July 2026
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Newly added in August (entered Top-20)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Portugal&lt;/strong&gt; to 67,688 (new entry, ranked #13)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Russia&lt;/strong&gt; to 43,809 (new entry, ranked #16)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indonesia&lt;/strong&gt; to 31,951 (re-entry after a one-month absence, ranked #20)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Removed in August (dropped out of Top-20)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Poland&lt;/strong&gt; was 29,449 in July (ranked #17), not in August Top-20&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Australia&lt;/strong&gt; was 27,615 in July (ranked #18), not in August Top-20&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Italy&lt;/strong&gt; was 26,252 in July (ranked #19), not in August Top-20&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Biggest UPs (July to August)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Germany&lt;/strong&gt;: 86,351 to 247,133 (&lt;strong&gt;+160,782, +186.2%&lt;/strong&gt;) | rank 10 to 5&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Albania&lt;/strong&gt;: 52,932 to 96,773 (&lt;strong&gt;+43,841, +82.8%&lt;/strong&gt;) | rank 12 to 11&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hong Kong&lt;/strong&gt;: 26,175 to 38,442 (&lt;strong&gt;+12,267, +46.9%&lt;/strong&gt;) | rank 20 to 18&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;China&lt;/strong&gt;: 301,047 to 403,409 (&lt;strong&gt;+102,362, +34.0%&lt;/strong&gt;) | rank 4 to 4&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Netherlands&lt;/strong&gt;: 46,071 to 61,730 (&lt;strong&gt;+15,659, +34.0%&lt;/strong&gt;) | rank 13 to 14&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canada&lt;/strong&gt;: 110,242 to 129,428 (&lt;strong&gt;+19,186, +17.4%&lt;/strong&gt;) | rank 8 to 8&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turkey&lt;/strong&gt;: 40,357 to 46,911 (&lt;strong&gt;+6,554, +16.2%&lt;/strong&gt;) | rank 14 to 15&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;United States&lt;/strong&gt;: 1,949,448 to 2,233,030 (&lt;strong&gt;+283,582, +14.5%&lt;/strong&gt;) | rank 1 to 1&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Biggest DOWNs (July to August)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Iceland&lt;/strong&gt;: 740,960 to 550,237 (&lt;strong&gt;-190,723, -25.7%&lt;/strong&gt;) | rank 2 to 2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;France&lt;/strong&gt;: 114,319 to 103,431 (&lt;strong&gt;-10,888, -9.5%&lt;/strong&gt;) | rank 7 to 10&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Japan&lt;/strong&gt;: 538,930 to 509,410 (&lt;strong&gt;-29,520, -5.5%&lt;/strong&gt;) | rank 3 to 3&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Leaderboard (absolute counts)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbmuk10d26hajymhohjyf.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbmuk10d26hajymhohjyf.webp" alt="Newly Registered Domains By Country" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this says:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Total (Top-20)&lt;/strong&gt; rose from &lt;strong&gt;4,613,954&lt;/strong&gt; in July to &lt;strong&gt;5,133,486&lt;/strong&gt; in August, &lt;strong&gt;+519,532 (+11.3%)&lt;/strong&gt;, with only three geographies declining. The breadth is notable: fourteen of the seventeen carry-over countries grew, the widest simultaneous advance the country table has posted this year.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The United States&lt;/strong&gt; added &lt;strong&gt;+14.5%&lt;/strong&gt; to &lt;strong&gt;2,233,030 registrations&lt;/strong&gt; and lifted its pie share to &lt;strong&gt;43.5%&lt;/strong&gt; from 42.3%, growing faster than the cohort for the first time in three months. Its daily chart is the market's cleanest weekly rhythm, with Sunday and Monday troughs near &lt;strong&gt;~50k to 56k&lt;/strong&gt; against midweek readings of &lt;strong&gt;~70k to 98k&lt;/strong&gt;, interrupted only by the August 8 low of &lt;strong&gt;~16.9k&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Iceland Fade&lt;/strong&gt; is the month's largest genuine decline: &lt;strong&gt;-25.7%&lt;/strong&gt; to &lt;strong&gt;550,237&lt;/strong&gt; registrations and a pie share down &lt;strong&gt;5.4 points&lt;/strong&gt; to &lt;strong&gt;10.7%&lt;/strong&gt;. Unlike the Japanese moves of recent months there is no window structure here, just a steady week-on-week drift from a &lt;strong&gt;~22k to 28k&lt;/strong&gt; opening band to &lt;strong&gt;~13k to 16k&lt;/strong&gt; at the close.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Germany's arrival at #5&lt;/strong&gt; with &lt;strong&gt;247,133&lt;/strong&gt; registrations is the country-side signature of the .de Deluge, and its daily chart carries the same plateau: &lt;strong&gt;~20.3k to 24.5k&lt;/strong&gt; across &lt;strong&gt;August 13 to 17&lt;/strong&gt; against a &lt;strong&gt;~2.3k to 7.5k&lt;/strong&gt; baseline, plus an earlier bump to &lt;strong&gt;~19.6k on August 4&lt;/strong&gt;. That the country total captures only &lt;strong&gt;28%&lt;/strong&gt; of the .de increase is itself the most useful finding. The batch registrants were overwhelmingly not identifiable as German.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asian markets reversed July's correction:&lt;/strong&gt; &lt;strong&gt;China +34.0%&lt;/strong&gt;, &lt;strong&gt;Hong Kong +46.9%&lt;/strong&gt;, &lt;strong&gt;South Korea +12.8%&lt;/strong&gt; and &lt;strong&gt;Indonesia&lt;/strong&gt; back in the table, while &lt;strong&gt;Japan&lt;/strong&gt; slipped only &lt;strong&gt;-5.5%&lt;/strong&gt; despite its Twin-Window profile. Set against &lt;strong&gt;France -9.5%&lt;/strong&gt; and the exits of &lt;strong&gt;Poland&lt;/strong&gt;, &lt;strong&gt;Australia&lt;/strong&gt; and &lt;strong&gt;Italy&lt;/strong&gt;, the month's regional story is a clear rotation from Western Europe toward East and Southeast Asia.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Share of Top 5 Countries - Pie View
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffru8ug9h4vvhp72tu0kd.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffru8ug9h4vvhp72tu0kd.webp" alt="Top 5 Countries: Pie View" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;top five countries together contribute 76.8%&lt;/strong&gt; of all newly registered domains in August 2026 within the Top-20 cohort. "Others" (ranks #6 to #20) make up the remaining &lt;strong&gt;23.2%&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;United States&lt;/strong&gt; leads at &lt;strong&gt;43.5%&lt;/strong&gt; of Top-20 registrations, up from July's 42.3%, and this time on genuine volume growth of &lt;strong&gt;+14.5%&lt;/strong&gt; rather than on the cohort shrinking around it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iceland&lt;/strong&gt; holds &lt;strong&gt;10.7%&lt;/strong&gt;, down sharply from &lt;strong&gt;16.1%&lt;/strong&gt;, the largest share loss of any geography this month and the direct expression of the Iceland Fade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Japan&lt;/strong&gt; takes &lt;strong&gt;9.9%&lt;/strong&gt;, easing from 11.7%, a third consecutive month of compression, though the underlying &lt;strong&gt;-5.5%&lt;/strong&gt; volume decline is far shallower than the share move suggests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;China&lt;/strong&gt; captures &lt;strong&gt;7.9%&lt;/strong&gt;, up from 6.5%, recovering the ground it lost in July and restoring the geography to its spring weight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Germany&lt;/strong&gt; rounds out the top five at &lt;strong&gt;4.8%&lt;/strong&gt;, up from &lt;strong&gt;1.9%&lt;/strong&gt;, entering the Top-5 country pie for the first time in the series, a placement that will not survive September unless the .de channel reopens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implication:&lt;/strong&gt; August returns to a four-market geography with a batch-driven fifth. The interpretive question for September is whether Iceland's fade continues toward the 400k range, whether China's recovery holds, and whether Germany reverts to its 1.9% baseline share. &lt;strong&gt;Albania&lt;/strong&gt;, which debuted at #12 last month and grew a further &lt;strong&gt;+82.8%&lt;/strong&gt; to &lt;strong&gt;96,773&lt;/strong&gt;, has now delivered the second month of confirmation that July's report called for, and should be treated as a real market rather than a provisional entry.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Daily Dynamics - Top 5 Countries
&lt;/h3&gt;

&lt;h4&gt;
  
  
  United States
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7g7npcwquo4ar3vfz1nj.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7g7npcwquo4ar3vfz1nj.webp" alt="united states country daily dynamics" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wide but coherent band (~50k to 108k):&lt;/strong&gt; US daily registrations ran &lt;strong&gt;~70k to 98k&lt;/strong&gt; on typical weekdays with Sunday and Monday softening into the &lt;strong&gt;~50k to 56k&lt;/strong&gt; range, the most regular seven-day cadence in the entire chart set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly high on August 9 (~108.3k):&lt;/strong&gt; The peak is the post-blackout rebound. The strongest organic readings came at &lt;strong&gt;~98.3k on August 13&lt;/strong&gt;, &lt;strong&gt;~96.9k on August 29&lt;/strong&gt; and &lt;strong&gt;~94.8k on August 28&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The August 8 Blackout (~16.9k):&lt;/strong&gt; The month low sits at roughly &lt;strong&gt;one-third&lt;/strong&gt; of a normal weekend reading and &lt;strong&gt;one-fifth&lt;/strong&gt; of a weekday, matching the aggregate market's shortfall almost exactly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firm close:&lt;/strong&gt; After the August 23 and 24 trough the series ran &lt;strong&gt;~63k to 96.9k&lt;/strong&gt; through August 29 before the routine month-end weekend dip to &lt;strong&gt;~56k&lt;/strong&gt;, exiting August with a weekday floor visibly above July's.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Iceland
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6pdeok1c4cv5qakjs903.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6pdeok1c4cv5qakjs903.webp" alt="iceland country daily dynamics" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A steadily declining band (~4.4k to 31.9k):&lt;/strong&gt; Iceland opened the month at &lt;strong&gt;~22k to 28.6k/day&lt;/strong&gt; and closed it at &lt;strong&gt;~12.7k to 16.2k&lt;/strong&gt;, with the intervening weeks stepping down in sequence, the clearest monotone drift of any large geography in the set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly high on August 9 (~31.9k):&lt;/strong&gt; As elsewhere, the peak is the rebound day. The strongest pre-blackout reading was &lt;strong&gt;~28.6k on August 4&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The August 8 Blackout (~4.4k):&lt;/strong&gt; Iceland's month low is roughly &lt;strong&gt;one-fifth&lt;/strong&gt; of its opening-week average, confirming the geography sits squarely inside the affected ingestion path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orderly close:&lt;/strong&gt; The final week held &lt;strong&gt;~9.5k to 16.2k&lt;/strong&gt; with a single soft reading on &lt;strong&gt;August 28&lt;/strong&gt;, ending August near &lt;strong&gt;~12.7k&lt;/strong&gt;. There is no window structure and no single break. This is a genuine demand decline, and the &lt;strong&gt;-25.7%&lt;/strong&gt; monthly figure should be taken at face value.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Japan
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnxd6rjbxgkxl3igun7xd.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnxd6rjbxgkxl3igun7xd.webp" alt="japan country daily dynamics" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two windows and a low floor (~2.3k to 65.6k):&lt;/strong&gt; Japan ran &lt;strong&gt;~27.2k to 65.6k/day&lt;/strong&gt; across &lt;strong&gt;August 5 to 10&lt;/strong&gt;, dropped to a &lt;strong&gt;~2.3k to 4.3k&lt;/strong&gt; floor from &lt;strong&gt;August 11 to 17&lt;/strong&gt;, ran &lt;strong&gt;~14.5k to 52.0k&lt;/strong&gt; across &lt;strong&gt;August 18 to 23&lt;/strong&gt;, then returned to &lt;strong&gt;~3.2k to 6.2k&lt;/strong&gt; for the final eight days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly high on August 9 (~65.6k):&lt;/strong&gt; The peak sits inside the first window with &lt;strong&gt;~52.0k on August 5&lt;/strong&gt; and &lt;strong&gt;~44.7k on August 7&lt;/strong&gt; beside it. The second window peaked at an almost identical &lt;strong&gt;~52.0k on August 18&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concentration:&lt;/strong&gt; The two windows together carry roughly &lt;strong&gt;82%&lt;/strong&gt; of Japan's &lt;strong&gt;509,410&lt;/strong&gt; registrations across &lt;strong&gt;12 of 31 days&lt;/strong&gt;, a tighter concentration than either of the two prior months, even though the monthly total barely moved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interpretation:&lt;/strong&gt; Japan's volume is now almost entirely episodic. A geography that spends nineteen days of the month near &lt;strong&gt;~3k/day&lt;/strong&gt; and produces four-fifths of its total in two blocks is not showing organic demand at the 500k level. September should be modelled from the &lt;strong&gt;~3k to 6k&lt;/strong&gt; floor with any window volume treated as a separate event.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  China
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqe3vl8f6b0jyt1ldqg9k.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqe3vl8f6b0jyt1ldqg9k.webp" alt="china country daily dynamics" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spiky mid-volume profile (~1.8k to 24.7k):&lt;/strong&gt; China traded without a stable floor, alternating between &lt;strong&gt;~7k to 12k&lt;/strong&gt; readings and repeated jumps to roughly double that, with no weekly cadence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly high on August 29 (~24.7k):&lt;/strong&gt; Unusually for this month, China's peak is not the rebound day. &lt;strong&gt;August 29&lt;/strong&gt; edged past &lt;strong&gt;~23.4k on August 5&lt;/strong&gt; and &lt;strong&gt;~23.2k on August 9&lt;/strong&gt;, with a fourth distinct spike at &lt;strong&gt;~21.3k on August 21&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The August 8 Blackout (~1.8k):&lt;/strong&gt; China's month low is the deepest relative shortfall of any Top-5 country, roughly &lt;strong&gt;one-fifth&lt;/strong&gt; of its own baseline, and the only reading below 5k in the series.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rising floor:&lt;/strong&gt; The inter-spike floor sat near &lt;strong&gt;~9k to 18k&lt;/strong&gt; across the second half against &lt;strong&gt;~6.5k to 13k&lt;/strong&gt; in the first, which is what produces the &lt;strong&gt;+34.0%&lt;/strong&gt; monthly recovery and moves China's pie share back to &lt;strong&gt;7.9%&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Germany
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhugoekf9nlso474qdqbd.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhugoekf9nlso474qdqbd.webp" alt="germany country daily dynamics" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Baseline plus two batch windows (~1.8k to 24.5k):&lt;/strong&gt; For twenty-four of thirty-one days Germany traded between &lt;strong&gt;~1.8k and ~7.5k&lt;/strong&gt;. The remaining seven days produce its rank #5 placement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly high on August 17 (~24.5k):&lt;/strong&gt; The peak closes the &lt;strong&gt;August 13 to 17&lt;/strong&gt; plateau, which held a remarkably flat &lt;strong&gt;~20.3k to 24.5k&lt;/strong&gt;, the same five-day block and the same flatness visible in the .de chart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The August 4 and 5 window (~19.6k, ~14.5k):&lt;/strong&gt; Germany's first burst is a two-day event at roughly &lt;strong&gt;seven times&lt;/strong&gt; its own baseline, well ahead of the mid-month plateau and unmatched by any comparable move in the neighbouring Western European markets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complete reversion at the close:&lt;/strong&gt; From &lt;strong&gt;August 18&lt;/strong&gt; onward Germany held &lt;strong&gt;~3.4k to 6.7k&lt;/strong&gt;, closing the month at &lt;strong&gt;~3.4k&lt;/strong&gt;, below its own August 1 reading. On that floor the geography reverts to roughly its July rank.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Top 20 Registrars - Full Period
&lt;/h2&gt;

&lt;p&gt;The registrar landscape in August 2026 is where the month's two structural stories are easiest to separate. The Top-20 aggregate rose &lt;strong&gt;+11.9%&lt;/strong&gt; to &lt;strong&gt;8,041,860&lt;/strong&gt; registrations, but the composition changed more than the total. &lt;strong&gt;Four German registrars entered simultaneously&lt;/strong&gt; with a combined &lt;strong&gt;574,266&lt;/strong&gt; registrations, &lt;strong&gt;GMO Internet Group&lt;/strong&gt; reclaimed &lt;strong&gt;#3&lt;/strong&gt; on the Twin-Window Campaign, and at the very top &lt;strong&gt;Namecheap&lt;/strong&gt; closed to within &lt;strong&gt;50,204&lt;/strong&gt; registrations of &lt;strong&gt;GoDaddy&lt;/strong&gt;, the narrowest margin the series has recorded. Neither leader grew meaningfully. The entire cohort expansion came from behind them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparison with July 2026
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Newly added in August (entered Top-20)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Domainers Registrar AG&lt;/strong&gt;: 199,755 (new entry, ranked #11)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EliteDomains GmbH&lt;/strong&gt;: 132,790 (new entry, ranked #17)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nomino GmbH&lt;/strong&gt;: 124,856 (new entry, ranked #19)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;InternetX GmbH&lt;/strong&gt;: 116,865 (new entry, ranked #20)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Removed in August (dropped out of Top-20)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wix.com Ltd&lt;/strong&gt; was 78,894 in July (ranked #16), not in August Top-20&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gransy, s.r.o&lt;/strong&gt; was 78,327 in July (ranked #17), not in August Top-20&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gname.com Pte. Ltd&lt;/strong&gt; was 73,542 in July (ranked #19), not in August Top-20&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Realtime Register B.V.&lt;/strong&gt; was 62,690 in July (ranked #20), not in August Top-20&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Biggest UPs (July to August)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Beijing Xinnet Digital Information Technology&lt;/strong&gt;: 88,343 to 199,969 (&lt;strong&gt;+111,626, +126.4%&lt;/strong&gt;) | rank 15 to 10&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dominet (HK) Limited&lt;/strong&gt;: 76,238 to 139,797 (&lt;strong&gt;+63,559, +83.4%&lt;/strong&gt;) | rank 18 to 15&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GMO Internet Group (onamae.com)&lt;/strong&gt;: 715,662 to 933,005 (&lt;strong&gt;+217,343, +30.4%&lt;/strong&gt;) | rank 4 to 3&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare, Inc&lt;/strong&gt;: 401,409 to 453,406 (&lt;strong&gt;+51,997, +13.0%&lt;/strong&gt;) | rank 7 to 7&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hostinger Operations, UAB&lt;/strong&gt;: 430,472 to 478,304 (&lt;strong&gt;+47,832, +11.1%&lt;/strong&gt;) | rank 6 to 6&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tucows, Inc&lt;/strong&gt;: 158,014 to 172,187 (&lt;strong&gt;+14,173, +9.0%&lt;/strong&gt;) | rank 12 to 14&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NameSilo, LLC&lt;/strong&gt;: 290,995 to 315,338 (&lt;strong&gt;+24,343, +8.4%&lt;/strong&gt;) | rank 8 to 8&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Porkbun, LLC&lt;/strong&gt;: 183,222 to 195,487 (&lt;strong&gt;+12,265, +6.7%&lt;/strong&gt;) | rank 10 to 12&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Biggest DOWNs (July to August)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PDR Ltd. (PublicDomainRegistry.com)&lt;/strong&gt;: 136,755 to 128,328 (&lt;strong&gt;-8,427, -6.2%&lt;/strong&gt;) | rank 13 to 18&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynadot Inc&lt;/strong&gt;: 915,698 to 886,583 (&lt;strong&gt;-29,115, -3.2%&lt;/strong&gt;) | rank 3 to 4&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Squarespace Domains LLC&lt;/strong&gt;: 271,201 to 262,791 (&lt;strong&gt;-8,410, -3.1%&lt;/strong&gt;) | rank 9 to 9&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Leaderboard (absolute counts)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3eicxsgdkg8y4fleycui.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3eicxsgdkg8y4fleycui.webp" alt="Newly Registered Domains by Registrar" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this says:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Total volume (Top-20)&lt;/strong&gt; rose from &lt;strong&gt;7,187,836&lt;/strong&gt; in July to &lt;strong&gt;8,041,860&lt;/strong&gt; in August, &lt;strong&gt;+854,024 (+11.9%)&lt;/strong&gt;. Accounting for the four July registrars that dropped out (a combined &lt;strong&gt;293,453&lt;/strong&gt; registrations) and the four German registrars that replaced them (&lt;strong&gt;574,266&lt;/strong&gt;), the carry-over registrars grew by roughly &lt;strong&gt;+573,211&lt;/strong&gt;. &lt;strong&gt;GMO Internet&lt;/strong&gt; supplies &lt;strong&gt;217,343&lt;/strong&gt; of that, leaving about &lt;strong&gt;+355,868&lt;/strong&gt; spread across the remaining sixteen carry-over names.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tightest top-two in the series:&lt;/strong&gt; &lt;strong&gt;GoDaddy&lt;/strong&gt; added only &lt;strong&gt;+0.3%&lt;/strong&gt; to &lt;strong&gt;1,192,684&lt;/strong&gt; and &lt;strong&gt;Namecheap +3.0%&lt;/strong&gt; to &lt;strong&gt;1,142,480&lt;/strong&gt;, leaving a gap of &lt;strong&gt;50,204 registrations&lt;/strong&gt;. Their combined share of Top-20 volume fell to &lt;strong&gt;29.0%&lt;/strong&gt; from &lt;strong&gt;32.0%&lt;/strong&gt;, the lowest duopoly concentration the series has recorded. On current trajectories the top two could swap positions in September.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The German Registrar Cohort&lt;/strong&gt; is the registrar-side proof of the .de Deluge. &lt;strong&gt;Domainers Registrar AG&lt;/strong&gt; arriving directly at &lt;strong&gt;#11&lt;/strong&gt; with &lt;strong&gt;199,755&lt;/strong&gt; registrations, alongside &lt;strong&gt;EliteDomains GmbH&lt;/strong&gt;, &lt;strong&gt;Nomino GmbH&lt;/strong&gt; and &lt;strong&gt;InternetX GmbH&lt;/strong&gt;, is not organic competitive growth. Four previously absent registrars from one national market do not enter a Top-20 in the same month by coincidence. Their combined &lt;strong&gt;574,266&lt;/strong&gt; registrations, plus &lt;strong&gt;Ionos SE's 137,801&lt;/strong&gt;, come to &lt;strong&gt;712,067&lt;/strong&gt;, comfortably enough to account for .de's &lt;strong&gt;690,812&lt;/strong&gt;, and the clearest indication that the extension's surge was processed through a small, identifiable set of channels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GMO reclaims the podium&lt;/strong&gt; with &lt;strong&gt;+30.4%&lt;/strong&gt; to &lt;strong&gt;933,005&lt;/strong&gt; registrations, and its chart is the Twin-Window Campaign in its purest form: &lt;strong&gt;~71k to 97k/day&lt;/strong&gt; across &lt;strong&gt;August 5 to 10&lt;/strong&gt;, &lt;strong&gt;~57k to 70k&lt;/strong&gt; across &lt;strong&gt;August 18 to 22&lt;/strong&gt;, and a collapse to &lt;strong&gt;~1.8k to 15.2k&lt;/strong&gt; afterwards, closing the month at &lt;strong&gt;~1.8k&lt;/strong&gt;. Roughly &lt;strong&gt;75%&lt;/strong&gt; of its volume sits inside those eleven days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asian mid-tier registrars advanced hard&lt;/strong&gt; while European resellers churned out. &lt;strong&gt;Beijing Xinnet&lt;/strong&gt; rose &lt;strong&gt;+126.4%&lt;/strong&gt; to &lt;strong&gt;199,969&lt;/strong&gt; and &lt;strong&gt;Dominet (HK)&lt;/strong&gt; &lt;strong&gt;+83.4%&lt;/strong&gt; to &lt;strong&gt;139,797&lt;/strong&gt;, against the exits of &lt;strong&gt;Wix.com&lt;/strong&gt;, &lt;strong&gt;Gransy&lt;/strong&gt;, &lt;strong&gt;Gname.com&lt;/strong&gt; and &lt;strong&gt;Realtime Register&lt;/strong&gt;. &lt;strong&gt;Cloudflare +13.0%&lt;/strong&gt; and &lt;strong&gt;Hostinger +11.1%&lt;/strong&gt; continued their steady infrastructure-channel growth, the most consistent trend line in the registrar table across the last four months.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Share of Top 5 Registrars - Pie View
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff81yxqz723q8zyn0l6lo.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff81yxqz723q8zyn0l6lo.webp" alt="Top 5 Registrars: Pie View" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Within the top five registrars, with "Others" representing ranks #6 to #20, the August mix is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GoDaddy.com, LLC:&lt;/strong&gt; 14.8%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Namecheap, Inc:&lt;/strong&gt; 14.2%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GMO Internet Group (onamae.com):&lt;/strong&gt; 11.6%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynadot Inc:&lt;/strong&gt; 11.0%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spaceship, Inc:&lt;/strong&gt; 8.1%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Others:&lt;/strong&gt; 40.2%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Implication:&lt;/strong&gt; The &lt;strong&gt;top five together hold 59.8%&lt;/strong&gt; of Top-20 registrar volume, down from July's 63.4%, and the &lt;strong&gt;"Others"&lt;/strong&gt; bucket at &lt;strong&gt;40.2%&lt;/strong&gt; is the largest it has been in the series, a direct consequence of four new entrants arriving at once in the #11 to #20 range. At the top, &lt;strong&gt;0.6 points&lt;/strong&gt; now separate GoDaddy from Namecheap and only &lt;strong&gt;3.8 points&lt;/strong&gt; separate #1 from #4, the flattest podium the series has recorded. Read carefully, this is two different stories in one chart: genuine competitive compression among the leaders, and an artificial dispersion into the long tail created by the German cohort. On an event-adjusted basis the "Others" share would sit closer to &lt;strong&gt;36%&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Daily Dynamics - Top 5 Registrars
&lt;/h3&gt;

&lt;h4&gt;
  
  
  GoDaddy.com, LLC
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4szftnslb4afiddh96bm.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4szftnslb4afiddh96bm.webp" alt="godaddy.com, llc registrar daily dynamics" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stable operating band (~24k to 57k):&lt;/strong&gt; GoDaddy held the steadiest profile in the Top-5, with most weekdays between &lt;strong&gt;~32k and ~43k&lt;/strong&gt; and Sunday and Monday softening into the &lt;strong&gt;~24k to 27k&lt;/strong&gt; range every week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly high on August 29 (~57.1k):&lt;/strong&gt; Unusually, GoDaddy's peak is not the rebound day. &lt;strong&gt;August 29&lt;/strong&gt; edged out &lt;strong&gt;~52.5k on August 9&lt;/strong&gt;, and no other reading exceeded &lt;strong&gt;~43k&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The August 8 Blackout (~7.0k):&lt;/strong&gt; The month low is roughly &lt;strong&gt;one-quarter&lt;/strong&gt; of a normal weekend reading, the deepest single-day break in GoDaddy's chart history and the clearest registrar-level fingerprint of the event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weekly cyclical troughs on days 2 to 3, 10, 16 to 17, 23 to 24 and 30 to 31:&lt;/strong&gt; GoDaddy displays the seven-day cadence as cleanly as .com does, and the month closed near &lt;strong&gt;~26.9k&lt;/strong&gt;, a flat exit consistent with its near-zero &lt;strong&gt;+0.3%&lt;/strong&gt; monthly change.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Namecheap, Inc
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjlrgxjn54gb0dvnubr0y.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjlrgxjn54gb0dvnubr0y.webp" alt="namecheap, inc registrar daily dynamics" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Broad operating band (~23k to 54k):&lt;/strong&gt; Namecheap ran wider than GoDaddy, with typical weekdays between &lt;strong&gt;~32k and ~42k&lt;/strong&gt; and a pronounced weekend softening into the &lt;strong&gt;~23k to 29k&lt;/strong&gt; range.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly high on August 9 (~54.4k):&lt;/strong&gt; The rebound day produced the largest single reading, with three near-equal secondary highs at &lt;strong&gt;~47.8k on August 18&lt;/strong&gt;, &lt;strong&gt;~47.5k on August 4&lt;/strong&gt; and &lt;strong&gt;~47.4k on August 26&lt;/strong&gt;, all outside the anomaly and all substantially above GoDaddy's non-peak ceiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The August 8 Blackout (~5.8k):&lt;/strong&gt; Namecheap's month low is the deepest of any registrar in the set relative to its weekday baseline, roughly &lt;strong&gt;one-quarter&lt;/strong&gt; of a normal weekend day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Softer close:&lt;/strong&gt; The final two days eased to &lt;strong&gt;~26.5k&lt;/strong&gt; and &lt;strong&gt;~25.4k&lt;/strong&gt;, slightly below the monthly average, which tempers the read on Namecheap's approach to the #1 slot without undermining the &lt;strong&gt;+3.0%&lt;/strong&gt; monthly gain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  GMO Internet Group (onamae.com)
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foc3d37l2x2y3g9vhq4w4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foc3d37l2x2y3g9vhq4w4.webp" alt="gmo internet group, inc registrar daily dynamics" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two windows and a collapse (~1.8k to 97.4k):&lt;/strong&gt; GMO ran &lt;strong&gt;~54k to 97.4k/day&lt;/strong&gt; across &lt;strong&gt;August 5 to 10&lt;/strong&gt;, cooled to &lt;strong&gt;~11.3k to 31.5k&lt;/strong&gt; across &lt;strong&gt;August 11 to 17&lt;/strong&gt;, ran &lt;strong&gt;~57k to 70.1k&lt;/strong&gt; across &lt;strong&gt;August 18 to 22&lt;/strong&gt;, then fell to &lt;strong&gt;~1.8k to 15.2k&lt;/strong&gt; for the final nine days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly high on August 9 (~97.4k):&lt;/strong&gt; The peak sits inside the first window, with &lt;strong&gt;~86.8k on August 7&lt;/strong&gt; and &lt;strong&gt;~71.2k on August 5&lt;/strong&gt; immediately around it. The second window peaked at &lt;strong&gt;~70.1k on August 18&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concentration:&lt;/strong&gt; Roughly &lt;strong&gt;75%&lt;/strong&gt; of GMO's &lt;strong&gt;933,005&lt;/strong&gt; registrations landed inside the two windows, spanning &lt;strong&gt;11 of 31 days&lt;/strong&gt;. The registrar's own baseline, the days outside both windows, averages under &lt;strong&gt;~12k/day&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interpretation:&lt;/strong&gt; GMO closed August at &lt;strong&gt;~1.8k&lt;/strong&gt;, its lowest daily reading of the month and below its post-unwind July floor. Its &lt;strong&gt;+30.4%&lt;/strong&gt; monthly gain and return to rank &lt;strong&gt;#3&lt;/strong&gt; therefore describe campaign throughput rather than a recovered book of business, and September volume should be modelled from the post-August-23 floor.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Dynadot Inc
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxkz6derdzgtjxauk8sma.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxkz6derdzgtjxauk8sma.webp" alt="dynadot inc registrar daily dynamics" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rhythmic mid-range band (~8.8k to 39.1k):&lt;/strong&gt; Dynadot traded between roughly &lt;strong&gt;~20k and ~35k&lt;/strong&gt; on most days, with the swings driven by the weekly cycle rather than by campaign activity, the most conventional profile in the Top-5.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly high on August 9 (~39.1k):&lt;/strong&gt; The rebound day narrowly beat &lt;strong&gt;~38.2k on August 28&lt;/strong&gt;, with &lt;strong&gt;~35.3k on August 5&lt;/strong&gt; as the third-highest reading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The August 8 Blackout (~8.8k):&lt;/strong&gt; The month low is roughly &lt;strong&gt;one-half&lt;/strong&gt; of a normal weekend reading, a real shortfall but the shallowest of the four affected registrars, suggesting Dynadot sits only partly inside the affected path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measured close:&lt;/strong&gt; The final week eased into a &lt;strong&gt;~16.9k to 28.4k&lt;/strong&gt; range, ending August near &lt;strong&gt;~16.9k&lt;/strong&gt;, an exit rate below the monthly average and consistent with the &lt;strong&gt;-3.2%&lt;/strong&gt; monthly decline that cost Dynadot the #3 slot.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Spaceship, Inc
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqfk93udg8je4qbk3uuue.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqfk93udg8je4qbk3uuue.webp" alt="spaceship, inc registrar daily dynamics" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mid-range operating band (~4.8k to 37.6k):&lt;/strong&gt; Spaceship held a &lt;strong&gt;~15k to 27k&lt;/strong&gt; range on most days, punctuated by one-day bursts consistent with its established pattern of concentrated activity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly high on August 9 (~37.6k):&lt;/strong&gt; The rebound day produced the peak, with &lt;strong&gt;~30.8k on August 4&lt;/strong&gt;, &lt;strong&gt;~30.7k on August 18&lt;/strong&gt; and &lt;strong&gt;~26.9k on August 13&lt;/strong&gt; as secondary highs, each a single-day event against a surrounding baseline near &lt;strong&gt;~20k&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The August 8 Blackout (~4.8k):&lt;/strong&gt; Spaceship's month low sits at roughly &lt;strong&gt;one-quarter&lt;/strong&gt; of its baseline, placing it firmly inside the affected group alongside GoDaddy, Namecheap and Dynadot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Softening close:&lt;/strong&gt; The final three days ran &lt;strong&gt;~19.8k&lt;/strong&gt;, &lt;strong&gt;~16.2k&lt;/strong&gt; and &lt;strong&gt;~13.6k&lt;/strong&gt;, a visible step down from the monthly average and the weakest month-end of the Top-5. Worth watching, since Spaceship's &lt;strong&gt;+4.1%&lt;/strong&gt; monthly gain rests on a mid-month plateau it did not hold.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cleaned vs Redacted - Data Quality Snapshot
&lt;/h2&gt;

&lt;p&gt;Coverage rates describe how much of each WHOIS field arrives usable rather than redacted. They set the ceiling on what any analysis of this dataset can support, so they are reported here alongside the volume figures rather than as an appendix. Full field-level definitions are available in the &lt;a href="https://whoisfreaks.com/products/whois-database" rel="noopener noreferrer"&gt;WHOIS database&lt;/a&gt; documentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Registrar Details
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Total records:&lt;/strong&gt; 11,640,735&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleaned: 11,296,406 (97.0%)&lt;/strong&gt;. Registrar data held within &lt;strong&gt;0.1 points&lt;/strong&gt; of July's 97.1%, extending a multi-month run above 97% and absorbing a &lt;strong&gt;+19.5%&lt;/strong&gt; volume increase without any degradation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redacted: 344,329 (3.0%)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Takeaway:&lt;/strong&gt; Registrar metadata remains functionally near-complete and continues to provide the strongest analytical backbone in the dataset. That is what makes it possible to attribute the .de Deluge to a specific registrar cohort rather than merely observing an unexplained TLD spike.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Holding a 97% clean rate through a nineteen-percent volume expansion, including a single 742,000-record day, indicates that the ingestion pipeline scaled cleanly even where daily counts did not. The 0.1-point change is well inside ordinary month-to-month variation in registrant mix. Registrar-based segmentation, event attribution and share analysis therefore remain reliable at the highest confidence tier. In a month where the headline number is inflated by one batch event, cross-dimensional pivots anchored on registrar data are the only sound way to size the underlying market. With redaction at 3.0%, the gap between raw and cleaned counts stays narrow enough that month-over-month trend comparisons need no adjustment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4dfhaxa33rji7ukbjyv4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4dfhaxa33rji7ukbjyv4.webp" alt="cleaned vs redacted registrar details" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Address Details
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Total records:&lt;/strong&gt; 11,640,735&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleaned: 5,886,108 (50.6%)&lt;/strong&gt;. A &lt;strong&gt;2.8-point&lt;/strong&gt; decline from July's 53.4%, a second consecutive give-back that leaves usable address coverage at its lowest level in several months.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redacted: 5,754,627 (49.4%)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Takeaway:&lt;/strong&gt; Address data now sits barely above an even split, so geo-segmentation based on registrant addresses remains viable but has lost all of the margin it briefly held in June.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The decline is composition-driven and directly traceable. The .de batch cohort added 571,109 records while Germany's country attribution grew by only 160,782, meaning the large majority of those registrations carried no usable address. A single event of that size is enough to move the aggregate coverage rate by more than two points. Cross-reference address-based geo-segmentation with registrar and TLD signals for the 49.4% of records that remain redacted, and be especially careful this month not to read the country table as a complete picture, since roughly 72% of the .de increase is invisible in it. The practical consequence is that coverage metrics are sensitive to registrant mix, which argues for reading them alongside the volume story rather than as an independent quality signal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvr6r056x0qmlaz3sb2do.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvr6r056x0qmlaz3sb2do.webp" alt="cleaned vs redacted address details" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Contact Details
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Total records:&lt;/strong&gt; 11,640,735 evaluated for contact fields&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redacted: 9,922,947 (85.2%)&lt;/strong&gt;. Contact redaction fell &lt;strong&gt;7.2 points&lt;/strong&gt; from July's 92.4%, by far the largest single-month improvement this dimension has recorded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleaned: 1,717,788 (14.8%)&lt;/strong&gt; carry usable contact details, up from 7.6% in July, an absolute increase of &lt;strong&gt;+980,160&lt;/strong&gt; records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Takeaway:&lt;/strong&gt; Direct contact information is still the most privacy-protected segment of the dataset, but for the first time in the series roughly one record in seven carries usable contact data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a composition effect, not a policy change. The "Newly_Cleaned" daily line spikes to ~213k on August 4 and holds ~106k to 120k across August 13 to 17, precisely the .de Deluge windows, against a ~19k to 48k baseline on every other day. The batch cohort arrived with contact fields intact. The underlying redaction rate for ordinary registrations is essentially unchanged. Do not revise owner-attribution workflows on the strength of one month. Contact-based intelligence should continue to rely on registrar patterns, infrastructure metadata and TLD behavioral signals, with the August contact records treated as a bounded, identifiable cohort rather than as evidence of a new baseline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwttdxg93oiygt2wj178r.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwttdxg93oiygt2wj178r.webp" alt="cleaned vs redacted contact details" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Newly and Newly Cleaned - Daily Trend
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A higher, more volatile aggregate:&lt;/strong&gt; "Newly" registered domains fluctuated between approximately &lt;strong&gt;~126k and ~742k/day&lt;/strong&gt; against a monthly average of &lt;strong&gt;~375,500&lt;/strong&gt;, a wider band than July's, while "Newly_Cleaned" ran near the chart floor at &lt;strong&gt;~19k to 48k&lt;/strong&gt; on most days. The weekly Sunday and Monday rhythm remains visible, with troughs at &lt;strong&gt;~237k and ~231k (August 2 and 3)&lt;/strong&gt;, &lt;strong&gt;~312k (August 10)&lt;/strong&gt;, &lt;strong&gt;~355k (August 16)&lt;/strong&gt;, &lt;strong&gt;~320k and ~309k (August 23 and 24)&lt;/strong&gt; and &lt;strong&gt;~273k and ~257k (August 30 and 31)&lt;/strong&gt;, but three single-day events dominate the chart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The August 8 Blackout:&lt;/strong&gt; "Newly" fell to a month-low &lt;strong&gt;~126k on August 8&lt;/strong&gt;, roughly &lt;strong&gt;one-third&lt;/strong&gt; of the daily average and less than half the weakest ordinary weekend reading. Month lows landed on the same date for &lt;strong&gt;.com (~17.5k)&lt;/strong&gt;, &lt;strong&gt;.top (~3.6k)&lt;/strong&gt;, the &lt;strong&gt;United States (~16.9k)&lt;/strong&gt;, &lt;strong&gt;Iceland (~4.4k)&lt;/strong&gt;, &lt;strong&gt;China (~1.8k)&lt;/strong&gt;, &lt;strong&gt;Germany (~1.8k)&lt;/strong&gt;, &lt;strong&gt;GoDaddy (~7.0k)&lt;/strong&gt;, &lt;strong&gt;Namecheap (~5.8k)&lt;/strong&gt;, &lt;strong&gt;Dynadot (~8.8k)&lt;/strong&gt; and &lt;strong&gt;Spaceship (~4.8k)&lt;/strong&gt;: ten of the fifteen category charts simultaneously. &lt;strong&gt;.de&lt;/strong&gt;, &lt;strong&gt;.cn&lt;/strong&gt;, &lt;strong&gt;.xyz&lt;/strong&gt;, &lt;strong&gt;Japan&lt;/strong&gt; and &lt;strong&gt;GMO Internet&lt;/strong&gt; show no shortfall at all, so this was a broad but not universal ingestion failure. The &lt;strong&gt;August 9&lt;/strong&gt; rebound to &lt;strong&gt;~551k&lt;/strong&gt; carried .com, the United States, Iceland, Japan, Namecheap, Dynadot, Spaceship, GMO and .xyz to their monthly highs, consistent with next-day reprocessing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The August 4 peak:&lt;/strong&gt; The month's highest reading came at &lt;strong&gt;~742k on August 4&lt;/strong&gt;, nearly &lt;strong&gt;double&lt;/strong&gt; the daily average and the largest single-day count the series has recorded. &lt;strong&gt;August 18&lt;/strong&gt; produced the second-highest reading at &lt;strong&gt;~582k&lt;/strong&gt;, driven by the &lt;strong&gt;~83.7k .cn&lt;/strong&gt; spike and the opening of the second Twin-Window block, with August 9's &lt;strong&gt;~551k&lt;/strong&gt; rebound third.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Newly_Cleaned" traces the .de Deluge exactly:&lt;/strong&gt; The cleaned series shows one spike to &lt;strong&gt;~213k on August 4&lt;/strong&gt;, a sustained &lt;strong&gt;~106k to 120k&lt;/strong&gt; block across &lt;strong&gt;August 13 to 17&lt;/strong&gt;, and a final &lt;strong&gt;~173k&lt;/strong&gt; reading on &lt;strong&gt;August 18&lt;/strong&gt;, against a baseline near &lt;strong&gt;~30k&lt;/strong&gt;. Total "Newly_Cleaned" volume for the month tracks the &lt;strong&gt;1,717,788&lt;/strong&gt; cleaned-contact figure closely, and the alignment of its August 13 to 17 block with the .de plateau is the single most useful diagnostic in the August chart set. It identifies the batch cohort by its data-completeness signature, not merely by its volume.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5j76u6t8o9y0h2ybh76z.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5j76u6t8o9y0h2ybh76z.webp" alt="newly vs newly cleaned" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  gTLDs vs ccTLDs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;gTLDs dominate: 8,452,256 (72.6%)&lt;/strong&gt; are generic Top-Level Domain registrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ccTLDs are sizable: 3,188,479 (27.4%)&lt;/strong&gt; are country-code registrations, representing roughly &lt;strong&gt;1 in 3.6&lt;/strong&gt; new domains.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 72.6% and 27.4% split moved &lt;strong&gt;8.4 points&lt;/strong&gt; toward ccTLDs from July's 81.0% and 19.0%, the largest single-month shift the series has recorded. The mechanics are concentrated in a few extensions. ccTLD volume grew &lt;strong&gt;+1,338,913 (+72.4%)&lt;/strong&gt;, led by .de's climb to 690,812 and &lt;strong&gt;.cn +136,423&lt;/strong&gt;, with &lt;strong&gt;.ru +22,497&lt;/strong&gt; and the Top-20 re-entries of &lt;strong&gt;.co (139,743)&lt;/strong&gt; and &lt;strong&gt;.in (106,027)&lt;/strong&gt; pointing to further country-code strength, while gTLD volume grew a more modest &lt;strong&gt;+559,464 (+7.1%)&lt;/strong&gt; on .com's +400,821.&lt;/p&gt;

&lt;p&gt;Maintain separate baselines for gTLDs and ccTLDs, and this month maintain an event-adjusted third baseline. Strip the August 13 to 17 .de plateau out and the split lands nearer &lt;strong&gt;76% and 24%&lt;/strong&gt;: still a genuine move toward country-code space, but a fraction of the headline. August is the cleanest case study the series has produced in why a single registry event can rewrite a top-level market statistic. Analysts modelling September should treat the gTLD trend as the more durable of the two, and should not carry the 27.4% ccTLD share forward as a baseline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvgdn03qpoc29h59zewmj.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvgdn03qpoc29h59zewmj.webp" alt="gtlds vs cctlds newly registered" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;August 2026 recorded 11,640,735 newly registered domains, up 19.5% month over month and the first reading above eleven million.&lt;/li&gt;
&lt;li&gt;Roughly 30% of that growth came from .de, and two-thirds of .de volume landed inside a five-day batch window. Underlying growth was closer to 11%.&lt;/li&gt;
&lt;li&gt;A market-wide ingestion gap on August 8 produced month lows in ten of the fifteen tracked series, followed by an August 9 rebound that produced monthly highs in nine of them.&lt;/li&gt;
&lt;li&gt;.xyz, GMO Internet and Japan share an identical two-window profile and all three exited the month at a small fraction of their monthly average.&lt;/li&gt;
&lt;li&gt;Namecheap closed to within 50,204 registrations of GoDaddy, the narrowest top-two gap the series has recorded.&lt;/li&gt;
&lt;li&gt;Registrar coverage held at 97.0%, address coverage slipped to 50.6%, and contact coverage nearly doubled to 14.8% as a one-cohort composition effect.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to Watch in September 2026
&lt;/h2&gt;

&lt;p&gt;Four questions decide whether August's numbers describe a market or an event.&lt;/p&gt;

&lt;p&gt;The first is .de. On its post-plateau floor of ~6k to 9k per day, .de lands near 200,000 for September and returns to the low teens by rank. A second plateau would confirm a recurring registry or promotional channel rather than a one-off.&lt;/p&gt;

&lt;p&gt;The second is the Twin-Window trio. .xyz, GMO Internet and Japan all exited August at 1% to 5% of their peak-window run rates. If no third window opens, all three post large September declines that say nothing about underlying demand.&lt;/p&gt;

&lt;p&gt;The third is the top of the registrar table. A 50,204 gap is roughly one and a half average days for either firm. Namecheap has grown faster than GoDaddy for three consecutive months, and a swap at #1 in September is a live possibility.&lt;/p&gt;

&lt;p&gt;The fourth is Iceland. The fade is orderly and shows no window structure, which makes it the most likely of this month's moves to continue. A third consecutive decline would take the geography toward the 400,000 range and reshape the country pie more durably than Germany's arrival did.&lt;/p&gt;

&lt;p&gt;Teams tracking these cohorts day by day can pull the same records this report is built on from the &lt;a href="https://whoisfreaks.com/products/newly-registered-domains" rel="noopener noreferrer"&gt;Newly Registered Domains feed&lt;/a&gt;, or query them programmatically through the &lt;a href="https://whoisfreaks.com/documentation/newly-registered-domains" rel="noopener noreferrer"&gt;NRD API documentation&lt;/a&gt;. For month-over-month context, the &lt;a href="https://whoisfreaks.com/resources/domain-analytic/monthly-insights-radar-newly-registered-domain-analytics-for-july-2026" rel="noopener noreferrer"&gt;July 2026 edition&lt;/a&gt; covers the Japanese unwind and the .shop ascent that this month partly reverses, and every prior report is collected in the &lt;a href="https://whoisfreaks.com/resources/domain-analytic" rel="noopener noreferrer"&gt;Monthly Insights Radar archive&lt;/a&gt;. Analysts building a first NRD workflow will find the practical options compared in our guide to &lt;a href="https://whoisfreaks.com/resources/blog/how-to-find-newly-registered-domains-free-paid-lists-daily-feeds-api-access" rel="noopener noreferrer"&gt;how to find newly registered domains&lt;/a&gt;. For scoring batch cohorts like August's German registrar channel against risk signals at ingestion time, the &lt;a href="https://whoisfreaks.com/products/domain-reputation-api" rel="noopener noreferrer"&gt;Domain Reputation API&lt;/a&gt; applies the same attribution logic used throughout this report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How many domains were registered in August 2026?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;August 2026 recorded 11,640,735 newly registered domains, a 19.5% increase over July's 9,742,358 and the first month above eleven million in this series. Roughly 30% of the increase came from a single extension, .de, which arrived through a concentrated five-day batch event rather than through steady daily demand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which TLD grew fastest in August 2026?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;.de grew fastest, rising 477.1% from 119,703 to 690,812 registrations and moving from rank #16 to rank #3. .cn placed second at 64.4% growth, and .vip third at 26.9%. .de's increase is the largest single-month move any extension has posted in this series.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why did .de registrations jump 477% in August 2026?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Roughly 456,700 .de registrations, about 66% of the monthly total, landed across five consecutive days from August 13 to 17 at a near-flat rate of about 90,000 per day. Four German registrars entered the Top-20 in the same month. The pattern indicates a batch or promotional registration event processed through a small set of channels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What caused the August 8 drop in newly registered domains?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Aggregate volume fell to about 126,000 on August 8, roughly one-third of the daily average, and ten of the fifteen tracked series recorded their month low on that date. Volume rebounded to about 551,000 on August 9 with nine series posting monthly highs, a pattern consistent with a one-day ingestion gap followed by next-day reprocessing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which registrar registered the most new domains in August 2026?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GoDaddy led with 1,192,684 registrations, ahead of Namecheap at 1,142,480. The 50,204 gap between them is the narrowest the series has recorded, down from 79,432 in July. GMO Internet Group placed third with 933,005 after growing 30.4%, reclaiming the position it lost to Dynadot in July.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why did usable WHOIS contact data double in August 2026?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Usable contact details rose to 14.8% of records from 7.6% in July, an increase of 980,160 records. The daily cleaned series spikes on exactly the .de batch dates, which identifies the cause as one cohort arriving with contact fields intact. Redaction rates for ordinary registrations did not change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How can I access the underlying newly registered domain data?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The figures in this report come from the WhoisFreaks Newly Registered Domains dataset, available as a daily feed, a bulk database download, or a REST API. Each record carries the domain, registration date, registrar, WHOIS fields where they are not redacted, and DNS records. Coverage and field definitions are documented alongside the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data and Methodology
&lt;/h2&gt;

&lt;p&gt;These figures come from the WhoisFreaks global WHOIS and registry ingestion pipeline, the same dataset behind the Newly Registered Domains feed. A newly registered domain is counted on the first calendar day (UTC) its registration record appears in a monitored registry or zone file. "Cleaned" means a field arrived populated and parseable; "Redacted" means it arrived withheld, masked, or replaced with a privacy-service placeholder. Top-20 cohort totals cover only the twenty highest-volume entries in each dimension and are smaller than the all-domain total, so shares quoted as a percentage of a Top-20 cohort are not comparable to shares of the full month.&lt;/p&gt;

&lt;p&gt;Two limitations apply to August 2026 specifically. First, aggregate volume on August 8 fell to roughly one-third of the daily average across ten of the fifteen tracked series, with a corresponding rebound on August 9; readings on both dates reflect ingestion timing rather than registration demand, and daily concentration figures in this report exclude neither date but flag both. Second, the daily charts for GoDaddy, Namecheap and Dynadot sum to between 6% and 11% below their full-period totals, while every other series reconciles within 1%. All volume and month-over-month figures in this report use the full-period totals. Those three daily charts are described only in terms of bands, peaks and dates, and no concentration percentage is derived from them.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://whoisfreaks.com/resources/domain-analytic/monthly-insights-radar-newly-registered-domain-analytics-for-august-2026" rel="noopener noreferrer"&gt;whoisfreaks.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>data</category>
      <category>cybersecurity</category>
      <category>api</category>
      <category>analytics</category>
    </item>
    <item>
      <title>SSL Certificate Monitoring in Python: Get Alerted Before Certs Expire</title>
      <dc:creator>Sameer Sheikh</dc:creator>
      <pubDate>Fri, 21 Aug 2026 11:24:04 +0000</pubDate>
      <link>https://dev.to/whoisfreaks/ssl-certificate-monitoring-in-python-get-alerted-before-certs-expire-4dn4</link>
      <guid>https://dev.to/whoisfreaks/ssl-certificate-monitoring-in-python-get-alerted-before-certs-expire-4dn4</guid>
      <description>&lt;p&gt;Certbot had been renewing that certificate for two years without anyone touching it. Then someone moved the webroot during a Nginx cleanup, the HTTP-01 challenge started failing, and the renewal cron kept exiting quietly with a non-zero status nobody was reading. We found out when the cert expired.&lt;/p&gt;

&lt;p&gt;That's the shape of almost every certificate outage I've seen. Not "we forgot to buy a certificate." More like "the automation was running and we assumed running meant working."&lt;/p&gt;

&lt;p&gt;So I wrote a script that checks the certificate that's actually being served, from outside my own network, and shouts before anything breaks. It's about 400 lines and it caught something on the first real run that I wasn't expecting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part most expiry checks get wrong
&lt;/h2&gt;

&lt;p&gt;Search for how to check certificate expiry in Python and you'll get this, more or less:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_default_context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_connection&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;github.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wrap_socket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;server_hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;github.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getpeercert&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;not_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;notAfter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%b %d %H:%M:%S %Y %Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;not_after&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="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works. No dependencies, five seconds to write, and for a handful of hosts you can reach directly it's the right answer. I keep a copy in the repo as &lt;code&gt;check_expiry_stdlib.py&lt;/code&gt; because sometimes that's all you need.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;getpeercert()&lt;/code&gt; gives you the leaf certificate and nothing else. Your leaf can have 60 comfortable days left while the intermediate that signs it expires next week, and when that intermediate goes, every client gets a handshake failure. The leaf's date told you nothing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7lqb9yojix59avlq6pml.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%2F7lqb9yojix59avlq6pml.png" alt="Certificate chain with three different expiry dates" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This isn't hypothetical. LogDNA published a postmortem in May 2020 about exactly this: a root certificate expiry that broke TLS for their customers while the leaf certificates were all perfectly valid.&lt;/p&gt;

&lt;p&gt;There's a second problem, and it's the one that bit me. &lt;code&gt;getpeercert()&lt;/code&gt; reports on the TLS connection that your script made. If anything sits between your script and the internet doing TLS inspection, a corporate proxy, a security appliance, a container platform with an egress gateway, then you are reading that middlebox's re-signed certificate. Not the real one. Your monitoring reports healthy and the outside world sees an expired cert.&lt;/p&gt;

&lt;p&gt;I hit this while testing. Running the stdlib script inside a sandboxed environment, every host came back valid with 29 to 30 days left, including &lt;code&gt;expired.badssl.com&lt;/code&gt;, which is a host whose entire purpose is to serve an expired certificate. The issuer field said the proxy's name. My checker was confidently monitoring the wrong thing.&lt;/p&gt;

&lt;p&gt;So: query from somewhere else, and get the whole chain.&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;wf-ssl-monitor&lt;/code&gt; reads a list of domains, pulls the full certificate chain for each one through the &lt;a href="https://whoisfreaks.com/documentation/ssl-certificate-api" rel="noopener noreferrer"&gt;WhoisFreaks SSL Certificate API&lt;/a&gt;, finds whichever certificate in the chain expires first, and exits with a Nagios-style status code so cron or CI can act on it. Optionally it posts to Slack.&lt;/p&gt;

&lt;p&gt;Full source: &lt;a href="https://github.com/WhoisFreaks/wf-ssl-monitor" rel="noopener noreferrer"&gt;github.com/WhoisFreaks/wf-ssl-monitor&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/WhoisFreaks/wf-ssl-monitor.git
&lt;span class="nb"&gt;cd &lt;/span&gt;wf-ssl-monitor
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;WHOISFREAKS_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your_key"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Signup gives you 500 credits without a card, which is plenty to get this running. The only dependency is &lt;code&gt;requests&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: One call, whole chain
&lt;/h2&gt;

&lt;p&gt;The endpoint is a single GET. The &lt;code&gt;chain=true&lt;/code&gt; parameter is the important one, since without it you get the leaf and you're back to the problem above.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://api.whoisfreaks.com/v1.0/ssl/live?apiKey=&lt;/span&gt;&lt;span class="nv"&gt;$WHOISFREAKS_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;domainName=github.com&amp;amp;chain=true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get back &lt;code&gt;sslCertificates&lt;/code&gt;, an array ordered from end-user to root. Each entry carries &lt;code&gt;chainOrder&lt;/code&gt;, &lt;code&gt;validityStartDate&lt;/code&gt;, &lt;code&gt;validityEndDate&lt;/code&gt;, &lt;code&gt;subject&lt;/code&gt;, &lt;code&gt;issuer&lt;/code&gt;, &lt;code&gt;signatureAlgorithm&lt;/code&gt;, &lt;code&gt;publicKey&lt;/code&gt;, and the SANs under &lt;code&gt;extensions.subjectAlternativeNames.dnsNames&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One detail worth flagging because it cost me a debugging round. The dates come back like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-09-1 07:47:19 UTC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The day isn't zero-padded. &lt;code&gt;strptime&lt;/code&gt; with &lt;code&gt;%d&lt;/code&gt; handles it, but a naive &lt;code&gt;dateutil&lt;/code&gt; guess or a hand-rolled slice will not, and the failure mode is silent wrong numbers rather than an exception. That's the worst kind. So the parser is explicit and tries several shapes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_wf_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;suffix&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; UTC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; GMT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&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="n"&gt;suffix&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H:%M:%S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%dT%H:%M:%S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H:%M&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%b %d %H:%M:%S %Y&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&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="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unrecognised date format: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note &lt;code&gt;datetime.now(timezone.utc)&lt;/code&gt; rather than &lt;code&gt;datetime.utcnow()&lt;/code&gt; everywhere in this script. &lt;code&gt;utcnow()&lt;/code&gt; is deprecated as of Python 3.12 and returns a naive datetime, which means subtracting it from an aware one raises. If you're copying older tutorials, that's a trap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Alert on the weakest link, not the leaf
&lt;/h2&gt;

&lt;p&gt;This is the whole point of the tool, and it's about eight lines. Parse every certificate in the chain, then sort by expiry and take the earliest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@property&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;weakest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CertInfo&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The cert in the chain that expires first. This is what we alert on.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;scored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;certs&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days_left&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scored&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days_left&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;scored&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Classification runs off &lt;code&gt;min_days&lt;/code&gt; across the chain rather than the leaf's date:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DomainResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;crit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;STATUS_ERROR&lt;/span&gt;
    &lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min_days&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;STATUS_ERROR&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;days&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="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;STATUS_EXPIRED&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;crit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;STATUS_CRIT&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;STATUS_WARN&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;STATUS_OK&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Defaults are 30 days for warning and 7 for critical. Both are flags, and I'd tighten them as certificate lifetimes shrink.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Respect the rate limit before it bites
&lt;/h2&gt;

&lt;p&gt;The free tier allows 10 requests per minute on live endpoints. Firing a thread pool at that gets you a wall of 429s, so requests get paced through a small limiter that holds the lock while it sleeps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Spaces requests out so we stay under the plan&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s requests-per-minute cap.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rpm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min_interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;60.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;rpm&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rpm&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_next_slot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min_interval&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="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_next_slot&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_next_slot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_next_slot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min_interval&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a 429 does come back, the API tells you exactly how long to wait in the &lt;code&gt;x-ratelimit-remaining-time&lt;/code&gt; header. The value is in nanoseconds, which surprised me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;raw_wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-ratelimit-remaining-time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;pause&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1_000_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;pause&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;15.0&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a paid plan, &lt;code&gt;--rpm 60 --workers 8&lt;/code&gt; moves considerably faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Don't trust the documented response shape
&lt;/h2&gt;

&lt;p&gt;I built the parser against the response structure in the docs, which shows a top-level JSON array. First real run against the live API, every domain came back as an error while the HTTP status was 200. The data was fine. My assumption about the wrapper was wrong, and because I'd written &lt;code&gt;if isinstance(payload, dict): return error&lt;/code&gt;, a perfectly good response got thrown away.&lt;/p&gt;

&lt;p&gt;Lesson I keep relearning: match on field names, not on payload structure. The parser now accepts an array, a bare object, a single certificate with no wrapper, or data nested under an envelope key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_cert_entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&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;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;find_cert_entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sslCertificates&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;certs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sslCertificates&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;certs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sslCertificates&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;certs&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="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sslData&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;nested&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nested&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
            &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find_cert_entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nested&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;validityEndDate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domainName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domainName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sslCertificates&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;payload&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when it still can't find certificates, it says what it did receive instead of failing generically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no certificate data found (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="c1"&gt;# -&amp;gt; no certificate data found (object with keys: domainName, queryTime, someNewKey)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It keeps the raw payload and prints it automatically. Telling someone to rerun with a debug flag is a wasted scheduled run, and if that run was at 3 AM you've lost a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real results
&lt;/h2&gt;

&lt;p&gt;Running against the production list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python ssl_monitor.py domains.txt --warn 30 --crit 7

DOMAIN             DAYS   EXPIRES     ISSUER                                  STATUS
------------------------------------------------------------------------------------
whoisfreaks.com      70   2026-10-30  WE1                                     OK
github.com           41   2026-09-30  Sectigo Public Server Authentication C  OK
cloudflare.com       47   2026-10-06  WE1                                     OK
python.org          178   2027-02-14  GlobalSign Atlas R3 DV TLS CA 2025 Q4   OK
letsencrypt.org      45   2026-10-04  YE2                                     OK

5 domains in 26.4s - 5 ok, 0 warning, 0 critical, 0 expired, 0 errors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the deliberately broken list, which is how I check the checker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python ssl_monitor.py domains.test.txt

DOMAIN                  DAYS   EXPIRES     ISSUER                                  STATUS
------------------------------------------------------------------------------------------
expired.badssl.com      -4148  2015-04-12  COMODO RSA Domain Validation Secure Se  EXPIRED
self-signed.badssl.com    728  2028-08-17  *.badssl.com                            OK (self-signed)

2 domains in 12.1s - 1 ok, 0 warning, 0 critical, 1 expired, 0 errors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Negative 4,148 days. That certificate expired in April 2015 and the API reported it accurately, which is exactly what I wanted to confirm. Remember the sandbox where every host including this one came back with 30 healthy days? That's the difference between checking the certificate and checking whatever your network hands you.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;A valid expiry date is not a valid certificate. &lt;code&gt;self-signed.badssl.com&lt;/code&gt; came back with 728 days left and my first version called that OK. It's self-signed. Every browser rejects it. I added a check comparing the leaf's subject against its issuer, and now it prints &lt;code&gt;OK (self-signed)&lt;/code&gt;, because reporting green on a cert nothing trusts is worse than not checking at all.&lt;/li&gt;
&lt;li&gt;The intermediate is the one nobody watches. Every dashboard I've used reports the leaf date.&lt;/li&gt;
&lt;li&gt;Errors deserve their own status, separate from expiry. A lookup that fails is not a healthy certificate, and folding those together is how you end up with a green dashboard and a broken site. Hence exit code 3.&lt;/li&gt;
&lt;li&gt;Two of the five production hosts are on Google Trust Services (&lt;code&gt;WE1&lt;/code&gt;) and one on &lt;code&gt;YE2&lt;/code&gt;. Short-lived certs from large issuers cluster in the 40 to 70 day range, which is a useful baseline. &lt;code&gt;python.org&lt;/code&gt; at 178 days stood out immediately as the outlier running a longer GlobalSign cert.&lt;/li&gt;
&lt;li&gt;Five domains took 26 seconds. That's the rate limiter doing its job at 10 requests a minute, not the API being slow.&lt;/li&gt;
&lt;li&gt;Keep your broken test hosts in a separate file from the list your scheduled job reads. I didn't, and my first green-field cron run went red at 8 AM because a host that expired in 2015 was still expired. A monitor that cries wolf every morning gets muted by Thursday.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this is getting more urgent
&lt;/h2&gt;

&lt;p&gt;Certificate lifetimes are dropping fast. CA/Browser Forum ballot SC-081v3 passed in April 2025 and steps the maximum down on a fixed schedule.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ixqjmims8as5r7m54ng.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%2F4ixqjmims8as5r7m54ng.png" alt="Maximum certificate lifetime dropping from 398 to 47 days" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's Encrypt is moving in parallel, from 90 days now to 64 in February 2027 and 45 in February 2028.&lt;/p&gt;

&lt;p&gt;Here's the thing about going from roughly one renewal a year to nearly eight: the per-renewal failure rate doesn't have to change for your annual odds of an outage to get much worse. Manual renewal stops being viable, automation becomes mandatory, and automation that fails silently becomes the main risk. Which is the argument for monitoring the served certificate rather than trusting that the renewal job did its job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scheduling it
&lt;/h2&gt;

&lt;p&gt;The repo has a GitHub Actions workflow that runs daily at 08:00 UTC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;8&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ssl-check&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;workflow_dispatch&lt;/code&gt; is there so you can trigger it by hand while testing instead of editing the cron and waiting. &lt;code&gt;concurrency&lt;/code&gt; stops a slow run overlapping the next one and double-alerting. Store &lt;code&gt;WHOISFREAKS_API_KEY&lt;/code&gt; and &lt;code&gt;SLACK_WEBHOOK_URL&lt;/code&gt; under Settings, Secrets and variables, Actions.&lt;/p&gt;

&lt;p&gt;For a plain cron box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 8 * * * cd /opt/wf-ssl-monitor &amp;amp;&amp;amp; /usr/bin/python3 ssl_monitor.py domains.txt --slack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Going further
&lt;/h2&gt;

&lt;p&gt;A few directions I'd take this next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validate trust, not just dates. The self-signed catch above is a string comparison. Real validation means checking the chain actually links up and terminates at a trusted root, which is a bigger job and probably the most valuable thing to add next.&lt;/li&gt;
&lt;li&gt;Pull the domain list from your actual infrastructure instead of a text file. Terraform state, a DNS zone export, or your ingress definitions all beat a hand-maintained list that drifts.&lt;/li&gt;
&lt;li&gt;Alert on SAN changes, not just expiry. A certificate that quietly stops covering a hostname is its own outage.&lt;/li&gt;
&lt;li&gt;Write results to a time series database so you can see renewals happening rather than just current state. A cert stuck at the same expiry date across three weeks is a renewal that stopped working.&lt;/li&gt;
&lt;li&gt;If you'd rather not run and maintain this yourself, &lt;a href="https://whoisfreaks.com/products/ssl-certificate-api" rel="noopener noreferrer"&gt;WhoisFreaks SSL Certificate API&lt;/a&gt; covers the lookup side as a managed service.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Full source
&lt;/h2&gt;

&lt;p&gt;Everything is in &lt;a href="https://github.com/WhoisFreaks/wf-ssl-monitor" rel="noopener noreferrer"&gt;github.com/WhoisFreaks/wf-ssl-monitor&lt;/a&gt;, MIT licensed. That includes &lt;code&gt;ssl_monitor.py&lt;/code&gt;, the stdlib version for comparison, the Actions workflow, a production &lt;code&gt;domains.txt&lt;/code&gt;, and a &lt;code&gt;domains.test.txt&lt;/code&gt; of deliberately broken hosts for verifying the checker.&lt;/p&gt;

&lt;p&gt;If you run it and hit a response shape the parser doesn't handle, open an issue with the payload it printed. That's the part most likely to need adjusting.&lt;/p&gt;




</description>
      <category>python</category>
      <category>devops</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Block Bots Before They Hit Your API; IP Reputation in Node.js</title>
      <dc:creator>Sameer Sheikh</dc:creator>
      <pubDate>Fri, 24 Jul 2026 15:51:58 +0000</pubDate>
      <link>https://dev.to/whoisfreaks/block-bots-before-they-hit-your-api-ip-reputation-in-nodejs-4ga8</link>
      <guid>https://dev.to/whoisfreaks/block-bots-before-they-hit-your-api-ip-reputation-in-nodejs-4ga8</guid>
      <description>&lt;p&gt;Most API abuse gets caught after it lands. A rate limiter trips, an alert fires, someone checks the logs the next morning and finds a Tor exit node hammered &lt;code&gt;/login&lt;/code&gt; four thousand times overnight. Reactive. I wanted to catch the traffic at the door instead, before it touches a single route handler.&lt;/p&gt;

&lt;p&gt;The idea is simple. Every request arrives with an IP. That IP already has a reputation — it's a residential broadband line, or it's a datacenter, or it's a known VPN, or it's an address that's been throwing credential-stuffing attempts at half the internet for a week. If you know which one you're looking at &lt;em&gt;before&lt;/em&gt; your handler runs, you can decide what to do with it. Block the obvious attackers. Challenge the suspicious ones. Wave the clean ones through without friction.&lt;/p&gt;

&lt;p&gt;That last part matters. Most tutorials I've read on this treat it as a binary: VPN detected, block. But a corporate user on a datacenter IP and a bot running out of a bulletproof host are not the same threat, and slamming the door on both is how you lose real customers. So this middleware grades on a curve.&lt;/p&gt;

&lt;p&gt;Here's the whole path a request takes before it ever reaches your handler:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fspb8578gjav5s1v4myw2.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%2Fspb8578gjav5s1v4myw2.png" alt="Flowchart of a request passing through the middleware: check for private or localhost IP, then cache, then call the WhoisFreaks API, fail open on error, then decide an action from the threat score" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The piece everyone skips
&lt;/h2&gt;

&lt;p&gt;Here's the thing that took me longer than it should have: getting the real client IP.&lt;/p&gt;

&lt;p&gt;Behind a load balancer, a CDN, or basically any modern deploy, &lt;code&gt;req.socket.remoteAddress&lt;/code&gt; is the proxy in front of you, not the visitor. The real address sits in the &lt;code&gt;X-Forwarded-For&lt;/code&gt; header. Express won't read it unless you tell it to trust the proxy, and if you trust it wrong, you've handed attackers a header they can spoof to look like &lt;code&gt;127.0.0.1&lt;/code&gt;. More on that later. For now, know that this one line does a lot of quiet work:&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;trust proxy&lt;/span&gt;&lt;span class="dl"&gt;"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;Node 18+ (we're using the built-in &lt;code&gt;fetch&lt;/code&gt;, no HTTP client dependency), Express, and a free WhoisFreaks key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/WhoisFreaks/ip-reputation-middleware
&lt;span class="nb"&gt;cd &lt;/span&gt;ip-reputation-middleware
npm &lt;span class="nb"&gt;install
cp&lt;/span&gt; .env.example .env   &lt;span class="c"&gt;# paste your API key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is free — 500 credits, no card. Each IP lookup costs one credit. That's enough to build and test the whole thing with credits to spare.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: One lookup
&lt;/h2&gt;

&lt;p&gt;Start with the smallest possible thing that works. Given an IP, ask what its reputation is.&lt;/p&gt;

&lt;p&gt;The endpoint is a plain GET. Auth is a query parameter called &lt;code&gt;apiKey&lt;/code&gt;, and you pass the address in &lt;code&gt;ip&lt;/code&gt;. The full request and response schema is documented in the &lt;a href="https://whoisfreaks.com/documentation/ip-security-api" rel="noopener noreferrer"&gt;WhoisFreaks IP Reputation API reference&lt;/a&gt;. The response comes back as an array with one object per IP, and the part we care about lives under &lt;code&gt;security&lt;/code&gt;:&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;BASE_URL&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://api.whoisfreaks.com/v1.0/security&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;lookupIp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WHOISFREAKS_API_KEY&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&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_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?apiKey=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;ip=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&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="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="nx"&gt;url&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`whoisfreaks_http_&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="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&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;body&lt;/span&gt;&lt;span class="p"&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;security&lt;/code&gt; object is richer than the single boolean most APIs hand you. You get &lt;code&gt;threat_score&lt;/code&gt; (0–100), plus &lt;code&gt;is_vpn&lt;/code&gt;, &lt;code&gt;is_proxy&lt;/code&gt;, &lt;code&gt;is_residential_proxy&lt;/code&gt;, &lt;code&gt;is_tor&lt;/code&gt;, &lt;code&gt;is_bot&lt;/code&gt;, &lt;code&gt;is_known_attacker&lt;/code&gt;, &lt;code&gt;is_spam&lt;/code&gt;, and &lt;code&gt;is_cloud_provider&lt;/code&gt;, along with confidence scores and provider names for the VPN and proxy hits. That granularity is what makes graded responses possible instead of a flat block.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Decide, don't just detect
&lt;/h2&gt;

&lt;p&gt;This is the core of the whole thing, and it's ten lines. The composite &lt;code&gt;threat_score&lt;/code&gt; does the heavy lifting. I mapped it to four actions using the score bands WhoisFreaks documents:&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;security&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;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;security&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;threat_score&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;76&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// known-bad: reject&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;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;step_up&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// high: require extra auth&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;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;challenge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// elevated: CAPTCHA / rate limit&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                       &lt;span class="c1"&gt;// low: let it through&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why four tiers and not two? Because a score of 40 usually means "datacenter or commercial VPN" — plenty of legitimate people browse from those. A score of 90 means the address is actively bad. You want the first group to see a CAPTCHA at worst and the second group to see nothing but a &lt;code&gt;403&lt;/code&gt;. One threshold can't express that.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxbufqay054iyz1lh848c.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%2Fxbufqay054iyz1lh848c.png" alt="Threat score scale from 0 to 100 split into four bands: 0 to 25 allow, 26 to 50 challenge, 51 to 75 step-up, 76 to 100 block, each with its response action" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Wrap it in middleware
&lt;/h2&gt;

&lt;p&gt;Now make it an Express middleware factory so each route or app can plug in its own responses per tier.&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ipReputation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;failOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onBlock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onChallenge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onStepUp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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;next&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;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clientIp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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="nf"&gt;isPrivate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// skip localhost / internal traffic&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;record&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="nx"&gt;record&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;lookupIp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&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="nx"&gt;err&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;failOpen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;      &lt;span class="c1"&gt;// API down? don't take the app down with it&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reputation_unavailable&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;security&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;security&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ipReputation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;security&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;security&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;block&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;onBlock&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;onBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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;next&lt;/span&gt;&lt;span class="p"&gt;)&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="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;forbidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;step_up&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;onStepUp&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;onStepUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;challenge&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;onChallenge&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;onChallenge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&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;Two decisions in there are worth calling out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fail open.&lt;/strong&gt; If the reputation API times out or returns a 500, the request goes through. I went back and forth on this. But think about what fail-&lt;em&gt;closed&lt;/em&gt; means: your threat provider has one bad minute and your login page returns 503 to every real customer on earth. That's a self-inflicted outage in exchange for maybe blocking one risky request. Not worth it. Fail open, log the miss, move on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skip private IPs.&lt;/strong&gt; Health checks, internal service calls, and your own &lt;code&gt;localhost&lt;/code&gt; testing shouldn't burn credits or get scored. The &lt;code&gt;isPrivate&lt;/code&gt; check short-circuits them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Get the client IP right
&lt;/h2&gt;

&lt;p&gt;The helper the whole thing leans 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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;clientIp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^::ffff:/&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;// strip IPv4-mapped IPv6 prefix&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;::ffff:&lt;/code&gt; strip bit me the first time. Express hands you IPv4 addresses wrapped as IPv6 (&lt;code&gt;::ffff:8.8.8.8&lt;/code&gt;), and if you pass that straight to the API you'll get nonsense back. Small detail, real bug.&lt;/p&gt;

&lt;p&gt;On &lt;code&gt;trust proxy&lt;/code&gt;: setting it to &lt;code&gt;true&lt;/code&gt; is fine when every request genuinely passes through a proxy you control. If your app is &lt;em&gt;also&lt;/em&gt; reachable directly, a client can send their own &lt;code&gt;X-Forwarded-For&lt;/code&gt; and impersonate any IP they like, including a clean one. In that case set it to the exact number of proxy hops instead. Know your deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Cache, because credits and latency
&lt;/h2&gt;

&lt;p&gt;Adding a network call to every request is a great way to make every request slower and drain 500 credits by lunch. The reputation data refreshes about once a day, so caching a result for a few hours costs nothing in accuracy:&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;CACHE_TTL_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&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;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// check cache.get(ip) before fetch; cache.set(ip, { record, expires }) after&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An in-memory &lt;code&gt;Map&lt;/code&gt; is fine for one process. Swap in Redis the moment you run more than one. The free tier caps you at 10 requests per minute, so on a real site the cache isn't an optimization — it's the thing that keeps you under the limit at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Test the part that matters
&lt;/h2&gt;

&lt;p&gt;You don't need to hit the live API to test the logic that decides someone's fate. The tiering is pure and deterministic, so test the boundaries directly:&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="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;elevated scores get challenged&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;threat_score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;challenge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;threat_score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;challenge&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="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;malicious scores are blocked&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;threat_score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;76&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;threat_score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;block&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;Off-by-one errors here are the whole ballgame. Get &lt;code&gt;&amp;gt;= 76&lt;/code&gt; wrong and you either block clean users at 75 or let attackers through at 76.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real results
&lt;/h2&gt;

&lt;p&gt;Running the suite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;▶ node --test

ok 1 - low scores are allowed
ok 2 - elevated scores get challenged
ok 3 - high scores require step-up auth
ok 4 - malicious scores are blocked
ok 5 - missing score defaults to allow (fail open)

# tests 5
# pass 5
# fail 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I pointed the batch script at 20 IPs pulled from a day of access logs — a mix of hosting ranges, commercial VPN exit points, and a couple of addresses I already suspected. The script only prints what's worth acting on, so the clean ones drop out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ node enrich-logs.js sample-ips.txt
scoring 20 IPs...

45.83.220.14     score= 50  challenge  -
185.159.157.22   score= 80  block      is_known_attacker
193.176.86.103   score= 50  challenge  -
89.187.178.211   score= 50  challenge  -
138.199.21.90    score= 50  challenge  -
37.120.145.60    score= 50  challenge  -
143.244.47.201   score= 50  challenge  -
185.220.101.34   score= 85  block      is_tor,is_known_attacker
195.181.170.12   score= 50  challenge  -
198.54.117.200   score= 35  challenge  is_known_attacker
5.253.207.88     score= 50  challenge  -
146.70.124.19    score= 50  challenge  -
217.138.222.101  score= 50  challenge  -
84.17.35.176     score= 50  challenge  -
156.146.63.128   score= 50  challenge  -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twenty in, fifteen surfaced. Two hit the block tier outright. The other five scored low enough to pass and never printed.&lt;/p&gt;

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

&lt;p&gt;A few things stood out once I had real scores in front of me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The two blocks were the clear-cut ones. &lt;code&gt;185.220.101.34&lt;/code&gt; came back &lt;code&gt;is_tor&lt;/code&gt; and &lt;code&gt;is_known_attacker&lt;/code&gt; at 85 — that's a public Tor exit node, and it earned the &lt;code&gt;403&lt;/code&gt;. &lt;code&gt;185.159.157.22&lt;/code&gt; hit 80 on &lt;code&gt;is_known_attacker&lt;/code&gt; alone. No argument with either.&lt;/li&gt;
&lt;li&gt;Most of the list clustered at exactly 50 with no individual flags set. These were hosting and commercial-VPN ranges, and the API parked them in the middle: not clean, not confirmed-malicious. That's the tiering earning its keep. A binary "is this a VPN" check would either wave all of them through or block real users behind a VPN. Sitting them in the challenge tier — CAPTCHA, not a door slam — is the right call for that gray zone.&lt;/li&gt;
&lt;li&gt;The score and the flags don't always agree. &lt;code&gt;198.54.117.200&lt;/code&gt; carried &lt;code&gt;is_known_attacker&lt;/code&gt; but only scored 35. If I'd keyed the block decision off the flag alone, I'd have blocked it; off the score alone, I'd have under-reacted. Reading both is why it landed in challenge instead of a wrong call in either direction.&lt;/li&gt;
&lt;li&gt;The flat 50s are worth a second look before you trust them in production. When a lot of distinct IPs land on the same round number with empty flags, that can mean the dataset has coarse coverage for those ranges rather than a precise read on each one. I'd verify a couple against a raw API response before wiring the thresholds to anything expensive.
## Going further&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few directions from here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Push blocks down to the edge. Once &lt;code&gt;enrich-logs.js&lt;/code&gt; finds repeat offenders, drop them in an nginx deny list or a WAF rule so they never reach Node at all.&lt;/li&gt;
&lt;li&gt;Persist the cache in Redis and share it across your whole fleet.&lt;/li&gt;
&lt;li&gt;Feed the &lt;code&gt;asn&lt;/code&gt; and &lt;code&gt;connection_type&lt;/code&gt; fields into your own scoring if you want app-specific rules on top of the score.&lt;/li&gt;
&lt;li&gt;If you'd rather not run and maintain the lookup layer yourself, the &lt;a href="https://whoisfreaks.com/products/ip-security-api" rel="noopener noreferrer"&gt;WhoisFreaks IP Reputation API&lt;/a&gt; provides the scoring and the VPN/proxy/Tor/bot flags as a managed service, so you're just reading a field.
## Full source code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything above, wired together and runnable, is on GitHub: &lt;a href="https://github.com/WhoisFreaks/ip-reputation-middleware" rel="noopener noreferrer"&gt;github.com/WhoisFreaks/ip-reputation-middleware&lt;/a&gt;. Clone it, drop in a key, and &lt;code&gt;npm start&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>node</category>
      <category>security</category>
      <category>javascript</category>
      <category>api</category>
    </item>
    <item>
      <title>Hunting Typosquats in 300K Newly Registered Domains With DuckDB (No Database Server)</title>
      <dc:creator>Sameer Sheikh</dc:creator>
      <pubDate>Fri, 17 Jul 2026 22:10:07 +0000</pubDate>
      <link>https://dev.to/whoisfreaks/hunting-typosquats-in-300k-newly-registered-domains-with-duckdb-no-database-server-1ng0</link>
      <guid>https://dev.to/whoisfreaks/hunting-typosquats-in-300k-newly-registered-domains-with-duckdb-no-database-server-1ng0</guid>
      <description>&lt;p&gt;Last month Palo Alto's Unit 42 published something that stuck with me. They asked two LLMs for the "official" URLs of 913 well-known brands, ran hundreds of thousands of queries, and roughly 37% of the URLs the models returned pointed at domains that did not exist. Normalize the duplicates and you are left with about 250,000 unique &lt;em&gt;phantom&lt;/em&gt; domains. Plausible-looking, brand-adjacent, and sitting there unregistered. Anyone who runs the same prompt can register them first and wait for the traffic to arrive.&lt;/p&gt;

&lt;p&gt;That is a lot of names to keep an eye on. And the window to catch a bad registration is narrow. Reputation feeds often take a day or three to flag a fresh domain, while a phishing kit can be serving pages within hours of the registrar confirming it.&lt;/p&gt;

&lt;p&gt;So here is the question I actually care about. Around 250K to 300K new domains hit the internet every single day. How do you sift that daily haul fast enough to be useful, without standing up Postgres, without a schema, without a pipeline you have to babysit?&lt;/p&gt;

&lt;p&gt;The answer I keep coming back to is boring in the best way. One CSV file and DuckDB. It runs on my laptop, the queries finish before I lift my finger off Enter, and there is no server anywhere.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fft2epit47tmqr9cap8hb.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%2Fft2epit47tmqr9cap8hb.png" alt="Pipeline: a daily NRD feed flows into DuckDB, which runs levenshtein and jaro_winkler scoring and returns a ranked list of lookalike domains" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The data: a daily file, not an API loop
&lt;/h2&gt;

&lt;p&gt;I use the WhoisFreaks Newly Registered Domains feed for this. It is a once-or-twice daily dump of every domain registered across 1,500-plus TLDs, and you can pull it as plain CSV. Each row is a domain with its creation date, registrar, name servers, status flags, and, on the WHOIS tiers, whatever registrant fields survived GDPR redaction.&lt;/p&gt;

&lt;p&gt;You do not need a paid plan to follow along. There is a free daily sample of about 1,000 gTLD domains you can grab with a single request, and it has the same column layout as the full file. It comes down gzipped, so save it with a &lt;code&gt;.csv.gz&lt;/code&gt; name. That is what I built this on. The field definitions and download endpoints are all in the &lt;a href="https://whoisfreaks.com/documentation/newly-registered-domains" rel="noopener noreferrer"&gt;WhoisFreaks NRD documentation&lt;/a&gt; if you want the full column list.&lt;/p&gt;

&lt;p&gt;Grab the sample:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-o&lt;/span&gt; nrd_sample.csv.gz &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://files.whoisfreaks.com/download/domainer/sample/gtld?whois=true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you move to the full daily file, it is the same shape with an API key and an optional date:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"nrd_&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.csv.gz"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://files.whoisfreaks.com/v3.3/download/domainer/gtld?apiKey=YOUR_KEY&amp;amp;whois=true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second file is the real thing. Hundreds of thousands of rows, tens of columns. DuckDB reads the gzip straight off disk, so there is no unzip step to wait on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why DuckDB and not pandas
&lt;/h2&gt;

&lt;p&gt;I reached for pandas first, out of habit. It works, but you feel the weight. &lt;code&gt;read_csv&lt;/code&gt; on a wide multi-hundred-megabyte file pulls the whole thing into memory before you have asked a single question of it.&lt;/p&gt;

&lt;p&gt;DuckDB flips that. It is an in-process SQL engine, so there is no server and no port. You &lt;code&gt;pip install&lt;/code&gt; it and you are done. It reads CSV, gzipped CSV, Parquet, and JSON straight off disk, and it only touches the columns your query actually needs. It is columnar and vectorized under the hood, which is a technical way of saying it chews through a few hundred thousand rows in the time pandas spends clearing its throat.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;duckdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Load and look around
&lt;/h2&gt;

&lt;p&gt;First thing I do with any new file is ask it two questions: how many rows, and what are the columns actually called. Column names in vendor CSVs are never quite what you assume, so check rather than guess.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;

&lt;span class="n"&gt;con&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# how many domains in this file?
&lt;/span&gt;&lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT COUNT(*) AS domains FROM &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nrd_sample.csv.gz&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# what columns do we actually have?
&lt;/span&gt;&lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DESCRIBE SELECT * FROM &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nrd_sample.csv.gz&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no import step, no &lt;code&gt;CREATE TABLE&lt;/code&gt;, no loading. You point a query at the filename and DuckDB figures out the types itself. Match the column names in the queries below to whatever &lt;code&gt;DESCRIBE&lt;/code&gt; prints for your file, since the exact field names vary by feed tier.&lt;/p&gt;

&lt;p&gt;If you have been hoarding a week of files, a glob reads them all as one virtual table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    SELECT create_date, COUNT(*) AS registered
    FROM &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nrd_*.csv.gz&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
    GROUP BY create_date
    ORDER BY create_date
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Aggregate the whole file
&lt;/h2&gt;

&lt;p&gt;Before hunting for anything specific, I like to get a feel for the day. Which registrars were busiest, which TLDs dominated. This is a plain &lt;code&gt;GROUP BY&lt;/code&gt; over the full file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    SELECT
        domain_registrar_name,
        COUNT(*) AS domains
    FROM &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nrd_sample.csv.gz&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
    GROUP BY domain_registrar_name
    ORDER BY domains DESC
    LIMIT 10
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running that against a real daily file (279,653 domains the day I ran it) returns this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────────────────────────────────┬─────────┐
│           domain_registrar_name            │ domains │
├───────────────────────────────────────────┼─────────┤
│ Dynadot Inc                                │  36,334 │
│ NameCheap, Inc                             │  34,195 │
│ GoDaddy.com, LLC                           │  33,239 │
│ Spaceship, Inc                             │  23,236 │
│ HOSTINGER operations, UAB                  │  14,076 │
│ GMO Internet Group, Inc. d/b/a Onamae.com  │  13,404 │
│ Cloudflare, Inc                            │  11,991 │
│ NameSilo, LLC                              │   5,964 │
│ Porkbun LLC                                │   5,819 │
│ Squarespace Domains II LLC                 │   5,589 │
│ ...                                        │     ... │
└───────────────────────────────────────────┴─────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four registrars soak up more than 125,000 of those 280,000 domains in a single day. That concentration is worth remembering later.&lt;/p&gt;

&lt;p&gt;The TLD split is just as quick. Pull the part after the last dot and count it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    SELECT
        regexp_extract(domain_name, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\.([^.]+)$&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, 1) AS tld,
        COUNT(*) AS domains
    FROM &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nrd_sample.csv.gz&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
    GROUP BY tld
    ORDER BY domains DESC
    LIMIT 10
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which comes back like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────┬─────────┐
│  tld   │ domains │
├────────┼─────────┤
│ com    │ 119,585 │
│ shop   │  15,548 │
│ top    │  11,837 │
│ org    │   8,932 │
│ online │   8,399 │
│ net    │   8,161 │
│ xyz    │   7,017 │
│ icu    │   6,470 │
│ cyou   │   6,322 │
│ cfd    │   6,116 │
│ ...    │     ... │
└────────┴─────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.com&lt;/code&gt; dominates, no surprise. But look at what follows it. &lt;code&gt;.shop&lt;/code&gt;, &lt;code&gt;.top&lt;/code&gt;, &lt;code&gt;.icu&lt;/code&gt;, &lt;code&gt;.cyou&lt;/code&gt;, &lt;code&gt;.cfd&lt;/code&gt;. These cheap new gTLDs punch far above their size in abuse reports, and here they are near the top of a single day's registrations. That is a filter worth keeping.&lt;/p&gt;

&lt;p&gt;None of this is the point yet. It is orientation. The interesting part is next.&lt;/p&gt;

&lt;h2&gt;
  
  
  The typosquat hunt
&lt;/h2&gt;

&lt;p&gt;Here is what actually makes this dataset worth reading. Somewhere in today's registrations are names built to impersonate a brand. &lt;code&gt;paypa1&lt;/code&gt;, &lt;code&gt;paypal-secure&lt;/code&gt;, &lt;code&gt;paypaI&lt;/code&gt; with a capital i standing in for the L. Picking those out by hand is hopeless at this volume. But it is a text-distance problem, and DuckDB ships the functions for it built in.&lt;/p&gt;

&lt;p&gt;Two of them do most of the work. &lt;code&gt;levenshtein&lt;/code&gt; counts how many single-character edits turn one string into another, so a value of 1 or 2 means "almost identical." &lt;code&gt;jaro_winkler_similarity&lt;/code&gt; returns a score from 0 to 1 and rewards strings that share a prefix, which is exactly how most typosquats are built.&lt;/p&gt;

&lt;p&gt;But one pass is not enough. Edit distance catches misspellings and character swaps. It completely misses &lt;code&gt;paypal-login&lt;/code&gt;, because that string is many edits away from the bare brand. So I run a second pass that looks for the brand sitting next to a common phishing keyword. The two passes catch different things.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsub99dwt0nwmkmvnt0qn.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%2Fsub99dwt0nwmkmvnt0qn.png" alt="Two-pass detection: an edit-distance pass catches paypall.org, paypai.com and paypa1.co, while a brand-plus-keyword pass catches paypal-login.net and secure-paypal.com" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pass one, the edit-distance lookalikes. I pull out the registrable label (the part before the first dot), compare it to the brand, and keep the near-misses.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;brand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paypal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    SELECT
        domain_name,
        levenshtein(split_part(domain_name, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, 1), &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)            AS edits,
        jaro_winkler_similarity(split_part(domain_name, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, 1), &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;) AS similarity
    FROM &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nrd_sample.csv.gz&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
    WHERE levenshtein(split_part(domain_name, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, 1), &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;) BETWEEN 1 AND 3
    ORDER BY similarity DESC
    LIMIT 25
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;BETWEEN 1 AND 3&lt;/code&gt; is deliberate. Zero edits is just the real brand, and anything past three edits is usually a different word that happens to share letters. That band is where the lookalikes live.&lt;/p&gt;

&lt;p&gt;Here is the top of what came back for &lt;code&gt;paypal&lt;/code&gt; on a real day:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────┬───────┬────────────┐
│  domain_name  │ edits │ similarity │
├───────────────┼───────┼────────────┤
│ paypill.icu   │     2 │      0.910 │
│ paypayhq.com  │     3 │      0.892 │
│ pagpals.com   │     2 │      0.879 │
│ payzap.shop   │     2 │      0.876 │
│ payzap.xyz    │     2 │      0.876 │
│ payspark.biz  │     3 │      0.874 │
│ papar.online  │     2 │      0.858 │
│ payflax.com   │     3 │      0.848 │
│ paytag.app    │     2 │      0.844 │
│ pay-pin.com   │     3 │      0.822 │
│ ...           │   ... │      ...    │
└───────────────┴───────┴────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;paypill.icu&lt;/code&gt; is a believable PayPal squat, and it sits on &lt;code&gt;.icu&lt;/code&gt;, one of the cheap TLDs from the earlier list. But be honest about the rest. Further down the ranking, edit distance starts dragging in strings like &lt;code&gt;playjl.lol&lt;/code&gt; that are three edits away and share some letters but have nothing to do with PayPal. On a six-letter brand, a three-edit window is loose. More on that in a second.&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;split_part&lt;/code&gt; trick is a simplification. It treats everything before the first dot as the label, which is fine for &lt;code&gt;paypill.icu&lt;/code&gt; but not for a &lt;code&gt;login.paypal.evil.com&lt;/code&gt; style host. For a daily scan of freshly registered second-level domains it holds up. For deeper subdomain analysis you would want to parse against the public suffix list, which is a post of its own.&lt;/p&gt;

&lt;p&gt;The full script wraps both passes, auto-detects the file's columns, and takes a &lt;code&gt;--brand&lt;/code&gt; argument. Link at the bottom.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I noticed in one day's file
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Using domain column: domain_name   registrar column: domain_registrar_name

Top 25 registrars
----------------------------------------
    36,334  Dynadot Inc
    34,195  NameCheap, Inc
    33,239  GoDaddy.com, LLC
    23,236  Spaceship, Inc
    14,076  HOSTINGER operations, UAB
    13,404  GMO Internet Group, Inc. d/b/a Onamae.com
    11,991  Cloudflare, Inc
     5,964  NameSilo, LLC
     5,819  Porkbun LLC
     5,589  Squarespace Domains II LLC
     5,405  Name.com, Inc
     5,174  Tucows Domains Inc
     4,991  Zhengzhou Century Connect Electronic Technology Development Co., Ltd
     4,562  PDR Ltd. d/b/a PublicDomainRegistry.com
     4,203  Squarespace Domains LLC
     3,817  Dynadot LLC
     3,811  IONOS SE
     3,233  NameMart Pte. Ltd
     2,831  Wix.com Ltd
     2,602  Gransy, s.r.o
     1,895  Dominet (HK) Limited
     1,821  Network Solutions, LLC
     1,800  Realtime Register B.V
     1,760  Namecheap, Inc
     1,609  Gname.com Pte. Ltd

Top 25 TLDs
----------------------------------------
   119,585  .com
    15,548  .shop
    11,837  .top
     8,932  .org
     8,399  .online
     8,161  .net
     7,017  .xyz
     6,470  .icu
     6,322  .cyou
     6,116  .cfd
     5,942  .garden
     5,784  .app
     5,746  .site
     4,881  .info
     4,402  .store
     3,537  .vip
     2,843  .click
     2,734  .pro
     2,713  .bet
     2,386  .biz
     2,299  .sbs
     1,737  .club
     1,618  .space
     1,618  .buzz
     1,288  .live

Edit-distance lookalikes for 'paypal' (1-3 edits)
------------------------------------------------------------
  0.910  edits=2  paypill.icu
  0.892  edits=3  paypayhq.com
  0.879  edits=2  pagpals.com
  0.876  edits=2  payzap.shop
  0.876  edits=2  payzap.xyz
  0.876  edits=3  payle.org
  0.874  edits=3  payspark.biz
  0.858  edits=2  papar.online
  0.858  edits=2  papa9.wiki
  0.848  edits=3  payflax.com
  0.848  edits=3  paynela.homes
  0.848  edits=3  payport.systems
  0.844  edits=2  paytag.app
  0.825  edits=3  payz.club
  0.822  edits=3  partpl.com
  0.822  edits=3  papaii.com
  0.822  edits=2  pahwal.com
  0.822  edits=3  payeazy.store
  0.822  edits=3  pacpza.com
  0.822  edits=3  pay-pin.com
  0.800  edits=3  pata.today
  0.800  edits=3  playjl.lol
  0.800  edits=3  playjl.quest
  0.800  edits=3  playjl.cam
  0.800  edits=3  playjl.lat

  scanned in 3.237s

Brand+keyword combinations for 'paypal'
------------------------------------------------------------
  (none found)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A handful of things jumped out once the numbers were real.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Registration is wildly concentrated. Dynadot, Namecheap, GoDaddy, and Spaceship together accounted for over 125,000 of the day's ~280,000 domains. Four names, nearly half the internet's daily growth.&lt;/li&gt;
&lt;li&gt;The data is not perfectly clean. Namecheap shows up twice under two spellings, &lt;code&gt;NameCheap, Inc&lt;/code&gt; with 34,195 and &lt;code&gt;Namecheap, Inc&lt;/code&gt; further down with another 1,760. If you build alerting on the registrar field, normalize it first. This part took me a minute to spot.&lt;/li&gt;
&lt;li&gt;Edit distance on a short brand is noisy. &lt;code&gt;paypill.icu&lt;/code&gt; is a real catch. But a three-edit window on a six-letter word also pulls in &lt;code&gt;playjl.lol&lt;/code&gt;, which shares letters and means nothing. For short brands, drop to &lt;code&gt;--max-edits 1&lt;/code&gt;, or once your candidate list is small, require the brand to sit at the start of the label.&lt;/li&gt;
&lt;li&gt;The two passes really do disagree. On this particular day the brand-plus-keyword pass returned nothing for &lt;code&gt;paypal&lt;/code&gt;, while edit distance returned plenty. Another brand, or another day, flips that. Which is the point of running both.&lt;/li&gt;
&lt;li&gt;Registration is not the same as reach. Plenty of these names resolve to nothing on day one. They sit parked, waiting. The dangerous fraction already has a live A record and a valid cert, and those are the ones worth a second query.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Performance, since that was the whole promise
&lt;/h2&gt;

&lt;p&gt;I timed the typosquat scan on the full daily file, ~280,000 rows, on an ordinary laptop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    SELECT domain_name
    FROM &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nrd_2026-07-18.csv.gz&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
    WHERE levenshtein(split_part(domain_name, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, 1), &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;paypal&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;) BETWEEN 1 AND 3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The registrar and TLD group-bys came back instantly, fast enough that there was nothing to time. The typosquat scan is heavier, because it runs a Levenshtein calculation against every one of the ~280,000 rows, and that took about 3.2 seconds. Not instant, but fast enough to sit there and iterate on your brand list without breaking focus, and all of it reading a gzipped file off disk with no load step. DuckDB stays in-memory friendly into the millions of rows and spills to disk beyond that, so a full month of daily files in one glob query is still reasonable on the same laptop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handing off to pandas for the parts SQL is bad at
&lt;/h2&gt;

&lt;p&gt;DuckDB is the wrong tool for charts. When I want to plot the daily registration curve or feed results into scikit-learn, I convert the result to a DataFrame with one method call. No copy back through disk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    SELECT create_date, COUNT(*) AS registered
    FROM &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nrd_*.csv.gz&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
    GROUP BY create_date
    ORDER BY create_date
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;df&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get the SQL engine for the heavy filtering and pandas for the last mile. Use each for what it is good at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going further
&lt;/h2&gt;

&lt;p&gt;There are a few directions this naturally grows into.&lt;/p&gt;

&lt;p&gt;The obvious next step is enrichment. Once you have your shortlist of suspicious names, look up their live WHOIS and DNS so you can sort the parked ones from the ones already serving pages.&lt;/p&gt;

&lt;p&gt;You can widen the hunt from one brand to a keyword set, and you can add homoglyph normalization so &lt;code&gt;paypaI&lt;/code&gt; and &lt;code&gt;pаypal&lt;/code&gt; with a Cyrillic a stop slipping through a plain edit-distance filter.&lt;/p&gt;

&lt;p&gt;And if you would rather not run a cron job and a distance-scoring script forever, this is a solved product. &lt;a href="https://whoisfreaks.com/products/brand-monitoring" rel="noopener noreferrer"&gt;WhoisFreaks Brand Monitoring&lt;/a&gt; watches new registrations for your brand across those 1,500-plus TLDs twice a day and handles the homoglyph and keyword-permutation matching for you, then emails or JSON-exports the hits for a takedown workflow.&lt;/p&gt;

&lt;p&gt;Why go to the trouble at all, when the big reputation feeds will flag most of these eventually? Because "eventually" is measured in days, and a phishing page's useful life is measured in hours. Reading the raw registrations yourself is how you close that gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full source
&lt;/h2&gt;

&lt;p&gt;The complete script, including the keyword-permutation variant and column auto-detection, is on GitHub: &lt;a href="https://github.com/WhoisFreaks/nrd-typosquat-hunter" rel="noopener noreferrer"&gt;github.com/WhoisFreaks/nrd-typosquat-hunter&lt;/a&gt;. Clone it, point it at the free sample, and swap in your own brand.&lt;/p&gt;

</description>
      <category>python</category>
      <category>security</category>
      <category>cybersecurity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>20 Ways WHOIS Data Gets Used (That Most Developers Never Think About)</title>
      <dc:creator>Sameer Sheikh</dc:creator>
      <pubDate>Mon, 06 Jul 2026 13:18:59 +0000</pubDate>
      <link>https://dev.to/whoisfreaks/20-ways-whois-data-gets-used-that-most-developers-never-think-about-4jo4</link>
      <guid>https://dev.to/whoisfreaks/20-ways-whois-data-gets-used-that-most-developers-never-think-about-4jo4</guid>
      <description>&lt;p&gt;Most developers know WHOIS as "that thing you run once to see who owns a domain, then forget about." Fair enough. But WHOIS and its modern replacement, RDAP, quietly power a surprising number of workflows across security, legal, marketing, and dev ops. I went digging into how it's actually used in 2026, past the basic lookup, and ended up with a list of twenty.&lt;/p&gt;

&lt;p&gt;A couple of these surprised me. One of them involved a botnet takedown that started with a single archived domain registration from 2013.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, the thing that changed everything
&lt;/h2&gt;

&lt;p&gt;If you haven't touched WHOIS since before 2018, the data looks different now. GDPR forced a redaction wave, and most gTLD records show "REDACTED FOR PRIVACY" instead of a name and email. Then on January 28, 2025, ICANN formally sunset the WHOIS requirement for gTLDs entirely. RDAP is the replacement: same idea, structured JSON over HTTPS instead of plaintext over port 43.&lt;/p&gt;

&lt;p&gt;RDAP query volume passed WHOIS query volume in June 2025. By August 2025, gTLD registries were seeing around 65 billion RDAP queries a month, up from 7 billion in January of that year. WHOIS queries dropped by more than half over the same stretch. Most of that traffic isn't humans typing into a terminal. It's automation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg99w2mnio0ma6m8s7b00.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%2Fg99w2mnio0ma6m8s7b00.png" alt="WHOIS to RDAP timeline showing GDPR in 2018, RDAP offered in 2019, WHOIS sunset in January 2025, and RDAP overtaking WHOIS in June 2025" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo5aooqhe7dcshkwhpz4f.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%2Fo5aooqhe7dcshkwhpz4f.png" alt="Bar chart comparing monthly WHOIS and RDAP query volume in January 2025 versus August 2025" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's why that matters before the list: almost none of these use cases still rely on "look up someone's name and email." They rely on dates, registrars, nameservers, and patterns across thousands of records at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security and fraud
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm6gx7hugrh0iq86b2w62.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%2Fm6gx7hugrh0iq86b2w62.png" alt="Grid of eight use-case categories: Security and Fraud, Legal and IP, Marketing and BI, Domain Investing, Compliance, Dev and Ops, Journalism and OSINT, Research" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Catching phishing domains before they're used.&lt;/strong&gt; Attackers usually register a domain right before a campaign, not months ahead. Security teams treat anything registered in the last 24 to 72 hours as elevated risk. One study of predicted malicious domains found that 75.8% of them were registered within the past month. That single data point, the creation date, does a lot of work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Tracing an attacker back to their other domains.&lt;/strong&gt; This is reverse WHOIS: take one known-bad domain, pull whatever registrant details or patterns are still visible, and search for the same fingerprint elsewhere. Brian Krebs used exactly this approach to identify the author of the Mirai botnet in 2017, tracing back to a domain registered in 2013 under a username, "Anna-senpai," that later became the botnet's namesake. Months of digging, one registration record as the thread to pull.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Spotting brand impersonation early.&lt;/strong&gt; Companies run daily sweeps of new domain registrations for anything that looks like their brand name with a typo or extra word. One financial firm's monitoring turned up over 12,000 suspicious lookalike domains in six months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Feeding reputation scores.&lt;/strong&gt; Domain age and registrar history get folded into the risk scores that block or allow traffic in real time. A domain that's three days old behaves very differently in a scoring model than one that's three years old.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Filtering spam.&lt;/strong&gt; Mail providers weight domain age heavily. A brand-new sending domain with zero history gets treated with suspicion by default, which is part of why cold outreach from freshly bought domains lands in spam so often.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Getting attacks shut down faster.&lt;/strong&gt; Every domain record still has to show an abuse contact, redaction or not. Incident responders use it to reach the right party directly instead of going through a slower formal channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Legal and trademark
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;7. Fighting cybersquatting.&lt;/strong&gt; Trademark holders use registration data as the backbone of a UDRP complaint (that's the formal domain dispute process). In 2024, brand owners from 133 countries filed over 6,000 of these cases through WIPO, and more than 95% ended in the domain being transferred to the trademark holder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Catching infringement before it escalates.&lt;/strong&gt; Brand protection teams monitor registrations continuously rather than waiting for a dispute to become necessary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Building a paper trail for takedowns.&lt;/strong&gt; Counterfeit and copyright investigations lean on WHOIS to identify who's actually running a site, which then feeds into formal takedown requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Marketing and business intelligence
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;10. Watching competitors move.&lt;/strong&gt; A competitor registering a batch of new domains, or letting an old TLD lapse, tells you something about where they're headed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;11. Sourcing leads from new registrations.&lt;/strong&gt; Some teams filter newly registered domains by TLD and region to find early-stage companies worth reaching out to. Worth saying clearly: this is legally narrower than it used to be. Redaction and anti-spam rules limit how much of this works on gTLDs now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;12. Mapping who owns what.&lt;/strong&gt; Finding every domain tied to one registrant reveals subsidiary structures and brand portfolios that aren't obvious from the outside.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain investing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;13. Valuing and sourcing domains.&lt;/strong&gt; Investors track creation and expiration dates to value domains and catch ones about to drop. Roughly 200,000 domains get registered every day, and a comparable number expire, so there's a constant flow through the "about to become available" window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;14. Confirming ownership before buying.&lt;/strong&gt; A public RDAP query confirms the technical facts (registrar, dates, nameservers) but usually not who's actually behind the domain anymore, since that's redacted. Serious buyers go through the registrar directly for anything that needs real verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compliance
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;15. Light-touch KYC checks.&lt;/strong&gt; A business claiming years of operating history should have a domain that matches. It's not a definitive signal on its own, but it's one more data point in onboarding checks, and cheap to pull.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;16. Vendor and M&amp;amp;A due diligence.&lt;/strong&gt; Ownership history helps confirm or contradict what a vendor or acquisition target claims about how long they've been operating.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developer and ops work (the part I actually deal with most)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;17. Not forgetting to renew a domain.&lt;/strong&gt; This sounds almost too basic to list, but a lapsed domain breaks email, invalidates certificates, and can be scooped up by someone else within days. A dashboard pulling expiration dates across every domain a team owns saves a genuinely bad day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;18. Catching a hijack in progress.&lt;/strong&gt; Domain hijacking almost always starts with a WHOIS change, a registrar transfer or a removed transfer lock, before DNS itself gets touched. Monitoring for that change is an early warning most teams don't set up until after it's too late once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;19. Verifying ownership and untangling DNS issues.&lt;/strong&gt; Combining registration data with nameserver and certificate info is a normal part of debugging "why isn't this domain pointing where I expect."&lt;/p&gt;

&lt;h2&gt;
  
  
  Journalism and research
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;20. Tracing disinformation networks.&lt;/strong&gt; Investigators cross-reference WHOIS records with shared SSL certificates and hosting patterns to connect sites that are trying hard not to look connected. A recent investigation linked a video agency registered in Abu Dhabi back to a sanctioned Russian broadcaster this way, using a domain record from 2022 and a shared certificate as the connecting threads.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd actually tell someone building on this today
&lt;/h2&gt;

&lt;p&gt;Don't design around registrant names. They're gone for most gTLDs now, and that's not changing back. Design around what's still there: dates, registrars, nameservers, status codes. If you need historical context, current lookups alone won't get you there anymore, you need a service that keeps snapshots over time.&lt;/p&gt;

&lt;p&gt;And check which protocol you're actually querying. Most ccTLDs still haven't moved to RDAP, so if you're building something that needs to work across TLDs, you'll want a WHOIS fallback for a while yet.&lt;/p&gt;

&lt;p&gt;For anyone doing this at any real volume, &lt;a href="https://whoisfreaks.com/documentation/whois-api" rel="noopener noreferrer"&gt;WhoisFreaks' WHOIS API documentation&lt;/a&gt; covers what's returned per TLD, which is worth checking before you assume a field will be there. And if historical tracking is the actual goal, their &lt;a href="https://whoisfreaks.com/products/whois-history-api" rel="noopener noreferrer"&gt;WHOIS History product&lt;/a&gt; handles the snapshot problem instead of you having to build and maintain your own archive.&lt;/p&gt;

</description>
      <category>security</category>
      <category>cybersecurity</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Registrant Pivot: Find Every Domain Owned by an Email Address Using Python</title>
      <dc:creator>Sameer Sheikh</dc:creator>
      <pubDate>Wed, 24 Jun 2026 15:23:04 +0000</pubDate>
      <link>https://dev.to/whoisfreaks/registrant-pivot-find-every-domain-owned-by-an-email-address-using-python-hk</link>
      <guid>https://dev.to/whoisfreaks/registrant-pivot-find-every-domain-owned-by-an-email-address-using-python-hk</guid>
      <description>&lt;p&gt;Most threat investigators still do this manually. They find a suspicious domain, look up the WHOIS record, copy the registrant email, paste it into a lookup tool, and repeat. One domain at a time.&lt;/p&gt;

&lt;p&gt;At 50 domains that's tedious. At 500 it breaks down completely.&lt;/p&gt;

&lt;p&gt;I wanted to automate the pivot. Given one registrant email or name, pull every domain they've ever registered across all TLDs and analyze the full portfolio in a single run. Here's how I built it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Key Insight
&lt;/h2&gt;

&lt;p&gt;A single threat actor rarely registers just one domain. They register clusters. The same email, org name, or phone number shows up across dozens of registrations spread across different TLDs and different campaign timelines.&lt;/p&gt;

&lt;p&gt;Standard WHOIS lookups miss this entirely. They're forward lookups: domain to owner. What you actually need is the reverse: owner to all domains.&lt;/p&gt;

&lt;p&gt;That's what the Reverse WHOIS API does. You hand it an email address, registrant name, company, or keyword, and it searches backward through the full WHOIS database and returns every matching domain. WhoisFreaks indexes 3.6B+ WHOIS records across 1,528 TLDs. Full product features and documentation at the &lt;a href="https://whoisfreaks.com/products/reverse-whois-api" rel="noopener noreferrer"&gt;WhoisFreaks Reverse WHOIS API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkfd2ccnkra3hlo3qgyua.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%2Fkfd2ccnkra3hlo3qgyua.png" alt="Forward lookup hits a dead end after one owner. Reverse lookup returns the full domain portfolio, then chains into new pivot targets." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/WhoisFreaks/reverse-whois-pivot
&lt;span class="nb"&gt;cd &lt;/span&gt;reverse-whois-pivot
pip &lt;span class="nb"&gt;install &lt;/span&gt;requests pandas tabulate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Get a free API key (500 credits, no card needed): &lt;a href="https://billing.whoisfreaks.com/signup" rel="noopener noreferrer"&gt;https://billing.whoisfreaks.com/signup&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your_api_key_here&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.whoisfreaks.com/v1.0/whois&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 1: Query the Reverse WHOIS API
&lt;/h2&gt;

&lt;p&gt;The API takes one search identifier per request. You pass it as a named parameter: &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;keyword&lt;/code&gt;, &lt;code&gt;owner&lt;/code&gt;, or &lt;code&gt;company&lt;/code&gt;. Pick the one that matches what you have.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reverse_whois_by_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Reverse WHOIS lookup by registrant email address.
    Returns all domains registered using this email.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apiKey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;whois&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reverse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;page&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reverse_whois_by_owner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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="n"&gt;exact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&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;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Reverse WHOIS lookup by registrant name.
    Set exact=True for strict name matching.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apiKey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;whois&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reverse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exact&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;page&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="c1"&gt;# Try it against a registrant email
&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reverse_whois_by_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;john.smith@protonmail.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total results : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;total_Result&lt;/span&gt;&lt;span class="sh"&gt;'&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="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total pages   : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;total_Pages&lt;/span&gt;&lt;span class="sh"&gt;'&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="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Current page  : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;current_Page&lt;/span&gt;&lt;span class="sh"&gt;'&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="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;whois_domains_historical&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&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;Output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;Total&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;results&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Total&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;pages&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Current&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;page&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"domain_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fastpaywall.net"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"create_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2021-03-12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"expiry_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2023-03-12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"domain_registrar"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"registrar_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NameCheap, Inc."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"registrant_contact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John Smith"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"email_address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"john.smith@protonmail.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"country_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Russia"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"domain_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"securepay-verify.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"create_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2021-05-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"expiry_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2022-05-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"domain_registrar"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"registrar_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GoDaddy.com, LLC"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"registrant_contact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John Smith"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"email_address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"john.smith@protonmail.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"country_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Russia"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fourteen domains from one email address.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Handle Pagination
&lt;/h2&gt;

&lt;p&gt;Results page at 10 per page by default. Large registrant portfolios need all pages pulled automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_all_domains_by_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fetch all pages for a registrant email query.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;all_domains&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reverse_whois_by_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;domains&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;whois_domains_historical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

        &lt;span class="n"&gt;all_domains&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;total_pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_Pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;total_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_Result&lt;/span&gt;&lt;span class="sh"&gt;"&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Page &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total_pages&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: fetched &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; domains &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
              &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_domains&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total_results&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; total)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;total_pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

        &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# stay polite
&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;all_domains&lt;/span&gt;


&lt;span class="n"&gt;domains&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_all_domains_by_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;john.smith@protonmail.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Done. Total domains: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Page 1/1: fetched 14 domains (14/14 total)

Done. Total domains: 14
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3: Structure the Portfolio
&lt;/h2&gt;

&lt;p&gt;Convert the raw records to a DataFrame and derive a few useful fields: status (active vs expired), TLD, lifespan in days.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;analyze_portfolio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;records&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;registrant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registrant_contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
        &lt;span class="n"&gt;registrar&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain_registrar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="p"&gt;{})&lt;/span&gt;

        &lt;span class="n"&gt;create_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;expiry_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expiry_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Determine active vs expired
&lt;/span&gt;        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;active&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;expiry_date&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expiry_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expired&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;pass&lt;/span&gt;

        &lt;span class="c1"&gt;# Lifespan in days
&lt;/span&gt;        &lt;span class="n"&gt;lifespan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;create_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expiry_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;lifespan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;
        &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;

        &lt;span class="n"&gt;domain_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;tld&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;domain_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;domain_name&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;domain_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tld&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;tld&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;create_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expires&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;expiry_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lifespan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;lifespan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registrar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;registrar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registrar_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;registrant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;registrant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email_address&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;country&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;registrant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;country_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;org&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;registrant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;company&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;notna&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ascending&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;


&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;analyze_portfolio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tld&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lifespan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registrar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]].&lt;/span&gt;&lt;span class="nf"&gt;to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&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;Output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;               domain   tld     created   status  lifespan           registrar
   crypto-wallet-pro.net  .net  2022-11-03   active       365     NameCheap, Inc.
     securepay-verify.com  .com  2021-05-01  expired       365   GoDaddy.com, LLC
          fastpaywall.net  .net  2021-03-12  expired       731     NameCheap, Inc.
     walletrecovery24.com  .com  2021-02-28  expired       365     NameCheap, Inc.
   bitcoin-claims-now.com  .com  2020-09-17  expired       365   GoDaddy.com, LLC
         cryptosafe-id.net  .net  2020-07-05  expired       365     NameCheap, Inc.
  paypal-secure-check.com  .com  2020-04-21  expired       365  Tucows Domains Inc.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pattern already visible: almost every domain registered for exactly 365 days. That's not coincidence.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Score for Risk
&lt;/h2&gt;

&lt;p&gt;Not every result is adversarial. Score each domain on keyword content, TLD risk, registration lifespan, and status.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SUSPICIOUS_KEYWORDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verify&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;account&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wallet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;crypto&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bitcoin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pay&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bank&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;update&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confirm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recover&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;HIGH_RISK_TLDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.xyz&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.top&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.click&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.tk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.ml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.cf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.ga&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.pw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score_domain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Series&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;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;domain_lower&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Keyword signals
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;kw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SUSPICIOUS_KEYWORDS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;kw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;domain_lower&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;

    &lt;span class="c1"&gt;# High-risk TLD
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tld&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;HIGH_RISK_TLDS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;

    &lt;span class="c1"&gt;# Short lifespan (exactly 1-year registrations = disposable infrastructure)
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lifespan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lifespan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;

    &lt;span class="c1"&gt;# Abandoned infrastructure
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expired&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;risk_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score_domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&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="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;risk_label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;risk_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;bins&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&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="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LOW&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MEDIUM&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HIGH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;risk_label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;value_counts&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;to_string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;high&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;risk_label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HIGH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sort_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;risk_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ascending&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;high&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;risk_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]].&lt;/span&gt;&lt;span class="nf"&gt;to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&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;Output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HIGH      9
MEDIUM    3
LOW       2

                    domain     created   status  risk_score
      securepay-verify.com  2021-05-01  expired          90
           fastpaywall.net  2021-03-12  expired          85
   paypal-secure-check.com  2020-04-21  expired          85
      walletrecovery24.com  2021-02-28  expired          80
    bitcoin-claims-now.com  2020-09-17  expired          75
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffd9plye2xgxoh32uvg6h.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%2Ffd9plye2xgxoh32uvg6h.png" alt="Risk score distribution across the 14-domain portfolio: 9 HIGH, 3 MEDIUM, 2 LOW" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Chain the Pivot
&lt;/h2&gt;

&lt;p&gt;Once you have the portfolio, pull every other identifier out of those records. Each one becomes a new search.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_pivot_targets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&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;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pivots&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;emails&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;dropna&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orgs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;org&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;dropna&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;names&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;dropna&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;countries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;country&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;dropna&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;# Strip blanks
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;vals&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vals&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pivots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;


&lt;span class="n"&gt;pivots&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_pivot_targets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ptype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pivots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ptype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EMAILS:
  j.smith.domains@gmail.com
  john.smith@protonmail.com

NAMES:
  John Smith

ORGS:
  Privacy Protection LLC
  SmithWeb Services

COUNTRIES:
  Russia
  Ukraine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two emails. Two orgs. Run &lt;code&gt;get_all_domains_by_email("j.smith.domains@gmail.com")&lt;/code&gt; next and you'll find the second campaign cluster.&lt;/p&gt;




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

&lt;p&gt;I ran this against an email tied to a credential-phishing campaign (sanitized for publication).&lt;/p&gt;

&lt;p&gt;47 domains came back. 34 expired, all registered for exactly 365 days, consistent with disposable infrastructure. 8 still active, 3 pointing to live phishing pages at the time I ran it. The remaining 5 had just been registered and weren't resolving yet.&lt;/p&gt;

&lt;p&gt;All 8 active domains were registered inside a 6-day window in late 2023. Without the reverse pivot, I'd have found maybe 2 or 3 of them through forward searches. The full historical portfolio put the entire campaign timeline in one view.&lt;/p&gt;

&lt;p&gt;The org field pivot found 11 more domains under "SmithWeb Services" that didn't share any email address with the primary cluster. That's the kind of connection that never shows up in manual investigation.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Exact vs fuzzy matching matters.&lt;/strong&gt; The &lt;code&gt;exact=true&lt;/code&gt; param on owner/company searches cuts noise significantly. For email searches, the API always uses exact match automatically, which is what you want.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;365-day lifespan is a stronger signal than keywords.&lt;/strong&gt; Legitimate domains with "secure" or "verify" in the name tend to be multi-year registrations. Malicious ones are almost always exactly 12 months. That 25-point score bump for sub-365-day lifespans earns its weight.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Org names are lazier than emails.&lt;/strong&gt; Threat actors rotate emails between campaigns. They reuse org names constantly. I've seen the same fake LLC appear across three separate campaigns, only findable through a company pivot after the email pivots came back clean.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Privacy protection creates gaps, but not complete ones.&lt;/strong&gt; About 30% of records had redacted registrant data from privacy proxies. The remaining 70% were enough to build the full graph, and some privacy-protected records had leaked the real contact in earlier WHOIS snapshots.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The &lt;code&gt;mode=mini&lt;/code&gt; param is worth knowing.&lt;/strong&gt; It returns a slimmer response (domain name, dates, registrar only), which is faster and cheaper if you're doing a first-pass sweep across many identifiers before pulling full records.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Going Further
&lt;/h2&gt;

&lt;p&gt;A few things worth building on top of this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Export the high-risk domain list as IOCs and push to your SIEM or threat intel platform via its API.&lt;/li&gt;
&lt;li&gt;For each active domain in the portfolio, pull DNS A records and cluster by shared IP. Shared hosting across campaign domains is common and easy to spot once you have the full list.&lt;/li&gt;
&lt;li&gt;Run pivots on a schedule against known adversary identifiers. Store a set of known bad emails and names, run the API daily, diff against yesterday's results, alert on anything new.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That third option is what &lt;a href="https://whoisfreaks.com/products/registrant-monitoring" rel="noopener noreferrer"&gt;WhoisFreaks Registrant Monitoring&lt;/a&gt; handles as a managed service. Give it a registrant name, email, or company and it watches across all 1,528 TLDs, firing an alert the moment that identifier registers a new domain. No cron job to babysit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full Source Code
&lt;/h2&gt;

&lt;p&gt;Complete combined script: &lt;a href="https://github.com/WhoisFreaks/reverse-whois-pivot" rel="noopener noreferrer"&gt;github.com/WhoisFreaks/reverse-whois-pivot&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>security</category>
      <category>cybersecurity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How I Built a Phishing Domain Detector in Python (Zero API Calls After Download)</title>
      <dc:creator>Sameer Sheikh</dc:creator>
      <pubDate>Tue, 16 Jun 2026 13:45:00 +0000</pubDate>
      <link>https://dev.to/whoisfreaks/how-i-built-a-phishing-domain-detector-in-python-zero-api-calls-after-download-4p02</link>
      <guid>https://dev.to/whoisfreaks/how-i-built-a-phishing-domain-detector-in-python-zero-api-calls-after-download-4p02</guid>
      <description>&lt;p&gt;Most phishing detection tools are reactive. They wait for a domain to be reported, then add it to a blocklist. By that point, the campaign has already been running for hours or days.&lt;/p&gt;

&lt;p&gt;I wanted to flip that. Instead of waiting for a domain to be used, I wanted to flag it the moment it was registered.&lt;/p&gt;

&lt;p&gt;Here's how I built a phishing domain detector using the &lt;a href="https://whoisfreaks.com" rel="noopener noreferrer"&gt;WhoisFreaks&lt;/a&gt; Newly Registered Domains (NRD) feed. The entire scoring process runs locally on a downloaded file with no per-domain API calls and no per-row credit consumption.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Key Insight: The Data Is Already There
&lt;/h2&gt;

&lt;p&gt;Most WHOIS-based tools work like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Download list of domains -&amp;gt; loop through -&amp;gt; call WHOIS API for each one
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's slow and expensive. WhoisFreaks' NRD feed works differently. The daily file ships with WHOIS data already embedded per domain: registrant name, registrar, registration date, expiry date, nameservers, domain status. All 65 columns, one row per domain, one file download.&lt;/p&gt;

&lt;p&gt;So the workflow becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Download gzipped CSV -&amp;gt; parse locally -&amp;gt; score everything -&amp;gt; done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No loop of API calls. No per-domain credit burn. You get the full picture in a single download.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the NRD File Looks Like
&lt;/h2&gt;

&lt;p&gt;The WhoisFreaks NRD "With WHOIS" endpoint returns a gzipped CSV. Each row covers one newly registered domain and includes columns like:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Example value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;domain_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;paypa1-secure-verify.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create_date&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2026-06-06&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expiry_date&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2027-06-06&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;domain_registrar_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Porkbun LLC&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;registrant_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Whois Privacy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;registrant_country&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;United States&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name_server_1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ns1.porkbun.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;domain_status_1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;clienttransferprohibited&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;WhoisFreaks publishes two daily files: a gTLD file (&lt;code&gt;.com&lt;/code&gt;, &lt;code&gt;.net&lt;/code&gt;, &lt;code&gt;.org&lt;/code&gt;, &lt;code&gt;.io&lt;/code&gt; etc.) and a ccTLD file covering country-code TLDs like &lt;code&gt;.uk&lt;/code&gt;, &lt;code&gt;.de&lt;/code&gt;, and &lt;code&gt;.pk&lt;/code&gt;. Each row carries the same 65 columns. Merged, the two files typically run 300,000 to 400,000 rows per day. The full detector downloads and merges both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Endpoints:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# gTLDs (.com .net .org .io etc.)&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://files.whoisfreaks.com/v3.1/download/domainer/gtld?apiKey=YOUR_KEY&amp;amp;whois=true&amp;amp;date=2026-06-07"&lt;/span&gt;

&lt;span class="c"&gt;# ccTLDs (.uk .de .pk .ca etc.)&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://files.whoisfreaks.com/v3.1/download/domainer/cctld?apiKey=YOUR_KEY&amp;amp;whois=true&amp;amp;date=2026-06-07"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;whois=true&lt;/code&gt; requests the With-WHOIS variant&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;date&lt;/code&gt; is optional, omit it for the most recent available file
Full docs: &lt;a href="https://whoisfreaks.com/documentation/newly-registered-domains" rel="noopener noreferrer"&gt;whoisfreaks.com/documentation/newly-registered-domains&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/WhoisFreaks/wf-python-detect-phishing-domains.git
&lt;span class="nb"&gt;cd &lt;/span&gt;wf-python-detect-phishing-domains
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="nb"&gt;cp &lt;/span&gt;config.example.py config.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;code&gt;config.py&lt;/code&gt; and add your API key and brand names. Get a free key at &lt;a href="https://whoisfreaks.com" rel="noopener noreferrer"&gt;whoisfreaks.com&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Download gTLD + ccTLD Files and Merge
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gzip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;NRD_BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://files.whoisfreaks.com/v3.1/download/domainer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;download_nrd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tld_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gtld&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Download one daily NRD gzipped CSV (With WHOIS).
    tld_type: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gtld&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; or &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cctld&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
    Returns a list of dicts, one per domain, WHOIS included.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apiKey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;whois&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;   &lt;span class="c1"&gt;# format: yyyy-MM-dd
&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;NRD_BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tld_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gzip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decompress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;download_and_merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Download both gTLD and ccTLD files and return a single merged list.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;gtld_rows&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;download_nrd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tld_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gtld&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cctld_rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;download_nrd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tld_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cctld&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;merged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&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="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;gtld_rows&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cctld_rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;merged&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two HTTP requests. Two CSV files merged into one dataset. All WHOIS data is already inside, no further API calls needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Fuzzy Similarity Scoring (Local, No API)
&lt;/h2&gt;

&lt;p&gt;The trick is normalization: strip the TLD, separators, digits, and noise words like "secure", "login", "account" before scoring. Otherwise &lt;code&gt;paypa1-secure-verify.com&lt;/code&gt; scores poorly against &lt;code&gt;paypal&lt;/code&gt; because all the noise drowns out the match.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;thefuzz&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fuzz&lt;/span&gt;

&lt;span class="n"&gt;NOISE_WORDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;account&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verify&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;update&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;official&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;online&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;portal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;help&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;center&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confirm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# rsplit strips the TLD generically -- works for all ccTLDs and gTLDs
&lt;/span&gt;    &lt;span class="c1"&gt;# A regex allowlist would miss unlisted TLDs (e.g. .de, .pk) and leave
&lt;/span&gt;    &lt;span class="c1"&gt;# them in the string, diluting the match score against brand names.
&lt;/span&gt;    &lt;span class="n"&gt;core&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;rsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[-_.]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# strip digits within tokens
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;NOISE_WORDS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;BRANDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paypal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amazon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;netflix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;microsoft&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score_domain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;norm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Reject strings shorter than 4 characters. Digit-heavy domains like
&lt;/span&gt;    &lt;span class="c1"&gt;# "y1288.com" normalize down to a single character ("y"), and
&lt;/span&gt;    &lt;span class="c1"&gt;# partial_ratio("y", "paypal") = 100 because "y" appears in "paypal".
&lt;/span&gt;    &lt;span class="c1"&gt;# A 4-character minimum cuts these false positives cleanly.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&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;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="n"&gt;best_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;best_brand&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="sh"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;brand&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;BRANDS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;fuzz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ratio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;fuzz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partial_ratio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;fuzz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;token_sort_ratio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brand&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="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;best_score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;best_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;best_brand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brand&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;best_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;best_brand&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs entirely in memory with no network calls and no credits consumed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Extract Signals From Embedded WHOIS
&lt;/h2&gt;

&lt;p&gt;Since WHOIS is already in each row, signal extraction is just reading columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;PRIVACY_KEYWORDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;privacy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redacted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;protected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proxy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;whoisguard&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;withheld&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;private&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data redacted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gdpr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_signals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;reg_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registrant_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expiry_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expiry_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registrar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain_registrar_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registrant_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registrant_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registrant_country&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registrant_country&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ns1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name_server_1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain_status_1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_private&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reg_name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;kw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PRIVACY_KEYWORDS&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;h2&gt;
  
  
  Step 4: Risk Scoring
&lt;/h2&gt;

&lt;p&gt;Combine similarity and WHOIS signals into a 0 to 100 score:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_risk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;similarity_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;similarity_score&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# similarity -&amp;gt; max 40 pts
&lt;/span&gt;
    &lt;span class="c1"&gt;# Domain age (max 20 pts)
&lt;/span&gt;    &lt;span class="n"&gt;create_raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;create_raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;created&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;create_raw&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;age_days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;
            &lt;span class="n"&gt;pts&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;age_days&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="nf"&gt;else &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;age_days&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="k"&gt;else&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;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;

    &lt;span class="c1"&gt;# Privacy protection (max 15 pts)
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_private&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;pts&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registrant_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;pts&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;

    &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Max possible score = 40 (similarity) + 20 (age) + 15 (privacy) = 75
&lt;/span&gt;    &lt;span class="c1"&gt;# CRITICAL threshold is set to 75 so the label is actually reachable
&lt;/span&gt;    &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CRITICAL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HIGH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MEDIUM&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LOW&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Downloading NRD feeds (gTLD + ccTLD)...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;download_and_merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; domains loaded.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;findings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;score_domain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;signals&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_signals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;risk_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculate_risk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;similarity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;risk_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;risk_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;risk_label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;risk_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;risk_label&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;domain&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Similarity to &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;brand&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;similarity&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%  |  Risk: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;risk_score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/100&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Registered : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;create_date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  |  Registrar : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;registrar&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Registrant : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;registrant_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;n/a&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;registrant_country&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;n/a&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; suspicious domains found.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;findings&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_WHOISFREAKS_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;Running this against the real merged WhoisFreaks NRD file for June 11, 2026 (336,486 domains, 5 brands):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Downloading NRD feeds (gTLD + ccTLD)...
  336,486 domains loaded.

=======================================================

[CRITICAL]  googledocument.org
  Similarity to 'google': 100%  |  Risk: 75/100
  Registered : 2026-06-10  |  Registrar : Cloudflare, Inc
  Registrant : DATA REDACTED  (United States)

[CRITICAL]  account-ads-google.com
  Similarity to 'google': 100%  |  Risk: 75/100
  Registered : 2026-06-10  |  Registrar : NICENIC INTERNATIONAL GROUP CO., LIMITED
  Registrant : REDACTED FOR PRIVACY  (United States)

[CRITICAL]  henryamazon.com
  Similarity to 'amazon': 100%  |  Risk: 75/100
  Registered : 2026-06-10  |  Registrar : DNSPod, Inc
  Registrant : Redacted for Privacy  (China)

[HIGH]  netflixsupport.help
  Similarity to 'netflix': 100%  |  Risk: 65/100
  Registered : 2026-06-10  |  Registrar : Key-Systems, LLC
  Registrant : n/a  (United States)

[HIGH]  amazonsellerpilot.com
  Similarity to 'amazon': 100%  |  Risk: 60/100
  Registered : 2026-06-10  |  Registrar : Whois Corp
  Registrant : WITHALICE Co., Ltd  (South Korea)

  2,912 suspicious domains found.
  CRITICAL: 62   HIGH: 1,197   MEDIUM: 1,653   LOW: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2,912 candidates from 336,486 rows, zero separate API calls.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Noticed In The Results
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The scale is real.&lt;/strong&gt; A single day's merged NRD dataset contains 336,000+ domains. Running against 5 brands flagged 2,912 candidates: 62 CRITICAL, 1,197 HIGH, and 1,653 MEDIUM. That is not a theoretical threat surface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The min-length guard matters.&lt;/strong&gt; Without it, digit-heavy domains like &lt;code&gt;y1288.com&lt;/code&gt; normalize down to a single character ("y") and score 100% against "paypal" via &lt;code&gt;partial_ratio&lt;/code&gt; because "y" appears in the brand. A 4-character minimum cuts these false positives cleanly. Every CRITICAL result above contains the brand name clearly in the domain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CRITICAL means what it says.&lt;/strong&gt; Every CRITICAL finding was registered within 7 days, had a privacy-protected registrant, and matched a brand at 100% similarity. &lt;code&gt;googledocument.org&lt;/code&gt; and &lt;code&gt;account-ads-google.com&lt;/code&gt; are obvious infrastructure setups. &lt;code&gt;henryamazon.com&lt;/code&gt; registered through a Chinese registrar with privacy protection is a clear watch target.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy protection is everywhere.&lt;/strong&gt; The majority of flagged domains had &lt;code&gt;REDACTED FOR PRIVACY&lt;/code&gt;, &lt;code&gt;DATA REDACTED&lt;/code&gt;, or similar as the registrant. Not suspicious alone, but combined with brand similarity and a fresh registration date, it is a meaningful signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some are clearly innocent false positives.&lt;/strong&gt; &lt;code&gt;amazonsellerpilot.com&lt;/code&gt; scores 100% similarity but is registered openly by WITHALICE Co., Ltd, a named Korean company, with no privacy protection. That is almost certainly a legitimate seller tool, not phishing. Tuning the threshold and normalization logic for your specific brand reduces these significantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clustering patterns are visible.&lt;/strong&gt; Several domains appeared in sequence with the same registrar, same registrant, and same nameservers all registered on the same day. That is a registration campaign, not independent registrations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Going Further
&lt;/h2&gt;

&lt;p&gt;A few directions worth exploring from here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Registrant clustering&lt;/strong&gt;: group flagged domains by registrar + nameserver + registration date. Clusters indicate coordinated campaigns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add your own brand&lt;/strong&gt;: replace &lt;code&gt;BRANDS&lt;/code&gt; in the script with your actual product name. One short string is all it takes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate daily&lt;/strong&gt;: add a cron job, pipe findings to Slack, and you have a lightweight brand protection monitor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both TLD feeds&lt;/strong&gt;: the full detector downloads both &lt;code&gt;/gtld&lt;/code&gt; and &lt;code&gt;/cctld&lt;/code&gt; feeds and merges them so no newly registered domain slips through.
If you want this running without maintaining the script, cron job, and threshold tuning yourself, WhoisFreaks offers a managed &lt;a href="https://whoisfreaks.com/products/brand-monitoring" rel="noopener noreferrer"&gt;brand monitoring&lt;/a&gt; product built on the same NRD feed. It adds homoglyph detection (visually identical characters from other alphabets, which this script does not catch), scans 1,528+ TLDs twice daily, and sends alerts with the full WHOIS record by email, Telegram, or webhook. The DIY version above is the better choice if you want full control over scoring logic or need to feed results into your own pipeline.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Full Source Code
&lt;/h2&gt;

&lt;p&gt;The complete version with proper error handling, &lt;code&gt;config.py&lt;/code&gt;, and JSON output is available on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/WhoisFreaks/wf-python-detect-phishing-domains" rel="noopener noreferrer"&gt;github.com/WhoisFreaks/wf-python-detect-phishing-domains&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;API docs and free signup: &lt;a href="https://whoisfreaks.com/documentation/newly-registered-domains" rel="noopener noreferrer"&gt;whoisfreaks.com/documentation/newly-registered-domains&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Questions about the fuzzy matching logic or risk scoring? Happy to discuss in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>security</category>
      <category>cybersecurity</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
