File size: 3,040 Bytes
bd28470
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import axios from "axios";
import { getEnv } from "../../shared/config/env";
import { withRetry, isCircuitOpen, recordFailure, recordSuccess } from "../../shared/utils/retry";
import { serperLimiter } from "../../shared/utils/rate-limiter";
import { logger } from "../../shared/utils/logger";

const PROVIDER = "serper";

export interface SerperResult {
  title: string;
  link: string;
  snippet: string;
  domain: string;
}

/**
 * Searches Google via Serper.dev API.
 * Builds targeted queries to find companies matching ICP in a given region.
 */
export async function searchCompanies(
  region: string,
  industry: string,
  keywords: string[],
  page = 1
): Promise<SerperResult[]> {
  if (isCircuitOpen(PROVIDER)) {
    logger.warn({ provider: PROVIDER }, "Circuit open — skipping Serper call");
    return [];
  }

  await serperLimiter.consume(PROVIDER);

  const queries = buildQueries(region, industry, keywords);
  const results: SerperResult[] = [];

  for (const query of queries) {
    try {
      const data = await withRetry(
        () => callSerper(query, page),
        { provider: PROVIDER }
      );
      results.push(...data);
      recordSuccess(PROVIDER);
    } catch (err) {
      recordFailure(PROVIDER);
      logger.error({ query, err }, "Serper search failed");
    }
  }

  // Deduplicate by domain
  const seen = new Set<string>();
  return results.filter((r) => {
    if (seen.has(r.domain)) return false;
    seen.add(r.domain);
    return true;
  });
}

async function callSerper(query: string, page: number): Promise<SerperResult[]> {
  const env = getEnv();
  const response = await axios.post(
    "https://google.serper.dev/search",
    { q: query, num: 10, page },
    {
      headers: {
        "X-API-KEY": env.SERPER_API_KEY,
        "Content-Type": "application/json",
      },
      timeout: 10_000,
    }
  );

  const organic = response.data?.organic ?? [];
  return organic.map((item: { title: string; link: string; snippet: string }) => ({
    title: item.title,
    link: item.link,
    snippet: item.snippet,
    domain: extractDomain(item.link),
  }));
}

function buildQueries(region: string, industry: string, keywords: string[]): string[] {
  // Precision queries — each targets a specific pain+industry+region combo
  const regionLabel = REGION_LABELS[region] ?? region;
  return [
    `"${industry}" company "${regionLabel}" "50 employees" OR "100 employees" OR "200 employees" automation`,
    `${industry} business ${regionLabel} site:linkedin.com/company`,
    `"${industry}" "${regionLabel}" "digital transformation" OR "AI" OR "automation" company`,
    `${keywords[0]} ${keywords[1] ?? ""} company ${regionLabel} -job -careers`,
  ].filter(Boolean);
}

function extractDomain(url: string): string {
  try {
    return new URL(url).hostname.replace(/^www\./, "");
  } catch {
    return url;
  }
}

const REGION_LABELS: Record<string, string> = {
  US: "United States",
  UK: "United Kingdom",
  AU: "Australia",
  UAE: "Dubai",
  SA: "Saudi Arabia",
  SG: "Singapore",
};