@sellable/mcp 0.1.123 → 0.1.125

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.
@@ -47,11 +47,13 @@ currentStep: "signal-discovery" })` before sampling so the watched Signal
47
47
  6. Fetch or sample engagers for promoted posts and score rough ICP fit from
48
48
  visible headline/display-name cues only. Do not enrich people during
49
49
  viability estimation.
50
- 7. Compute capacity before recommending the source: target good-fit leads
51
- (default 500 unless the parent supplies a target), reachable engagers,
50
+ 7. Compute capacity before recommending the source: source target good-fit
51
+ leads (default 300 unless the parent supplies a target), reachable engagers,
52
52
  sampled ICP-fit rate as `n/N` plus an easy percentage/range, expected usable
53
- leads per right-content post after dedupe/cleanup, and posts needed to hit
54
- the target.
53
+ leads per 100 engagers before and after a conservative dedupe/cleanup
54
+ factor, required engagers to scrape (`source target / fit rate`), average
55
+ reachable engagers per right-content post, expected usable leads per
56
+ right-content post after dedupe/cleanup, and posts needed to hit the target.
55
57
  8. Select/promote enough right-content posts to plausibly hit the target. If the
56
58
  warm Signals pool is useful but too small, return the expected warm range and
57
59
  recommend Sales Nav/Prospeo for scale instead of padding with noisy posts.
@@ -64,9 +66,12 @@ Return a concise structured result with:
64
66
  - `keyword_lanes` with timeframe, raw posts found, finalist posts reviewed
65
67
  - `selected_posts` with URL/title, author/topic, age, engager count, sampled engagers, good fits as n/N, estimated usable prospects per post, use/discard
66
68
  - `sample_leads`, if any
67
- - `approval_math` with eligible posts, target good-fit leads, sampled engagers,
68
- ICP-fit rate as `n/N` plus percentage/range, posts needed for target, selected
69
- post count, expected usable lead range, and scale fallback
69
+ - `approval_math` with eligible posts, source target good-fit leads, sampled
70
+ engagers, ICP-fit rate as `n/N` plus percentage/range, good-fit prospects per
71
+ 100 engagers, required engagers to scrape, average reachable engagers per
72
+ post, expected usable prospects per post after cleanup, posts needed for
73
+ target, selected post count, review-batch import limit, expected usable lead
74
+ range, and scale fallback
70
75
  - `estimated_good_fit_range`
71
76
  - `message_context_strength`, directional and source-specific
72
77
  - `false_positive_patterns`
@@ -82,7 +87,11 @@ Evidence standards:
82
87
  GTM/outbound/buyer pain, workflow, or role context that makes the campaign
83
88
  relevant.
84
89
  - Do not make the user infer capacity. Say, plainly, how many eligible posts
85
- exist, what percent of sampled engagers looked in-ICP, how many posts are
86
- needed for 500+ good-fit leads, and which posts you would use.
90
+ exist, how many sampled engagers looked in-ICP, how many good-fit prospects
91
+ that implies per 100 engagers, how many usable prospects one right-content
92
+ post should yield after cleanup, how many engagers must be scraped for the
93
+ 300-good-fit source target, how many posts are needed for that source target,
94
+ and which posts you would use. Also say the first campaign import remains the
95
+ bounded 15-row review batch.
87
96
  - If `fetch_post_engagers` is unavailable or fails, report that explicitly and mark the estimate lower-confidence.
88
97
  - Keep LinkedIn Engagement viable when selected posts can produce roughly 150+ ICP-fit warm prospects before final filtering, even if Sales Nav is more scalable.
@@ -26,8 +26,16 @@ Process:
26
26
  2. Preserve target role names with `CURRENT_TITLE` lookups; do not rely on seniority alone when the brief names concrete roles.
27
27
  3. When `lookup_sales_nav_filter` returns multiple title options, choose the closest semantic title match instead of the first result.
28
28
  4. Build a broad-but-reasonable baseline from role/title, geography, company size, industry/account context, and recent LinkedIn activity when relevant.
29
- 5. Run the baseline plus 1-2 refinements if the first pass is noisy or under-scaled.
30
- 6. Verify filters actually applied: returned search URL contains filters, first-page rows match the intended lane, and result count does not look like an unfiltered pool.
29
+ 5. Check scale against the source target good-fit lead count (default 300
30
+ unless the parent supplies a target). If raw preview volume or projected usable volume
31
+ is below target, do not present the tiny result as the scale fallback yet.
32
+ Loosen nonessential filters in order: remove recent-activity first, widen
33
+ adjacent title variants, widen geography/company-size constraints, and only
34
+ keep hard ICP requirements from the brief.
35
+ 6. Run the baseline plus 1-2 refinements or loosening passes if the first pass
36
+ is noisy or under-scaled. Label the final pool as constrained if it still
37
+ cannot plausibly reach the target after loosening.
38
+ 7. Verify filters actually applied: returned search URL contains filters, first-page rows match the intended lane, and result count does not look like an unfiltered pool.
31
39
 
32
40
  Return a concise structured result with:
33
41
 
@@ -36,6 +44,11 @@ Return a concise structured result with:
36
44
  - `exact_filter_recipe`
37
45
  - `lookup_ids_used`
38
46
  - `raw_result_count`
47
+ - `scale_check` with source target good-fit lead count, preview/raw volume, sampled
48
+ good fits as n/N, projected usable count, and whether the pool can reach the
49
+ target
50
+ - `loosening_attempts` with what was removed or widened when the pool was too
51
+ tight
39
52
  - `sampled_people` and good fits as n/N
40
53
  - `estimated_good_fit_range_after_cleanup`
41
54
  - `expected_acceptance_rate_range`, directional if inferred
@@ -49,5 +62,9 @@ Evidence standards:
49
62
 
50
63
  - Optimize for a useful prospect pool, not max volume at any cost.
51
64
  - Bias toward `POSTED_ON_LINKEDIN` for reply-likelihood when the pool still has enough scale.
65
+ - Do not over-tighten fallback filters into a pool that cannot be meaningfully
66
+ larger than the warm-post path. If Sales Nav is offered for scale, it should
67
+ either project to the target good-fit count or clearly say it is too tight and
68
+ name the next broadening/Prospeo option.
52
69
  - Do not hand-wave missing filter IDs.
53
70
  - If Sales Nav returns a giant unfiltered pool, discard that result and retry with valid filters before recommending it.
@@ -1,4 +1,16 @@
1
1
  export declare function normalizeTargetLeadCount(targetLeadCount: unknown, maxImportCount: number): number | undefined;
2
+ type SignalPostForImportSelection = {
3
+ likes: number;
4
+ comments: number;
5
+ };
6
+ export declare function selectSignalPostsForImport<T extends SignalPostForImportSelection>(posts: T[], options: {
7
+ targetEngagerCount?: number;
8
+ maxPostsToScrape?: number;
9
+ }): {
10
+ posts: T[];
11
+ estimatedEngagers: number;
12
+ limited: boolean;
13
+ };
2
14
  export type GetProviderPromptInput = {
3
15
  provider: "apollo" | "sales-nav" | "prospeo" | "signal-discovery";
4
16
  campaignOfferId?: string;
@@ -112,6 +124,8 @@ export type ImportLeadsInput = {
112
124
  searchName?: string;
113
125
  leadListName?: string;
114
126
  headlineICPCriteria?: string[];
127
+ targetEngagerCount?: number;
128
+ maxPostsToScrape?: number;
115
129
  rubricGuidelines?: string[];
116
130
  };
117
131
  type ImportLeadProvider = NonNullable<ImportLeadsInput["provider"]>;
@@ -213,6 +227,8 @@ export declare const leadToolDefinitions: ({
213
227
  sourceLeadListId?: undefined;
214
228
  targetLeadCount?: undefined;
215
229
  mode?: undefined;
230
+ targetEngagerCount?: undefined;
231
+ maxPostsToScrape?: undefined;
216
232
  tableId?: undefined;
217
233
  campaignName?: undefined;
218
234
  keepInSync?: undefined;
@@ -386,6 +402,8 @@ export declare const leadToolDefinitions: ({
386
402
  sourceLeadListId?: undefined;
387
403
  targetLeadCount?: undefined;
388
404
  mode?: undefined;
405
+ targetEngagerCount?: undefined;
406
+ maxPostsToScrape?: undefined;
389
407
  tableId?: undefined;
390
408
  campaignName?: undefined;
391
409
  keepInSync?: undefined;
@@ -458,6 +476,8 @@ export declare const leadToolDefinitions: ({
458
476
  sourceLeadListId?: undefined;
459
477
  targetLeadCount?: undefined;
460
478
  mode?: undefined;
479
+ targetEngagerCount?: undefined;
480
+ maxPostsToScrape?: undefined;
461
481
  tableId?: undefined;
462
482
  campaignName?: undefined;
463
483
  keepInSync?: undefined;
@@ -602,6 +622,8 @@ export declare const leadToolDefinitions: ({
602
622
  sourceLeadListId?: undefined;
603
623
  targetLeadCount?: undefined;
604
624
  mode?: undefined;
625
+ targetEngagerCount?: undefined;
626
+ maxPostsToScrape?: undefined;
605
627
  tableId?: undefined;
606
628
  campaignName?: undefined;
607
629
  keepInSync?: undefined;
@@ -688,6 +710,8 @@ export declare const leadToolDefinitions: ({
688
710
  sourceLeadListId?: undefined;
689
711
  targetLeadCount?: undefined;
690
712
  mode?: undefined;
713
+ targetEngagerCount?: undefined;
714
+ maxPostsToScrape?: undefined;
691
715
  tableId?: undefined;
692
716
  campaignName?: undefined;
693
717
  keepInSync?: undefined;
@@ -783,6 +807,8 @@ export declare const leadToolDefinitions: ({
783
807
  sourceLeadListId?: undefined;
784
808
  targetLeadCount?: undefined;
785
809
  mode?: undefined;
810
+ targetEngagerCount?: undefined;
811
+ maxPostsToScrape?: undefined;
786
812
  tableId?: undefined;
787
813
  campaignName?: undefined;
788
814
  keepInSync?: undefined;
@@ -860,6 +886,8 @@ export declare const leadToolDefinitions: ({
860
886
  sourceLeadListId?: undefined;
861
887
  targetLeadCount?: undefined;
862
888
  mode?: undefined;
889
+ targetEngagerCount?: undefined;
890
+ maxPostsToScrape?: undefined;
863
891
  tableId?: undefined;
864
892
  campaignName?: undefined;
865
893
  keepInSync?: undefined;
@@ -1475,6 +1503,8 @@ export declare const leadToolDefinitions: ({
1475
1503
  sourceLeadListId?: undefined;
1476
1504
  targetLeadCount?: undefined;
1477
1505
  mode?: undefined;
1506
+ targetEngagerCount?: undefined;
1507
+ maxPostsToScrape?: undefined;
1478
1508
  tableId?: undefined;
1479
1509
  campaignName?: undefined;
1480
1510
  keepInSync?: undefined;
@@ -1604,6 +1634,8 @@ export declare const leadToolDefinitions: ({
1604
1634
  sourceLeadListId?: undefined;
1605
1635
  targetLeadCount?: undefined;
1606
1636
  mode?: undefined;
1637
+ targetEngagerCount?: undefined;
1638
+ maxPostsToScrape?: undefined;
1607
1639
  tableId?: undefined;
1608
1640
  campaignName?: undefined;
1609
1641
  keepInSync?: undefined;
@@ -1666,6 +1698,14 @@ export declare const leadToolDefinitions: ({
1666
1698
  minItems?: undefined;
1667
1699
  maxItems?: undefined;
1668
1700
  };
1701
+ targetEngagerCount: {
1702
+ type: string;
1703
+ description: string;
1704
+ };
1705
+ maxPostsToScrape: {
1706
+ type: string;
1707
+ description: string;
1708
+ };
1669
1709
  rubricGuidelines: {
1670
1710
  type: string;
1671
1711
  items: {
@@ -1791,6 +1831,8 @@ export declare const leadToolDefinitions: ({
1791
1831
  sourceLeadListId?: undefined;
1792
1832
  targetLeadCount?: undefined;
1793
1833
  mode?: undefined;
1834
+ targetEngagerCount?: undefined;
1835
+ maxPostsToScrape?: undefined;
1794
1836
  campaignName?: undefined;
1795
1837
  keepInSync?: undefined;
1796
1838
  jobId?: undefined;
@@ -1882,6 +1924,8 @@ export declare const leadToolDefinitions: ({
1882
1924
  headlineICPCriteria?: undefined;
1883
1925
  rubricGuidelines?: undefined;
1884
1926
  mode?: undefined;
1927
+ targetEngagerCount?: undefined;
1928
+ maxPostsToScrape?: undefined;
1885
1929
  tableId?: undefined;
1886
1930
  selections?: undefined;
1887
1931
  selectionMode?: undefined;
@@ -1983,6 +2027,8 @@ export declare const leadToolDefinitions: ({
1983
2027
  sourceLeadListId?: undefined;
1984
2028
  targetLeadCount?: undefined;
1985
2029
  mode?: undefined;
2030
+ targetEngagerCount?: undefined;
2031
+ maxPostsToScrape?: undefined;
1986
2032
  tableId?: undefined;
1987
2033
  campaignName?: undefined;
1988
2034
  keepInSync?: undefined;
@@ -2060,6 +2106,8 @@ export declare const leadToolDefinitions: ({
2060
2106
  sourceLeadListId?: undefined;
2061
2107
  targetLeadCount?: undefined;
2062
2108
  mode?: undefined;
2109
+ targetEngagerCount?: undefined;
2110
+ maxPostsToScrape?: undefined;
2063
2111
  tableId?: undefined;
2064
2112
  campaignName?: undefined;
2065
2113
  keepInSync?: undefined;
@@ -2318,6 +2366,8 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
2318
2366
  searchName?: string;
2319
2367
  leadListName?: string;
2320
2368
  headlineICPCriteria?: string[];
2369
+ targetEngagerCount?: number;
2370
+ maxPostsToScrape?: number;
2321
2371
  rubricGuidelines?: string[];
2322
2372
  };
2323
2373
  }[];
@@ -2328,6 +2378,10 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
2328
2378
  jobId?: undefined;
2329
2379
  estimatedEngagers?: undefined;
2330
2380
  selectedPostCount?: undefined;
2381
+ availableSelectedPostCount?: undefined;
2382
+ targetEngagerCount?: undefined;
2383
+ maxPostsToScrape?: undefined;
2384
+ limitedSelectedPosts?: undefined;
2331
2385
  targetLeadCount?: undefined;
2332
2386
  existingCount?: undefined;
2333
2387
  createdLeadList?: undefined;
@@ -2353,6 +2407,8 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
2353
2407
  searchName?: string;
2354
2408
  leadListName?: string;
2355
2409
  headlineICPCriteria?: string[];
2410
+ targetEngagerCount?: number;
2411
+ maxPostsToScrape?: number;
2356
2412
  rubricGuidelines?: string[];
2357
2413
  };
2358
2414
  }[];
@@ -2360,6 +2416,10 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
2360
2416
  jobId?: undefined;
2361
2417
  estimatedEngagers?: undefined;
2362
2418
  selectedPostCount?: undefined;
2419
+ availableSelectedPostCount?: undefined;
2420
+ targetEngagerCount?: undefined;
2421
+ maxPostsToScrape?: undefined;
2422
+ limitedSelectedPosts?: undefined;
2363
2423
  targetLeadCount?: undefined;
2364
2424
  existingCount?: undefined;
2365
2425
  createdLeadList?: undefined;
@@ -2370,6 +2430,10 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
2370
2430
  jobId: string;
2371
2431
  estimatedEngagers: number;
2372
2432
  selectedPostCount: number;
2433
+ availableSelectedPostCount: number;
2434
+ targetEngagerCount: number | null;
2435
+ maxPostsToScrape: number | null;
2436
+ limitedSelectedPosts: boolean;
2373
2437
  targetLeadCount: number | null;
2374
2438
  message: string;
2375
2439
  error?: undefined;
@@ -2402,6 +2466,8 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
2402
2466
  searchName?: string;
2403
2467
  leadListName?: string;
2404
2468
  headlineICPCriteria?: string[];
2469
+ targetEngagerCount?: number;
2470
+ maxPostsToScrape?: number;
2405
2471
  rubricGuidelines?: string[];
2406
2472
  };
2407
2473
  }[];
@@ -2409,6 +2475,10 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
2409
2475
  jobId?: undefined;
2410
2476
  estimatedEngagers?: undefined;
2411
2477
  selectedPostCount?: undefined;
2478
+ availableSelectedPostCount?: undefined;
2479
+ targetEngagerCount?: undefined;
2480
+ maxPostsToScrape?: undefined;
2481
+ limitedSelectedPosts?: undefined;
2412
2482
  targetLeadCount?: undefined;
2413
2483
  createdLeadList?: undefined;
2414
2484
  jobResult?: undefined;
@@ -2427,6 +2497,10 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
2427
2497
  modeOptions?: undefined;
2428
2498
  estimatedEngagers?: undefined;
2429
2499
  selectedPostCount?: undefined;
2500
+ availableSelectedPostCount?: undefined;
2501
+ targetEngagerCount?: undefined;
2502
+ maxPostsToScrape?: undefined;
2503
+ limitedSelectedPosts?: undefined;
2430
2504
  existingCount?: undefined;
2431
2505
  }>;
2432
2506
  export declare function cancelLeadImport(input: CancelLeadImportInput): Promise<{
@@ -265,6 +265,46 @@ export function normalizeTargetLeadCount(targetLeadCount, maxImportCount) {
265
265
  }
266
266
  return Math.min(numeric, maxImportCount);
267
267
  }
268
+ function normalizePositiveInteger(value) {
269
+ if (value === null || value === undefined) {
270
+ return undefined;
271
+ }
272
+ const numeric = typeof value === "number" ? value : Number(value);
273
+ if (!Number.isFinite(numeric) || numeric <= 0) {
274
+ return undefined;
275
+ }
276
+ return Math.floor(numeric);
277
+ }
278
+ export function selectSignalPostsForImport(posts, options) {
279
+ const normalizedTargetEngagers = normalizePositiveInteger(options.targetEngagerCount);
280
+ const normalizedMaxPosts = normalizePositiveInteger(options.maxPostsToScrape);
281
+ if (!normalizedTargetEngagers && !normalizedMaxPosts) {
282
+ return {
283
+ posts,
284
+ estimatedEngagers: posts.reduce((sum, post) => sum + post.likes + post.comments, 0),
285
+ limited: false,
286
+ };
287
+ }
288
+ const ranked = [...posts].sort((a, b) => b.likes + b.comments - (a.likes + a.comments));
289
+ const selected = [];
290
+ let estimatedEngagers = 0;
291
+ for (const post of ranked) {
292
+ if (normalizedMaxPosts && selected.length >= normalizedMaxPosts) {
293
+ break;
294
+ }
295
+ selected.push(post);
296
+ estimatedEngagers += post.likes + post.comments;
297
+ if (normalizedTargetEngagers &&
298
+ estimatedEngagers >= normalizedTargetEngagers) {
299
+ break;
300
+ }
301
+ }
302
+ return {
303
+ posts: selected.length > 0 ? selected : ranked,
304
+ estimatedEngagers,
305
+ limited: selected.length > 0 && selected.length < posts.length,
306
+ };
307
+ }
268
308
  function toDate(value) {
269
309
  if (!value)
270
310
  return null;
@@ -967,6 +1007,14 @@ export const leadToolDefinitions = [
967
1007
  items: { type: "string" },
968
1008
  description: "Signal Discovery: headline ICP criteria (3-5 rules). Uses campaign.headlineICPCriteria if not provided.",
969
1009
  },
1010
+ targetEngagerCount: {
1011
+ type: "number",
1012
+ description: "Signal Discovery: target number of post engagers to scrape. Compute from source target good-fit leads / sampled fit rate; e.g. 300 good fits at 20% fit requires about 1500 engagers. Limits selected posts before starting scrape.",
1013
+ },
1014
+ maxPostsToScrape: {
1015
+ type: "number",
1016
+ description: "Signal Discovery: optional hard cap on selected posts to scrape after ranking selected posts by engagement.",
1017
+ },
970
1018
  rubricGuidelines: {
971
1019
  type: "array",
972
1020
  items: { type: "string" },
@@ -1845,7 +1893,7 @@ export async function searchSignals(input) {
1845
1893
  }
1846
1894
  export async function importLeads(input) {
1847
1895
  const api = getApi();
1848
- const { campaignOfferId, currentStep, sourceLeadListId: inputSourceLeadListId, searchId, targetLeadCount, mode, searchName, leadListName, headlineICPCriteria, rubricGuidelines, confirmed, } = input;
1896
+ const { campaignOfferId, currentStep, sourceLeadListId: inputSourceLeadListId, searchId, targetLeadCount, mode, searchName, leadListName, headlineICPCriteria, targetEngagerCount, maxPostsToScrape, rubricGuidelines, confirmed, } = input;
1849
1897
  assertInteractionApproval({
1850
1898
  campaignId: campaignOfferId,
1851
1899
  action: "import-leads",
@@ -1989,12 +2037,17 @@ export async function importLeads(input) {
1989
2037
  }
1990
2038
  }
1991
2039
  const uniqueSelectedPosts = Array.from(uniqueByUrl.values());
2040
+ const importSelection = selectSignalPostsForImport(uniqueSelectedPosts, {
2041
+ targetEngagerCount,
2042
+ maxPostsToScrape,
2043
+ });
2044
+ const postsToScrape = importSelection.posts;
1992
2045
  const effectiveHeadlineICPCriteria = headlineICPCriteria && headlineICPCriteria.length > 0
1993
2046
  ? headlineICPCriteria
1994
2047
  : rubricGuidelines;
1995
2048
  // Start the scrape job
1996
2049
  const result = await api.post(`/api/v3/campaigns/${campaignOfferId}/signal-leads/create`, {
1997
- posts: uniqueSelectedPosts,
2050
+ posts: postsToScrape,
1998
2051
  ...(effectiveHeadlineICPCriteria &&
1999
2052
  effectiveHeadlineICPCriteria.length > 0
2000
2053
  ? {
@@ -2014,9 +2067,15 @@ export async function importLeads(input) {
2014
2067
  leadListId: result.tableId,
2015
2068
  jobId: result.jobId,
2016
2069
  estimatedEngagers: result.estimatedEngagers,
2017
- selectedPostCount: uniqueSelectedPosts.length,
2070
+ selectedPostCount: postsToScrape.length,
2071
+ availableSelectedPostCount: uniqueSelectedPosts.length,
2072
+ targetEngagerCount: normalizePositiveInteger(targetEngagerCount) ?? null,
2073
+ maxPostsToScrape: normalizePositiveInteger(maxPostsToScrape) ?? null,
2074
+ limitedSelectedPosts: importSelection.limited,
2018
2075
  targetLeadCount: cappedTargetLeadCount ?? null,
2019
- message: `Started scraping ${uniqueSelectedPosts.length} posts (~${result.estimatedEngagers} engagers). Leads will appear as scraping completes.`,
2076
+ message: `Started scraping ${postsToScrape.length} posts (~${result.estimatedEngagers} engagers). Leads will appear as scraping completes.${importSelection.limited
2077
+ ? ` Limited from ${uniqueSelectedPosts.length} selected posts by the approved source-capacity scrape plan.`
2078
+ : ""}`,
2020
2079
  };
2021
2080
  }
2022
2081
  // === SALES NAV / PROSPEO FLOW ===
@@ -2449,7 +2508,7 @@ export async function selectPromisingPosts(input) {
2449
2508
  selectedCount: selectionResult.selectedCount,
2450
2509
  unselectedCount: selectionResult.unselectedCount,
2451
2510
  criteriaCount: selectionResult.criteriaCount,
2452
- message: `Selected ${selectionResult.selectedCount} posts with ${selectionResult.criteriaCount} ICP criteria (persisted). Criteria are already saved to the campaign. If the user has already approved the lead-source decision for create-campaign-v2, call import_leads with provider: "signal-discovery", confirmed: true, and the approved bounded targetLeadCount. Otherwise ask the user to confirm before importing.`,
2511
+ message: `Selected ${selectionResult.selectedCount} posts with ${selectionResult.criteriaCount} ICP criteria (persisted). Criteria are already saved to the campaign. If the user has already approved the lead-source decision for create-campaign-v2, call import_leads with provider: "signal-discovery", confirmed: true, the approved source-capacity targetLeadCount, and targetEngagerCount/postsNeeded from the approval math. Otherwise ask the user to confirm before importing.`,
2453
2512
  };
2454
2513
  }
2455
2514
  export async function setHeadlineICPCriteria(input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.123",
3
+ "version": "0.1.125",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -826,7 +826,9 @@ Required behavior:
826
826
  2. If Signals does not have enough recent, relevant, ICP-looking engagers,
827
827
  switch to Sales Nav with recent activity when the target can be expressed
828
828
  as title/persona/company filters. Run preview filters, inspect preview rows,
829
- validate the filters applied, and estimate scalable-fit volume.
829
+ validate the filters applied, and estimate scalable-fit volume. If the
830
+ preview/projected usable pool is below the target good-fit count, loosen
831
+ nonessential filters before presenting Sales Nav as the scale fallback.
830
832
  3. If recent-activity Sales Nav is too small or noisy, broaden to normal Sales
831
833
  Nav title + company filters and call out the weaker activity context.
832
834
  4. Use Prospeo Contact only when the campaign has a domain/account path, the
@@ -877,10 +879,13 @@ Required behavior:
877
879
  broad anchor-only lanes as fallback inventory only, and retarget before
878
880
  sampling if the first pass is broad.
879
881
  - Signals viability is based on estimated ICP-fit reachable engagers, not raw
880
- post count and not whether it beats Sales Nav scale. If 5-8 selected posts can
881
- plausibly produce ~150+ ICP-fit warm prospects before final filtering, keep
882
- Signals as a viable focused option. If stricter cleanup likely reduces that to
883
- ~100 usable prospects, say so plainly instead of discarding it.
882
+ post count and not whether it beats Sales Nav scale. Keep source-capacity
883
+ planning separate from the 15-row review-batch import: the default source
884
+ target is 300 likely good-fit leads, while `import.importLimit` remains 15 for
885
+ the first campaign review batch. If 5-8 selected posts can plausibly produce
886
+ ~150+ ICP-fit warm prospects before final filtering, keep Signals as a viable
887
+ focused option, but say it is below the default 300-lead source target unless
888
+ the math shows otherwise.
884
889
  - When Signals is viable-but-smaller and Sales Nav or Prospeo is more scalable,
885
890
  the user-facing lead review must present the choice: Signals for a warmer,
886
891
  smaller, higher-reply-upside first batch versus Sales Nav/Prospeo for broader
@@ -901,8 +906,13 @@ fit x cleanup factor = ~300-500 likely usable warm leads`.
901
906
  last 30 days, prefer the last 7-14 days when quality is comparable, and call
