When you're launching an indie SaaS, one of the first big questions is: Who are your real competitors? Not just in features, but in positioning, niches, and customer overlap (i.e. ICP overlap). In this post I’ll walk you through how to programmatically build a competitor map using Google SERPs, G2, and Product Hunt. We’ll use Node.js and a few APIs to discover, cluster, and analyze your competitive landscape — all without manual spreadsheets.

This is not purely about calling a single API; it’s about combining data sources, merging signals, and producing insights.

Why automate competitor mapping?

Manual competitor research is slow, subjective, and hard to update. By automating:

The goal is a working “competitor graph” that shows domains/tools clustered by similarity to your product / ICP.

High-level approach

  1. Seed keywords & product descriptions — define your domain / core value propositions.
  2. Fetch SERP results for those keywords to find competitor domains.
  3. Enrich from G2 & Product Hunt to capture tools in review ecosystems and launch hubs.
  4. Cluster / embed competitors by feature / category / keyword overlap.
  5. Rank & interpret: which clusters overlap your ICP, which are tangential, which are trending.

We’ll build a minimal pipeline in Node.js for steps 2–4.

Prerequisites & tools

You’ll need:

Note: Product Hunt provides a GraphQL API for its data. oai_citation:0‡api.producthunt.com. For G2, one route is to use a G2 “competitors” API (if you have access) or use a G2 scraping / data provider. oai_citation:1‡documentation.g2.com

Also, consider rate limits, caching, and ethical scraping rules.

Step 1: Seed keywords & query setup

Decide on 3–10 seed keywords or product slogans that best represent your niche. E.g.:

const seeds = [ 
"email API for developers", 
"invoicing automation for freelancers", 
"no-code analytics for SaaS" 
];

Also define:

const myDomain = "myproduct.io";
const maxCompetitorsPerSeed = 10;

These keywords are the basis for SERP and other lookups.

Step 2: Fetch domains from SERP

If you have a SERP API (like serpnode.com), you can query it and extract competitor domains:

const axios = require("axios");

async function fetchSerpDomains(query) {
  const res = await axios.get("https://api.serpnode.com/v1/search", {
    params: { q: query },
    headers: { apikey: process.env.SERPNODE_API_KEY },
  });

  const organic = res.data.result.organic_results || [];
  return organic
    .map(r => {
      try {
        const u = new URL(r.url);
        return u.hostname.replace(/^www\./, "");
      } catch {
        return null;
      }
    })
    .filter(d => d && d !== myDomain);
}

Run that for each seed term. You’ll collect a list of candidate domains.

Step 3: Enrich from Product Hunt

Product Hunt is often first-to-market for many indie tools. Use its GraphQL API to search for product posts relating to your keywords:

const { gql, ApolloClient, InMemoryCache } = require("@apollo/client");
const fetch = require("cross-fetch");

const client = new ApolloClient({
  uri: "https://api.producthunt.com/v2/api/graphql",
  cache: new InMemoryCache(),
  headers: {
    Authorization: `Bearer ${process.env.PH_API_TOKEN}`
  },
  fetch
});

async function fetchProductHuntDomains(query) {
  const Q = gql`
    query SearchPosts($q: String!) {
      posts(search: $q) {
        edges {
          node {
            name
            website
          }
        }
      }
    }
  `;
  const resp = await client.query({ query: Q, variables: { q: query } });
  return resp.data.posts.edges
    .map(e => {
      const site = e.node.website;
      if (!site) return null;
      try {
        return new URL(site).hostname.replace(/^www\./, "");
      } catch {
        return null;
      }
    })
    .filter(Boolean);
}

Combine these domains with your SERP-derived list.

Step 4: Enrich from G2

If you can access a G2 competitors API endpoint, fetch competitor domains for your product. Otherwise use a G2 scraper service or public competitor data:

async function fetchG2Competitors(myProductId) {
  const res = await axios.get(`https://api.g2.com/v1/products/${myProductId}/competitors`, {
    headers: { Authorization: `Bearer ${process.env.G2_API_TOKEN}` }
  });
  return res.data.competitors.map(c => c.domain);
}

Merge those domains into your candidate pool.

Step 5: Aggregate, dedupe, and score

Merge your domain lists (SERP, PH, G2) and count frequency:

function mergeCounts(arrays) {
  const freq = {};
  arrays.flat().forEach(domain => {
    if (!domain) return;
    freq[domain] = (freq[domain] || 0) + 1;
  });
  return Object.entries(freq)
    .map(([domain, count]) => ({ domain, count }))
    .sort((a, b) => b.count - a.count);
}

You now have something like:

[
  { "domain": "competitor1.com", "count": 3 },
  { "domain": "competitor2.io", "count": 2 },
  …
]

Then pick your top N (say, 20) to cluster.

Step 6: Clustering & similarity

Once you have top candidate domains, fetch textual content (e.g. homepage, “about” page) and vectorize or embed them to cluster.

async function fetchSiteText(domain) {
  try {
    const res = await axios.get(`https://${domain}`, { timeout: 10000 });
    return res.data;
  } catch {
    return "";
  }
}

You can then use:

After clustering, assign labels to clusters and identify which ones overlap strongly with your ICP.

Sample flow

(async () => {
  const serpLists = await Promise.all(seeds.map(q => fetchSerpDomains(q)));
  const phLists = await Promise.all(seeds.map(q => fetchProductHuntDomains(q)));
  // const g2List = await fetchG2Competitors(myG2ProductID);

  const merged = mergeCounts([...serpLists, ...phLists /*, g2List */]);
  const top = merged.slice(0, 20);

  console.log("Top competitors:", top);

  const texts = await Promise.all(top.map(o => fetchSiteText(o.domain)));
  // embed & cluster here…
})();

Interpretation & ICP overlap

Once you have clusters:

You might shape your strategy: go after underserved sub-niches, differentiate from overlapping clusters, or discover adjacency opportunities.

Wrap-up & next steps

You now have:

From here, you can build dashboards, visual graphs, alerts when a new competitor enters, or enrich domain profiles with technologies, pricing, features, etc. This pipeline saves you from manual lists and gives you a live, evolving competitor map.