← back to Commercialrealestate
CRCP lending layer: hub page + compliance-gated loan-officers surface + data policy/source register
84529cea20b69c8aded59c8562034e32e5a9926c · 2026-07-31 10:54:32 -0700 · Steve Abrams
lending.html hub links all 5 datasets w/ live counts; loan-officers.html is manual_review_only (ships empty, deep-links NMLS/DFPI, never scraped); DATA_POLICY.md + PUBLIC_SOURCE_REGISTER.md codify the public-only, evidence-first, no-PII rules (TK-10090).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A data/loan-officers.jsonA docs/DATA_POLICY.mdA docs/PUBLIC_SOURCE_REGISTER.mdA public/lending.htmlA public/loan-officers.html
Diff
commit 84529cea20b69c8aded59c8562034e32e5a9926c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jul 31 10:54:32 2026 -0700
CRCP lending layer: hub page + compliance-gated loan-officers surface + data policy/source register
lending.html hub links all 5 datasets w/ live counts; loan-officers.html is manual_review_only (ships empty, deep-links NMLS/DFPI, never scraped); DATA_POLICY.md + PUBLIC_SOURCE_REGISTER.md codify the public-only, evidence-first, no-PII rules (TK-10090).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
data/loan-officers.json | 16 +++++++
docs/DATA_POLICY.md | 40 ++++++++++++++++
docs/PUBLIC_SOURCE_REGISTER.md | 27 +++++++++++
public/lending.html | 102 +++++++++++++++++++++++++++++++++++++++++
public/loan-officers.html | 87 +++++++++++++++++++++++++++++++++++
5 files changed, 272 insertions(+)
diff --git a/data/loan-officers.json b/data/loan-officers.json
new file mode 100644
index 0000000..6766390
--- /dev/null
+++ b/data/loan-officers.json
@@ -0,0 +1,16 @@
+{
+ "meta": {
+ "source": "NMLS Consumer Access (official lookup) + authorized manual entry",
+ "source_home": "https://www.nmlsconsumeraccess.org/",
+ "access_method": "manual_review_only",
+ "personal_data": true,
+ "count": 0,
+ "policy": "Individual mortgage loan officers are added ONLY from an officially-permitted source or authorized manual entry. NMLS Consumer Access prohibits automated access/scraping, so this dataset is never bulk-crawled. No personal mobile numbers, personal emails, or home addresses are stored. Every record must carry a public NMLS license number + source_url. See docs/DATA_POLICY.md.",
+ "official_lookups": [
+ { "name": "NMLS Consumer Access", "url": "https://www.nmlsconsumeraccess.org/", "scope": "nationwide MLO + company license lookup" },
+ { "name": "CA DFPI licensee search", "url": "https://dfpi.ca.gov/", "scope": "California mortgage lenders & originators" }
+ ],
+ "retrieved_at": null
+ },
+ "officers": []
+}
diff --git a/docs/DATA_POLICY.md b/docs/DATA_POLICY.md
new file mode 100644
index 0000000..1aa0706
--- /dev/null
+++ b/docs/DATA_POLICY.md
@@ -0,0 +1,40 @@
+# CRCP Lending-Intelligence — Data Policy
+
+Code-enforced rules for the lending layer (FDIC / NCUA / HMDA / SBA / NMLS-LO). These extend the
+existing CRCP sourcing doctrine (`docs/SOURCING.md`) — public data only, evidence-first, honest blanks.
+
+## Hard rules
+1. **Public / open sources only.** No paid or proprietary data feed is required for the core product.
+ Allowed access methods: `official_api`, `official_bulk_download`, `official_open_data_portal`,
+ `first_party_public_web` (allowlisted), `manual_review_only`.
+2. **Do not scrape or republish** Model Match, LinkedIn, CoStar, LoopNet, ZoomInfo, Apollo, MMI, Modex,
+ MLS systems, NMLS Consumer Access, or any login-gated / proprietary database. Do not bypass auth,
+ paywalls, CAPTCHAs, robots directives, or rate limits.
+3. **No personal contact inference.** Never infer personal emails from name patterns. Never publish
+ personal mobile numbers, personal emails, dates of birth, or home addresses. Business contact points
+ only, and only when explicitly published by the org, a government source, or an authorized user.
+4. **Evidence-first.** Every displayed factual field must be traceable to a source record: `source`
+ (owner), `source_url` (official public URL), `identifier` (e.g. FDIC CERT #, NMLS #), and
+ `retrieved_at`. No unlabeled calculated metric is displayed.
+5. **HMDA is institution-level.** Public HMDA aggregates are never represented as any individual loan
+ officer's production. Demographic HMDA fields are used only as privacy-safe market aggregates, if at
+ all — never to rank or target people.
+6. **No individual-production inference.** Individual loan-officer performance is shown only when a
+ reliable public source explicitly states it, labeled with source + date.
+7. **Manual-only fallback.** If an official source offers only a restricted interactive lookup with no
+ permitted automated/bulk route (e.g. NMLS Consumer Access, CA DFPI), it is registered as
+ `manual_review_only`: we deep-link to the official lookup and accept authorized manual entry — we do
+ not automate it.
+8. **Never fabricate.** A dataset with no permitted population path ships honestly empty with an
+ explanatory empty-state, never with synthetic individuals.
+
+## Individual loan officers (NMLS)
+The `loan-officers` dataset is `manual_review_only`. It starts empty. Records are added only from an
+officially-permitted source or authorized manual entry, and each must carry a public NMLS license number
+plus a `source_url`. Populating it at scale from a live individual-LO feed is a **gated** decision
+requiring a permitted/licensed source — surfaced to Steve, never auto-executed.
+
+## Enforcement
+- The `/api/lending/:dataset` route is allowlisted to known datasets only.
+- Ingestion scripts fetch only from the official endpoints recorded in `PUBLIC_SOURCE_REGISTER.md`.
+- Every row written by an ingestion script carries `source`, `source_url`, `retrieved_at`.
diff --git a/docs/PUBLIC_SOURCE_REGISTER.md b/docs/PUBLIC_SOURCE_REGISTER.md
new file mode 100644
index 0000000..ddd42c5
--- /dev/null
+++ b/docs/PUBLIC_SOURCE_REGISTER.md
@@ -0,0 +1,27 @@
+# CRCP Lending-Intelligence — Public Source Register
+
+Every source powering the lending layer, its official endpoint, access method, and review status.
+Scope is Los Angeles County, CA (the CRCP footprint). All `$0`, public, unauthenticated unless noted.
+
+| Dataset | Source (owner) | Official endpoint | Access method | Personal data | Ingestion script | Status |
+|---|---|---|---|---|---|---|
+| `lenders` | FDIC BankFind Suite | `https://api.fdic.gov/banks/institutions` + `/banks/locations` | official_api | No | `scripts/fetch-fdic-lenders.js` | ✅ live (95 banks / 1,414 LA branches) |
+| `branches` | FDIC BankFind Suite | `https://api.fdic.gov/banks/locations` | official_api | No | `scripts/fetch-fdic-lenders.js` | ✅ live |
+| `credit-unions` | NCUA | NCUA Research/Quarterly Call Report data files | official_bulk_download | No | `scripts/fetch-ncua-credit-unions.js` | ⏳ building |
+| `market` | CFPB / FFIEC HMDA | `https://ffiec.cfpb.gov/v2/data-browser-api/...` | official_api | No (aggregate only) | `scripts/fetch-hmda-market.js` | ⏳ building |
+| `sba-lenders` | U.S. SBA | SBA 7(a) & 504 FOIA datasets (data.sba.gov / CSV) | official_bulk_download | No | `scripts/fetch-sba-lenders.js` | ⏳ building |
+| `loan-officers` | NMLS Consumer Access / CA DFPI | `https://www.nmlsconsumeraccess.org/` (lookup) | manual_review_only | Yes | — (authorized entry only) | 🔒 gated, ships empty |
+
+## Notes
+- **FDIC** — the `api.fdic.gov/banks` host is current (the old `banks.data.fdic.gov/api` 301-redirects to
+ it). Institutions filtered to those with ≥1 branch in LA County; joined to institution metadata by CERT.
+ Pagination captures `total` from the first page only (a later page occasionally omits it).
+- **HMDA** — institution-level originations by lender + geography for LA County (FIPS 06037). Never
+ represented as individual production (see `DATA_POLICY.md` §5).
+- **NCUA** — no clean REST API like FDIC; the public quarterly Call Report / active-institution files are
+ the permitted bulk route. Scope to credit unions with an LA-County office/branch.
+- **SBA** — 7(a)/504 loan-level FOIA data is large; aggregate to lender + volume for the LA market.
+- **NMLS** — Consumer Access prohibits automated access; registered `manual_review_only`. We deep-link to
+ the official lookup and never crawl it.
+
+_Last reviewed: 2026-07-31._
diff --git a/public/lending.html b/public/lending.html
new file mode 100644
index 0000000..c505a02
--- /dev/null
+++ b/public/lending.html
@@ -0,0 +1,102 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230c0f15'/%3E%3Ctext x='8' y='12' font-size='9' text-anchor='middle' fill='%23C8A24B'%3E%F0%9F%92%B0%3C/text%3E%3C/svg%3E">
+<title>Lending Intelligence — Public-data lenders, credit unions, market activity</title>
+<link rel="stylesheet" href="/lending-grid.css">
+<style>
+ .hero{padding:34px 28px 10px;max-width:1100px;}
+ .hero h1{font-size:26px;margin:0 0 6px;} .hero h1 b{color:var(--gold);}
+ .hero p{color:var(--mut);max-width:760px;line-height:1.5;}
+ .cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;padding:14px 28px 40px;max-width:1100px;}
+ .lc{background:var(--card);border:1px solid var(--line);border-radius:13px;padding:18px 20px;text-decoration:none;color:var(--ink);display:block;transition:border-color .15s;}
+ .lc:hover{border-color:var(--gold);}
+ .lc h3{margin:0 0 4px;font-size:17px;} .lc h3 .ic{margin-right:7px;}
+ .lc .src{color:var(--acc);font-size:11px;text-transform:uppercase;letter-spacing:.4px;}
+ .lc .desc{color:var(--mut);font-size:13px;line-height:1.45;margin:8px 0 10px;}
+ .lc .cnt{font-size:12px;color:var(--ink);} .lc .cnt b{color:var(--gold);}
+ .lc.gated{opacity:.96;border-style:dashed;}
+ .note{max-width:1100px;margin:0 28px 40px;color:var(--mut);font-size:12px;line-height:1.55;border-top:1px solid var(--line);padding-top:16px;}
+ .note a{color:var(--blue);}
+ .pub{color:var(--acc);border:1px solid rgba(112,173,71,.5);border-radius:8px;padding:2px 9px;font-size:11px;text-transform:uppercase;letter-spacing:.4px;}
+</style>
+</head>
+<body>
+<header>
+ <h1><b>💰 Lending</b> Intelligence</h1>
+ <div class="nav"><a href="/crcp.html">← CRCP</a><a href="/">Explorer</a><a href="/broker-grid.html">👥 Brokers</a><a href="/fha-loans.html">🏘️ FHA Loans</a></div>
+ <span class="spacer"></span>
+ <span class="pub">100% public data</span>
+</header>
+
+<div class="hero">
+ <h1>Lenders, credit unions & mortgage-market activity — <b>for loan officers</b></h1>
+ <p>A source-cited directory and market view of the banks, credit unions, and mortgage lenders active in
+ Los Angeles County — built entirely from official government / open-data sources (FDIC, NCUA, CFPB/HMDA,
+ SBA). Every record links back to its authoritative public record. No proprietary databases, no scraped
+ personal contacts, no inferred emails.</p>
+</div>
+
+<div class="cards" id="cards">
+ <a class="lc" href="/lenders.html" data-ds="lenders">
+ <span class="src">FDIC BankFind</span>
+ <h3><span class="ic">🏦</span>Lenders — Banks</h3>
+ <div class="desc">Every FDIC-insured bank with a branch in LA County — charter type, assets, deposits, HQ, branch count.</div>
+ <div class="cnt"><b class="n">…</b> banks · <span class="b">…</span> LA branches</div>
+ </a>
+ <a class="lc" href="/credit-unions.html" data-ds="credit-unions">
+ <span class="src">NCUA</span>
+ <h3><span class="ic">🤝</span>Credit Unions</h3>
+ <div class="desc">Federally-insured credit unions serving LA County — membership, assets, member counts, branches.</div>
+ <div class="cnt"><b class="n">…</b> credit unions</div>
+ </a>
+ <a class="lc" href="/mortgage-market.html" data-ds="market">
+ <span class="src">CFPB / HMDA</span>
+ <h3><span class="ic">📊</span>Mortgage Market</h3>
+ <div class="desc">Public HMDA lending activity — who originates mortgages in LA County by lender, count, and dollar volume. Institution-level only.</div>
+ <div class="cnt"><b class="n">…</b> lender-market rows</div>
+ </a>
+ <a class="lc" href="/sba-lenders.html" data-ds="sba-lenders">
+ <span class="src">SBA</span>
+ <h3><span class="ic">🏢</span>SBA Lenders</h3>
+ <div class="desc">Lenders originating SBA 7(a) / 504 small-business & commercial loans in the LA market, with volumes.</div>
+ <div class="cnt"><b class="n">…</b> SBA lenders</div>
+ </a>
+ <a class="lc gated" href="/loan-officers.html" data-ds="loan-officers">
+ <span class="src">NMLS Consumer Access</span>
+ <h3><span class="ic">👤</span>Loan Officers</h3>
+ <div class="desc">Individual mortgage loan officers by NMLS license. Populated only from authorized entry or an officially-permitted source — never scraped. Deep-links to the official NMLS lookup.</div>
+ <div class="cnt"><b class="n">…</b> officers on file</div>
+ </a>
+</div>
+
+<div class="note">
+ <strong>Data sources & methodology.</strong> All data is drawn from official public / open-government
+ sources and is displayed with its source, identifier, and retrieval date. Institution-level HMDA lending
+ metrics are never represented as any individual's production. We do not scrape or republish NMLS, LinkedIn,
+ CoStar, MLS, or any login-gated / proprietary database, and we do not infer or publish personal contact
+ details. Individual loan-officer records are <a href="/loan-officers.html">compliance-gated</a> and the
+ per-record evidence panel on each page shows the exact public source. Corrections: contact the CRCP admin.
+</div>
+
+<script>
+const H = { credentials: 'same-origin' };
+async function counts(){
+ const els = document.querySelectorAll('.lc[data-ds]');
+ for (const el of els){
+ const ds = el.dataset.ds;
+ try{
+ const d = await (await fetch('/api/lending/'+ds, H)).json();
+ const key = { lenders:'lenders','credit-unions':'credit_units', market:'market','sba-lenders':'lenders','loan-officers':'officers' }[ds];
+ const arr = d.credit_unions || d.lenders || d.market || d.officers || [];
+ const n = el.querySelector('.n'); if(n) n.textContent = (d.meta && d.meta.count!=null ? d.meta.count : arr.length).toLocaleString();
+ if(ds==='lenders'){ const b=el.querySelector('.b'); if(b){ let br=0; try{ br=(await (await fetch('/api/lending/branches',H)).json()).meta.count; }catch(e){} b.textContent=br.toLocaleString(); } }
+ }catch(e){ const n=el.querySelector('.n'); if(n) n.textContent='—'; }
+ }
+}
+counts();
+</script>
+</body>
+</html>
diff --git a/public/loan-officers.html b/public/loan-officers.html
new file mode 100644
index 0000000..c35beac
--- /dev/null
+++ b/public/loan-officers.html
@@ -0,0 +1,87 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230c0f15'/%3E%3Ctext x='8' y='12' font-size='9' text-anchor='middle' fill='%23C8A24B'%3E%F0%9F%91%A4%3C/text%3E%3C/svg%3E">
+<title>Loan Officers — NMLS-licensed (compliance-gated)</title>
+<link rel="stylesheet" href="/lending-grid.css">
+<style>
+ .compliance{margin:16px 20px 0;padding:14px 18px;background:rgba(210,153,34,.08);border:1px solid rgba(210,153,34,.4);border-radius:11px;color:var(--ink);font-size:13px;line-height:1.5;max-width:980px;}
+ .compliance b{color:var(--warn);}
+ .compliance a{color:var(--blue);} .compliance .lk{display:inline-block;margin-top:8px;margin-right:14px;}
+ .empty{margin:20px;padding:40px 24px;text-align:center;color:var(--mut);border:1px dashed var(--line);border-radius:12px;}
+</style>
+</head>
+<body>
+<header>
+ <h1><b>👤 Loan Officers</b></h1>
+ <div class="nav"><a href="/lending.html">← Lending</a><a href="/lenders.html">🏦 Lenders</a><a href="/credit-unions.html">🤝 Credit Unions</a><a href="/mortgage-market.html">📊 Market</a></div>
+ <span class="spacer"></span>
+ <input class="search" id="q" type="text" placeholder="search all fields — name, company, NMLS #… (space = AND)">
+ <div class="toggle" id="view"><button data-v="table" class="on">▦ List</button><button data-v="grid">▤ Grid</button></div>
+ <select id="sortsel" title="Sort"></select>
+ <label id="denswrap" title="Card density — grid view">⤢<input type="range" id="density" min="1" max="10" step="1"></label>
+ <button class="csvbtn" id="csv" title="Export current rows to CSV">⬇ CSV</button>
+ <span class="count" id="count">…</span>
+</header>
+<div class="compliance">
+ <b>⚖️ Compliance-gated dataset.</b> Individual loan-officer records are added <b>only</b> from an
+ officially-permitted source or authorized manual entry — <b>never scraped</b> from NMLS Consumer Access,
+ LinkedIn, or any login-gated database. Personal mobile numbers, personal emails, and home addresses are
+ never stored. Look up a licensed originator at the official source:
+ <div>
+ <a class="lk" href="https://www.nmlsconsumeraccess.org/" target="_blank" rel="noopener noreferrer">🔎 NMLS Consumer Access ↗</a>
+ <a class="lk" href="https://dfpi.ca.gov/" target="_blank" rel="noopener noreferrer">🔎 CA DFPI licensee search ↗</a>
+ </div>
+</div>
+<div class="shell">
+<aside class="rail">
+ <div id="railFacets"></div>
+ <div class="rsec"><h4>Fields / columns — show / hide</h4>
+ <div class="fieldtoggles" id="fFields"></div>
+ <div style="margin-top:8px"><button class="minibtn" id="fAll">All</button><button class="minibtn" id="fNone">None</button><button class="minibtn" id="fReset">↺ Reset</button></div>
+ </div>
+</aside>
+<div class="wrap">
+ <div class="databar"><span class="pubtag">Public data</span><span id="demoNote">NMLS Consumer Access — manual/authorized entry only</span></div>
+ <div id="emptyState" class="empty hide">No loan-officer records on file yet. Records appear here once added from a permitted source or authorized entry — each with a public NMLS license number and a source link. Use the official lookups above in the meantime.</div>
+ <div id="tableView" class="tblwrap"><table class="g" data-no-sort><thead id="thead"></thead><tbody id="tbody"></tbody></table></div>
+ <div id="gridView" class="grid hide"></div>
+</div>
+</div>
+<div id="ov"><div id="modal"><button class="x" aria-label="Close" onclick="document.getElementById('ov').classList.remove('on')">✕</button><div id="mbody"></div></div></div>
+<script>
+window.GRID_CONFIG = {
+ ns: 'crcpLO', noun: 'loan officers', nameKey: 'name', idKey: 'id',
+ title: 'NMLS-licensed loan officer',
+ api: '/api/lending/loan-officers', dataKey: 'officers',
+ sortKey: 'name', sortDir: 1,
+ defaultCols: ['name', 'company', 'nmls_id', 'city', 'source'],
+ cols: [
+ { k: 'name', l: 'Name', t: 's', g: 'Identity' },
+ { k: 'company', l: 'Company', t: 's', g: 'Identity' },
+ { k: 'nmls_id', l: 'NMLS #', t: 's', g: 'Identity' },
+ { k: 'title', l: 'Title', t: 's', g: 'Identity', def: 0 },
+ { k: 'city', l: 'City', t: 's', g: 'Location' },
+ { k: 'state', l: 'State', t: 's', g: 'Location', def: 0 },
+ { k: 'business_phone', l: 'Business phone', t: 'tel', g: 'Contact', def: 0 },
+ { k: 'source', l: 'Source', t: 'src', g: 'Evidence' },
+ { k: 'retrieved_at', l: 'Retrieved', t: 'date', g: 'Evidence', def: 0 },
+ ],
+ facets: [
+ { k: 'company', l: 'Company', limit: 16 },
+ { k: 'city', l: 'City', limit: 16 },
+ ],
+};
+</script>
+<script src="/lending-grid.js" defer></script>
+<script src="/col-resize.js" defer></script>
+<script>
+// Show the empty-state notice when there are zero records (compliance-gated dataset starts empty).
+fetch('/api/lending/loan-officers', { credentials:'same-origin' }).then(r=>r.json()).then(d=>{
+ if(!(d.officers||[]).length){ document.getElementById('emptyState').classList.remove('hide'); document.getElementById('tableView').classList.add('hide'); }
+}).catch(()=>{});
+</script>
+</body>
+</html>
← e25aad3 crcp: drilldown on Warrantability (FHA proxy) — bars + tile
·
back to Commercialrealestate
·
CRCP: Just Listed active-inventory page (just-listed.html) + 384b541 →