902
907
  out any older post as a deliberate tradeoff. Do not hide post age inside the
903
908
  raw search count
904
- - default source quality target is 500+ likely good-fit leads for scalable
905
- outbound unless the campaign or user supplies a different target. Accept
909
+ - default source quality target is 300 likely good-fit leads for scalable
910
+ outbound unless the campaign or user supplies a different source target.
911
+ Compute required engagers from the observed fit rate: `target good-fit leads /
912
+ sampled fit rate`. For example, 20 good fits per 100 engagers means a 300
913
+ good-fit source target needs about 1,500 engagers scraped; use average
914
+ reachable engagers per right-content post to calculate how many posts are
915
+ needed. Accept
906
916
  ~150+ likely ICP-fit warm prospects as viable for a focused Signals-first
907
917
  campaign or first review batch only when the user is choosing warmth over
908
918
  scale. Name the volume tradeoff in `lead-review.md` and the source approval
@@ -1025,8 +1035,10 @@ not a research memo. It must include only:
1025
1035
  activity filters. For Signals, name the selected post set. For Prospeo, name
1026
1036
  the account/domain and title recipe.
1027
1037
  - `Math:` with these labels and values:
1028
- `Eligible posts`, `Sample`, `ICP-fit`, `Target`, `Posts needed`, `Selected`,
1029
- `Expected good-fit leads`, and `Scale option`.
1038
+ `Eligible posts`, `Sample`, `ICP-fit`, `Good-fit / 100 engagers`,
1039
+ `Required engagers`, `Avg engagers/post`, `Good-fit / post`, `Source target`,
1040
+ `Posts needed`, `Selected`, `Review batch`, `Expected good-fit leads`, and
1041
+ `Scale option`.
1030
1042
  - `Why this source:` with at most two bullets.
