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. programmatically build a competitor map Google SERPs G2 G2 Product Hunt Product Hunt 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 You capture fresh signals (new entrants, rising tools) fresh signals You avoid bias (you don’t just list big brands you already know) You can rerun the mapping monthly and spot shifts rerun the mapping monthly 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. 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. Seed keywords & product descriptions — define your domain / core value propositions. Seed keywords & product descriptions Fetch SERP results for those keywords to find competitor domains. Fetch SERP results Enrich from G2 & Product Hunt to capture tools in review ecosystems and launch hubs. Enrich from G2 & Product Hunt Cluster / embed competitors by feature / category / keyword overlap. Cluster / embed Rank & interpret: which clusters overlap your ICP, which are tangential, which are trending. Rank & interpret 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 Node.js (v16+) axios (HTTP client) axios cheerio (HTML parsing) cheerio @apollographql/client or any GraphQL client (for Product Hunt) @apollographql/client A SERP API (e.g. serpnode.com) or your own scraping logic serpnode.com (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 Note: oai_citation:0‡api.producthunt.com 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" ]; const seeds = [ "email API for developers", "invoicing automation for freelancers", "no-code analytics for SaaS" ]; Also define: const myDomain = "myproduct.io"; const maxCompetitorsPerSeed = 10; const myDomain = "myproduct.io"; const maxCompetitorsPerSeed = 10; These keywords are the basis for SERP and other lookups. Step 2: Fetch domains from SERP 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); } 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 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); } 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 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); } 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 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); } 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 }, … ] [ { "domain": "competitor1.com", "count": 3 }, { "domain": "competitor2.io", "count": 2 }, … ] Then pick your top N (say, 20) to cluster. Step 6: Clustering & similarity 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 ""; } } 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. 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 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… })(); (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 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) 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 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 A way to discover competitor domains from SERP, G2, and Product Hunt discover competitor domains A method to frequency-rank and dedupe frequency-rank and dedupe A path to cluster and interpret the results cluster and interpret 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.