[object Object]

← back to Commercialrealestate

CRCP: Call Segments panel (Arcstone loan products) — mixed-use / non-warrantable condos / standard condos (FNMA CPM offer) / DSCR 1-4 / DSCR 5-9 / SFR bank-stmt / non-QM; click a segment → agents-to-call list → segment-tailored pitch

f51be7bcfdc396e9a73822b2294b447a8e3a2eca · 2026-06-28 15:51:05 -0700 · Steve

Files touched

Diff

commit f51be7bcfdc396e9a73822b2294b447a8e3a2eca
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Jun 28 15:51:05 2026 -0700

    CRCP: Call Segments panel (Arcstone loan products) — mixed-use / non-warrantable condos / standard condos (FNMA CPM offer) / DSCR 1-4 / DSCR 5-9 / SFR bank-stmt / non-QM; click a segment → agents-to-call list → segment-tailored pitch
---
 public/crcp.html | 31 +++++++++++++++++--
 scripts/serve.js | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 115 insertions(+), 6 deletions(-)

diff --git a/public/crcp.html b/public/crcp.html
index bc5f821..c923957 100644
--- a/public/crcp.html
+++ b/public/crcp.html
@@ -71,6 +71,9 @@
 </header>
 <div class="wrap">
   <div class="tiles" id="tiles"></div>
+  <div class="panel"><h3>📞 Call Segments — your prospects by Arcstone loan product <span class="c">click a segment for the call list</span></h3>
+    <table id="segTbl"><thead><tr><th>Segment</th><th>Arcstone product / hook</th><th class="n">Properties</th><th class="n">Agents to call</th></tr></thead><tbody></tbody></table>
+  </div>
   <div class="cols">
     <div>
       <div class="panel"><h3>Broker enrichment <span class="c" id="enrLbl"></span></h3>
@@ -97,7 +100,8 @@
 const $=s=>document.querySelector(s);
 const money=n=>n?'$'+Number(n).toLocaleString():'—';
 const atype=t=>t==='residential'?'<span class="atype res" title="Residential listing agent (Redfin)">Residential</span>':'<span class="atype com" title="Commercial broker (Crexi)">Commercial</span>';