1031
1043
  - `Watch link: {watchUrl}` only when the campaign shell exists and the host
1032
1044
  needs a handoff link.
@@ -1036,14 +1048,22 @@ Do not include keyword-lane tables, LinkedIn-post-sampled tables, sample-lead
1036
1048
  lists, or long tradeoff prose in the default approval packet. Keep those details
1037
1049
  in `lead-review.md` or campaign/source decision state. The math block is the
1038
1050
  bottom-line arithmetic that makes the recommendation obvious. For example:
1039
- `Eligible posts: 8 outbound + Claude posts. Sample: 120 engagers. ICP-fit: 52/120
1040
- (~43%, directional). Target: 500 good-fit leads. Posts needed: about 8-10 at
1041
- this fit rate. Selected: 8 posts. Expected good-fit leads: ~420-560 after
1042
- dedupe/cleanup. Scale option: Sales Nav gives more rows but loses the engaged
1043
- with this content message hook.` For Sales Nav/Prospeo alternatives, include
1044
- preview/raw volume, sampled usable rows as `n/N` plus percentage/range,
1045
- estimated usable range, and the warmth or context tradeoff. Do not use
1046
- percent-only fit rates or unsupported reply-rate claims.
1051
+ `Eligible posts: 12 outbound + Claude posts. Sample: 25/125 ICP fits. ICP-fit:
1052
+ ~20%, directional. Good-fit / 100 engagers: ~20 before cleanup. Required
1053
+ engagers: ~1,500 to reach 300 good-fit prospects. Avg engagers/post: ~300
1054
+ reachable. Good-fit / post: ~60. Source target: 300 good-fit leads. Posts
1055
+ needed: about 5 right-content posts. Selected: 5 posts. Review batch: import
1056
+ only 15 campaign rows first. Expected good-fit leads: ~300 before stricter
1057
+ filtering. Scale option: Sales Nav can broaden to founder/GTM/RevOps filters if
1058
+ you want colder scale, but loses the engaged-with-this-content message hook.` If
1059
+ the selected posts only support the 15-row review batch or a smaller warm pilot,
1060
+ say that plainly and do not imply the lane can hit the source target. For
1061
+ Sales Nav/Prospeo
1062
+ alternatives, include preview/raw volume, sampled usable rows as `n/N` plus
1063
+ percentage/range, estimated usable range, and the warmth or context tradeoff.
1064
+ If Sales Nav preview volume is too tight, broaden nonessential filters before
1065
+ using it as the scale option, or label it constrained and name the next fallback.
1066
+ Do not use percent-only fit rates or unsupported reply-rate claims.
1047
1067
 
