iDevBuddy
feat: Phase 1 β€” AI Client Acquisition System
bd28470
import axios from "axios";
import { getEnv } from "../../shared/config/env";
import { withRetry, isCircuitOpen, recordFailure, recordSuccess } from "../../shared/utils/retry";
import { hunterLimiter } from "../../shared/utils/rate-limiter";
import { logger } from "../../shared/utils/logger";
const PROVIDER = "hunter";
export interface HunterEmailResult {
email: string | null;
score: number; // Hunter confidence 0-100
source: "hunter";
firstName: string | null;
lastName: string | null;
}
/**
* Finds a professional email address using Hunter.io.
* Falls through to pattern generation if not found.
*/
export async function findEmail(
domain: string,
firstName: string,
lastName: string
): Promise<HunterEmailResult | null> {
if (isCircuitOpen(PROVIDER)) return null;
await hunterLimiter.consume(PROVIDER);
try {
const result = await withRetry(
() => callHunterEmailFinder(domain, firstName, lastName),
{ provider: PROVIDER }
);
recordSuccess(PROVIDER);
return result;
} catch (err) {
recordFailure(PROVIDER);
logger.warn({ domain, err }, "Hunter email find failed β€” will try pattern generation");
return null;
}
}
/**
* Searches all known emails for a domain (domain search).
*/
export async function searchDomain(domain: string): Promise<HunterEmailResult[]> {
if (isCircuitOpen(PROVIDER)) return [];
await hunterLimiter.consume(PROVIDER);
try {
const result = await withRetry(
() => callHunterDomainSearch(domain),
{ provider: PROVIDER }
);
recordSuccess(PROVIDER);
return result;
} catch (err) {
recordFailure(PROVIDER);
logger.warn({ domain, err }, "Hunter domain search failed");
return [];
}
}
async function callHunterEmailFinder(
domain: string,
firstName: string,
lastName: string
): Promise<HunterEmailResult | null> {
const env = getEnv();
const response = await axios.get("https://api.hunter.io/v2/email-finder", {
params: {
domain,
first_name: firstName,
last_name: lastName,
api_key: env.HUNTER_API_KEY,
},
timeout: 8_000,
});
const data = response.data?.data;
if (!data?.email) return null;
return {
email: data.email,
score: data.score ?? 0,
source: "hunter",
firstName: data.first_name ?? null,
lastName: data.last_name ?? null,
};
}
async function callHunterDomainSearch(domain: string): Promise<HunterEmailResult[]> {
const env = getEnv();
const response = await axios.get("https://api.hunter.io/v2/domain-search", {
params: { domain, api_key: env.HUNTER_API_KEY, limit: 10 },
timeout: 8_000,
});
const emails = response.data?.data?.emails ?? [];
return emails
.filter((e: { type: string }) => e.type === "professional")
.map((e: { value: string; confidence: number; first_name: string; last_name: string }) => ({
email: e.value,
score: e.confidence,
source: "hunter" as const,
firstName: e.first_name ?? null,
lastName: e.last_name ?? null,
}));
}
// ─── Aliases for contact-enricher.ts compatibility ──────────
export type HunterContact = {
value: string; // email
first_name: string | null;
last_name: string | null;
position: string | null;
seniority: string | null;
confidence: number;
};
/**
* Search for contacts at a domain β€” used by contact-enricher.
* Maps Hunter's domain-search response to HunterContact format.
*/
export async function searchHunterContacts(domain: string): Promise<HunterContact[]> {
if (isCircuitOpen(PROVIDER)) return [];
await hunterLimiter.consume(PROVIDER);
try {
const env = getEnv();
const response = await axios.get("https://api.hunter.io/v2/domain-search", {
params: { domain, api_key: env.HUNTER_API_KEY, limit: 10 },
timeout: 8_000,
});
recordSuccess(PROVIDER);
const emails = response.data?.data?.emails ?? [];
return emails.map((e: Record<string, unknown>) => ({
value: (e.value as string) ?? "",
first_name: (e.first_name as string) ?? null,
last_name: (e.last_name as string) ?? null,
position: (e.position as string) ?? null,
seniority: (e.seniority as string) ?? null,
confidence: (e.confidence as number) ?? 0,
}));
} catch (err) {
recordFailure(PROVIDER);
logger.warn({ domain, err }, "Hunter domain search failed");
return [];
}
}