← back to Commercialrealestate
assumable-loan heuristic + estimate model + credentialed source adapters + integration plan
5f05fc15738c8fe95975631e09e5075b00845feb · 2026-06-28 16:33:27 -0700 · Steve Abrams
Files touched
A docs/data-integration-plan.mdA scripts/assumable-heuristic.jsA scripts/db/assumable-schema.sqlA scripts/sources/appraisal-panel.jsA scripts/sources/fnma-cpm.jsA scripts/sources/title-records.js
Diff
commit 5f05fc15738c8fe95975631e09e5075b00845feb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Jun 28 16:33:27 2026 -0700
assumable-loan heuristic + estimate model + credentialed source adapters + integration plan
---
docs/data-integration-plan.md | 79 ++++++++++++++
scripts/assumable-heuristic.js | 208 +++++++++++++++++++++++++++++++++++++
scripts/db/assumable-schema.sql | 57 ++++++++++
scripts/sources/appraisal-panel.js | 59 +++++++++++
scripts/sources/fnma-cpm.js | 60 +++++++++++
scripts/sources/title-records.js | 78 ++++++++++++++
6 files changed, 541 insertions(+)
diff --git a/docs/data-integration-plan.md b/docs/data-integration-plan.md
new file mode 100644
index 0000000..19c2a19
--- /dev/null
+++ b/docs/data-integration-plan.md
@@ -0,0 +1,79 @@
+# Data Integration Plan — assumable-loan signal & credentialed sources
+
+LA County CRE / loan-origination control panel (Arcstone). This plan covers the four credentialed data
+sources that sharpen the **assumable FHA/VA loan** signal and the broader warrantability / valuation work,
+plus the recommended sequencing.
+
+## The assumable-loan feature, today (shipped)
+
+FHA & VA loans are **assumable** — a qualified buyer can take over the seller's existing low-rate note
+instead of originating a new loan at today's rate. For Arcstone that is a callable deal: a listing likely
+carrying a sub-4% assumable FHA/VA loan is worth a phone call.
+
+We ship a transparent **heuristic** now (`scripts/assumable-heuristic.js` → `cre.assumable_estimate`):
+
+- **Inputs we already have ($0, local):** property type (condo/SFR are FHA/VA-eligible; commercial isn't),
+ price band (LA County 2024 FHA ceiling ≈ **$1,149,825**; VA uncapped but jumbo-VA rare), and purchase era
+ (when a listing's address matches `closed_sale`, the sale year dates the loan; 2020-2022 → sub-4% era).
+- **Rate proxy:** `assumable_rate_proxy` maps recording year → an **approximate** historical-average 30yr
+ fixed rate (2016-2026), labelled approximate everywhere.
+- **Output per property:** `{ assumable_likelihood: low|medium|high, est_assumable_rate, est_sale_year,
+ basis, confidence:'heuristic — verify in title records', signals }`.
+
+**Honest labeling (hard rule):** every estimate is a screening signal, not a title-verified fact. The
+heuristic CAN misfire (a 2020-2022 sale may have been cash/conventional, not FHA/VA). It exists to
+**prioritize calls**, never to assert an assumption.
+
+**Known limitation (recorded dissent, in data):** the for-sale feed and the recently-sold feed barely
+overlap (~6 address matches across 10,392 properties), so the era signal is sparse today — most properties
+score on price-band only. This is exactly the gap **title records** close: title carries the recorded
+deed-of-trust date for *every* property, not just ones that recently re-sold.
+
+---
+
+## Source 1 — TITLE RECORDS (adapter: `scripts/sources/title-records.js`) ★ recommended first
+
+| | |
+|---|---|
+| **Data it provides** | The recorded **deed of trust** per property: lender, loan **type** (FHA / VA / Conventional), **recording date**, original amount. Rate is inferable from program + date. This is the **authoritative** assumable-loan signal. |
+| **Access / credential needed** | A title-company recorded-document login — `TITLE_RECORDS_API_KEY` (API key or session token) for one of **TitlePro247 / Property360 / DataTrace / ResWare / SoftPro**, plus `TITLE_RECORDS_PROVIDER` (which system) and `TITLE_RECORDS_BASE_URL` (API base). Steve can likely get a login. |
+| **Integration design** | Adapter exports `getRecordedLoans({address,city,zip}) → { available, matched, loans[] }`. The heuristic calls it **first** for every property; on an FHA/VA recorded loan it flips the estimate to `confidence:'title-verified'` (`basis:'title'`). Uncredentialed → `{available:false}` and the heuristic runs. **Zero rework** when the login lands — only the provider response mapping in the marked INTEGRATION POINT. |
+| **Why first** | Highest leverage: converts the whole assumable feature from screening-heuristic to ground truth, and closes the sparse-era gap above. Steve can plausibly get the login. |
+
+## Source 2 — APPRAISAL PANEL (adapter: `scripts/sources/appraisal-panel.js`)
+
+| | |
+|---|---|
+| **Data it provides** | Near-guaranteed **supportable value** on LA-area properties — the gold standard vs our Census-rent / closed-sale estimate. |
+| **Access / credential needed** | `APPRAISAL_PANEL_API_KEY` + `APPRAISAL_PANEL_BASE_URL`, OR confirmation of how Steve pulls values (manual portal vs API). Steve has **direct access**. |
+| **Integration design** | `getSupportableValue({address,city,zip}) → { available, matched, value, confidence, asOf }`. Where present, replaces the Census/closed-sale value estimate on a card and relabels it "supportable (appraisal panel)". |
+| **Sequencing** | Second. Steve already has access (low friction), and it upgrades valuation across the whole grid — but it's value, not the assumable signal, so it follows title. |
+
+## Source 3 — FNMA Condo Project Manager (CPM) (adapter: `scripts/sources/fnma-cpm.js`)
+
+| | |
+|---|---|
+| **Data it provides** | Per-complex Fannie Mae condo-project **approval / rejection / conditions** — beyond our HUD FHA-approved list (FHA only). Authoritative warrantability for the `standard-condo` segment. |
+| **Access / credential needed** | `FNMA_CPM_API_KEY` (lender CPM credential / session token, or a marker that lookups are manual + exported) and optional `FNMA_CPM_BASE_URL`. Credentialed and **partly manual** (lender portal, no public API). |
+| **Integration design** | `getProjectStatus({project,address,city,zip}) → { available, matched, status, date, conditions }`. Best fed by a periodic CPM **export** this adapter reads, keyed by project name + zip; upgrades condo warrantability from FHA-proxy to Fannie-authoritative. |
+| **Sequencing** | Third. Highest friction (manual/lender-gated, no API) and narrower (condos only). |
+
+## Source 4 — (in place) HUD FHA-approved condo list
+
+Already loaded (`cre.fha_condo`, 2,661 rows) as the warrantability **proxy**. CPM (Source 3) supersedes it
+where available; until then it stays the labeled FHA-only proxy.
+
+---
+
+## Recommended sequencing (highest leverage first)
+
+1. **TITLE RECORDS** — turns the assumable feature into ground truth and closes the sparse-era gap. Plausibly obtainable. **Light this up first.**
+2. **APPRAISAL PANEL** — Steve already has access; upgrades valuation grid-wide. Low friction.
+3. **FNMA CPM** — narrower (condos) and partly manual; do after the two above.
+
+All four adapters share one contract: read credential from a config slot (env / `.env`), return
+`{available:false, needs:'<exact credential>'}` until configured, and expose a fixed return shape so the
+moment a login is dropped in it plugs in with zero caller rework.
+
+**Cost:** the assumable heuristic and all adapter scaffolding are **$0 (local)**. The credentialed
+integrations themselves carry whatever per-lookup cost the provider charges — surfaced when wired.
diff --git a/scripts/assumable-heuristic.js b/scripts/assumable-heuristic.js
new file mode 100644
index 0000000..70b32d6
--- /dev/null
+++ b/scripts/assumable-heuristic.js
@@ -0,0 +1,208 @@
+// assumable-heuristic.js — estimate P(a for-sale property carries an assumable FHA/VA loan) for every
+// condo + SFR in cre, and UPSERT the result into cre.assumable_estimate.
+//
+// WHY: FHA & VA loans are assumable — a qualified buyer can take over the seller's existing low-rate
+// note. For Arcstone (loan originator) a listing likely carrying a sub-4% assumable FHA/VA loan is a
+// callable deal. The AUTHORITATIVE source is the recorded deed of trust in TITLE RECORDS. We do not have
+// title access yet, so this is a transparent HEURISTIC screen that plugs title data in the moment the
+// adapter (scripts/sources/title-records.js) is credentialed.
+//
+// HONEST LABELING (HARD): every estimate is a screening signal, confidence 'heuristic — verify in title
+// records'. The heuristic CAN misfire (a 2021 sale may have been cash/conventional, not FHA/VA) — it is
+// call-prioritization, never an authoritative assumption finding. When the title adapter returns a real
+// recorded loan, basis flips to 'title' and confidence to 'title-verified' for that property.
+//
+// HEURISTIC INPUTS (all from what we already have, $0 local):
+// 1. PROPERTY TYPE — condos + SFR are FHA/VA-eligible; commercial is not. (We only score condo+sfr.)
+// 2. PRICE BAND — FHA/VA borrow within program limits. LA County 2024 FHA ceiling ~$1,149,825; VA
+// has no cap but jumbo-VA is rare. Above the FHA ceiling, FHA is impossible and a
+// large VA loan is unlikely -> likelihood drops.
+// 3. PURCHASE ERA — when the property's address matches a closed_sale, the SALE YEAR dates the loan.
+// 2020-2022 purchases sit on sub-4% rates (assumption is most valuable then).
+// The rate is read from assumable_rate_proxy (APPROXIMATE historical averages).
+//
+// Usage: node scripts/assumable-heuristic.js (recompute all; applies schema first)
+// node scripts/assumable-heuristic.js --limit 50 (smoke test)
+'use strict';
+const fs = require('fs');
+const path = require('path');
+const { Pool } = require('pg');
+const titleRecords = require('./sources/title-records');
+
+const ROOT = path.join(__dirname, '..');
+const pool = new Pool({
+ host: process.env.CRE_PGHOST || '/tmp', port: +(process.env.CRE_PGPORT || 5432),
+ database: process.env.CRE_PGDATABASE || 'cre', user: process.env.CRE_PGUSER || process.env.USER || 'stevestudio2'
+});
+
+const FHA_CEILING_LA_2024 = 1149825; // LA County 2024 FHA one-unit loan limit (the price-band cutoff)
+
+// APPROXIMATE annual-average FHA/VA 30yr fixed rates by recording year. Ballpark historical averages
+// (Freddie Mac PMMS-grade), used only to infer the rate a loan recorded that year likely carries.
+// Labelled approximate everywhere it surfaces. Pre-2016 collapses into a single ~4.0% bucket.
+const RATE_PROXY = [
+ [2016, 3.65, 'approx avg 30yr fixed 2016'],
+ [2017, 3.99, 'approx avg 30yr fixed 2017'],
+ [2018, 4.54, 'approx avg 30yr fixed 2018'],
+ [2019, 3.94, 'approx avg 30yr fixed 2019'],
+ [2020, 3.11, 'approx avg 30yr fixed 2020 — sub-4 era'],
+ [2021, 2.96, 'approx avg 30yr fixed 2021 — record-low era'],
+ [2022, 5.34, 'approx avg 30yr fixed 2022 — rates rising'],
+ [2023, 6.81, 'approx avg 30yr fixed 2023'],
+ [2024, 6.72, 'approx avg 30yr fixed 2024'],
+ [2025, 6.80, 'approx avg 30yr fixed 2025 (provisional)'],
+ [2026, 6.70, 'approx avg 30yr fixed 2026 (provisional)']
+];
+// Normalize a street address to a join key: lowercase, strip unit/suite/#, collapse whitespace.
+const normAddr = (a) => String(a || '').toLowerCase()
+ .replace(/\b(unit|apt|apartment|ste|suite|#)\s*[\w-]+/g, '')
+ .replace(/[.,]/g, ' ').replace(/\s+/g, ' ').trim();
+
+async function applySchema() {
+ await pool.query(fs.readFileSync(path.join(__dirname, 'db', 'assumable-schema.sql'), 'utf8'));
+ // (Re)seed the rate proxy idempotently.
+ for (const [yr, rate, note] of RATE_PROXY) {
+ await pool.query(`INSERT INTO assumable_rate_proxy(rate_year, est_rate, note) VALUES($1,$2,$3)
+ ON CONFLICT(rate_year) DO UPDATE SET est_rate=EXCLUDED.est_rate, note=EXCLUDED.note`, [yr, rate, note]);
+ }
+}
+
+// Build an address -> most-recent sale-year map from closed_sale (the era-sharpening signal).
+async function saleYearByAddress() {
+ const r = await pool.query(
+ `SELECT address, zip, date_part('year', sold_date)::int yr FROM closed_sale
+ WHERE sold_date IS NOT NULL ORDER BY sold_date ASC`);
+ const m = new Map(); // key = normAddr|zip (last write wins -> most recent sale, since ordered ASC)
+ for (const row of r.rows) m.set(normAddr(row.address) + '|' + (row.zip || ''), row.yr);
+ return m;
+}
+
+const rateForYear = (yr) => {
+ if (yr == null) return null;
+ const hit = RATE_PROXY.find(([y]) => y === yr);
+ if (hit) return hit[1];
+ return yr < 2016 ? 4.00 : RATE_PROXY[RATE_PROXY.length - 1][1];
+};
+
+// The core heuristic. Given a property + an optional matched sale year, produce the estimate object.
+function scoreProperty(p, saleYear) {
+ const signals = [];
+ let score = 0; // 0..3 informal; mapped to low/medium/high at the end
+
+ // (1) type — condo + SFR are FHA/VA-eligible stock (we only feed those in, so this always contributes).
+ signals.push({ factor: 'property_type', value: p.src, note: `${p.src} is FHA/VA-eligible stock` });
+ score += 1;
+
+ // (2) price band vs FHA ceiling.
+ const price = Number(p.price) || 0;
+ if (price > 0 && price <= FHA_CEILING_LA_2024) {
+ signals.push({ factor: 'price_band', value: price, note: `<= LA 2024 FHA limit $${FHA_CEILING_LA_2024.toLocaleString()} — FHA/VA-financeable` });
+ score += 1;
+ } else if (price > FHA_CEILING_LA_2024) {
+ signals.push({ factor: 'price_band', value: price, note: `> LA 2024 FHA limit — FHA impossible, jumbo-VA rare` });
+ score -= 1;
+ } else {
+ signals.push({ factor: 'price_band', value: null, note: 'price unknown' });
+ }
+
+ // (3) purchase era — only when an address matched a closed_sale. 2020-2022 -> sub-4% loan likely.
+ let estRate = null, estSaleYear = null;
+ if (saleYear != null) {
+ estSaleYear = saleYear;
+ estRate = rateForYear(saleYear);
+ if (saleYear >= 2020 && saleYear <= 2022) {
+ signals.push({ factor: 'purchase_era', value: saleYear, note: `bought ${saleYear} (sub-4% era) — assumable rate ~${estRate}% (approx)` });
+ score += 1;
+ } else {
+ signals.push({ factor: 'purchase_era', value: saleYear, note: `bought ${saleYear} — est rate ~${estRate}% (approx); assumption value lower` });
+ }
+ } else {
+ signals.push({ factor: 'purchase_era', value: null, note: 'no matched recent sale — era unknown' });
+ }
+
+ // Map informal score -> likelihood. High requires BOTH a financeable price AND a sub-4% era match.
+ const subFourEra = saleYear != null && saleYear >= 2020 && saleYear <= 2022;
+ const financeable = price > 0 && price <= FHA_CEILING_LA_2024;
+ let likelihood;
+ if (financeable && subFourEra) likelihood = 'high';
+ else if (financeable && (saleYear == null || score >= 2)) likelihood = 'medium';
+ else likelihood = 'low';
+
+ const basisBits = [`${p.src} ${price ? '$' + price.toLocaleString() : 'price?'}`,
+ financeable ? '≤FHA limit' : '>FHA limit',
+ saleYear != null ? `last sale ${saleYear} → ~${estRate}% (approx)` : 'sale era unknown'];
+
+ return {
+ assumable_likelihood: likelihood,
+ est_assumable_rate: estRate,
+ est_sale_year: estSaleYear,
+ basis: basisBits.join('; '),
+ confidence: 'heuristic — verify in title records',
+ signals: { factors: signals, source: 'heuristic' }
+ };
+}
+
+(async () => {
+ const LIMIT = (() => { const i = process.argv.indexOf('--limit'); return i > -1 ? +process.argv[i + 1] : 0; })();
+ await applySchema();
+
+ // The title adapter plugs in here. When credentialed it returns real recorded loans; until then it is
+ // {available:false} and we fall through to the heuristic for every property.
+ const titleStatus = await titleRecords.status();
+ if (titleStatus.available) console.log('title-records adapter ONLINE — verified loans will override the heuristic.');
+ else console.log(`title-records adapter offline (${titleStatus.needs}) — heuristic only. Estimates are screening signals.`);
+
+ const saleYears = await saleYearByAddress();
+ const props = (await pool.query(
+ `SELECT id, address, zip, price, 'condo' src FROM condo
+ UNION ALL SELECT id, address, zip, price, 'sfr' src FROM sfr
+ ${LIMIT ? 'LIMIT ' + LIMIT : ''}`)).rows;
+
+ let high = 0, med = 0, low = 0, titleVerified = 0;
+ for (const p of props) {
+ let est;
+ // 1) Title records FIRST — authoritative when available.
+ const recorded = await titleRecords.getRecordedLoans({ address: p.address, city: p.city, zip: p.zip });
+ if (recorded && recorded.available && Array.isArray(recorded.loans)) {
+ const fhaVa = recorded.loans.find(l => /fha|va/i.test(l.type || ''));
+ if (fhaVa) {
+ titleVerified++;
+ est = {
+ assumable_likelihood: 'high',
+ est_assumable_rate: fhaVa.rate ?? rateForYear(new Date(fhaVa.recordedDate).getFullYear()),
+ est_sale_year: fhaVa.recordedDate ? new Date(fhaVa.recordedDate).getFullYear() : null,
+ basis: `recorded ${fhaVa.type} deed of trust (${fhaVa.lender || 'lender?'}, ${fhaVa.recordedDate || 'date?'})`,
+ confidence: 'title-verified',
+ signals: { factors: [{ factor: 'title_record', value: fhaVa }], source: 'title' }
+ };
+ }
+ }
+ // 2) Fall back to the heuristic.
+ if (!est) {
+ const yr = saleYears.get(normAddr(p.address) + '|' + (p.zip || ''));
+ est = scoreProperty(p, yr);
+ }
+
+ if (est.assumable_likelihood === 'high') high++;
+ else if (est.assumable_likelihood === 'medium') med++;
+ else low++;
+
+ await pool.query(
+ `INSERT INTO assumable_estimate(property_id, src, assumable_likelihood, est_assumable_rate,
+ est_sale_year, basis, confidence, signals, computed_at)
+ VALUES($1,$2,$3,$4,$5,$6,$7,$8,now())
+ ON CONFLICT(property_id) DO UPDATE SET
+ src=EXCLUDED.src, assumable_likelihood=EXCLUDED.assumable_likelihood,
+ est_assumable_rate=EXCLUDED.est_assumable_rate, est_sale_year=EXCLUDED.est_sale_year,
+ basis=EXCLUDED.basis, confidence=EXCLUDED.confidence, signals=EXCLUDED.signals,
+ computed_at=now()`,
+ [p.id, p.src, est.assumable_likelihood, est.est_assumable_rate, est.est_sale_year,
+ est.basis, est.confidence, JSON.stringify(est.signals)]);
+ }
+
+ console.log(`\nassumable estimate computed for ${props.length} properties`);
+ console.log(` high ${high} | medium ${med} | low ${low} | title-verified ${titleVerified}`);
+ console.log('cost: $0 (local Postgres + local heuristic; no paid API).');
+ console.log('LABEL: every estimate is "heuristic — verify in title records" until the title adapter is credentialed.');
+ await pool.end();
+})().catch(e => { console.error('FATAL', e.message); process.exit(1); });
diff --git a/scripts/db/assumable-schema.sql b/scripts/db/assumable-schema.sql
new file mode 100644
index 0000000..7febafc
--- /dev/null
+++ b/scripts/db/assumable-schema.sql
@@ -0,0 +1,57 @@
+-- assumable-schema.sql — the assumable-loan ESTIMATE layer for the CRE explorer (local Postgres "cre").
+--
+-- WHAT THIS IS: FHA & VA mortgages are assumable — a qualified buyer can take over the seller's existing
+-- low-rate loan instead of originating a new one at today's rate. That is a live, callable signal for a
+-- loan originator (Arcstone): a listing that likely carries a sub-4% assumable FHA/VA note is a deal worth
+-- pitching. The AUTHORITATIVE source for "does this property carry an assumable FHA/VA loan" is the
+-- recorded deed of trust in TITLE RECORDS (lender + loan type + recording date). We do NOT have title
+-- access yet (see scripts/sources/title-records.js — gated adapter).
+--
+-- HONEST LABELING (HARD RULE — matches the condo warrantability rule): every row here is a HEURISTIC
+-- ESTIMATE, never a title-verified fact. assumable_likelihood is a screening signal only. confidence is
+-- ALWAYS 'heuristic — verify in title records' until scripts/sources/title-records.js returns real data,
+-- at which point the basis flips to 'title' and confidence becomes 'title-verified'. The heuristic can
+-- misfire (a 2021 purchase may have been all-cash or conventional, not FHA/VA) — RECORDED DISSENT, by
+-- design: this is a call-prioritization screen, NOT an authoritative assumption finding.
+
+-- Recording-date -> FHA/VA 30yr rate PROXY. Approximate annual-average 30yr fixed rates by program-year,
+-- used to infer the rate a loan recorded in year Y likely carries. Rates are APPROXIMATE historical
+-- averages (Freddie Mac PMMS-grade ballpark for FHA/VA 30yr) and are LABELLED approximate everywhere.
+CREATE TABLE IF NOT EXISTS assumable_rate_proxy (
+ rate_year int PRIMARY KEY,
+ est_rate numeric NOT NULL, -- approximate avg FHA/VA 30yr fixed rate for loans recorded that year
+ note text -- provenance / caveat
+);
+
+-- Per-property assumable estimate. One row per condo/sfr id. Recomputed (idempotent UPSERT) by
+-- scripts/assumable-heuristic.js. src distinguishes which for-sale table the id lives in.
+DO $$ BEGIN
+ CREATE TYPE assumable_likelihood AS ENUM ('low', 'medium', 'high');
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+CREATE TABLE IF NOT EXISTS assumable_estimate (
+ property_id text PRIMARY KEY, -- condo.id or sfr.id
+ src text NOT NULL, -- 'condo' | 'sfr'
+ assumable_likelihood assumable_likelihood NOT NULL,
+ est_assumable_rate numeric, -- proxy rate inferred from last-sale era (may be NULL)
+ est_sale_year int, -- the purchase era used for the rate proxy, when matched
+ basis text NOT NULL, -- human-readable why ("condo $850k <FHA limit; last sale 2021 -> ~3.1%")
+ confidence text NOT NULL DEFAULT 'heuristic — verify in title records',
+ signals jsonb, -- full factor breakdown for transparency on the card
+ computed_at timestamptz DEFAULT now()
+);
+CREATE INDEX IF NOT EXISTS idx_assum_src ON assumable_estimate(src);
+CREATE INDEX IF NOT EXISTS idx_assum_likelihood ON assumable_estimate(assumable_likelihood);
+
+-- Card view: assumable estimate joined back to its for-sale property (condo + sfr union), ready for
+-- /api/assumable. Carries the address/price/city so the front end can render a badge with no extra join.
+CREATE OR REPLACE VIEW assumable_card AS
+ SELECT a.property_id, a.src, a.assumable_likelihood, a.est_assumable_rate, a.est_sale_year,
+ a.basis, a.confidence, a.signals, a.computed_at,
+ p.address, p.city, p.zip, p.price, p.year_built
+ FROM assumable_estimate a
+ JOIN (
+ SELECT id, address, city, zip, price, year_built, 'condo'::text src FROM condo
+ UNION ALL
+ SELECT id, address, city, zip, price, year_built, 'sfr'::text src FROM sfr
+ ) p ON p.id = a.property_id AND p.src = a.src;
diff --git a/scripts/sources/appraisal-panel.js b/scripts/sources/appraisal-panel.js
new file mode 100644
index 0000000..9bb344b
--- /dev/null
+++ b/scripts/sources/appraisal-panel.js
@@ -0,0 +1,59 @@
+// sources/appraisal-panel.js — credentialed adapter for Steve's APPRAISAL PANEL.
+//
+// PURPOSE: Steve has direct access to an appraisal panel and can get near-guaranteed supportable values
+// on LA-area properties — the gold standard versus our Census-rent / closed-sale value estimates. This
+// adapter is the plug-in slot: once the panel's API/login is configured, getSupportableValue(address)
+// returns a real supportable value + confidence; until then it is INERT and returns {available:false}.
+//
+// PLUG-IN CONTRACT:
+// status() -> { available:boolean, needs?:string, provider?:string }
+// getSupportableValue({address,city,zip})
+// -> { available:false, needs:string } // not configured
+// -> { available:true, matched:boolean, value?:number,
+// confidence?:'supportable'|'indicative', asOf?:ISO, basis?:string }
+//
+// GATED: the appraisal-panel access is Steve-provided. Fabricates NOTHING with no credential.
+'use strict';
+
+const CREDENTIAL_ENV = 'APPRAISAL_PANEL_API_KEY'; // panel API key / login token
+const BASE_URL_ENV = 'APPRAISAL_PANEL_BASE_URL'; // panel API base, when applicable
+
+function readCredential() {
+ if (process.env[CREDENTIAL_ENV]) return { key: process.env[CREDENTIAL_ENV], baseUrl: process.env[BASE_URL_ENV] || null };
+ try {
+ const fs = require('fs'), path = require('path');
+ const env = fs.readFileSync(path.join(__dirname, '..', '..', '.env'), 'utf8');
+ const m = env.match(new RegExp('^' + CREDENTIAL_ENV + '=(.*)$', 'm'));
+ if (m) return {
+ key: m[1].replace(/['"]/g, '').trim(),
+ baseUrl: (env.match(new RegExp('^' + BASE_URL_ENV + '=(.*)$', 'm')) || [])[1]?.replace(/['"]/g, '').trim() || null
+ };
+ } catch (_) {}
+ return null;
+}
+
+const NEEDS = `appraisal-panel access — an ${CREDENTIAL_ENV} (the panel's API key or login token) and `
+ + `${BASE_URL_ENV} (the panel API base URL), OR confirmation of how Steve pulls supportable values from `
+ + `his panel (manual portal vs API). Steve has direct access; set these in .env once the integration path is chosen.`;
+
+async function status() {
+ const cred = readCredential();
+ return cred ? { available: true } : { available: false, needs: NEEDS };
+}
+
+// Supportable value for one address. While uncredentialed, {available:false} -> caller uses the existing
+// Census/closed-sale estimate and labels it as such.
+async function getSupportableValue({ address, city, zip } = {}) {
+ const cred = readCredential();
+ if (!cred) return { available: false, needs: NEEDS };
+
+ // --- INTEGRATION POINT (wire when panel access exists) ----------------------------------------------
+ // const url = `${cred.baseUrl}/value?address=${encodeURIComponent(address)}&zip=${zip}`;
+ // const r = await fetch(url, { headers: { Authorization: `Bearer ${cred.key}` } });
+ // const j = await r.json();
+ // return { available:true, matched:true, value:j.supportableValue, confidence:'supportable',
+ // asOf:j.effectiveDate, basis:'appraisal-panel supportable value' };
+ return { available: true, matched: false, note: 'credential present; panel value mapping not yet wired' };
+}
+
+module.exports = { status, getSupportableValue, CREDENTIAL_ENV, BASE_URL_ENV, NEEDS };
diff --git a/scripts/sources/fnma-cpm.js b/scripts/sources/fnma-cpm.js
new file mode 100644
index 0000000..094eb0f
--- /dev/null
+++ b/scripts/sources/fnma-cpm.js
@@ -0,0 +1,60 @@
+// sources/fnma-cpm.js — credentialed adapter for FANNIE MAE Condo Project Manager (CPM).
+//
+// PURPOSE: Fannie Mae's CPM holds per-complex condo-project approvals / rejections / conditions — beyond
+// our HUD FHA-approved list (which only covers FHA). For the 'standard-condo' segment (run the complex
+// through CPM for prior approvals) this is the authoritative warrantability source. CPM is credentialed
+// and partly manual (lender portal). This adapter is the plug-in slot: once Steve provides CPM access,
+// getProjectStatus(project) returns the real status; until then it is INERT and returns {available:false}.
+//
+// PLUG-IN CONTRACT:
+// status() -> { available:boolean, needs?:string }
+// getProjectStatus({ project, address, city, zip })
+// -> { available:false, needs:string } // not configured
+// -> { available:true, matched:boolean, status?:'Approved'|'Conditional'|
+// 'Unavailable'|'Ineligible', date?:ISO, conditions?:string[] } // configured
+//
+// GATED: CPM access is Steve-provided (lender credential; some lookups are manual). Fabricates NOTHING.
+'use strict';
+
+const CREDENTIAL_ENV = 'FNMA_CPM_API_KEY'; // CPM credential / session token (or a marker that manual export is used)
+const BASE_URL_ENV = 'FNMA_CPM_BASE_URL'; // CPM endpoint, when an API/export path is available
+
+function readCredential() {
+ if (process.env[CREDENTIAL_ENV]) return { key: process.env[CREDENTIAL_ENV], baseUrl: process.env[BASE_URL_ENV] || null };
+ try {
+ const fs = require('fs'), path = require('path');
+ const env = fs.readFileSync(path.join(__dirname, '..', '..', '.env'), 'utf8');
+ const m = env.match(new RegExp('^' + CREDENTIAL_ENV + '=(.*)$', 'm'));
+ if (m) return {
+ key: m[1].replace(/['"]/g, '').trim(),
+ baseUrl: (env.match(new RegExp('^' + BASE_URL_ENV + '=(.*)$', 'm')) || [])[1]?.replace(/['"]/g, '').trim() || null
+ };
+ } catch (_) {}
+ return null;
+}
+
+const NEEDS = `Fannie Mae Condo Project Manager (CPM) access — an ${CREDENTIAL_ENV} (lender CPM credential / `
+ + `session token, or a marker that lookups are done manually and exported) and optional ${BASE_URL_ENV}. `
+ + `CPM is credentialed and partly manual; Steve provides lender access. Set in .env, or supply a periodic `
+ + `CPM export this adapter can read.`;
+
+async function status() {
+ const cred = readCredential();
+ return cred ? { available: true } : { available: false, needs: NEEDS };
+}
+
+// CPM project status for one complex. While uncredentialed, {available:false} -> the condo segment uses
+// the HUD FHA-approved proxy only and labels it as such.
+async function getProjectStatus({ project, address, city, zip } = {}) {
+ const cred = readCredential();
+ if (!cred) return { available: false, needs: NEEDS };
+
+ // --- INTEGRATION POINT (wire when CPM access exists) ------------------------------------------------
+ // CPM has no public API; integration is typically a credentialed lookup or a periodic export this
+ // adapter reads. Example (export-file path):
+ // const rec = lookupCpmExport({ project, zip }); // local CPM export keyed by project name + zip
+ // if (rec) return { available:true, matched:true, status:rec.status, date:rec.statusDate, conditions:rec.conditions };
+ return { available: true, matched: false, note: 'credential present; CPM lookup/export not yet wired' };
+}
+
+module.exports = { status, getProjectStatus, CREDENTIAL_ENV, BASE_URL_ENV, NEEDS };
diff --git a/scripts/sources/title-records.js b/scripts/sources/title-records.js
new file mode 100644
index 0000000..a786d8a
--- /dev/null
+++ b/scripts/sources/title-records.js
@@ -0,0 +1,78 @@
+// sources/title-records.js — credentialed adapter for TITLE-COMPANY recorded-document search.
+//
+// PURPOSE: the authoritative answer to "does this property carry an assumable FHA/VA loan" lives in the
+// recorded deed of trust (lender, loan type, recording date; rate inferable from program + date). Title
+// companies expose this via systems like TitlePro247, Property360, DataTrace, ResWare, or SoftPro. Steve
+// can likely get a login. Until that credential is dropped in, this adapter is INERT and returns
+// {available:false, needs:'<exact login Steve must provide>'} so callers fall back to the heuristic.
+//
+// PLUG-IN CONTRACT (zero rework when credentialed):
+// status() -> { available:boolean, needs?:string, provider?:string }
+// getRecordedLoans({address,city,zip})
+// -> { available:false, needs:string } // not configured
+// -> { available:true, matched:boolean, loans:RecordedLoan[] } // configured
+// RecordedLoan = { lender, type ('FHA'|'VA'|'Conventional'|...), recordedDate (ISO), origAmount (number), rate? }
+//
+// The assumable heuristic calls getRecordedLoans() FIRST for every property and only falls back to the
+// heuristic when {available:false}. So the moment the title login is configured, real recorded loans
+// flip the estimate from "heuristic" to "title-verified" with no caller changes.
+//
+// GATED: the title login is Steve-provided. This file fabricates NOTHING — with no credential it returns
+// {available:false}. Do not stub fake loans here.
+'use strict';
+
+// Credential slot. Resolved from env first, then the project .env. The exact var Steve must provide.
+const CREDENTIAL_ENV = 'TITLE_RECORDS_API_KEY'; // primary credential (API key or session token)
+const PROVIDER_ENV = 'TITLE_RECORDS_PROVIDER'; // e.g. 'titlepro247' | 'datatrace' | 'property360'
+const BASE_URL_ENV = 'TITLE_RECORDS_BASE_URL'; // provider API base, when applicable
+
+function readCredential() {
+ const fromEnv = process.env[CREDENTIAL_ENV];
+ if (fromEnv) return { key: fromEnv, provider: process.env[PROVIDER_ENV] || 'unknown', baseUrl: process.env[BASE_URL_ENV] || null };
+ // (Optional) fall back to a .env line without taking a dotenv dependency.
+ try {
+ const fs = require('fs'), path = require('path');
+ const env = fs.readFileSync(path.join(__dirname, '..', '..', '.env'), 'utf8');
+ const m = env.match(new RegExp('^' + CREDENTIAL_ENV + '=(.*)$', 'm'));
+ if (m) return {
+ key: m[1].replace(/['"]/g, '').trim(),
+ provider: (env.match(new RegExp('^' + PROVIDER_ENV + '=(.*)$', 'm')) || [])[1]?.replace(/['"]/g, '').trim() || 'unknown',
+ baseUrl: (env.match(new RegExp('^' + BASE_URL_ENV + '=(.*)$', 'm')) || [])[1]?.replace(/['"]/g, '').trim() || null
+ };
+ } catch (_) {}
+ return null;
+}
+
+const NEEDS = `a title-company recorded-document login — a ${CREDENTIAL_ENV} (API key or session token) for one of `
+ + `TitlePro247 / Property360 / DataTrace / ResWare / SoftPro, plus ${PROVIDER_ENV} (which system) and `
+ + `${BASE_URL_ENV} (the API base URL). Steve provides the login; set these in .env.`;
+
+async function status() {
+ const cred = readCredential();
+ if (!cred) return { available: false, needs: NEEDS };
+ return { available: true, provider: cred.provider };
+}
+
+// Look up recorded deeds of trust for one address. Returns the loans the title system has on file.
+// While uncredentialed, returns {available:false} — callers fall back to the heuristic.
+async function getRecordedLoans({ address, city, zip } = {}) {
+ const cred = readCredential();
+ if (!cred) return { available: false, needs: NEEDS };
+
+ // --- INTEGRATION POINT (wire when the title login exists) -------------------------------------------
+ // Each provider differs; the shape returned to the caller is fixed (RecordedLoan[]). Example skeleton:
+ //
+ // const url = `${cred.baseUrl}/search?address=${encodeURIComponent(address)}&zip=${zip}`;
+ // const r = await fetch(url, { headers: { Authorization: `Bearer ${cred.key}` } });
+ // const docs = (await r.json()).documents || [];
+ // const loans = docs.filter(d => /deed of trust/i.test(d.docType)).map(d => ({
+ // lender: d.beneficiary, type: classifyLoanType(d), recordedDate: d.recordedDate,
+ // origAmount: d.amount, rate: null /* inferred downstream from program+date */ }));
+ // return { available: true, matched: loans.length > 0, loans };
+ //
+ // Until then, signal "configured but not yet wired" so it's obvious the credential landed but the
+ // provider mapping is the remaining work.
+ return { available: true, matched: false, loans: [], note: `credential present for ${cred.provider}; provider mapping not yet wired` };
+}
+
+module.exports = { status, getRecordedLoans, CREDENTIAL_ENV, PROVIDER_ENV, BASE_URL_ENV, NEEDS };
← 50e36fe CRE: transaction-party entity model — title/escrow/insurance
·
back to Commercialrealestate
·
additive: Assumable FHA/VA call segment + read-only /api/ass 80c2838 →