1048
1068
  The first sentence of the visible decision must make the actual choice clear:
1049
1069
  `I recommend {primary source} using {exact filter/source recipe}. The runner-up
@@ -1091,12 +1111,16 @@ For Signals, default to a quick capacity test before final recommendation. Pick
1091
1111
  a narrow first sample of fresh, high-density posts, and in campaign-attached
1092
1112
  watch runs promote those posts with `select_promising_posts` before sampling so
1093
1113
  the user sees which posts are being tested. Then sample engagers, show ICP-fit
1094
- rate as `n/N` plus percentage/range, calculate how many right-content posts are
1095
- needed to reach the target good-fit lead count, and select/promote enough
1096
- right-content posts if the lane can hit the target. If the first sample is good
1097
- but volume is low, say how many more posts would be needed and offer the Sales
1098
- Nav/Prospeo scale alternative instead of recommending an under-sized source as
1099
- if it can produce 500+ leads.
1114
+ rate as `n/N` plus percentage/range, convert that to good-fit prospects per 100
1115
+ engagers, estimate average reachable engagers per right-content post, apply a
1116
+ conservative dedupe/cleanup factor, calculate required engagers to scrape,
1117
+ calculate good-fit prospects per post, then calculate how many right-content
1118
+ posts are needed to reach the source target good-fit lead count. Select/promote
1119
+ only enough right-content posts to hit that source target; do not scrape every
1120
+ promoted sample post by default. If the first sample is good but volume is low,
1121
+ say how many more posts would be needed and offer the Sales Nav/Prospeo scale
1122
+ alternative instead of recommending an under-sized source as if it can produce
1123
+ the default 300 good-fit source target.
1100
1124
 
1101
1125
  Keep discarded paths and full sample rows in the campaign/source decision
1102
1126
  context. In explicit debug/UAT runs they may also be copied into
@@ -525,7 +525,7 @@
525
525
  "suppliedProfilesOrCsv": "supplied-list",
526
526
  "explicitCompare": "compare-requested-sources"
527
527
  },
528
- "quickViabilityRule": "Run only enough of the current lane to decide whether it can supply relevant, reachable ICP-looking leads. For Signals, viability requires sampled ICP-fit rate plus posts-needed math against the target good-fit lead count, not raw post count. Stop on the first viable source unless the user asked for comparison.",
528
+ "quickViabilityRule": "Run only enough of the current lane to decide whether it can supply relevant, reachable ICP-looking leads. For Signals, viability requires sampled ICP-fit rate normalized to good-fit prospects per 100 engagers, required engagers to scrape, average reachable engagers per right-content post, projected good-fit prospects per post after cleanup, and posts-needed math against the 300-good-fit source target, not raw post count or the 15-row review-batch import limit. Stop on the first viable source unless the user asked for comparison.",
529
529
  "parallelAllowedOnlyWhen": [
530
530
  "user explicitly requested source comparison",
531
531
  "resuming already-started parallel scouts",
@@ -568,7 +568,7 @@
568
568
  },
569
569
  "when": "after_source_selection_checkpoint_before_first_source_attempt",
570
570
  "requiresPriorVisibleStep": "pick-provider",
571
- "rule": "Choose the first visible source lane from explicit source direction when present, otherwise default to Signal Discovery. Only run this after the Pick Provider source-selection checkpoint has been made visible and explained in chat/watchNarration. Send watchNarration with stage find-leads that says why this lane is being tried now, what quick sample or filter gate will pass/fail it, and why that helps this campaign. For Signal Discovery sampling, make it clear this is a viability path before lead import: generate intersection-first keyword lanes that combine the campaign anchor with the buyer pain/use case/ICP role, not broad anchor-only lanes. For Claude/GTM campaigns, prefer lanes like Claude outbound, Claude Code LinkedIn outreach, AI SDR Claude, GTM automation Claude, and founder-led sales Claude over generic Claude Code, MCP, AI agents, or agentic coding. If the first pass is broad, retarget around the campaign wedge before selecting posts. Promote/select the first narrow sample posts with select_promising_posts before fetch_post_engagers so the watched table shows the exact posts being sampled; the guide copy should say Codex is pulling sample engagers from these posts to confirm the ICP is actually engaging and estimate whether enough right-content posts exist for the target lead count. Before final source approval, compute eligible posts, sampled ICP-fit rate, target good-fit lead count, posts needed, selected posts, expected good-fit range, and scale fallback. If the lane fails, update currentStep and watchNarration before moving to Sales Nav recent activity, normal Sales Nav, or Prospeo."
571
+ "rule": "Choose the first visible source lane from explicit source direction when present, otherwise default to Signal Discovery. Only run this after the Pick Provider source-selection checkpoint has been made visible and explained in chat/watchNarration. Send watchNarration with stage find-leads that says why this lane is being tried now, what quick sample or filter gate will pass/fail it, and why that helps this campaign. For Signal Discovery sampling, make it clear this is a viability path before lead import: generate intersection-first keyword lanes that combine the campaign anchor with the buyer pain/use case/ICP role, not broad anchor-only lanes. For Claude/GTM campaigns, prefer lanes like Claude outbound, Claude Code LinkedIn outreach, AI SDR Claude, GTM automation Claude, and founder-led sales Claude over generic Claude Code, MCP, AI agents, or agentic coding. If the first pass is broad, retarget around the campaign wedge before selecting posts. Promote/select the first narrow sample posts with select_promising_posts before fetch_post_engagers so the watched table shows the exact posts being sampled; the guide copy should say Codex is pulling sample engagers from these posts to confirm the ICP is actually engaging and estimate whether enough right-content posts exist for the 300-good-fit source target. Before final source approval, compute eligible posts, sampled ICP-fit rate, good-fit prospects per 100 engagers, required engagers to scrape, average reachable engagers per right-content post, expected good-fit prospects per post after cleanup, source target good-fit lead count, posts needed, selected posts, expected good-fit range, and scale fallback. If the lane fails, update currentStep and watchNarration before moving to Sales Nav recent activity, normal Sales Nav, or Prospeo."
572
572
  },
573
573
  {
574
574
  "action": "run_first_campaign_attached_source_search",
@@ -617,7 +617,7 @@
617
617
  "action": "run_subskill",
618
618
  "target": "find-leads",
619
619
  "mode": "campaign-attached-required",
620
- "sourceScoutRule": "Shell-first flow requires the CampaignOffer campaignId from durable state. Pass campaignId as campaignOfferId into every provider prompt/search that can persist source state (`get_provider_prompt({ provider, campaignOfferId, confirmed: true })`, `search_signals`, `search_sales_nav`, `search_prospeo`) and include currentStep for tools that accept it so the user can watch the selected source inside the campaign. Use the default sequential source viability funnel when the user did not specify a source: Signal Discovery first, then Sales Nav with recent activity, then general Sales Nav, then Prospeo only as fallback. For Signal Discovery, use intersection-first keyword lanes that combine the campaign anchor with the buyer pain/use case/ICP role; broad anchor-only lanes are fallback inventory, not selectable source lanes while narrower wedge lanes exist. Do not recommend from raw post count; sample engagers, calculate ICP-fit rate, target good-fit lead count, posts needed, selected posts, expected good-fit range, and scale fallback. Stop on the first viable source unless the user explicitly asked to compare. If the user names hiring signals, domains/accounts, supplied lists, posts/comments, or title/persona filters, start with the matching source instead. Parallel source scouts only when the user requested comparison, an existing parallel run is being resumed, or the first viable source is borderline and one cheap fallback check is needed. The later import_leads call must use the same campaignOfferId. Do not import, confirm, enrich, queue, or start leads during source discovery."
620
+ "sourceScoutRule": "Shell-first flow requires the CampaignOffer campaignId from durable state. Pass campaignId as campaignOfferId into every provider prompt/search that can persist source state (`get_provider_prompt({ provider, campaignOfferId, confirmed: true })`, `search_signals`, `search_sales_nav`, `search_prospeo`) and include currentStep for tools that accept it so the user can watch the selected source inside the campaign. Use the default sequential source viability funnel when the user did not specify a source: Signal Discovery first, then Sales Nav with recent activity, then general Sales Nav, then Prospeo only as fallback. For Signal Discovery, use intersection-first keyword lanes that combine the campaign anchor with the buyer pain/use case/ICP role; broad anchor-only lanes are fallback inventory, not selectable source lanes while narrower wedge lanes exist. Do not recommend from raw post count; sample engagers, calculate ICP-fit rate, good-fit prospects per 100 engagers, required engagers to scrape, average reachable engagers per right-content post, expected good-fit prospects per post after cleanup, source target good-fit lead count (default 300), posts needed, selected posts, expected good-fit range, and scale fallback. For Sales Nav fallback, do not over-tighten filters into a tiny pool; if preview/projected usable volume is below the target, loosen nonessential filters before presenting Sales Nav as the scale option and label it constrained if it still cannot reach the target. Stop on the first viable source unless the user explicitly asked to compare. If the user names hiring signals, domains/accounts, supplied lists, posts/comments, or title/persona filters, start with the matching source instead. Parallel source scouts only when the user requested comparison, an existing parallel run is being resumed, or the first viable source is borderline and one cheap fallback check is needed. The later import_leads call must use the same campaignOfferId and, for Signal Discovery, targetEngagerCount from the approved math so it does not scrape every selected sample post by default. Do not import, confirm, enrich, queue, or start leads during source discovery."
621
621
  },
622
622
  {
623
623
  "action": "optional_debug_artifacts",
@@ -720,9 +720,14 @@
720
720
  "Eligible posts",
721
721
  "Sample",
722
722
  "ICP-fit",
723
- "Target",
723
+ "Good-fit / 100 engagers",
724
+ "Required engagers",
725
+ "Avg engagers/post",
726
+ "Good-fit / post",
727
+ "Source target",
724
728
  "Posts needed",
725
729
  "Selected",
730
+ "Review batch",
726
731
  "Expected good-fit leads",
727
732
  "Scale option",
728
733
  "Why this source"
@@ -764,13 +769,20 @@
764
769
  "reachable engagers count",
765
770
  "sampled engagers count",
766
771
  "sampled ICP fits as n/N plus percentage or range",
767
- "target good-fit lead count",
772
+ "good-fit prospects per 100 engagers",
773
+ "required engagers to scrape",
774
+ "average reachable engagers per post",
775
+ "expected good-fit prospects per post after cleanup",
776
+ "dedupe or cleanup factor",
777
+ "source target good-fit lead count default 300",
768
778
  "posts needed for target",
769
779
  "selected posts count",
780
+ "15-row review-batch import limit",
770
781
  "estimated usable prospects range",
771
782
  "scale alternative source",
772
783
  "scale alternative raw or preview volume",
773
784
  "scale alternative sampled usable rows as n/N plus percentage or range",
785
+ "Sales Nav broadening or loosening attempts when fallback volume is too tight",
774
786
  "warmth or message-context tradeoff",
775
787
  "what to choose if the user wants more volume"
776
788
  ],
@@ -794,7 +806,7 @@
794
806
  "doNotCompressToSummaryOnly": false,
795
807
  "doNotRenderArtifactLinksOnly": true,
796
808
  "sourceRecommendationReadyWatchRule": "When the source recommendation decision card is ready in chat with counts and sample quality, and before asking for source approval, call update_campaign with leadSourceType `new`, leadSourceProvider set to the recommended primary provider, currentStep set to that provider lane (`sales-nav`, `signal-discovery`, or `prospeo`), and find-leads watchNarration. If the recommendation changed from the lane last sampled, switch the watched provider page to the recommended lane first so the user can inspect what they are approving. Use a headline like `Review the source in Codex`, body copy that says the browser is showing the evaluated source/results, and nextAction like `Approve in Codex`. Do not keep future-tense copy like `I'll show a source recommendation` after the decision is visible. Include a safety note that no leads import until the user approves the source.",
