← back to Sublease Agentabrams
crawl/crexi-broker-enrich.js
63 lines
'use strict';
// Enrich national (crexi) brokers with a real contact PATH: profile_url (crexi.com/brokers/<id>, $0)
// + brokerage website + title from api.crexi.com/brokers/<id> (cheap in-page batch). Crexi does NOT
// expose broker phone/email, so we never fabricate those — we link to the broker's real Crexi profile.
const fs = require('fs');
const path = require('path');
const { chromium } = require('playwright-core');
const Browserbase = require('@browserbasehq/sdk').default || require('@browserbasehq/sdk');
const { Pool } = require('pg');
const pool = new Pool({ host: '/tmp', database: 'crunified' });
const envTxt = fs.readFileSync(path.join(process.env.HOME, '.claude/skills/browserbase/.env'), 'utf8');
const API_KEY = (envTxt.match(/BROWSERBASE_API_KEY=(.*)/) || [])[1]?.trim();
const PROJECT = (envTxt.match(/BROWSERBASE_PROJECT_ID=(.*)/) || [])[1]?.trim();
(async () => {
const t0 = Date.now();
// 1) $0: profile_url for every crexi broker with an id
await pool.query(`UPDATE brokers SET profile_url = 'https://www.crexi.com/brokers/' || crexi_id
WHERE source='crexi' AND crexi_id IS NOT NULL AND crexi_id <> '' AND profile_url IS NULL`);
const { rows: targets } = await pool.query(
`SELECT crexi_id FROM brokers WHERE source='crexi' AND crexi_id IS NOT NULL AND crexi_id<>'' AND (website IS NULL OR website='') ORDER BY crexi_id`);
console.log(`profile_url set for all crexi brokers; ${targets.length} to enrich with website/title`);
if (!targets.length) { await pool.end(); return; }
const bb = new Browserbase({ apiKey: API_KEY });
const session = await bb.sessions.create({ projectId: PROJECT, browserSettings: { solveCaptchas: true, viewport: { width: 1440, height: 1000 } } });
const browser = await chromium.connectOverCDP(session.connectUrl);
const ctx = browser.contexts()[0]; const page = ctx.pages()[0] || await ctx.newPage();
page.setDefaultTimeout(45000);
await page.goto('https://www.crexi.com/brokers/806', { waitUntil: 'domcontentloaded', timeout: 35000 }).catch(()=>{});
await page.waitForTimeout(4000);
const ids = targets.map(t => t.crexi_id);
let updated = 0, withSite = 0;
for (let i = 0; i < ids.length; i += 30) {
if ((Date.now() - t0) / 60000 > 30) { console.log('\n⏹ cap 30min'); break; }
const batch = ids.slice(i, i + 30);
const out = await page.evaluate(async (batch) => {
const r = [];
for (const id of batch) {
try { const resp = await fetch(`https://api.crexi.com/brokers/${id}`, { headers: {} });
if (resp.ok) { const j = await resp.json(); r.push({ id, website: (j.brokerage && j.brokerage.website) || j.website || null, title: j.title || j.jobTitle || null }); } }
catch {}
}
return r;
}, batch);
for (const b of out) {
if (!b.website && !b.title) continue;
const res = await pool.query(
`UPDATE brokers SET website=COALESCE(NULLIF($2,''), website), specialties=COALESCE(specialties,$3)
WHERE crexi_id=$1 AND source='crexi'`, [b.id, b.website || '', b.title || null]);
if (res.rowCount) { updated++; if (b.website) withSite++; }
}
process.stdout.write(` ${Math.min(i + 30, ids.length)}/${ids.length}\r`);
}
await browser.close().catch(()=>{});
const mins = (Date.now() - t0) / 60000;
const [{ pc, wc }] = (await pool.query(`SELECT count(*) FILTER (WHERE profile_url IS NOT NULL) pc, count(*) FILTER (WHERE website IS NOT NULL) wc FROM brokers WHERE source='crexi'`)).rows;
console.log(`\n=== DONE · ${mins.toFixed(1)} min · $${(mins*0.15).toFixed(2)} ===`);
console.log(`crexi brokers: ${pc} now have a profile_url (contact path), ${wc} have a website. This run updated ${updated} (${withSite} websites).`);
await pool.end();
})();