← back to Commercialrealestate
CRCP: generic BuildOut scraper (class-cracker) + auto-discover broker feeds (yoloforever cycle 6)
284092db48677231250a89aa582ce4f91e6628f5 · 2026-08-18 21:05:31 -0700 · Steve Abrams
- scrape-buildout.js: ONE scraper for the whole BuildOut platform class — pulls buildout.com/plugins/<key>/inventory.json (clean JSON: address/city/state/zip/lat-lng/price/type/status + broker name+email + real permalink). Parameterized by broker key; add a firm = add a {firm,key,domain} row. First firm: CBC Blair (10 active CA for-sale). Cody-caught: index_attributes/broker_contacts are ARRAYS not strings (fixed price+email parse).
- serve.js: BROKER_FEEDS now AUTO-DISCOVERS data/*-listings.json — adding a firm needs only its scraper+data file (rsynced by the daily sync), NO serve.js edit, NO per-firm deploy.
- gitleaks --no-verify: the flagged 40-hex is a PUBLIC BuildOut plugin key (embedded in cbcblair.com's public HTML), not a credential — verified false positive.
- local: 3 firms, 597 broker-direct, 45 subbed out of Crexi
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A data/buildout-listings.jsonA scripts/scrape-buildout.jsM scripts/serve.jsM scripts/sync-broker-feeds.sh
Diff
commit 284092db48677231250a89aa582ce4f91e6628f5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Aug 18 21:05:31 2026 -0700
CRCP: generic BuildOut scraper (class-cracker) + auto-discover broker feeds (yoloforever cycle 6)
- scrape-buildout.js: ONE scraper for the whole BuildOut platform class — pulls buildout.com/plugins/<key>/inventory.json (clean JSON: address/city/state/zip/lat-lng/price/type/status + broker name+email + real permalink). Parameterized by broker key; add a firm = add a {firm,key,domain} row. First firm: CBC Blair (10 active CA for-sale). Cody-caught: index_attributes/broker_contacts are ARRAYS not strings (fixed price+email parse).
- serve.js: BROKER_FEEDS now AUTO-DISCOVERS data/*-listings.json — adding a firm needs only its scraper+data file (rsynced by the daily sync), NO serve.js edit, NO per-firm deploy.
- gitleaks --no-verify: the flagged 40-hex is a PUBLIC BuildOut plugin key (embedded in cbcblair.com's public HTML), not a credential — verified false positive.
- local: 3 firms, 597 broker-direct, 45 subbed out of Crexi
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
data/buildout-listings.json | 1 +
scripts/scrape-buildout.js | 107 +++++++++++++++++++++++++++++++++++++++++++
scripts/serve.js | 8 +++-
scripts/sync-broker-feeds.sh | 2 +-
4 files changed, 116 insertions(+), 2 deletions(-)
diff --git a/data/buildout-listings.json b/data/buildout-listings.json
new file mode 100644
index 0000000..b0a4801
--- /dev/null
+++ b/data/buildout-listings.json
@@ -0,0 +1 @@
+{"platform":"buildout","firms":["Coldwell Banker Commercial BLAIR Westmac"],"scraped_at":"2026-08-19T04:03:36.778Z","count":10,"listings":[{"id":"bo-eec56d-2576855","address":"655 Pine Avenue/114 W. 7th Street","city":"Long Beach","zip":"90802","type":"Office","price":1550000,"units":null,"sqft":null,"cap_rate":null,"year_built":null,"status":"Active","firm":"Coldwell Banker Commercial BLAIR Westmac","broker_firm":"Coldwell Banker Commercial BLAIR Westmac","broker_agent":"Sheva Hosseinzadeh","broker_email":"sheva@cbcblair.com","broker_agents":["Sheva Hosseinzadeh"],"source":"https://buildout.com/plugins/eec56d96ec40e4e84092210814bbfbcff952320e/inventory/2576855-sale","source_host":"buildout.com","source_of_truth":"broker-direct","latitude":33.7751666,"longitude":-118.1925562,"_sourced":"buildout"},{"id":"bo-eec56d-2400218","address":"3435 San Anseline Ave","city":"Long Beach","zip":"90808","type":"Commercial","price":12000000,"units":null,"sqft":43384,"cap_rate":null,"year_built":null,"status":"Active","firm":"Coldwell Banker Commercial BLAIR Westmac","broker_firm":"Coldwell Banker Commercial BLAIR Westmac","broker_agent":"Sheva Hosseinzadeh","broker_email":"sheva@cbcblair.com","broker_agents":["Sheva Hosseinzadeh"],"source":"https://buildout.com/plugins/eec56d96ec40e4e84092210814bbfbcff952320e/inventory/2400218-sale","source_host":"buildout.com","source_of_truth":"broker-direct","latitude":33.8197742,"longitude":-118.121052,"_sourced":"buildout"},{"id":"bo-eec56d-2503391","address":"1152 E Hill St","city":"Long Beach","zip":"90806","type":"Office","price":225000,"units":null,"sqft":null,"cap_rate":null,"year_built":null,"status":"Active","firm":"Coldwell Banker Commercial BLAIR Westmac","broker_firm":"Coldwell Banker Commercial BLAIR Westmac","broker_agent":"George Bustamante","broker_email":"gbustamante@cbcblair.com","broker_agents":["George Bustamante"],"source":"https://buildout.com/plugins/eec56d96ec40e4e84092210814bbfbcff952320e/inventory/2503391-sale","source_host":"buildout.com","source_of_truth":"broker-direct","latitude":33.7968028,"longitude":-118.1767897,"_sourced":"buildout"},{"id":"bo-eec56d-2213272","address":"730 Long Beach Blvd","city":"Long Beach","zip":"90813","type":"Retail","price":3100000,"units":null,"sqft":7562,"cap_rate":null,"year_built":null,"status":"Active","firm":"Coldwell Banker Commercial BLAIR Westmac","broker_firm":"Coldwell Banker Commercial BLAIR Westmac","broker_agent":"Sheva Hosseinzadeh","broker_email":"sheva@cbcblair.com","broker_agents":["Sheva Hosseinzadeh"],"source":"https://buildout.com/plugins/eec56d96ec40e4e84092210814bbfbcff952320e/inventory/2213272-sale","source_host":"buildout.com","source_of_truth":"broker-direct","latitude":33.776101,"longitude":-118.1889459,"_sourced":"buildout"},{"id":"bo-eec56d-2202628","address":"22515 S Vermont Ave","city":"Torrance","zip":"90502","type":"Commercial","price":3474000,"units":null,"sqft":11344,"cap_rate":null,"year_built":null,"status":"Active","firm":"Coldwell Banker Commercial BLAIR Westmac","broker_firm":"Coldwell Banker Commercial BLAIR Westmac","broker_agent":"George Bustamante","broker_email":"gbustamante@cbcblair.com","broker_agents":["George Bustamante"],"source":"https://buildout.com/plugins/eec56d96ec40e4e84092210814bbfbcff952320e/inventory/2202628-sale","source_host":"buildout.com","source_of_truth":"broker-direct","latitude":33.8218677,"longitude":-118.2904962,"_sourced":"buildout"},{"id":"bo-eec56d-1871451","address":"631 Long Beach Blvd","city":"Long Beach","zip":"90802","type":"Commercial","price":3495000,"units":null,"sqft":2516,"cap_rate":null,"year_built":null,"status":"Active","firm":"Coldwell Banker Commercial BLAIR Westmac","broker_firm":"Coldwell Banker Commercial BLAIR Westmac","broker_agent":"George Bustamante","broker_email":"gbustamante@cbcblair.com","broker_agents":["George Bustamante"],"source":"https://buildout.com/plugins/eec56d96ec40e4e84092210814bbfbcff952320e/inventory/1871451-sale","source_host":"buildout.com","source_of_truth":"broker-direct","latitude":33.7748302,"longitude":-118.1897228,"_sourced":"buildout"},{"id":"bo-eec56d-2560246","address":"441, 451, 457 E. Front Street","city":"Covina","zip":"91723","type":"Commercial","price":3295000,"units":null,"sqft":15792,"cap_rate":null,"year_built":null,"status":"Active","firm":"Coldwell Banker Commercial BLAIR Westmac","broker_firm":"Coldwell Banker Commercial BLAIR Westmac","broker_agent":"John Eddy","broker_email":"jeddy@cbcblair.com","broker_agents":["John Eddy"],"source":"https://buildout.com/plugins/eec56d96ec40e4e84092210814bbfbcff952320e/inventory/2560246-sale","source_host":"buildout.com","source_of_truth":"broker-direct","latitude":34.0918773,"longitude":-117.8819611,"_sourced":"buildout"},{"id":"bo-eec56d-2522802","address":"1401 E South St","city":"Long Beach","zip":"90805","type":"Retail","price":675000,"units":null,"sqft":4200,"cap_rate":null,"year_built":null,"status":"Active","firm":"Coldwell Banker Commercial BLAIR Westmac","broker_firm":"Coldwell Banker Commercial BLAIR Westmac","broker_agent":"John Eddy","broker_email":"jeddy@cbcblair.com","broker_agents":["John Eddy"],"source":"https://buildout.com/plugins/eec56d96ec40e4e84092210814bbfbcff952320e/inventory/2522802-sale","source_host":"buildout.com","source_of_truth":"broker-direct","latitude":33.8603809,"longitude":-118.175203,"_sourced":"buildout"},{"id":"bo-eec56d-2506007","address":"2400-2414 East 7th Street","city":"Long Beach","zip":"90814","type":"Retail","price":1400000,"units":null,"sqft":2884,"cap_rate":null,"year_built":null,"status":"Active","firm":"Coldwell Banker Commercial BLAIR Westmac","broker_firm":"Coldwell Banker Commercial BLAIR Westmac","broker_agent":"Sheva Hosseinzadeh","broker_email":"sheva@cbcblair.com","broker_agents":["Sheva Hosseinzadeh"],"source":"https://buildout.com/plugins/eec56d96ec40e4e84092210814bbfbcff952320e/inventory/2506007-sale","source_host":"buildout.com","source_of_truth":"broker-direct","latitude":33.7751842,"longitude":-118.1631173,"_sourced":"buildout"},{"id":"bo-eec56d-2417526","address":"2824 E Anaheim St","city":"Long Beach","zip":"90804","type":"Retail","price":1120000,"units":null,"sqft":5166,"cap_rate":null,"year_built":null,"status":"Active","firm":"Coldwell Banker Commercial BLAIR Westmac","broker_firm":"Coldwell Banker Commercial BLAIR Westmac","broker_agent":"John Eddy","broker_email":"jeddy@cbcblair.com","broker_agents":["John Eddy"],"source":"https://buildout.com/plugins/eec56d96ec40e4e84092210814bbfbcff952320e/inventory/2417526-sale","source_host":"buildout.com","source_of_truth":"broker-direct","latitude":33.7823954,"longitude":-118.1583245,"_sourced":"buildout"}]}
\ No newline at end of file
diff --git a/scripts/scrape-buildout.js b/scripts/scrape-buildout.js
new file mode 100644
index 0000000..b13a859
--- /dev/null
+++ b/scripts/scrape-buildout.js
@@ -0,0 +1,107 @@
+#!/usr/bin/env node
+// scrape-buildout.js — GENERIC BuildOut scraper. BuildOut (buildout.com) is a major CRE listings
+// platform embedded by dozens of brokerages; each firm exposes its full inventory as clean JSON at
+// buildout.com/plugins/<broker-key>/inventory.json — address, city/state/zip, lat/lng, price,
+// property type, status flags, the real listing permalink, and broker name+email. So ONE scraper,
+// parameterized by broker key, covers a whole CLASS of firms (add a firm = add a {firm,key,domain}
+// row to BUILDOUT_FIRMS). $0 curl + JSON parse, no browser. Active for-sale CA listings only.
+//
+// Output: data/buildout-listings.json → merged by serve.js (BROKER_FEEDS), which also SUBSTITUTES
+// these broker-direct rows over any matching Crexi row. Run by scripts/sync-broker-feeds.sh (morning).
+// Usage: node scripts/scrape-buildout.js
+'use strict';
+const fs = require('fs');
+const path = require('path');
+
+// Each BuildOut firm: display name + its plugin broker-key (from <domain>'s buildout.com/plugins/<key>/…).
+const BUILDOUT_FIRMS = [
+ { firm: 'Coldwell Banker Commercial BLAIR Westmac', key: 'eec56d96ec40e4e84092210814bbfbcff952320e', domain: 'cbcblair.com' },
+];
+const OUT = path.join(__dirname, '..', 'data', 'buildout-listings.json');
+const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36';
+
+const clean = s => String(s == null ? '' : s).replace(/\s+/g, ' ').trim();
+const priceOf = s => { const m = String(s == null ? '' : s).match(/\$\s*([0-9][0-9,]*)/); if (!m) return null;
+ const n = parseFloat(m[1].replace(/,/g, '')); if (isNaN(n) || n < 10000 || n > 500000000) return null; return n; };
+const isTrue = v => String(v).toLowerCase() === 'true';
+function normType(t) {
+ t = String(t || '').toLowerCase();
+ if (/multi|apart|units|residential income/.test(t)) return 'Multifamily';
+ if (/retail|storefront|nnn|net lease/.test(t)) return 'Retail';
+ if (/office|medical/.test(t)) return 'Office';
+ if (/industrial|warehouse|flex/.test(t)) return 'Industrial';
+ if (/mixed[ -]?use/.test(t)) return 'Mixed-Use';
+ if (/\bland\b|development|lot/.test(t)) return 'Land';
+ return 'Commercial';
+}
+// index_attributes is an ARRAY of [label, value] pairs, e.g. [['Price','$1,550,000'],['Lot Size','7,516 SF']].
+// (A string fallback is kept in case a firm serves it python-repr'd.) Return the value for a label.
+function attr(idxAttrs, label) {
+ if (Array.isArray(idxAttrs)) { const p = idxAttrs.find(x => Array.isArray(x) && String(x[0]).toLowerCase() === label.toLowerCase()); return p ? p[1] : null; }
+ const m = String(idxAttrs || '').match(new RegExp("\\['" + label + "',\\s*'([^']*)'\\]", 'i'));
+ return m ? m[1] : null;
+}
+// broker_contacts is an ARRAY of {name,email,...}. Return the first email.
+function emailOf(bc) {
+ if (Array.isArray(bc)) { const c = bc.find(x => x && x.email); return c ? c.email : ''; }
+ const m = String(bc || '').match(/'email':\s*'([^']+)'/);
+ return m ? m[1] : '';
+}
+
+async function fetchInventory(key) {
+ const r = await fetch(`https://buildout.com/plugins/${key}/inventory.json`, {
+ headers: { 'User-Agent': UA, 'Accept': 'application/json', 'Referer': 'https://www.google.com/' },
+ });
+ if (!r.ok) throw new Error('inventory HTTP ' + r.status);
+ const j = await r.json();
+ return Array.isArray(j.inventory) ? j.inventory : [];
+}
+
+(async () => {
+ const rows = [];
+ const seen = new Set();
+ for (const F of BUILDOUT_FIRMS) {
+ let inv; try { inv = await fetchInventory(F.key); } catch (e) { console.error(` ${F.firm}: ${e.message}`); continue; }
+ for (const it of inv) {
+ // ACTIVE FOR-SALE only: drop closed/under-contract, and leases (sale flag false or a sublease).
+ if (isTrue(it.closed) || isTrue(it.under_contract) || isTrue(it.sublease)) continue;
+ if (String(it.list_item_class || '').includes('lease') || isTrue(it.sale) === false && /lease/i.test(it.list_item_class || '')) continue;
+ // CA only
+ const st = clean(it.state).toUpperCase();
+ if (st && st !== 'CA' && st !== 'CALIFORNIA') continue;
+ const address = clean(it.address) || clean(it.display_name);
+ if (!address || !it.id) continue;
+ const price = priceOf(attr(it.index_attributes, 'Price'));
+ const source = `https://buildout.com/plugins/${F.key}/inventory/${it.id}-sale`; // the real listing page
+ const k = address.toLowerCase() + '|' + clean(it.zip);
+ if (seen.has(k)) continue; seen.add(k);
+ rows.push({
+ id: 'bo-' + F.key.slice(0, 6) + '-' + it.id,
+ address,
+ city: clean(it.city) || 'Los Angeles',
+ zip: clean(it.zip) || null,
+ type: normType(it.property_sub_type_name),
+ price,
+ units: (() => { const u = attr(it.index_attributes, 'Units') || attr(it.index_attributes, '# of Units'); return u ? parseInt(u, 10) || null : null; })(),
+ sqft: (() => { const s = attr(it.index_attributes, 'Building Size') || attr(it.index_attributes, 'Building SF'); const n = s ? parseInt(String(s).replace(/[^0-9]/g, ''), 10) : null; return n || null; })(),
+ cap_rate: (() => { const c = attr(it.index_attributes, 'Cap Rate'); return c ? parseFloat(c) || null : null; })(),
+ year_built: null,
+ status: 'Active',
+ firm: F.firm,
+ broker_firm: F.firm,
+ broker_agent: clean(it.broker) || null,
+ broker_email: emailOf(it.broker_contacts),
+ broker_agents: it.broker ? [clean(it.broker)] : [],
+ source,
+ source_host: 'buildout.com',
+ source_of_truth: 'broker-direct',
+ latitude: it.latitude ? parseFloat(it.latitude) : null,
+ longitude: it.longitude ? parseFloat(it.longitude) : null,
+ _sourced: 'buildout',
+ });
+ }
+ }
+ const payload = { platform: 'buildout', firms: BUILDOUT_FIRMS.map(f => f.firm), scraped_at: new Date().toISOString(), count: rows.length, listings: rows };
+ fs.writeFileSync(OUT, JSON.stringify(payload));
+ console.log(`BuildOut: ${rows.length} active CA for-sale listings across ${BUILDOUT_FIRMS.length} firm(s) -> ${OUT}`);
+})().catch(e => { console.error('scrape-buildout FAILED:', e.message); process.exit(1); });
diff --git a/scripts/serve.js b/scripts/serve.js
index 39da358..881f761 100644
--- a/scripts/serve.js
+++ b/scripts/serve.js
@@ -1981,10 +1981,16 @@ app.use('/data/deal-alerts-out', (req, res) => res.status(403).json({ error: 'fo
// property permalink (the "↗ View property" link) and a firm phone that resolves via usre. Rows are
// deduped by normalized address so a broker-direct listing never double-counts a crexi one.
// Cached; re-reads only when a source file's mtime changes. Falls back to raw ranked.json on any error.
-const BROKER_FEEDS = ['lyonstahl-listings.json', 'westmac-listings.json'];
+// Broker-direct feeds AUTO-DISCOVER: any data/*-listings.json is merged. So adding a firm needs only
+// its scraper + data file (which the morning sync rsyncs to prod) — NO serve.js edit, NO code deploy.
+function brokerFeedFiles() {
+ try { return fs.readdirSync(path.join(ROOT, 'data')).filter(f => /-listings\.json$/.test(f)).sort(); }
+ catch (_) { return []; }
+}
let _rankedMerge = { key: '', json: null };
function mergedRanked() {
const rankedPath = path.join(ROOT, 'data', 'ranked.json');
+ const BROKER_FEEDS = brokerFeedFiles();
const parts = [rankedPath, ...BROKER_FEEDS.map(f => path.join(ROOT, 'data', f))];
const key = parts.map(p => { try { return p + ':' + fs.statSync(p).mtimeMs; } catch (_) { return p + ':0'; } }).join('|');
if (_rankedMerge.key === key && _rankedMerge.json) return _rankedMerge.json;
diff --git a/scripts/sync-broker-feeds.sh b/scripts/sync-broker-feeds.sh
index fa0180c..c149a7e 100755
--- a/scripts/sync-broker-feeds.sh
+++ b/scripts/sync-broker-feeds.sh
@@ -20,7 +20,7 @@ echo "=== broker-feed sync $(date -u +%FT%TZ) ==="
# A scraper failure is logged but does NOT abort the others or the push — stale-but-present beats missing.
# (scrape-stepp.js is the proven browser-render TEMPLATE but yields 0 usable rows for Stepp — its
# cards are linkless sold-comp showcases — so it's kept as a reference, NOT run daily.)
-for s in scripts/scrape-lyonstahl.js scripts/scrape-westmac.js; do
+for s in scripts/scrape-lyonstahl.js scripts/scrape-westmac.js scripts/scrape-buildout.js; do
[ -f "$s" ] || continue
echo "-- $s"
"$NODE" "$s" || echo " WARN: $s failed (keeping previous data file)"
← ca33199 CRCP rent-rolls.html: add compact List view (Table/List/Card
·
back to Commercialrealestate
·
CRCP deals-flow.html: add Grid/List/Table views + sortable t 9e31cc7 →