[object Object]

← back to Commercialrealestate

B2B: assumable-loan lead viewer (/fha-leads.html, 100% HUD public data) + Recorder sale-price verdict (no $0 clean source; DTT-proxy or ATTOM ~$500-2k/yr)

ea76efd8011b4ff6f6be432c10147c8bc8ef6154 · 2026-07-31 08:47:13 -0700 · steve

Files touched

Diff

commit ea76efd8011b4ff6f6be432c10147c8bc8ef6154
Author: steve <steve@designerwallcoverings.com>
Date:   Fri Jul 31 08:47:13 2026 -0700

    B2B: assumable-loan lead viewer (/fha-leads.html, 100% HUD public data) + Recorder sale-price verdict (no $0 clean source; DTT-proxy or ATTOM ~$500-2k/yr)
---
 docs/B2B-DATA-MAP.md  |  18 +++++--
 public/fha-leads.html | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 155 insertions(+), 5 deletions(-)

diff --git a/docs/B2B-DATA-MAP.md b/docs/B2B-DATA-MAP.md
index bc6dd56..d1fc21e 100644
--- a/docs/B2B-DATA-MAP.md
+++ b/docs/B2B-DATA-MAP.md
@@ -24,11 +24,19 @@ if we got it from the county it's clean. Verified provenance per dataset from ea
 | `redfin-agents` / `sfr-agents` | **Redfin** | aggregator |
 | `broker-blocks` / `brokers-snapshot` | **CREXi** | broker IDENTITY is discovery-OK + cross-refs to DRE-public; CREXi contact fields are tainted |
 
-## The one gap to close for a clean DEAL-FLOW feed
-Recorded **sale PRICE** is not in the public assessor layer — it lives in the LA County Recorder's
-documentary-transfer-tax on grant deeds (public, but harder to extract). The `re-props`
-`recent_commercial_deals` feed currently leans on the Redfin-sourced `closed-sales` → to make it
-B2B-clean, re-source sale price/date from the Recorder (public), keyed by AIN to the assessor parcel.
+## The one gap to close for a clean DEAL-FLOW feed — RESEARCHED VERDICT (2026-07-31)
+Recorded **sale PRICE** is the missing clean field. Research verdict: **there is NO $0 public source
+for clean LA sale prices.** LA County does not publish them — grant deeds don't carry price as a field,
+the assessor exposes only assessed value + last-transfer date, and no open-data sales dataset exists.
+Two legitimate paths:
+- **Documentary Transfer Tax (DTT) back-calc (~$0, unreliable):** DTT IS stamped on recorded deeds
+  (public); price ≈ DTT / 0.0011 (LA base $1.10/$1,000). BUT assumed-loan balances aren't taxed
+  (undervalues leveraged deals — ironic for an assumable-loan product) and exemptions distort it, and
+  you still pay per-deed image fees (NETR/LexisNexis) to read the DTT. Rough proxy only.
+- **Licensed provider (paid, clean): ATTOM ~$500–2,000/yr** (or DataTree/CoreLogic) — sale prices
+  sourced direct-from-county, keyed by AIN. This is the legitimate way to a reliable price feed.
+→ Recommendation: don't build the free-scrape; either accept DTT-proxy with loud caveats, or budget
+  ATTOM for a real deal-flow product. Either way, drop the Redfin `closed-sales`.
 
 ## The B2B product you can build TODAY at $0 data cost (public records + our IP)
 A **property + loan intelligence service** for loan officers / brokers / investors ("Frank the loan officer"):
