← back to Sublease Agentabrams

crawl/firms/naicapital.js

44 lines

'use strict';
// NAI Capital — independent LA commercial brokerage. Its public listing search is
// powered by an external platform (Buildout/Catylist), embedded rather than at /properties/.
// This agent discovers the embed/search endpoint, then parses listing cards. Low ToS risk.
const ENTRY = ['https://www.naicapital.com/properties',
  'https://www.naicapital.com/commercial-real-estate-listings',
  'https://www.naicapital.com/', ];

module.exports = {
  meta: { firm: 'NAI Capital' },
  async crawl({ base, sponsor, upsert }) {
    let buildout = null, html = '';
    for (const u of ENTRY) {
      const r = await base.httpGet(u, { timeout: 18000 });
      if (r.ok && r.text) {
        html = r.text;
        // Buildout embeds: buildout.com/plugins/<hash>/... or my.buildout.com search widgets
        const m = html.match(/https?:\/\/(?:my\.)?buildout\.com\/[^"'\s]+/);
        if (m) { buildout = m[0]; break; }
        // Catylist / other CRE search iframes
        const c = html.match(/https?:\/\/[^"'\s]*catylist\.com\/[^"'\s]+/);
        if (c) { buildout = c[0]; break; }
      }
      await base.sleep(800);
    }
    if (!buildout) {
      return { cost: 0, notes: 'No Buildout/Catylist embed found on public pages — needs manual URL discovery (their search may be JS-injected). Agent scaffolded; listing selector pending calibration.' };
    }
    // Buildout exposes a JSON inventory endpoint; try the common /inventory path.
    const invUrl = buildout.replace(/\/search.*/, '') + '/inventory';
    const inv = await base.httpGet(invUrl, { timeout: 18000 });
    let count = 0;
    if (inv.ok) {
      for (const m of inv.text.matchAll(/"(?:address|street_address)":"([^"]+)"[\s\S]{0,400}?"(?:id|listing_id)":"?(\w+)/g)) {
        await upsert({ external_id: m[2], title: 'NAI Capital listing', address: m[1], city: null,
          space_type: 'office', source_url: buildout, raw: {} });
        count++;
        if (count > 500) break;
      }
    }
    return { cost: 0, notes: `Buildout embed found (${buildout}); parsed ${count} candidate rows. Calibrate selectors against live inventory JSON.` };
  },
};