← back to Commercialrealestate
scripts/broker-enrich-poc.js
77 lines
// 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) }));
})();