[object Object]

← back to Commercialrealestate

CRCP: per-broker Crexi-tail phone grind — $0 openclaw CDP own-tabs, resumable (Steve: grind the tail in background)

7992c2479330a302b83cbd9cbabed2ebb0682516 · 2026-08-19 10:38:38 -0700 · Steve Abrams

- backfill-crexi-brokers.js: 4 parallel openclaw tabs fetch each crexi detail page (Akamai-cleared by real Chrome), extract listing broker name+phone -> overlay by broker name. Sidecar .crexi-broker-done.json for resume. ~80% early hit. Background grind (~2h for 1773 rows).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 7992c2479330a302b83cbd9cbabed2ebb0682516
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 19 10:38:38 2026 -0700

    CRCP: per-broker Crexi-tail phone grind — $0 openclaw CDP own-tabs, resumable (Steve: grind the tail in background)
    
    - backfill-crexi-brokers.js: 4 parallel openclaw tabs fetch each crexi detail page (Akamai-cleared by real Chrome), extract listing broker name+phone -> overlay by broker name. Sidecar .crexi-broker-done.json for resume. ~80% early hit. Background grind (~2h for 1773 rows).
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/backfill-crexi-brokers.js | 70 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 70 insertions(+)

diff --git a/scripts/backfill-crexi-brokers.js b/scripts/backfill-crexi-brokers.js
new file mode 100644
index 0000000..330e314
--- /dev/null
+++ b/scripts/backfill-crexi-brokers.js
@@ -0,0 +1,70 @@
+#!/usr/bin/env node
+// backfill-crexi-brokers.js — ALL-DAY, parallel, FREE ($0) PER-BROKER phone backfill for the Crexi tail.
+// The rows the firm-level pass couldn't cover each have their own crexi.com detail page (row.source),
+// which carries the listing broker's name + phone. Crexi is Akamai-protected, but openclaw's REAL
+// Chrome clears it — driven over CDP in OUR OWN tabs (so the concurrent Fabricut agent's active tab is
+// never stolen). N tabs run in parallel. Extracted phones append to data/broker-phone-overlay.json
+// keyed by broker name; serve.js overlays them. Resumable: marks each row id done in a sidecar so a
+// re-run continues the tail. Slow by design (real Chrome, throttled) — meant to grind in the background.
+// Usage: node scripts/backfill-crexi-brokers.js [maxRows]
+'use strict';
+const fs = require('fs'), path = require('path');
+const NP = require('child_process').execSync('npm root -g').toString().trim();
+process.env.NODE_PATH = NP; require('module').Module._initPaths();
+const { chromium } = require('playwright');
+const DATA = path.join(__dirname, '..', 'data');
+const OVER = path.join(DATA, 'broker-phone-overlay.json');
+const DONE = path.join(DATA, '.crexi-broker-done.json');       // sidecar: row ids already attempted
+const CDP = 'http://127.0.0.1:18800';
+const TABS = parseInt(process.env.TABS || '4', 10);
+const MAX = parseInt(process.argv[2] || '3000', 10);
+const norm = s => String(s || '').toLowerCase().replace(/[^a-z0-9]/g, '');
+const fmt = p => { const d = String(p||'').replace(/\D/g,'').replace(/^1(?=\d{10}$)/,''); return d.length===10 ? `(${d.slice(0,3)}) ${d.slice(3,6)}-${d.slice(6)}` : ''; };
+
+let overlay = fs.existsSync(OVER) ? JSON.parse(fs.readFileSync(OVER,'utf8')) : {};
+let doneIds = fs.existsSync(DONE) ? new Set(JSON.parse(fs.readFileSync(DONE,'utf8'))) : new Set();
+const ranked = JSON.parse(fs.readFileSync(path.join(DATA,'ranked.json'),'utf8')).ranked;
+const bn = r => r.broker_agent || (r.broker_agents||[])[0] || r.broker_name;
+// worklist: crexi-sourced rows whose broker still has no overlay phone (name or firm) and not yet attempted
+const work = ranked.filter(r => r.source && /crexi\.com/.test(r.source) && !doneIds.has(r.id)
+  && !(bn(r) && overlay[norm(bn(r))]) && !(r.broker_firm && overlay['firm:'+norm(r.broker_firm)])).slice(0, MAX);
+
+let idx = 0, done = 0, hit = 0;
+const flush = () => { fs.writeFileSync(OVER, JSON.stringify(overlay)); fs.writeFileSync(DONE, JSON.stringify([...doneIds])); };
+
+async function worker(page) {
+  while (idx < work.length) {
+    const r = work[idx++];
+    try {
+      await page.goto(r.source, { waitUntil: 'domcontentloaded', timeout: 30000 });
+      await page.waitForTimeout(1500);
+      const info = await page.evaluate(() => {
+        const bt = document.body.innerText;
+        if (/access denied|just a moment|verify you are human/i.test(bt.slice(0, 200))) return { blocked: true };
+        // broker/agent name near a phone; Crexi shows "Listed By" / a broker card with tel:
+        const tel = [...document.querySelectorAll('a[href^="tel:"]')].map(a => a.getAttribute('href').replace('tel:', '')).filter(Boolean);
+        const nameM = bt.match(/(Listed By|Listing (Broker|Agent)|Presented By)[:\s]*([A-Z][a-zA-Z.'-]+ [A-Z][a-zA-Z.'-]+)/);
+        return { phone: tel[0] || (bt.match(/\(?\d{3}\)?[ .\-]\d{3}[ .\-]\d{4}/) || [])[0] || '', name: nameM ? nameM[3] : '' };
+      });
+      doneIds.add(r.id);
+      if (info && !info.blocked && info.phone) {
+        const ph = fmt(info.phone);
+        const key = info.name ? norm(info.name) : (bn(r) ? norm(bn(r)) : null);
+        if (ph && key && !overlay[key]) { overlay[key] = { phone: ph, firm: r.broker_firm || '', src: 'crexi-openclaw' }; hit++; }
+      }
+    } catch (_) { /* leave unmarked → retry next run */ }
+    if (++done % 10 === 0) { flush(); process.stdout.write(`  ${done}/${work.length} (${hit} phones)\r`); }
+    await page.waitForTimeout(700);                    // throttle — polite + reduce contention
+  }
+}
+
+(async () => {
+  console.log(`Crexi per-broker backfill: ${work.length} rows, ${TABS} parallel openclaw tabs ($0). Background grind.`);
+  const browser = await chromium.connectOverCDP(CDP);
+  const ctx = browser.contexts()[0] || await browser.newContext();
+  const pages = []; for (let i = 0; i < TABS; i++) pages.push(await ctx.newPage());
+  await Promise.all(pages.map(p => worker(p)));
+  for (const p of pages) await p.close().catch(()=>{});
+  flush();
+  console.log(`\nDone: ${done} pages, ${hit} broker phones added. ${work.length - done} left (re-run to continue).`);
+})().catch(e => { flush(); console.error('backfill-crexi-brokers FAILED:', e.message); process.exit(1); });

← a04fb2b Fold 8,796 new active broker-book listings into catalog (nat  ·  back to Commercialrealestate  ·  CRCP: wire per-broker crexi grind into daily sync (capped 40 edd6422 →