-async function openContact(name, id){
+async function openContact(name, id, seg){
+  window.__seg = seg || null;
   const ov=$('#ov'), mb=$('#mbody'); ov.classList.add('on'); mb.innerHTML='<div class="meta">loading '+name+'…</div>';
   const qs = id ? 'id='+encodeURIComponent(id) : 'name='+encodeURIComponent(name);
   let d; try{ d=await (await fetch('/api/broker?'+qs)).json(); }catch(e){ mb.innerHTML='error'; return; }
@@ -113,7 +117,7 @@ async function openContact(name, id){
     <div class="sec"><h4>Current listings (${d.current.length})</h4>${d.current.length?d.current.map(l=>`<div class="litem"><span class="a">${l.address}, ${l.city}</span><span class="m">${l.type} · ${money(l.price)}${l.cap_rate?' · '+l.cap_rate+'% cap':''}</span></div>`).join(''):'<div class="meta">none in our set</div>'}</div>
     <div class="sec"><h4>Closed listings — past 10 yrs (${d.closed.length})</h4>${d.closed.length?d.closed.map(l=>`<div class="litem"><span class="a">${l.address}, ${l.city}</span><span class="m">${money(l.sold_price)} · ${l.sold_date||''}</span></div>`).join(''):'<div class="meta">'+(d.closedNote||'none')+'</div>'}</div>
     ${(d.comps&&d.comps.length)?`<div class="sec"><h4>Recent area comps (${d.comps.length})</h4>${d.comps.map(c=>`<div class="litem"><span class="a">${c.address}, ${c.city}</span><span class="m">${money(c.sold_price)} · ${(c.sold_date||'').toString().slice(0,10)}</span></div>`).join('')}<div class="meta">${d.compsNote||''}</div></div>`:''}
-    <div class="sec"><h4>Arcstone pitch (non-warrantable condo financing)</h4>
+    <div class="sec"><h4>Arcstone pitch${window.__seg?' — '+window.__seg.replace(/-/g,' '):' (non-warrantable condo financing)'}</h4>
       <button class="btn" onclick="genPitch('${name.replace(/'/g,"\\'")}','email',${b.id})">✉ Generate email pitch</button>
       <button class="btn alt" onclick="genPitch('${name.replace(/'/g,"\\'")}','call_script',${b.id})">☎ Generate call script</button>
       <div id="pitchout">${d.pitches&&d.pitches.length?renderPitch(d.pitches[0]):''}</div>
@@ -133,9 +137,29 @@ async function openFirm(name){
         <span class="m">${b.listings} lists · ${b.phone?'☎':'·'} ${b.email?'✉':'·'}</span></div>`).join('')}</div>`;
   document.querySelectorAll('.brow2').forEach(r=>r.onclick=()=>openContact(r.dataset.name, r.dataset.id));
 }
+// ---- Call Segments (Arcstone loan products) ----
+async function loadSegments(){
+  let d; try{ d=await (await fetch('/api/segments')).json(); }catch(e){ return; }
+  $('#segTbl tbody').innerHTML=(d.segments||[]).map(g=>`<tr class="srow" data-key="${g.key}" style="cursor:${g.props?'pointer':'default'}${g.props?'':';opacity:.5'}">
+    <td><b>${g.label}</b></td><td style="color:var(--muted)">${g.product}</td>
+    <td class="n">${(g.props||0).toLocaleString()}</td><td class="n" style="color:var(--draft)">${g.agents!=null?g.agents.toLocaleString():'—'}</td></tr>`).join('');
+  document.querySelectorAll('.srow').forEach(r=>r.onclick=()=>{ const g=(d.segments||[]).find(x=>x.key===r.dataset.key); if(g&&g.props) openSegment(r.dataset.key); else if(g&&g.note) alert(g.note); });
+}
+async function openSegment(key){
+  const ov=$('#ov'), mb=$('#mbody'); ov.classList.add('on'); mb.innerHTML='<div class="meta">loading…</div>';
+  let d; try{ d=await (await fetch('/api/segment?key='+encodeURIComponent(key))).json(); }catch(e){ mb.innerHTML='error'; return; }
+  if(d.error){ mb.innerHTML='<div class="meta">'+d.error+'</div>'; return; }
+  mb.innerHTML=`<h2>📞 ${d.label}</h2><div class="mfirm">${d.product}</div>
+    <div class="meta" style="margin:8px 0;border-left:2px solid var(--gold);padding-left:8px">${d.hook}</div>
+    <div class="sec"><h4>Agents to call (${d.callList.length}) — click one for full contact + a tailored ${d.label} pitch</h4>
+      ${d.callList.length?d.callList.map(c=>`<div class="litem brow3" data-id="${c.id}" data-name="${(c.name||'').replace(/"/g,'&quot;')}" style="cursor:pointer">
+        <span class="a">${c.name} ${atype(c.agent_type)}</span>
+        <span class="m">${c.n} props · ${c.phone?`<a href="tel:${c.phone}" class="has" style="color:var(--active)">${c.phone}</a>`:'no phone'} ${c.email?'✉':''}</span></div>`).join(''):'<div class="meta">'+(d.note||'no matches')+'</div>'}</div>`;
+  document.querySelectorAll('.brow3').forEach(r=>r.onclick=e=>{ if(e.target.tagName==='A') return; openContact(r.dataset.name, r.dataset.id, key); });
+}
 async function genPitch(name,channel,id){
   const out=$('#pitchout'); out.innerHTML='<div class="meta">drafting…</div>';
-  try{ const r=await (await fetch('/api/broker/pitch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name,channel,id})})).json();
+  try{ const r=await (await fetch('/api/broker/pitch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name,channel,id,segment:window.__seg||undefined})})).json();
     out.innerHTML = r.error?('<div class="meta">'+r.error+'</div>'):renderPitch(r.pitch);
   }catch(e){ out.innerHTML='<div class="meta">'+e.message+'</div>'; }
 }
@@ -206,6 +230,7 @@ async function poll(){
   setTimeout(()=>document.querySelectorAll('.flash').forEach(e=>e.classList.remove('flash')),800);
 }
 poll(); setInterval(poll, 3000);
+loadSegments(); setInterval(loadSegments, 20000);
 </script>
 </body>
 </html>
diff --git a/scripts/serve.js b/scripts/serve.js
index bb64c90..281532d 100644
--- a/scripts/serve.js
+++ b/scripts/serve.js
@@ -342,9 +342,28 @@ app.post('/api/broker/pitch', async (req, res) => {
     const first = broker.name.split(/\s+/)[0];
     const refs = listings.length ? `I’ve seen your work around LA — including ${listings.map(l => `${l.address} (${l.city}, ${l.type})`).slice(0, 2).join(' and ')}. ` : '';
     const progLines = progs.map(p => `• ${p.title} — ${p.detail}`).join('\n');
-    const subject = `Financing for non-warrantable condos your buyers can’t close conventionally`;
-    const body = `Hi ${first},\n\n${refs}I work with Arcstone Financial, a national non-QM/portfolio lender, and I reach out to active ${firm} agents because we close the deals conventional lenders walk away from — specifically non-warrantable and non-conforming condos (projects that fail Fannie/Freddie/FHA: high investor concentration, off the FHA-approved list, HOA litigation, high commercial ratio).\n\nWhen your buyer loves a condo but the project won’t qualify conventionally, that’s where we keep your deal alive:\n${progLines}\n\nIf you’ve got a condo buyer stuck on warrantability right now, send it over and I’ll tell you in a day whether we can finance it.\n\nArcstone Financial · ${contact}\n\n— (drafted via the CRE control panel; NMLS-licensed; this is not an offer to lend)`;
-    const callScript = `CALL SCRIPT — ${broker.name} (${firm})\n\nOpener: "Hi ${first}, this is [you] with Arcstone Financial — do you have 30 seconds?"\nHook: "${refs}We finance the condos conventional lenders decline — non-warrantable and non-conforming projects."\nValue: ${progs.map(p => p.title).join(', ')}.\nAsk: "Do you have any condo buyers stuck on warrantability right now? Send me the project and I’ll tell you in a day if we can fund it."\nClose: Arcstone Financial · ${contact}`;
+    // Segment-aware pitch: subject + hook + ask match the loan product Steve is offering.
+    const seg = SEGMENTS[(req.body || {}).segment] || SEGMENTS['nonwarrantable-condo'];
+    const SUBJ = {
+      'mixed-use': 'Financing for mixed-use deals conventional lenders pass on',
+      'nonwarrantable-condo': 'Financing for non-warrantable condos your buyers can’t close conventionally',
+      'standard-condo': 'I’ll check your condo complex in Fannie Mae’s CPM — and finance the ones that don’t qualify',
+      'dscr-1-4': 'DSCR financing for your 1-4 unit investor buyers (no income docs)',
+      'dscr-5-9': 'DSCR financing for 5-9 unit small multifamily',
+      'sfr-bankstmt': 'Bank-statement / P&L financing for your self-employed buyers',
+      'nonqm': 'Non-QM financing for the buyers conventional lenders decline'
+    };
+    const ASK = {
+      'standard-condo': 'Send me any condo project you’re working and I’ll run it through Fannie Mae’s CPM for prior approvals/rejections — free, in a day.',
+      'dscr-1-4': 'Got an investor buyer on a 1-4 unit who’s tight on income docs? Send it — DSCR qualifies on the property’s cash flow.',
+      'dscr-5-9': 'Have a 5-9 unit deal? DSCR can qualify it on cash flow — send it over.',
+      'sfr-bankstmt': 'Have a self-employed buyer the banks turned down? Bank-statement / P&L income can get them qualified.',
+      'mixed-use': 'Got a mixed-use deal stuck conventionally? Send it and I’ll tell you in a day if we can fund it.'
+    };
+    const subject = SUBJ[(req.body || {}).segment] || SUBJ['nonwarrantable-condo'];
+    const ask = ASK[(req.body || {}).segment] || 'If you’ve got a buyer stuck on financing right now, send it over and I’ll tell you in a day whether we can fund it.';
+    const body = `Hi ${first},\n\n${refs}I work with Arcstone Financial, a national non-QM/portfolio lender, and I reach out to active ${firm} agents because we close the deals conventional lenders walk away from — ${seg.hook}.\n\nWhere we keep your deal alive (${seg.product}):\n${progLines}\n\n${ask}\n\nArcstone Financial · ${contact}\n\n— (drafted via the CRE control panel; NMLS-licensed; this is not an offer to lend)`;
+    const callScript = `CALL SCRIPT — ${broker.name} (${firm}) · ${seg.label}\n\nOpener: "Hi ${first}, this is [you] with Arcstone Financial — do you have 30 seconds?"\nHook: "${refs}We do ${seg.hook}."\nProduct: ${seg.product} — ${progs.map(p => p.title).join(', ')}.\nAsk: "${ask}"\nClose: Arcstone Financial · ${contact}`;
     const r = (await q(`INSERT INTO broker_pitch(broker_id, author_user_id, channel, subject, body, status)
       VALUES($1,(SELECT id FROM app_user WHERE username='frank'),$2,$3,$4,'draft') RETURNING id, channel, subject, body, status, created_at`,
       [broker.id, channel, channel === 'email' ? subject : null, channel === 'email' ? body : callScript]))[0];
@@ -374,6 +393,71 @@ app.get('/api/closed-sales', async (req, res) => {
   } catch (e) { res.status(502).json({ error: String(e.message).split('\n')[0], rows: [] }); }
 });
 
+// ── Call Segments — Steve's prospect buckets mapped to Arcstone loan products ──────────────────────
+// Each segment = a set of properties + the AGENTS to call, with the matching Arcstone product + pitch
+// hook. src 'listing' (commercial, has units/type) | 'condo' (residential, warrantability) | 'sfr'
+// (none scraped yet) | 'nonqm' (umbrella over the others).
+const SEGMENTS = {
+  'mixed-use':            { label: 'Mixed-use', product: 'Commercial / portfolio (non-QM)', hook: 'mixed-use financing conventional lenders avoid', src: 'listing', where: "type ILIKE '%mixed%'" },
+  'nonwarrantable-condo': { label: 'Non-warrantable condos', product: 'Non-QM / portfolio condo', hook: 'finance the non-warrantable condo (not FHA-approved) — the deal conventional lenders decline', src: 'condo', where: "warrantable_status <> 'fha_approved'" },
+  'standard-condo':       { label: 'Standard condos (FHA-approved)', product: 'Conventional + Fannie Mae CPM check', hook: "offer to run the complex through Fannie Mae Condo Project Manager (CPM) for prior approvals/rejections", src: 'condo', where: "warrantable_status = 'fha_approved'" },
+  'dscr-1-4':             { label: 'DSCR — 1-4 units', product: 'DSCR investor loan (1-4 units)', hook: 'DSCR — qualify on property cash flow, no income docs, 1-4 units', src: 'listing', where: 'units BETWEEN 1 AND 4' },
+  'dscr-5-9':             { label: 'DSCR — 5-9 units', product: 'DSCR small multifamily (5-9 units)', hook: 'DSCR small multifamily, 5-9 units on cash flow', src: 'listing', where: 'units BETWEEN 5 AND 9' },
+  'sfr-bankstmt':         { label: 'SFR — bank-statement / P&L', product: 'Bank Statement / P&L loan', hook: 'self-employed SFR buyers who need bank-statement or P&L income qualification', src: 'sfr', where: '1=0' },
+  'nonqm':                { label: 'Non-QM (all)', product: 'Non-QM umbrella', hook: 'any non-QM scenario — non-warrantable condo, DSCR, bank-statement, asset-based', src: 'nonqm', where: null }
+};
+function segCallList(seg) {
+  if (seg.src === 'listing') return {
+    sql: `SELECT b.id, b.name, b.agent_type, b.phone, b.email, count(*)::int n, min(l.address) sample
+          FROM listing l JOIN broker_listing bl ON bl.listing_id=l.id JOIN broker b ON b.id=bl.broker_id
+          WHERE ${seg.where} GROUP BY b.id ORDER BY n DESC, b.name LIMIT 300`, args: [] };
+  if (seg.src === 'condo') return {
+    sql: `SELECT b.id, b.name, b.agent_type, b.phone, b.email, count(*)::int n, min(c.address) sample
+          FROM condo c JOIN broker_condo bc ON bc.condo_id=c.id JOIN broker b ON b.id=bc.broker_id
+          WHERE ${seg.where} GROUP BY b.id ORDER BY n DESC, b.name LIMIT 300`, args: [] };
+  if (seg.src === 'nonqm') return {
+    sql: `SELECT id, name, agent_type, phone, email, sum(n)::int n, min(sample) sample FROM (
+            SELECT b.id, b.name, b.agent_type, b.phone, b.email, count(*) n, min(c.address) sample
+              FROM condo c JOIN broker_condo bc ON bc.condo_id=c.id JOIN broker b ON b.id=bc.broker_id
+              WHERE c.warrantable_status <> 'fha_approved' GROUP BY b.id
+            UNION ALL
+            SELECT b.id, b.name, b.agent_type, b.phone, b.email, count(*) n, min(l.address) sample
+              FROM listing l JOIN broker_listing bl ON bl.listing_id=l.id JOIN broker b ON b.id=bl.broker_id
+              WHERE l.units BETWEEN 1 AND 9 OR l.type ILIKE '%mixed%' GROUP BY b.id
+          ) u GROUP BY id, name, agent_type, phone, email ORDER BY n DESC LIMIT 300`, args: [] };
+  return null; // sfr → no data
+}
+app.get('/api/segments', async (req, res) => {
+  if (!brokerdb) return res.json({ segments: [] });
+  try {
+    const out = [];
+    for (const [key, seg] of Object.entries(SEGMENTS)) {
+      let props = 0, agents = 0;
+      if (seg.src === 'listing') {
+        props = (await brokerdb.pool.query(`SELECT count(*)::int n FROM listing WHERE ${seg.where}`)).rows[0].n;
+        agents = (await brokerdb.pool.query(`SELECT count(DISTINCT bl.broker_id)::int n FROM listing l JOIN broker_listing bl ON bl.listing_id=l.id WHERE ${seg.where}`)).rows[0].n;
+      } else if (seg.src === 'condo') {
+        props = (await brokerdb.pool.query(`SELECT count(*)::int n FROM condo WHERE ${seg.where}`)).rows[0].n;
+        agents = (await brokerdb.pool.query(`SELECT count(DISTINCT bc.broker_id)::int n FROM condo c JOIN broker_condo bc ON bc.condo_id=c.id WHERE ${seg.where}`)).rows[0].n;
+      } else if (seg.src === 'nonqm') {
+        props = (await brokerdb.pool.query(`SELECT (SELECT count(*) FROM condo WHERE warrantable_status<>'fha_approved') + (SELECT count(*) FROM listing WHERE units BETWEEN 1 AND 9 OR type ILIKE '%mixed%') n`)).rows[0].n;
+      }
+      out.push({ key, label: seg.label, product: seg.product, hook: seg.hook, props, agents, note: seg.src === 'sfr' ? 'No SFR listings scraped yet — would need an SFR data pull.' : null });
+    }
+    res.json({ segments: out });
+  } catch (e) { res.status(502).json({ error: String(e.message).split('\n')[0], segments: [] }); }
+});
+app.get('/api/segment', async (req, res) => {
+  if (!brokerdb) return res.status(503).json({ error: 'DB unavailable' });
+  const seg = SEGMENTS[req.query.key];
+  if (!seg) return res.status(404).json({ error: 'unknown segment' });
+  try {
+    const cl = segCallList(seg);
+    const callList = cl ? (await brokerdb.pool.query(cl.sql, cl.args)).rows : [];
+    res.json({ key: req.query.key, label: seg.label, product: seg.product, hook: seg.hook, note: seg.src === 'sfr' ? 'No SFR listings scraped yet.' : null, callList });
+  } catch (e) { res.status(502).json({ error: String(e.message).split('\n')[0] }); }
+});
+
 app.use('/data', express.static(path.join(ROOT, 'data')));
 app.use('/', express.static(path.join(ROOT, 'public')));
 

← 99ec9a7 CRE: local XLSX exporter (no Drive API) — 5-tab workbook of  ·  back to Commercialrealestate  ·  Add cre.sfr table + broker_sfr edge (mirror condo/broker_con 44fe3fb →