[object Object]

← back to Re Flyer Aggregator

be-first PRIMARY feed: free CRE-news closed-deal monitor (TRD-LA/CommercialObserver/ConnectCRE/Registry-NorCal), sale+price+west filter + non-deal exclusion, dedupe, alert feed; wired into daily cron alongside EDGAR + DTT (TK-10708)

854c054fe271d2cae50a6cdbd28c4af803fbdedf · 2026-08-19 13:43:57 -0700 · Steve Abrams

Files touched

Diff

commit 854c054fe271d2cae50a6cdbd28c4af803fbdedf
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 19 13:43:57 2026 -0700

    be-first PRIMARY feed: free CRE-news closed-deal monitor (TRD-LA/CommercialObserver/ConnectCRE/Registry-NorCal), sale+price+west filter + non-deal exclusion, dedupe, alert feed; wired into daily cron alongside EDGAR + DTT (TK-10708)
---
 data/crenews-seen.json                |  1 +
 out/crenews-new-deals-2026-08-19.json | 57 +++++++++++++++++++++++++
 scripts/cre-news-monitor.mjs          | 80 +++++++++++++++++++++++++++++++++++
 scripts/dtt-cron.sh                   | 11 ++---
 4 files changed, 144 insertions(+), 5 deletions(-)

diff --git a/data/crenews-seen.json b/data/crenews-seen.json
new file mode 100644
index 0000000..9daf756
--- /dev/null
+++ b/data/crenews-seen.json
@@ -0,0 +1 @@
+["https://therealdeal.com/la/2026/08/19/pomerancs-brothers-miss-maturity-loan-in-special-servicing/","https://therealdeal.com/la/2026/08/19/kennedy-wilson-taps-jaime-lee/","https://therealdeal.com/la/2026/08/19/rexford-selling-industrial-portfolio-to-eqt-real-estate/","https://therealdeal.com/la/2026/08/19/century-city-office-overflows-to-beverly-hills/","https://therealdeal.com/la/2026/08/18/srs-arranges-sale-of-san-dimas-industrial-campus/","https://therealdeal.com/la/2026/08/18/newport-beach-approves-tpg-lincoln-housing-plans/","https://therealdeal.com/la/2026/08/18/pacific-urban-buys-miracle-mile-apartments/","https://therealdeal.com/la/2026/08/18/luxury-contracts-in-la-county-hit-lowest-week-since-january/","https://therealdeal.com/la/2026/08/18/irvine-co-usa-properties-start-tustin-affordable-housing/","https://therealdeal.com/la/2026/08/17/planning-commission-approves-apartments-on-la-cienega/","https://commercialobserver.com/2026/08/rexford-eqt-sale-california-industrial/","https://commercialobserver.com/2026/08/diamond-home-lease-20-west-36th-street/","https://commercialobserver.com/2026/08/zar-properties-sale-31-35-great-jones-street-noho/","https://commercialobserver.com/2026/08/mnrk-music-group-lease-rosen-443-park-avenue-south/","https://commercialobserver.com/2026/08/esrt-1359-broadway-sale-thor-equities/","https://commercialobserver.com/2026/08/finmarc-virginia-office-cim-group/","https://commercialobserver.com/2026/08/l3-capital-leases-bylt-basics-zara-williamsburg/","https://commercialobserver.com/2026/08/miami-design-district-snohetta-sweetbird-north/","https://commercialobserver.com/2026/08/burlington-lease-bay-ridge-retail-century-marketplace/","https://commercialobserver.com/2026/08/urban-standard-capital-27m-land-loan-vail-colo-complex/","https://commercialobserver.com/2026/08/new-york-485-x-apartment-limits/","https://commercialobserver.com/2026/08/investors-confidence-office-situsamc-survey/","https://commercialobserver.com/2026/08/santo-taco-sublease-dig-inn-232-bleecker-street/","https://commercialobserver.com/2026/08/goodland-pickleball-lease-williamsburg-rego-park-clubs/","https://commercialobserver.com/2026/08/national-resources-450m-sale-lincoln-property-company-saber-hightower-waterfall-asset-management/","https://commercialobserver.com/2026/08/reits-q2-hoya-capital-investors-report/","https://commercialobserver.com/2026/08/ocvibe-scott-frick-anaheim-housing-entertainment/","https://www.connectcre.com/stories/massachusetts-ag-orders-post-merger-sale-of-two-boston-apartment-towers/","https://www.connectcre.com/stories/goldie-initiative-draws-record-applicant-pool-for-45-scholar-spots/","https://www.connectcre.com/stories/entre-commercial-realty-negotiates-long-term-lease-at-truck-maintenance-facility/","https://www.connectcre.com/stories/net-leased-dutch-bros-property-in-indiana-changes-hands/","https://www.connectcre.com/stories/woodmore-towne-centre-in-md-signs-tenants-to-long-term-leases/","https://www.connectcre.com/stories/saragon-launches-to-serve-growing-demand-for-colocation/","https://www.connectcre.com/stories/lincoln-property-company-names-tricia-moore-as-executive-vice-president-asset-management/","https://www.connectcre.com/stories/potrero-hill-apartments-fetch-one-of-san-franciscos-highest-per-unit-prices/","https://www.connectcre.com/stories/jpmorgan-chase-opens-magnificent-mile-branch/","https://www.connectcre.com/stories/kidder-mathews-expanding-into-dallas/","https://news.theregistrysf.com/valiance-capital-canyon-partners-open-265-bed-the-valiant-student-housing-tower-in-berkeleys-southside/","https://news.theregistrysf.com/alameda-county-weighs-handing-supervisors-power-over-measure-d-easing-development-limits-on-livermores-protected-hills/","https://news.theregistrysf.com/93000-sqft-downtown-novato-center-lists-for-24-76mm-in-marin-county/","https://news.theregistrysf.com/valley-oak-partners-41000-sqft-agnews-historic-campus-hits-the-market-in-santa-clara/","https://news.theregistrysf.com/nimitz-groups-2mm-sqft-mare-island-shipyard-hits-the-market-in-vallejo/","https://news.theregistrysf.com/sack-capital-partners-lem-capital-acquire-122-unit-koda-apartments-in-fremont/","https://news.theregistrysf.com/sacramentos-rental-housing-bills-advance-toward-final-votes-with-real-stakes-for-northern-california-landlords/","https://news.theregistrysf.com/state-of-california-signs-28000-sqft-lease-at-del-paso-corporate-center-in-north-natomas/","https://news.theregistrysf.com/canopy-clinches-presidios-largest-lease-of-2026-with-full-40000-sqft-building-102-in-san-francisco/","https://news.theregistrysf.com/agility-robotics-lands-60000-sqft-physical-ai-hub-in-fremont-adds-nearly-200-jobs-ahead-of-nasdaq-debut/"]
\ No newline at end of file
diff --git a/out/crenews-new-deals-2026-08-19.json b/out/crenews-new-deals-2026-08-19.json
new file mode 100644
index 0000000..ee05f78
--- /dev/null
+++ b/out/crenews-new-deals-2026-08-19.json
@@ -0,0 +1,57 @@
+[
+  {
+    "feed": "TRD-LA",
+    "feed_region": "LA",
+    "title": "Rexford strikes $1.2B industrial portfolio deal with EQT amid strategic selloff",
+    "link": "https://therealdeal.com/la/2026/08/19/rexford-selling-industrial-portfolio-to-eqt-real-estate/",
+    "date": "Wed, 19 Aug 2026 19:00:00 +0000",
+    "price": 1200000000,
+    "label": "$1.2B",
+    "location": "Southern California",
+    "type": "sale"
+  },
+  {
+    "feed": "CommercialObserver",
+    "feed_region": "National",
+    "title": "Rexford to Sell $1.2B SoCal Portfolio to EQT",
+    "link": "https://commercialobserver.com/2026/08/rexford-eqt-sale-california-industrial/",
+    "date": "Wed, 19 Aug 2026 20:30:46 +0000",
+    "price": 1200000000,
+    "label": "$1.2B",
+    "location": "SoCal",
+    "type": "sale"
+  },
+  {
+    "feed": "ConnectCRE",
+    "feed_region": "National",
+    "title": "Potrero Hill Apartments Fetch One of San Francisco's Highest Per-Unit Prices",
+    "link": "https://www.connectcre.com/stories/potrero-hill-apartments-fetch-one-of-san-franciscos-highest-per-unit-prices/",
+    "date": "Wed, 19 Aug 2026 19:41:44 +0000",
+    "price": 132000000,
+    "label": "$132M",
+    "location": "San Francisco",
+    "type": "sale"
+  },
+  {
+    "feed": "TRD-LA",
+    "feed_region": "LA",
+    "title": "Pacific Urban buys Miracle Mile apartments from Sares-Regis for $70.5M",
+    "link": "https://therealdeal.com/la/2026/08/18/pacific-urban-buys-miracle-mile-apartments/",
+    "date": "Tue, 18 Aug 2026 20:30:00 +0000",
+    "price": 70500000,
+    "label": "$70.5M",
+    "location": "Los Angeles",
+    "type": "sale"
+  },
+  {
+    "feed": "TRD-LA",
+    "feed_region": "LA",
+    "title": "Institutional investor drops $41M on San Dimas industrial park",
+    "link": "https://therealdeal.com/la/2026/08/18/srs-arranges-sale-of-san-dimas-industrial-campus/",
+    "date": "Tue, 18 Aug 2026 22:45:00 +0000",
+    "price": 41000000,
+    "label": "$41M",
+    "location": null,
+    "type": "sale"
+  }
+]
\ No newline at end of file
diff --git a/scripts/cre-news-monitor.mjs b/scripts/cre-news-monitor.mjs
new file mode 100644
index 0000000..5997d52
--- /dev/null
+++ b/scripts/cre-news-monitor.mjs
@@ -0,0 +1,80 @@
+#!/usr/bin/env node
+// TK-10708  FREE CRE-news closed-deal monitor. Aggregates working CRE trade-press RSS
+// feeds (TRD-LA, Commercial Observer, Connect CRE, The Registry NorCal), filters to
+// SALE ("closed deal") items with a $ amount in the West footprint, extracts price +
+// property + location, dedupes by link -> the same-day "be first" alert feed. $0, no key.
+//
+// Not "before the news exists" -- it's first to have EVERY outlet's closed deals compiled
+// same-day, before a competitor assembles them. Usage: node scripts/cre-news-monitor.mjs [--dry]
+
+import { readFileSync, writeFileSync, existsSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { dirname, join } from 'node:path';
+
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
+const args = process.argv.slice(2);
+const UA = 'Mozilla/5.0 (DW-research; steve@designerwallcoverings.com)';
+const SEEN = join(ROOT, 'data', 'crenews-seen.json');
+
+const FEEDS = [
+  { name: 'TRD-LA', region: 'LA', url: 'https://therealdeal.com/la/feed/' },
+  { name: 'CommercialObserver', region: 'National', url: 'https://commercialobserver.com/feed/' },
+  { name: 'ConnectCRE', region: 'National', url: 'https://www.connectcre.com/feed/' },
+  { name: 'Registry-NorCal', region: 'NorCal', url: 'https://news.theregistrysf.com/feed/' },
+];
+// West footprint terms (regional feeds like TRD-LA / Registry are inherently in-footprint).
+const WEST = /\b(Los Angeles|SoCal|Southern California|California|\bL\.?A\.?\b|San Diego|Orange County|Inland Empire|Ventura|San Francisco|Bay Area|Silicon Valley|San Jose|Oakland|Sacramento|NorCal|Seattle|Portland|Phoenix|Scottsdale|Las Vegas|Reno|Denver|Texas|Austin|Dallas|Houston|Nevada|Arizona|Oregon|Washington|Colorado)\b/i;
+const SALE = /\b(sells?|sold|buys?|acquires?|acquired|purchase[sd]?|pays?|trades?|snaps up|picks up|closes? on|nabs?|grabs?)\b/i;
+const LOAN = /\b(loan|refinanc|financing|mortgage|CMBS|debt)\b/i;
+
+const decode = s => (s || '').replace(/<!\[CDATA\[|\]\]>/g, '').replace(/&amp;/g, '&').replace(/&#8217;|&#039;|&rsquo;/g, "'").replace(/&#8211;|&#8212;/g, '-').replace(/&quot;/g, '"').replace(/&#\d+;/g, ' ').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
+const tag = (block, t) => { const m = block.match(new RegExp(`<${t}[^>]*>([\\s\\S]*?)</${t}>`, 'i')); return m ? decode(m[1]) : ''; };
+
+function priceOf(text) {
+  const m = text.match(/\$\s?([\d]+(?:\.\d+)?)\s*(billion|million|B|M|K|thousand)\b/i);
+  if (!m) return { price: null, label: null };
+  const unit = m[2].toLowerCase();
+  const mult = unit.startsWith('b') ? 1e9 : unit.startsWith('m') ? 1e6 : (unit === 'k' || unit === 'thousand') ? 1e3 : 1;
+  return { price: Math.round(parseFloat(m[1]) * mult), label: `$${m[1]}${unit[0].toUpperCase()}` };
+}
+function locOf(text) { const m = text.match(WEST); return m ? m[0] : null; }
+function typeOf(text) { return SALE.test(text) ? 'sale' : LOAN.test(text) ? 'loan' : 'other'; }
+
+async function pull(feed) {
+  try {
+    const xml = await (await fetch(feed.url, { headers: { 'User-Agent': UA }, signal: AbortSignal.timeout(12000) })).text();
+    const items = xml.split(/<item[\s>]/).slice(1).map(b => '<item ' + b.split('</item>')[0] + '</item>');
+    return items.map(b => {
+      const title = tag(b, 'title');
+      let link = tag(b, 'link'); if (!link) link = (b.match(/<link[^>]*>([^<]+)</i) || [])[1] || tag(b, 'guid');
+      const desc = tag(b, 'description');
+      const date = tag(b, 'pubDate');
+      const text = `${title} ${desc}`;
+      return { feed: feed.name, feed_region: feed.region, title, link: (link || '').trim(), date, ...priceOf(text), location: locOf(text), type: typeOf(text) };
+    });
+  } catch (e) { console.error(`  ${feed.name} feed error: ${e.message}`); return []; }
+}
+
+const all = (await Promise.all(FEEDS.map(pull))).flat().filter(x => x.title && x.link);
+// closed-deal filter: SALE type + a $ amount + in-footprint (regional feeds count as in-footprint)
+const inWest = x => x.location || x.feed_region === 'LA' || x.feed_region === 'NorCal';
+// exclude non-deal headlines that happen to carry a $ figure (personnel, rankings, op-eds).
+const NOT_DEAL = /\b(to head|to lead|hires?|names?|appoints?|joins?|promotes?|taps|steps down|resigns?|obituary|op-ed|opinion|ranking|list of|top \d+|named to|to oversee)\b/i;
+const dealItems = all.filter(x => x.type === 'sale' && x.price && inWest(x) && !NOT_DEAL.test(x.title));
+
+const seen = existsSync(SEEN) ? new Set(JSON.parse(readFileSync(SEEN, 'utf8'))) : new Set();
+const fresh = dealItems.filter(x => !seen.has(x.link));
+fresh.sort((a, b) => (b.price || 0) - (a.price || 0));
+
+console.log(`CRE-news scan: ${all.length} items across ${FEEDS.length} feeds -> ${dealItems.length} closed-deal (sale+price+west), ${fresh.length} NEW.`);
+for (const d of fresh.slice(0, 15)) console.log(`  ${d.label.padStart(7)}  ${(d.location||d.feed_region).padEnd(14)}  ${d.title.slice(0,60)}  [${d.feed}]`);
+
+if (!args.includes('--dry')) {
+  all.forEach(x => seen.add(x.link));   // mark everything seen so non-deal items don't re-scan
+  writeFileSync(SEEN, JSON.stringify([...seen], null, 0));
+  if (fresh.length) {
+    const stamp = new Date().toISOString().slice(0, 10);
+    writeFileSync(join(ROOT, 'out', `crenews-new-deals-${stamp}.json`), JSON.stringify(fresh, null, 2));
+    console.log(`-> out/crenews-new-deals-${stamp}.json (${fresh.length} deals) for the alert feed.`);
+  }
+}
diff --git a/scripts/dtt-cron.sh b/scripts/dtt-cron.sh
index c30d0b6..66e6c5f 100755
--- a/scripts/dtt-cron.sh
+++ b/scripts/dtt-cron.sh
@@ -1,12 +1,13 @@
 #!/bin/bash
-# TK-10708  Daily "closed-deal" jobs (launchd com.steve.dtt-import).
-# Baked PATH so bare node/psql resolve under launchd's minimal env (bare-node lesson).
+# TK-10708  Daily "be first to a closed deal" jobs (launchd com.steve.dtt-import).
 export PATH="/opt/homebrew/bin:/opt/homebrew/opt/postgresql@14/bin:/usr/bin:/bin:/usr/sbin:/sbin"
 cd "$(dirname "$0")/.." || exit 1
 LOG="data/dtt-cron.log"; mkdir -p data out
 echo "=== $(date '+%Y-%m-%d %H:%M:%S') daily run ===" >> "$LOG"
-# 1) FREE be-first feed: SEC EDGAR CRE acquisition 8-Ks (same-week, $0)
-node scripts/edgar-cre-monitor.mjs --loc "Los Angeles" --days 14 >> "$LOG" 2>&1
-# 2) DTT sales import (free assessor source; frozen until a fresh feed is added)
+# 1) FREE broad same-day feed: CRE trade-press closed deals (private + public)
+node scripts/cre-news-monitor.mjs >> "$LOG" 2>&1
+# 2) FREE early-signal: SEC EDGAR CRE acquisition 8-Ks (biggest public deals)
+node scripts/edgar-cre-monitor.mjs --days 14 >> "$LOG" 2>&1
+# 3) DTT sales import (free assessor source; frozen until a fresh feed is added)
 node scripts/ingest-la-sales.mjs >> "$LOG" 2>&1
 echo "--- exit $? ---" >> "$LOG"

← dda8f01 EDGAR monitor: West-footprint sweep + filing enrichment (pro  ·  back to Re Flyer Aggregator  ·  morning email digest: build-digest.mjs + email-digest.sh sen 01cc8e0 →