← back to Commercialrealestate
fetch-redfin-agents (condo): record ALL license numbers per listing (multi-agent, role listing/co-listing) — mirrors the SFR + FHA-condo fix; completes multi-agent capture across all 3 Redfin listing scrapers
1e8ea8f5000e491d2a6b2a0ecf9e59a0af6745de · 2026-08-03 11:42:08 -0700 · Steve Abrams
Files touched
M scripts/fetch-redfin-agents.js
Diff
commit 1e8ea8f5000e491d2a6b2a0ecf9e59a0af6745de
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 3 11:42:08 2026 -0700
fetch-redfin-agents (condo): record ALL license numbers per listing (multi-agent, role listing/co-listing) — mirrors the SFR + FHA-condo fix; completes multi-agent capture across all 3 Redfin listing scrapers
---
scripts/fetch-redfin-agents.js | 75 ++++++++++++++++++++++++++----------------
1 file changed, 47 insertions(+), 28 deletions(-)
diff --git a/scripts/fetch-redfin-agents.js b/scripts/fetch-redfin-agents.js
index 99dfe3c..6af668d 100644
--- a/scripts/fetch-redfin-agents.js
+++ b/scripts/fetch-redfin-agents.js
@@ -37,23 +37,33 @@ const strip = s => s.replace(/^[)\]}'&\s]*\{\}&&/, '').replace(/^[)\]}'\s]+/, ''
const pn = v => (v && typeof v === 'object' ? v.phoneNumber : v) || null; // phone object -> string
// Pull the residential listing agent out of a mainHouseInfo payload. Returns null when suppressed.
-function extractAgent(payloadText) {
- let j; try { j = JSON.parse(strip(payloadText)); } catch { return { parseErr: true }; }
+// Return EVERY listing agent (Steve 2026-08-03: record all license numbers), not just [0] — the
+// primary AND any co-listing agent, each with its own license. Deduped by license||name.
+function extractAgents(payloadText) {
+ let j; try { j = JSON.parse(strip(payloadText)); } catch { return { parseErr: true, agents: [] }; }
const mh = (j.payload && j.payload.mainHouseInfo) || (j.mainHouseInfo) || null;
- if (!mh) return { noPayload: true };
- const la = Array.isArray(mh.listingAgents) ? mh.listingAgents[0] : null;
- if (!la) return { suppressed: true };
- const ai = la.agentInfo || {};
- const name = (ai.agentName || '').trim();
- if (!name || ai.isAgentNameBlank) return { suppressed: true };
- return {
- name,
- brokerage: (la.brokerName || '').trim() || null,
- license: (la.license || '').trim() || null,
- phone: pn(la.agentPhoneNumber) || pn(la.brokerPhoneNumber) || null,
- email: (la.agentEmailAddress || la.brokerEmailAddress || '').trim() || null,
- isRedfinAgent: !!ai.isRedfinAgent
- };
+ if (!mh) return { noPayload: true, agents: [] };
+ const list = Array.isArray(mh.listingAgents) ? mh.listingAgents : [];
+ if (!list.length) return { suppressed: true, agents: [] };
+ const agents = [], seen = new Set();
+ for (const la of list) {
+ if (!la) continue;
+ const ai = la.agentInfo || {};
+ const name = (ai.agentName || '').trim();
+ if (!name || ai.isAgentNameBlank) continue;
+ const a = {
+ name,
+ brokerage: (la.brokerName || '').trim() || null,
+ license: (la.license || '').trim() || null,
+ phone: pn(la.agentPhoneNumber) || pn(la.brokerPhoneNumber) || null,
+ email: (la.agentEmailAddress || la.brokerEmailAddress || '').trim() || null,
+ isRedfinAgent: !!ai.isRedfinAgent
+ };
+ const k = a.license || a.name;
+ if (!seen.has(k)) { seen.add(k); agents.push(a); }
+ }
+ if (!agents.length) return { suppressed: true, agents: [] };
+ return { agents };
}
async function loadTargets() {
@@ -70,7 +80,7 @@ async function loadTargets() {
return r.rows.filter(x => x.pid && x.pid !== x.source);
}
-async function persistAgent(condo, a, sourceUrl) {
+async function persistAgent(condo, a, sourceUrl, role = 'listing') {
const firmId = a.brokerage ? await brokerdb.upsertFirm(a.brokerage) : null;
// Insert/refresh the broker as a RESIDENTIAL agent. upsertBroker keys on (name, firm_id); we set
// agent_type/license/website directly so commercial rows are untouched.
@@ -87,12 +97,15 @@ async function persistAgent(condo, a, sourceUrl) {
const brokerId = r.rows[0].id;
await brokerdb.pool.query(
- `INSERT INTO broker_condo(broker_id, condo_id, role) VALUES($1,$2,'listing')
- ON CONFLICT DO NOTHING`, [brokerId, condo.id]);
+ `INSERT INTO broker_condo(broker_id, condo_id, role) VALUES($1,$2,$3)
+ ON CONFLICT DO NOTHING`, [brokerId, condo.id, role]);
- await brokerdb.pool.query(
- `UPDATE condo SET broker_name=$2, firm_name=$3, firm_id=$4 WHERE id=$1`,
- [condo.id, a.name, a.brokerage, firmId]);
+ // Only the PRIMARY listing agent writes the flat condo.broker_name (a co-agent must not clobber it).
+ if (role === 'listing') {
+ await brokerdb.pool.query(
+ `UPDATE condo SET broker_name=$2, firm_name=$3, firm_id=$4 WHERE id=$1`,
+ [condo.id, a.name, a.brokerage, firmId]);
+ }
// Per-field provenance (CCPA audit trail), tier 'redfin-detail'.
const prov = [];
@@ -148,19 +161,25 @@ async function persistAgent(condo, a, sourceUrl) {
return { status: r.status, text: await r.text() };
}, u);
if (res.status !== 200 || !res.text) { summary.errors++; results.push({ condo: t.id, pid: t.pid, status: res.status, error: 'non-200' }); continue; }
- const a = extractAgent(res.text);
- if (a.suppressed || a.noPayload || a.parseErr) {
+ const ex = extractAgents(res.text);
+ if (ex.suppressed || ex.noPayload || ex.parseErr || !ex.agents.length) {
summary.suppressed++;
results.push({ condo: t.id, pid: t.pid, agent: null, label: 'no public agent (Redfin-listed / suppressed)' });
continue;
}
- await persistAgent(t, a, t.source);
+ // Persist EVERY agent (record all licenses): primary = 'listing', the rest = 'co-listing'.
+ for (let i = 0; i < ex.agents.length; i++) {
+ await persistAgent(t, ex.agents[i], t.source, i === 0 ? 'listing' : 'co-listing');
+ }
+ const a = ex.agents[0];
+ const coAgents = ex.agents.length - 1;
summary.captured++;
+ summary.coAgents = (summary.coAgents || 0) + coAgents;
if (a.phone) summary.withPhone++;
if (a.email) summary.withEmail++;
- if (a.license) summary.withLicense++;
- results.push({ condo: t.id, pid: t.pid, agent: a.name, brokerage: a.brokerage, phone: !!a.phone, email: !!a.email });
- process.stderr.write(` ${t.address}, ${t.city}: ${a.name} / ${a.brokerage || '?'}${a.phone ? ' ☎' : ''}${a.email ? ' ✉' : ''}\n`);
+ if (ex.agents.some(x => x.license)) summary.withLicense++;
+ results.push({ condo: t.id, pid: t.pid, agent: a.name, brokerage: a.brokerage, phone: !!a.phone, email: !!a.email, agents: ex.agents.length, licenses: ex.agents.filter(x => x.license).map(x => x.license) });
+ process.stderr.write(` ${t.address}, ${t.city}: ${a.name} / ${a.brokerage || '?'}${a.phone ? ' ☎' : ''}${a.email ? ' ✉' : ''}${coAgents ? ` +${coAgents} co-agent` : ''}\n`);
await page.waitForTimeout(500);
} catch (e) { summary.errors++; results.push({ condo: t.id, pid: t.pid, error: String(e.message).slice(0, 80) }); }
}
← 5bb6bfb fetch-sfr-agents: record ALL license numbers per SFR listing
·
back to Commercialrealestate
·
auto-save: 2026-08-03T11:53:46 (3 files) — data/condo-broker 92e42fc →