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:
- You capture fresh signals (new entrants, rising tools)
- You avoid bias (you don’t just list big brands you already know)
- You can rerun the mapping monthly and spot shifts
- You feed clustering / positioning models, not just spreadsheets
The goal is a working “competitor graph” that shows domains/tools clustered by similarity to your product / ICP.
High-level approach
- Seed keywords & product descriptions — define your domain / core value propositions.
- Fetch SERP results for those keywords to find competitor domains.
- Enrich from G2 & Product Hunt to capture tools in review ecosystems and launch hubs.
- Cluster / embed competitors by feature / category / keyword overlap.
- 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:
- Node.js (v16+)
axios
(HTTP client)cheerio
(HTML parsing)@apollographql/client
or any GraphQL client (for Product Hunt)- A SERP API (e.g. serpnode.com) or your own scraping logic
- (Optionally) a G2 data API or a G2 review/competitor extraction service
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:
- TF-IDF and cosine similarity
- Or embed APIs (OpenAI, Cohere)
- Then cluster with k-means, hierarchical clustering, etc.
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:
- See which competitor domains appear across multiple seed terms
- Mark clusters with strong G2 or PH activity
- Detect “hidden” competitors you didn’t know
- Identify which clusters are closest to your ICP (by content similarity or keyword overlap)
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:
- A way to discover competitor domains from SERP, G2, and Product Hunt
- A method to frequency-rank and dedupe
- A path to cluster and interpret the results
- A lightweight sketch in Node.js to bootstrap this process
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.