<?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: Petteri Pucilowski</title>
    <description>The latest articles on DEV Community by Petteri Pucilowski (@petteri_pucilowski_7ec755).</description>
    <link>https://dev.to/petteri_pucilowski_7ec755</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3958159%2F10c01a3a-4ac9-40e7-8cb4-63d6b0869fdb.jpg</url>
      <title>DEV Community: Petteri Pucilowski</title>
      <link>https://dev.to/petteri_pucilowski_7ec755</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/petteri_pucilowski_7ec755"/>
    <language>en</language>
    <item>
      <title>We had 45 subscribers and had sent them zero emails</title>
      <dc:creator>Petteri Pucilowski</dc:creator>
      <pubDate>Tue, 18 Aug 2026 10:06:19 +0000</pubDate>
      <link>https://dev.to/petteri_pucilowski_7ec755/we-had-45-subscribers-and-had-sent-them-zero-emails-25jk</link>
      <guid>https://dev.to/petteri_pucilowski_7ec755/we-had-45-subscribers-and-had-sent-them-zero-emails-25jk</guid>
      <description>&lt;p&gt;We had 45 people on our email list and had sent them, in total, zero emails.&lt;/p&gt;

&lt;p&gt;Not zero this month. Zero ever. I found it while checking something else, and the reason is boring enough that I suspect it is sitting in your app too.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a form ends up promising something no code owns
&lt;/h2&gt;

&lt;p&gt;Our subscribe endpoint did exactly what I wrote it to do:&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;@router.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/newsletter/subscribe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SubscribeBody&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="bp"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;resend_audience_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_contact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;audience_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resend_newsletter_audience_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;email&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="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="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the contact to the audience, return 200. That is a complete, correct, tested implementation of "subscribe". It is also the whole bug, because &lt;em&gt;nothing else in the system ever mails that audience except a weekly broadcast I had paused a month earlier&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Meanwhile the frontend was making promises. The exit intent popup said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;get one email when the next release is live&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and the blog form said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;one tactical post a week&lt;/p&gt;

&lt;p&gt;+ a free domain audit when you sign up&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three promises. A per release email, a weekly post, and an audit. Grep the backend for the code that keeps any of them and you get one hit: the weekly broadcast, behind a feature flag that was set to false. The audit never existed at all. I had written it as copy, liked how it read, and shipped it.&lt;/p&gt;

&lt;p&gt;That is the actual failure mode. Not a bug in a function. A promise that lives in JSX with no counterpart in the backend, so no test can fail and no error can fire. The system was 100% healthy while doing nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first touch has to exist, and exactly once
&lt;/h2&gt;

&lt;p&gt;The fix is a welcome email, which sounds trivial until you write down what it has to survive: a resubscribe must not send a second one, a Resend hiccup during the request must not lose the only email that subscriber ever gets, and a retry must not send two.&lt;/p&gt;

&lt;p&gt;I already had a &lt;code&gt;delayed_events&lt;/code&gt; table for scheduling emails after a purchase, with this index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;uq_delayed_events_email_event&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;delayed_events&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;event_name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That index is the whole dedupe. &lt;code&gt;schedule()&lt;/code&gt; returns the new row id, or &lt;code&gt;None&lt;/code&gt; if a row for that pair already exists. So "have we ever welcomed this address" is not a new question, it is the insert:&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;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_welcome_once&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;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;delayed_events_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;schedule&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;delayed_events_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NEWSLETTER_WELCOME_EVENT&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event_id&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;already_welcomed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;          &lt;span class="c1"&gt;# resubscribe, ever, forever
&lt;/span&gt;    &lt;span class="n"&gt;sent&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;send_newsletter_welcome_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="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queued&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;                    &lt;span class="c1"&gt;# row stays due, poller retries
&lt;/span&gt;    &lt;span class="n"&gt;delayed_events_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mark_sent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sending inline matters more than it looks. A welcome email that arrives five minutes after signup is a worse welcome, and the poller interval was five minutes. So the row is a receipt, not a queue: written first, stamped after delivery, and if delivery fails it is simply left due for the poller that already exists.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mark_sent&lt;/code&gt; is guarded so a poller tick that fires in the same second cannot double send:&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;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&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 delayed_events SET sent_at = ?, last_error = NULL &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WHERE id = ? AND sent_at IS NULL&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;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rowcount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Seeding a ledger so shipping the feature does not fire it
&lt;/h2&gt;

&lt;p&gt;The popup promise was per release: our data comes from Common Crawl, which republishes its hyperlink graph about every three months, and people signed up to hear when a fresh one lands.&lt;/p&gt;

&lt;p&gt;So: a table keyed by release id, a poller check, one broadcast per new id. Easy, except for the first run. The moment that code deploys it finds the current release, sees no row, and mails 45 people about a graph that went live three weeks ago.&lt;/p&gt;

