← back to Commercialrealestate
CRCP: DRE-licensed agents/brokers DB from CA public govt data
e914e06828917200fe0c0ee14cacc3d3aa13a389 · 2026-07-10 17:08:31 -0700 · Steve Abrams
- scripts/fetch-dre-licenses.js: name->License ID->detail against the CA DRE
public lookup (www2.dre.ca.gov), $0 local curl, resumable, honest labeling
- /api/agents/licensed route (DB->snapshot fallback, same idiom as /api/residential)
- public/licensed-agents.html viewer: search + sort + density, per-agent DRE
verification (license #, type Salesperson/Broker, status, expiration, resp. broker)
- mls.html nav link; bounded 25-agent proof run (19 verified, 0 errors, $0)
- full 637 run + launchd + prod deploy drafted to pending-approval (Steve-gated)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M .gitignoreA data/licensed-agents.jsonA public/licensed-agents.htmlM public/mls.htmlA scripts/fetch-dre-licenses.jsM scripts/serve.js
Diff
commit e914e06828917200fe0c0ee14cacc3d3aa13a389
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jul 10 17:08:31 2026 -0700
CRCP: DRE-licensed agents/brokers DB from CA public govt data
- scripts/fetch-dre-licenses.js: name->License ID->detail against the CA DRE
public lookup (www2.dre.ca.gov), $0 local curl, resumable, honest labeling
- /api/agents/licensed route (DB->snapshot fallback, same idiom as /api/residential)
- public/licensed-agents.html viewer: search + sort + density, per-agent DRE
verification (license #, type Salesperson/Broker, status, expiration, resp. broker)
- mls.html nav link; bounded 25-agent proof run (19 verified, 0 errors, $0)
- full 637 run + launchd + prod deploy drafted to pending-approval (Steve-gated)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
.gitignore | 1 +
data/licensed-agents.json | 415 ++++++++++++++++++++++++++++++++++++++++++
public/licensed-agents.html | 147 +++++++++++++++
public/mls.html | 2 +-
scripts/fetch-dre-licenses.js | 221 ++++++++++++++++++++++
scripts/serve.js | 29 +++
6 files changed, 814 insertions(+), 1 deletion(-)
diff --git a/.gitignore b/.gitignore
index 1d368ff..9925182 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@ data/morning-report.html
data/assessor.sqlite
data/assessor.sqlite-shm
data/assessor.sqlite-wal
+data/dre-cache.json
diff --git a/data/licensed-agents.json b/data/licensed-agents.json
new file mode 100644
index 0000000..17aaf5c
--- /dev/null
+++ b/data/licensed-agents.json
@@ -0,0 +1,415 @@
+{
+ "meta": {
+ "source": "California Department of Real Estate — public license lookup (www2.dre.ca.gov/PublicASP/pplinfo.asp)",
+ "note": "Public government data. Every dre_verified record was confirmed against the live DRE lookup by name -> License ID -> detail. Unmatched/ambiguous agents are labeled honestly, never fabricated.",
+ "cost": "$0 (local plain-HTTP, no captcha, no paid API)",
+ "fetched_at": "2026-07-11T00:07:47.403Z",
+ "total_agents": 556,
+ "counts": {
+ "verified": 19,
+ "unmatched": 6,
+ "ambiguous": 0,
+ "errors": 0,
+ "from_cache": 6,
+ "in_snapshot": 25
+ }
+ },
+ "agents": [
+ {
+ "agent": "Allen Avanesyan",
+ "dre_query": "Avanesyan, Allen",
+ "brokerage": "Beverly & Company Luxury Properties",
+ "license": "02203716",
+ "license_type": "Salesperson",
+ "license_city": "BURBANK",
+ "status": "LICENSED",
+ "expiration": "12/13/26",
+ "issued": "12/14/22",
+ "responsible_broker": "Beverly and Company, Inc. 15301 VENTURA BLVD BLDG B STE 315 SHERMAN OAKS, CA 91403",
+ "responsible_broker_id": "02078273",
+ "discipline": "none",
+ "match_confidence": "exact",
+ "dre_verified": true,
+ "status_label": "active",
+ "dre_url": "https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=02203716"
+ },
+ {
+ "agent": "Walter Castillo",
+ "dre_query": "Castillo, Walter",
+ "brokerage": "Coldwell Banker Realty",
+ "license": "01141632",
+ "license_type": "Broker/Officer",
+ "license_city": "LOS ANGELES",
+ "status": "LICENSED",
+ "expiration": "08/29/26",
+ "issued": "07/15/92 (Unofficial -- taken from secondary records) Broker License Issued : 08/30/10",
+ "responsible_broker": "",
+ "responsible_broker_id": "",
+ "discipline": "none",
+ "match_confidence": "surname-only",
+ "dre_verified": true,
+ "status_label": "active",
+ "dre_url": "https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=01141632"
+ },
+ {
+ "agent": "Tony Prichard",
+ "dre_query": "Prichard, Tony",
+ "brokerage": "BERKSHIRE HATHAWAY HOMESEVICES Crest Real estate",
+ "dre_verified": false,
+ "status_label": "unmatched",
+ "match_confidence": "no-match",
+ "candidates": 0
+ },
+ {
+ "agent": "Vahan Lux",
+ "dre_query": "Lux, Vahan",
+ "brokerage": "BERKSHIRE HATHAWAY HOMESERVICES Crest Real Estate",
+ "license": "00977686",
+ "license_type": "Salesperson",
+ "license_city": "BURBANK",
+ "status": "LICENSED",
+ "expiration": "05/21/28",
+ "issued": "12/28/87 (Unofficial -- taken from secondary records)",
+ "responsible_broker": "E A M Enterprises Inc 4005 FOOTHILL BLVD LA CRESCENTA, CA 91214",
+ "responsible_broker_id": "01190835",
+ "discipline": "none",
+ "match_confidence": "exact",
+ "dre_verified": true,
+ "status_label": "active",
+ "dre_url": "https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=00977686"
+ },
+ {
+ "agent": "Narine Khachatryan",
+ "dre_query": "Khachatryan, Narine",
+ "brokerage": "EMPOWER Real Estate",
+ "license": "01843510",
+ "license_type": "Broker/Officer",
+ "license_city": "BURBANK",
+ "status": "LICENSED",
+ "expiration": "04/08/28",
+ "issued": "04/07/08",
+ "responsible_broker": "",
+ "responsible_broker_id": "",
+ "discipline": "none",
+ "match_confidence": "exact",
+ "dre_verified": true,
+ "status_label": "active",
+ "dre_url": "https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=01843510"
+ },
+ {
+ "agent": "Bradley Korb",
+ "dre_query": "Korb, Bradley",
+ "brokerage": "Brad Korb Real Estate Group",
+ "license": "00698730",
+ "license_type": "Broker/Officer",
+ "license_city": "BURBANK",
+ "status": "LICENSED",
+ "expiration": "11/29/28",
+ "issued": "07/02/83 (Unofficial -- taken from secondary records) Broker License Issued : 11/30/92 (Unofficial -- taken from secondary records)",
+ "responsible_broker": "",
+ "responsible_broker_id": "",
+ "discipline": "none",
+ "match_confidence": "surname-only",
+ "dre_verified": true,
+ "status_label": "active",
+ "dre_url": "https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=00698730"
+ },
+ {
+ "agent": "Gun Swanson",
+ "dre_query": "Swanson, Gun",
+ "brokerage": "Engel & Volkers Burbank",
+ "license": "00638098",
+ "license_type": "Salesperson",
+ "license_city": "BURBANK",
+ "status": "LICENSED",
+ "expiration": "03/08/30",
+ "issued": "03/09/78 (Unofficial -- taken from secondary records)",
+ "responsible_broker": "SFRE LA Ventura, Inc. 345 E COLORADO BLVD PASADENA, CA 91101",
+ "responsible_broker_id": "02221897",
+ "discipline": "none",
+ "match_confidence": "surname-only",
+ "dre_verified": true,
+ "status_label": "active",
+ "dre_url": "https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=00638098"
+ },
+ {
+ "agent": "Ida Altounian",
+ "dre_query": "Altounian, Ida",
+ "brokerage": "Coldwell Banker Realty",
+ "license": "02038924",
+ "license_type": "Salesperson",
+ "license_city": "GLENDALE",
+ "status": "LICENSED",
+ "expiration": "02/21/30",
+ "issued": "02/22/18",
+ "responsible_broker": "Coldwell Banker Residential Brokerage Company 300 COMMERCE STE 250 IRVINE, CA 92602",
+ "responsible_broker_id": "00616212",
+ "discipline": "none",
+ "match_confidence": "exact",
+ "dre_verified": true,
+ "status_label": "active",
+ "dre_url": "https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=02038924"
+ },
+ {
+ "agent": "Armik Avedisian",
+ "dre_query": "Avedisian, Armik",
+ "brokerage": "Keller Williams R. E. Services",
+ "license": "01279085",
+ "license_type": "Broker/Officer",
+ "license_city": "GLENDALE",
+ "status": "LICENSED",
+ "expiration": "04/26/29",
+ "issued": "04/26/00 Broker License Issued : 04/27/17",
+ "responsible_broker": "",
+ "responsible_broker_id": "",
+ "discipline": "none",
+ "match_confidence": "exact",
+ "dre_verified": true,
+ "status_label": "active",
+ "dre_url": "https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=01279085"
+ },
+ {
+ "agent": "Ronnie Gharibian",
+ "dre_query": "Gharibian, Ronnie",
+ "brokerage": "Gharibian Real Estate",
+ "license": "01239506",
+ "license_type": "Broker/Officer",
+ "license_city": "GLENDALE",
+ "status": "LICENSED MLO License Endorsement : NMLS ID: 76000 (Click here to check the status of the MLO License Endorsement )",
+ "expiration": "02/07/27",
+ "issued": "06/05/98 (Unofficial -- taken from secondary records) Broker License Issued : 02/08/19",
+ "responsible_broker": "",
+ "responsible_broker_id": "",
+ "discipline": "none",
+ "match_confidence": "exact",
+ "dre_verified": true,
+ "status_label": "active",
+ "dre_url": "https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=01239506"
+ },
+ {
+ "agent": "Kyle Rodriguez",
+ "dre_query": "Rodriguez, Kyle",
+ "brokerage": "BLU DOT REALTY GROUP",
+ "license": "01904809",
+ "license_type": "Broker/Officer",
+ "license_city": "SANTA MONICA",
+ "status": "LICENSED",
+ "expiration": "08/15/29",
+ "issued": "11/01/11 Broker License Issued : 08/16/17",
+ "responsible_broker": "",
+ "responsible_broker_id": "",
+ "discipline": "none",
+ "match_confidence": "surname-only",
+ "dre_verified": true,
+ "status_label": "active",
+ "dre_url": "https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=01904809"
+ },
+ {
+ "agent": "JONATHAN BRAY",
+ "dre_query": "BRAY, JONATHAN",
+ "brokerage": "PULTE HOMES OF CALIFORNIA, INC",
+ "dre_verified": false,
+ "status_label": "unmatched",
+ "match_confidence": "ambiguous(2)",
+ "candidates": 2
+ },
+ {
+ "agent": "Mandy Liu",
+ "dre_query": "Liu, Mandy",
+ "brokerage": "BluEleven Realty Group",
+ "dre_verified": false,
+ "status_label": "unmatched",
+ "match_confidence": "no-match",
+ "candidates": 0
+ },
+ {
+ "agent": "Kristine Ng",
+ "dre_query": "Ng, Kristine",
+ "brokerage": "COMPASS",
+ "license": "01782549",
+ "license_type": "Broker/Officer",
+ "license_city": "SAN FRANCISCO",
+ "status": "LICENSED",
+ "expiration": "01/30/29",
+ "issued": "11/27/06",
+ "responsible_broker": "",
+ "responsible_broker_id": "",
+ "discipline": "none",
+ "match_confidence": "exact",
+ "dre_verified": true,
+ "status_label": "active",
+ "dre_url": "https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=01782549"
+ },
+ {
+ "agent": "Kenny Kim",
+ "dre_query": "Kim, Kenny",
+ "brokerage": "Coldwell Banker Realty",
+ "dre_verified": false,
+ "status_label": "unmatched",
+ "match_confidence": "ambiguous(6)",
+ "candidates": 6
+ },
+ {
+ "agent": "Danny Umana",
+ "dre_query": "Umana, Danny",
+ "brokerage": "Keller Williams World Media Center",
+ "license": "01927654",
+ "license_type": "Broker/Officer",
+ "license_city": "BURBANK",
+ "status": "LICENSED",
+ "expiration": "02/07/29",
+ "issued": "02/08/13",
+ "responsible_broker": "",
+ "responsible_broker_id": "",
+ "discipline": "none",
+ "match_confidence": "surname-only",
+ "dre_verified": true,
+ "status_label": "active",
+ "dre_url": "https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=01927654"
+ },
+ {
+ "agent": "Samuel Heller",
+ "dre_query": "Heller, Samuel",
+ "brokerage": "Realty World Legends of Santa Clarita Valley Inc.",
+ "license": "00891062",
+ "license_type": "Salesperson",
+ "license_city": "SANTA CLARITA",
+ "status": "LICENSED",
+ "expiration": "06/27/29",
+ "issued": "06/28/85 (Unofficial -- taken from secondary records)",
+ "responsible_broker": "Realty World Legends Of Santa Clarita Valley Inc 25115 AVENUE STANFORD STE B121 VALENCIA, CA 91355",
+ "responsible_broker_id": "01251341",
+ "discipline": "none",
+ "match_confidence": "surname-only",
+ "dre_verified": true,
+ "status_label": "active",
+ "dre_url": "https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=00891062"
+ },
+ {
+ "agent": "Brenda Ross",
+ "dre_query": "Ross, Brenda",
+ "brokerage": "Equity Union",
+ "dre_verified": false,
+ "status_label": "unmatched",
+ "match_confidence": "ambiguous(2)",
+ "candidates": 2
+ },
+ {
+ "agent": "Vahe Patatanyan",
+ "dre_query": "Patatanyan, Vahe",
+ "brokerage": "CA Real Estate Group",
+ "license": "01747381",
+ "license_type": "Broker/Officer",
+ "license_city": "BURBANK",
+ "status": "LICENSED MLO License Endorsement : NMLS ID: 351242 (Click here to check the status of the MLO License Endorsement )",
+ "expiration": "07/13/30",
+ "issued": "06/02/06 Broker License Issued : 07/14/10",
+ "responsible_broker": "",
+ "responsible_broker_id": "",
+ "discipline": "none",
+ "match_confidence": "exact",
+ "dre_verified": true,
+ "status_label": "active",
+ "dre_url": "https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=01747381"
+ },
+ {
+ "agent": "Jerry Park",
+ "dre_query": "Park, Jerry",
+ "brokerage": "AVM Real Estate Services",
+ "dre_verified": false,
+ "status_label": "unmatched",
+ "match_confidence": "ambiguous(2)",
+ "candidates": 2
+ },
+ {
+ "agent": "Elliot Rousso",
+ "dre_query": "Rousso, Elliot",
+ "brokerage": "Pinnacle Estate Properties, Inc.",
+ "license": "01854651",
+ "license_type": "Salesperson",
+ "license_city": "CALABASAS",
+ "status": "",
+ "expiration": "",
+ "issued": "",
+ "responsible_broker": "",
+ "responsible_broker_id": "",
+ "discipline": "unknown",
+ "match_confidence": "exact",
+ "dre_verified": true,
+ "status_label": "unknown",
+ "dre_url": "https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=01854651"
+ },
+ {
+ "agent": "Michale Meyers",
+ "dre_query": "Meyers, Michale",
+ "brokerage": "The Agency",
+ "license": "01450380",
+ "license_type": "Salesperson",
+ "license_city": "CALABASAS",
+ "status": "LICENSED",
+ "expiration": "09/22/28",
+ "issued": "08/30/04",
+ "responsible_broker": "UMRO Realty Corp. 331 FOOTHILL RD STE 100 BEVERLY HILLS, CA 90210",
+ "responsible_broker_id": "01904054",
+ "discipline": "none",
+ "match_confidence": "exact",
+ "dre_verified": true,
+ "status_label": "active",
+ "dre_url": "https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=01450380"
+ },
+ {
+ "agent": "Anthony Alonzo",
+ "dre_query": "Alonzo, Anthony",
+ "brokerage": "Casablanca Real Estate & Inv",
+ "license": "01200471",
+ "license_type": "Broker/Officer",
+ "license_city": "NORTHRIDGE",
+ "status": "LICENSED MLO License Endorsement : NMLS ID: 350312 (Click here to check the status of the MLO License Endorsement ) Company MLO License Endorsement : 379383 (Click here to check the status of the MLO License Endorsement.)",
+ "expiration": "10/19/27",
+ "issued": "08/15/95 (Unofficial -- taken from secondary records) Broker License Issued : 10/20/99 (Unofficial -- taken from secondary records)",
+ "responsible_broker": "",
+ "responsible_broker_id": "",
+ "discipline": "none",
+ "match_confidence": "surname-only",
+ "dre_verified": true,
+ "status_label": "active",
+ "dre_url": "https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=01200471"
+ },
+ {
+ "agent": "Hewitt Rolle",
+ "dre_query": "Rolle, Hewitt",
+ "brokerage": "Hewitt Rolle",
+ "license": "01453736",
+ "license_type": "Broker/Officer",
+ "license_city": "PORTER RANCH",
+ "status": "LICENSED",
+ "expiration": "04/02/30",
+ "issued": "09/16/04 Broker License Issued : 07/11/07",
+ "responsible_broker": "",
+ "responsible_broker_id": "",
+ "discipline": "none",
+ "match_confidence": "surname-only",
+ "dre_verified": true,
+ "status_label": "active",
+ "dre_url": "https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=01453736"
+ },
+ {
+ "agent": "Neal Kang",
+ "dre_query": "Kang, Neal",
+ "brokerage": "Best Realty & Investment, Inc.",
+ "license": "00994541",
+ "license_type": "Salesperson",
+ "license_city": "NORTHRIDGE",
+ "status": "LICENSED",
+ "expiration": "09/16/28",
+ "issued": "06/21/88 (Unofficial -- taken from secondary records)",
+ "responsible_broker": "Best Realty & Investment Inc 17057 CHATSWORTH ST GRANADA HILLS, CA 91344",
+ "responsible_broker_id": "01816803",
+ "discipline": "none",
+ "match_confidence": "surname-only",
+ "dre_verified": true,
+ "status_label": "active",
+ "dre_url": "https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=00994541"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/public/licensed-agents.html b/public/licensed-agents.html
new file mode 100644
index 0000000..9d64e07
--- /dev/null
+++ b/public/licensed-agents.html
@@ -0,0 +1,147 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Licensed Agents & Brokers — CRCP</title>
+<style>
+ :root{ --bg:#0e1116; --card:#161b22; --line:#2a313c; --ink:#e6edf3; --mut:#8b949e; --acc:#3fb950; --warn:#d29922; --bad:#f85149; --blue:#58a6ff; }
+ *{box-sizing:border-box;} body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;}
+ header{display:flex;align-items:center;gap:12px;padding:11px 18px;border-bottom:1px solid var(--line);position:sticky;top:0;z-index:40;background:rgba(14,17,22,.98);backdrop-filter:blur(8px);flex-wrap:wrap;}
+ header h1{font-size:16px;margin:0;} header h1 b{color:var(--acc);}
+ header .nav{display:flex;gap:12px;margin-left:6px;} header .nav a{color:var(--blue);text-decoration:none;font-size:13px;} header .nav a:hover{color:var(--warn);}
+ .search{background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:9px;padding:8px 12px;font-size:14px;min-width:240px;}
+ select,#density{background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:7px;padding:6px 8px;font-size:12px;}
+ label.densl{display:inline-flex;align-items:center;gap:6px;color:var(--mut);font-size:12px;}
+ .count{color:var(--mut);font-size:12px;}
+ .bar{display:flex;gap:8px;flex-wrap:wrap;padding:10px 18px;border-bottom:1px solid var(--line);align-items:center;}
+ .pill{display:inline-flex;gap:6px;align-items:center;background:var(--card);border:1px solid var(--line);border-radius:16px;padding:5px 11px;font-size:12px;color:var(--mut);cursor:pointer;}
+ .pill b{color:var(--ink);font-variant-numeric:tabular-nums;} .pill.on{border-color:var(--blue);color:var(--ink);}
+ .note{color:var(--mut);font-size:11.5px;padding:8px 18px;border-bottom:1px solid var(--line);}
+ main{padding:14px 18px;}
+ .grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fill,minmax(var(--cw,320px),1fr));}
+ .card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:13px 14px;}
+ .card .top{display:flex;justify-content:space-between;gap:8px;align-items:flex-start;}
+ .card .nm{font-weight:700;font-size:14px;}
+ .card .bk{color:var(--mut);font-size:12px;margin-top:1px;}
+ .badge{font-size:10.5px;font-weight:700;padding:3px 8px;border-radius:12px;white-space:nowrap;text-transform:uppercase;letter-spacing:.3px;}
+ .b-active{background:rgba(63,185,80,.14);color:var(--acc);border:1px solid rgba(63,185,80,.5);}
+ .b-un{background:rgba(139,148,158,.12);color:var(--mut);border:1px solid var(--line);}
+ .b-warn{background:rgba(210,153,34,.14);color:var(--warn);border:1px solid rgba(210,153,34,.5);}
+ .b-bad{background:rgba(248,81,73,.14);color:var(--bad);border:1px solid rgba(248,81,73,.5);}
+ .kv{margin-top:10px;display:grid;grid-template-columns:auto 1fr;gap:3px 10px;font-size:12px;}
+ .kv .k{color:var(--mut);} .kv .v{font-variant-numeric:tabular-nums;}
+ .kv .v a{color:var(--blue);text-decoration:none;} .kv .v a:hover{text-decoration:underline;}
+ .conf{font-size:10px;color:var(--mut);margin-top:7px;}
+ .verified{color:var(--acc);font-weight:700;}
+</style>
+</head>
+<body>
+<header>
+ <h1>🎫 Licensed <b>Agents & Brokers</b></h1>
+ <div class="nav"><a href="/mls.html">← MLS</a><a href="/">Explorer</a><a href="/broker-grid.html">Broker Grid</a><a href="/crcp.html">🛰️ CRCP</a></div>
+ <input class="search" id="q" type="text" placeholder="search name, brokerage, license #, responsible broker…">
+ <select id="sortsel" title="Sort">
+ <option value="name">Sort: Name A→Z</option>
+ <option value="status">Sort: Status</option>
+ <option value="type">Sort: License type</option>
+ <option value="exp">Sort: Expiration (soonest)</option>
+ <option value="brokerage">Sort: Brokerage</option>
+ <option value="conf">Sort: Match confidence</option>
+ </select>
+ <label class="densl" title="Card density">⤢<input type="range" id="density" min="240" max="460" step="20" value="320"></label>
+ <span class="count" id="count">…</span>
+</header>
+<div class="bar" id="pills"></div>
+<div class="note" id="note"></div>
+<main><div class="grid" id="grid"></div></main>
+<script>
+const $=s=>document.querySelector(s);
+let DATA=[], META={}, filt='all', q='', sortKey='name';
+const esc=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
+
+function statusClass(a){
+ if(!a.dre_verified) return a.status_label==='error'?'b-bad':'b-un';
+ const s=(a.status_label||'').toLowerCase();
+ if(s==='active') return 'b-active';
+ if(/expire|surrender|cancel|revok|suspend/.test(s)) return 'b-bad';
+ return 'b-warn';
+}
+function statusText(a){
+ if(!a.dre_verified) return a.status_label==='error'?'DRE error':(a.match_confidence||'unmatched');
+ return a.status||a.status_label||'?';
+}
+// expiration mm/dd/yy -> sortable
+function expNum(a){ const m=/(\d{2})\/(\d{2})\/(\d{2})/.exec(a.expiration||''); return m?(2000+ +m[3])*10000+ +m[1]*100+ +m[2]:99999999; }
+
+function render(){
+ let rows=DATA.filter(a=>{
+ if(filt==='verified'&&!a.dre_verified) return false;
+ if(filt==='broker'&&!/broker/i.test(a.license_type||'')) return false;
+ if(filt==='salesperson'&&!/salesperson/i.test(a.license_type||'')) return false;
+ if(filt==='unmatched'&&a.dre_verified) return false;
+ if(q){ const hay=(a.agent+' '+(a.brokerage||'')+' '+(a.license||'')+' '+(a.responsible_broker||'')+' '+(a.license_type||'')).toLowerCase(); if(!hay.includes(q)) return false; }
+ return true;
+ });
+ const cmp={
+ name:(a,b)=>a.agent.localeCompare(b.agent),
+ status:(a,b)=>(b.dre_verified-a.dre_verified)||(a.status_label||'').localeCompare(b.status_label||''),
+ type:(a,b)=>(a.license_type||'zz').localeCompare(b.license_type||'zz'),
+ exp:(a,b)=>expNum(a)-expNum(b),
+ brokerage:(a,b)=>(a.brokerage||'zz').localeCompare(b.brokerage||'zz'),
+ conf:(a,b)=>(a.match_confidence||'zz').localeCompare(b.match_confidence||'zz'),
+ }[sortKey];
+ rows.sort(cmp);
+ $('#grid').innerHTML=rows.map(cardHtml).join('')||'<div class="note">No agents match.</div>';
+ $('#count').textContent=rows.length+' / '+DATA.length+' agents';
+}
+function cardHtml(a){
+ const dre=a.dre_url?`<a href="${esc(a.dre_url)}" target="_blank" rel="noopener noreferrer">${esc(a.license)} ↗</a>`:'—';
+ const rb=a.responsible_broker?esc(a.responsible_broker.split(/\s\d/)[0]).slice(0,60):'—';
+ return `<div class="card">
+ <div class="top">
+ <div><div class="nm">${esc(a.agent)}${a.dre_verified?' <span class="verified" title="Verified against CA DRE public license record">✔</span>':''}</div>
+ <div class="bk">${esc(a.brokerage||'—')}</div></div>
+ <span class="badge ${statusClass(a)}">${esc(statusText(a))}</span>
+ </div>
+ <div class="kv">
+ <span class="k">License</span><span class="v">${dre}</span>
+ <span class="k">Type</span><span class="v">${esc(a.license_type||'—')}</span>
+ <span class="k">Expires</span><span class="v">${esc(a.expiration||'—')}</span>
+ <span class="k">Resp. broker</span><span class="v">${rb}</span>
+ <span class="k">Discipline</span><span class="v">${esc(a.discipline||'—')}</span>
+ </div>
+ ${a.match_confidence?`<div class="conf">match: ${esc(a.match_confidence)}</div>`:''}
+ </div>`;
+}
+function pills(){
+ const n=k=>DATA.filter(k).length;
+ const defs=[
+ ['all','All',()=>true],
+ ['verified','✔ DRE-verified',a=>a.dre_verified],
+ ['broker','Brokers',a=>/broker/i.test(a.license_type||'')],
+ ['salesperson','Salespersons',a=>/salesperson/i.test(a.license_type||'')],
+ ['unmatched','Unmatched',a=>!a.dre_verified],
+ ];
+ $('#pills').innerHTML=defs.map(([k,l,f])=>`<span class="pill ${filt===k?'on':''}" data-f="${k}">${l} <b>${n(f)}</b></span>`).join('');
+ document.querySelectorAll('.pill').forEach(p=>p.onclick=()=>{filt=p.dataset.f;pills();render();});
+}
+async function load(){
+ let d; try{ d=await (await fetch('/api/agents/licensed')).json(); }catch(e){ $('#note').textContent='Failed to load /api/agents/licensed'; return; }
+ DATA=d.agents||[]; META=d.meta||{};
+ const c=META.counts||{};
+ $('#note').innerHTML=`Source: <b>${esc(d.source||'?')}</b> · ${esc(d.label||'')} `+
+ (c.verified!=null?`— <span class="verified">${c.verified} verified</span>, ${c.unmatched||0} unmatched, ${c.errors||0} errors of ${META.total_agents||DATA.length}. ${esc(META.cost||'')}`:'');
+ pills(); render();
+}
+// controls
+$('#q').oninput=e=>{q=e.target.value.toLowerCase().trim();render();};
+$('#sortsel').onchange=e=>{sortKey=e.target.value;localStorage.setItem('laSort',sortKey);render();};
+$('#density').oninput=e=>{document.documentElement.style.setProperty('--cw',e.target.value+'px');localStorage.setItem('laDens',e.target.value);};
+// restore persisted
+(()=>{const s=localStorage.getItem('laSort'); if(s){sortKey=s;$('#sortsel').value=s;}
+ const dv=localStorage.getItem('laDens')||'320'; $('#density').value=dv; document.documentElement.style.setProperty('--cw',dv+'px');})();
+load();
+</script>
+</body>
+</html>
diff --git a/public/mls.html b/public/mls.html
index 2210983..8b62ed3 100644
--- a/public/mls.html
+++ b/public/mls.html
@@ -91,7 +91,7 @@
<body>
<header>
<h1><b>MLS</b> · LA County — Residential + CRE</h1>
- <div class="nav"><a href="/">← Explorer</a><a href="/map.html">🗺️ Map</a><a href="/crcp.html">🛰️ CRCP</a></div>
+ <div class="nav"><a href="/">← Explorer</a><a href="/map.html">🗺️ Map</a><a href="/licensed-agents.html" title="Real-estate agents & brokers verified against the CA DRE public license roll">🎫 Licensed Agents</a><a href="/crcp.html">🛰️ CRCP</a></div>
<span class="spacer"></span>
<input class="search" id="q" type="text" placeholder="search anything — address, city, firm, type, year…">
<div class="toggle" id="view"><button data-v="table" class="on">▦ Table</button><button data-v="grid">▤ Grid</button></div>
diff --git a/scripts/fetch-dre-licenses.js b/scripts/fetch-dre-licenses.js
new file mode 100644
index 0000000..18118c4
--- /dev/null
+++ b/scripts/fetch-dre-licenses.js
@@ -0,0 +1,221 @@
+#!/usr/bin/env node
+/*
+ * fetch-dre-licenses.js — obtain a REAL California DRE license for every CRCP
+ * real-estate agent/broker from PUBLIC GOVERNMENT DATA ($0, no captcha, no paid API).
+ *
+ * Source: CA Dept. of Real Estate public license lookup
+ * https://www2.dre.ca.gov/PublicASP/pplinfo.asp (classic ASP form)
+ *
+ * Two-step pipeline (both plain HTTP, $0 local):
+ * 1) NAME SEARCH POST pplinfo.asp?start=1 LICENSEE_NAME="Last, First"
+ * -> result rows: <a href="pplinfo.asp?License_id=NNNNNNNN">NNN</a>
+ * <td> Name </td><td>Type</td><td>City</td>
+ * 2) DETAIL GET pplinfo.asp?License_id=NNNNNNNN
+ * -> License Type / Name / License ID / Expiration Date / License Status /
+ * Salesperson|Broker License Issued / Responsible Broker (name+id) / Comment
+ *
+ * Input : data/redfin-agents.json (agent NAMES + brokerage; no license #s persisted)
+ * Output: data/licensed-agents.json (snapshot the /api/agents/licensed route serves)
+ * Cache : data/dre-cache.json (per-name, resumable — safe to Ctrl-C and re-run)
+ *
+ * Honest labeling: unmatched / ambiguous / multi-match are recorded as such,
+ * never fabricated. Only records confirmed against DRE carry dre_verified=true.
+ *
+ * Usage: node scripts/fetch-dre-licenses.js [--limit N] [--delay MS] [--force]
+ * --limit N cap agents processed this run (bounded proof runs)
+ * --delay MS polite delay between requests (default 1200ms + jitter)
+ * --force ignore cache, re-fetch
+ */
+'use strict';
+const fs = require('fs');
+const path = require('path');
+
+const ROOT = path.join(__dirname, '..');
+const IN = path.join(ROOT, 'data', 'redfin-agents.json');
+const OUT = path.join(ROOT, 'data', 'licensed-agents.json');
+const CACHE = path.join(ROOT, 'data', 'dre-cache.json');
+const BASE = 'https://www2.dre.ca.gov/PublicASP/pplinfo.asp';
+const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36';
+
+const arg = (k, d) => { const i = process.argv.indexOf(k); return i > -1 ? (process.argv[i + 1] ?? true) : d; };
+const LIMIT = +arg('--limit', 0) || Infinity;
+const DELAY = +arg('--delay', 1200);
+const FORCE = process.argv.includes('--force');
+
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+const jitter = () => DELAY + Math.floor(Math.random() * 500);
+const clean = s => (s || '').replace(/ /g, ' ').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
+
+// "First M Last Jr" (Redfin order) -> "Last, First M" (DRE order). Strips brokerage suffixes.
+function toDreName(raw) {
+ let n = (raw || '').replace(/\s+/g, ' ').trim();
+ if (!n || n.includes(',')) return n; // already Last, First
+ n = n.replace(/\b(dre|lic|license)\s*#?\s*\d+/ig, '').trim();
+ const parts = n.split(' ').filter(Boolean);
+ if (parts.length < 2) return n;
+ const suffix = /^(jr|sr|ii|iii|iv)\.?$/i;
+ let last = parts.pop();
+ if (suffix.test(last) && parts.length >= 2) last = parts.pop(); // keep real surname before Jr/Sr
+ return `${last}, ${parts.join(' ')}`;
+}
+
+async function post(name) {
+ const body = new URLSearchParams({ LICENSEE_NAME: name, LICENSE_ID: '', CITY_STATE: '', B2: 'Find', h_nextstep: '' });
+ const r = await fetch(`${BASE}?start=1`, {
+ method: 'POST', headers: { 'User-Agent': UA, 'Content-Type': 'application/x-www-form-urlencoded' }, body,
+ });
+ return r.ok ? r.text() : '';
+}
+async function getDetail(id) {
+ const r = await fetch(`${BASE}?License_id=${encodeURIComponent(id)}`, { headers: { 'User-Agent': UA } });
+ return r.ok ? r.text() : '';
+}
+
+// Parse the name-search results table into candidate rows.
+function parseSearch(html) {
+ const out = [];
+ const re = /<a[^>]*href="pplinfo\.asp\?License_id=(\d+)"[^>]*>[^<]*<\/a>\s*<\/td>\s*<td>([^<]*)<\/td>\s*<td>([^<]*)<\/td>\s*<td>([^<]*)<\/td>/gi;
+ let m;
+ while ((m = re.exec(html))) {
+ out.push({ license: m[1], name: clean(m[2]), type: clean(m[3]), city: clean(m[4]) });
+ }
+ return out;
+}
+
+// Parse the license-detail page into the fields Steve wants.
+// The page renders each field as <strong>Label:</strong> ... </td><td><FONT>VALUE</FONT>,
+// so we flatten all tags to spaces and read each label-to-next-label span.
+function parseDetail(html) {
+ const flat = clean(html.replace(/ /g, ' '));
+ // value between `label` and the first of the `stops` labels (or end).
+ const span = (label, stops) => {
+ const re = new RegExp(label + '\\s*:?\\s*(.*?)\\s*(?:' + stops.map(s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|') + '|$)', 'i');
+ const m = flat.match(re);
+ return m ? m[1].trim() : '';
+ };
+ const type = span('License Type', ['Name:', 'License ID', 'Expiration']);
+ const name = span('\\bName', ['Mailing Address', 'License ID', 'License Type', 'Expiration']);
+ const expiration = span('Expiration Date', ['License Status', 'Salesperson License', 'Broker License']);
+ const status = span('License Status', ['Salesperson License Issued', 'Broker License Issued', 'Former Name', 'Responsible Broker']);
+ const issued = span('Salesperson License Issued', ['Former Name', 'Responsible Broker', 'Comment'])
+ || span('Broker License Issued', ['Former Name', 'Responsible Broker', 'Comment']);
+ const idm = flat.match(/License ID\s*:?\s*(\d{6,8})/i);
+ const licenseId = idm ? idm[1] : '';
+ // Responsible broker: first "Responsible Broker:" block -> its License ID + firm name.
+ let respBrokerId = '', respBrokerName = '';
+ const rb = flat.match(/Responsible Broker\s*:?\s*License ID\s*:?\s*(\d{6,8})\s*(.*?)\s*(?:Former Responsible Broker|From \d|Comment|$)/i);
+ if (rb) { respBrokerId = rb[1]; respBrokerName = rb[2].trim(); }
+ const discipline = /NO DISCIPLINARY ACTION/i.test(flat) ? 'none'
+ : (/(disciplin|revoked|suspend|barred)/i.test(flat) ? 'see-dre-record' : 'unknown');
+ return { detail_type: type, dre_name: name, detail_license_id: licenseId, status, expiration, issued,
+ responsible_broker_id: respBrokerId, responsible_broker: respBrokerName, discipline };
+}
+
+// Choose the best candidate for an agent name (+ optional city hint).
+function pick(cands, wantName, cityHint) {
+ if (!cands.length) return { match: null, ambiguity: 'no-match' };
+ const norm = s => s.toLowerCase().replace(/[^a-z, ]/g, '').replace(/\s+/g, ' ').trim();
+ const want = norm(wantName);
+ const exact = cands.filter(c => norm(c.name) === want);
+ if (exact.length === 1) return { match: exact[0], ambiguity: 'exact' };
+ if (exact.length > 1) {
+ if (cityHint) {
+ const byCity = exact.filter(c => c.city && cityHint.toUpperCase().includes(c.city.toUpperCase()));
+ if (byCity.length === 1) return { match: byCity[0], ambiguity: 'exact+city' };
+ }
+ return { match: exact[0], ambiguity: `multi-exact(${exact.length})` };
+ }
+ // last-name-only fallthrough: if exactly one candidate shares the surname, take it (low confidence)
+ const surname = want.split(',')[0];
+ const bySur = cands.filter(c => norm(c.name).split(',')[0] === surname);
+ if (bySur.length === 1) return { match: bySur[0], ambiguity: 'surname-only' };
+ return { match: null, ambiguity: `ambiguous(${cands.length})` };
+}
+
+function loadCache() { try { return JSON.parse(fs.readFileSync(CACHE, 'utf8')); } catch { return {}; } }
+
+async function main() {
+ const src = JSON.parse(fs.readFileSync(IN, 'utf8'));
+ // Unique agent names (+ a brokerage/city hint) from redfin-agents results.
+ const seen = new Map();
+ for (const r of (src.results || [])) {
+ if (!r.agent) continue;
+ const key = r.agent.trim();
+ if (!seen.has(key)) seen.set(key, { agent: key, brokerage: r.brokerage || '', cityHint: '' });
+ }
+ const agents = [...seen.values()];
+ const cache = FORCE ? {} : loadCache();
+ let processed = 0, verified = 0, unmatched = 0, ambiguous = 0, fromCache = 0, errors = 0;
+ const results = [];
+
+ for (const a of agents) {
+ if (processed >= LIMIT) break;
+ const dreName = toDreName(a.agent);
+ if (cache[a.agent] && !FORCE) {
+ results.push(cache[a.agent]); fromCache++;
+ const c = cache[a.agent];
+ if (c.dre_verified) verified++; else if (c.status_label === 'unmatched') unmatched++; else ambiguous++;
+ continue;
+ }
+ processed++;
+ let rec = { agent: a.agent, dre_query: dreName, brokerage: a.brokerage };
+ try {
+ const html = await post(dreName);
+ const cands = parseSearch(html);
+ const { match, ambiguity } = pick(cands, dreName, a.cityHint);
+ if (!match) {
+ rec = { ...rec, dre_verified: false, status_label: 'unmatched', match_confidence: ambiguity, candidates: cands.length };
+ unmatched++;
+ } else {
+ await sleep(jitter());
+ const detail = parseDetail(await getDetail(match.license));
+ rec = {
+ ...rec,
+ license: match.license,
+ license_type: match.type || detail.detail_type,
+ license_city: match.city,
+ status: detail.status,
+ expiration: detail.expiration,
+ issued: detail.issued,
+ responsible_broker: detail.responsible_broker,
+ responsible_broker_id: detail.responsible_broker_id,
+ discipline: detail.discipline,
+ match_confidence: ambiguity,
+ dre_verified: true,
+ status_label: /LICENSED/i.test(detail.status) ? 'active' : (detail.status ? detail.status.toLowerCase() : 'unknown'),
+ dre_url: `${BASE}?License_id=${match.license}`,
+ };
+ verified++;
+ }
+ } catch (e) {
+ rec = { ...rec, dre_verified: false, status_label: 'error', error: String(e.message).split('\n')[0] };
+ errors++;
+ }
+ cache[a.agent] = rec;
+ results.push(rec);
+ fs.writeFileSync(CACHE, JSON.stringify(cache, null, 1)); // persist every step -> resumable
+ process.stdout.write(`\r processed ${processed} verified ${verified} unmatched ${unmatched} err ${errors} `);
+ await sleep(jitter());
+ }
+ process.stdout.write('\n');
+
+ const snapshot = {
+ meta: {
+ source: 'California Department of Real Estate — public license lookup (www2.dre.ca.gov/PublicASP/pplinfo.asp)',
+ note: 'Public government data. Every dre_verified record was confirmed against the live DRE lookup by name -> License ID -> detail. Unmatched/ambiguous agents are labeled honestly, never fabricated.',
+ cost: '$0 (local plain-HTTP, no captcha, no paid API)',
+ fetched_at: new Date().toISOString(),
+ total_agents: agents.length,
+ counts: { verified, unmatched, ambiguous, errors, from_cache: fromCache, in_snapshot: results.length },
+ },
+ agents: results,
+ };
+ fs.writeFileSync(OUT, JSON.stringify(snapshot, null, 1));
+ console.log(`\n✔ wrote ${OUT}`);
+ console.log(` agents total ${agents.length} | verified ${verified} | unmatched ${unmatched} | errors ${errors} | cached ${fromCache}`);
+ console.log(` cost: $0 (local)`);
+}
+module.exports = { toDreName, parseSearch, parseDetail, pick };
+if (require.main === module) {
+ main().catch(e => { console.error('FATAL', e); process.exit(1); });
+}
diff --git a/scripts/serve.js b/scripts/serve.js
index 4c8914e..edfe026 100644
--- a/scripts/serve.js
+++ b/scripts/serve.js
@@ -225,6 +225,35 @@ app.get('/api/leads/frank', async (req, res) => {
} catch (e) { res.status(502).json({ error: String(e.message).split('\n')[0], leads: [] }); }
});
+// Licensed agents/brokers — every RE agent CRCP harvested, backed by a REAL California DRE
+// license pulled from PUBLIC government data (www2.dre.ca.gov). DB-first -> snapshot fallback,
+// same idiom as /api/residential so it works locally (Postgres) AND on prod (snapshot JSON,
+// produced by scripts/fetch-dre-licenses.js). Public-safe fields only — no mailing addresses.
+const LICENSED_LABEL = 'Real-estate agents/brokers verified against the California DRE public license lookup (govt data, $0). Unmatched/ambiguous agents labeled honestly, never fabricated.';
+app.get('/api/agents/licensed', async (req, res) => {
+ const PUBLIC = ['agent', 'brokerage', 'license', 'license_type', 'license_city', 'status',
+ 'status_label', 'expiration', 'issued', 'responsible_broker', 'responsible_broker_id',
+ 'discipline', 'match_confidence', 'dre_verified', 'dre_url'];
+ const pubOnly = a => Object.fromEntries(PUBLIC.filter(k => k in a).map(k => [k, a[k]]));
+ // 1) live DB first (broker table already carries a license column)
+ if (brokerdb) {
+ try {
+ const r = await brokerdb.pool.query(
+ `SELECT name AS agent, firm_name AS brokerage, license,
+ license_type, license_status AS status, license_expiration AS expiration
+ FROM broker WHERE license IS NOT NULL AND license <> '' ORDER BY name LIMIT 5000`);
+ if (r.rows.length) return res.json({ agents: r.rows, label: LICENSED_LABEL, source: 'db' });
+ } catch (_) { /* columns may not exist yet -> fall through to snapshot */ }
+ }
+ // 2) snapshot fallback (prod / no-DB)
+ try {
+ const { meta = {}, agents = [] } = JSON.parse(fs.readFileSync(path.join(ROOT, 'data', 'licensed-agents.json'), 'utf8'));
+ return res.json({ agents: agents.map(pubOnly), meta, label: LICENSED_LABEL, source: 'snapshot' });
+ } catch (e) {
+ return res.json({ agents: [], note: 'no licensed-agent data yet — run scripts/fetch-dre-licenses.js', error: String(e.message).split('\n')[0] });
+ }
+});
+
// Property history for ONE address — last ~100 yrs, ADDRESS-ONLY (no owner/resident names, by design).
// Combines: the public assessor parcel roll (year built, beds/baths/sqft, use, valuation — current roll),
// sold transactions from closed_sale (Redfin sold history), and the WhoLivedThere/pastdoor archive
← a3bff78 afternoon CRE update 2026-07-10
·
back to Commercialrealestate
·
CRCP DRE: contrarian fixes — verified only on exact match, d 6abac2e →