797
- "chatRenderRule": "Show a compact rendered-Markdown decision card only, never a fenced code block. The first sentence must make the decision explicit: 'I recommend {primary source} using {exact filter/source recipe}. The runner-up is {source} because {reason}.' Use only these sections: Lead source decision, Source recommendation, Math, Why this source, and the concrete approval question. The Math section must include labeled lines for Eligible posts, Sample, ICP-fit, Target, Posts needed, Selected, Expected good-fit leads, and Scale option. Show fit as n/N plus an easy percentage or range, never percent-only. Do not include keyword-lane tables, LinkedIn-post-sampled tables, sample-lead lists, or long tradeoff prose in the default approval packet; keep those details in lead-review.md or campaign/source decision state. Do not forecast connection acceptance rates, reply rates, meetings, pipeline, revenue, or ROI unless the user supplied verified benchmark data for this exact workspace/sender. For Signals, do not recommend from raw post count. Compute eligible right-content posts, reachable engagers, sampled ICP-fit rate, target good-fit lead count, posts needed, selected posts, expected good-fit range, and scale fallback. If the user wants more volume than the warm post-engagement estimate, explicitly say which Sales Nav or Prospeo alternative provides that volume and what message-context warmth is lost. If no engagers could be fetched, say that explicitly and lower confidence. Do not show plain filesystem paths unless links cannot be created."
809
+ "chatRenderRule": "Show a compact rendered-Markdown decision card only, never a fenced code block. The first sentence must make the decision explicit: 'I recommend {primary source} using {exact filter/source recipe}. The runner-up is {source} because {reason}.' Use only these sections: Lead source decision, Source recommendation, Math, Why this source, and the concrete approval question. The Math section must include labeled lines for Eligible posts, Sample, ICP-fit, Good-fit / 100 engagers, Required engagers, Avg engagers/post, Good-fit / post, Source target, Posts needed, Selected, Review batch, Expected good-fit leads, and Scale option. Show fit as n/N plus an easy percentage or range, never percent-only. Do not include keyword-lane tables, LinkedIn-post-sampled tables, sample-lead lists, or long tradeoff prose in the default approval packet; keep those details in lead-review.md or campaign/source decision state. Do not forecast connection acceptance rates, reply rates, meetings, pipeline, revenue, or ROI unless the user supplied verified benchmark data for this exact workspace/sender. For Signals, do not recommend from raw post count. Compute eligible right-content posts, reachable engagers, sampled ICP-fit rate, good-fit prospects per 100 engagers, required engagers to scrape, average reachable engagers per right-content post, expected good-fit prospects per post after cleanup, source target good-fit lead count defaulting to 300, posts needed, selected posts, expected good-fit range, and scale fallback. Distinguish source-capacity math from the 15-row review-batch import limit. If the selected warm posts only support a review batch or smaller pilot, say they do not hit 300 instead of calling them scalable. If the user wants more volume than the warm post-engagement estimate, explicitly say which Sales Nav or Prospeo alternative provides that volume and what message-context warmth is lost; Sales Nav must be broadened/loosened before it is presented as the scale option if the initial filter pool is too tight. If no engagers could be fetched, say that explicitly and lower confidence. Do not show plain filesystem paths unless links cannot be created."
798
810
  },
