← back to Commercialrealestate

scripts/discover-broker-endpoint.js

63 lines

// discover-broker-endpoint.js — DEFINITIVE one-session discovery of Crexi's per-broker listings API.
// Navigates real broker PROFILE pages by publicProfileId slug (crexi.com/broker/<slug>) and logs
// EVERY api.crexi.com response: method, path, top keys, and whether it carries sale[]/lease[]/assets.
// Read-only; writes data/raw/discover-broker-endpoint.json. ~$0.04 (1 Browserbase session).
'use strict';
const fs = require('fs');
const path = require('path');
const { chromium } = require('playwright-core');
const Browserbase = require('@browserbasehq/sdk').default;
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 ROOT = path.join(__dirname, '..');
// Real Crexi profile paths: crexi.com/profile/<firstname-lastname>-<publicProfileId>.
// Add via env: PROFILES="george-ouzounian-georgeouzo,ash-ghavami-ashgha"
const PROFILES = (process.env.PROFILES || 'george-ouzounian-georgeouzo,ash-ghavami-ashgha,steven-roberts-stevenrober').split(',').map(s => s.trim()).filter(Boolean);

(async () => {
  const hits = [];
  const postBodies = [];
  let browser, auth = null;
  try {
    const bb = new Browserbase({ apiKey: get(env, 'BROWSERBASE_API_KEY') });
    const s = await bb.sessions.create({ projectId: get(env, 'BROWSERBASE_PROJECT_ID'), browserSettings: { solveCaptchas: true, viewport: { width: 1440, height: 1000 } } });
    console.log('bb session', s.id);
    browser = await chromium.connectOverCDP(s.connectUrl);
    const ctx = browser.contexts()[0];
    const page = ctx.pages()[0] || await ctx.newPage();
    page.setDefaultTimeout(45000);
    page.on('request', r => {
      const h = r.headers(); if (!auth && h.authorization && /bearer/i.test(h.authorization) && r.url().includes('api.crexi.com')) auth = h.authorization;
      if (r.method() === 'POST' && /api\.crexi\.com\/assets\/(geo\/)?search/.test(r.url())) { const pd = r.postData(); if (pd) postBodies.push({ url: r.url(), body: pd }); }
    });
    page.on('response', async (resp) => {
      const u = resp.url();
      if (!u.includes('api.crexi.com')) return;
      if ((resp.headers()['content-type'] || '').indexOf('json') < 0) return;
      let j; try { j = await resp.json(); } catch { return; }
      let p; try { p = new URL(u).pathname + (new URL(u).search ? '?' + new URL(u).search.slice(1, 60) : ''); } catch { p = u; }
      const keys = Array.isArray(j) ? '[array]' : Object.keys(j || {}).slice(0, 16);
      const has = (k) => Array.isArray(j?.[k]) && j[k].length;
      const listLen = (Array.isArray(j) ? j.length : ([].concat(j.data || [], j.assets || [], j.results || [], j.items || [], j.sale || [], j.lease || [], j.forSale || [])).length);
      hits.push({ method: resp.request().method(), path: p, keys, sale: has('sale'), lease: has('lease'), assets: has('assets') || has('data') || has('results'), listLen });
    });
    for (const slug of PROFILES) {
      const url = `https://www.crexi.com/profile/${slug}`;
      console.log('→', url);
      try { await page.goto(url, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(6000); await page.mouse.wheel(0, 4000).catch(() => {}); await page.waitForTimeout(4000); } catch (e) { console.log('  nav err', e.message.split('\n')[0]); }
    }
  } catch (e) { console.error('FATAL', e.message); }
  finally { if (browser) try { await browser.close(); } catch {} }

  hits.sort((a, b) => b.listLen - a.listLen);
  fs.writeFileSync(path.join(ROOT, 'data', 'raw', 'discover-broker-endpoint.json'), JSON.stringify({ slugs: PROFILES, bearer: !!auth, postBodies, hits }, null, 2));
  console.log('\n/assets/search POST bodies captured:', postBodies.length);
  postBodies.slice(0, 4).forEach(b => console.log('  ', b.url.split('crexi.com')[1], '→', b.body.slice(0, 400)));
  console.log('\nendpoints carrying listings (sale/lease/assets), richest first:');
  for (const h of hits.filter(x => x.sale || x.lease || x.assets || x.listLen > 1).slice(0, 15))
    console.log(`  [${h.method}] ${h.path}  len=${h.listLen} sale=${h.sale} lease=${h.lease}  keys=${Array.isArray(h.keys) ? h.keys.join(',') : h.keys}`);
  console.log(`\ntotal api.crexi.com JSON responses: ${hits.length}  |  bearer: ${auth ? 'yes' : 'no'}`);
  console.log('💰 ~$0.04 (1 session). Full dump: data/raw/discover-broker-endpoint.json');
  process.exit(0);
})();