[object Object]

← back to Rentv

PR intel: paginate linkedin-enrich target pool (Cody cycle 3)

e8fc556f55827c6420b07f4dc00a2997eb23ca1b · 2026-08-05 16:26:08 -0700 · Steve

A MAX>500 request silently stalled at 500 targets (the /people API caps a page
at 500), so the broker-first overnight run for 1100 only fetched 500. Paginate
+ dedupe by id until we have MAX*2 candidates to sort + slice. Applies to the
NEXT run; does not touch the live process. Broker-first hit-rate confirmed ~100%
(vs 11% on the media/obscure tail) — targeting, not data, was the limiter.

Files touched

Diff

commit e8fc556f55827c6420b07f4dc00a2997eb23ca1b
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Aug 5 16:26:08 2026 -0700

    PR intel: paginate linkedin-enrich target pool (Cody cycle 3)
    
    A MAX>500 request silently stalled at 500 targets (the /people API caps a page
    at 500), so the broker-first overnight run for 1100 only fetched 500. Paginate
    + dedupe by id until we have MAX*2 candidates to sort + slice. Applies to the
    NEXT run; does not touch the live process. Broker-first hit-rate confirmed ~100%
    (vs 11% on the media/obscure tail) — targeting, not data, was the limiter.
---
 src/pr/tools/linkedin-enrich.js | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/src/pr/tools/linkedin-enrich.js b/src/pr/tools/linkedin-enrich.js
index aa328db7..ed40b1c1 100644
--- a/src/pr/tools/linkedin-enrich.js
+++ b/src/pr/tools/linkedin-enrich.js
@@ -64,8 +64,16 @@ async function main() {
   // Optional targeting: LI_DEPT=agency points enrichment at broker/agent contacts (clear firm
   // name → high public-LinkedIn hit-rate) instead of the top-priority pool (bank/obscure = misses).
   const deptQ = process.env.LI_DEPT ? `&department=${encodeURIComponent(process.env.LI_DEPT)}` : '';
-  const pool = (await api(`/people?linkedin_status=none${deptQ}&limit=${Math.max(MAX * 4, 300)}`));
-  let targets = (pool.rows || pool.people || []);
+  // The /people API caps a page at 500, so a MAX>500 request silently stalled at 500
+  // targets (Cody, cycle 3). Paginate + dedupe until we have enough to sort + slice MAX.
+  const want = Math.max(MAX * 2, 300);
+  const seenIds = new Set(); let targets = [];
+  for (let off = 0; targets.length < want; off += 500) {
+    const page = await api(`/people?linkedin_status=none${deptQ}&limit=500&offset=${off}`);
+    const rows = page.rows || page.people || [];
+    for (const p of rows) if (!seenIds.has(p.id)) { seenIds.add(p.id); targets.push(p); }
+    if (rows.length < 500) break;
+  }
   if (process.env.LI_MEDIA_ONLY) targets = targets.filter((p) => mediaIds.has(p.organization_id));
   // Default: media/PR contacts first (they're the outreach priority). LI_BROKERS_FIRST=1
   // flips it — non-media (broker/agent) contacts have a clear firm name so their public

← 39587e39 org drawer: surface each contact's LinkedIn as a clickable �  ·  back to Rentv  ·  Audience: CRM contacts now surface their REAL enriched Linke 49947825 →