feat(integrations): hosted email-enrichment providers + cascade wiring#5087
Conversation
Add Datagma, Dropcontact, LeadMagic, Icypeas, and Enrow integrations — tools, blocks, brand icons, and BYOK + metered hosted-key support — and register each in the tool/block registries and BYOK provider list. Wire the new finders/verifiers into the enrichment cascades: - work-email: Datagma, LeadMagic, Dropcontact, Icypeas, Enrow - phone-number: LeadMagic, Datagma, Dropcontact - email-verification: Icypeas, Enrow - company-info: Datagma, LeadMagic - company-domain: Datagma Add hosting tests for all five providers and cascade tests covering the new providers (incl. new test files for email-verification, company-info, and company-domain). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Those providers are appended to the existing enrichment waterfalls—work email, phone, email verification, company info, and company domain—each with Reviewed by Cursor Bugbot for commit 68af663. Bugbot is set up for automated code reviews on this repo. Configure here. |
Greptile SummaryThis PR adds five new email-enrichment integrations — Datagma, Dropcontact, LeadMagic, Icypeas, and Enrow — each with tool definitions, block UI, brand icons, and full BYOK + hosted-key support, then wires all five into the five enrichment cascades (work-email, phone-number, email-verification, company-info, company-domain).
Confidence Score: 5/5This PR is safe to merge. All cascade integrations follow the established contract, and the previously reported Icypeas success/false bugs are correctly resolved. The Icypeas async-poll tools now return All Datagma tool files ( Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Caller
participant CascadeRunner as enrichments/run.ts
participant Hunter
participant Findymail
participant Prospeo
participant Wiza
participant PDL
participant Datagma
participant LeadMagic
participant Dropcontact
participant Icypeas
participant Enrow
Caller->>CascadeRunner: runEnrichment(work-email, inputs)
CascadeRunner->>Hunter: executeTool(hunter_email_finder)
Hunter-->>CascadeRunner: "{success:true, output:{email:null}}"
Note over CascadeRunner: mapOutput→null, fall through
CascadeRunner->>Findymail: executeTool(findymail_find_email_from_name)
Findymail-->>CascadeRunner: "{success:true, output:{contact:{email:"..."}}}"
alt email found
CascadeRunner-->>Caller: "{result:{email}, provider:"Findymail"}"
else no result
CascadeRunner->>Prospeo: executeTool(...)
CascadeRunner->>Wiza: executeTool(...)
CascadeRunner->>PDL: executeTool(...)
CascadeRunner->>Datagma: executeTool(datagma_find_email)
Note over Datagma: GET ?apiId=key (URL param auth)
CascadeRunner->>LeadMagic: executeTool(leadmagic_find_email)
CascadeRunner->>Dropcontact: executeTool(dropcontact_enrich_contact)
Note over Dropcontact: POST submit → poll until ready
CascadeRunner->>Icypeas: executeTool(icypeas_find_email)
Note over Icypeas: POST submit → poll until terminal status
CascadeRunner->>Enrow: executeTool(enrow_find_email)
Note over Enrow: POST submit → poll until 200
Enrow-->>CascadeRunner: "{success:true, output:{email, qualification:"valid"}}"
CascadeRunner-->>Caller: "{result:{email}, provider:"Enrow"}"
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Caller
participant CascadeRunner as enrichments/run.ts
participant Hunter
participant Findymail
participant Prospeo
participant Wiza
participant PDL
participant Datagma
participant LeadMagic
participant Dropcontact
participant Icypeas
participant Enrow
Caller->>CascadeRunner: runEnrichment(work-email, inputs)
CascadeRunner->>Hunter: executeTool(hunter_email_finder)
Hunter-->>CascadeRunner: "{success:true, output:{email:null}}"
Note over CascadeRunner: mapOutput→null, fall through
CascadeRunner->>Findymail: executeTool(findymail_find_email_from_name)
Findymail-->>CascadeRunner: "{success:true, output:{contact:{email:"..."}}}"
alt email found
CascadeRunner-->>Caller: "{result:{email}, provider:"Findymail"}"
else no result
CascadeRunner->>Prospeo: executeTool(...)
CascadeRunner->>Wiza: executeTool(...)
CascadeRunner->>PDL: executeTool(...)
CascadeRunner->>Datagma: executeTool(datagma_find_email)
Note over Datagma: GET ?apiId=key (URL param auth)
CascadeRunner->>LeadMagic: executeTool(leadmagic_find_email)
CascadeRunner->>Dropcontact: executeTool(dropcontact_enrich_contact)
Note over Dropcontact: POST submit → poll until ready
CascadeRunner->>Icypeas: executeTool(icypeas_find_email)
Note over Icypeas: POST submit → poll until terminal status
CascadeRunner->>Enrow: executeTool(enrow_find_email)
Note over Enrow: POST submit → poll until 200
Enrow-->>CascadeRunner: "{success:true, output:{email, qualification:"valid"}}"
CascadeRunner-->>Caller: "{result:{email}, provider:"Enrow"}"
end
Reviews (6): Last reviewed commit: "fix(enrichment): only accept valid-quali..." | Re-trigger Greptile |
| const status = output.status as string | undefined | ||
| if (!status) { | ||
| throw new Error('Icypeas verify-email: cannot determine cost — status is missing') | ||
| } | ||
| // Billable when the status name contains DEBITED (i.e. DEBITED or DEBITED_NOT_FOUND). | ||
| const billable = status.includes('DEBITED') | ||
| // 0.1 credit; express as a fractional number so ICYPEAS_CREDIT_USD math works. | ||
| return billable ? 0.1 : 0 | ||
| }), |
There was a problem hiding this comment.
Billing function throws when
status is falsy
getCost throws 'cannot determine cost — status is missing' if output.status is falsy. In normal operation postProcess always returns a terminal status, so this is unreachable in practice. But if the hosting layer ever evaluates cost on the initial transformResponse result (before postProcess runs), output.status will be null and the throw propagates as an unhandled billing error. Consider returning 0 when status is absent, matching the defensive posture of the other providers.
| type: 'string', | ||
| required: true, | ||
| visibility: 'user-only', | ||
| description: 'Datagma API key', | ||
| }, | ||
| }, | ||
|
|
||
| request: { | ||
| url: (params) => { | ||
| const url = new URL('https://gateway.datagma.net/api/ingress/v6/findEmail') | ||
| url.searchParams.set('apiId', params.apiKey) | ||
| url.searchParams.set('fullName', params.fullName) |
There was a problem hiding this comment.
API key embedded in URL query string (
?apiId=...) for all Datagma endpoints
Every Datagma tool appends url.searchParams.set('apiId', params.apiKey). This is Datagma's documented auth scheme and can't be changed client-side, but the hosted API key appears verbatim in every request URL and will be captured by server-side access logs at Datagma and any intermediary. A note in the DATAGMA_API_KEY_PREFIX doc comment that this API uses URL-parameter auth would help operators understand the risk when rotating a compromised key.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
- Icypeas find_email/verify_email postProcess return success:true for all terminal statuses (NOT_FOUND/DEBITED_NOT_FOUND included) so the cascade runner calls mapOutput and records invalid/not-found verdicts instead of throwing and inflating the error count - Bill Icypeas verify FOUND (not just DEBITED*) per the documented 0.1-credit charge - Datagma enrich_person only applies the 30-credit phone surcharge when a phone lookup (phoneFull) was requested - Note Datagma's URL-param (apiId) auth in the hosted-key doc comment - Update hosting tests to match Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@greptile review |
getCost returned a flat 0.25 credits regardless of output, so a job that fell back to the initial submit response (poll never completed, no qualification) was still metered. Charge 0.25 only when a qualification is present; 0 otherwise. Add a no-qualification test case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@greptile review |
…aid plan Align *_CREDIT_USD to the entry tier Sim will provision: - Datagma: Regular $49/3,000 emails → $0.0163 (was Popular $0.0132) - LeadMagic: Basic $49/2,000 → $0.0245 (was Growth $0.0104) Icypeas (Basic $0.019), Enrow (Starter $0.012), and Dropcontact (Starter ~$0.17) already reflect their lowest plan. Tests derive from the constants, so values stay consistent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| } | ||
| } | ||
|
|
||
| throw new Error('Dropcontact enrichment did not complete within the polling window') |
There was a problem hiding this comment.
Poll failures look like misses
Medium Severity
When async postProcess polling times out or hits poll errors, these tools throw, but executeTool catches that and keeps the initial stub with success: true. Enrichment then treats timeouts and poll failures as a clean no-match instead of an error, and may keep trying later providers.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit b9aae9d. Configure here.
… map - work-email LeadMagic: pass full_name + domain so single-token (mononym) names are no longer skipped - work-email Icypeas: firstname/lastname are optional on the API, so run a mononym with firstname alone instead of self-skipping - icypeas_verify_email mapItem reads item.email (verify payload shape) with a fallback to the nested results.emails[0].email Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@greptile review |
getCost compared qualification to exactly 'valid' while the cascade normalizes with toLowerCase(), so a differently-cased API qualifier could zero out billing on a valid email. Lowercase before comparing; add a test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@greptile review |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 33dc8dc. Configure here.
Dropcontact is an email/company-data enrichment service, not a phone-discovery provider — its phone/mobile_phone fields are unreliable and were surfacing firmographic data (an employee-count range like "5000-20077") as the phone. Keep the two purpose-built phone finders (LeadMagic find_mobile, Datagma find_phone); Dropcontact stays in the work-email and company cascades where its data is reliable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enrow's finder qualifies each email valid/invalid. The work-email mapOutput accepted any non-empty email, so an invalid-qualified address could fill the cell while hosted billing (which only charges on valid) charged zero. Gate the cell on qualification === 'valid', consistent with billing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@greptile review |


Summary
Type of Change
Testing
Tested manually.
bun run lint,bun run check:api-validation:strict, andtsc --noEmitall pass; 161 unit tests pass (hosting + cascade + blocks). Live-API verification of per-credit pricing and provider response shapes still pending.Checklist