diff --git a/public/fha-leads.html b/public/fha-leads.html
new file mode 100644
index 0000000..b0ec50d
--- /dev/null
+++ b/public/fha-leads.html
@@ -0,0 +1,142 @@
+<!doctype html><html lang="en"><head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Assumable-Loan Leads · FHA Multifamily — CRCP</title>
+<style>
+:root{--bg:#0e1116;--card:#161b22;--line:#2a313c;--ink:#e6edf3;--mut:#8b949e;--acc:#3fb950;--blue:#58a6ff;--warn:#d29922;--gold:#ffa600;--cols:4}
+*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font-family:-apple-system,Helvetica,Arial,sans-serif;font-size:13px}
+header{padding:14px 18px;border-bottom:1px solid var(--line);display:flex;flex-wrap:wrap;gap:10px;align-items:center}
+h1{font-size:17px;margin:0}h1 b{color:#fff}
+.badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#0e1116;background:var(--acc);padding:3px 8px;border-radius:6px}
+.nav a{color:var(--blue);text-decoration:none;font-size:12px;margin-left:12px}
+.shell{display:flex;align-items:flex-start}
+.rail{width:230px;flex:0 0 230px;border-right:1px solid var(--line);padding:12px;height:calc(100vh - 52px);overflow:auto;position:sticky;top:0}
+.rsec{border-bottom:1px solid var(--line);padding:9px 0}
+.rsec h4{margin:0 0 7px;font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--mut);cursor:pointer}
+.chips{display:flex;flex-wrap:wrap;gap:5px}
+.chip{border:1px solid var(--line);border-radius:20px;padding:3px 9px;font-size:11px;cursor:pointer;color:var(--ink);background:var(--card);user-select:none}
+.chip.active{background:var(--blue);color:#0e1116;border-color:var(--blue);font-weight:600}
+.chip .ct{color:var(--mut);margin-left:4px;font-size:10px}.chip.active .ct{color:#0e1116}
+.main{flex:1;padding:12px 16px;min-width:0}
+.toolbar{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:10px}
+.search{flex:1;min-width:200px;background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:8px 11px;font-size:13px}
+select,input[type=number]{background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:7px 9px;font-size:12px}
+.count{color:var(--mut);font-size:12px}
+table{width:100%;border-collapse:collapse;font-size:12px}
+th{text-align:left;color:var(--mut);text-transform:uppercase;font-size:10px;letter-spacing:.4px;padding:7px 8px;border-bottom:1px solid var(--line);cursor:pointer;white-space:nowrap}
+td{padding:7px 8px;border-bottom:1px solid #20262f}
+tr:hover td{background:#131922}
+.pill{font-size:10px;font-weight:700;padding:2px 7px;border-radius:20px}
+.pill.gold{background:rgba(255,166,0,.16);color:var(--gold)}
+.spread{font-weight:700;color:var(--gold)}
+.mono{font-variant-numeric:tabular-nums}
+.prov{font-size:11px;color:var(--mut);margin:2px 0 12px;line-height:1.5}
+.stat{display:inline-block;background:var(--card);border:1px solid var(--line);border-radius:9px;padding:8px 12px;margin:0 8px 10px 0}
+.stat b{font-size:19px;color:#fff}.stat span{display:block;font-size:10px;text-transform:uppercase;color:var(--mut);letter-spacing:.4px}
+</style></head><body>
+<header>
+  <h1><b>Assumable-Loan Leads</b> · FHA Multifamily</h1>
+  <span class="badge">100% public data · resellable</span>
+  <div class="nav"><a href="/mls.html">← Listings</a><a href="/">Explorer</a></div>
+</header>
+<div class="shell">
+<aside class="rail">
+  <div class="rsec"><h4>State</h4><div class="chips" id="fState"></div></div>
+  <div class="rsec"><h4>City</h4><div class="chips" id="fCity"></div></div>
+  <div class="rsec"><h4>Lead quality</h4><div class="chips" id="fQual">
+    <span class="chip" data-q="below" title="Rate is below the market average at origination — an assumable advantage">◆ Below-market only</span>
+    <span class="chip" data-q="historic" title="Originated in a historic-low-rate era">◆ Historic-low era</span></div></div>
+  <div class="rsec"><h4>Holder / lender</h4><div class="chips" id="fHolder"></div></div>
+  <div class="rsec"><h4>Units</h4><div style="display:flex;gap:6px"><input type="number" id="uMin" placeholder="min" style="width:70px"><input type="number" id="uMax" placeholder="max" style="width:70px"></div></div>
+  <div class="rsec"><h4>Max rate %</h4><input type="number" id="rMax" placeholder="e.g. 4" step="0.5" style="width:90px"></div>
+</aside>
+<div class="main">
+  <div id="stats"></div>
+  <div class="prov">Source: <b>HUD/FHA Multifamily Insured Mortgages (Active)</b> — U.S. government public data ($0, no license, resellable). Rate spread = market average (Freddie PMMS) at the loan's origination month − the loan's actual rate. A large spread = a below-market note a qualified buyer may be able to assume. Single-family FHA per-loan rates are NOT public (borrower privacy), so this is the commercial multifamily universe.</div>
+  <div class="toolbar">
+    <input class="search" id="q" placeholder="search property, city, lender, zip…">
+    <select id="sort"></select>
+    <label style="font-size:11px;color:var(--mut)">density <input type="range" id="dens" min="1" max="8" step="1"></label>
+    <span class="count" id="count"></span>
+  </div>
+  <table><thead id="head"></thead><tbody id="body"></tbody></table>
+</div></div>
+<script>
+const $=s=>document.querySelector(s);
+const money=n=>n==null||n===''||isNaN(+n)?'—':'$'+Math.round(+n).toLocaleString();
+const esc=s=>(s==null?'':String(s)).replace(/&/g,'&amp;').replace(/</g,'&lt;');
+const COLS=[
+  {k:'property',l:'Property',t:'s'},{k:'city',l:'City',t:'s'},{k:'state',l:'ST',t:'s'},{k:'zip',l:'Zip',t:'s'},
+  {k:'units',l:'Units',t:'n'},{k:'originalAmount',l:'Orig $',t:'money'},{k:'rate',l:'Rate %',t:'pct'},
+  {k:'pmmsBenchmark',l:'Mkt @ orig',t:'pct'},{k:'spread',l:'Spread',t:'spread',calc:r=>(r.pmmsBenchmark!=null&&r.rate!=null)?+(r.pmmsBenchmark-r.rate).toFixed(2):null},
+  {k:'holder',l:'Lender',t:'s'},{k:'originationDate',l:'Originated',t:'s'},{k:'belowMarket',l:'Assumable?',t:'flag',calc:r=>r.belowMarket}
+];
+let DATA=[],sortKey='spread',sortDir=-1,q='';
+const F={state:new Set(),city:new Set(),holder:new Set(),qual:new Set(),uMin:null,uMax:null,rMax:null};
+let dens=(function(){const v=parseInt(localStorage.getItem('fhaDens'),10);return(v>=1&&v<=8)?v:4;})();
+const cval=(r,c)=>c.calc?c.calc(r):r[c.k];
+const tog=(s,v)=>{s.has(v)?s.delete(v):s.add(v);};
+
+function pass(r){
+  if(F.state.size&&!F.state.has(r.state))return false;
+  if(F.city.size&&!F.city.has(r.city))return false;
+  if(F.holder.size&&!F.holder.has(r.holder))return false;
+  if(F.qual.has('below')&&!r.belowMarket)return false;
+  if(F.qual.has('historic')&&!r.historicLowEra)return false;
+  if(F.uMin!=null&&!(+r.units>=F.uMin))return false;
+  if(F.uMax!=null&&!(+r.units<=F.uMax))return false;
+  if(F.rMax!=null&&!(+r.rate<=F.rMax))return false;
+  if(q){const t=q.toLowerCase();return COLS.some(c=>{const v=cval(r,c);return v!=null&&String(v).toLowerCase().includes(t);});}
+  return true;
+}
+function filtered(){
+  let rows=DATA.filter(pass);
+  const c=COLS.find(x=>x.k===sortKey)||COLS[8];
+  rows=rows.slice().sort((a,b)=>{let x=cval(a,c),y=cval(b,c);const xm=(x==null||x===''),ym=(y==null||y===''); if(xm&&ym)return 0;if(xm)return 1;if(ym)return -1;
+    if(!isNaN(+x)&&!isNaN(+y)){x=+x;y=+y;}else{x=String(x).toLowerCase();y=String(y).toLowerCase();}
+    return x<y?-1*sortDir:x>y?sortDir:0;});
+  return rows;
+}
+function cell(r,c){const v=cval(r,c);
+  if(c.t==='money')return money(v);
+  if(c.t==='pct')return v==null||v===''?'—':(+v).toFixed(2)+'%';
+  if(c.t==='n')return v==null?'—':(+v).toLocaleString();
+  if(c.t==='spread')return v==null?'—':`<span class="spread">▼ ${v} pts</span>`;
+  if(c.t==='flag')return v?`<span class="pill gold">ASSUMABLE ◆</span>`:'—';
+  return esc(v==null||v===''?'—':v);
+}
+function chip(obj,attr,lim){return Object.entries(obj).sort((a,b)=>b[1]-a[1]).slice(0,lim||99).map(([k,c])=>`<span class="chip" data-${attr}="${esc(k)}">${esc(k)}<span class="ct">${c}</span></span>`).join('');}
+function buildRail(){
+  const sc={},cc={},hc={};
+  DATA.forEach(r=>{if(r.state)sc[r.state]=(sc[r.state]||0)+1;if(r.city)cc[r.city]=(cc[r.city]||0)+1;if(r.holder)hc[r.holder]=(hc[r.holder]||0)+1;});
+  $('#fState').innerHTML=chip(sc,'st',12);$('#fCity').innerHTML=chip(cc,'cy',18);$('#fHolder').innerHTML=chip(hc,'ho',14);
+}
+function render(){
+  const rows=filtered();
+  const below=rows.filter(r=>r.belowMarket).length;
+  const avgSpread=rows.filter(r=>r.belowMarket&&r.pmmsBenchmark!=null&&r.rate!=null);
+  const avg=avgSpread.length?(avgSpread.reduce((s,r)=>s+(r.pmmsBenchmark-r.rate),0)/avgSpread.length).toFixed(1):'—';
+  $('#stats').innerHTML=`<div class="stat"><b>${rows.length.toLocaleString()}</b><span>leads shown</span></div>`+
+    `<div class="stat"><b style="color:var(--gold)">${below.toLocaleString()}</b><span>below-market (assumable)</span></div>`+
+    `<div class="stat"><b>${avg}${avg==='—'?'':' pts'}</b><span>avg assumable spread</span></div>`+
+    `<div class="stat"><b>${new Set(rows.map(r=>r.city)).size}</b><span>cities</span></div>`;
+  $('#count').textContent=`${rows.length.toLocaleString()} of ${DATA.length.toLocaleString()}`;
+  $('#head').innerHTML='<tr>'+COLS.map(c=>`<th data-k="${c.k}">${esc(c.l)}${sortKey===c.k?(sortDir<0?' ↓':' ↑'):''}</th>`).join('')+'</tr>';
+  const fs=Math.max(10,15-dens);
+  $('#body').innerHTML=rows.slice(0,600).map(r=>`<tr style="font-size:${fs}px">`+COLS.map(c=>`<td class="${c.t==='money'||c.t==='n'||c.t==='pct'?'mono':''}">${cell(r,c)}</td>`).join('')+'</tr>').join('');
+}
+$('#head').addEventListener('click',e=>{const th=e.target.closest('th');if(!th)return;const k=th.dataset.k;if(sortKey===k)sortDir*=-1;else{sortKey=k;sortDir=(k==='property'||k==='city'||k==='holder'||k==='state')?1:-1;}render();});
+$('#fState').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.state,c.dataset.st);c.classList.toggle('active');render();});
+$('#fCity').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.city,c.dataset.cy);c.classList.toggle('active');render();});
+$('#fHolder').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.holder,c.dataset.ho);c.classList.toggle('active');render();});
+$('#fQual').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.qual,c.dataset.q);c.classList.toggle('active');render();});
+$('#uMin').addEventListener('input',()=>{F.uMin=$('#uMin').value?+$('#uMin').value:null;render();});
+$('#uMax').addEventListener('input',()=>{F.uMax=$('#uMax').value?+$('#uMax').value:null;render();});
+$('#rMax').addEventListener('input',()=>{F.rMax=$('#rMax').value?+$('#rMax').value:null;render();});
+$('#q').addEventListener('input',()=>{q=$('#q').value.trim();render();});
+$('#sort').addEventListener('change',()=>{sortKey=$('#sort').value;sortDir=(sortKey==='property'||sortKey==='city')?1:-1;render();});
+$('#dens').addEventListener('input',()=>{dens=+$('#dens').value;localStorage.setItem('fhaDens',dens);render();});
+$('#sort').innerHTML=COLS.map(c=>`<option value="${c.k}"${c.k==='spread'?' selected':''}>Sort: ${esc(c.l)}</option>`).join('');
+$('#dens').value=dens;
+fetch('/data/fha-loans.json').then(r=>r.json()).then(j=>{DATA=(j.rows||[]).filter(r=>r.rate!=null);buildRail();render();})
+  .catch(()=>{$('#count').textContent='failed to load /data/fha-loans.json';});
+</script></body></html>

← 982582e broker-grid: tight-default columns, drag-to-reorder, search  ·  back to Commercialrealestate  ·  broker-grid: nulls-last sort so sparse columns (Company/Firm cfbe503 →