← back to Commercialrealestate
scripts/fetch-ula-deals.js
90 lines
#!/usr/bin/env node
// fetch-ula-deals.js — LA City Measure ULA transfer-tax sales → CRCP deal-flow feed.
//
// WHY: CRCP's county "Closed Deal Flow" is built from the LA County Assessor ANNUAL roll, which
// lags 1-2 years (tops out at 2024). The LA City Office of Finance publishes the Measure ULA
// transfer tax it collects on every $5.3M+ property sale in the CITY of LA — CURRENT (through the
// latest month) and PUBLIC RECORD. Because ULA is a flat % of the FULL sale price, we can recover
// the EXACT sale price from the tax paid. This surfaces current 2025/2026 high-value LA City CRE
// deals that the assessor roll won't show for another 1-2 years.
//
// SOURCE (free, no key, non-proprietary): Socrata dataset `jqan-regh` on data.lacity.org.
// https://data.lacity.org/resource/jqan-regh.json
// Fields: id (RR/CC instrument number), month, fiscal_year, revenue (ULA $ paid),
// transaction_value (bracket), property_use, zip_code, council_district.
//
// PRICE = revenue / rate: "$5.3-$10.6M" -> 4.0% ; "Over $10.6M" -> 5.5% (ULA is on full price).
// DATE = (month name + LA City fiscal year). FY runs Jul 1–Jun 30, so Jul–Dec belong to FY-1.
//
// Output: data/ula-la-deals.json {deals:[...]} in the same shape deals-flow.html merges for the
// SEC EDGAR feed, so the front-end picks it up as a 4th source with zero schema work.
// $0, local, read-only against a public gov API. Run: node scripts/fetch-ula-deals.js
'use strict';
const fs = require('fs');
const path = require('path');
const https = require('https');
const OUT = path.join(__dirname, '..', 'data', 'ula-la-deals.json');
const URL = 'https://data.lacity.org/resource/jqan-regh.json?$limit=5000&$order=index%20DESC';
// CRE-focused: keep income/commercial property types; drop pure single-family + (optionally) vacant.
const KEEP_USES = new Set(['Commercial', 'Multi-Family Residential', 'Mixed Use', 'Mixed Residential', 'Vacant']);
const RATE = { '$5.3-$10.6M': 0.04, 'Over $10.6M': 0.055 };
const MONTHS = { january:1, february:2, march:3, april:4, may:5, june:6, july:7, august:8, september:9, october:10, november:11, december:12 };
function get(url) {
return new Promise((resolve, reject) => {
https.get(url, { headers: { 'User-Agent': 'CRCP-deal-flow/1.0 (public-records)' } }, res => {
if (res.statusCode !== 200) { res.resume(); return reject(new Error('HTTP ' + res.statusCode)); }
let b = ''; res.on('data', c => b += c); res.on('end', () => { try { resolve(JSON.parse(b)); } catch (e) { reject(e); } });
}).on('error', reject);
});
}
// (month name, fiscal_year) -> ISO date. LA City FY: Jul–Jun. Jul–Dec => calendar year FY-1.
function calDate(monthName, fy) {
const m = MONTHS[String(monthName || '').trim().toLowerCase()];
const f = parseInt(fy, 10);
if (!m || !f) return null;
const year = m >= 7 ? f - 1 : f; // Jul(7)–Dec(12) fall in the prior calendar year
return `${year}-${String(m).padStart(2, '0')}-01`;
}
(async () => {
const rows = await get(URL);
const deals = [];
let skippedUse = 0, skippedPrice = 0;
for (const r of rows) {
if (!KEEP_USES.has(r.property_use)) { skippedUse++; continue; }
const rate = RATE[r.transaction_value];
const rev = parseFloat(r.revenue);
if (!rate || !(rev > 0)) { skippedPrice++; continue; }
const price = Math.round(rev / rate);
const date = calDate(r.month, r.fiscal_year);
const year = date ? parseInt(date.slice(0, 4), 10) : null;
const instrument = (r.id && r.id !== 'corporate') ? r.id : null;
deals.push({
kind: 'ula',
name: `${r.property_use} — Los Angeles ${r.zip_code || ''}`.trim(),
use: r.property_use,
price,
date, year,
deal_city: 'Los Angeles',
zip: r.zip_code || null,
council_district: r.council_district || null,
doc_number: instrument, // RR/CC instrument number (bridge to the recorded deed)
confidence: 'high', // exact price derived from the transfer tax
source_label: 'LA City ULA',
source: 'LA City Office of Finance — Measure ULA transfer tax (public record, resellable)',
source_type: 'public-record',
price_basis: `ULA ${(rate * 100).toFixed(1)}% of sale price`,
});
}
deals.sort((a, b) => String(b.date || '').localeCompare(String(a.date || '')));
const byYear = {};
for (const d of deals) { const y = d.year || '—'; byYear[y] = (byYear[y] || 0) + 1; }
const out = { generated_at: new Date().toISOString(), source: 'data.lacity.org jqan-regh (Measure ULA)', count: deals.length, byYear, deals };
fs.writeFileSync(OUT, JSON.stringify(out, null, 1));
console.log(`[ula] wrote ${deals.length} deals -> ${path.relative(process.cwd(), OUT)} (skipped ${skippedUse} non-CRE uses, ${skippedPrice} no-price)`);
console.log('[ula] by year:', JSON.stringify(byYear));
})().catch(e => { console.error('[ula] FAILED:', e.message); process.exit(1); });