← 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.` };
},
};