[object Object]

← back to Rentv 2026

Add real CRE deal listings: parse rentv.com transaction stories into structured deal cards (/deals page + /api/deals + pull-deals.mjs)

49078ccf77f44173eb66dbf55de1d8a93c8ec106 · 2026-07-22 21:10:28 -0700 · Steve Abrams

Files touched

Diff

commit 49078ccf77f44173eb66dbf55de1d8a93c8ec106
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 22 21:10:28 2026 -0700

    Add real CRE deal listings: parse rentv.com transaction stories into structured deal cards (/deals page + /api/deals + pull-deals.mjs)
---
 data/deals.json        | 577 +++++++++++++++++++++++++++++++++++++++++++++++++
 data/markets.json      |   2 +-
 data/news.json         |   2 +-
 data/ticker.json       |   2 +-
 public/deals.html      | 130 +++++++++++
 public/index.html      |   2 +-
 scripts/pull-deals.mjs | 150 +++++++++++++
 server.js              |  19 ++
 8 files changed, 880 insertions(+), 4 deletions(-)

diff --git a/data/deals.json b/data/deals.json
new file mode 100644
index 00000000..5847be97
--- /dev/null
+++ b/data/deals.json
@@ -0,0 +1,577 @@
+{
+ "source": "rentv.com — live deal listings",
+ "fetched_at": "2026-07-23T04:07:13.798Z",
+ "count": 30,
+ "deals": [
+  {
+   "id": "34238",
+   "title": "Gilbert, AZ Multifamily Community Refinanced with New Loan for $51.8 Mil",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34238",
+   "image": "https://www.rentv.com/images/dynamic/news_20296_4353EElliotRdGilbertAZDarcieGiacchettoJul212026.jpg",
+   "txn_type": "Financing",
+   "property_type": "Multifamily",
+   "city": "Gilbert",
+   "state": "AZ",
+   "amount": 51800000,
+   "amount_label": "$51.8M",
+   "size_label": "236 units · 986 SF",
+   "units": 236,
+   "sqft": 986,
+   "address": "4353 E. Elliot Rd.",
+   "occupancy_pct": 97,
+   "year_built": 2025,
+   "summary": "A joint venture between Phoenix Capital Management and P. B."
+  },
+  {
+   "id": "34236",
+   "title": "The Swig Company Obtains Refi on Office High-Rise in San Francisco’s Financial District",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34236",
+   "image": "https://www.rentv.com/images/dynamic/news_20294_220MontgomeryStSFNancyAmaralA2N2Jul162026.jpg",
+   "txn_type": "Financing",
+   "property_type": "Office",
+   "city": "San Francisco",
+   "state": null,
+   "amount": 95000000,
+   "amount_label": "$95.0M",
+   "size_label": null,
+   "units": null,
+   "sqft": null,
+   "address": "220 Montgomery St",
+   "occupancy_pct": null,
+   "year_built": 1891,
+   "summary": "RENTV. com - Commercial real estate news and property information window."
+  },
+  {
+   "id": "34233",
+   "title": "Tailwind Investment Group Spends $65 Mil on Two Sacramento Res Properties",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34233",
+   "image": "https://www.rentv.com/images/dynamic/news_20291_6633ValleyHiDrSacramentoDavidHulmeTailwindJul182026.jpg",
+   "txn_type": "Sale",
+   "property_type": "Multifamily",
+   "city": "Sacramento",
+   "state": null,
+   "amount": 65000000,
+   "amount_label": "$65.0M",
+   "size_label": "419 units",
+   "units": 419,
+   "sqft": null,
+   "address": "4350 Galbrath Dr",
+   "occupancy_pct": 94,
+   "year_built": null,
+   "summary": "Tailwind Investment Group has acquired two multifamily properties totaling 419 units in Sacramento for a combined price of $65 mil.  The properties include Bent Tree Apartments (208 units) and California Place Apartments (211 units)."
+  },
+  {
+   "id": "34239",
+   "title": "Fresno Multifamily Sale Works Out to $177k per Unit",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34239",
+   "image": "https://www.rentv.com/images/dynamic/news_20297_585WestSierraAveFresnoDaveEbelingJul212026.jpg",
+   "txn_type": "Sale",
+   "property_type": "Multifamily",
+   "city": "Fresno",
+   "state": null,
+   "amount": 177000,
+   "amount_label": "$177,000",
+   "size_label": "248 units · 878 SF",
+   "units": 248,
+   "sqft": 878,
+   "address": null,
+   "occupancy_pct": null,
+   "year_built": null,
+   "summary": "In a Fresno multifamily trade, Maroa Park Apartments, a 248-unit community, was purchased by a private investor for $44 mil ($177. 4k/unit)."
+  },
+  {
+   "id": "34235",
+   "title": "Retail Portfolio in Downtown Long Beach Fetches $30 Mil",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34235",
+   "image": "https://www.rentv.com/images/dynamic/news_20293_MosiacLongBeachAnneMonaghanJul202026.jpg",
+   "txn_type": "Sale",
+   "property_type": "Retail",
+   "city": "Long Beach",
+   "state": null,
+   "amount": 30000000,
+   "amount_label": "$30.0M",
+   "size_label": "4.8 acres",
+   "units": null,
+   "sqft": null,
+   "address": "50 East 4th St",
+   "occupancy_pct": 78,
+   "year_built": null,
+   "summary": "Mosaic, a six building, 148. 4k sf retail portfolio spanning multiple city blocks in Downtown Long Beach, traded hands in a recent $30 mil ($202/sf transaction."
+  },
+  {
+   "id": "34224",
+   "title": "Shorenstein Buys 77k sf Bay Area Office Building",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34224",
+   "image": "https://www.rentv.com/images/dynamic/news_20283_550AllertonRedwoodCityNickTrombolaNWMKJul162026.jpg",
+   "txn_type": "Sale",
+   "property_type": "Office",
+   "city": "Bay Area",
+   "state": null,
+   "amount": null,
+   "amount_label": null,
+   "size_label": null,
+   "units": null,
+   "sqft": null,
+   "address": null,
+   "occupancy_pct": 81,
+   "year_built": null,
+   "summary": "Shorenstein has closed on a Bay Area office buy, acquiring 550 Allerton, a 76. 6k sf, Class A property in the heart of Downtown Redwood City."
+  },
+  {
+   "id": "34226",
+   "title": "Carson Distribution Facility Acquired by WareSpace in a $23 Mil Deal",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34226",
+   "image": "https://www.rentv.com/images/dynamic/news_20285_860SandhillAveCarsonElisabethManvilleSmartJul162026.jpg",
+   "txn_type": "Sale",
+   "property_type": "Industrial",
+   "city": "Phoenix",
+   "state": null,
+   "amount": 23000000,
+   "amount_label": "$23.0M",
+   "size_label": null,
+   "units": null,
+   "sqft": null,
+   "address": "860 Sandhill Ave",
+   "occupancy_pct": null,
+   "year_built": null,
+   "summary": "A 90. 5k sf, industrial distribution facility in the city of Carson traded hands in a $23 mil ($254/sf) transaction."
+  },
+  {
+   "id": "34218",
+   "title": "Camden Property Trust Buys Gilbert, AZ Res Community in $124 Mil Deal",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34218",
+   "image": "https://www.rentv.com/images/dynamic/news_20278_3150ERayRdGilbertKimHoidalCBREJul132026.jpg",
+   "txn_type": "Sale",
+   "property_type": "Multifamily",
+   "city": "Gilbert",
+   "state": "AZ",
+   "amount": 124000000,
+   "amount_label": "$124.0M",
+   "size_label": "320 units",
+   "units": 320,
+   "sqft": null,
+   "address": "3150 E Ray Rd",
+   "occupancy_pct": null,
+   "year_built": null,
+   "summary": "In a major Arizona multifamily trade, Camden Property Trust acquired The Tyler, a 320-unit multifamily community located in the Agritopia master-planned community in Gilbert.  The asset was sold by StreetLights Residential in a deal worth $124."
+  },
+  {
+   "id": "34225",
+   "title": "CentrePoint Properties Spends $30 Mil on Trio of Colorado Retail Assets",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34225",
+   "image": "",
+   "txn_type": "Sale",
+   "property_type": "Retail",
+   "city": "Phoenix",
+   "state": null,
+   "amount": 30000000,
+   "amount_label": "$30.0M",
+   "size_label": null,
+   "units": null,
+   "sqft": null,
+   "address": null,
+   "occupancy_pct": null,
+   "year_built": null,
+   "summary": "CentrePoint Properties has purchased a three-property Colorado retail portfolio, including Cottonwood Square in Parker, Bristol Pointe in Aurora and Foothill Green in Littleton.  The assets were sold by Western Centers Inc in a $30."
+  },
+  {
+   "id": "34227",
+   "title": "MG Properties Buys 188-Unit Portland Res Asset",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34227",
+   "image": "https://www.rentv.com/images/dynamic/news_20286_TupeloAlleyPortlandPaulKaseburgMGPropJul162026.jpg",
+   "txn_type": "Sale",
+   "property_type": "Multifamily",
+   "city": "Portland",
+   "state": null,
+   "amount": null,
+   "amount_label": null,
+   "size_label": "188 units",
+   "units": 188,
+   "sqft": null,
+   "address": "3850 N. Mississippi Ave",
+   "occupancy_pct": null,
+   "year_built": 2009,
+   "summary": "MG Properties has acquired Tupelo Alley, a 188-unit mixed use apartment community in Portland, OR.  Located at 3850 N."
+  },
+  {
+   "id": "34216",
+   "title": "Meta Housing Well Underway with 300-Unit Res Community in Woodland Hills",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34216",
+   "image": "https://www.rentv.com/images/dynamic/news_20277_21300WOxnardStWHillsHollyJenvyJul152026.jpg",
+   "txn_type": "Development",
+   "property_type": "Multifamily",
+   "city": "Phoenix",
+   "state": null,
+   "amount": null,
+   "amount_label": null,
+   "size_label": "300 units · 400 SF",
+   "units": 300,
+   "sqft": 400,
+   "address": "21300 W. Oxnard St",
+   "occupancy_pct": null,
+   "year_built": null,
+   "summary": "Meta Housing, with R. D."
+  },
+  {
+   "id": "34214",
+   "title": "Intercontinental Real Estate Corporation Closes $69 Mil Retail Buy in Washington",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34214",
+   "image": "https://www.rentv.com/images/dynamic/news_20274_1402LakeTappsPkwySEAuburnWABruceBeckJul152026.jpg",
+   "txn_type": "Sale",
+   "property_type": "Retail",
+   "city": "Auburn",
+   "state": "WA",
+   "amount": 69000000,
+   "amount_label": "$69.0M",
+   "size_label": "13,000 SF · 12.61 acres",
+   "units": null,
+   "sqft": 13000,
+   "address": "1402 Lake Tapps Pkwy",
+   "occupancy_pct": 100,
+   "year_built": 2002,
+   "summary": "Intercontinental Real Estate Corporation has acquired Lakeland Town Center, a 125. 2k sf grocery-anchored retail center located in the Seattle/Tacoma suburb of Auburn, WA, for $69."
+  },
+  {
+   "id": "34208",
+   "title": "Eagle Partners and The Vistria Group Spend $132 Mil on Garden Grove Mixed-Income Res Community",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34208",
+   "image": "https://www.rentv.com/images/dynamic/news_20267_12091BayportStGardenGroveCOSTARAnnieNoebelCornersCommJul142026.jpg",
+   "txn_type": "Sale",
+   "property_type": "Multifamily",
+   "city": "Phoenix",
+   "state": null,
+   "amount": 132000000,
+   "amount_label": "$132.0M",
+   "size_label": "402 units",
+   "units": 402,
+   "sqft": null,
+   "address": "12091 Bayport St",
+   "occupancy_pct": null,
+   "year_built": 1968,
+   "summary": "Eagle Partners and The Vistria Group have purchased Crystal View Apartments, a 402-unit apartment mixed-income workforce housing community located in Garden Grove, for $132. 5 mil, or $329."
+  },
+  {
+   "id": "34204",
+   "title": "Oceanside Res Community Trades for $77.5 Mil",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34204",
+   "image": "https://www.rentv.com/images/dynamic/news_20262_3851SherbourneDrOceansideDANKIRKSEYClaudiaLipmannCBREJul142026.jpg",
+   "txn_type": "Sale",
+   "property_type": "Multifamily",
+   "city": "Phoenix",
+   "state": null,
+   "amount": 77500000,
+   "amount_label": "$77.5M",
+   "size_label": "240 units · 14.24 acres",
+   "units": 240,
+   "sqft": null,
+   "address": "3851 Sherbourne Dr",
+   "occupancy_pct": null,
+   "year_built": 1975,
+   "summary": "Sunterra, a 240-unit multifamily community in Oceanside, has traded hands in a $77. 5 mil ($323k/unit) transaction."
+  },
+  {
+   "id": "34210",
+   "title": "BKM Capital Partners Boosts Pac Northwest Holdings with Purchase of 400k sf Industrial Buy in Washington",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34210",
+   "image": "https://www.rentv.com/images/dynamic/news_20270_SouthcenterWestBusinessParkTukwilaWASuleAygorenConnectJul142026.jpg",
+   "txn_type": "Sale",
+   "property_type": "Industrial",
+   "city": "Tukwila",
+   "state": "WA",
+   "amount": 4000000,
+   "amount_label": "$4.0M",
+   "size_label": "21 units · 19 acres",
+   "units": 21,
+   "sqft": null,
+   "address": null,
+   "occupancy_pct": 84,
+   "year_built": null,
+   "summary": "BKM Capital Partners has acquired a five-building Metro Seattle industrial portfolio.  The assets, located in the market’s premier South King County industrial corridor, were acquired on behalf of BKM Industrial Value Fund III."
+  },
+  {
+   "id": "34203",
+   "title": "The Bascom Group Pays Over $53 Mil for OC Res Community",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34203",
+   "image": "https://www.rentv.com/images/dynamic/news_20261_6968CampusDrBuenaParkJerryFinkBascomJul132026.jpg",
+   "txn_type": "Sale",
+   "property_type": "Multifamily",
+   "city": "Phoenix",
+   "state": null,
+   "amount": 53000000,
+   "amount_label": "$53.0M",
+   "size_label": "183 units · 8.7 acres",
+   "units": 183,
+   "sqft": null,
+   "address": "6968 Campus Dr",
+   "occupancy_pct": null,
+   "year_built": 1985,
+   "summary": "The Bascom Group LLC has acquired Castlewood Park Apartments, a low-density, 183-unit value-add apartment community in Buena Park, for $53. 125 mil, or $290k per unit."
+  },
+  {
+   "id": "34199",
+   "title": "Leasing Assignment for 650k sf San Francisco Office Tower Awarded to CBRE",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34199",
+   "image": "https://www.rentv.com/images/dynamic/news_20259_415NatomaSFranciscoIrineSpivakCBREJun162026.jpg",
+   "txn_type": "Lease",
+   "property_type": "Office",
+   "city": "San Francisco",
+   "state": null,
+   "amount": null,
+   "amount_label": null,
+   "size_label": null,
+   "units": null,
+   "sqft": null,
+   "address": null,
+   "occupancy_pct": null,
+   "year_built": null,
+   "summary": "The Meridian Group and Fenway Capital Advisors have awarded the leasing assignment for 415 Natoma, a 25-story, 650k sf Class A office tower in San Francisco’s SoMa neighborhood, to CBRE.  Delivered in 2022 as the trophy office component of the 5M mixed-use development, 415 Natoma is the newest high-rise office tower in"
+  },
+  {
+   "id": "34202",
+   "title": "SummerHill Apartment Communities Breaks Ground on New 251-Unit Bay Area Res Development",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34202",
+   "image": "https://www.rentv.com/images/dynamic/news_20260_11ElcaminoRealSMateoAnneMonaghanJul72026.jpg",
+   "txn_type": "Development",
+   "property_type": "Multifamily",
+   "city": "Bay Area",
+   "state": null,
+   "amount": null,
+   "amount_label": null,
+   "size_label": "251 units · 2.2 acres",
+   "units": 251,
+   "sqft": null,
+   "address": null,
+   "occupancy_pct": null,
+   "year_built": null,
+   "summary": "SummerHill Apartment Communities has broken ground on 11 El Camino Real, a new 251-unit multifamily rental community in San Mateo County.  The development, which includes 38 essential affordable homes at various income levels, brings a contemporary, pedestrian-oriented design to a prominent 2."
+  },
+  {
+   "id": "34196",
+   "title": "Rubicon Point Partners Spends $53 Mil on Silicon Valley Medical/Office Property",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34196",
+   "image": "",
+   "txn_type": "Sale",
+   "property_type": "Office",
+   "city": "Silicon Valley",
+   "state": null,
+   "amount": 53000000,
+   "amount_label": "$53.0M",
+   "size_label": null,
+   "units": null,
+   "sqft": null,
+   "address": null,
+   "occupancy_pct": 93,
+   "year_built": null,
+   "summary": "In a recent Bay Area office trade, Rubicon Point Partners purchased Wolfe Square, a 118k sf Class A asset in Cupertino, for $53 mil ($449/sf).  The two-building, multi-tenant office/medical property is located at 10050 and 10080 N."
+  },
+  {
+   "id": "34195",
+   "title": "Borstein Enterprises Picks Up 282k sf OC Industrial Asset",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34195",
+   "image": "https://www.rentv.com/images/dynamic/news_20256_15339BarrancaPkwyIrvineSavannahDurbinCWJul62026.jpg",
+   "txn_type": "Sale",
+   "property_type": "Industrial",
+   "city": "Phoenix",
+   "state": null,
+   "amount": null,
+   "amount_label": null,
+   "size_label": "16.2 acres",
+   "units": null,
+   "sqft": null,
+   "address": null,
+   "occupancy_pct": null,
+   "year_built": null,
+   "summary": "Borstein Enterprises has acquired Barranca Business Park, a 13-building industrial business park totaling 282k sf of space on 16. 2 acres of land in Irvine."
+  },
+  {
+   "id": "34188",
+   "title": "Chino Distribution Facility Leased by Auto Parts Distributor in $42 Mil Deal",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34188",
+   "image": "https://www.rentv.com/images/dynamic/news_20252_16380EuclidAveChinoElisabethManvilleSmartJul22026.jpg",
+   "txn_type": "Lease",
+   "property_type": "Industrial",
+   "city": "Phoenix",
+   "state": null,
+   "amount": 42000000,
+   "amount_label": "$42.0M",
+   "size_label": null,
+   "units": null,
+   "sqft": null,
+   "address": "16380 Euclid Ave",
+   "occupancy_pct": null,
+   "year_built": null,
+   "summary": "An auto parts distribution company, believed to be Titanium Plus Auto Parts, has inked a lease for a 522. 3k sf distribution facility in Chino for $42 mil."
+  },
+  {
+   "id": "34186",
+   "title": "Santa Clara Industrial Property Trades for $25.75 Mil",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34186",
+   "image": "https://www.rentv.com/images/dynamic/news_20250_651MartinAveSClaraGraceLewisJLLJul12026.jpg",
+   "txn_type": "Sale",
+   "property_type": "Industrial",
+   "city": "Phoenix",
+   "state": null,
+   "amount": 25750000,
+   "amount_label": "$25.8M",
+   "size_label": "7.17 acres",
+   "units": null,
+   "sqft": null,
+   "address": "651 Martin Ave",
+   "occupancy_pct": null,
+   "year_built": null,
+   "summary": "In a Bay Area development purchase, Lift Partners acquired a 7. 17-acre infill site in Santa Clara for $25."
+  },
+  {
+   "id": "34181",
+   "title": "Redwood West and Panattoni Buy The Pike Destination Center in Long Beach",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34181",
+   "image": "https://www.rentv.com/images/dynamic/news_20245_95SPineAveLongBeachJessicaSpauldingJun302026.jpg",
+   "txn_type": "Sale",
+   "property_type": "Retail",
+   "city": "Long Beach",
+   "state": null,
+   "amount": 20000000,
+   "amount_label": "$20.0M",
+   "size_label": null,
+   "units": null,
+   "sqft": null,
+   "address": "95 S. Pine Ave",
+   "occupancy_pct": 91,
+   "year_built": 2003,
+   "summary": "Redwood West, in partnership with Panattoni, has acquired The Pike, a 394. 6k sf waterfront retail, dining, and entertainment lifestyle center in Long Beach."
+  },
+  {
+   "id": "34180",
+   "title": "Kennedy Wilson and Jamison Form Partnership to Develop 4,000 Units of Affordable Housing in Los Angeles",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34180",
+   "image": "https://www.rentv.com/images/dynamic/news_20244_350SHopeBruceBeckJune302026.jpg",
+   "txn_type": "Development",
+   "property_type": "Office",
+   "city": "Los Angeles",
+   "state": null,
+   "amount": null,
+   "amount_label": null,
+   "size_label": "4,000 units",
+   "units": 4000,
+   "sqft": null,
+   "address": "350 S. Figueroa St",
+   "occupancy_pct": null,
+   "year_built": null,
+   "summary": "Kennedy Wilson and L. A."
+  },
+  {
+   "id": "34086",
+   "title": "Oak Park Investments and Arise Investments Acquire Westlake Village Warehouse/Office Building in $19 Mil Transaction",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34086",
+   "image": "https://www.rentv.com/images/dynamic/news_20161_5785LinderoCanyonRdWstlkVillageDannySoroudiAriseJune22026.jpg",
+   "txn_type": "Sale",
+   "property_type": "Office",
+   "city": "Phoenix",
+   "state": null,
+   "amount": 19000000,
+   "amount_label": "$19.0M",
+   "size_label": null,
+   "units": null,
+   "sqft": null,
+   "address": null,
+   "occupancy_pct": null,
+   "year_built": null,
+   "summary": "Oak Park Investments and Arise Investments, in partnership with Nelnet Real Estate Ventures, have acquired a 98. 1k sf warehouse facility in Westlake Village for $19."
+  },
+  {
+   "id": "34177",
+   "title": "Holland Partner Group Closes $105 Mil Bay Area Res Buy",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34177",
+   "image": "https://www.rentv.com/images/dynamic/news_20242_1432WestSanCarlosSt.jpg",
+   "txn_type": "Sale",
+   "property_type": "Multifamily",
+   "city": "Bay Area",
+   "state": null,
+   "amount": 105000000,
+   "amount_label": "$105.0M",
+   "size_label": "218 units",
+   "units": 218,
+   "sqft": null,
+   "address": "1432 West San Carlos St",
+   "occupancy_pct": null,
+   "year_built": 2015,
+   "summary": "In a significant NoCal res trade, Holland Partner Group has purchased Meridian at Midtown, a 218-unit luxury apartment complex in San Jose.  The asset was sold by Essex Property Trust in a deal valued at around $105 mil ($482k/unit)."
+  },
+  {
+   "id": "34172",
+   "title": "Haven Housing buys 264-Unit Res Community in Austin Metro Area",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34172",
+   "image": "https://www.rentv.com/images/dynamic/news_20237_15001StrathavenPassPflugervilleTXElliottGolanConnectJun262026.jpg",
+   "txn_type": "Sale",
+   "property_type": "Multifamily",
+   "city": "Austin",
+   "state": null,
+   "amount": null,
+   "amount_label": null,
+   "size_label": "264 units",
+   "units": 264,
+   "sqft": null,
+   "address": null,
+   "occupancy_pct": null,
+   "year_built": 2018,
+   "summary": "Haven Housing has acquired Wells Park Apartments, a 264-unit garden-style apartment community in Pflugerville, TX, a growing suburb of Austin.  Haven Housing is the affordable and multifamily housing platform of BH Properties."
+  },
+  {
+   "id": "34174",
+   "title": "MG Properties Acquires Two Multifamily Properties in Gresham, OR in Deals Totaling $60.8 Mil",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34174",
+   "image": "https://www.rentv.com/images/dynamic/news_20239_ParksideApartmentsGreshamORPaulKasebergMGPropJune262026.jpg",
+   "txn_type": "Sale",
+   "property_type": "Multifamily",
+   "city": "Gresham",
+   "state": "OR",
+   "amount": 60800000,
+   "amount_label": "$60.8M",
+   "size_label": "337 units · 32 acres",
+   "units": 337,
+   "sqft": null,
+   "address": null,
+   "occupancy_pct": null,
+   "year_built": null,
+   "summary": "San Diego-based MG Properties purchased two multifamily communities totaling 337 units in Gresham, OR in deals worth just over $60 mil.  The acquisitions were of Mountain High Apartments for $18."
+  },
+  {
+   "id": "34171",
+   "title": "Leading AI Firm Leases 1.1 msf of Space at Luke Field in Glendale, AZ",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34171",
+   "image": "https://www.rentv.com/images/dynamic/news_20236_7733LitchfieldRdGlendaleAZStaceyHershauerJun242026.jpg",
+   "txn_type": "Lease",
+   "property_type": "Industrial",
+   "city": "Glendale",
+   "state": "AZ",
+   "amount": 152000000,
+   "amount_label": "$152.0M",
+   "size_label": "140 acres",
+   "units": null,
+   "sqft": null,
+   "address": "13543 A and 13543 B Northern Ave",
+   "occupancy_pct": 100,
+   "year_built": null,
+   "summary": "There have been two full-building leases to report at Luke Field, a 2. 4 msf development from Lincoln Property Company in Glendale, AZ."
+  },
+  {
+   "id": "34169",
+   "title": "Seattle Area Res Community Refinanced with $82.5 Mil Loan from Mesa West Capital",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34169",
+   "image": "https://www.rentv.com/images/dynamic/news_20233_OlinFieldsEverettWABruceBeckJun242026.jpg",
+   "txn_type": "Financing",
+   "property_type": "Multifamily",
+   "city": "Seattle",
+   "state": null,
+   "amount": 82500000,
+   "amount_label": "$82.5M",
+   "size_label": "352 units · 16 acres",
+   "units": 352,
+   "sqft": null,
+   "address": null,
+   "occupancy_pct": null,
+   "year_built": null,
+   "summary": "A Harrison Street Asset Management and Security Properties joint venture has secured a refi loan of $82. 5 mil on Olin Fields, a 352-unit, garden-style apartment community in the Seattle suburb of Everett, WA."
+  }
+ ]
+}
\ No newline at end of file
diff --git a/data/markets.json b/data/markets.json
index eaf70a45..79183fe0 100644
--- a/data/markets.json
+++ b/data/markets.json
@@ -1,5 +1,5 @@
 {
-  "fetched_at": "2026-07-23T03:45:03.643Z",
+  "fetched_at": "2026-07-23T04:05:04.724Z",
   "source": "Yahoo Finance — live (CBOE Treasury yields, Dow Jones/MSCI REIT indices)",
   "curve": [
     {
diff --git a/data/news.json b/data/news.json
index a28e970d..77ae501b 100644
--- a/data/news.json
+++ b/data/news.json
@@ -1,6 +1,6 @@
 {
  "source": "rentv.com — live",
- "fetched_at": "2026-07-23T03:45:03.162Z",
+ "fetched_at": "2026-07-23T04:05:04.370Z",
  "count": 30,
  "items": [
   {
diff --git a/data/ticker.json b/data/ticker.json
index 94628011..0b98cd9a 100644
--- a/data/ticker.json
+++ b/data/ticker.json
@@ -1,5 +1,5 @@
 {
-  "fetched_at": "2026-07-23T03:45:03.616Z",
+  "fetched_at": "2026-07-23T04:05:04.697Z",
   "la_office_vac": {
     "label": "LA OFFICE VAC",
     "value": "23.1%",
diff --git a/public/deals.html b/public/deals.html
new file mode 100644
index 00000000..b1369fac
--- /dev/null
+++ b/public/deals.html
@@ -0,0 +1,130 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>CRE Deal Listings — RENTV.com</title>
+<meta name="description" content="Live commercial real estate deal listings — sales, leases, financings and developments across the Western U.S., parsed from RENTV.com transaction reporting.">
+<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet">
+<style>
+  :root{--red:#c8102e;--ink:#14171a;--sub:#5b636b;--line:#e4e7ea;--bg:#fff;--wash:#f5f6f8;--sans:'Inter',system-ui,sans-serif;--serif:'Playfair Display',Georgia,serif}
+  *{box-sizing:border-box;margin:0;padding:0}
+  body{font-family:var(--sans);color:var(--ink);background:var(--wash);line-height:1.45}
+  a{color:inherit;text-decoration:none}
+  .wrap{max-width:1200px;margin:0 auto;padding:0 20px}
+  .util{background:var(--ink);color:#cfd4d9;font-size:12px}
+  .util .wrap{display:flex;justify-content:space-between;align-items:center;height:34px}
+  .util .live{display:inline-flex;align-items:center;gap:7px;letter-spacing:.05em;text-transform:uppercase;font-size:11px}
+  .util .dot{width:7px;height:7px;border-radius:50%;background:#22c55e;animation:pulse 2s infinite}
+  @keyframes pulse{0%{box-shadow:0 0 0 0 rgba(34,197,94,.6)}70%{box-shadow:0 0 0 7px rgba(34,197,94,0)}100%{box-shadow:0 0 0 0 rgba(34,197,94,0)}}
+  .util .acts a{margin-left:14px;color:#cfd4d9}.util .acts a.sub{color:#fff;background:var(--red);padding:4px 12px;border-radius:3px;font-weight:700}
+  .mast{background:#fff;border-bottom:2px solid var(--ink);padding:16px 0 12px}
+  .mast .wrap{display:flex;align-items:center;justify-content:space-between}
+  .logo{font-family:var(--serif);font-weight:700;font-size:32px;letter-spacing:-1px}.logo span{color:var(--red)}
+  .logo small{display:block;font-size:11px;font-weight:600;letter-spacing:3px;color:var(--sub);text-transform:uppercase;margin-top:2px;font-family:var(--sans)}
+  nav{background:#fff;border-bottom:1px solid var(--line)}
+  nav .wrap{display:flex;gap:2px;align-items:center;height:46px;overflow-x:auto}
+  nav a{font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:13px 8px;border-bottom:3px solid transparent;white-space:nowrap}
+  nav a:hover,nav a.on{color:var(--red);border-color:var(--red)}
+  .head{padding:30px 0 6px}
+  .head h1{font-family:var(--serif);font-size:34px;letter-spacing:-.5px}
+  .head p{color:var(--sub);font-size:14px;margin-top:4px;max-width:820px}
+  .head .fresh{font-size:12px;color:var(--sub);margin-top:8px}
+  .controls{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:18px 0 6px}
+  .pill{font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:7px 13px;border:1px solid var(--line);border-radius:999px;background:#fff;cursor:pointer;color:var(--sub)}
+  .pill.on{background:var(--ink);color:#fff;border-color:var(--ink)}
+  .controls input{margin-left:auto;font-family:var(--sans);font-size:13px;padding:8px 12px;border:1px solid var(--line);border-radius:6px;min-width:200px}
+  .count{font-size:12px;color:var(--sub);margin:6px 0 16px}
+  .grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}
+  .card{background:#fff;border:1px solid var(--line);border-radius:10px;overflow:hidden;display:flex;flex-direction:column;transition:box-shadow .15s,transform .15s}
+  .card:hover{box-shadow:0 8px 24px rgba(0,0,0,.09);transform:translateY(-2px)}
+  .card .ph{aspect-ratio:16/9;background:var(--wash) center/cover no-repeat;position:relative}
+  .card .badges{position:absolute;top:10px;left:10px;display:flex;gap:6px}
+  .badge{font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;padding:4px 8px;border-radius:4px;color:#fff;background:var(--ink)}
+  .badge.txn{background:var(--red)}
+  .card .amt{position:absolute;bottom:10px;right:10px;background:rgba(20,23,26,.92);color:#fff;font-family:var(--serif);font-weight:700;font-size:18px;padding:4px 10px;border-radius:5px}
+  .card .body{padding:14px 16px 16px;display:flex;flex-direction:column;gap:8px;flex:1}
+  .card h3{font-family:var(--serif);font-size:17px;line-height:1.25}
+  .card .meta{font-size:12px;color:var(--sub);font-weight:600;display:flex;flex-wrap:wrap;gap:4px 10px}
+  .card .meta b{color:var(--ink)}
+  .card .sum{font-size:13px;color:var(--sub);line-height:1.5}
+  .card .more{margin-top:auto;font-size:12px;font-weight:700;color:var(--red);text-transform:uppercase;letter-spacing:.04em}
+  .empty{background:#fff;border:1px dashed var(--line);border-radius:10px;padding:40px;text-align:center;color:var(--sub)}
+  footer{background:var(--ink);color:#8b939b;margin-top:40px;padding:26px 0;font-size:12px}
+  footer .wrap{display:flex;justify-content:space-between;flex-wrap:wrap;gap:10px}
+  @media(max-width:900px){.grid{grid-template-columns:1fr 1fr}}
+  @media(max-width:600px){.grid{grid-template-columns:1fr}.controls input{margin-left:0;width:100%}}
+</style>
+</head>
+<body>
+  <div class="util"><div class="wrap">
+    <span class="live"><span class="dot"></span> Live CRE deal flow</span>
+    <div class="acts"><a href="/subscribe">Newsletters</a><a href="/advertise">Advertise</a><a class="sub" href="/subscribe">Subscribe</a></div>
+  </div></div>
+  <header class="mast"><div class="wrap">
+    <a class="logo" href="/">REN<span>TV</span>.com<small>Commercial Real Estate News</small></a>
+  </div></header>
+  <nav><div class="wrap">
+    <a href="/">Home</a><a href="/deals" class="on">Deals</a><a href="/sales">Sales</a><a href="/leases">Leases</a><a href="/financing">Financing</a><a href="/development">Development</a><a href="/markets">Markets</a><a href="/la-commercial">LA Commercial</a><a href="/desk">Broker &amp; Owner Desk</a><a href="/conferences">Conferences</a><a href="/blog">Blog</a>
+  </div></nav>
+  <main class="wrap">
+    <div class="head">
+      <h1>CRE Deal Listings</h1>
+      <p>Every commercial real estate transaction RENTV.com is reporting right now — sales, leases, financings and developments across the Western U.S. — parsed into structured listings with price, property type, size and location. Each card links to the full story.</p>
+      <div class="fresh" id="fresh">Loading…</div>
+    </div>
+    <div class="controls" id="txns"></div>
+    <div class="controls" id="types"></div>
+    <div class="controls"><input id="q" type="search" placeholder="Search city, property, party…" autocomplete="off"></div>
+    <div class="count" id="count"></div>
+    <div class="grid" id="grid"></div>
+  </main>
+  <footer><div class="wrap"><span>© RENTV.com — Commercial Real Estate News</span><span>Deal listings parsed from live RENTV.com reporting, refreshed every 20 min</span></div></footer>
+<script>
+const $=s=>document.querySelector(s);
+let ALL=[], fTxn='', fType='', fQ='';
+const TXNS=['Sale','Lease','Financing','Development'];
+const TYPES=['Multifamily','Office','Industrial','Retail','Hospitality','Mixed-Use','Land'];
+function esc(s){return (s||'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));}
+function pills(host,list,cur,pick){
+  host.innerHTML=`<button class="pill${cur===''?' on':''}" data-v="">All</button>`+
+    list.map(v=>`<button class="pill${cur===v?' on':''}" data-v="${v}">${v}</button>`).join('');
+  host.querySelectorAll('.pill').forEach(b=>b.onclick=()=>{pick(b.dataset.v);});
+}
+function render(){
+  const rows=ALL.filter(d=>
+    (!fTxn||d.txn_type===fTxn)&&(!fType||d.property_type===fType)&&
+    (!fQ||`${d.title} ${d.city} ${d.state} ${d.summary}`.toLowerCase().includes(fQ)));
+  $('#count').textContent=`${rows.length} listing${rows.length===1?'':'s'}${fTxn?' · '+fTxn:''}${fType?' · '+fType:''}`;
+  $('#grid').innerHTML=rows.length?rows.map(d=>{
+    const loc=[d.city,d.state].filter(Boolean).join(', ');
+    const ph=d.image?`background-image:url('${esc(d.image)}')`:'';
+    return `<a class="card" href="${esc(d.url)}" target="_blank" rel="noopener">
+      <div class="ph" style="${ph}">
+        <div class="badges"><span class="badge txn">${esc(d.txn_type)}</span><span class="badge">${esc(d.property_type)}</span></div>
+        ${d.amount_label?`<span class="amt">${esc(d.amount_label)}</span>`:''}
+      </div>
+      <div class="body">
+        <h3>${esc(d.title)}</h3>
+        <div class="meta">${loc?`<span>📍 <b>${esc(loc)}</b></span>`:''}${d.size_label?`<span>📐 ${esc(d.size_label)}</span>`:''}${d.year_built?`<span>🏗 ${d.year_built}</span>`:''}${d.occupancy_pct!=null?`<span>${d.occupancy_pct}% leased</span>`:''}</div>
+        ${d.summary?`<div class="sum">${esc(d.summary)}</div>`:''}
+        <div class="more">Read the full deal →</div>
+      </div></a>`;
+  }).join(''):'<div class="empty">No listings match these filters yet. Clear them or check back — the feed refreshes every 20 minutes.</div>';
+}
+(async()=>{
+  try{
+    const d=await (await fetch('/api/deals')).json();
+    ALL=d.deals||[];
+    $('#fresh').textContent=d.fetched_at?('Deal flow as of '+new Date(d.fetched_at).toLocaleString()+' · source: RENTV.com'):'';
+  }catch(e){ $('#fresh').textContent='Could not load deal listings — reload to retry.'; }
+  const pickTxn=v=>{fTxn=v;pills($('#txns'),TXNS,fTxn,pickTxn);render();};
+  const pickType=v=>{fType=v;pills($('#types'),TYPES,fType,pickType);render();};
+  pills($('#txns'),TXNS,fTxn,pickTxn);
+  pills($('#types'),TYPES,fType,pickType);
+  $('#q').oninput=e=>{fQ=e.target.value.trim().toLowerCase();render();};
+  render();
+})();
+</script>
+</body>
+</html>
diff --git a/public/index.html b/public/index.html
index b1a6521d..5cffc7a5 100644
--- a/public/index.html
+++ b/public/index.html
@@ -96,7 +96,7 @@
   <div class="grp"><div class="gt">Sections</div></div>
   <nav><a href="/sales">Sales</a><a href="/leases">Leases</a><a href="/financing">Financing</a><a href="/development">Development</a><a href="/multifamily">Multifamily</a><a href="/industrial">Industrial</a><a href="/retail">Retail</a></nav>
   <div class="grp"><div class="gt">Intelligence</div></div>
-  <nav><a href="/markets">Markets</a><a href="/la-commercial">LA Commercial</a><a href="/desk">Broker &amp; Owner Desk</a><a href="/review">The REview</a><a href="/cre-talk">CRE Talk</a><a href="/conferences">Conferences</a><a href="/blog">Blog</a></nav>
+  <nav><a href="/deals">Deals</a><a href="/markets">Markets</a><a href="/la-commercial">LA Commercial</a><a href="/desk">Broker &amp; Owner Desk</a><a href="/review">The REview</a><a href="/cre-talk">CRE Talk</a><a href="/conferences">Conferences</a><a href="/blog">Blog</a></nav>
   <div class="grp"><div class="gt">Company</div></div>
   <nav><a href="/about">About</a><a href="/advertise">Advertise</a><a href="/subscribe">Subscribe / Newsletters</a></nav>
   <div class="df">RENTV.com — Commercial Real Estate News across the Western U.S. since 1998.</div>
diff --git a/scripts/pull-deals.mjs b/scripts/pull-deals.mjs
new file mode 100644
index 00000000..d72eced3
--- /dev/null
+++ b/scripts/pull-deals.mjs
@@ -0,0 +1,150 @@
+#!/usr/bin/env node
+// Deal-listings engine for rentv-v1: turns REAL rentv.com transaction stories into
+// structured deal cards (data/deals.json). $0 plain fetch, no bot-wall. Steve-approved
+// source (rentv.com is allowed, 2026-07-22). Fills the previously-empty listings surface.
+//
+// Each rentv.com "news" item is actually a CRE transaction (sale / lease / financing /
+// development). We fetch each story body and parse: $ amount, property type, location,
+// size, occupancy, address, date, and a clean summary. Fields we can't find stay null —
+// never fabricated (honest-empty rule).
+import { writeFileSync, readFileSync, mkdirSync } from 'node:fs';
+import { dirname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+const HERE = dirname(fileURLToPath(import.meta.url));
+const DATA = join(HERE, '..', 'data'); mkdirSync(DATA, { recursive: true });
+
+async function fetchDecoded(url) {
+  const r = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 RENTV-v1' }, signal: AbortSignal.timeout(20000) });
+  const buf = Buffer.from(await r.arrayBuffer());
+  let s = buf.toString('utf8');
+  if ((s.match(/�/g) || []).length > 5) s = buf.toString('latin1');
+  return s;
+}
+const clean = (t) => t.replace(/&amp;/g, '&').replace(/&#39;/g, "'").replace(/&quot;/g, '"')
+  .replace(/&nbsp;|&#160;/g, ' ').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
+
+// $51.8 mil / $1.2 bil / $985,000 → normalized number of dollars
+function parseAmount(text) {
+  let m = text.match(/\$\s?([\d,]+(?:\.\d+)?)\s?(bil|billion|mil|million|k)?/i);
+  if (!m) return { amount: null, amount_label: null };
+  let n = parseFloat(m[1].replace(/,/g, ''));
+  const unit = (m[2] || '').toLowerCase();
+  if (/bil/.test(unit)) n *= 1e9; else if (/mil/.test(unit)) n *= 1e6; else if (unit === 'k') n *= 1e3;
+  const label = n >= 1e9 ? `$${(n / 1e9).toFixed(2)}B` : n >= 1e6 ? `$${(n / 1e6).toFixed(1)}M` : `$${n.toLocaleString()}`;
+  return { amount: Math.round(n), amount_label: label };
+}
+
+// Classify from the TITLE first (it states the deal precisely); only fall back to
+// the body when the title is inconclusive — bodies leak generic words ("loan",
+// "units") that mis-drive the type of an office/retail deal.
+function txnOf(t) {
+  return /refinanc|\brefi\b|obtains? (a )?(new )?loan|senior loan|recap|bridge loan/.test(t) ? 'Financing'
+    : /\bsold\b|sells|\bsale\b|acquir|buys|purchas|trades|closes on|pays|spends|snaps up|picks up|fetches|changes hands|nets \$|lands \$|works out to/.test(t) ? 'Sale'
+    : /leas|tenant|renew|signs? (a|new)|inks? a/.test(t) ? 'Lease'
+    : /break(s)? ground|develop|deliver|top(s|ped) out|complet|construction|to build|unveils?|plan(s|ned) to|underway|rises|proposes?/.test(t) ? 'Development'
+    : null;
+}
+function typeOf(t) {
+  return /office|\bhq\b|high-?rise|tower/.test(t) ? 'Office'
+    : /industrial|warehouse|logistics|distribution|\bflex\b/.test(t) ? 'Industrial'
+    : /retail|shopping|mall|grocery|storefront|strip (center|mall)/.test(t) ? 'Retail'
+    : /hotel|hospitality|resort|motel/.test(t) ? 'Hospitality'
+    : /medical|life science|\blab\b|biotech/.test(t) ? 'Medical/Life Science'
+    : /mixed-?use/.test(t) ? 'Mixed-Use'
+    : /self-?storage/.test(t) ? 'Self-Storage'
+    : /multifamily|apartment|\bres\b|residential communit/.test(t) ? 'Multifamily'
+    : /\bland\b|\bsite\b|\bacres?\b/.test(t) ? 'Land'
+    : null;
+}
+function classify(title, body) {
+  const tl = title.toLowerCase(), bl = body.toLowerCase();
+  const txn = txnOf(tl) || txnOf(bl) || 'Deal';
+  // for type, allow a bare "unit(s)" body signal only as a last resort
+  const type = typeOf(tl) || typeOf(bl) || (/\bunit(s)?\b/.test(bl) ? 'Multifamily' : 'Commercial');
+  return { txn, type };
+}
+
+// "Gilbert, AZ" / "Los Angeles, CA" — first City, ST in the title, else body.
+// Falls back to a bare city (title lead word or "in <City>") when no state is stated.
+const KNOWN_CITY = /\b(Bay Area|Silicon Valley|Inland Empire|Orange County|South Bay|East Bay|Downtown L\.?A\.?|Los Angeles|San Francisco|San Diego|Sacramento|Fresno|Oakland|San Jose|Long Beach|Irvine|Anaheim|Pasadena|Phoenix|Scottsdale|Tucson|Mesa|Las Vegas|Reno|Henderson|Seattle|Portland|Denver|Dallas|Houston|Austin|Miami|Atlanta|Chicago|New York|Boston)\b/;
+function parseLocation(title, body) {
+  // City token is the ONE word right before ", ST", optionally extended left by a
+  // known multi-word prefix (San/Los/Long/…) — this stops the match from swallowing
+  // preceding verbs/entities ("…Trust Buys Gilbert, AZ" → "Gilbert", not "Property Trust").
+  const re = /\b((?:San|Los|Long|Santa|New|Bay|Silicon|Inland|Orange|South|East|West|North|Downtown|Mission|Culver|Sherman)\s)?([A-Z][a-z.\-]{2,}),\s?([A-Z]{2})\b/;
+  // Title is authoritative for place — a body can name several cities (comps,
+  // buyer HQ) and pick the wrong one. Only fall to the body if the title has none.
+  const join = (m) => ((m[1] || '') + m[2]).trim().replace(/[’']s$/, '');
+  const tm = title.match(re);
+  if (tm) return { city: join(tm), state: tm[3] };
+  const tk = title.match(KNOWN_CITY);
+  if (tk) return { city: tk[1], state: null };
+  const bm = body.match(re);
+  if (bm) return { city: join(bm), state: bm[3] };
+  const bk = body.match(KNOWN_CITY);
+  if (bk) return { city: bk[1], state: null };
+  return { city: null, state: null };
+}
+
+function parseSize(text) {
+  const units = text.match(/([\d,]+)[- ]unit/i);
+  const sf = text.match(/([\d,]+)\s?(?:sf|sq\.?\s?ft|square[ -]feet|square[ -]foot)/i);
+  const acres = text.match(/([\d,.]+)[- ]acre/i);
+  const parts = [];
+  if (units) parts.push(`${units[1]} units`);
+  if (sf) parts.push(`${sf[1]} SF`);
+  if (acres) parts.push(`${acres[1]} acres`);
+  return { size_label: parts.join(' · ') || null,
+    units: units ? +units[1].replace(/,/g, '') : null,
+    sqft: sf ? +sf[1].replace(/,/g, '') : null };
+}
+
+function parseAddress(body) {
+  const m = body.match(/(?:located at|situated at|address is|at)\s+(\d{2,6}\s[A-Z][A-Za-z0-9.\- ]+?(?:Rd|Road|St|Street|Ave|Avenue|Blvd|Boulevard|Dr|Drive|Way|Ln|Lane|Pl|Place|Ct|Court|Pkwy|Parkway|Hwy|Highway)\.?)/);
+  return m ? m[1].trim() : null;
+}
+function parseOccupancy(body) { const m = body.match(/(\d{1,3})%\s?(?:leased|occup)/i); return m ? +m[1] : null; }
+function parseYearBuilt(body) { const m = body.match(/built in (\d{4})/i); return m ? +m[1] : null; }
+
+// two-sentence summary from the body (drop the leading title echo + date stamp)
+function summarize(title, body) {
+  let b = body;
+  const ti = b.indexOf(title);
+  if (ti >= 0) b = b.slice(ti + title.length);
+  b = b.replace(/^\s*\d{1,2}\/\d{1,2}\/\d{2,4}\s*/, '').trim();
+  const sentences = b.match(/[^.!?]+[.!?]+/g) || [b];
+  return sentences.slice(0, 2).join(' ').trim().slice(0, 320);
+}
+
+try {
+  const news = JSON.parse(readFileSync(join(DATA, 'news.json'), 'utf8'));
+  const items = news.items || [];
+  const deals = [];
+  for (const it of items) {
+    let body = '';
+    try { body = clean(await fetchDecoded(it.url)); } catch { /* skip body, keep headline */ }
+    const hay = it.title + ' ' + body;
+    const { amount, amount_label } = parseAmount(it.title.match(/\$/) ? it.title : hay);
+    const { txn, type } = classify(it.title, body);
+    const loc = parseLocation(it.title, body);
+    const size = parseSize(hay);
+    deals.push({
+      id: it.id, title: it.title, url: it.url, image: it.image || '',
+      txn_type: txn, property_type: type,
+      city: loc.city, state: loc.state,
+      amount, amount_label,
+      size_label: size.size_label, units: size.units, sqft: size.sqft,
+      address: parseAddress(body),
+      occupancy_pct: parseOccupancy(body),
+      year_built: parseYearBuilt(body),
+      summary: summarize(it.title, body) || null,
+    });
+  }
+  const payload = { source: 'rentv.com — live deal listings', fetched_at: new Date().toISOString(), count: deals.length, deals };
+  writeFileSync(join(DATA, 'deals.json'), JSON.stringify(payload, null, 1));
+  const withAmt = deals.filter((d) => d.amount).length;
+  console.log(`OK: parsed ${deals.length} deal listings (${withAmt} with $ amount)`);
+} catch (e) {
+  console.error('deals pull failed:', e.message, '— last-good deals.json kept');
+  process.exitCode = 1;
+}
diff --git a/server.js b/server.js
index 1507d81f..db9359d3 100644
--- a/server.js
+++ b/server.js
@@ -65,6 +65,24 @@ app.get('/api/la-commercial', (_q, r) => {
   r.json(readJSON('la-commercial.json', { marquee: [], recent: [], firms: [] }));
 });
 
+// ── Deals: structured CRE transaction listings parsed from LIVE rentv.com stories
+//    (refreshed by scripts/pull-deals.mjs cron). The real-listings surface. Supports
+//    ?type=, ?txn=, ?q= filters + ?limit so the UI can slice by property/deal type. ──
+app.get('/api/deals', (req, r) => {
+  r.set('Cache-Control', 'public, max-age=300');
+  const data = readJSON('deals.json', { deals: [] });
+  let deals = Array.isArray(data.deals) ? data.deals : [];
+  const { type, txn, q } = req.query;
+  if (type) deals = deals.filter((d) => (d.property_type || '').toLowerCase() === String(type).toLowerCase());
+  if (txn) deals = deals.filter((d) => (d.txn_type || '').toLowerCase() === String(txn).toLowerCase());
+  if (q) {
+    const needle = String(q).toLowerCase();
+    deals = deals.filter((d) => `${d.title} ${d.city} ${d.state} ${d.summary}`.toLowerCase().includes(needle));
+  }
+  const limit = Math.min(parseInt(req.query.limit, 10) || deals.length, 200);
+  r.json({ source: data.source, fetched_at: data.fetched_at, count: deals.length, deals: deals.slice(0, limit) });
+});
+
 // ── Newsletter capture: append-only local JSONL (NO external send-to-list). ──
 const SUBS = path.join(DATA, 'subscribers.jsonl');
 const MAX_SUBS = 50000; // hard ceiling — guards against disk-fill / append abuse
@@ -212,6 +230,7 @@ app.get('/api/article/:id', async (req, res) => {
 //    live feed, one consistent model site-wide; no thin static section pages). ──
 const SECTIONS = ['financing', 'leases', 'development', 'retail', 'multifamily', 'industrial', 'sales'];
 SECTIONS.forEach(s => app.get('/' + s, (_q, r) => r.redirect(301, '/?cat=' + s.charAt(0).toUpperCase() + s.slice(1))));
+app.get('/deals', (_q, r) => r.sendFile(path.join(PUB, 'deals.html')));
 app.get('/news/:id', (_q, r) => r.sendFile(path.join(PUB, 'article.html')));
 // The REview — the folded-in video platform, served natively in the RENTV shell
 app.get('/review', (_q, r) => r.sendFile(path.join(PUB, 'review.html')));

← 4a206b7a auto-save: 2026-07-22T20:46:37 (3 files) — data/markets.json  ·  back to Rentv 2026  ·  chore: refactor (drop dead decode branch) + v0.6.0 (deals-li 16ef70d0 →