799
811
  {
800
812
  "action": "render_post_lead_parallel_progress",
@@ -816,7 +828,7 @@
816
828
  {
817
829
  "action": "ask_continue_revise_or_confirm_only_if_needed",
818
830
  "approvalQuestionRule": "If asking the user, the question and options must name the concrete decision. Prefer: 'Approve Sales Nav with {filters}, or use warmer Signals instead?' over 'Approve this lead source?' Option labels must name the source choice, such as 'Approve Sales Nav filters', 'Use warmer Signals posts', 'Try Prospeo/account search', and 'Revise source'.",
819
- "approvalMathRule": "Immediately above the approval question, render the compact Math block: given this many eligible relevant posts, this many reachable engagers, this sampled ICP fit as n/N plus percentage/range, this target good-fit lead count, and this many posts needed, we expect this usable lead range from the selected posts; if the user wants more volume, name the Sales Nav or Prospeo alternative, its preview/sample math, and the colder/weaker-context tradeoff. Do not ask the approval question until this math is visible.",
831
+ "approvalMathRule": "Immediately above the approval question, render the compact Math block: given this many eligible relevant posts, this many reachable engagers, this sampled ICP fit as n/N plus percentage/range, this many good-fit prospects per 100 engagers, this many required engagers to scrape, this average reachable engagers per post, this expected good-fit prospects per post after cleanup, this source target good-fit lead count defaulting to 300, and this many posts needed, we expect this usable lead range from the selected posts; also state that approval only imports a 15-row campaign review batch first after the source list is materialized. If the user wants more volume, name the Sales Nav or Prospeo alternative, its preview/sample math, any Sales Nav loosening used to avoid an over-tight pool, and the colder/weaker-context tradeoff. Do not ask the approval question until this math is visible.",
820
832
  "autoContinueWhen": {
821
833
  "status": "confirmed",
822
834
  "confidenceIn": [
@@ -902,10 +914,10 @@
902
914
  "manifest": "lead-source-intake.json",
903
915
  "defaultWhenMissing": "escalate_to_re_run_find_leads_with_campaignOfferId",
904
916
  "branches": {
905
- "normal-discovery": "Reuse the campaign-attached source selections/searches created during find-leads. Do not replay searches in Step 13; if no selected source/search exists, route back to find-leads with campaignOfferId.",
917
+ "normal-discovery": "Reuse the campaign-attached source selections/searches created during find-leads. Do not replay searches in Step 13; if no selected source/search exists, route back to find-leads with campaignOfferId. Provider import materializes the source lead list against the approved source-capacity plan, not the 15-row review batch.",
906
918
  "supplied-linkedin-profiles": "Use the campaign-attached preview/list choice. Materialize the CSV into a source lead list when needed, then confirm_lead_list({ sourceLeadListId: leadListId, targetLeadCount: import.importLimit }).",
907
919
  "existing-lead-list": "Use the campaign-attached existingLeadListId as selectedLeadListId and call confirm_lead_list({ sourceLeadListId: existingLeadListId, targetLeadCount: import.importLimit }).",
908
- "supplied-domains": "Reuse the campaign-attached domainFilterId and Prospeo search created during find-leads, then import_leads({ campaignOfferId, targetLeadCount: import.importLimit })."
920
+ "supplied-domains": "Reuse the campaign-attached domainFilterId and Prospeo search created during find-leads, then import_leads({ campaignOfferId, targetLeadCount: sourceTargetGoodFitLeadCount default 300 }) to materialize the source list before confirm_lead_list imports the bounded review batch."
909
921
  },
910
922
  "persistMaterializedIds": [
911
923
  "leadListId",
@@ -925,7 +937,7 @@
925
937
  },
926
938
  {
927
939
  "action": "watch_mode_orient",
928
- "watchNarrationRule": "Before import_leads or confirm_lead_list starts, align the guide with chat by setting review-batch watchNarration to current-tense import copy. Also persist the approved source provider/currentStep first: Sales Nav approval must set leadSourceProvider `sales-nav` and currentStep `sales-nav`, Signals approval must set `signal-discovery`, and Prospeo approval must set `prospeo`. Use a headline like `Importing the review batch`; explain that the browser is on the approved source lane while Codex imports only the bounded 15-row review batch; include a no-launch safety note."
940
+ "watchNarrationRule": "Before import_leads or confirm_lead_list starts, align the guide with chat by setting review-batch watchNarration to current-tense source materialization copy. Also persist the approved source provider/currentStep first: Sales Nav approval must set leadSourceProvider `sales-nav` and currentStep `sales-nav`, Signals approval must set `signal-discovery`, and Prospeo approval must set `prospeo`. Use a headline like `Preparing the source list`; explain that the browser is on the approved source lane while Codex materializes only the approved source-capacity plan, then imports only the bounded 15-row review batch into the campaign table; include a no-launch safety note."
929
941
  },
930
942
  {
931
943
  "tool": "import_leads",
@@ -934,7 +946,12 @@
934
946
  "campaignOfferId",
935
947
  "targetLeadCount"
936
948
  ],
937
- "targetLeadCountFromConfig": "import.importLimit (15)",
949
+ "targetLeadCountFromConfig": "sourceTargetGoodFitLeadCount (default 300) for provider source-list materialization, not import.importLimit",
950
+ "signalDiscoveryRequiredFields": [
951
+ "targetEngagerCount from approved source math",
952
+ "maxPostsToScrape or selected post plan when available"
953
+ ],
954
+ "signalDiscoveryRule": "For Signal Discovery, pass targetEngagerCount = ceil(sourceTargetGoodFitLeads / sampledFitRateAfterCleanup) and maxPostsToScrape = postsNeeded when known. This prevents import_leads from scraping every selected/promoted sample post by default. The first campaign review batch is still limited by confirm_lead_list targetLeadCount import.importLimit.",
938
955
  "onZeroLeads": "escalate_hard_fail",
939
956
  "modeAddHandshake": {
940
957
  "firstCallReturns": "needsModeSelection=true + existingLeadListId",
@@ -46,7 +46,9 @@ Supported branches:
46
46
  - **No manifest / normal discovery** — reuse the approved provider source that
47
47
  is already attached to this campaign, then import. Never call a bare
48
48
  `import_leads({ campaignOfferId })` for Signal Discovery unless selected posts
49
- already exist on the campaign.
49
+ already exist on the campaign. Provider import materializes the source lead
50
+ list from the approved source-capacity plan; it is not the same target as the
51
+ 15-row campaign review batch.
50
52
  - **Supplied LinkedIn profile CSV** — confirm `load_csv_linkedin_leads` only
51
53
  after rubrics and the approved message set are ready. Batch/materialize the
52
54
  uploaded CSV into a Sellable lead-list table. Persist the returned
@@ -65,7 +67,8 @@ Supported branches:
65
67
  - **Supplied domain/account CSV** — reuse or confirm `load_csv_domains`, keep
66
68
  its `domainFilterId`, run a campaign-associated Prospeo people search with
67
69
  `campaignOfferId`, provider prompt preflight, and `domainFilterId`, then
68
- import the review batch from that search.
70
+ materialize the source list from that search before importing the bounded
71
+ review batch into the campaign table.
69
72
 
70
73
  ## Campaign-Attached Source Contract
71
74
 
@@ -90,12 +93,16 @@ the domain constraint.
90
93
 
91
94
  ## First-Import Path
92
95
 
93
- Default case — no existing lead list bound to the campaign:
96
+ Default case — no existing lead list bound to the campaign. This creates or
97
+ fills the source lead list; it does **not** clone rows into the campaign table:
94
98
 
95
99
  ```text
96
100
  import_leads({
97
101
  campaignOfferId,
98
- targetLeadCount: <importLimit from auto-execute.yaml; default 15>
102
+ targetLeadCount: <sourceTargetGoodFitLeads; default 300>,
103
+ // Signal Discovery only:
104
+ targetEngagerCount: <ceil(sourceTargetGoodFitLeads / sampledFitRateAfterCleanup)>,
105
+ maxPostsToScrape: <postsNeeded from approved math>
99
106
  })
100
107
  ```
101
108
 
@@ -104,6 +111,14 @@ and should already be saved on the campaign before import. Response returns
104
111
  `{ imported, skipped, duplicates }` — surface all three to the escalation logic
105
112
  so dedup ratios are visible.
106
113
 
114
+ For Signal Discovery, do not scrape every currently selected/promoted sample
115
+ post by default. Before `import_leads`, reconcile selected posts with the
116
+ approved source math. If the math says 20 good fits per 100 engagers and the
117
+ source target is 300 good-fit leads, pass `targetEngagerCount` around 1,500 and
118
+ only enough posts to reach that engager count. The subsequent
119
+ `confirm_lead_list` call still uses `targetLeadCount: <importLimit>` so only the
120
+ bounded review batch enters the campaign table.
121
+
107
122
  For supplied direct lists, `confirm_lead_list` must receive
108
123
  `targetLeadCount: <importLimit>` or explicit `sourceRowIds` so a 100-row source
109
124
  imports only the bounded review batch before greenlight.
@@ -89,6 +89,20 @@ You must estimate:
89
89
  - how many sampled people matched the rough headline rubric
90
90
  - `passRate`
91
91
  - sampled pass rate used for extrapolation
92
+ - `goodFitPer100Engagers`
93
+ - normalized fit rate as good-fit prospects per 100 engagers, before and
94
+ after a conservative dedupe/cleanup factor
95
+ - `avgReachableEngagersPerPost`
96
+ - average reachable engagers per right-content post used for capacity math
97
+ - `goodFitProspectsPerPost`
98
+ - expected good-fit prospects per right-content post after dedupe/cleanup
99
+ - `postsNeededForTarget`
100
+ - number of right-content posts needed to reach the target good-fit lead
101
+ count, defaulting to 300 unless the campaign says otherwise
102
+ - `requiredEngagersToScrape`
103
+ - `ceil(targetGoodFitLeads / sampledFitRate)`; for example 20 good-fit
104
+ prospects per 100 engagers means a 300-good-fit source target needs about
105
+ 1,500 engagers scraped
92
106
  - `projectedRange`
93
107
  - the conservative range implied by the observed sample
94
108
 
@@ -112,7 +126,7 @@ Use conservative logic:
112
126
  - stale posts
113
127
  - adjacent communities that are not actual buyers
114
128
  4. Explain the pass-through briefly and explicitly:
115
- - preferred: "`sampledCount`: 40, `passCount`: 9, `passRate`: ~22%, `recentStrongPostCount`: 15-25, `freshEnoughPostCount`: 8-12, `avgUsableEngagersPerStrongPost`: 20-40, `projectedRange`: 210-600"
129
+ - preferred: "`sampledCount`: 40, `passCount`: 9, `passRate`: ~22%, `goodFitPer100Engagers`: ~22 before cleanup / ~13-16 after cleanup, `requiredEngagersToScrape`: ~1,875-2,300 for a 300-good-fit target after cleanup, `avgReachableEngagersPerPost`: 240, `goodFitProspectsPerPost`: ~31-38, `postsNeededForTarget`: ~8-10, `recentStrongPostCount`: 15-25, `freshEnoughPostCount`: 8-12, `projectedRange`: 300-380 from selected posts"
116
130
  - fallback: "I could not fetch engagers, so this is inferred from post and author quality only"
117
131
  5. If the evidence is too weak, say so and return a low-confidence estimate instead of pretending precision.
118
132
  6. Do not manually enumerate dozens of engagers in the final answer. Summarize the sample by `sampledCount`, `passCount`, `passRate`, `projectedRange`, and 3-6 representative examples.
@@ -364,17 +378,21 @@ For `create-campaign-v2` source approval, do not treat the default
364
378
  `selectionTarget` of 3 posts as enough by itself. Before the final source
365
379
  recommendation, estimate source capacity from real sample math:
366
380
 
367
- - target good-fit leads (default 500 unless the campaign says otherwise)
381
+ - source target good-fit leads (default 300 unless the campaign says otherwise)
368
382
  - eligible right-content posts by lane/content type
369
383
  - reachable engagers from those posts
370
384
  - sampled ICP-fit rate as `n/N` plus an easy percentage/range
385
+ - good-fit prospects per 100 engagers before and after conservative cleanup
386
+ - required engagers to scrape for the source target
387
+ - average reachable engagers per right-content post
371
388
  - expected good-fit leads per selected post after dedupe/cleanup
372
389
  - posts needed to reach the target
373
390
 
374
- Then select enough right-content posts to plausibly hit the target. If the math
375
- says the warm post lane only supports a smaller first batch, say that and name
376
- the Sales Nav or Prospeo scale fallback rather than padding the selection with
377
- noisy posts.
391
+ Then select the smallest right-content post set that plausibly hits the source
392
+ target. Do not scrape every promoted sample post by default; promoted sampling
393
+ state and final scrape plan are separate. If the math says the warm post lane
394
+ only supports a smaller first batch, say that and name the Sales Nav or Prospeo
395
+ scale fallback rather than padding the selection with noisy posts.
378
396
 
379
397
  ```json
380
398
  select_promising_posts({
@@ -420,13 +438,16 @@ as `select_promising_posts`; ask a clear yes/no question and wait for the user
420
438
  to say **yes**.
421
439
 
422
440
  For `create-campaign-v2` after the user has already approved the lead-source
423
- decision, that approval is the explicit confirmation for the bounded review
424
- batch. In that tail flow, follow `create-campaign-v2-tail`: call
441
+ decision, that approval is the explicit confirmation to materialize the approved
442
+ source list and then import only the bounded campaign review batch. In that
443
+ tail flow, follow `create-campaign-v2-tail`: call
425
444
  `select_promising_posts({ campaignOfferId, selectionMode: "replace",
426
445
  selections, headlineICPCriteria })`, then call
427
446
  `import_leads({ campaignOfferId, provider: "signal-discovery",
428
- targetLeadCount })` for the approved review batch without asking for another
429
- yes/no gate. Do not import more than the approved bounded batch.
447
+ targetLeadCount, targetEngagerCount, maxPostsToScrape })` for the approved
448
+ source-capacity plan without asking for another yes/no gate. Then
449
+ `confirm_lead_list` imports only the bounded review batch into the campaign
450
+ table. Do not confuse the source scrape target with the 15-row review batch.
430
451
 
431
452
  The promotion/select step is required campaign state. Use post IDs from the
432
453
  current campaign-scoped search result, not stale IDs copied from a source review
@@ -434,12 +455,14 @@ or scout summary. If `select_promising_posts` returns `selectedCount: 0`, stop:
434
455
  do not call `import_leads`, do not switch providers automatically, and do not
435
456
  claim the source failed. Re-run a narrow campaign-scoped search to recover
436
457
  current post rows, or have the user promote the posts in the UI and then retry
437
- the bounded import.
458
+ the source-list materialization.
438
459
 
439
460
  ```json
440
461
  import_leads({
441
462
  "campaignOfferId": "cmp_xxx",
442
- "targetLeadCount": 500
463
+ "targetLeadCount": 300,
464
+ "targetEngagerCount": 1500,
465
+ "maxPostsToScrape": 5
443
466
  })
444
467
  ```
445
468
 
@@ -451,7 +474,7 @@ After the lead list finishes and the user confirms it looks good, call:
451
474
  confirm_lead_list({
452
475
  "campaignOfferId": "cmp_xxx",
453
476
  "jobId": "<jobId from import_leads>",
454
- "targetLeadCount": 500
477
+ "targetLeadCount": 15
455
478
  })
456
479
  ```
457
480