@sellable/mcp 0.1.231 → 0.1.233
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.
- package/dist/tools/leads.js +192 -26
- package/package.json +1 -1
- package/skills/create-campaign/SKILL.md +17 -0
- package/skills/create-campaign/core/providers/prospeo.json +2 -2
- package/skills/create-campaign/references/provider-selection-strategy.md +1 -1
- package/skills/create-campaign-v2/SKILL.md +17 -0
- package/skills/find-leads/SKILL.md +16 -0
- package/skills/providers/prospeo.md +42 -3
package/dist/tools/leads.js
CHANGED
|
@@ -522,6 +522,7 @@ search_prospeo_companies({
|
|
|
522
522
|
}
|
|
523
523
|
})
|
|
524
524
|
Do not invent company_oids. When seedCompanies or seedDomains are present, omit company_oids and let the backend resolve real Prospeo company IDs.
|
|
525
|
+
For customer, best-customer, top-customer, or past-company lookalikes, never substitute the sender's current company or domain as a lookalike seed and never use the sender's current company/domain as a silent substitute. Use explicit customer/account/company/domain input first, then verified customer/worked-with/past-company evidence from research/proof, then ask for the missing seed or switch to non-lookalike company filters if YOLO requires moving without a seed. Label profile/work-history seeds as past-company/public proof clues, not confirmed customers unless the source proves customer status. If the user asks for a geography like Germany, preserve it in company discovery where supported and in the follow-on people search.
|
|
525
526
|
After account approval, copy the companySearchToken exactly into confirm_prospeo_company_accounts; package-backed MCP may return a short mcp-prospeo-company-search-token:* reference to avoid long-token copy errors.
|
|
526
527
|
For accounts in the news: { "company_news": { "categories": ["Funding & Investment"], "timeframe_days": 90 } }.
|
|
527
528
|
For awards: { "company_awards": { "include": ["G2"], "match_mode": "CONTAINS" } }.
|
|
@@ -1497,7 +1498,8 @@ export const leadToolDefinitions = [
|
|
|
1497
1498
|
},
|
|
1498
1499
|
{
|
|
1499
1500
|
name: "search_prospeo_companies",
|
|
1500
|
-
description: 'Search Prospeo for company/account results through the public /search-company lane. Requires get_provider_prompt({ provider: "prospeo" }) first. Use this first for company lookalike/account asks such as "find companies like Red Rover that use AI and have an API", "find founders at companies like our best customers", or "find accounts in the news about funding or partnerships". Results are accounts, not finished people leads. Review the returned accounts with the user, then call confirm_prospeo_company_accounts with the companySearchToken to create a domainFilterId before using search_prospeo for people at those accounts. Supports company lookalike, confirmed AI Attributes including pricing, company news, awards, website search, products/services, integrations, key customers, operating languages, Google discovery, headcount by location, structured company ICP, and experimental website traffic/key executive filters. Do not invent company_oids; when seedCompanies or seedDomains are present, omit company_oids and let the backend resolve real Prospeo IDs. company_intent and support-channel guesses like phone/email/chat/ticket/social are unsupported. Public API rows do not include lookalike tier/score/reason. ' +
|
|
1501
|
+
description: 'Search Prospeo for company/account results through the public /search-company lane. Requires get_provider_prompt({ provider: "prospeo" }) first. Use this first for company lookalike/account asks such as "find companies like Red Rover that use AI and have an API", "find founders at companies like our best customers", or "find accounts in the news about funding or partnerships". Results are accounts, not finished people leads. Review the returned accounts with the user, then call confirm_prospeo_company_accounts with the companySearchToken to create a domainFilterId before using search_prospeo for people at those accounts. Supports company lookalike, confirmed AI Attributes including pricing, company news, awards, website search, products/services, integrations, key customers, operating languages, Google discovery, headcount by location, structured company ICP, and experimental website traffic/key executive filters. Do not invent company_oids; when seedCompanies or seedDomains are present, omit company_oids and let the backend resolve real Prospeo IDs. company_intent and support-channel guesses like phone/email/chat/ticket/social are unsupported. Public API rows do not include lookalike tier/score/reason. If output includes omittedFilters or warnings, report them as truthful MCP/backend safety handling rather than claiming Prospeo enforced those omitted constraints. ' +
|
|
1502
|
+
"For best-customer, top-customer, customer, or past-company lookalikes, never substitute the sender's current company/domain as the seed unless current-company peers were explicitly requested; use explicit or verified customer/account/past-company seeds and preserve geography like Germany in account or people filters. " +
|
|
1501
1503
|
prospeoCompanyAccountWorkflowGuidance,
|
|
1502
1504
|
inputSchema: {
|
|
1503
1505
|
type: "object",
|
|
@@ -1630,7 +1632,7 @@ export const leadToolDefinitions = [
|
|
|
1630
1632
|
},
|
|
1631
1633
|
{
|
|
1632
1634
|
name: "search_prospeo",
|
|
1633
|
-
description: 'Search Prospeo for people using filters and optional domainFilterId. Requires get_provider_prompt({ provider: "prospeo" }) first. Use Prospeo first for hiring-led targeting because it supports company_job_posting_hiring_for and company_job_posting_quantity; Sales Nav does not filter companies by hiring role. When targeting known accounts, call load_csv_domains for CSV-on-disk workflows or save_domain_filters for pasted/raw domain lists, then pass domainFilterId. For company lookalike/account asks, call search_prospeo_companies first, review accounts, then confirm_prospeo_company_accounts to create a domainFilterId before using search_prospeo for people. Raw domain inputs and company-name targeting are NOT supported in this MCP tool. Strategy: start with 2-3 high-signal filters (title/seniority + industry or domainFilterId + headcount), add job-posting filters for hiring-led campaigns, then tighten one filter at a time. For security, AppSec, SOC, RevOps, Demand Gen, and similar function-specific lanes, do not widen with bare seniority labels like "Head" or "Director" alone; pair them with explicit function-title keywords and inspect the sample for off-function `Head of X` leakage. Prefer person location over company HQ unless HQ is explicitly needed. `campaignOfferId` routing rule: OMIT campaignOfferId ONLY in pre-mint Phase 84 `find leads` discovery mode (validating ICP before the commit gate). In every other context — post-mint lead additions, operator-driven searches on a live campaign, any search where the intent is to persist results to a specific campaign — you MUST pass campaignOfferId so the search shows up in that campaign\'s Contact Search panel. Post-mint create-campaign-v2 watch runs MUST pass currentStep: "prospeo". Omitting campaignOfferId post-mint orphans the search from the UI. Returns normalized results with pagination.',
|
|
1635
|
+
description: 'Search Prospeo for people using filters and optional domainFilterId. Requires get_provider_prompt({ provider: "prospeo" }) first. Use Prospeo first for hiring-led targeting because it supports company_job_posting_hiring_for and company_job_posting_quantity; Sales Nav does not filter companies by hiring role. When targeting known accounts, call load_csv_domains for CSV-on-disk workflows or save_domain_filters for pasted/raw domain lists, then pass domainFilterId. For company lookalike/account asks, call search_prospeo_companies first, review accounts, then confirm_prospeo_company_accounts to create a domainFilterId before using search_prospeo for people. Raw domain inputs and company-name targeting are NOT supported in this MCP tool. Strategy: start with 2-3 high-signal filters (title/seniority + industry or domainFilterId + headcount), add job-posting filters for hiring-led campaigns, then tighten one filter at a time. For security, AppSec, SOC, RevOps, Demand Gen, and similar function-specific lanes, do not widen with bare seniority labels like "Head" or "Director" alone; pair them with explicit function-title keywords and inspect the sample for off-function `Head of X` leakage. If output includes warnings/fallback, report that the MCP retried a rejected precise title request with a safer domain-filter people search instead of claiming the original precise filter succeeded. Prefer person location over company HQ unless HQ is explicitly needed. `campaignOfferId` routing rule: OMIT campaignOfferId ONLY in pre-mint Phase 84 `find leads` discovery mode (validating ICP before the commit gate). In every other context — post-mint lead additions, operator-driven searches on a live campaign, any search where the intent is to persist results to a specific campaign — you MUST pass campaignOfferId so the search shows up in that campaign\'s Contact Search panel. Post-mint create-campaign-v2 watch runs MUST pass currentStep: "prospeo". Omitting campaignOfferId post-mint orphans the search from the UI. Returns normalized results with pagination.',
|
|
1634
1636
|
inputSchema: {
|
|
1635
1637
|
type: "object",
|
|
1636
1638
|
properties: {
|
|
@@ -2367,7 +2369,7 @@ export async function searchProspeoCompanies(input) {
|
|
|
2367
2369
|
page: safeInput?.page,
|
|
2368
2370
|
sort: safeInput?.sort,
|
|
2369
2371
|
}));
|
|
2370
|
-
return compactProspeoCompanySearchResponse(response);
|
|
2372
|
+
return compactProspeoCompanySearchResponse(response, safeInput.omittedFilters ?? []);
|
|
2371
2373
|
}
|
|
2372
2374
|
export async function confirmProspeoCompanyAccounts(input) {
|
|
2373
2375
|
if (!input?.companySearchToken) {
|
|
@@ -2404,6 +2406,7 @@ function removeUndefinedValues(input) {
|
|
|
2404
2406
|
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
|
|
2405
2407
|
}
|
|
2406
2408
|
function normalizeProspeoCompanySearchInputForMcp(input) {
|
|
2409
|
+
const omittedFilters = [];
|
|
2407
2410
|
const seedDomains = normalizeMcpSeeds(input.seedDomains, "domain");
|
|
2408
2411
|
const seedCompanies = seedDomains.length > 0
|
|
2409
2412
|
? []
|
|
@@ -2418,22 +2421,28 @@ function normalizeProspeoCompanySearchInputForMcp(input) {
|
|
|
2418
2421
|
typeof lookalike === "object" &&
|
|
2419
2422
|
!Array.isArray(lookalike) &&
|
|
2420
2423
|
"company_oids" in lookalike) {
|
|
2424
|
+
omittedFilters.push({
|
|
2425
|
+
field: "company_lookalike.company_oids",
|
|
2426
|
+
reason: "Dropped model-supplied company_oids because seedDomains/seedCompanies require backend resolution of real Prospeo IDs.",
|
|
2427
|
+
value: lookalike.company_oids,
|
|
2428
|
+
});
|
|
2421
2429
|
delete lookalike.company_oids;
|
|
2422
2430
|
removeEmptyObjectFilter(filters, "company_lookalike");
|
|
2423
2431
|
}
|
|
2424
2432
|
}
|
|
2425
|
-
|
|
2426
|
-
normalizeMcpCompanyKeywords(filters);
|
|
2427
|
-
normalizeMcpCompanyWebsiteSearch(filters);
|
|
2433
|
+
normalizeMcpSeedMatchAll(filters, inferConcreteLookalikeSeedCount(filters, seedDomains.length + seedCompanies.length), omittedFilters);
|
|
2434
|
+
normalizeMcpCompanyKeywords(filters, omittedFilters);
|
|
2435
|
+
normalizeMcpCompanyWebsiteSearch(filters, omittedFilters);
|
|
2428
2436
|
normalizeMcpCompanyIcp(filters);
|
|
2429
2437
|
stripMcpDuplicateCompanyIndustry(filters);
|
|
2430
|
-
stripMcpSeededLookalikeRiskyRefinements(filters, hasSeeds || hasMcpCompanyLookalikeOids(filters));
|
|
2438
|
+
stripMcpSeededLookalikeRiskyRefinements(filters, hasSeeds || hasMcpCompanyLookalikeOids(filters), omittedFilters);
|
|
2431
2439
|
stripMcpKeyCustomerCompanionFilters(filters);
|
|
2432
2440
|
return {
|
|
2433
2441
|
...input,
|
|
2434
2442
|
seedCompanies: seedCompanies.length > 0 ? seedCompanies : undefined,
|
|
2435
2443
|
seedDomains: seedDomains.length > 0 ? seedDomains : undefined,
|
|
2436
2444
|
filters,
|
|
2445
|
+
omittedFilters: omittedFilters.length > 0 ? omittedFilters : undefined,
|
|
2437
2446
|
};
|
|
2438
2447
|
}
|
|
2439
2448
|
function normalizeProspeoSearchInputForMcp(input) {
|
|
@@ -2446,7 +2455,7 @@ function normalizeProspeoSearchInputForMcp(input) {
|
|
|
2446
2455
|
filters,
|
|
2447
2456
|
};
|
|
2448
2457
|
}
|
|
2449
|
-
function normalizeMcpCompanyKeywords(filters) {
|
|
2458
|
+
function normalizeMcpCompanyKeywords(filters, omittedFilters = []) {
|
|
2450
2459
|
const keywords = filters.company_keywords;
|
|
2451
2460
|
if (!isPlainObject(keywords)) {
|
|
2452
2461
|
return;
|
|
@@ -2460,6 +2469,13 @@ function normalizeMcpCompanyKeywords(filters) {
|
|
|
2460
2469
|
keywords.include = uniqueStrings(validInclude);
|
|
2461
2470
|
}
|
|
2462
2471
|
else {
|
|
2472
|
+
if (keywords.exclude !== undefined) {
|
|
2473
|
+
omittedFilters.push({
|
|
2474
|
+
field: "company_keywords.exclude",
|
|
2475
|
+
reason: "Dropped exclude-only company_keywords because Prospeo company search requires a positive keyword include signal.",
|
|
2476
|
+
value: keywords.exclude,
|
|
2477
|
+
});
|
|
2478
|
+
}
|
|
2463
2479
|
delete keywords.include;
|
|
2464
2480
|
delete keywords.exclude;
|
|
2465
2481
|
}
|
|
@@ -2468,7 +2484,7 @@ function normalizeMcpCompanyKeywords(filters) {
|
|
|
2468
2484
|
}
|
|
2469
2485
|
removeEmptyObjectFilter(filters, "company_keywords");
|
|
2470
2486
|
}
|
|
2471
|
-
function normalizeMcpCompanyWebsiteSearch(filters) {
|
|
2487
|
+
function normalizeMcpCompanyWebsiteSearch(filters, omittedFilters = []) {
|
|
2472
2488
|
const websiteSearch = filters.company_website_search;
|
|
2473
2489
|
if (!isPlainObject(websiteSearch)) {
|
|
2474
2490
|
return;
|
|
@@ -2479,6 +2495,13 @@ function normalizeMcpCompanyWebsiteSearch(filters) {
|
|
|
2479
2495
|
normalizeStringArray(websiteSearch.urls).length > 0 ||
|
|
2480
2496
|
Object.entries(websiteSearch).some(([key, value]) => key.startsWith("has_") && typeof value === "boolean" && value);
|
|
2481
2497
|
if (!hasPositiveWebsiteSignal) {
|
|
2498
|
+
if (websiteSearch.exclude_keywords !== undefined) {
|
|
2499
|
+
omittedFilters.push({
|
|
2500
|
+
field: "company_website_search.exclude_keywords",
|
|
2501
|
+
reason: "Dropped website-search exclusions because Prospeo needs a positive website signal before exclude_keywords are safe.",
|
|
2502
|
+
value: websiteSearch.exclude_keywords,
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2482
2505
|
delete websiteSearch.exclude_keywords;
|
|
2483
2506
|
}
|
|
2484
2507
|
removeEmptyObjectFilter(filters, "company_website_search");
|
|
@@ -2568,11 +2591,27 @@ function stripMcpDuplicateCompanyIndustry(filters) {
|
|
|
2568
2591
|
delete filters.company_industry;
|
|
2569
2592
|
}
|
|
2570
2593
|
}
|
|
2571
|
-
function
|
|
2572
|
-
if (
|
|
2594
|
+
function inferConcreteLookalikeSeedCount(filters, explicitSeedCount) {
|
|
2595
|
+
if (explicitSeedCount > 0) {
|
|
2596
|
+
return explicitSeedCount;
|
|
2597
|
+
}
|
|
2598
|
+
const lookalike = filters.company_lookalike;
|
|
2599
|
+
if (!isPlainObject(lookalike) || !Array.isArray(lookalike.company_oids)) {
|
|
2600
|
+
return 0;
|
|
2601
|
+
}
|
|
2602
|
+
return lookalike.company_oids.filter((oid) => typeof oid === "string" && /^cccc[a-z0-9]+$/i.test(oid)).length;
|
|
2603
|
+
}
|
|
2604
|
+
function normalizeMcpSeedMatchAll(filters, concreteSeedCount, omittedFilters = []) {
|
|
2605
|
+
if (!isPlainObject(filters.company_lookalike)) {
|
|
2573
2606
|
return;
|
|
2574
2607
|
}
|
|
2575
|
-
if (filters.company_lookalike.match_all === true
|
|
2608
|
+
if (filters.company_lookalike.match_all === true &&
|
|
2609
|
+
concreteSeedCount < 2) {
|
|
2610
|
+
omittedFilters.push({
|
|
2611
|
+
field: "company_lookalike.match_all",
|
|
2612
|
+
reason: "Dropped match_all because fewer than two concrete approved lookalike seeds remained after MCP seed normalization.",
|
|
2613
|
+
value: true,
|
|
2614
|
+
});
|
|
2576
2615
|
delete filters.company_lookalike.match_all;
|
|
2577
2616
|
}
|
|
2578
2617
|
}
|
|
@@ -2582,7 +2621,7 @@ function hasMcpCompanyLookalikeOids(filters) {
|
|
|
2582
2621
|
Array.isArray(lookalike.company_oids) &&
|
|
2583
2622
|
lookalike.company_oids.some((oid) => typeof oid === "string" && oid.length > 0));
|
|
2584
2623
|
}
|
|
2585
|
-
function stripMcpSeededLookalikeRiskyRefinements(filters, hasSeeds) {
|
|
2624
|
+
function stripMcpSeededLookalikeRiskyRefinements(filters, hasSeeds, omittedFilters = []) {
|
|
2586
2625
|
if (!hasSeeds || !isPlainObject(filters.company_lookalike)) {
|
|
2587
2626
|
return;
|
|
2588
2627
|
}
|
|
@@ -2593,17 +2632,43 @@ function stripMcpSeededLookalikeRiskyRefinements(filters, hasSeeds) {
|
|
|
2593
2632
|
!filters.company_industry) {
|
|
2594
2633
|
filters.company_industry = { include: icp.industries };
|
|
2595
2634
|
}
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2635
|
+
if (filters.company_icp !== undefined) {
|
|
2636
|
+
omittedFilters.push({
|
|
2637
|
+
field: "company_icp",
|
|
2638
|
+
reason: "Dropped company_icp from seeded lookalike account search; use it as a later refinement after the seed returns a safe account sample.",
|
|
2639
|
+
value: filters.company_icp,
|
|
2640
|
+
});
|
|
2641
|
+
delete filters.company_icp;
|
|
2642
|
+
}
|
|
2643
|
+
if (filters.company_keywords !== undefined) {
|
|
2644
|
+
omittedFilters.push({
|
|
2645
|
+
field: "company_keywords",
|
|
2646
|
+
reason: "Dropped company_keywords from seeded lookalike account search to avoid Prospeo vendor 400s; use keywords as a later refinement after the seed works.",
|
|
2647
|
+
value: filters.company_keywords,
|
|
2648
|
+
});
|
|
2649
|
+
delete filters.company_keywords;
|
|
2650
|
+
}
|
|
2651
|
+
if (filters.company_website_search !== undefined) {
|
|
2652
|
+
omittedFilters.push({
|
|
2653
|
+
field: "company_website_search",
|
|
2654
|
+
reason: "Dropped company_website_search from seeded lookalike account search to avoid Prospeo vendor 400s; use website search as a later refinement after the seed works.",
|
|
2655
|
+
value: filters.company_website_search,
|
|
2656
|
+
});
|
|
2657
|
+
delete filters.company_website_search;
|
|
2658
|
+
}
|
|
2659
|
+
simplifyMcpSeededLookalikeAttributes(filters, omittedFilters);
|
|
2600
2660
|
}
|
|
2601
|
-
function simplifyMcpSeededLookalikeAttributes(filters) {
|
|
2661
|
+
function simplifyMcpSeededLookalikeAttributes(filters, omittedFilters = []) {
|
|
2602
2662
|
const attributes = filters.company_attributes;
|
|
2603
2663
|
if (!isPlainObject(attributes)) {
|
|
2604
2664
|
return;
|
|
2605
2665
|
}
|
|
2606
2666
|
if (attributes.has_api === true && attributes.has_sso === true) {
|
|
2667
|
+
omittedFilters.push({
|
|
2668
|
+
field: "company_attributes.has_sso",
|
|
2669
|
+
reason: "Dropped has_sso from the first seeded lookalike call because has_api + has_sso is safer as a two-step refinement.",
|
|
2670
|
+
value: true,
|
|
2671
|
+
});
|
|
2607
2672
|
delete attributes.has_sso;
|
|
2608
2673
|
}
|
|
2609
2674
|
}
|
|
@@ -2694,7 +2759,7 @@ function storeProspeoCompanySearchTokenRef(token) {
|
|
|
2694
2759
|
function resolveProspeoCompanySearchTokenRef(token) {
|
|
2695
2760
|
return prospeoCompanySearchTokenRefs.get(token) ?? token;
|
|
2696
2761
|
}
|
|
2697
|
-
function compactProspeoCompanySearchResponse(response) {
|
|
2762
|
+
function compactProspeoCompanySearchResponse(response, omittedFilters = []) {
|
|
2698
2763
|
if (!response || typeof response !== "object") {
|
|
2699
2764
|
return response;
|
|
2700
2765
|
}
|
|
@@ -2704,7 +2769,7 @@ function compactProspeoCompanySearchResponse(response) {
|
|
|
2704
2769
|
const sampleResults = accountResults
|
|
2705
2770
|
.slice(0, 10)
|
|
2706
2771
|
.map(compactProspeoCompanyAccount);
|
|
2707
|
-
return {
|
|
2772
|
+
return removeUndefinedValues({
|
|
2708
2773
|
success: response.success ?? true,
|
|
2709
2774
|
totalCount: typeof response.totalCount === "number"
|
|
2710
2775
|
? response.totalCount
|
|
@@ -2723,9 +2788,19 @@ function compactProspeoCompanySearchResponse(response) {
|
|
|
2723
2788
|
},
|
|
2724
2789
|
requestedFilters: response.normalizedFilters ?? response.filters ?? {},
|
|
2725
2790
|
warnings: Array.isArray(response.warnings) ? response.warnings : [],
|
|
2791
|
+
omittedFilters: omittedFilters.length > 0
|
|
2792
|
+
? sanitizeMcpFilterAdjustments(omittedFilters)
|
|
2793
|
+
: undefined,
|
|
2726
2794
|
companySearchToken: storeProspeoCompanySearchTokenRef(response.companySearchToken),
|
|
2727
2795
|
nextStep: "Review accounts, then call confirm_prospeo_company_accounts with companySearchToken and selectedCompanyIds. These accounts are not people leads yet.",
|
|
2728
|
-
};
|
|
2796
|
+
});
|
|
2797
|
+
}
|
|
2798
|
+
function sanitizeMcpFilterAdjustments(adjustments) {
|
|
2799
|
+
return adjustments.map((adjustment) => removeUndefinedValues({
|
|
2800
|
+
field: adjustment.field,
|
|
2801
|
+
reason: adjustment.reason,
|
|
2802
|
+
value: adjustment.value,
|
|
2803
|
+
}));
|
|
2729
2804
|
}
|
|
2730
2805
|
function compactProspeoCompanyAccount(account) {
|
|
2731
2806
|
const compact = {
|
|
@@ -2804,10 +2879,99 @@ export async function searchProspeo(input) {
|
|
|
2804
2879
|
throw new Error("search_prospeo does not accept filters.company.websites or filters.company.names. For known accounts, resolve names/domains into a domainFilterId with load_csv_domains or save_domain_filters. For company/account lookalikes, use search_prospeo_companies first, then confirm_prospeo_company_accounts.");
|
|
2805
2880
|
}
|
|
2806
2881
|
const safeInput = normalizeProspeoSearchInputForMcp(input);
|
|
2807
|
-
|
|
2808
|
-
|
|
2882
|
+
try {
|
|
2883
|
+
const response = await api.post(`/api/v3/prospeo/search`, safeInput);
|
|
2884
|
+
return compactProspeoSearchResponse(response);
|
|
2885
|
+
}
|
|
2886
|
+
catch (error) {
|
|
2887
|
+
const fallbackInput = buildProspeoPeopleSearchFallbackInput(safeInput, error);
|
|
2888
|
+
if (!fallbackInput) {
|
|
2889
|
+
throw error;
|
|
2890
|
+
}
|
|
2891
|
+
const fallbackResponse = await api.post(`/api/v3/prospeo/search`, fallbackInput);
|
|
2892
|
+
const keyword = String(fallbackInput.filters.person_name_or_job_title ?? "title keyword");
|
|
2893
|
+
return compactProspeoSearchResponse(fallbackResponse, {
|
|
2894
|
+
warnings: [
|
|
2895
|
+
`Prospeo rejected precise person-title filters for this domainFilterId; retried with person_name_or_job_title "${keyword}" plus seniority fallback.`,
|
|
2896
|
+
],
|
|
2897
|
+
fallback: {
|
|
2898
|
+
reason: "precise_person_title_filters_rejected",
|
|
2899
|
+
originalStatus: getErrorStatus(error),
|
|
2900
|
+
filters: fallbackInput.filters,
|
|
2901
|
+
},
|
|
2902
|
+
});
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
function getErrorStatus(error) {
|
|
2906
|
+
if (!error || typeof error !== "object") {
|
|
2907
|
+
return undefined;
|
|
2908
|
+
}
|
|
2909
|
+
const status = error.status;
|
|
2910
|
+
return typeof status === "number" ? status : undefined;
|
|
2911
|
+
}
|
|
2912
|
+
function buildProspeoPeopleSearchFallbackInput(input, error) {
|
|
2913
|
+
if (getErrorStatus(error) !== 400 || !input.domainFilterId) {
|
|
2914
|
+
return null;
|
|
2915
|
+
}
|
|
2916
|
+
const filters = input.filters;
|
|
2917
|
+
const jobTitle = filters?.person_job_title;
|
|
2918
|
+
if (!isPlainObject(jobTitle) || filters.person_name_or_job_title) {
|
|
2919
|
+
return null;
|
|
2920
|
+
}
|
|
2921
|
+
const keyword = inferProspeoPeopleFallbackKeyword(jobTitle);
|
|
2922
|
+
if (!keyword) {
|
|
2923
|
+
return null;
|
|
2924
|
+
}
|
|
2925
|
+
const fallbackFilters = {
|
|
2926
|
+
person_name_or_job_title: keyword,
|
|
2927
|
+
person_seniority: {
|
|
2928
|
+
include: [
|
|
2929
|
+
"C-Suite",
|
|
2930
|
+
"Vice President",
|
|
2931
|
+
"Head",
|
|
2932
|
+
"Director",
|
|
2933
|
+
"Manager",
|
|
2934
|
+
],
|
|
2935
|
+
},
|
|
2936
|
+
max_person_per_company: typeof filters.max_person_per_company === "number"
|
|
2937
|
+
? filters.max_person_per_company
|
|
2938
|
+
: 1,
|
|
2939
|
+
};
|
|
2940
|
+
if (isPlainObject(filters.person_location_search)) {
|
|
2941
|
+
fallbackFilters.person_location_search = filters.person_location_search;
|
|
2942
|
+
}
|
|
2943
|
+
return removeUndefinedValues({
|
|
2944
|
+
campaignOfferId: input.campaignOfferId,
|
|
2945
|
+
domainFilterId: input.domainFilterId,
|
|
2946
|
+
page: input.page,
|
|
2947
|
+
searchName: input.searchName,
|
|
2948
|
+
currentStep: input.currentStep,
|
|
2949
|
+
confirmed: input.confirmed,
|
|
2950
|
+
filters: fallbackFilters,
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
function inferProspeoPeopleFallbackKeyword(jobTitle) {
|
|
2954
|
+
const text = [
|
|
2955
|
+
typeof jobTitle.boolean_search === "string" ? jobTitle.boolean_search : "",
|
|
2956
|
+
...normalizeStringArray(jobTitle.include),
|
|
2957
|
+
].join(" ");
|
|
2958
|
+
if (/security|ciso|appsec|soc/i.test(text))
|
|
2959
|
+
return "Security";
|
|
2960
|
+
if (/platform/i.test(text))
|
|
2961
|
+
return "Platform";
|
|
2962
|
+
if (/product/i.test(text))
|
|
2963
|
+
return "Product";
|
|
2964
|
+
if (/growth|gtm|go[-\s]?to[-\s]?market/i.test(text))
|
|
2965
|
+
return "Growth";
|
|
2966
|
+
if (/sales|revenue/i.test(text))
|
|
2967
|
+
return "Sales";
|
|
2968
|
+
if (/marketing|demand/i.test(text))
|
|
2969
|
+
return "Marketing";
|
|
2970
|
+
if (/engineering|technical|developer/i.test(text))
|
|
2971
|
+
return "Engineering";
|
|
2972
|
+
return null;
|
|
2809
2973
|
}
|
|
2810
|
-
function compactProspeoSearchResponse(response) {
|
|
2974
|
+
function compactProspeoSearchResponse(response, metadata = {}) {
|
|
2811
2975
|
if (!response || typeof response !== "object") {
|
|
2812
2976
|
return response;
|
|
2813
2977
|
}
|
|
@@ -2815,7 +2979,7 @@ function compactProspeoSearchResponse(response) {
|
|
|
2815
2979
|
? response.people.results
|
|
2816
2980
|
: [];
|
|
2817
2981
|
const sampleResults = peopleResults.slice(0, 10).map(compactProspeoPerson);
|
|
2818
|
-
return {
|
|
2982
|
+
return removeUndefinedValues({
|
|
2819
2983
|
success: response.success ?? true,
|
|
2820
2984
|
searchId: response.searchId ?? null,
|
|
2821
2985
|
searchName: response.searchName ?? null,
|
|
@@ -2836,7 +3000,9 @@ function compactProspeoSearchResponse(response) {
|
|
|
2836
3000
|
sampleCount: sampleResults.length,
|
|
2837
3001
|
results: sampleResults,
|
|
2838
3002
|
},
|
|
2839
|
-
|
|
3003
|
+
warnings: metadata.warnings,
|
|
3004
|
+
fallback: metadata.fallback,
|
|
3005
|
+
});
|
|
2840
3006
|
}
|
|
2841
3007
|
function compactProspeoPerson(person) {
|
|
2842
3008
|
const currentRole = Array.isArray(person?._prospeo?.job_history)
|
package/package.json
CHANGED
|
@@ -220,6 +220,23 @@ reference to avoid long-token copy errors. Account rows are not people leads
|
|
|
220
220
|
yet. The confirmation creates the `domainFilterId` that constrains the follow-on
|
|
221
221
|
`search_prospeo` people search.
|
|
222
222
|
|
|
223
|
+
For lookalike seed selection, treat "best customer", "top customer", "past
|
|
224
|
+
companies", "companies I worked with", and similar wording as customer/account
|
|
225
|
+
or past-company seed asks. Never substitute the sender's current company or
|
|
226
|
+
domain as a lookalike seed; never use the sender's current company/domain as a
|
|
227
|
+
silent substitute unless the user explicitly asks for companies like the current
|
|
228
|
+
employer, sender company, or its direct peers/competitors. Seed priority is:
|
|
229
|
+
explicit user-provided customer/account/company/domain; verified
|
|
230
|
+
customer/worked-with/past-company evidence from research/proof; then ask for the
|
|
231
|
+
missing seed or switch to non-lookalike company filters if YOLO requires moving
|
|
232
|
+
without a seed. If using profile/work-history seeds, label them as
|
|
233
|
+
past-company/public proof clues, not confirmed customers unless the source proves
|
|
234
|
+
customer status. If the user asks for a geography like Germany, preserve it in
|
|
235
|
+
account discovery where supported (`company_location_search` or
|
|
236
|
+
`company_icp.geographic_markets`) and in follow-on people search
|
|
237
|
+
(`person_location_search`); do not drop geography when moving from lookalike
|
|
238
|
+
accounts to people leads.
|
|
239
|
+
|
|
223
240
|
Prospeo company/account search is useful when the source plan depends on
|
|
224
241
|
website traffic (`company_website_traffic`), confirmed AI Attributes including
|
|
225
242
|
`pricing`, `uses_ai`, `has_api`, `has_chrome_extension`, `has_sso`,
|
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
"useWhen": [
|
|
25
25
|
"You want Prospeo filters or domain-based search",
|
|
26
26
|
"You need company lookalike account discovery before finding people",
|
|
27
|
-
"You want companies like X, best-customer lookalikes, or accounts using AI/API/SSO/Chrome extensions",
|
|
27
|
+
"You want companies like X, best-customer/top-customer/past-company lookalikes, or accounts using AI/API/SSO/Chrome extensions",
|
|
28
28
|
"You need companies hiring for specific roles using job-posting filters",
|
|
29
29
|
"You need high deliverability from Prospeo"
|
|
30
30
|
],
|
|
31
31
|
"avoidWhen": ["You need LinkedIn activity filters"],
|
|
32
32
|
"reason": "Strong for hiring-led search, company/account lookalikes, Prospeo-specific filters, verified contacts, and domain lists.",
|
|
33
|
-
"prose": "Since you're targeting **{icp}**, I'd recommend **Prospeo**.\n\nHere's why:\n- Prospeo can discover lookalike accounts first, then turn approved accounts into a domainFilterId for people search\n- Prospeo can filter for companies hiring specific roles with job-posting filters\n- We can pair those company signals with buyer/referrer titles and verified-contact coverage\n\nFor lookalike accounts, I will show an account sample first; those account rows are not people leads yet. After approval, I will use the companySearchToken to confirm the accounts, then search people at the returned domainFilterId.\n\nShould I search Prospeo for {icp}?"
|
|
33
|
+
"prose": "Since you're targeting **{icp}**, I'd recommend **Prospeo**.\n\nHere's why:\n- Prospeo can discover lookalike accounts first, then turn approved accounts into a domainFilterId for people search\n- Prospeo can filter for companies hiring specific roles with job-posting filters\n- We can pair those company signals with buyer/referrer titles and verified-contact coverage\n\nFor lookalike accounts, I will show an account sample first; those account rows are not people leads yet. After approval, I will use the companySearchToken to confirm the accounts, then search people at the returned domainFilterId. For best-customer, top-customer, or past-company lookalikes, I will use explicit customer/account/past-company seeds, not the sender's current company, unless you explicitly ask for current-company peers.\n\nShould I search Prospeo for {icp}?"
|
|
34
34
|
},
|
|
35
35
|
"askOption": {
|
|
36
36
|
"label": "Prospeo",
|
|
@@ -99,7 +99,7 @@ Is your ICP LinkedIn-active (founders, sales, marketing, GTM, engineers)?
|
|
|
99
99
|
Need hiring-by-role filters? -> Prospeo (has company job-posting filters)
|
|
100
100
|
Need tech stack targeting? -> Apollo (has technology filters)
|
|
101
101
|
Have specific company names/domains? -> Sales Navigator for small lists or Prospeo with domainFilterId
|
|
102
|
-
Need companies like X, best-customer lookalikes, AI/API/SSO/Chrome extension filters, news/awards/integrations/key customers? -> Prospeo account discovery first
|
|
102
|
+
Need companies like X, best-customer/top-customer/past-company lookalikes, AI/API/SSO/Chrome extension filters, news/awards/integrations/key customers? -> Prospeo account discovery first; use explicit customer/account/past-company seeds, not the sender's current company unless current-company peers were requested
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
## ICP-to-Provider Quick Reference
|
|
@@ -188,6 +188,23 @@ Show the account sample first, ask approval, then pass the returned
|
|
|
188
188
|
`confirm_prospeo_company_accounts`. The confirmed `domainFilterId` constrains
|
|
189
189
|
the follow-on people search; account rows are not people leads yet.
|
|
190
190
|
|
|
191
|
+
For lookalike seed selection, treat "best customer", "top customer", "past
|
|
192
|
+
companies", "companies I worked with", and similar wording as customer/account
|
|
193
|
+
or past-company seed asks. Never substitute the sender's current company or
|
|
194
|
+
domain as a lookalike seed; never use the sender's current company/domain as a
|
|
195
|
+
silent substitute unless the user explicitly asks for companies like the current
|
|
196
|
+
employer, sender company, or its direct peers/competitors. Seed priority is:
|
|
197
|
+
explicit user-provided customer/account/company/domain; verified
|
|
198
|
+
customer/worked-with/past-company evidence from research/proof; then ask for the
|
|
199
|
+
missing seed or switch to non-lookalike company filters if YOLO requires moving
|
|
200
|
+
without a seed. If using profile/work-history seeds, label them as
|
|
201
|
+
past-company/public proof clues, not confirmed customers unless the source proves
|
|
202
|
+
customer status. If the user asks for a geography like Germany, preserve it in
|
|
203
|
+
account discovery where supported (`company_location_search` or
|
|
204
|
+
`company_icp.geographic_markets`) and in follow-on people search
|
|
205
|
+
(`person_location_search`); do not drop geography when moving from lookalike
|
|
206
|
+
accounts to people leads.
|
|
207
|
+
|
|
191
208
|
Before any provider prompt/search/scout call, move the watched campaign to
|
|
192
209
|
source selection, show `## Find Buyers Plan`, then open `request_user_input`
|
|
193
210
|
without repeating the URL. The plan must appear before the question and only
|
|
@@ -391,6 +391,22 @@ Use first for broad persona expansion, ABM/domain targeting, hiring-led targetin
|
|
|
391
391
|
- For companies like X, our best customers, lookalike accounts, companies that use AI, companies with API/SSO/Chrome extension, or
|
|
392
392
|
news/award/integration/key-customer account filters, use
|
|
393
393
|
`search_prospeo_companies` before people search.
|
|
394
|
+
- For lookalike seed selection, treat "best customer", "top customer", "past
|
|
395
|
+
companies", "companies I worked with", and similar wording as
|
|
396
|
+
customer/account or past-company seed asks. Never substitute the sender's
|
|
397
|
+
current company or domain as a lookalike seed; never use the sender's current
|
|
398
|
+
company/domain as a silent substitute unless the user explicitly asks for
|
|
399
|
+
companies like the current employer, sender company, or its direct
|
|
400
|
+
peers/competitors. Seed priority is: explicit user-provided
|
|
401
|
+
customer/account/company/domain; verified customer/worked-with/past-company
|
|
402
|
+
evidence from research/proof; then ask for the missing seed or switch to
|
|
403
|
+
non-lookalike company filters if YOLO requires moving without a seed. If using
|
|
404
|
+
profile/work-history seeds, label them as past-company/public proof clues, not
|
|
405
|
+
confirmed customers unless the source proves customer status. If the user asks
|
|
406
|
+
for a geography like Germany, preserve it in account discovery where supported
|
|
407
|
+
(`company_location_search` or `company_icp.geographic_markets`) and in
|
|
408
|
+
follow-on people search (`person_location_search`); do not drop geography when
|
|
409
|
+
moving from lookalike accounts to people leads.
|
|
394
410
|
- Use Prospeo company/account search when the ask depends on website traffic
|
|
395
411
|
(`company_website_traffic`), confirmed AI Attributes including `pricing`,
|
|
396
412
|
`uses_ai`, `has_api`, `has_chrome_extension`, `has_sso`, `has_open_source`,
|
|
@@ -87,6 +87,23 @@ backend resolves real Prospeo company IDs. Do not invent company_oids such as
|
|
|
87
87
|
placeholder IDs from examples. Only send `company_lookalike.company_oids` when
|
|
88
88
|
they are real Prospeo company IDs returned by a prior tool/search.
|
|
89
89
|
|
|
90
|
+
For lookalike seed selection, treat "best customer", "top customer", "past
|
|
91
|
+
companies", "companies I worked with", and similar wording as customer/account
|
|
92
|
+
or past-company seed asks. Never substitute the sender's current company/domain
|
|
93
|
+
as a lookalike seed, and never use the sender's current company/domain as a
|
|
94
|
+
silent substitute unless the user explicitly asks for companies like the current
|
|
95
|
+
employer, sender company, or its direct peers/competitors. Seed priority is:
|
|
96
|
+
explicit user-provided customer/account/company/domain; verified
|
|
97
|
+
customer/worked-with/past-company evidence from research/proof; then ask for the
|
|
98
|
+
missing seed or switch to non-lookalike company filters if YOLO requires moving
|
|
99
|
+
without a seed. If using profile/work-history seeds, label them as
|
|
100
|
+
past-company/public proof clues, not confirmed customers unless the source proves
|
|
101
|
+
customer status. If the user asks for a geography like Germany, preserve it in
|
|
102
|
+
account discovery where supported (`company_location_search` or
|
|
103
|
+
`company_icp.geographic_markets`) and in follow-on people search
|
|
104
|
+
(`person_location_search`); do not drop geography when moving from lookalike
|
|
105
|
+
accounts to people leads.
|
|
106
|
+
|
|
90
107
|
For structured ICP sizing, pair `company_icp.company_sizes` with
|
|
91
108
|
`company_headcount_range` when possible. The MCP normalizer derives headcount
|
|
92
109
|
ranges for `micro`, `smb`, `midmarket`, `enterprise`, and `large_enterprise`
|
|
@@ -109,15 +126,30 @@ Avoidable-400 guardrails:
|
|
|
109
126
|
- For seeded company lookalikes, keep the first call simple: seed company/domain + `company_lookalike.minimum_tier` + confirmed attributes, headcount, or industry. Do not add `company_website_search`, `company_keywords`, or `company_icp` until the account sample proves the seed works.
|
|
110
127
|
- For seeded company lookalikes, do not combine `has_api` and `has_sso` in the first call; start with `has_api` and refine after a valid account sample if SSO still matters.
|
|
111
128
|
- Do not send placeholder seed names such as `another approved best-customer seed`; use only concrete company names or domains you have actually resolved.
|
|
129
|
+
- Do not silently use the sender company as a customer lookalike seed; use
|
|
130
|
+
explicit customer/account/past-company evidence instead.
|
|
112
131
|
- If the user references another approved seed but does not name it, ask for the
|
|
113
132
|
seed or run a one-seed lookalike without `match_all`; do not invent a second
|
|
114
133
|
seed from examples, competitors, or exclusions.
|
|
115
134
|
- Prefer seed domains for single-seed lookalikes. For multi-seed `match_all`
|
|
116
|
-
lookalikes, use
|
|
117
|
-
|
|
118
|
-
`match_all
|
|
135
|
+
lookalikes, use two or more concrete approved seed domains or company names;
|
|
136
|
+
never infer the second seed from examples, competitors, or exclusions. If
|
|
137
|
+
Prospeo rejects `match_all`, the backend may retry without `match_all` and
|
|
138
|
+
return a warning. Report that as a vendor fallback, not as proof that strict
|
|
139
|
+
`match_all` succeeded. Do not mix `seedDomains` and `seedCompanies` in the
|
|
140
|
+
same call.
|
|
119
141
|
- Do not send `company_website_search.exclude_keywords` without a positive website include signal.
|
|
142
|
+
- If `search_prospeo_companies` returns `omittedFilters`, those fields were
|
|
143
|
+
intentionally not sent to Prospeo to avoid known public-API failures. Report
|
|
144
|
+
them as safety handling and keep the account sample separate from people
|
|
145
|
+
leads; use a follow-up refinement or manual review for those omitted
|
|
146
|
+
constraints instead of claiming Prospeo enforced them.
|
|
120
147
|
- For post-confirm people search, prefer `person_job_title.boolean_search` for long role synonym lists instead of many `person_job_title.include` values plus broad department/seniority filters.
|
|
148
|
+
- If `search_prospeo` returns a `warnings` array or `fallback` object after a
|
|
149
|
+
domain-filter people search, report the retry honestly. The MCP may retry a
|
|
150
|
+
Prospeo 400 from precise title filters with `person_name_or_job_title` plus
|
|
151
|
+
seniority so the run produces a usable sample without hiding the vendor
|
|
152
|
+
limitation.
|
|
121
153
|
|
|
122
154
|
Unsupported/caveats:
|
|
123
155
|
|
|
@@ -262,6 +294,13 @@ Preference rules:
|
|
|
262
294
|
- Run `company_key_customers` as a standalone first-pass filter. Do not combine `company_key_customers` with ICP, website-search, keyword, attribute, industry, or headcount filters until the standalone pass proves useful.
|
|
263
295
|
- For seeded lookalikes, avoid first-call `company_website_search`, `company_keywords`, and `company_icp`; use resolved seeds plus lookalike tier and simple attributes/headcount/industry first.
|
|
264
296
|
- For seeded lookalikes, avoid first-call `has_api` + `has_sso`; use one confirmed attribute first, then refine.
|
|
297
|
+
- For customer, best-customer, top-customer, or past-company lookalikes, never
|
|
298
|
+
use the sender's current company/domain as a silent substitute; use explicit
|
|
299
|
+
customer/account/past-company evidence, then label profile-derived seeds as
|
|
300
|
+
past-company/public proof clues unless customer status is verified.
|
|
301
|
+
- If the user gives geography like Germany, keep that geography in the company
|
|
302
|
+
discovery or follow-on people filters instead of dropping it after account
|
|
303
|
+
confirmation.
|
|
265
304
|
- Do not invent missing approved seeds. If a second seed is not named, ask for it
|
|
266
305
|
or run one seed without `match_all`.
|
|
267
306
|
- Prefer `seedDomains` for single-seed lookalikes. For multi-seed `match_all`
|