Finds 3-5 competitors for a brand by querying SerpAPI, then DataForSEO, and finally OpenAI fallback if needed, returning names, websites, and reasons.
Identifies 3-5 competitors for a given brand by searching the web via SerpAPI and, as a last resort, falling back to a minimal OpenAI call. Returns competitor names, websites, and optionally the reason they are considered competitors. This collector feeds into the Marketing Audit Pipeline to populate the Competitor Landscape section of the final report.
// Function signature
collectCompetitors(brandName: string, domain?: string): Promise<CompetitorData>
// brandName: The brand name to find competitors for (e.g. "Gymshark")
// domain: Optional domain for additional context (e.g. "gymshark.com").
// Helps refine competitor search and filter out the brand itself from results.
interface CompetitorData {
competitors: CompetitorEntry[]; // 3-5 competitor entries
error?: string; // Present only when collector fails
}
interface CompetitorEntry {
name: string; // e.g. "Nike"
website: string; // e.g. "nike.com"
reason?: string; // e.g. "Direct competitor in activewear market"
}
https://serpapi.com/search.jsonSERPAPI_KEY environment variablehttps://api.dataforseo.com/v3/dataforseo_labs/google/competitors_domain/liveDATAFORSEO_LOGIN + DATAFORSEO_PASSWORD environment variablesgpt-4.1-miniOPENAI_API_KEY environment variablebrandName and optional domain from the pipelineCompetitorData// Query: "top competitors of {brandName}"
{
api_key: process.env.SERPAPI_KEY,
engine: "google",
q: `top competitors of ${brandName}`,
num: 10
}
[{
target: domain, // e.g. "gymshark.com"
language_code: "en",
location_code: 2840, // United States
limit: 5
}]
// ONLY used when Methods 1 and 2 both fail
// This is a MINIMAL prompt -- keep token usage as low as possible
const response = await openai.chat.completions.create({
model: 'gpt-4.1-mini',
max_tokens: 200,
temperature: 0.3,
messages: [
{
role: 'system',
content: 'You are a marketing analyst. Return only a JSON array of competitor objects.'
},
{
role: 'user',
content: `List 5 direct competitors of "${brandName}"${domain ? ` (${domain})` : ''}. Return JSON: [{"name":"...","website":"...","reason":"..."}]`
}
]
});
name and website populatedtry/catchEMPTY_COMPETITOR_DATA with error field set:return { ...EMPTY_COMPETITOR_DATA, error: 'Competitor data unavailable: <reason>' };
CompetitorData objectlogger.error('Competitor collector failed', { brandName, domain, method, err });
logger.warn('Competitor finder: SerpAPI failed, falling back to DataForSEO', { brandName });
logger.warn('Competitor finder: DataForSEO failed, falling back to OpenAI', { brandName });
import { collectCompetitors } from '../collectors/competitorCollector';
// Successful collection (via SerpAPI)
const data = await collectCompetitors('Gymshark', 'gymshark.com');
// Returns:
// {
// competitors: [
// { name: "Nike", website: "nike.com", reason: "Global leader in athletic apparel" },
// { name: "Lululemon", website: "lululemon.com", reason: "Premium activewear competitor" },
// { name: "Under Armour", website: "underarmour.com", reason: "Direct competitor in gym wear" },
// { name: "Alphalete", website: "alphalete.com", reason: "DTC fitness apparel brand" },
// { name: "Fabletics", website: "fabletics.com", reason: "Subscription-based activewear" },
// ],
// }
// Partial result (only OpenAI fallback worked)
const partial = await collectCompetitors('ObscureBrand');
// Returns:
// {
// competitors: [
// { name: "CompetitorA", website: "competitora.com", reason: "Similar product category" },
// { name: "CompetitorB", website: "competitorb.com", reason: "Same target market" },
// { name: "CompetitorC", website: "competitorc.com" },
// ],
// }
// Failed collection (graceful degradation)
const failedData = await collectCompetitors('UnknownBrand');
// Returns:
// {
// competitors: [],
// error: "Competitor data unavailable: All methods failed"
// }
reportGenerator.ts where an AI model call is permitted. It must be minimal (max 200 tokens) and should be logged as a warning for cost monitoring.'instagram' (no domain available), skip Method 2 (DataForSEO requires a domain) and rely on Methods 1 and 3.EMPTY_COMPETITOR_DATA constant is defined in src/types/audit.types.ts and should be imported for fallback returns.ZIP package — ready to use