<?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>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;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;
  
  
  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>
