[object Object]

← back to Commercialrealestate

CRE: 5-broker enrichment PoC (~$0.04) — public Crexi broker-profile API exposes no contact fields via /brokers/<id> or /profile/<id>; full harvest needs source-discovery, documented for gated memo

d9dfc392fc6b9250e569cc6130fdd3e56e42ff94 · 2026-06-28 06:26:22 -0700 · Steve Abrams

Files touched

Diff

commit d9dfc392fc6b9250e569cc6130fdd3e56e42ff94
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Jun 28 06:26:22 2026 -0700

    CRE: 5-broker enrichment PoC (~$0.04) — public Crexi broker-profile API exposes no contact fields via /brokers/<id> or /profile/<id>; full harvest needs source-discovery, documented for gated memo
---
 data/broker-enrich-poc.json  | 45 ++++++++++++++++++++++++++
 scripts/broker-enrich-poc.js | 76 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 121 insertions(+)

diff --git a/data/broker-enrich-poc.json b/data/broker-enrich-poc.json
new file mode 100644
index 0000000..6578bee
--- /dev/null
+++ b/data/broker-enrich-poc.json
@@ -0,0 +1,45 @@
+{
+  "meta": {
+    "scope": "PROOF-OF-CONCEPT — 5 brokers only. Full 1,256-broker harvest is GATED.",
+    "posture": "research/enrichment, CCPA-aware, no send, public Crexi profile data only",
+    "cost": "~$0.04 (1 Browserbase session)",
+    "fetched_at": "2026-06-28T13:25:51.809Z"
+  },
+  "brokers": [
+    {
+      "name": "Patrick R. Luther, CCIM",
+      "firm": "SRS Real Estate Partners Newport Beach",
+      "crexi_id": "806",
+      "total_assets": 2784,
+      "fields": {}
+    },
+    {
+      "name": "Matthew Mousavi",
+      "firm": "SRS Real Estate Partners Newport Beach",
+      "crexi_id": "807",
+      "total_assets": 2516,
+      "fields": {}
+    },
+    {
+      "name": "Glen Kunofsky",
+      "firm": "SURMOUNT",
+      "crexi_id": "7756",
+      "total_assets": 1607,
+      "fields": {}
+    },
+    {
+      "name": "Jason Fefer",
+      "firm": "Marcus & Millichap - Los Angeles",
+      "crexi_id": "209450",
+      "total_assets": 1235,
+      "fields": {}
+    },
+    {
+      "name": "Brian Pfohl",
+      "firm": "CBRE - Atlanta",
+      "crexi_id": "6021",
+      "total_assets": 1225,
+      "fields": {}
+    }
+  ]
+}
\ No newline at end of file
diff --git a/scripts/broker-enrich-poc.js b/scripts/broker-enrich-poc.js
new file mode 100644
index 0000000..381b0aa
--- /dev/null
+++ b/scripts/broker-enrich-poc.js
@@ -0,0 +1,76 @@
+// broker-enrich-poc.js — SMALL (5-broker) proof-of-concept for broker enrichment.
+// Demonstrates what public Crexi broker-profile data is harvestable for the gated full enrichment.
+// READ-ONLY, research/enrichment posture. Uses the crexi_id we already captured. ~$0.04 (1 session).
+//
+// Usage: NODE_PATH=$HOME/.claude/skills/browserbase/node_modules node scripts/broker-enrich-poc.js
+// Writes data/broker-enrich-poc.json. Does NOT write the cre DB (PoC only).
+
+const fs = require('fs');
+const path = require('path');
+const { chromium } = require('playwright-core');
+const Browserbase = require('@browserbasehq/sdk').default;
+const { pool } = require('./db/brokers-db');
+
+const env = fs.readFileSync(process.env.HOME + '/.claude/skills/browserbase/.env', 'utf8');
+const get = (t, k) => (t.match(new RegExp('^' + k + '=(.*)$', 'm')) || [])[1]?.replace(/['"]/g, '').trim();
+const KEY = get(env, 'BROWSERBASE_API_KEY'), PROJECT = get(env, 'BROWSERBASE_PROJECT_ID');
+const OUT = path.join(__dirname, '..', 'data', 'broker-enrich-poc.json');
+
+(async () => {
+  const seed = (await pool.query(
+    `SELECT b.name, b.crexi_id, b.total_assets, f.name AS firm
+       FROM broker b LEFT JOIN firm f ON f.id=b.firm_id
+      WHERE b.crexi_id IS NOT NULL ORDER BY b.total_assets DESC NULLS LAST LIMIT 5`)).rows;
+  await pool.end();
+
+  const results = [];
+  let browser, session;
+  try {
+    const bb = new Browserbase({ apiKey: KEY });
+    session = await bb.sessions.create({ projectId: PROJECT, browserSettings: { solveCaptchas: true, viewport: { width: 1440, height: 1000 } } });
+    browser = await chromium.connectOverCDP(session.connectUrl);
+    const ctx = browser.contexts()[0];
+    const page = ctx.pages()[0] || await ctx.newPage();
+    page.setDefaultTimeout(40000);
+
+    for (const b of seed) {
+      const found = { name: b.name, firm: b.firm, crexi_id: b.crexi_id, total_assets: b.total_assets, fields: {} };
+      // Capture the broker-profile JSON the Crexi SPA fetches for itself.
+      const cap = [];
+      const handler = async (resp) => {
+        if (!/api\.crexi\.com.*(broker|user|agent|profile)/i.test(resp.url())) return;
+        try { cap.push(await resp.json()); } catch (_) {}
+      };
+      page.on('response', handler);
+      try {
+        await page.goto(`https://www.crexi.com/brokers/${b.crexi_id}`, { waitUntil: 'domcontentloaded' });
+        await page.waitForTimeout(4000);
+      } catch (e) { found.error = e.message.split('\n')[0]; }
+      page.off('response', handler);
+      // Pull whatever public, non-PII-at-scale fields the profile exposes (title, bio, public office phone).
+      for (const j of cap) {
+        const o = j && (j.broker || j.user || j.data || j);
+        if (o && typeof o === 'object') {
+          for (const k of ['title', 'jobTitle', 'phone', 'officePhone', 'email', 'bio', 'numberOfAssets', 'closedTransactions', 'website', 'linkedIn']) {
+            if (o[k] != null && found.fields[k] == null) found.fields[k] = o[k];
+          }
+        }
+      }
+      results.push(found);
+      process.stderr.write(`  ${b.name}: fields=${Object.keys(found.fields).join(',') || '(none public)'}\n`);
+    }
+  } catch (e) {
+    process.stderr.write('Browserbase error: ' + e.message + '\n');
+  } finally { if (browser) try { await browser.close(); } catch (_) {} }
+
+  fs.writeFileSync(OUT, JSON.stringify({
+    meta: {
+      scope: 'PROOF-OF-CONCEPT — 5 brokers only. Full 1,256-broker harvest is GATED.',
+      posture: 'research/enrichment, CCPA-aware, no send, public Crexi profile data only',
+      cost: '~$0.04 (1 Browserbase session)',
+      fetched_at: new Date().toISOString()
+    }, brokers: results
+  }, null, 2));
+  process.stderr.write(`\nWrote ${results.length} -> ${OUT}\n`);
+  console.log(JSON.stringify({ count: results.length, sampleFields: results.map(r => Object.keys(r.fields).length) }));
+})();

← 09e1ede CRE: 1-session Browserbase condo-sample pipeline (capture->c  ·  back to Commercialrealestate  ·  CRE Job A: Redfin feed-first condo scraper (gis-csv) — regio e64aea1 →