@coldiq/mcp 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,8 +2,8 @@ import { z } from 'zod';
2
2
  import { callApi } from '../client.js';
3
3
  export const findEmailsName = 'find_emails';
4
4
  export const findEmailsDescription = 'Find professional emails for multiple people in one batch. ' +
5
- 'Uses bulk enrichment first, then a secondary provider for misses, then ' +
6
- 'two additional providers in parallel for any remaining misses. ' +
5
+ 'Uses Prospeo bulk first; for any misses, runs FullEnrich and FindyMail/IcyPeas in parallel ' +
6
+ 'whichever provider returns an email first wins. ' +
7
7
  'Much faster than calling find_email one-by-one. Max 50 people per call. ' +
8
8
  'Each person needs a unique id to match results back. ' +
9
9
  'Always pass first_name, last_name, and domain — they are the primary enrichment signal. ' +
@@ -27,88 +27,69 @@ function sleep(ms) {
27
27
  function missesOf(people, results) {
28
28
  return people.filter((p) => !results.find((r) => r.id === p.id)?.email);
29
29
  }
30
- export async function findEmailsHandler(input) {
31
- const people = input.people;
32
- const results = people.map((p) => ({ id: p.id, email: null, provider: null }));
33
- // Step 1: Prospeo bulk — 1 call for all people
34
- const bulkBody = {
35
- data: people.map((p) => p.linkedin_url
36
- ? { identifier: p.id, linkedin_url: p.linkedin_url }
37
- : { identifier: p.id, first_name: p.first_name, last_name: p.last_name, company_name: p.domain }),
30
+ async function fullEnrichStep(misses, results) {
31
+ const feBody = {
32
+ name: 'mcp-enrich-batch',
33
+ data: misses.map((p) => ({
34
+ custom_id: p.id,
35
+ first_name: p.first_name,
36
+ last_name: p.last_name,
37
+ domain: p.domain,
38
+ ...(p.linkedin_url ? { linkedin_url: p.linkedin_url } : {}),
39
+ enrich_fields: ['contact.emails'],
40
+ })),
38
41
  };
39
- const bulkRes = await callApi('POST', '/prospeo/bulk-enrich-person', bulkBody);
40
- if (bulkRes.ok) {
41
- const data = bulkRes.data;
42
- for (const item of data.results ?? []) {
43
- const email = item.person?.email?.email;
44
- if (typeof email === 'string' && email.includes('@')) {
45
- const hit = results.find((r) => r.id === item.identifier);
46
- if (hit) {
47
- hit.email = email;
48
- hit.provider = 'prospeo';
49
- }
50
- }
51
- }
52
- }
53
- // Step 2: FullEnrich batch for Prospeo misses (async, better European coverage)
54
- const afterProspeo = missesOf(people, results);
55
- if (afterProspeo.length > 0) {
56
- const feBody = {
57
- name: 'mcp-enrich-batch',
58
- data: afterProspeo.map((p) => ({
59
- custom_id: p.id,
60
- first_name: p.first_name,
61
- last_name: p.last_name,
62
- domain: p.domain,
63
- ...(p.linkedin_url ? { linkedin_url: p.linkedin_url } : {}),
64
- enrich_fields: ['contact.emails'],
65
- })),
66
- };
67
- const feCreateRes = await callApi('POST', '/fullenrich/contact/enrich/bulk', feBody);
68
- if (feCreateRes.ok) {
69
- const enrichmentId = feCreateRes.data.enrichment_id;
70
- if (enrichmentId) {
71
- const deadline = Date.now() + 90_000;
72
- while (Date.now() < deadline) {
73
- await sleep(5000);
74
- const pollRes = await callApi('GET', `/fullenrich/contact/enrich/bulk/${enrichmentId}`);
75
- if (!pollRes.ok)
42
+ const feCreateRes = await callApi('POST', '/fullenrich/contact/enrich/bulk', feBody);
43
+ if (!feCreateRes.ok)
44
+ return;
45
+ const enrichmentId = feCreateRes.data.enrichment_id;
46
+ if (!enrichmentId)
47
+ return;
48
+ // Tightened from 90s 45s: Step 3 runs in parallel and typically fills misses
49
+ // faster, so FullEnrich's marginal value decays past this point.
50
+ const deadline = Date.now() + 45_000;
51
+ while (Date.now() < deadline) {
52
+ await sleep(5000);
53
+ const pollRes = await callApi('GET', `/fullenrich/contact/enrich/bulk/${enrichmentId}`);
54
+ if (!pollRes.ok)
55
+ continue;
56
+ const pd = pollRes.data;
57
+ const status = pd.status;
58
+ if (status !== 'DONE' && status !== 'FAILED')
59
+ continue;
60
+ if (status === 'DONE') {
61
+ const feItems = pd.data;
62
+ if (Array.isArray(feItems)) {
63
+ for (const item of feItems) {
64
+ const personId = item.custom_id;
65
+ const hit = personId ? results.find((r) => r.id === personId) : undefined;
66
+ if (!hit || hit.email)
76
67
  continue;
77
- const pd = pollRes.data;
78
- const status = pd.status;
79
- if (status === 'DONE' || status === 'FAILED') {
80
- if (status === 'DONE') {
81
- const feItems = pd.data;
82
- if (Array.isArray(feItems)) {
83
- feItems.forEach((item) => {
84
- const personId = item.custom_id;
85
- const hit = personId ? results.find((r) => r.id === personId) : undefined;
86
- if (!hit || hit.email)
87
- return;
88
- const emails = item.emails;
89
- if (Array.isArray(emails) && emails.length > 0 && typeof emails[0] === 'string' && emails[0].includes('@')) {
90
- hit.email = emails[0];
91
- hit.provider = 'fullenrich';
92
- }
93
- });
94
- }
95
- }
96
- break;
68
+ const emails = item.emails;
69
+ if (Array.isArray(emails) && emails.length > 0 && typeof emails[0] === 'string' && emails[0].includes('@')) {
70
+ hit.email = emails[0];
71
+ hit.provider = 'fullenrich';
97
72
  }
98
73
  }
99
74
  }
100
75
  }
76
+ return;
101
77
  }
102
- // Step 3: parallel fallback for remaining misses — FindyMail then IcyPeas per person
103
- const afterFullEnrich = missesOf(people, results);
104
- await Promise.all(afterFullEnrich.map(async (person) => {
78
+ }
79
+ async function findymailIcypeasStep(misses, results) {
80
+ await Promise.all(misses.map(async (person) => {
105
81
  const hit = results.find((r) => r.id === person.id);
82
+ if (!hit)
83
+ return;
84
+ // Skip if the parallel FullEnrich branch already filled this person.
85
+ if (hit.email)
86
+ return;
106
87
  const fullName = [person.first_name, person.last_name].filter(Boolean).join(' ');
107
88
  const fmRes = await callApi('POST', '/findymail/search/name', {
108
89
  name: fullName,
109
90
  domain: person.domain,
110
91
  });
111
- if (fmRes.ok) {
92
+ if (!hit.email && fmRes.ok) {
112
93
  const d = fmRes.data;
113
94
  if (typeof d.email === 'string' && d.email.includes('@')) {
114
95
  hit.email = d.email;
@@ -116,12 +97,14 @@ export async function findEmailsHandler(input) {
116
97
  return;
117
98
  }
118
99
  }
100
+ if (hit.email)
101
+ return;
119
102
  const icyRes = await callApi('POST', '/icypeas/email-search', {
120
103
  firstname: person.first_name,
121
104
  lastname: person.last_name,
122
105
  domainOrCompany: person.domain,
123
106
  });
124
- if (icyRes.ok) {
107
+ if (!hit.email && icyRes.ok) {
125
108
  const d = icyRes.data;
126
109
  const email = typeof d.email === 'string' && d.email.includes('@')
127
110
  ? d.email
@@ -134,6 +117,40 @@ export async function findEmailsHandler(input) {
134
117
  }
135
118
  }
136
119
  }));
120
+ }
121
+ export async function findEmailsHandler(input) {
122
+ const people = input.people;
123
+ const results = people.map((p) => ({ id: p.id, email: null, provider: null }));
124
+ // Step 1: Prospeo bulk — 1 call for all people
125
+ const bulkBody = {
126
+ data: people.map((p) => p.linkedin_url
127
+ ? { identifier: p.id, linkedin_url: p.linkedin_url }
128
+ : { identifier: p.id, first_name: p.first_name, last_name: p.last_name, company_name: p.domain }),
129
+ };
130
+ const bulkRes = await callApi('POST', '/prospeo/bulk-enrich-person', bulkBody);
131
+ if (bulkRes.ok) {
132
+ const data = bulkRes.data;
133
+ for (const item of data.results ?? []) {
134
+ const email = item.person?.email?.email;
135
+ if (typeof email === 'string' && email.includes('@')) {
136
+ const hit = results.find((r) => r.id === item.identifier);
137
+ if (hit) {
138
+ hit.email = email;
139
+ hit.provider = 'prospeo';
140
+ }
141
+ }
142
+ }
143
+ }
144
+ // Steps 2 & 3 run concurrently for Prospeo misses.
145
+ // Step 2: FullEnrich bulk async (slow tail, better European coverage)
146
+ // Step 3: per-person FindyMail → IcyPeas waterfall
147
+ // Both branches write to the shared `results` array; every write site checks
148
+ // `if (hit.email)` first so whichever provider arrives first wins and the
149
+ // other does not overwrite.
150
+ const misses = missesOf(people, results);
151
+ if (misses.length > 0) {
152
+ await Promise.all([fullEnrichStep(misses, results), findymailIcypeasStep(misses, results)]);
153
+ }
137
154
  const found = results.filter((r) => r.email !== null).length;
138
155
  return {
139
156
  content: [
@@ -1 +1 @@
1
- {"version":3,"file":"find-emails.js","sourceRoot":"","sources":["../../src/tools/find-emails.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"find-emails.js","sourceRoot":"","sources":["../../src/tools/find-emails.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coldiq/mcp",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/executor.ts CHANGED
@@ -13,6 +13,10 @@ export interface ExecutionResult {
13
13
  export interface ExecutionError {
14
14
  error: string
15
15
  providers_tried: Array<{ provider: string; status: number; error: string }>
16
+ /** Set when at least one provider returned 402 — points the user at the top-up page. */
17
+ billingUrl?: string
18
+ /** True when every attempted provider failed with 402 (the user is fully blocked on credits). */
19
+ insufficientCredits?: boolean
16
20
  }
17
21
 
18
22
  function isExecutionError(result: ExecutionResult | ExecutionError): result is ExecutionError {
@@ -69,7 +73,11 @@ async function executeAsync(
69
73
  const deadline = Date.now() + asyncCfg.timeoutMs
70
74
  let pollCount = 0
71
75
  while (Date.now() < deadline) {
72
- await sleep(asyncCfg.pollIntervalMs)
76
+ const interval =
77
+ typeof asyncCfg.pollIntervalMs === 'function'
78
+ ? asyncCfg.pollIntervalMs(pollCount + 1)
79
+ : asyncCfg.pollIntervalMs
80
+ await sleep(interval)
73
81
  pollCount++
74
82
 
75
83
  const pollRes = await callApi('GET', asyncCfg.pollEndpoint(jobId))
@@ -140,6 +148,7 @@ export async function executeWithFallback(
140
148
  : getProviders(capability)
141
149
 
142
150
  const errors: Array<{ id: string; status: number; error: string }> = []
151
+ let billingUrl: string | undefined
143
152
 
144
153
  for (const provider of providers) {
145
154
  if (provider.isApplicable) {
@@ -189,16 +198,33 @@ export async function executeWithFallback(
189
198
  : `Status ${result.status}, no results`
190
199
  log(`${capability} → ${provider.id} ✗ ${errMsg}`)
191
200
  errors.push({ id: provider.id, status: result.status, error: errMsg })
201
+
202
+ // Capture the billing URL the API surfaces in 402 responses so the
203
+ // top-level error can include a clickable link for the user.
204
+ if (result.status === 402 && !billingUrl
205
+ && result.data && typeof result.data === 'object'
206
+ && typeof (result.data as Record<string, unknown>).billingUrl === 'string') {
207
+ billingUrl = (result.data as Record<string, string>).billingUrl
208
+ }
192
209
  }
193
210
 
194
211
  debug(`${capability}: all ${errors.length} providers failed — ${JSON.stringify(errors)}`)
195
212
  const sanitized = errors.map((e, i) => ({
196
213
  provider: `provider_${i + 1}`,
197
214
  status: e.status,
198
- error: e.error.slice(0, 120),
215
+ error: e.error.slice(0, 200),
199
216
  }))
200
- return {
201
- error: `All ${errors.length} providers failed for ${capability}`,
217
+
218
+ const allOutOfCredits = errors.length > 0 && errors.every((e) => e.status === 402)
219
+ const topLevelError = allOutOfCredits && billingUrl
220
+ ? `Insufficient credits — every provider for ${capability} returned 402. Top up at ${billingUrl} to continue.`
221
+ : `All ${errors.length} providers failed for ${capability}`
222
+
223
+ const out: ExecutionError = {
224
+ error: topLevelError,
202
225
  providers_tried: sanitized,
203
226
  }
227
+ if (billingUrl) out.billingUrl = billingUrl
228
+ if (allOutOfCredits) out.insufficientCredits = true
229
+ return out
204
230
  }
package/src/registry.ts CHANGED
@@ -5,8 +5,12 @@
5
5
  export interface AsyncConfig {
6
6
  /** Build the poll endpoint path from the ID returned by the create call */
7
7
  pollEndpoint: (id: string) => string
8
- /** Milliseconds between poll attempts */
9
- pollIntervalMs: number
8
+ /**
9
+ * Milliseconds between poll attempts. Either a constant or a function of the
10
+ * 1-indexed attempt number (1 = first poll). Use the function form to apply
11
+ * a fast-then-backoff schedule.
12
+ */
13
+ pollIntervalMs: number | ((attempt: number) => number)
10
14
  /** Max milliseconds before giving up */
11
15
  timeoutMs: number
12
16
  /** Check if the async job is done */
@@ -963,7 +967,11 @@ const findPeopleProviders: ProviderEntry[] = [
963
967
  },
964
968
  async: {
965
969
  pollEndpoint: (id: string) => `/leadsfactory/contact-finder/searches/${id}`,
966
- pollIntervalMs: 15000,
970
+ // Continuously growing backoff: fast first probe (3s), then ramp up indefinitely
971
+ // — never plateaus, but the cumulative delay is bounded by timeoutMs.
972
+ // Schedule: 3s, 7s, 15s, 25s, 35s, 45s, 55s, 65s, +10s per attempt thereafter.
973
+ pollIntervalMs: (attempt) =>
974
+ attempt === 1 ? 3000 : attempt === 2 ? 7000 : 15000 + (attempt - 3) * 10000,
967
975
  timeoutMs: 300_000, // 5 minutes
968
976
  isComplete: (data) => {
969
977
  const d = data as Record<string, unknown>
@@ -4,7 +4,10 @@ import { executeWithFallback, isExecutionError } from '../executor.js'
4
4
  export const findEmailName = 'find_email'
5
5
 
6
6
  export const findEmailDescription =
7
- "Find a person's professional email given their name and company. Tries multiple providers in sequence until an email is found (waterfall). Returns the email and which provider found it."
7
+ "Find a person's professional email given their name and company. " +
8
+ 'For more than one person, ALWAYS use find_emails instead — never loop this tool. ' +
9
+ 'Looping find_email runs the full provider waterfall sequentially per person and is dramatically slower than the batched find_emails path. ' +
10
+ 'Tries multiple providers in sequence until an email is found (waterfall). Returns the email and which provider found it.'
8
11
 
9
12
  export const findEmailSchema = {
10
13
  first_name: z.string().optional().describe('First name of the person'),
@@ -5,8 +5,8 @@ export const findEmailsName = 'find_emails'
5
5
 
6
6
  export const findEmailsDescription =
7
7
  'Find professional emails for multiple people in one batch. ' +
8
- 'Uses bulk enrichment first, then a secondary provider for misses, then ' +
9
- 'two additional providers in parallel for any remaining misses. ' +
8
+ 'Uses Prospeo bulk first; for any misses, runs FullEnrich and FindyMail/IcyPeas in parallel ' +
9
+ 'whichever provider returns an email first wins. ' +
10
10
  'Much faster than calling find_email one-by-one. Max 50 people per call. ' +
11
11
  'Each person needs a unique id to match results back. ' +
12
12
  'Always pass first_name, last_name, and domain — they are the primary enrichment signal. ' +
@@ -50,106 +50,72 @@ function missesOf(people: PersonInput[], results: EmailResult[]): PersonInput[]
50
50
  return people.filter((p) => !results.find((r) => r.id === p.id)?.email)
51
51
  }
52
52
 
53
- export async function findEmailsHandler(input: Record<string, unknown>) {
54
- const people = input.people as PersonInput[]
55
- const results: EmailResult[] = people.map((p) => ({ id: p.id, email: null, provider: null }))
56
-
57
- // Step 1: Prospeo bulk — 1 call for all people
58
- const bulkBody = {
59
- data: people.map((p) =>
60
- p.linkedin_url
61
- ? { identifier: p.id, linkedin_url: p.linkedin_url }
62
- : { identifier: p.id, first_name: p.first_name, last_name: p.last_name, company_name: p.domain },
63
- ),
53
+ async function fullEnrichStep(misses: PersonInput[], results: EmailResult[]): Promise<void> {
54
+ const feBody = {
55
+ name: 'mcp-enrich-batch',
56
+ data: misses.map((p) => ({
57
+ custom_id: p.id,
58
+ first_name: p.first_name,
59
+ last_name: p.last_name,
60
+ domain: p.domain,
61
+ ...(p.linkedin_url ? { linkedin_url: p.linkedin_url } : {}),
62
+ enrich_fields: ['contact.emails'],
63
+ })),
64
64
  }
65
65
 
66
- const bulkRes = await callApi('POST', '/prospeo/bulk-enrich-person', bulkBody)
67
-
68
- if (bulkRes.ok) {
69
- const data = bulkRes.data as {
70
- results?: Array<{
71
- identifier: string
72
- person?: { email?: { email?: string } }
73
- }>
74
- }
75
- for (const item of data.results ?? []) {
76
- const email = item.person?.email?.email
77
- if (typeof email === 'string' && email.includes('@')) {
78
- const hit = results.find((r) => r.id === item.identifier)
79
- if (hit) {
80
- hit.email = email
81
- hit.provider = 'prospeo'
82
- }
83
- }
84
- }
85
- }
86
-
87
- // Step 2: FullEnrich batch for Prospeo misses (async, better European coverage)
88
- const afterProspeo = missesOf(people, results)
89
-
90
- if (afterProspeo.length > 0) {
91
- const feBody = {
92
- name: 'mcp-enrich-batch',
93
- data: afterProspeo.map((p) => ({
94
- custom_id: p.id,
95
- first_name: p.first_name,
96
- last_name: p.last_name,
97
- domain: p.domain,
98
- ...(p.linkedin_url ? { linkedin_url: p.linkedin_url } : {}),
99
- enrich_fields: ['contact.emails'],
100
- })),
101
- }
102
-
103
- const feCreateRes = await callApi('POST', '/fullenrich/contact/enrich/bulk', feBody)
104
-
105
- if (feCreateRes.ok) {
106
- const enrichmentId = (feCreateRes.data as Record<string, unknown>).enrichment_id as string | undefined
107
-
108
- if (enrichmentId) {
109
- const deadline = Date.now() + 90_000
110
- while (Date.now() < deadline) {
111
- await sleep(5000)
112
- const pollRes = await callApi('GET', `/fullenrich/contact/enrich/bulk/${enrichmentId}`)
113
- if (!pollRes.ok) continue
114
-
115
- const pd = pollRes.data as Record<string, unknown>
116
- const status = pd.status as string | undefined
117
- if (status === 'DONE' || status === 'FAILED') {
118
- if (status === 'DONE') {
119
- const feItems = pd.data as Array<Record<string, unknown>> | undefined
120
- if (Array.isArray(feItems)) {
121
- feItems.forEach((item) => {
122
- const personId = item.custom_id as string | undefined
123
- const hit = personId ? results.find((r) => r.id === personId) : undefined
124
- if (!hit || hit.email) return
125
- const emails = item.emails as string[] | undefined
126
- if (Array.isArray(emails) && emails.length > 0 && typeof emails[0] === 'string' && emails[0].includes('@')) {
127
- hit.email = emails[0]
128
- hit.provider = 'fullenrich'
129
- }
130
- })
131
- }
132
- }
133
- break
66
+ const feCreateRes = await callApi('POST', '/fullenrich/contact/enrich/bulk', feBody)
67
+ if (!feCreateRes.ok) return
68
+
69
+ const enrichmentId = (feCreateRes.data as Record<string, unknown>).enrichment_id as string | undefined
70
+ if (!enrichmentId) return
71
+
72
+ // Tightened from 90s 45s: Step 3 runs in parallel and typically fills misses
73
+ // faster, so FullEnrich's marginal value decays past this point.
74
+ const deadline = Date.now() + 45_000
75
+ while (Date.now() < deadline) {
76
+ await sleep(5000)
77
+ const pollRes = await callApi('GET', `/fullenrich/contact/enrich/bulk/${enrichmentId}`)
78
+ if (!pollRes.ok) continue
79
+
80
+ const pd = pollRes.data as Record<string, unknown>
81
+ const status = pd.status as string | undefined
82
+ if (status !== 'DONE' && status !== 'FAILED') continue
83
+
84
+ if (status === 'DONE') {
85
+ const feItems = pd.data as Array<Record<string, unknown>> | undefined
86
+ if (Array.isArray(feItems)) {
87
+ for (const item of feItems) {
88
+ const personId = item.custom_id as string | undefined
89
+ const hit = personId ? results.find((r) => r.id === personId) : undefined
90
+ if (!hit || hit.email) continue
91
+ const emails = item.emails as string[] | undefined
92
+ if (Array.isArray(emails) && emails.length > 0 && typeof emails[0] === 'string' && emails[0].includes('@')) {
93
+ hit.email = emails[0]
94
+ hit.provider = 'fullenrich'
134
95
  }
135
96
  }
136
97
  }
137
98
  }
99
+ return
138
100
  }
101
+ }
139
102
 
140
- // Step 3: parallel fallback for remaining misses — FindyMail then IcyPeas per person
141
- const afterFullEnrich = missesOf(people, results)
142
-
103
+ async function findymailIcypeasStep(misses: PersonInput[], results: EmailResult[]): Promise<void> {
143
104
  await Promise.all(
144
- afterFullEnrich.map(async (person) => {
145
- const hit = results.find((r) => r.id === person.id)!
105
+ misses.map(async (person) => {
106
+ const hit = results.find((r) => r.id === person.id)
107
+ if (!hit) return
108
+
109
+ // Skip if the parallel FullEnrich branch already filled this person.
110
+ if (hit.email) return
111
+
146
112
  const fullName = [person.first_name, person.last_name].filter(Boolean).join(' ')
147
113
 
148
114
  const fmRes = await callApi('POST', '/findymail/search/name', {
149
115
  name: fullName,
150
116
  domain: person.domain,
151
117
  })
152
- if (fmRes.ok) {
118
+ if (!hit.email && fmRes.ok) {
153
119
  const d = fmRes.data as Record<string, unknown>
154
120
  if (typeof d.email === 'string' && d.email.includes('@')) {
155
121
  hit.email = d.email
@@ -158,12 +124,14 @@ export async function findEmailsHandler(input: Record<string, unknown>) {
158
124
  }
159
125
  }
160
126
 
127
+ if (hit.email) return
128
+
161
129
  const icyRes = await callApi('POST', '/icypeas/email-search', {
162
130
  firstname: person.first_name,
163
131
  lastname: person.last_name,
164
132
  domainOrCompany: person.domain,
165
133
  })
166
- if (icyRes.ok) {
134
+ if (!hit.email && icyRes.ok) {
167
135
  const d = icyRes.data as Record<string, unknown>
168
136
  const email =
169
137
  typeof d.email === 'string' && d.email.includes('@')
@@ -178,6 +146,53 @@ export async function findEmailsHandler(input: Record<string, unknown>) {
178
146
  }
179
147
  }),
180
148
  )
149
+ }
150
+
151
+ export async function findEmailsHandler(input: Record<string, unknown>) {
152
+ const people = input.people as PersonInput[]
153
+ const results: EmailResult[] = people.map((p) => ({ id: p.id, email: null, provider: null }))
154
+
155
+ // Step 1: Prospeo bulk — 1 call for all people
156
+ const bulkBody = {
157
+ data: people.map((p) =>
158
+ p.linkedin_url
159
+ ? { identifier: p.id, linkedin_url: p.linkedin_url }
160
+ : { identifier: p.id, first_name: p.first_name, last_name: p.last_name, company_name: p.domain },
161
+ ),
162
+ }
163
+
164
+ const bulkRes = await callApi('POST', '/prospeo/bulk-enrich-person', bulkBody)
165
+
166
+ if (bulkRes.ok) {
167
+ const data = bulkRes.data as {
168
+ results?: Array<{
169
+ identifier: string
170
+ person?: { email?: { email?: string } }
171
+ }>
172
+ }
173
+ for (const item of data.results ?? []) {
174
+ const email = item.person?.email?.email
175
+ if (typeof email === 'string' && email.includes('@')) {
176
+ const hit = results.find((r) => r.id === item.identifier)
177
+ if (hit) {
178
+ hit.email = email
179
+ hit.provider = 'prospeo'
180
+ }
181
+ }
182
+ }
183
+ }
184
+
185
+ // Steps 2 & 3 run concurrently for Prospeo misses.
186
+ // Step 2: FullEnrich bulk async (slow tail, better European coverage)
187
+ // Step 3: per-person FindyMail → IcyPeas waterfall
188
+ // Both branches write to the shared `results` array; every write site checks
189
+ // `if (hit.email)` first so whichever provider arrives first wins and the
190
+ // other does not overwrite.
191
+ const misses = missesOf(people, results)
192
+
193
+ if (misses.length > 0) {
194
+ await Promise.all([fullEnrichStep(misses, results), findymailIcypeasStep(misses, results)])
195
+ }
181
196
 
182
197
  const found = results.filter((r) => r.email !== null).length
183
198
  return {