&lt;p&gt;The fix is one line in &lt;code&gt;ensure_tables&lt;/code&gt;, and it is the part I would have forgotten:&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;empty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&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(*) FROM release_announcements&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT OR IGNORE INTO release_announcements &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(release_id, status, created_at, sent_at) VALUES (?, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;seeded&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_release_id&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;now&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;Bootstrapping the ledger with "already handled" means only genuinely new state can trigger the side effect. Any time you add "notify on change" to a system that has been running for a while, the current value is not a change.&lt;/p&gt;

&lt;p&gt;The other decision worth naming: this is deliberately not behind the same flag as the weekly newsletter. I had paused a weekly column because I did not have time to write it. That is my choice to make. Cancelling a per release notification someone explicitly opted into is not the same choice, and one boolean should not silently do both.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bounce nobody handled
&lt;/h2&gt;

&lt;p&gt;While in there I pulled the last 100 sends from the Resend API. Six bounces, from two addresses. Both were junk domains someone typed into the form. Both had then received the &lt;em&gt;entire&lt;/em&gt; rest of the sequence.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-08-06  bounced  your free CrawlGraph API key
2026-08-08  bounced  the question your free API key can't answer
2026-08-12  bounced  your API key works with hosted MCP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One signup, three bounces, on a schedule. At my volume that is cosmetic. Bounce rate is also the single number mailbox providers use to decide whether you are a sender or a problem, and the list only grows.&lt;/p&gt;

&lt;p&gt;The interesting part is that the fix was already half built. Resend was posting webhooks to an endpoint I had written for inbound support email, and that handler said:&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;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;type&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;email.received&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;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;ignored&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;type&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;type&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;Every bounce notification Resend ever sent me hit that line and was thrown away with a cheerful 200. Now &lt;code&gt;email.bounced&lt;/code&gt; and &lt;code&gt;email.complained&lt;/code&gt; route to a suppression table, and permanent failures cancel anything already scheduled for that address.&lt;/p&gt;

&lt;p&gt;Two details worth stealing. Transient bounces do not suppress, because a full mailbox is not a reason to stop mailing someone forever. And the marketing sender reports &lt;strong&gt;success&lt;/strong&gt; when it skips a suppressed address:&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;email_suppression_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_suppressed&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;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;skipping %s to a suppressed address&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;   &lt;span class="c1"&gt;# terminal: the durable sender must retire the row, not retry
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returning &lt;code&gt;False&lt;/code&gt; there would be more honest looking and completely wrong: every durable sender I have treats &lt;code&gt;False&lt;/code&gt; as "retry later", so a suppressed address would spin until it hit the attempt cap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ten minutes on your own funnel
&lt;/h2&gt;

&lt;p&gt;Three queries, and you do not need my stack to run the equivalent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every collection point, and what it promises.&lt;/strong&gt; Grep the frontend for your form components and read the copy out loud. Every promise is a claim about backend behaviour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For each promise, the code that keeps it.&lt;/strong&gt; If you cannot point at a function, the promise is decoration. Delete the copy or write the function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your provider's recent sends, grouped by event.&lt;/strong&gt; &lt;code&gt;delivered&lt;/code&gt;, &lt;code&gt;bounced&lt;/code&gt;, &lt;code&gt;complained&lt;/code&gt;. Then check whether anything that bounced got mailed again afterwards.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The thing that stung was that every dashboard was green. Signups were arriving, the API returned 200, tests passed, the provider reported no errors. There is no monitor for "the email that was never designed."&lt;/p&gt;

&lt;p&gt;I build &lt;a href="https://crawlgraph.com" rel="noopener noreferrer"&gt;CrawlGraph&lt;/a&gt;, backlink data on Common Crawl's open web graph. The free API key is 15 calls a month with no card if you want to poke at link graphs: &lt;a href="https://crawlgraph.com/docs/api" rel="noopener noreferrer"&gt;crawlgraph.com/docs/api&lt;/a&gt;. But mostly, go read your own signup form's copy and then go looking for the function that keeps it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>python</category>
      <category>showdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>12 domains link to every major help desk</title>
      <dc:creator>Petteri Pucilowski</dc:creator>
      <pubDate>Mon, 17 Aug 2026 02:18:26 +0000</pubDate>
      <link>https://dev.to/petteri_pucilowski_7ec755/12-domains-link-to-every-major-help-desk-3ela</link>
      <guid>https://dev.to/petteri_pucilowski_7ec755/12-domains-link-to-every-major-help-desk-3ela</guid>
      <description>&lt;p&gt;we compared 8 help desks in common crawl's domain-level link graph. 12 non-platform domains link to all 8, while 81% of linking domains appear for only one subject.&lt;/p&gt;

&lt;h2&gt;
  
  
  how we pulled this
&lt;/h2&gt;

&lt;p&gt;for each subject domain, we pulled the top 2,000 referring domains by authority from common crawl release Apr-Jun 2026 (cc-main-2026-apr-may-jun). we intersected those lists, then removed platform, cdn, social, and other non-editorial domains from the clean overlap counts.&lt;/p&gt;

&lt;p&gt;the filter matters. hosting providers, cdns, url shorteners and the big social networks link to almost everything, so leaving them in would produce a universal list that is technically correct and useless for outreach. the counts below are after that filter unless a column says otherwise. everything here comes from the open common crawl webgraph, so you can reproduce it without a paid backlink tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;subject domains&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;domain&lt;/th&gt;
&lt;th&gt;referring domains&lt;/th&gt;
&lt;th&gt;cg authority&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;zendesk.com&lt;/td&gt;
&lt;td&gt;92,417&lt;/td&gt;
&lt;td&gt;69&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;intercom.com&lt;/td&gt;
&lt;td&gt;22,114&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;helpscout.com&lt;/td&gt;
&lt;td&gt;9,356&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;freshdesk.com&lt;/td&gt;
&lt;td&gt;24,015&lt;/td&gt;
&lt;td&gt;59&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gorgias.com&lt;/td&gt;
&lt;td&gt;3,165&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;front.com&lt;/td&gt;
&lt;td&gt;1,401&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kayako.com&lt;/td&gt;
&lt;td&gt;2,111&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zoho.com&lt;/td&gt;
&lt;td&gt;67,333&lt;/td&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  the overlap
&lt;/h2&gt;

&lt;p&gt;across the 8 subjects, we found 11,563 unique linking domains. the clean universal set contains 12 domains.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;metric&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;unique linking domains&lt;/td&gt;
&lt;td&gt;11,563&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to one subject&lt;/td&gt;
&lt;td&gt;81%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to all 8&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;overlap distribution&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;overlap&lt;/th&gt;
&lt;th&gt;all domains&lt;/th&gt;
&lt;th&gt;non-platform&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;link to all 8&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to 7&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to 6&lt;/td&gt;
&lt;td&gt;66&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to 5&lt;/td&gt;
&lt;td&gt;108&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to 4&lt;/td&gt;
&lt;td&gt;199&lt;/td&gt;
&lt;td&gt;196&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to 3&lt;/td&gt;
&lt;td&gt;458&lt;/td&gt;
&lt;td&gt;454&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to 2&lt;/td&gt;
&lt;td&gt;1,305&lt;/td&gt;
&lt;td&gt;1,302&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to just 1&lt;/td&gt;
&lt;td&gt;9,392&lt;/td&gt;
&lt;td&gt;9,386&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;the distribution is the interesting part. most linking domains sit in the bottom row: they mention one product and never come back. the rows above it are where outreach lives, because a site that already covers several products in a category has an editorial reason to cover another one. a short universal list usually means the category is covered by a handful of directories, review sites and integration hubs rather than by a broad press base. a long one means the category has real editorial coverage, and the same sites are reachable for you.&lt;/p&gt;

&lt;p&gt;the highest-authority sites linking to all of them are shopify.com, substack.com and hubspot.com. those are the pages worth reading before you pitch anything, because they already decided this category is worth covering.&lt;/p&gt;

&lt;p&gt;zendesk.com carries the widest referring-domain profile of the set and front.com the narrowest, which is usually a function of age and press coverage rather than product quality. the gap between them is the practical opportunity: sites that cover the widest profile but not the narrowest are demonstrably willing to write about this category.&lt;/p&gt;

&lt;h2&gt;
  
  
  the category linkers
&lt;/h2&gt;

&lt;p&gt;93 non-platform domains link to at least 6 of the 8 subjects. these repeat linkers are the sites most consistently connected to help desks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;highest-overlap category linkers&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;domain&lt;/th&gt;
&lt;th&gt;subjects&lt;/th&gt;
&lt;th&gt;cg authority&lt;/th&gt;
&lt;th&gt;links to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;shopify.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;zendesk.com, intercom.com, helpscout.com, freshdesk.com, gorgias.com, front.com, kayako.com, zoho.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;substack.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;td&gt;zendesk.com, intercom.com, helpscout.com, freshdesk.com, gorgias.com, front.com, kayako.com, zoho.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hubspot.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;zendesk.com, intercom.com, helpscout.com, freshdesk.com, gorgias.com, front.com, kayako.com, zoho.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zapier.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;68&lt;/td&gt;
&lt;td&gt;zendesk.com, intercom.com, helpscout.com, freshdesk.com, gorgias.com, front.com, kayako.com, zoho.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jotform.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;65&lt;/td&gt;
&lt;td&gt;zendesk.com, intercom.com, helpscout.com, freshdesk.com, gorgias.com, front.com, kayako.com, zoho.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;webflow.io&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;td&gt;zendesk.com, intercom.com, helpscout.com, freshdesk.com, gorgias.com, front.com, kayako.com, zoho.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;clickup.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;59&lt;/td&gt;
&lt;td&gt;zendesk.com, intercom.com, helpscout.com, freshdesk.com, gorgias.com, front.com, kayako.com, zoho.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;activecampaign.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;59&lt;/td&gt;
&lt;td&gt;zendesk.com, intercom.com, helpscout.com, freshdesk.com, gorgias.com, front.com, kayako.com, zoho.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tidio.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;td&gt;zendesk.com, intercom.com, helpscout.com, freshdesk.com, gorgias.com, front.com, kayako.com, zoho.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zendesk.co.uk&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;57&lt;/td&gt;
&lt;td&gt;zendesk.com, intercom.com, helpscout.com, freshdesk.com, gorgias.com, front.com, kayako.com, zoho.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;crisp.chat&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;zendesk.com, intercom.com, helpscout.com, freshdesk.com, gorgias.com, front.com, kayako.com, zoho.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zight.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;zendesk.com, intercom.com, helpscout.com, freshdesk.com, gorgias.com, front.com, kayako.com, zoho.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;github.com&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;81&lt;/td&gt;
&lt;td&gt;zendesk.com, intercom.com, helpscout.com, freshdesk.com, front.com, kayako.com, zoho.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;salesforce.com&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;zendesk.com, intercom.com, helpscout.com, gorgias.com, front.com, kayako.com, zoho.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bigcommerce.com&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;zendesk.com, intercom.com, helpscout.com, freshdesk.com, gorgias.com, kayako.com, zoho.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sendpulse.com&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;td&gt;zendesk.com, intercom.com, helpscout.com, freshdesk.com, gorgias.com, kayako.com, zoho.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;entrepreneur.com&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;65&lt;/td&gt;
&lt;td&gt;zendesk.com, intercom.com, helpscout.com, freshdesk.com, gorgias.com, kayako.com, zoho.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;g2.com&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;zendesk.com, intercom.com, helpscout.com, gorgias.com, front.com, kayako.com, zoho.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sproutsocial.com&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;td&gt;zendesk.com, intercom.com, helpscout.com, freshdesk.com, front.com, kayako.com, zoho.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kinsta.com&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;td&gt;zendesk.com, intercom.com, helpscout.com, freshdesk.com, front.com, kayako.com, zoho.com&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  the full data
&lt;/h3&gt;

&lt;p&gt;download all 93 clean domains that link to at least 6 of the 8 subjects, ranked by overlap and authority.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://crawlgraph.com/api/studies/who-links-to-every-help-desk/dataset.csv" rel="noopener noreferrer"&gt;download csv&lt;/a&gt; | &lt;a href="https://crawlgraph.com/api/studies/who-links-to-every-help-desk/dataset.json" rel="noopener noreferrer"&gt;download json&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  check any of these domains
&lt;/h3&gt;

&lt;p&gt;every domain in the tables above links to its full referring-domain profile. run your own domain to see who links to you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://crawlgraph.com/" rel="noopener noreferrer"&gt;look up your backlinks&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  run this for your own niche
&lt;/h2&gt;

&lt;p&gt;pick your closest competitors, compare their referring domains, and keep the sites that link to several of them but not to you. sorting that gap by overlap and authority turns the same method into a specific outreach list.&lt;/p&gt;

&lt;p&gt;the ordering is what makes it usable. a site that links to most of your competitors and not to you is a gap with an obvious pitch. the observed overlap gives you a specific reason to contact each site instead of relying on domain authority alone. work down from the top of the overlap column and stop when the reasons start feeling thin, because a pitch you cannot justify in one sentence is not worth sending.&lt;/p&gt;

&lt;p&gt;one caveat worth stating plainly. common crawl is a sample of the web, not a census, so treat these counts as the shape of a link profile rather than an exact total. this study measures the Apr-Jun 2026 release, so rerun the comparison against later releases before treating any movement as a trend.&lt;/p&gt;

&lt;h3&gt;
  
  
  find the overlap in your market
&lt;/h3&gt;

&lt;p&gt;run a backlink gap analysis on the same common crawl data used for this study.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://crawlgraph.com/gap-analysis" rel="noopener noreferrer"&gt;run a free gap analysis&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>35 domains link to every major web host</title>
      <dc:creator>Petteri Pucilowski</dc:creator>
      <pubDate>Mon, 10 Aug 2026 00:13:38 +0000</pubDate>
      <link>https://dev.to/petteri_pucilowski_7ec755/35-domains-link-to-every-major-web-host-308p</link>
      <guid>https://dev.to/petteri_pucilowski_7ec755/35-domains-link-to-every-major-web-host-308p</guid>
      <description>&lt;p&gt;we compared 8 web hosts in common crawl's domain-level link graph. 35 non-platform domains link to all 8, while 72% of linking domains appear for only one subject.&lt;/p&gt;

&lt;h2&gt;
  
  
  how we pulled this
&lt;/h2&gt;

&lt;p&gt;for each subject domain, we pulled the top 2,000 referring domains by authority from common crawl release Apr-Jun 2026 (cc-main-2026-apr-may-jun). we intersected those lists, then removed platform, cdn, social, and other non-editorial domains from the clean overlap counts.&lt;/p&gt;

&lt;p&gt;the filter matters. hosting providers, cdns, url shorteners and the big social networks link to almost everything, so leaving them in would produce a universal list that is technically correct and useless for outreach. the counts below are after that filter unless a column says otherwise. everything here comes from the open common crawl webgraph, so you can reproduce it without a paid backlink tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;subject domains&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;domain&lt;/th&gt;
&lt;th&gt;referring domains&lt;/th&gt;
&lt;th&gt;cg authority&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;hostinger.com&lt;/td&gt;
&lt;td&gt;36,900&lt;/td&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;siteground.com&lt;/td&gt;
&lt;td&gt;13,882&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bluehost.com&lt;/td&gt;
&lt;td&gt;48,747&lt;/td&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dreamhost.com&lt;/td&gt;
&lt;td&gt;36,470&lt;/td&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wpengine.com&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cloudways.com&lt;/td&gt;
&lt;td&gt;13,795&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kinsta.com&lt;/td&gt;
&lt;td&gt;13,963&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;namecheap.com&lt;/td&gt;
&lt;td&gt;16,701&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  the overlap
&lt;/h2&gt;

&lt;p&gt;across the 8 subjects, we found 10,077 unique linking domains. the clean universal set contains 35 domains.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;metric&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;unique linking domains&lt;/td&gt;
&lt;td&gt;10,077&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to one subject&lt;/td&gt;
&lt;td&gt;72%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to all 8&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;overlap distribution&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;overlap&lt;/th&gt;
&lt;th&gt;all domains&lt;/th&gt;
&lt;th&gt;non-platform&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;link to all 8&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to 7&lt;/td&gt;
&lt;td&gt;89&lt;/td&gt;
&lt;td&gt;86&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to 6&lt;/td&gt;
&lt;td&gt;133&lt;/td&gt;
&lt;td&gt;131&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to 5&lt;/td&gt;
&lt;td&gt;185&lt;/td&gt;
&lt;td&gt;182&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to 4&lt;/td&gt;
&lt;td&gt;336&lt;/td&gt;
&lt;td&gt;335&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to 3&lt;/td&gt;
&lt;td&gt;614&lt;/td&gt;
&lt;td&gt;608&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to 2&lt;/td&gt;
&lt;td&gt;1,432&lt;/td&gt;
&lt;td&gt;1,430&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to just 1&lt;/td&gt;
&lt;td&gt;7,246&lt;/td&gt;
&lt;td&gt;7,240&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;the distribution is the interesting part. most linking domains sit in the bottom row: they mention one product and never come back. the rows above it are where outreach lives, because a site that already covers several products in a category has an editorial reason to cover another one. a short universal list usually means the category is covered by a handful of directories, review sites and integration hubs rather than by a broad press base. a long one means the category has real editorial coverage, and the same sites are reachable for you.&lt;/p&gt;

&lt;p&gt;the highest-authority sites linking to all of them are wordpress.org, github.com and shopify.com. those are the pages worth reading before you pitch anything, because they already decided this category is worth covering.&lt;/p&gt;

&lt;p&gt;wpengine.com carries the widest referring-domain profile of the set and cloudways.com the narrowest, which is usually a function of age and press coverage rather than product quality. the gap between them is the practical opportunity: sites that cover the widest profile but not the narrowest are demonstrably willing to write about this category.&lt;/p&gt;

&lt;h2&gt;
  
  
  the category linkers
&lt;/h2&gt;

&lt;p&gt;252 non-platform domains link to at least 6 of the 8 subjects. these repeat linkers are the sites most consistently connected to web hosts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;highest-overlap category linkers&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;domain&lt;/th&gt;
&lt;th&gt;subjects&lt;/th&gt;
&lt;th&gt;cg authority&lt;/th&gt;
&lt;th&gt;links to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;wordpress.org&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;td&gt;hostinger.com, siteground.com, bluehost.com, dreamhost.com, wpengine.com, cloudways.com, kinsta.com, namecheap.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;github.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;81&lt;/td&gt;
&lt;td&gt;hostinger.com, siteground.com, bluehost.com, dreamhost.com, wpengine.com, cloudways.com, kinsta.com, namecheap.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;shopify.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;hostinger.com, siteground.com, bluehost.com, dreamhost.com, wpengine.com, cloudways.com, kinsta.com, namecheap.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hubspot.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;hostinger.com, siteground.com, bluehost.com, dreamhost.com, wpengine.com, cloudways.com, kinsta.com, namecheap.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iubenda.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;69&lt;/td&gt;
&lt;td&gt;hostinger.com, siteground.com, bluehost.com, dreamhost.com, wpengine.com, cloudways.com, kinsta.com, namecheap.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sendpulse.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;td&gt;hostinger.com, siteground.com, bluehost.com, dreamhost.com, wpengine.com, cloudways.com, kinsta.com, namecheap.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;habr.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;td&gt;hostinger.com, siteground.com, bluehost.com, dreamhost.com, wpengine.com, cloudways.com, kinsta.com, namecheap.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;smallbiztrends.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;td&gt;hostinger.com, siteground.com, bluehost.com, dreamhost.com, wpengine.com, cloudways.com, kinsta.com, namecheap.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;freecodecamp.org&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;66&lt;/td&gt;
&lt;td&gt;hostinger.com, siteground.com, bluehost.com, dreamhost.com, wpengine.com, cloudways.com, kinsta.com, namecheap.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;entrepreneur.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;65&lt;/td&gt;
&lt;td&gt;hostinger.com, siteground.com, bluehost.com, dreamhost.com, wpengine.com, cloudways.com, kinsta.com, namecheap.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;smashingmagazine.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;hostinger.com, siteground.com, bluehost.com, dreamhost.com, wpengine.com, cloudways.com, kinsta.com, namecheap.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;letsencrypt.org&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;td&gt;hostinger.com, siteground.com, bluehost.com, dreamhost.com, wpengine.com, cloudways.com, kinsta.com, namecheap.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dev.to&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;td&gt;hostinger.com, siteground.com, bluehost.com, dreamhost.com, wpengine.com, cloudways.com, kinsta.com, namecheap.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;digitalocean.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;td&gt;hostinger.com, siteground.com, bluehost.com, dreamhost.com, wpengine.com, cloudways.com, kinsta.com, namecheap.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sucuri.net&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;hostinger.com, siteground.com, bluehost.com, dreamhost.com, wpengine.com, cloudways.com, kinsta.com, namecheap.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getresponse.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;hostinger.com, siteground.com, bluehost.com, dreamhost.com, wpengine.com, cloudways.com, kinsta.com, namecheap.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;elementor.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;hostinger.com, siteground.com, bluehost.com, dreamhost.com, wpengine.com, cloudways.com, kinsta.com, namecheap.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;searchenginejournal.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;hostinger.com, siteground.com, bluehost.com, dreamhost.com, wpengine.com, cloudways.com, kinsta.com, namecheap.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kit.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;59&lt;/td&gt;
&lt;td&gt;hostinger.com, siteground.com, bluehost.com, dreamhost.com, wpengine.com, cloudways.com, kinsta.com, namecheap.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;freshdesk.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;59&lt;/td&gt;
&lt;td&gt;hostinger.com, siteground.com, bluehost.com, dreamhost.com, wpengine.com, cloudways.com, kinsta.com, namecheap.com&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  the full data
&lt;/h3&gt;

&lt;p&gt;download all 252 clean domains that link to at least 6 of the 8 subjects, ranked by overlap and authority.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://crawlgraph.com/api/studies/who-links-to-every-web-host/dataset.csv" rel="noopener noreferrer"&gt;download csv&lt;/a&gt; | &lt;a href="https://crawlgraph.com/api/studies/who-links-to-every-web-host/dataset.json" rel="noopener noreferrer"&gt;download json&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  run this for your own niche
&lt;/h2&gt;

&lt;p&gt;pick your closest competitors, compare their referring domains, and keep the sites that link to several of them but not to you. sorting that gap by overlap and authority turns the same method into a specific outreach list.&lt;/p&gt;

&lt;p&gt;the ordering is what makes it usable. a site that links to most of your competitors and not to you is a gap with an obvious pitch. the observed overlap gives you a specific reason to contact each site instead of relying on domain authority alone. work down from the top of the overlap column and stop when the reasons start feeling thin, because a pitch you cannot justify in one sentence is not worth sending.&lt;/p&gt;

&lt;p&gt;one caveat worth stating plainly. common crawl is a sample of the web, not a census, so treat these counts as the shape of a link profile rather than an exact total. this study measures the Apr-Jun 2026 release, so rerun the comparison against later releases before treating any movement as a trend.&lt;/p&gt;

&lt;h3&gt;
  
  
  find the overlap in your market
&lt;/h3&gt;

&lt;p&gt;run a backlink gap analysis on the same common crawl data used for this study.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://crawlgraph.com/gap-analysis" rel="noopener noreferrer"&gt;run a free gap analysis&lt;/a&gt;&lt;/p&gt;

</description>
      <category>analysis</category>
      <category>data</category>
      <category>seo</category>
    </item>
    <item>
      <title>13 domains link to every major no-code tool</title>
      <dc:creator>Petteri Pucilowski</dc:creator>
      <pubDate>Fri, 07 Aug 2026 10:33:32 +0000</pubDate>
      <link>https://dev.to/petteri_pucilowski_7ec755/13-domains-link-to-every-major-no-code-tool-5136</link>
      <guid>https://dev.to/petteri_pucilowski_7ec755/13-domains-link-to-every-major-no-code-tool-5136</guid>
      <description>&lt;p&gt;we compared 8 no-code tools in common crawl's domain-level link graph. 13 non-platform domains link to all 8, while 76% of linking domains appear for only one subject.&lt;/p&gt;

&lt;h2&gt;
  
  
  how we pulled this
&lt;/h2&gt;

&lt;p&gt;for each subject domain, we pulled the top 2,000 referring domains by authority from common crawl release Apr-Jun 2026 (cc-main-2026-apr-may-jun). we intersected those lists, then removed platform, cdn, social, and other non-editorial domains from the clean overlap counts.&lt;/p&gt;

&lt;p&gt;the filter matters. hosting providers, cdns, url shorteners and the big social networks link to almost everything, so leaving them in would produce a universal list that is technically correct and useless for outreach. the counts below are after that filter unless a column says otherwise. everything here comes from the open common crawl webgraph, so you can reproduce it without a paid backlink tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;subject domains&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;domain&lt;/th&gt;
&lt;th&gt;referring domains&lt;/th&gt;
&lt;th&gt;cg authority&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bubble.io&lt;/td&gt;
&lt;td&gt;14,655&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;webflow.com&lt;/td&gt;
&lt;td&gt;72,800&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;softr.io&lt;/td&gt;
&lt;td&gt;1,782&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;glideapps.com&lt;/td&gt;
&lt;td&gt;2,074&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;adalo.com&lt;/td&gt;
&lt;td&gt;1,889&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flutterflow.io&lt;/td&gt;
&lt;td&gt;1,182&lt;/td&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zapier.com&lt;/td&gt;
&lt;td&gt;45,819&lt;/td&gt;
&lt;td&gt;68&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;make.com&lt;/td&gt;
&lt;td&gt;12,686&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  the overlap
&lt;/h2&gt;

&lt;p&gt;across the 8 subjects, we found 10,566 unique linking domains. the clean universal set contains 13 domains.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;metric&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;unique linking domains&lt;/td&gt;
&lt;td&gt;10,566&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to one subject&lt;/td&gt;
&lt;td&gt;76%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to all 8&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;overlap distribution&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;overlap&lt;/th&gt;
&lt;th&gt;all domains&lt;/th&gt;
&lt;th&gt;non-platform&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;link to all 8&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to 7&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to 6&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to 5&lt;/td&gt;
&lt;td&gt;97&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to 4&lt;/td&gt;
&lt;td&gt;258&lt;/td&gt;
&lt;td&gt;251&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to 3&lt;/td&gt;
&lt;td&gt;586&lt;/td&gt;
&lt;td&gt;583&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to 2&lt;/td&gt;
&lt;td&gt;1,519&lt;/td&gt;
&lt;td&gt;1,514&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;link to just 1&lt;/td&gt;
&lt;td&gt;8,036&lt;/td&gt;
&lt;td&gt;8,030&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;the distribution is the interesting part. most linking domains sit in the bottom row: they mention one product and never come back. the rows above it are where outreach lives, because a site that already covers several products in a category has an editorial reason to cover another one. a short universal list usually means the category is covered by a handful of directories, review sites and integration hubs rather than by a broad press base. a long one means the category has real editorial coverage, and the same sites are reachable for you.&lt;/p&gt;

&lt;p&gt;the highest-authority sites linking to all of them are substack.com, jotform.com and airtable.com. those are the pages worth reading before you pitch anything, because they already decided this category is worth covering.&lt;/p&gt;

&lt;p&gt;webflow.com carries the widest referring-domain profile of the set and flutterflow.io the narrowest, which is usually a function of age and press coverage rather than product quality. the gap between them is the practical opportunity: sites that cover the widest profile but not the narrowest are demonstrably willing to write about this category.&lt;/p&gt;

&lt;h2&gt;
  
  
  the category linkers
&lt;/h2&gt;

&lt;p&gt;64 non-platform domains link to at least 6 of the 8 subjects. these repeat linkers are the sites most consistently connected to no-code tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;highest-overlap category linkers&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;domain&lt;/th&gt;
&lt;th&gt;subjects&lt;/th&gt;
&lt;th&gt;cg authority&lt;/th&gt;
&lt;th&gt;links to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;substack.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;td&gt;bubble.io, webflow.com, softr.io, glideapps.com, adalo.com, flutterflow.io, zapier.com, make.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jotform.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;65&lt;/td&gt;
&lt;td&gt;bubble.io, webflow.com, softr.io, glideapps.com, adalo.com, flutterflow.io, zapier.com, make.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;airtable.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;bubble.io, webflow.com, softr.io, glideapps.com, adalo.com, flutterflow.io, zapier.com, make.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dev.to&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;td&gt;bubble.io, webflow.com, softr.io, glideapps.com, adalo.com, flutterflow.io, zapier.com, make.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;coda.io&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;59&lt;/td&gt;
&lt;td&gt;bubble.io, webflow.com, softr.io, glideapps.com, adalo.com, flutterflow.io, zapier.com, make.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;explodingtopics.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;59&lt;/td&gt;
&lt;td&gt;bubble.io, webflow.com, softr.io, glideapps.com, adalo.com, flutterflow.io, zapier.com, make.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;framer.website&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;bubble.io, webflow.com, softr.io, glideapps.com, adalo.com, flutterflow.io, zapier.com, make.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lovable.dev&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;td&gt;bubble.io, webflow.com, softr.io, glideapps.com, adalo.com, flutterflow.io, zapier.com, make.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;taskade.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;td&gt;bubble.io, webflow.com, softr.io, glideapps.com, adalo.com, flutterflow.io, zapier.com, make.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;francenum.gouv.fr&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;bubble.io, webflow.com, softr.io, glideapps.com, adalo.com, flutterflow.io, zapier.com, make.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zite.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;bubble.io, webflow.com, softr.io, glideapps.com, adalo.com, flutterflow.io, zapier.com, make.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;webcatalog.io&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;bubble.io, webflow.com, softr.io, glideapps.com, adalo.com, flutterflow.io, zapier.com, make.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;entireweb.com&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;bubble.io, webflow.com, softr.io, glideapps.com, adalo.com, flutterflow.io, zapier.com, make.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;notion.site&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;bubble.io, webflow.com, softr.io, glideapps.com, adalo.com, zapier.com, make.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;habr.com&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;td&gt;bubble.io, webflow.com, softr.io, glideapps.com, flutterflow.io, zapier.com, make.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;webflow.io&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;td&gt;bubble.io, webflow.com, softr.io, glideapps.com, flutterflow.io, zapier.com, make.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;monday.com&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;td&gt;bubble.io, webflow.com, softr.io, glideapps.com, adalo.com, zapier.com, make.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;formstack.com&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;59&lt;/td&gt;
&lt;td&gt;bubble.io, webflow.com, glideapps.com, adalo.com, flutterflow.io, zapier.com, make.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;clickup.com&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;59&lt;/td&gt;
&lt;td&gt;bubble.io, webflow.com, softr.io, glideapps.com, adalo.com, zapier.com, make.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;beehiiv.com&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;td&gt;bubble.io, webflow.com, softr.io, glideapps.com, flutterflow.io, zapier.com, make.com&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  the full data
&lt;/h3&gt;

&lt;p&gt;download all 64 clean domains that link to at least 6 of the 8 subjects, ranked by overlap and authority.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://crawlgraph.com/api/studies/who-links-to-every-no-code-tool/dataset.csv" rel="noopener noreferrer"&gt;download csv&lt;/a&gt; | &lt;a href="https://crawlgraph.com/api/studies/who-links-to-every-no-code-tool/dataset.json" rel="noopener noreferrer"&gt;download json&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  run this for your own niche
&lt;/h2&gt;

&lt;p&gt;pick your closest competitors, compare their referring domains, and keep the sites that link to several of them but not to you. sorting that gap by overlap and authority turns the same method into a specific outreach list.&lt;/p&gt;

&lt;p&gt;the ordering is what makes it usable. a site that links to most of your competitors and not to you is a gap with an obvious pitch. the observed overlap gives you a specific reason to contact each site instead of relying on domain authority alone. work down from the top of the overlap column and stop when the reasons start feeling thin, because a pitch you cannot justify in one sentence is not worth sending.&lt;/p&gt;

&lt;p&gt;one caveat worth stating plainly. common crawl is a sample of the web, not a census, so treat these counts as the shape of a link profile rather than an exact total. this study measures the Apr-Jun 2026 release, so rerun the comparison against later releases before treating any movement as a trend.&lt;/p&gt;

&lt;h3&gt;
  
  
  find the overlap in your market
&lt;/h3&gt;

&lt;p&gt;run a backlink gap analysis on the same common crawl data used for this study.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://crawlgraph.com/gap-analysis" rel="noopener noreferrer"&gt;run a free gap analysis&lt;/a&gt;&lt;/p&gt;

</description>
      <category>analysis</category>
      <category>data</category>
      <category>seo</category>
    </item>
    <item>
      <title>8 domains link to every major fintech app (Common Crawl analysis)</title>
      <dc:creator>Petteri Pucilowski</dc:creator>
      <pubDate>Mon, 06 Jul 2026 08:39:36 +0000</pubDate>
      <link>https://dev.to/petteri_pucilowski_7ec755/8-domains-link-to-every-major-fintech-app-common-crawl-analysis-lo5</link>
      <guid>https://dev.to/petteri_pucilowski_7ec755/8-domains-link-to-every-major-fintech-app-common-crawl-analysis-lo5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update 2026-08-12:&lt;/strong&gt; this post originally treated all eight as one kind of link. They are not. Five of the eight are publishing platforms, which cuts the actionable list from eight to three. Original analysis unchanged below, correction in Update: three categories, not two.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fourth niche, same experiment. Take a category's biggest players, pull every domain that links to them from Common Crawl's open hyperlink graph, keep the ones that link to the whole set. SEO tools gave 37 universal linkers (media). CRMs gave 2 (integration hubs). AI agent frameworks gave 5 (dev platforms). Fintech just gave 8 - and they are the most consistent set yet.&lt;/p&gt;

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

&lt;p&gt;Eight fintech apps across payments, banking, and spend: Stripe, Wise, Revolut, Plaid, Brex, Ramp, Mercury, Chime. For each, referring domains ranked by authority from the Common Crawl webgraph (120M domains, 4.4B edges), then intersect the eight lists. Stripe is its own planet - 100,000+ referring domains, more than the other seven combined.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 8 that link to all 8
&lt;/h2&gt;

&lt;p&gt;substack.com, entrepreneur.com, libsyn.com (podcast host), beehiiv.com, pymnts.com, thefinancialbrand.com, spocket.co, coda.io.&lt;/p&gt;

&lt;p&gt;Two newsletter platforms, a podcast host, a business magazine, and two dedicated fintech trade publications. Widen to the 7-of-8 club and the pattern gets sharp: github, crunchbase, ycombinator, bvp (Bessemer), contrary, builtin, bankrate, bankingdive, finovate, 11fs. Finance trade press plus startup/VC infrastructure. Almost nothing else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update: three categories, not two (2026-08-12)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/mihirkanzariya"&gt;@mihirkanzariya&lt;/a&gt; pointed out in the comments that these eight are not one kind of thing, and he is right.&lt;/p&gt;

&lt;p&gt;Five of them are UGC platform domains: &lt;strong&gt;substack.com&lt;/strong&gt;, &lt;strong&gt;beehiiv.com&lt;/strong&gt;, &lt;strong&gt;libsyn.com&lt;/strong&gt;, &lt;strong&gt;coda.io&lt;/strong&gt; and &lt;strong&gt;spocket.co&lt;/strong&gt;. A link from those is not an editorial decision by the platform. It is one person publishing a newsletter, a podcast episode or a public doc that happens to mention the app. You cannot pitch substack.com and you cannot fill in a profile on beehiiv.com. That link shows up only because somebody already wrote about you.&lt;/p&gt;

&lt;p&gt;So the universal set splits three ways rather than reading as one list:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Domain&lt;/th&gt;
&lt;th&gt;cg authority&lt;/th&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;entrepreneur.com&lt;/td&gt;
&lt;td&gt;66&lt;/td&gt;
&lt;td&gt;press, pitchable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pymnts.com&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;td&gt;trade press, pitchable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;thefinancialbrand.com&lt;/td&gt;
&lt;td&gt;54&lt;/td&gt;
&lt;td&gt;trade press, pitchable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;substack.com&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;td&gt;platform, earned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;libsyn.com&lt;/td&gt;
&lt;td&gt;59&lt;/td&gt;
&lt;td&gt;platform, earned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;beehiiv.com&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;td&gt;platform, earned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;coda.io&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;td&gt;platform, earned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spocket.co&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;td&gt;platform, earned&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The list you can actually act on is three domains, not eight. The full study's overlap pyramid has a "non-platform" column, and for these five it is wrong: that filter stripped the obvious social platforms and stopped there, when it should have stripped the publishing platforms too. Leaving it as published rather than quietly rewriting it.&lt;/p&gt;

&lt;p&gt;The more useful half of the correction is what the third category is &lt;em&gt;for&lt;/em&gt;. The pitch list and the profile list are things you go and do. The platform cluster is what happens after those work: if Substack and beehiiv start linking to you, the coverage you earned got read by people who then went and wrote their own thing. That makes it the best available proxy for whether the first two are landing, and it is the one part of the set you cannot buy.&lt;/p&gt;

&lt;p&gt;It also means the effect scales with how much a category gets written about, which puts an asterisk on the table below. SEO tools gave 37 universal linkers, and some of that is likely the same inflation rather than genuinely broader coverage. Re-running the earlier studies with platform domains split out is on the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four niches, four shapes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Link to all 8&lt;/th&gt;
&lt;th&gt;Who they are&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SEO tools&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;trade media + roundups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CRMs&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;integration hubs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI agent frameworks&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;dev platforms + directories&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fintech apps&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;finance press + VC databases&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The takeaway for anyone building in fintech: you do not earn links with a payments integration. You earn them by being &lt;em&gt;covered&lt;/em&gt; (get into PYMNTS, The Financial Brand, Banking Dive, Tearsheet) and by being &lt;em&gt;tracked as a company&lt;/em&gt; (complete Crunchbase / Sacra / BuiltIn profiles, land on VC portfolio pages). Mercury has 1,689 referring domains vs Stripe's 100,000+, yet both are reached by the same ten publications and three databases. You don't out-link Stripe; you get on the same shelf.&lt;/p&gt;

&lt;p&gt;Full study with the overlap pyramid and downloadable CSV/JSON: &lt;a href="https://crawlgraph.com/blog/who-links-to-every-fintech-app" rel="noopener noreferrer"&gt;8 domains link to every major fintech app&lt;/a&gt;. Method is category-agnostic - it's a &lt;a href="https://crawlgraph.com/gap-analysis" rel="noopener noreferrer"&gt;backlink gap analysis&lt;/a&gt; on Common Crawl data, free.&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>data</category>
      <category>seo</category>
      <category>startup</category>
    </item>
    <item>
      <title>Only 5 domains link to every major AI agent framework (Common Crawl analysis)</title>
      <dc:creator>Petteri Pucilowski</dc:creator>
      <pubDate>Fri, 03 Jul 2026 15:50:16 +0000</pubDate>
      <link>https://dev.to/petteri_pucilowski_7ec755/only-5-domains-link-to-every-major-ai-agent-framework-common-crawl-analysis-1e6a</link>
      <guid>https://dev.to/petteri_pucilowski_7ec755/only-5-domains-link-to-every-major-ai-agent-framework-common-crawl-analysis-1e6a</guid>
      <description>&lt;p&gt;Same experiment, third niche. Take a category's biggest players, pull every domain that links to them from Common Crawl's open hyperlink graph, and keep the ones that link to the whole set. SEO tools gave 37 universal linkers (nearly all media). CRMs gave 2 (both integration hubs). AI agent frameworks just gave 5 - and they are a different animal again.&lt;/p&gt;

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

&lt;p&gt;Eight frameworks: LangChain, LlamaIndex, CrewAI, Dify, Flowise, Langflow, AutoGPT, SuperAGI. For each, we pulled referring domains ranked by authority from the Common Crawl webgraph (120M domains, 4.4B links), then intersected the eight lists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;7,508&lt;/strong&gt; unique domains link to at least one framework&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;77%&lt;/strong&gt; link to exactly one (usually LangChain: 5,949 referring domains vs ~450 for Langflow and AutoGPT)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5&lt;/strong&gt; link to all eight: github.com, substack.com, analyticsvidhya.com, and two product sites&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of the universal linkers are tech press.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the 7-and-6 club looks like
&lt;/h2&gt;

&lt;p&gt;dev.to (yes, this site), qiita.com, zenn.dev, n8n.io, arize.com, zenml.io, langfuse.com... and then a whole crop of AI agent directories that did not exist two years ago: aiagentsdirectory.com, agenthunter.io, everydev.ai, findmyagentai.com, bestaiagents.ai. Most have single-digit-to-low authority scores. The category's shelf is still being built.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway for anyone shipping agent tooling
&lt;/h2&gt;

&lt;p&gt;The niche's link graph is developer publishing platforms plus young directories, not journalists. So the play is: publish where developers already read (GitHub, Substack, dev.to, Qiita), and get listed in the agent directories while they are young and take an email. A directory with authority 20 today is not impressive. The one that becomes the geekflare of AI agents in three years is - and you got in early.&lt;/p&gt;

&lt;p&gt;Full study with methodology, the overlap pyramid, and the downloadable CSV/JSON dataset: &lt;a href="https://crawlgraph.com/blog/who-links-to-every-ai-agent-framework" rel="noopener noreferrer"&gt;only 5 domains link to every major AI agent framework&lt;/a&gt;. The DuckDB query behind it is &lt;a href="https://gist.github.com/pucilpet/54ad13ecfd6ba3c0c54e920c24064339" rel="noopener noreferrer"&gt;a public gist&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>seo</category>
      <category>data</category>
      <category>opensource</category>
    </item>
    <item>
      <title>We shipped a free backlink API tier (15 calls/month on Common Crawl data)</title>
      <dc:creator>Petteri Pucilowski</dc:creator>
      <pubDate>Fri, 03 Jul 2026 15:49:40 +0000</pubDate>
      <link>https://dev.to/petteri_pucilowski_7ec755/we-shipped-a-free-backlink-api-tier-15-callsmonth-on-common-crawl-data-4a7</link>
      <guid>https://dev.to/petteri_pucilowski_7ec755/we-shipped-a-free-backlink-api-tier-15-callsmonth-on-common-crawl-data-4a7</guid>
      <description>&lt;p&gt;Until this week, the CrawlGraph API required the $99 lifetime license. Now there is a free tier: &lt;strong&gt;15 backlink calls a month&lt;/strong&gt; on Common Crawl's open hyperlink graph (120M domains, 4.4B links, refreshed quarterly). No card, no signup form - the key lands in your inbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two curl commands
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. get a key (delivered by email)&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://crawlgraph.com/api/v1/free-key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"email": "you@company.com"}'&lt;/span&gt;

&lt;span class="c"&gt;# 2. look up any domain's backlinks&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://crawlgraph.com/api/v1/backlinks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer cg_live_..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"domain": "example.com", "limit": 10}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Responses carry &lt;code&gt;X-RateLimit-*&lt;/code&gt; headers so you always know where you stand. One key per email - delivery by mail is the ownership check, which is why there is no signup flow to speak of.&lt;/p&gt;

&lt;h2&gt;
  
  
  It works inside your AI agent
&lt;/h2&gt;

&lt;p&gt;The same key powers the open-source &lt;a href="https://github.com/pucilpet/crawlgraph-mcp" rel="noopener noreferrer"&gt;crawlgraph-mcp server&lt;/a&gt; in Claude, Cursor, Cline, Zed, or Windsurf:&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;"mcpServers"&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;"crawlgraph"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"crawlgraph-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"CRAWLGRAPH_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cg_live_..."&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="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;Your agent can pull the backlink profile of any domain mid-conversation. The &lt;code&gt;gap_analysis&lt;/code&gt; and &lt;code&gt;gap_outreach_targets&lt;/code&gt; tools (the ones that turn "research my niche" into a ranked outreach list) stay part of the $99 lifetime tier, which also raises the quota to 1,000 calls + 50 gap analyses a month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why free
&lt;/h2&gt;

&lt;p&gt;The underlying data is open - Common Crawl publishes the hyperlink graph quarterly. Gating basic lookups behind a purchase never felt right. Try the data, build something; if you need the gap tools or more volume, the upgrade is a single payment, not a subscription.&lt;/p&gt;

&lt;p&gt;Docs: &lt;a href="https://crawlgraph.com/docs/api" rel="noopener noreferrer"&gt;crawlgraph.com/docs/api&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>seo</category>
      <category>mcp</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Find your competitor's backlinks from inside Claude Code (free, via MCP)</title>
      <dc:creator>Petteri Pucilowski</dc:creator>
      <pubDate>Mon, 01 Jun 2026 19:06:11 +0000</pubDate>
      <link>https://dev.to/petteri_pucilowski_7ec755/find-your-competitors-backlinks-from-inside-claude-code-free-via-mcp-1a63</link>
      <guid>https://dev.to/petteri_pucilowski_7ec755/find-your-competitors-backlinks-from-inside-claude-code-free-via-mcp-1a63</guid>
      <description>&lt;p&gt;Backlink prospecting is a tab-hopping chore: dashboard → export CSV → eyeball → copy domains → switch to email. It's a filter-and-rank problem, which is exactly the kind of thing an agent should do for you. So here's how to do the whole thing from inside Claude Code (or Cursor / Cline / Zed / Windsurf) with one prompt, using a free MCP server.&lt;/p&gt;

&lt;p&gt;No affiliation required to follow along — the data is the public &lt;a href="https://commoncrawl.org" rel="noopener noreferrer"&gt;Common Crawl&lt;/a&gt; webgraph, and the MCP wrapper is open source.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we're building toward
&lt;/h2&gt;

&lt;p&gt;By the end you'll be able to type this into your agent and get back a ranked, de-noised list:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Find the sites linking to competitor-a.com, competitor-b.com and competitor-c.com but not to my-site.com, and draft a short outreach email to each."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent runs a competitor gap analysis, filters to the highest-value targets, and writes the emails — without you opening a single SEO dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: install the MCP server
&lt;/h2&gt;

&lt;p&gt;The server is &lt;a href="https://github.com/pucilpet/crawlgraph-mcp" rel="noopener noreferrer"&gt;&lt;code&gt;crawlgraph-mcp&lt;/code&gt;&lt;/a&gt; on npm. It's a thin TypeScript stdio wrapper over a backlink API; nothing to clone or build.&lt;/p&gt;

&lt;p&gt;Add it to your MCP config (&lt;code&gt;claude_desktop_config.json&lt;/code&gt;, or &lt;code&gt;.mcp.json&lt;/code&gt; for Claude Code):&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;"mcpServers"&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;"crawlgraph"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"crawlgraph-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"CRAWLGRAPH_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cg_live_..."&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="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;Grab the &lt;code&gt;cg_live_&lt;/code&gt; key from your account page, restart the client, and you'll see four tools appear: &lt;code&gt;backlinks&lt;/code&gt;, &lt;code&gt;gap_analysis&lt;/code&gt;, &lt;code&gt;gap_outreach_targets&lt;/code&gt;, and &lt;code&gt;releases&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: sanity-check the data
&lt;/h2&gt;

&lt;p&gt;Before trusting it for outreach, point it at a domain you know. In your agent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Use the backlinks tool on stripe.com, limit 5, sorted by authority."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You'll get the top referring domains plus the target's own authority score:&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;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stripe.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;"total_linking_domains"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cg_authority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;78&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"results"&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;"linking_domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"github.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;"num_hosts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cg_authority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;96&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;"linking_domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"news.ycombinator.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;"num_hosts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;880&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cg_authority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;94&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;That &lt;code&gt;total_linking_domains&lt;/code&gt; count is the quick gut-check: compare it to whatever your current tool reports for the same domain. If it's in the same ballpark, the graph is complete enough to prospect from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: the actual play — gap analysis
&lt;/h2&gt;

&lt;p&gt;The primitive is &lt;code&gt;gap_analysis&lt;/code&gt;: give it your domain plus 2-5 competitors, and it returns every domain linking to at least one competitor but not to you, each tagged with which competitors it links to (&lt;code&gt;found_on&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The raw output is the &lt;em&gt;material&lt;/em&gt;, not the answer — you don't want a 1,000-row dump, you want the few dozen worth emailing. That's what &lt;code&gt;gap_outreach_targets&lt;/code&gt; does on top:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;keeps only domains in the gap that link to &lt;strong&gt;all&lt;/strong&gt; your competitors (a site linking to all three covers your whole niche and just hasn't heard of you — the warmest possible target);&lt;/li&gt;
&lt;li&gt;strips platform/CDN noise (amazonaws, github.io, facebook, shorteners);&lt;/li&gt;
&lt;li&gt;ranks the survivors by authority.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So you skip straight to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Run gap_outreach_targets for my-site.com against competitor-a.com, competitor-b.com and competitor-c.com."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and get back something like:&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;"priority_targets"&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;"linking_domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"industry-roundup.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;"found_on"&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="s2"&gt;"competitor-a.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"competitor-b.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"competitor-c.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;"cg_authority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;73&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;"linking_domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"niche-review.io"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"found_on"&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="s2"&gt;"competitor-a.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"competitor-b.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"competitor-c.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;"cg_authority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;61&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;"secondary_targets"&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="err"&gt;/*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;link&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&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="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"platforms_filtered"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;26&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;h2&gt;
  
  
  Step 4: let the agent write the outreach
&lt;/h2&gt;

&lt;p&gt;This is where doing it in an agent beats a dashboard. The list is already in context, so:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"For each priority target, draft a 3-sentence outreach email: reference the kind of content they link to in my niche, and pitch my-site.com as a fit. Keep it specific, no templates."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You get first-draft emails per target in the same session. You still edit them in your voice — cold outreach that goes out unedited gets ignored — but the gather → filter → rank → draft chain that used to be an afternoon is now one conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why 2-3 competitors, not one
&lt;/h2&gt;

&lt;p&gt;A site linking to one competitor might be a fluke or a paid placement. A site linking to three of your competitors is a publisher who covers your category. That overlap is the qualifier — it's the difference between a prospect list and a noise list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quarterly snapshot.&lt;/strong&gt; Common Crawl publishes ~4×/year, so this is for one-off prospecting, not live link monitoring. For "what changed this week," use a continuous-crawl tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No anchor text&lt;/strong&gt; in the gap output (the webgraph is source→destination edges).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quotas:&lt;/strong&gt; the free path covers light use; heavier use needs the lifetime tier (1,000 backlink calls + 50 gap jobs/month).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this pattern matters beyond backlinks
&lt;/h2&gt;

&lt;p&gt;The interesting bit isn't the SEO — it's that a well-scoped MCP tool can encode a &lt;em&gt;workflow&lt;/em&gt;, not just an API call. &lt;code&gt;gap_outreach_targets&lt;/code&gt; doesn't mirror an endpoint; it does the filtering-and-ranking judgment you'd otherwise re-explain to the model every time. If you're building MCP servers, that's the lever: ship the composite tool that returns a decision-ready answer, not just the raw rows.&lt;/p&gt;

&lt;p&gt;Server's MIT and on &lt;a href="https://github.com/pucilpet/crawlgraph-mcp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; (&lt;code&gt;npx -y crawlgraph-mcp&lt;/code&gt;). If you try the gap play, I'd genuinely like to hear whether the priority/secondary split matches what you'd have picked by hand.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>seo</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I wrapped a backlink API in an MCP server so I could do SEO gap analysis from inside Claude</title>
      <dc:creator>Petteri Pucilowski</dc:creator>
      <pubDate>Sun, 31 May 2026 21:44:12 +0000</pubDate>
      <link>https://dev.to/petteri_pucilowski_7ec755/i-wrapped-a-backlink-api-in-an-mcp-server-so-i-could-do-seo-gap-analysis-from-inside-claude-13mk</link>
      <guid>https://dev.to/petteri_pucilowski_7ec755/i-wrapped-a-backlink-api-in-an-mcp-server-so-i-could-do-seo-gap-analysis-from-inside-claude-13mk</guid>
      <description>&lt;p&gt;I do a fair amount of competitor backlink research, and the workflow always annoyed me: open a dashboard, run a query, export a CSV, eyeball it, copy domains into a doc, switch to email. Lots of tab-hopping for what is fundamentally a data-filtering problem an agent should handle.&lt;/p&gt;

&lt;p&gt;So I wrapped the backlink API I'd been using into an &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; server. Now I stay in Claude Code (or Cursor, Cline, Zed, Windsurf) and just describe the goal. This is the build: the architecture, the four tools, and the one design decision I'm still not sure about.&lt;/p&gt;

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

&lt;p&gt;The server runs on the &lt;a href="https://commoncrawl.org" rel="noopener noreferrer"&gt;Common Crawl&lt;/a&gt; hyperlink webgraph — about 4.4 billion edges across 120 million domains, published quarterly as Parquet. That matters for an MCP tool specifically: the data is open, so there's no scraped-proprietary-index liability in handing it to an agent, and the same query is reproducible by anyone.&lt;/p&gt;

&lt;p&gt;The HTTP API in front of it (&lt;a href="https://crawlgraph.com" rel="noopener noreferrer"&gt;CrawlGraph&lt;/a&gt;) does the heavy DuckDB work; the MCP server is a thin TypeScript stdio client over it. Keeping the server thin was deliberate — all the query cost, caching, and quota logic lives server-side, so the MCP package stays a ~300-line wrapper that's easy to audit before you hand it your API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four tools
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;backlinks            → referring domains for a target, with authority scores
gap_analysis         → domains linking to your competitors but not to you
gap_outreach_targets → the composite play (below)
releases             → list the Common Crawl snapshots
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;backlinks&lt;/code&gt; and &lt;code&gt;gap_analysis&lt;/code&gt; map 1:1 to API endpoints. &lt;code&gt;gap_analysis&lt;/code&gt; is the interesting primitive: submit your domain plus 2-5 competitors, and it returns every domain that links to at least one competitor but not to you, each tagged with a &lt;code&gt;found_on&lt;/code&gt; array listing &lt;em&gt;which&lt;/em&gt; competitors it links to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The composite tool, and the decision I'm unsure about
&lt;/h2&gt;

&lt;p&gt;Most API-wrapper MCP servers are pure 1:1 mappings. I added one opinionated composite tool, &lt;code&gt;gap_outreach_targets&lt;/code&gt;, because the raw gap output isn't the thing you actually want — it's the raw material for the thing you want.&lt;/p&gt;

&lt;p&gt;What it does on top of &lt;code&gt;gap_analysis&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Filters to total overlap.&lt;/strong&gt; Keep only domains whose &lt;code&gt;found_on&lt;/code&gt; covers &lt;em&gt;every&lt;/em&gt; competitor you listed. A site linking to one competitor might be a fluke or a paid placement. A site linking to all three is a publisher who covers your whole niche and has simply never heard of you. That overlap is the qualifier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strips platform noise.&lt;/strong&gt; &lt;code&gt;amazonaws.com&lt;/code&gt;, &lt;code&gt;github.io&lt;/code&gt;, &lt;code&gt;facebook.com&lt;/code&gt;, CDNs, URL shorteners — they show up in every backlink profile and are never outreach targets. There's a denylist with suffix matching so subdomains get caught too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ranks by authority.&lt;/strong&gt; For the top N survivors it makes a cheap per-domain authority lookup and sorts, so the highest-value warm targets surface first. This is opt-out (&lt;code&gt;enrich_top: 0&lt;/code&gt;) because each lookup costs one API call against quota.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// the core filter, roughly&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gaps&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isPlatformNoise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;linking_domain&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;found_on&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;competitors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cg_authority&lt;/span&gt; &lt;span class="o"&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="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cg_authority&lt;/span&gt; &lt;span class="o"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The decision I keep going back and forth on:&lt;/strong&gt; is a composite, opinionated tool the right call for an MCP server, or should it stay a pure API mirror and let the agent do the filtering/ranking in its own reasoning?&lt;/p&gt;

&lt;p&gt;Arguments for the composite tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It encodes a workflow the model would otherwise have to reconstruct each time, costing tokens and inviting mistakes (I watched an agent forget to filter platforms more than once).&lt;/li&gt;
&lt;li&gt;It returns a small, ranked, decision-ready list instead of a 1,000-row dump the model has to chew through.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Arguments against:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's a leaky abstraction. The moment someone wants a slightly different filter (2-of-3 overlap, a different noise list), they're fighting my opinion instead of composing primitives.&lt;/li&gt;
&lt;li&gt;It hides the platform denylist, which is a judgment call that should arguably be visible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I landed on "ship both" — the raw &lt;code&gt;gap_analysis&lt;/code&gt; primitive &lt;em&gt;and&lt;/em&gt; the composite — but I'm genuinely unsure that's not just indecision dressed up as flexibility. If you've built MCP servers, I'd like to hear where you draw the primitive-vs-composite line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it
&lt;/h2&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;"mcpServers"&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;"crawlgraph"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"crawlgraph-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"CRAWLGRAPH_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cg_live_..."&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="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;Then the whole workflow collapses to one sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Use gap_outreach_targets for mydomain.com against competitor-a.com and competitor-b.com, then draft a short outreach email to each priority target."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent submits the gap job, polls it, filters and ranks, and writes the emails — all in one turn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quarterly snapshot.&lt;/strong&gt; Common Crawl publishes ~4x/year, so this is for one-off prospecting, not live link monitoring. If you need "what changed this week," it's the wrong tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No anchor text in the gap result.&lt;/strong&gt; The webgraph is &lt;code&gt;(src, dst)&lt;/code&gt; edges; anchor text needs a separate WARC pass I didn't wire into the MCP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authority enrichment costs calls.&lt;/strong&gt; Each scored domain is one API call, hence the cap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Code is MIT, on &lt;a href="https://github.com/pucilpet/crawlgraph-mcp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and npm (&lt;code&gt;npx -y crawlgraph-mcp&lt;/code&gt;). Feedback on the composite-tool question especially welcome — it's the part of the design I'm least settled on.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>seo</category>
      <category>typescript</category>
    </item>
    <item>
      <title>How I Built a Free Backlink Intelligence Tool on Common Crawl + DuckDB</title>
      <dc:creator>Petteri Pucilowski</dc:creator>
      <pubDate>Fri, 29 May 2026 09:32:31 +0000</pubDate>
      <link>https://dev.to/petteri_pucilowski_7ec755/how-i-built-a-free-backlink-intelligence-tool-on-common-crawl-duckdb-367h</link>
      <guid>https://dev.to/petteri_pucilowski_7ec755/how-i-built-a-free-backlink-intelligence-tool-on-common-crawl-duckdb-367h</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Backlink data is a $1.5B/year SaaS category. Ahrefs is $129/month, SEMrush is $140/month, Moz is $99/month. As an indie I needed competitor backlinks for outreach — the prospecting half of what these tools do — but I wasn't going to pay $1,548/year just for a quarterly list of domains.&lt;/p&gt;

&lt;p&gt;Turns out the data is already public. &lt;a href="https://commoncrawl.org" rel="noopener noreferrer"&gt;Common Crawl&lt;/a&gt; publishes a hyperlink graph every ~3 months containing every public link they discover. The latest release I pulled has &lt;strong&gt;4.4 billion edges across 120 million domains&lt;/strong&gt; — comparable to the size of Ahrefs' index, just refreshed quarterly instead of continuously.&lt;/p&gt;

&lt;p&gt;This is a walkthrough of the actual stack I used to turn that public dataset into a queryable backlink lookup. Total infra cost: about $40/month for a small VPS.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data: Common Crawl's webgraph
&lt;/h2&gt;

&lt;p&gt;Common Crawl publishes the webgraph as Parquet on S3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s3://commoncrawl/cc-webgraph/
  cc-main-2026-jan-feb-mar/
    vertices/  # domain registry, with reverse-DNS string keys
    edges/     # (src_id, dst_id) tuples
    ranks/     # PageRank-equivalent + harmonic centrality per vertex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full edges table is ~120GB compressed Parquet across a few hundred files. The vertices table is ~3GB. Both are accessible without authentication — Common Crawl publishes everything as open data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The query engine: DuckDB over Parquet
&lt;/h2&gt;

&lt;p&gt;The trick that makes the whole thing economical: &lt;strong&gt;DuckDB can query Parquet files directly from S3 via &lt;code&gt;httpfs&lt;/code&gt;&lt;/strong&gt; without downloading them. For a backlink lookup ("show me every domain linking to stripe.com"), you don't need the full graph in memory — you need columnar scans for the specific domain ID.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Install the httpfs extension once&lt;/span&gt;
&lt;span class="n"&gt;INSTALL&lt;/span&gt; &lt;span class="n"&gt;httpfs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;LOAD&lt;/span&gt; &lt;span class="n"&gt;httpfs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Resolve "stripe.com" to its vertex ID&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;vertex_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://data.commoncrawl.org/cc-webgraph/.../vertices/*.parquet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rev_domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'com.stripe'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- (Common Crawl stores domains reverse-DNS: stripe.com → com.stripe)&lt;/span&gt;

&lt;span class="c1"&gt;-- Pull every incoming edge&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rev_domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ranks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cg_authority&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://.../edges/*.parquet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://.../vertices/*.parquet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;src_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vertex_id&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://.../ranks/*.parquet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ranks&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;src_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ranks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vertex_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dst_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;stripe&lt;/span&gt;&lt;span class="s1"&gt;'s vertex_id&amp;gt;
ORDER BY ranks.cg_authority DESC
LIMIT 1000;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DuckDB plans the scan, fetches only the relevant Parquet row groups via HTTP range requests, and returns in 2-30 seconds depending on how popular the target domain is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cache layer: SQLite
&lt;/h2&gt;

&lt;p&gt;DuckDB-on-S3 is fast enough for ad-hoc queries but you don't want every user hitting Common Crawl's S3 bucket. I added a SQLite cache keyed on &lt;code&gt;(domain, release_id)&lt;/code&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_backlinks&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="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="n"&gt;Backlink&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;cache_key&lt;/span&gt; &lt;span class="o"&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="nf"&gt;current_release_id&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_one&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 result FROM cache WHERE key = ?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache_key&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;cached&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;is_expired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&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;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&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="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;duckdb_query_s3&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;sqlite_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO cache (key, result, expires_at) VALUES (?, ?, ?)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;cache_key&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="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&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;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;About 90% of traffic hits the cache after the first month. The DuckDB-on-S3 path stays as the cold-start handler.&lt;/p&gt;

&lt;h2&gt;
  
  
  The frontend: Next.js with SSR
&lt;/h2&gt;

&lt;p&gt;The user-facing tool is a Next.js 14 app with App Router. The domain pages (&lt;code&gt;/backlinks/&amp;lt;domain&amp;gt;&lt;/code&gt;) are server-rendered so they're indexable by Google — which matters because the long-tail SEO traffic ("backlinks for [specific competitor]") is a real channel.&lt;/p&gt;

&lt;p&gt;Per-domain pages include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The referring-domain list with their authority scores&lt;/li&gt;
&lt;li&gt;TLD breakdown (visual)&lt;/li&gt;
&lt;li&gt;Anchor text distribution (when available from the WARC, separate pipeline)&lt;/li&gt;
&lt;li&gt;CSV/JSON export&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The frontend talks to a thin FastAPI backend that owns the SQLite cache and the DuckDB connection pool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations (what this doesn't replace)
&lt;/h2&gt;

&lt;p&gt;Common Crawl is a &lt;strong&gt;quarterly&lt;/strong&gt; snapshot. The current release I'm querying is from January-March 2026, so:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No live signal monitoring.&lt;/strong&gt; When a new high-authority site links you on Thursday, Ahrefs surfaces it by Friday. I won't see it until the next Common Crawl release ingests (~3 months later).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No anchor-text-velocity tracking.&lt;/strong&gt; Same reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No spam-filtering layer.&lt;/strong&gt; Common Crawl publishes the graph as it found it. Ahrefs runs continuous re-validation and de-duplication; I don't replicate that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So this isn't a replacement for Ahrefs if your job is live rank-impact attribution or campaign monitoring. &lt;strong&gt;It is a replacement for the outreach-prospecting use case&lt;/strong&gt; — give me a 50-domain list to pitch today — which is what indies actually need most of the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Common Crawl webgraph: free (open data)&lt;/li&gt;
&lt;li&gt;DuckDB: free&lt;/li&gt;
&lt;li&gt;SQLite: free&lt;/li&gt;
&lt;li&gt;VPS hosting (small Hetzner box): ~$40/month&lt;/li&gt;
&lt;li&gt;Next.js on Vercel: included in hobby tier for now&lt;/li&gt;
&lt;li&gt;Domain + email: ~$15/month&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total: under $60/month to run a tool that does ~1/30th of what Ahrefs does, for free or $99 lifetime per user. The unit economics are sharp enough that I could make this a public project — which I did.&lt;/p&gt;

&lt;p&gt;You can try it at &lt;a href="https://crawlgraph.com" rel="noopener noreferrer"&gt;crawlgraph.com&lt;/a&gt; (5 queries free, no signup). Source data and methodology are documented; if you want to build your own version on the same dataset, all the SQL above is roughly the right shape. Common Crawl's &lt;a href="https://commoncrawl.org/get-started" rel="noopener noreferrer"&gt;getting started guide&lt;/a&gt; is the place to begin.&lt;/p&gt;

&lt;p&gt;Happy to answer questions about the architecture in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>duckdb</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
