I'll start with the deal intake. Fill in what you have. The required fields are marked, and anything you leave blank I'll resolve from data or bring back to you for confirmation before I write it to the model.
Suggest an offering price for Golden Nugget based on my return hurdles. Evaluate the risks in this transaction, including room for rent growth and the tenancy.
Goal: select multifamily RENT comps for the subject so they can be written into an underwriting workbook (Rent Comps tab, 8 comp columns). The result feeds an investment-committee view of whether the subject's rents have room to grow.
Subject: Golden Nugget Apartments, 291 W Belleview Ave, Englewood, CO 80110. RealAI property_mfr id = 59b6289028200764db02b635724279d2. Latitude 39.62497383356103, longitude -104.99101102352144. 206 units, 123,806 rentable SF (avg 601 SF, mostly 1BR with some 2BR), built 1976, 3-story LOW_RISE garden-style, frame/brick, market-rate. Subject in-place avg rent $1,117.06, asking avg $1,172.71, occupancy 95.15%. county_id 08005 (Arapahoe), submarket_id d16015f307a9f41908e3caf0eeb77ed5, zipcode_id 80110, market_id 19740 (Denver).
Method: load the rental-comps skill with sandbox_skills and follow it (progressively wider geographies, filter/rank by proximity, household restrictions (market-rate only), unit-type overlap, size, building type, vintage). Note: the live in-place rent field is mf_rent_and_occupancy_snapshot.in_place_rent_latest_avg (the skill text may say in_place_rent_latest; that field does not exist). Per-bed fields: in_place_rent_latest_0_bed ... in_place_rent_latest_4_bed. Occupancy: occupancy_latest (decimal). Physical attributes in topic mf_property_attributes: unit_count, total_rentable_sqft, year_built, year_renovated, latitude, longitude, building_style, rent_type. Exclude the subject itself. Cast all numbers to float/int. Need at least 3, target 8 comps, each with a non-null in_place_rent_latest_avg and occupancy_latest in (0,1].
Compute haversine distance in miles from the subject in a sandbox python script.
Output: save subagents/rent_comps/rent_comps.json — a list ordered by rank (best first), up to 8 entries, each with keys: rank, property_mfr_id, name, address, city_state_zip (format "City, ST 12345" from address_city, address_state, address_zip), distance_mi (2 decimals), unit_count, total_rentable_sqft, year_built (int or null), year_renovated (int or null), in_place_rent_avg, in_place_0bed, in_place_1bed, in_place_2bed, in_place_3bed, in_place_4bed (null if missing), occupancy_latest (decimal), asking_rent_avg, building_style. Also in your report give a short table of the comps and the comp in-place average, and note any comps with thin samples. Flag gaps rather than filling them. Do not invent data.
Selected 8 market-rate rent comps for Golden Nugget Apartments (206 units, LOW_RISE garden-style, 1976, avg 601 SF 1BR-heavy, in-place avg $1,117.06, occ 95.15%) using the rental-comps skill methodology. All candidates came from the census-place step (Englewood, CO, census_place_id 0824785) via query_data on property_mfr topics mf_rent_and_occupancy_snapshot + mf_property_attributes, filtered to rent_type=MARKET, non-null in_place_rent_latest_avg and unit_count, subject excluded. 31 eligible candidates were retrieved (2 pages, full pagination) — since ≥20 candidates existed at the census-place step, no county-wide step was run (per skill's <20-candidate trigger). Distances computed via haversine in subagents/rent_comps/rank_comps.py from subject lat/lon (39.62497, -104.99101). Ranking followed skill priority order: distance<5mi bucket → household restrictions (all 31 candidates unrestricted/null or "[]", so no mismatches to trade off) → bedroom-type rent overlap (1BR/2BR) → unit-count tier match (subject = Standard, 75-250 units) → building-style group match (subject = low-density: LOW_RISE/GARDEN/TOWNHOUSE) → vintage proximity to 1976 (or renovation year if present) → raw distance as final tiebreak.
Final 8 comps (rank, name, distance, in-place avg rent, sample size, flag):
Simple average of the 8 comps' in-place rent = ~$1,433/unit vs. subject's $1,117.06 in-place — comps sit noticeably above subject, consistent with rent upside potential, but comps #3-#6 skew the average upward with larger/newer/taller product that is less physically comparable to the 46-year-old low-rise subject. The three closest and most physically similar comps (Brix, Off Broadway, Oxford Station — all within ~2 mi, LOW_RISE/GARDEN, standard unit-count tier) average ~$1,323/unit, which may be a more defensible "like-for-like" benchmark for the IC view than the full 8-comp average.
Gaps/flags (not filled, per instructions):
Output file: subagents/rent_comps/rent_comps.json — ordered list of 8 comps with all required keys (rank, property_mfr_id, name, address, city_state_zip, distance_mi, unit_count, total_rentable_sqft, year_built, year_renovated, in_place_rent_avg, in_place_0bed..4bed, occupancy_latest, asking_rent_avg, building_style), ready to write into the Rent Comps tab (8 comp columns). Supporting scripts: subagents/rent_comps/candidates.json (raw pool of 31 eligible candidates with source fields), subagents/rent_comps/rank_comps.py (haversine distance calc + ranking logic — rerun to verify/recompute).
Goal: select multifamily SALES comps for the subject so they can be written into an underwriting workbook (Sales Comps tab, up to 8 comp columns). They feed a price-per-unit valuation cross-check for an acquisition offer.
Subject: Golden Nugget Apartments, 291 W Belleview Ave, Englewood, CO 80110. RealAI property_mfr id = 59b6289028200764db02b635724279d2. Latitude 39.62497383356103, longitude -104.99101102352144. 206 units, 123,806 SF (avg 601 SF, mostly 1BR), built 1976, 3-story LOW_RISE garden, market-rate, in-place rent ~$1,117/unit. county_id 08005 (Arapahoe County), submarket_id d16015f307a9f41908e3caf0eeb77ed5, market_id 19740 (Denver-Aurora). Today is 2026-09-28.
Method: load the sales-comps skill with sandbox_skills and follow it (progressively wider geographies; filter/rank on proximity, size, building type, vintage, sale recency). Sale fields are in property_mfr topic mf_sales_history: latest_sale_date, latest_sale_price, latest_sale_price_per_unit. Physical attributes in mf_property_attributes (unit_count, total_rentable_sqft, year_built, year_renovated, latitude, longitude, building_style, rent_type). In-place rent: mf_rent_and_occupancy_snapshot.in_place_rent_latest_avg (this is CURRENT rent, not rent at the time of sale). Exclude the subject. Prefer arm's-length sales since 2022 with price per unit that looks plausible (exclude obvious partial-interest or nominal transfers, e.g. < $40k/unit for this product type, and state what you excluded). Need at least 3 and up to 8 comps with a sale price. Cast all numbers to float/int.
For each chosen comp, try a quick web search for the buyer name ("<property name> Englewood/Denver sold <year> buyer"); if not found, leave buyer null. Don't spend more than ~1 search per comp.
Also propose, for each comp, adjustments as decimals (positive = comp inferior to subject, adjust up) with a one-line rationale each: time (market conditions since sale date — Denver multifamily values have declined since 2022; reason about it), size, year built/condition, location, market conditions (use 0 where time already captures it). Keep each adjustment within ±15% and total within ±25%. Propose weights (decimals summing to exactly 1.0) favoring the closest, most recent, most similar comps.
Compute haversine distance in miles in a sandbox python script.
Output: save subagents/sales_comps/sales_comps.json — list ordered by rank, each entry with: rank, property_mfr_id, name, address, city_state_zip ("City, ST 12345"), distance_mi, unit_count, year_built, year_renovated, sale_date (YYYY-MM-DD), sale_price, price_per_unit, in_place_rent_avg, buyer (or null), buyer_source_url (or null), adj_time, adj_size, adj_condition, adj_location, adj_market, adj_rationale (dict of one-liners), weight. In your report give a compact table and the weighted adjusted $/unit you'd expect, plus any exclusions or caveats. Flag gaps rather than filling them.
Selected 6 multifamily sales comps for Golden Nugget Apartments (206 units, 1976, LOW_RISE/garden, market-rate, in-place rent ~$1,117/unit) in Englewood/Littleton/Centennial submarkets of the Denver-Aurora market (19740). Followed sales-comps skill (zip→census place→county progressive widening; census-place-level query on 0824785 returned 14 rows, exceeding the 10-row stop threshold, so county-level query was used only to probe for additional standard-tier candidates). Distances computed via haversine in subagents/sales_comps/compute.py (also subagents/sales_comps/distance.py for the initial scan).
EXCLUSIONS (from property_mfr, county_id 08005 / census_place_id 0824785 pool):
FINAL 6 COMPS (ranked, weights sum to 1.0), all MARKET rent_type, sold since 2022, all with confirmed sale price/unit:
All comps: individual adjustments within ±15%, total adjustments within ±25% (see subagents/sales_comps/compute.py, verified programmatically). adj_market set to 0 across the board — market-conditions/timing effect is fully captured in adj_time to avoid double counting. Time adjustments are negative (~-2%/year since sale date, capped at -15%) reflecting that Denver-area multifamily values have declined since the 2022 peak (national values down ~10% from 2022 peak per RealPage Analytics, June 2026; Denver specifically cited as "still working through a supply overhang" in the same source) — meaning older sales transacted in a stronger pricing environment than today's and need a downward adjustment to be comparable.
WEIGHTED ADJUSTED $/UNIT (cross-check value indication): $194,233/unit (computed in subagents/sales_comps/compute.py). This spans a fairly wide adjusted range ($149k–$268k/unit across comps), driven mainly by unit-size and vintage differences among comps; Summit Riverside and Rustic Arms pull the average up, while SouthGlenn Place and Fielder's Creek pull it down. Given the subject's below-market in-place rent (~$1,117 vs. comp range $1,025–$1,679), the true indicated value may sit toward the lower half of this range — flag for the underwriter to sanity-check against income-approach valuation rather than relying on the sales-comp average alone.
CAVEATS/GAPS:
Goal: produce three forecast reads with the RealAI forecasting skill for a 7-year multifamily acquisition underwriting (hold 2026-2033) of Golden Nugget Apartments, a 206-unit 1976 garden property in Englewood, CO (Denver CBSA). The outputs set: (1) the annual rent growth assumption, (2) the annual operating-expense growth assumption, and (3) cap-rate context for the exit cap. Today is 2026-09-28.
Rules (from the skill; follow them exactly): load the skill with sandbox_skills("forecasting") and read references/request.md, methodology.md and response.md. scripts/forecast.py is the SOLE math authority: run python skills/forecasting/scripts/forecast.py --version once, then run it with payload files. Never estimate, recompute or adjust a forecast by hand, never write your own forecasting code, never hand-type history arrays (parse them programmatically from saved raw query results), include history_check (and peer_history_check) in every payload, and populate context_signals only from actual tool results (omit otherwise and list omissions in caller_disclosures.signals_omitted). Report the returned confidence tier and every data_quality_flags entry verbatim.
Data (RealAI datamart via explore_data/query_data; check fields with explore_data before querying):
Save payloads and raw outputs under subagents/forecasts/ (rent_payload.json, rent_out.json, opex_payload.json, opex_out.json, cap_payload.json, cap_out.json, plus raw query results). Return: for rent and expense, the base/upside/downside annual growth arrays exactly as returned, a single recommended flat annual rate to type into the model (state which engine output it is, e.g. the base-case CAGR/average field the engine returns — take it from the engine's JSON, do not compute your own), confidence tier, and all flags verbatim; for cap rates, the band position and signals. Flag gaps rather than filling them.
All three forecasting-skill runs executed via skills/forecasting/scripts/forecast.py (verified via --version: params_version 2026Q3.5, dated 2026-07-22, sha256 517fce0a...). Payloads/outputs saved under subagents/forecasts/. No numbers below were hand-computed by the engine's math — only the single flat-rate flattening (CAGR of the engine's own base_case path) was computed in code per response.md's explicit "flattening a multi-year path" rule (script: subagents/forecasts/build_payloads.py plus an inline CAGR snippet).
=== 1) RENT GROWTH (family rent_or_occupancy, mode=trend) === Primary subject: Englewood submarket (id d16015f307a9f41908e3caf0eeb77ed5), in-place rent (mf_rent_ts.in_place_rent_latest_avg, MONTH). History: only 26 valid months exist (2024-07 to 2026-08) — the submarket series has no data before Jul-2024, confirming the brief's concern about a short window. This is NOT "too short" for the engine's 6-point minimum, but it is short of the 30-month depth that clears the cyclical-drawdown/boom guard and sits entirely inside a single post-2020 regime. Peer = Denver, CO CBSA (market_id 19740), same field/window (parent-geography default; level ratio 1.076). File: rent_payload.json / rent_out.json.
Base case (in-place rent, $): Yr1 1695.03 (+0.14%), Yr2 1691.43 (-0.21%), Yr3 1679.18 (-0.72%), Yr4 1663.51 (-0.93%), Yr5 1647.99 (-0.93%), Yr6 1632.61 (-0.93%), Yr7 1617.37 (-0.93%) Upside case: Yr1 1728.89 (+2.14%) … Yr7 2013.85 (+3.69%) Downside case: Yr1 1661.18 (-1.86%) … Yr7 1289.04 (-5.56%)
Confidence: LOW (forced down by boom-extrapolation guard + short window). Data_quality_flags (verbatim):
RECOMMENDED FLAT ANNUAL RATE TO TYPE INTO THE MODEL: -0.65% per year. This is the CAGR of the engine's own base_case path (start 1692.71 → Year-7 1617.37, compounded over 7 years), computed in code, not the engine's own scalar field — the engine does not return a single flattened number; per response.md's rule this CAGR-of-the-path is the correct way to compress a multi-year base_case into one cell. Given the LOW confidence and the guard warning above, this negative flat figure should be treated with caution and layered with judgment before use (see below), not taken at face value.
Sibling check (MANDATORY — divergence 266bps > 100bps threshold): asking rent (mf_rent_ts.asking_rent_latest_avg) trailing-12mo growth is -4.19% vs in-place -1.53%, a 266bps gap, so both were run. Asking-rent base case: cyclical_drawdown_suspected=TRUE, confidence LOW, flag: "cyclical drawdown suspected: history is short (<=30mo) and trailing trend is negative. The engine held flat after year 1 rather than extrapolating the correction... Do NOT use this base case as a multi-year assumption without that judgment." Asking-rent base case holds flat at $1,643.26 (-5.34% Yr1, 0% thereafter). File: rent_asking_payload.json / rent_asking_out.json.
Analyst judgment layered on top (explicitly labeled, per skill's "where the override lives" rule — NOT re-run into the engine): the negative in-place base case and flat/negative asking-rent base case are consistent with the known-known context signals cited in the brief but NOT fed into context_signals (they weren't sourced this session for supply/migration/employment blocks, logged below): submarket occupancy 93.4% with T12 in-place -2.07% and tradeout -2.65% (softening), and Arapahoe County occupancy 93.2%/-1.88%/-3.59%. Those independently-known softening signals corroborate rather than contradict the engine's negative base case; permitted MF units T13-T24 in Arapahoe County were 1,734 (elevated supply, passed to the engine as supply_pipeline.permitted_units_t13_t24 but the engine could not classify supply_pressure without existing_stock, which was not gathered — signal reads "unknown"). Given the corroboration, a defensible underwriting assumption is a flat-to-modestly-positive path (e.g., 0%-1.5%) rather than either extreme, but that judgment sits outside the engine and is the analyst's, not a re-run. Signals_omitted (disclosed in payload, not fabricated): migration (not gathered), employment (not gathered). supply_pressure/migration_signal/employment_signal all returned "unknown" because existing_stock/inbound-outbound income/job growth were never pulled this session.
=== 2) OPERATING EXPENSE GROWTH (family operating, mode=trend) === History: no property-specific expense time series exists in the datamart (per the skill's own rule, "operating" has no geographic time series); used national_metrics_monthly.consumer_price_index as the sourced inflation proxy per the brief's instruction, full available history 2015-01 through 2026-08 (139 valid monthly points, one null month 2025-10 dropped per no-interpolation rule). No peer (peer_omitted_reason: "no comparable peer at grain"). File: opex_payload.json / opex_out.json.
Base case (CPI index level, for reference only — pct_change is the read): Yr1 +3.32%, Yr2 +3.32%, Yr3 +3.33%, Yr4 +3.34%, Yr5 +3.36%, Yr6 +3.37%, Yr7 +3.39% Upside case: Yr1 +4.82% … Yr7 +6.86% Downside case: Yr1 +1.82% … Yr7 -0.08%
Confidence: LOW. Data_quality_flags (verbatim): "thin or absent peer cohort; mean reversion not applied" Methodology: recent_annualized_growth 3.37%, long_run_mean_growth 3.10%, terminal_rate_applied 3.5% (operating family terminal, basis: ~CPI+100bps / Trepp 2015-2024 MF OpEx CAGR 4.15%), no boom/drawdown guard fired, trend_r2 0.9588.
RECOMMENDED FLAT ANNUAL RATE TO TYPE INTO THE MODEL: 3.35% per year — CAGR of the engine's base_case CPI path over the 7-year horizon (334.131 → 420.80), computed in code. Note this is a national CPI-based proxy, not a multifamily-specific expense trend; the engine's own terminal-rate basis (Trepp MF OpEx CAGR 4.15%, insurance ~11.8%, taxes ~5.4%) suggests real multifamily expense growth — especially insurance and property tax lines — could run above this flat CPI-based figure. Caller judgment: consider layering a modest premium above 3.35% for insurance/tax-heavy expense lines, labeled as judgment, not re-run through the engine.
=== 3) CAP RATE (family capital_markets, mode=directional — no projection produced, by design) === Subject: Denver, CO CBSA multifamily cap rate (caprate_ts.multifamily, market_id 19740), full available quarterly history 1Q05-2Q26 (86 valid quarters, 21.25 years). File: cap_payload.json / cap_out.json.
Full-history band: min 3.50%, structural mean 5.62%, max 8.22%. Current level 5.48%. position_in_band 0.4195 → position_label "mid". Trailing-10-year band: min 3.50%, structural mean 4.81%, max 5.50%. Current level 5.48%. position_in_band 0.99 → position_label "rich". Convention (per skill's mandatory statement): "rich"/"cheap" describe the metric's own LEVEL, not asset pricing — a rich (high) cap rate means a wide yield, i.e., a cheap asset on that basis; the current level sits near the top of its trailing-10-year band even though it reads "mid" against the full 21-year history that includes the GFC spike (2008-09, up to 8.22%) and ZIRP trough (2021, down to 3.50%). Signals: rate_environment = "unknown" — only the current 10-yr Treasury actual (5.18% as of 2026-09-24, national_metrics_daily.ten_year_treasury_pct) was sourced this session; no forward-curve/projected 10-yr figure was pulled, so projected_rate was omitted rather than estimated (disclosed, not fabricated). Confidence: HIGH (21+ years of clean quarterly history). Data_quality_flags (verbatim):
Caller synthesis (explicitly the analyst's judgment, per the skill's rule that the engine performs no outlook): the multi-regime disagreement itself is informative — Denver multifamily cap rates have already re-priced most of the way from the 2021 ZIRP trough (3.50%) back toward a rate-regime-consistent level, and sit near the top of the post-GFC decade's range while still mid-pack against the full 21-year cycle that includes the 2008-09 spike. With the 10-yr Treasury at 5.18% (elevated vs. the last decade), an entry/exit cap in the low-to-mid 5% range (roughly in line with the current 5.48% level, perhaps 25-75bps of further widening risk if rates stay elevated through the 2033 exit) is a defensible starting point for an exit-cap assumption — this is analyst judgment layered on the two engine facts (band position + rate level), not an engine output.
=== Gaps flagged rather than filled ===
cd /vercel/sandbox && python skills/xlsx/scripts/workbook_profile.py CentrePoint_Pro_Forma_IC_Template_v1.0.xlsx | head -60; python -c "
import openpyxl
wb=openpyxl.load_workbook('CentrePoint_Pro_Forma_IC_Template_v1.0.xlsx')
print(wb.sheetnames)
a=wb['Assumptions']
for r in range(1,90):
row=[]
for c in 'BCDGHIJ':
v=a[f'{c}{r}'].value
if v is not None: row.append(f'{c}{r}={v!r}'[:70])
if row: print(' | '.join(row))
m=wb['IC Memo']
for r in range(1,50):
row=[]
for c in 'BCDEFG':
v=m[f'{c}{r}'].value
if v is not None: row.append(f'{c}{r}={v!r}'[:80])
if row: print(' | '.join(row))
"cd /vercel/sandbox && python -c "
import json
for f in ['subagents/rent_comps/rent_comps.json','subagents/sales_comps/sales_comps.json']:
d=json.load(open(f))
for x in d:
x=dict(x); x.pop('adj_rationale',None); print(x)
print()
"; python -c "
import openpyxl
wb=openpyxl.load_workbook('CentrePoint_Pro_Forma_IC_Template_v1.0.xlsx')
for s in ['Rent Comps','Sales Comps']:
ws=wb[s]
for r in range(1,36):
row=[f'{c}{r}={ws[f\"{c}{r}\"].value!r}'[:60] for c in 'BCD' if ws[f'{c}{r}'].value is not None]
if row: print(s,' | '.join(row))
""""Write Golden Nugget inputs into the CentrePoint template (approved cells only)."""
import json, shutil, sys, datetime
import openpyxl
SRC = 'CentrePoint_Pro_Forma_IC_Template_v1.0.xlsx'
OUT = sys.argv[1] if len(sys.argv) > 1 else 'Golden_Nugget_Apartments_CentrePoint_Underwriting_2026-09-28.xlsx'
PRICE = float(sys.argv[2]) if len(sys.argv) > 2 else 25_000_000.0
shutil.copy(SRC, OUT)
wb = openpyxl.load_workbook(OUT)
def w(ws, ref, val):
cur = ws[ref].value
if isinstance(cur, str) and cur.startswith('='):
raise SystemExit(f'VIOLATION: {ws.title}!{ref} is a formula')
ws[ref].value = val
A = wb['Assumptions']
# ---- property facts (datamart: property_mfr) ----
w(A, 'C5', 'Golden Nugget Apartments')
w(A, 'C6', '291 W Belleview Ave')
w(A, 'C7', 'Englewood, CO 80110')
w(A, 'C8', 'LOW_RISE')
w(A, 'C9', int(206))
w(A, 'C10', int(123806))
w(A, 'C11', int(1976))
w(A, 'C12', None)
# ---- acquisition / disposition ----
w(A, 'C15', PRICE)
w(A, 'C18', 0.02)
w(A, 'C22', 0.0625)
w(A, 'C23', 0.02)
# ---- capex (none provided) ----
w(A, 'C26', None)
w(A, 'C28', None)
# ---- reserves (pre-1990 vintage default) ----
w(A, 'C32', 350.0)
# ---- growth / hold ----
w(A, 'H15', int(7))
w(A, 'H16', 0.01)
w(A, 'H17', 0.035)
w(A, 'H18', 0.01)
w(A, 'H19', 0.95)
# ---- T12 ----
GPR = float(2765884.51)
VAC = -abs(float(95085.2))
EGI = GPR + VAC # other income blank -> EGI basis for county benchmark lines
bm = { # Arapahoe County mf_pnl_benchmarks, pct of EGI (sample n=188)
'H42': 0.0375, # insurance
'H44': 0.0694, # R&M
'H45': 0.0334, # management
'H46': 0.1123, # payroll
'H47': 0.0375, # G&A
'H48': 0.0128, # advertising
'H49': 0.0007, # other
}
w(A, 'H33', round(GPR))
w(A, 'H34', round(VAC))
w(A, 'H37', None)
w(A, 'H41', round(float(110830.44)))
w(A, 'H43', None)
for ref, pct in bm.items():
w(A, ref, round(pct * EGI))
# ---- senior debt ----
w(A, 'C36', 'IO Then Amortizing')
w(A, 'C38', 0.60)
w(A, 'C40', 0.0611)
w(A, 'C41', int(2))
w(A, 'C42', int(30))
w(A, 'C43', int(7))
# protected defaults left as-is: C55=0, C73='No', H22='No'
# ---- Rent comps ----
R = wb['Rent Comps']
for rng in ['D6:K14', 'C17:K21', 'D25:K25']:
for row in R[rng]:
for c in row:
if not (isinstance(c.value, str) and str(c.value).startswith('=')):
c.value = None
rc = json.load(open('subagents/rent_comps/rent_comps.json'))[:8]
cols = 'DEFGHIJK'
num = lambda v: None if v is None else float(v)
for i, c in enumerate(rc):
col = cols[i]
w(R, f'{col}6', c['name']); w(R, f'{col}7', c['address']); w(R, f'{col}8', c['city_state_zip'])
w(R, f'{col}9', float(c['distance_mi'])); w(R, f'{col}10', int(c['unit_count']))
w(R, f'{col}11', int(c['total_rentable_sqft']) if c['total_rentable_sqft'] else None)
w(R, f'{col}12', int(c['year_built']) if c['year_built'] else None)
w(R, f'{col}13', int(c['year_renovated']) if c['year_renovated'] else None)
w(R, f'{col}14', float(c['in_place_rent_avg']))
for r, k in zip(range(17, 22), ['in_place_0bed', 'in_place_1bed', 'in_place_2bed', 'in_place_3bed', 'in_place_4bed']):
w(R, f'{col}{r}', num(c[k]))
w(R, f'{col}25', float(c['occupancy_latest']))
# subject by-bed in-place (property snapshot)
w(R, 'C17', None); w(R, 'C18', 1082.03); w(R, 'C19', 1506.75); w(R, 'C20', None); w(R, 'C21', None)
# ---- Sales comps ----
S = wb['Sales Comps']
for rng in ['D6:K12', 'D15:K16', 'D18:K19', 'D22:K26', 'D28:K28']:
for row in S[rng]:
for c in row:
if not (isinstance(c.value, str) and str(c.value).startswith('=')):
c.value = None
sc = json.load(open('subagents/sales_comps/sales_comps.json'))[:8]
wsum = sum(c['weight'] for c in sc)
assert abs(wsum - 1) < 1e-6, wsum
for i, c in enumerate(sc):
col = cols[i]
w(S, f'{col}6', c['name']); w(S, f'{col}7', c['address']); w(S, f'{col}8', c['city_state_zip'])
w(S, f'{col}9', float(c['distance_mi'])); w(S, f'{col}10', int(c['unit_count']))
w(S, f'{col}11', int(c['year_built']) if c['year_built'] else None)
w(S, f'{col}12', int(c['year_renovated']) if c['year_renovated'] else None)
w(S, f'{col}15', datetime.datetime.strptime(c['sale_date'], '%Y-%m-%d'))
S[f'{col}15'].number_format = S[f'{col}15'].number_format if S[f'{col}15'].number_format != 'General' else 'm/d/yyyy'
w(S, f'{col}16', float(c['sale_price']))
w(S, f'{col}18', float(c['in_place_rent_avg']))
w(S, f'{col}19', c['buyer'])
for r, k in zip(range(22, 27), ['adj_time', 'adj_size', 'adj_condition', 'adj_location', 'adj_market']):
w(S, f'{col}{r}', float(c[k]))
w(S, f'{col}28', float(c['weight']))
# ---- IC memo header ----
M = wb['IC Memo']
w(M, 'F4', datetime.datetime(2026, 9, 28))
w(M, 'F6', 'Initial Screen')
wb.save(OUT)
print('wrote', OUT, 'price', PRICE)
cd /vercel/sandbox && python build_inputs.py scratch.xlsx 25000000 && python skills/xlsx/scripts/recalc.py scratch.xlsx | head -30 && python -c "
import openpyxl
wb=openpyxl.load_workbook('scratch.xlsx',data_only=True)
A=wb['Assumptions'];R=wb['Returns Summary'];M=wb['IC Memo']
for ref in ['H5','H6','H7','H36','H38','H50','H52','M52','H60','C50','C51','C52','C64','C70']: print(ref,A[ref].value)
for ref in ['C15','C16','C21','C22','C24','C26']: print('RS',ref,R[ref].value)
for r in range(30,35): print(M[f'B{r}'].value,M[f'D{r}'].value,M[f'E{r}'].value)
for r in range(38,44): print(M[f'B{r}'].value,M[f'C{r}'].value)
print(M['C47'].value,M['C48'].value, wb['Sales Comps']['C32'].value, wb['Sales Comps']['C33'].value, wb['Rent Comps']['C29'].value)
"cd /vercel/sandbox && sed -i "s/w(A, 'H17', 0.035)/w(A, 'H17', 0.0335)/" build_inputs.py && grep H17 build_inputs.py && cat > goal_seek.py <<'EOF'
"""Goal-seek Assumptions!C15 against IC hurdles. Varies only C15; recalc.py after each step."""
import openpyxl, subprocess, shutil, json, sys, math
BASE = sys.argv[1]
def run(price, tag='gs'):
p = f'gs_{tag}.xlsx'; shutil.copy(BASE, p)
wb = openpyxl.load_workbook(p); wb['Assumptions']['C15'].value = float(price); wb.save(p)
subprocess.run(['python', 'skills/xlsx/scripts/recalc.py', p], check=True, capture_output=True, timeout=300)
v = openpyxl.load_workbook(p, data_only=True)
A, R, M, S = v['Assumptions'], v['Returns Summary'], v['IC Memo'], v['Sales Comps']
return dict(price=price, irr=R['C21'].value, em=R['C22'].value, dscr=A['C50'].value, dy=A['C51'].value,
coc1=M['D34'].value, stress=M['C41'].value, res={f'E{r}': M[f'E{r}'].value for r in range(30, 35)},
hurdles={f'C{r}': M[f'C{r}'].value for r in range(30, 35)}, comp_val=S['C33'].value,
cap_t12=A['H5'].value, cap_y1=A['H6'].value)
cache = {}
def ev(price):
price = round(price / 10000) * 10000
if price not in cache: cache[price] = run(price)
return cache[price]
def solve(key, hurdle, lo, hi, tol):
"""max price with metric(key) >= hurdle; metric decreasing in price. <=10 iterations."""
flo, fhi = ev(lo)[key] - hurdle, ev(hi)[key] - hurdle
it = 0; best = lo if flo >= 0 else None
if fhi >= 0: return dict(price=hi, converged=False, note='clears at upper bracket', iters=0)
if flo < 0: return dict(price=lo, converged=False, note='fails at lower bracket', iters=0)
while it < 10 and hi - lo > 10000:
it += 1
x = lo + (hi - lo) * flo / (flo - fhi) # secant / regula falsi
x = min(max(x, lo + 10000), hi - 10000)
x = round(x / 10000) * 10000
fx = ev(x)[key] - hurdle
if fx >= 0: lo, flo, best = x, fx, x
else: hi, fhi = x, fx
if 0 <= fx <= tol: break
return dict(price=best, converged=(hi - lo <= 10000) or (best is not None and ev(best)[key]-hurdle <= tol), iters=it,
bracket=[lo, hi])
if __name__ == '__main__':
lo, hi = float(sys.argv[2]), float(sys.argv[3])
h = ev(hi)['hurdles']
out = {}
out['irr_price'] = solve('irr', h['C30'], lo, hi, 0.001)
targets = {'E30': ('irr', h['C30'], 0.001), 'E31': ('em', h['C31'], 0.01), 'E32': ('dscr', h['C32'], 0.01),
'E33': ('dy', h['C33'], 0.001), 'E34': ('coc1', h['C34'], 0.001)}
per = {}
for k, (key, hv, tol) in targets.items():
per[k] = out['irr_price'] if k == 'E30' else solve(key, hv, lo, hi, tol)
out['per_hurdle'] = per
bind = min(per, key=lambda k: per[k]['price'])
out['all_hurdles_price'] = dict(per[bind], binding=bind)
out['approve_price'] = solve('stress', h['C30'], lo, hi, 0.001)
for k in ['irr_price', 'all_hurdles_price', 'approve_price']:
p = out[k]['price']; out[k]['metrics'] = ev(p)
json.dump(out, open('goal_seek_results.json', 'w'), indent=1, default=str)
for k in ['irr_price', 'all_hurdles_price', 'approve_price']:
m = out[k]['metrics']
print(k, out[k]['price'], 'per unit', round(out[k]['price']/206), 'vs comp', round(out[k]['price']/m['comp_val']-1, 4),
'IRR', round(m['irr'], 4), 'EM', round(m['em'], 3), 'DSCR', round(m['dscr'], 2), 'DY', round(m['dy'], 4),
'CoC1', round(m['coc1'], 4), 'stress', round(m['stress'], 4), 'capT12', round(m['cap_t12'], 4), out[k].get('binding',''), out[k]['converged'], out[k]['iters'])
for k, v in per.items(): print(k, v['price'], v['converged'])
EOF
python build_inputs.py scratch.xlsx 20000000 && python skills/xlsx/scripts/recalc.py scratch.xlsx >/dev/null && time python goal_seek.py scratch.xlsx 8000000 30000000cd /vercel/sandbox && python - <<'EOF'
import re
s=open('goal_seek.py').read()
s=s.replace(""" x = lo + (hi - lo) * flo / (flo - fhi) # secant / regula falsi""",""" x = lo + (hi - lo) * flo / (flo - fhi) if it % 2 else (lo + hi) / 2 # alternate secant / bisection""")
open('goal_seek.py','w').write(s)
EOF
time python goal_seek.py scratch.xlsx 8000000 42000000cd /vercel/sandbox && F=Golden_Nugget_Apartments_CentrePoint_Underwriting_2026-09-28.xlsx && python build_inputs.py $F 22750000 && python skills/xlsx/scripts/recalc.py $F && python skills/xlsx/scripts/workbook_integrity_scan.py $F --json integrity.json; echo exit=$?; python -c "
import json;d=json.load(open('integrity.json'))
fs=d.get('findings',d)
hi=[f for f in fs if str(f.get('severity','')).lower()=='high']
from collections import Counter
print(Counter((f.get('category') or f.get('check'),f.get('sheet')) for f in hi))
for f in hi[:60]: print(f.get('category') or f.get('check'), f.get('sheet'), f.get('cell') or f.get('range'), str(f.get('message') or f.get('detail'))[:120])
"Output too long; let me summarize the HIGH findings only.
cd /vercel/sandbox && python -c "
import json;d=json.load(open('integrity.json'))
hi=[f for f in d['findings'] if str(f.get('severity','')).lower()=='high']
for f in hi: print(f.get('category'), f.get('sheet'), f.get('cell') or f.get('range'), str(f.get('reason'))[:90], str(f.get('references') or f.get('target') or '')[:80])
" 2>&1 | head -50cd /vercel/sandbox && cat > validate.py <<'EOF'
import openpyxl, sys
F=sys.argv[1]; v=openpyxl.load_workbook(F,data_only=True)
A,S,RS,W,SE,RC,SC,M=[v[s] for s in ['Assumptions','Sources & Uses','Returns Summary','Waterfall','Sensitivity','Rent Comps','Sales Comps','IC Memo']]
errs=[]
for ws in v.worksheets:
for row in ws.iter_rows():
for c in row:
if isinstance(c.value,str) and c.value.strip() in ('#REF!','#VALUE!','#DIV/0!','#NAME?','#N/A'): errs.append(f'{ws.title}!{c.coordinate}')
chk={}
chk['V1']=not errs
chk['V2']=abs(S['C11'].value or 0)<1 and S['G11'].value=='BALANCED'
chk['V3']=W['C71'].value=='PASS' and all(W.cell(71,c).value in ('PASS','',None) for c in range(4,14))
t=RS['C21'].value
chk['V4']=abs(SE['F8'].value-t)<1e-6 and abs(SE['F26'].value-t)<1e-6 and abs(SE['F17'].value-RS['C22'].value)<1e-6 and M['C48'].value=='TIES'
chk['V5']=all(abs(a-b)<1e-9 for a,b in [(SE['F5'].value,A['H16'].value),(SE['B8'].value,A['C22'].value),(SE['F14'].value,A['H16'].value),(SE['B17'].value,A['C22'].value),(SE['F23'].value,A['C38'].value),(SE['B26'].value,A['C15'].value)])
chk['V6']=A['H34'].value<=0 and 0<A['H36'].value<=1 and 0<A['H19'].value<=1 and all(0<RC.cell(25,c).value<=1 for c in range(4,12) if RC.cell(25,c).value is not None)
chk['V7']=isinstance(A['H15'].value,int) and 1<=A['H15'].value<=10
chk['V8']=A['C36'].value in ['Interest Only','IO Then Amortizing','Fully Amortizing'] and (A['C37'].value==1 or A['C42'].value>0) and (A['C37'].value!=3 or A['C41'].value>=1)
rents=[RC.cell(14,c).value for c in range(4,12) if isinstance(RC.cell(14,c).value,(int,float))]
prices=[c for c in range(4,12) if isinstance(SC.cell(16,c).value,(int,float))]
wts=[SC.cell(28,c).value or 0 for c in range(4,12)]
chk['V9']=len(rents)>=3 and RC['C29'].value not in ('',None) and len(prices)>=3 and abs(sum(wts)-1)<=0.001 and all((SC.cell(28,c).value or 0)==0 or c in prices for c in range(4,12)) and SC['C32'].value>0
chk['V10']=abs(A['C77'].value+A['C78'].value-A['C64'].value)<=1 and A['H52'].value>0 and A['H50'].value<A['H38'].value
adv={'H5 in [2%,15%]':0.02<=A['H5'].value<=0.15,'C50>0':A['C50'].value>0,'IRR not 0':RS['C21'].value!=0,'C43>=H15':A['C43'].value>=A['H15'].value,
'C22>=H6-0.005':A['C22'].value>=A['H6'].value-0.005,'opex ratio H60 in [0.30,0.60]':0.30<=A['H60'].value<=0.60}
print(chk); print('advisory',adv); print('errors',errs[:5])
EOF
python validate.py Golden_Nugget_Apartments_CentrePoint_Underwriting_2026-09-28.xlsxcd /vercel/sandbox && cat > context_price.py <<'EOF'
"""IRR/EM/DSCR at the comp-implied value and at a market-cap-rate price (only C15 changed)."""
import json, sys
sys.argv=['x','Golden_Nugget_Apartments_CentrePoint_Underwriting_2026-09-28.xlsx']
import goal_seek as g
g.BASE='Golden_Nugget_Apartments_CentrePoint_Underwriting_2026-09-28.xlsx'
out={}
for label,p in [('comp_implied_40.01M',40010000),('cap_5.48pct_31.92M',31920000)]:
m=g.run(p,tag=label[:4]); out[label]={k:m[k] for k in ['price','irr','em','dscr','dy','coc1','cap_t12']}
print(label, {k:(round(v,4) if isinstance(v,float) else v) for k,v in out[label].items()})
json.dump(out,open('context_price_results.json','w'),indent=1)
EOF
python context_price.py; python -c "
import openpyxl
v=openpyxl.load_workbook('Golden_Nugget_Apartments_CentrePoint_Underwriting_2026-09-28.xlsx',data_only=True)
A,RS,W,SE,RC,SC,M,PF=[v[s] for s in ['Assumptions','Returns Summary','Waterfall','Sensitivity','Rent Comps','Sales Comps','IC Memo','Pro Forma']]
for r in ['H5','H6','H7','C50','C51','C52','H52','M52','C64','C70','C39','C46','C47','H50','H38','H60','C19']: print('A',r,A[r].value)
for r in ['C7','C11','C15','C16','C18','C21','C22','C23','C24','C25','C26']: print('RS',r,RS[r].value)
for r in ['C29','C30','C31','C32','C33','C35']: print('RC',r,RC[r].value)
for r in ['C32','C33','C34','C35']: print('SC',r,SC[r].value)
print('F7',M['F7'].value)
for r in range(30,35): print(M[f'B{r}'].value,M[f'D{r}'].value,M[f'E{r}'].value)
for r in range(38,44): print(M[f'B{r}'].value,M[f'C{r}'].value,M[f'D{r}'].value)
print('F44',M['F44'].value)
for r in range(5,11): print([SE.cell(r,c).value for c in range(2,9)])
for r in range(23,29): print([SE.cell(r,c).value for c in range(2,9)])
print('NOI row', [PF.cell(26,c).value for c in range(3,12)])
print('lev CF', [PF.cell(38,c).value for c in range(3,12)])
print('CoC', [PF.cell(39,c).value for c in range(3,12)])
"cd /vercel/sandbox && python -c "
import openpyxl
v=openpyxl.load_workbook('Golden_Nugget_Apartments_CentrePoint_Underwriting_2026-09-28.xlsx')
for s,refs in {'Assumptions':['H5','H6','H7','C50','C51','C52','H52','M52','C64','C70','C39','H60','C19','C16'],'Returns Summary':['C7','C11','C15','C16','C18','C21','C22','C23','C24','C25','C26'],'Rent Comps':['C29','C30','C33','C35','C14'],'Sales Comps':['C32','C33','C35'],'IC Memo':['C41','F44','D34'],'Sensitivity':['F8','C8']}.items():
print(s,[(r,v[s][r].number_format) for r in refs])
"; ls subagents/forecasts/cd /vercel/sandbox && cat > forecast_readback.py <<'EOF'
"""Read back forecast-engine outputs (subagents/forecasts) and compute rent-to-income headroom."""
import json
r=json.load(open('subagents/forecasts/rent_out.json')); o=json.load(open('subagents/forecasts/opex_out.json')); c=json.load(open('subagents/forecasts/cap_out.json'))
def path(x):
b=x.get('base_case') or x.get('scenarios',{}).get('base_case'); return b
for name,x in [('rent',r),('opex',o)]:
b=path(x); print(name,'confidence',x.get('confidence'),'flags',x.get('data_quality_flags'))
vals=[p['value'] if isinstance(p,dict) else p for p in b]
start=x.get('methodology',{}).get('last_observed') or x.get('last_observed_value')
print(' base path',[round(v,2) for v in vals])
print(' terminal',x.get('methodology',{}).get('terminal_rate_applied'))
# CAGR of base path over 7 years from last observed history value
for name,f in [('rent','subagents/forecasts/rent_payload.json'),('opex','subagents/forecasts/opex_payload.json')]:
p=json.load(open(f)); h=p['history']; last=h[-1]['value'] if isinstance(h[-1],dict) else h[-1]
x=r if name=='rent' else o; b=[q['value'] if isinstance(q,dict) else q for q in path(x)]
print(name,'base-case 7yr CAGR',round((b[-1]/last)**(1/len(b))-1,4))
print('cap band', json.dumps({k:c.get(k) for k in ['band_position','signals','confidence','data_quality_flags']})[:900])
hhi=50582; ip=1117.06
print('RTI on in-place', round(ip*12/hhi,4), '30% RTI rent ceiling', round(0.30*hhi/12,2), 'headroom', round(0.30*hhi/12/ip-1,4))
print('loss-to-lease asking vs in-place', round(1172.71/1117.06-1,4), 'Brix premium', round(1273.70/1117.06-1,4))
EOF
python forecast_readback.pyimport math, os, shutil, sys, tempfile
import openpyxl
NARRATIVE_CELLS = {'executive_summary': 'B11', 'investment_thesis': 'B52', 'property_overview': 'B56', 'market_overview': 'B60',
'rent_comps': 'B64', 'sales_comps': 'B68', 'business_plan': 'B72', 'financing': 'B76', 'returns_sensitivity': 'B80',
'risks_mitigants': 'B84', 'conditions_next_steps': 'B88', 'sources_provenance': 'B92'}
def _height(text): # merged B:G at Calibri 10 ~ 125 chars/line, 13.5 pt/line
return max(30, round(sum(max(1, math.ceil(len(p) / 125)) for p in text.split('\n')) * 13.5 + 8, 1))
def write_narratives(path, narratives):
wb = openpyxl.load_workbook(path); m = wb['IC Memo']
for key, text in narratives.items():
ref = NARRATIVE_CELLS[key]; m[ref].value = text; m.row_dimensions[int(ref[1:])].height = _height(text)
wb.save(path)
def export_memo_pdf(recalculated_xlsx, pdf_out): # run only on a workbook that has just been recalculated
vals = openpyxl.load_workbook(recalculated_xlsx, data_only=True)['IC Memo']
wb = openpyxl.load_workbook(recalculated_xlsx); m = wb['IC Memo']
for row in m.iter_rows():
for c in row:
if isinstance(c.value, str) and c.value.startswith('='): c.value = vals[c.coordinate].value
for name in [n for n in wb.sheetnames if n != 'IC Memo']: del wb[name]
wb.active = 0; tmp = tempfile.mkdtemp(); stage = os.path.join(tmp, 'IC_Memo.xlsx'); wb.save(stage)
sys.path.insert(0, 'skills/xlsx/scripts'); from office.soffice import run_soffice
run_soffice(['--headless', '--convert-to', 'pdf', '--outdir', tmp, stage], check=True, capture_output=True, timeout=240)
shutil.move(os.path.join(tmp, 'IC_Memo.pdf'), pdf_out); return pdf_out
import glob, subprocess
from openpyxl.drawing.image import Image as XLImage
def logo_png(out='CentrePoint_logo.png'):
svg = next(iter(glob.glob('**/CentrePoint-logo.svg', recursive=True)), None)
if svg is None: raise FileNotFoundError('CentrePoint-logo.svg not attached')
try:
import cairosvg # pip install cairosvg if absent
cairosvg.svg2png(url=svg, write_to=out, output_width=2400, background_color='white')
except ImportError:
subprocess.run(['convert', '-background', 'white', '-density', '600', svg, '-resize', '2400x', '-flatten', out], check=True)
return out
def ensure_memo_logo(path):
wb = openpyxl.load_workbook(path); m = wb['IC Memo']
if m._images: return 'present'
img = XLImage(logo_png()); r = img.width / img.height; img.height = 40; img.width = int(40 * r)
m.add_image(img, 'B1'); m.row_dimensions[1].height = 46; wb.save(path); return 'restored'
N = {}
N['executive_summary'] = (
"CentrePoint is screening Golden Nugget Apartments, a 206-unit, 1976 low-rise garden community at 291 W Belleview Ave in Englewood, CO, with no asking price; this memo sets our bid. "
"At $22,750,000 ($110,437/unit, a 7.7% T12 cap rate) the deal clears all five IC hurdles with a levered IRR of 11.0% and a 1.87x equity multiple on $9,555,000 of equity at close, 60% LTV senior debt, and a 7-year hold. "
"The all-hurdles price is $22,750,000 and the binding hurdle is the 11.0% levered IRR; equity multiple, cash-on-cash, debt yield and DSCR all clear at higher prices. "
"The deciding metric is levered IRR, and it is fragile: the combined stress case (exit cap +50 bp, rent growth -150 bp) falls to 2.0%, and a 10% price increase drops the IRR to 7.1%. "
"Our bid is 43.1% below the $40,012,063 comp-implied value, so the seller is unlikely to accept it unless diligence shows more NOI than the datamart T12 supports. "
"Proposed recommendation: Approve with Conditions at no more than $22,750,000; the price that moves this to Approve is $18,720,000.")
N['investment_thesis'] = (
"- No business plan was provided (the sponsor thesis field was 'none'), so this is underwritten as a stabilized, as-is hold with no capital program.\n"
"- The case rests on buying current cash flow at a high going-in yield (7.7% T12 cap, 7.5% Year-1 cap) against a Denver multifamily cap rate of 5.48%, then exiting at 6.25%. The exit cap sits below the going-in cap on purpose: we are pricing below market, not betting on cap compression.\n"
"- Rent upside is modest and depends on execution. In-place rent of $1,117 trails the $1,173 asking rent by 5.0%, and the adjacent Brix on Belleview gets 14.0% more. Submarket rents are falling, though, so we model 1.0% growth, not a mark-to-market.\n"
"- Expenses grow at 3.35% versus 1.0% for rent, so NOI erodes from $1,749,115 (T12) to $1,701,305 in Year 1 and keeps sliding. Returns come from the entry yield, not from growth.")
N['property_overview'] = (
"Golden Nugget Apartments is a 206-unit, 3-story frame/brick low-rise garden property built in 1976 on 4.36 acres, with 123,806 rentable SF (601 SF average unit). The mix is mostly 1BR ($1,082 in-place) with some 2BR ($1,507). "
"The property is market-rate, FEMA Zone X, and has no recorded renovation year. "
"Physical occupancy is 95.2% (30-day average 96.7%), with a trailing-12-month retention estimate of 70.9% and a 27-day median days on market for recent leases. "
"The T12 is a datamart reconstruction, not a seller statement: GPR $2,765,885, vacancy and credit loss $95,085 (96.6% implied economic occupancy), and 2025 taxes of $110,830 ($538/unit) on a $1.43M assessed value. "
"All other expense lines are Arapahoe County benchmarks. Total opex is $921,685 (34.5% of EGI), which is light for a 50-year-old asset because utilities and other income are both blank.")
N['market_overview'] = (
"The Englewood submarket (Denver CBSA) is soft. Submarket occupancy is 93.4%, in-place rents fell 2.07% over the trailing 12 months, and new-lease tradeouts average -2.65%. Submarket asking rent of $1,730 sits well above the subject. "
"Arapahoe County is similar: 93.2% occupancy, T12 in-place rent -1.88%, tradeouts -3.59%, and a 66-day median days on market. "
"Supply is easing. County multifamily permits fell from 2,702 units (T25-T36) to 1,734 (T13-T24) to 1,534 in the trailing 12 months through July 2026. "
"The forecast engine's base case for submarket in-place rent is -0.65%/yr over 7 years. Confidence is LOW, with the flag 'boom extrapolation suspected' because history starts in July 2024, so we override it with judgment at 1.0%. "
"The Denver multifamily cap rate of 5.48% sits mid-band on 21-year history but near the top of its trailing 10-year band, with the 10-year Treasury elevated.")
N['rent_comps'] = (
"Eight market-rate comps within 2.6 miles average $1,454 in-place (median $1,333). The subject's $1,119 T12 GPR-per-unit sits 23.0% below the average. "
"That gap overstates the upside. Four comps are newer or taller product (Oxford Station 2016, Sequel 2023, Noble Old Hampden 2024, Parkview Towers high-rise). The closest like-for-like vintage comps, Off Broadway Flats ($1,036) and Creekside ($1,059), rent at or below the subject. "
"The real benchmark is Brix on Belleview (0.06 mi, 1962, renovated 2012) at $1,274, which suggests a renovation-driven premium of roughly 14% if CentrePoint funds a program. "
"Comp average occupancy is 89.4%, pulled down by Creekside (70.6%, n=6) and the two lease-ups. Creekside and Debbie J II have thin rent samples.")
N['sales_comps'] = (
"Six market-rate sales from July 2022 to May 2025 within 3.0 miles give a weighted adjusted value of $194,233/unit, or $40,012,063 for the subject (unadjusted average $212,210/unit). "
"Adjustments are negative for time (Denver values have fallen since 2022) and for the larger, newer comps (Fielder's Creek 1983, Summit Riverside 1986/2013). "
"Our $22,750,000 bid is 43.1% below comp-implied value. The comps traded on higher rents ($1,025-$1,679 today) and likely lower cap rates. At the comp-implied price our NOI is only a 4.4% cap, and the model loses equity. "
"Excluded: pre-development land trades, restricted and affordable assets, 2014-vintage product, and Lorinda Apartments, where the datamart price conflicts with an OM source by about 2x. "
"Kingsbrook Arms ($150,000/unit, 2025) and SouthGlenn Place ($164,338/unit, 2024) are the most recent and most relevant prints, and even they sit well above our basis.")
N['business_plan'] = (
"- No capital program is underwritten (CapEx budget blank). Replacement reserves are $350/unit/year, the pre-1990 house default, or $72,100 per year.\n"
"- Operations are underwritten as-is: 95.0% stabilized economic occupancy (capped from 95.2% physical), rent growth 1.0%, other income growth 1.0%, expense growth 3.35%.\n"
"- Upside we have not underwritten: a RUBS/utility bill-back and fee program (other income is blank in the T12), and a light interior program aimed at the Brix on Belleview premium. Both need a seller T12 and a unit walk before they can go into the price.\n"
"- A renovation program would also have to clear the tenant affordability ceiling described in Risks.")
N['financing'] = (
"Senior loan of $13,650,000 (60% LTV) at 6.11%, the Freddie Mac CME 7-year fixed quote for 65% LTV. It is interest-only for 2 years, then amortizes over 30 years, with a 7-year term matched to the 7-year hold. "
"Going-in DSCR is 2.04x on IO debt service and debt yield is 12.8%, both well above the 1.25x and 9.0% hurdles; breakeven occupancy is 63.5%. "
"Levered cash flow steps down in Year 3 when amortization starts. "
"No mezzanine and no promote structure are modeled. Adding 10 points of LTV lifts the IRR to 12.3%, but we do not recommend it given the NOI trend.")
N['returns_sensitivity'] = (
"Base case at $22,750,000: levered IRR 11.0%, equity multiple 1.87x, total profit $8,294,267, and average cash-on-cash 6.9%, all on equity at close of $9,555,000. Unlevered IRR is 8.3%, average DSCR 1.79x, exit value $26,501,392, and net sale proceeds $13,223,376. Year-1 cash-on-cash on total equity required ($9,627,100) is 8.3%.\n"
"Stress cases (levered IRR): exit cap +50 bp 9.0%; rent growth -150 bp 4.4%; both combined 2.0%; price +10% 7.1%; LTV +10 pts 12.3%. Only the LTV case clears 11.0%.\n"
"Solved prices (goal-seek on purchase price only):\n"
"- IRR price: $22,750,000 ($110,437/unit, 43.1% below comp-implied value).\n"
"- All-hurdles price: $22,750,000, bound by levered IRR. Equity multiple would allow about $25.1M, Year-1 cash-on-cash $28.2M, and debt yield $32.4M.\n"
"- Approve price (combined stress clears 11.0%): $18,720,000 ($90,874/unit, 53.2% below comp-implied), with a base levered IRR of 18.6% and a 2.71x multiple.\n"
"Returns fall steeply with price. At the 5.48% market cap-rate price (about $31.9M) the base-case levered IRR is negative (-4.5%, 0.75x).")
N['risks_mitigants'] = (
"- Price vs. market: our bid is 43.1% below comp-implied value, and the market is pricing this asset on rent growth we cannot support. Mitigant: hold the price discipline and walk away if the gap cannot be closed.\n"
"- Stress cases below hurdle: exit cap +50 bp (9.0%), rent growth -150 bp (4.4%), combined (2.0%), price +10% (7.1%). A -0.5% rent path is close to the forecast engine's base case (-0.65%/yr, LOW confidence), so the downside is realistic.\n"
"- Tenancy credit: the average resident FICO is 614, median household income is $50,582 (74.5% of households earn $35-50K), 59.9% have net worth under $25K, and the past-due rate is 14.05%, up 5.66 pts in 12 months. Expect bad debt above the T12's 3.4% vacancy and credit loss. Mitigant: require the seller's aged receivables and a bad-debt history.\n"
"- Rent headroom: rent-to-income is 25.1% today. A 30% ceiling on median income implies about $1,265/month, roughly 13% above in-place rent. That caps mark-to-market and renovation premiums.\n"
"- T12 built from fallbacks: insurance, R&M, management, payroll, G&A, marketing and other expenses come from Arapahoe County benchmarks (n=188). Utilities and other income are blank; the county other-income benchmark (85.8% of net rent) was rejected as implausible. Insurance at $486/unit is likely light for Denver hail exposure. Mitigant: seller T12 and a quote.\n"
"- Property tax: the $110,830 bill reflects a $1.43M assessed value. Colorado's biennial reappraisal could lift it after a sale is recorded.\n"
"- Exit cap (6.25%) is below the going-in cap (7.7%). This is intentional (a below-market entry) and not a compression bet.\n"
"- Vintage: a 1976 frame building with no recorded renovation and only $350/unit of reserves. Mitigant: a PCA before LOI.")
N['conditions_next_steps'] = (
"- Pricing condition: bid no more than $22,750,000 (the all-hurdles price, bound by levered IRR). This is 43.1% below the $40,012,063 comp-implied value, so it is probably not achievable against market pricing; treat it as a disciplined bid, not an expected clearing price.\n"
"- To Approve outright: $18,720,000, or diligence evidence that lifts NOI.\n"
"- Obtain the seller T12, rent roll, aged receivables and utility bills. Re-underwrite other income, utilities, insurance and bad debt from actuals.\n"
"- Order a PCA, insurance quote, and a property-tax reassessment estimate.\n"
"- Confirm a Freddie/Fannie term sheet at 60% LTV with 2 years IO.\n"
"- Confirm Prepared By and the recommendation before IC.")
N['sources_provenance'] = (
"- Purchase price $22,750,000: ai-estimate (goal-seek to IC hurdles; the user asked us to suggest a price). LTV 60%, 7-yr hold, 7-yr term, IO then amortizing, 2-yr IO, 30-yr amortization: user-provided.\n"
"- Property facts, GPR, vacancy loss, taxes, occupancy, rents, tenant profile: property data (RealAI datamart, as of 2026-09-26).\n"
"- Insurance, R&M, management, payroll, G&A, marketing, other expense: Arapahoe County P&L benchmarks x EGI (county benchmark, n=188). Utilities and other income: blank (no qualifying source).\n"
"- Stabilized occupancy 95.0%: property data (95.15% physical), capped at 0.95 per house rule.\n"
"- Rent growth 1.0%: ai-estimate. Forecast engine base -0.65%/yr, LOW confidence, flags 'boom extrapolation suspected' and 'wide scenario divergence'; overridden by judgment. Other income growth = rent growth.\n"
"- Expense growth 3.35%: forecast engine (CPI proxy, base-case CAGR, LOW confidence, flag 'thin or absent peer cohort; mean reversion not applied').\n"
"- Exit cap 6.25%: ai-estimate (Denver MF cap 5.48% + about 75 bp for vintage and the rate environment). Interest rate 6.11%: Freddie Mac CME 7-yr, 65% LTV. Closing and disposition 2.0%, reserves $350/unit: default.\n"
"- Rent and sales comps: RealAI datamart; buyer names from web research where found; sales-comp adjustments and weights: ai-estimate. Protected defaults (no mezz, no promote, no staged inputs, house hurdles) unchanged.")
if __name__ == '__main__':
F = sys.argv[1]
wb = openpyxl.load_workbook(F); m = wb['IC Memo']
assert not (isinstance(m['C7'].value, str) and m['C7'].value.startswith('='))
m['C7'].value = 'Approve with Conditions'; wb.save(F)
write_narratives(F, N)
for k, t in N.items(): print(k, len(t))
cd /vercel/sandbox && sed -i 's/within 3.0 miles give/within about 3 miles give/' narratives.py && F=Golden_Nugget_Apartments_CentrePoint_Underwriting_2026-09-28.xlsx && python narratives.py $F && python skills/xlsx/scripts/recalc.py $F && python validate.py $F && python -c "
import narratives as n
print(n.ensure_memo_logo('$F'))
" && python skills/xlsx/scripts/recalc.py $F >/dev/null && python -c "
import narratives as n
n.export_memo_pdf('$F','Golden_Nugget_Apartments_IC_Memo_2026-09-28.pdf')
" && pdfinfo Golden_Nugget_Apartments_IC_Memo_2026-09-28.pdf | grep Pages && pdftoppm -r 50 -png Golden_Nugget_Apartments_IC_Memo_2026-09-28.pdf pg && ls pg*cd /vercel/sandbox && pdftoppm -r 70 -png -f 1 -l 1 Golden_Nugget_Apartments_IC_Memo_2026-09-28.pdf p1 && pdftotext -layout Golden_Nugget_Apartments_IC_Memo_2026-09-28.pdf - | head -80
My suggested offer is a maximum of $22,750,000 ($110,437/unit). That is the highest price at which the deal clears all five of CentrePoint's hurdles. The recommendation is a proposal of Approve with Conditions. The deciding metric is levered IRR, and it is also the hurdle that sets the price. Two things to know up front:
No asking price was given, so I've put the workbook at this price. You can change the purchase price (Assumptions!C15) if you want to test a different bid.
| Metric | Actual | Hurdle |
|---|---|---|
| Levered IRR | 11.0% | 11.0% |
| Equity multiple | 1.87x | 1.50x |
| Going-in DSCR | 2.04x | 1.25x |
| Debt yield | 12.8% | 9.0% |
| Year-1 cash-on-cash | 8.3% | 5.0% |
| Target | Price | $/unit | vs. comp-implied value | Levered IRR / EM / DSCR / debt yield |
|---|---|---|---|---|
| IRR price | $22,750,000 | $110,437 | −43.1% | 11.0% / 1.87x / 2.04x / 12.8% |
| All-hurdles price (set by IRR) | $22,750,000 | $110,437 | −43.1% | same |
| Approve price (combined stress still ≥11%) | $18,720,000 | $90,874 | −53.2% | 18.6% / 2.71x / 2.48x / 15.6% |
The prices come from solving for the maximum purchase price in the model . The other hurdles would allow higher prices, so they don't constrain the bid: equity multiple about $25.1M, cash-on-cash $28.2M, debt yield $32.4M. At the Denver market cap rate of 5.48% (a price of about $31.9M), the levered IRR is −4.5% .
1. There isn't much room for rent growth.
2. The tenant base limits rent increases and adds collection risk.
3. Most of the operating statement is benchmarks, not actuals.
| Assumption | Value | Source |
|---|---|---|
| Purchase price | $22,750,000 | ai-estimate (solved to hurdles) |
| LTV / hold / loan term / structure | 60% / 7 yrs / 7 yrs / 2 yrs interest-only, then 30-yr amortization | user-provided |
| Interest rate | 6.11% | Freddie Mac 7-yr fixed at 65% LTV |
| Rent growth / other income growth | 1.0% / 1.0% | ai-estimate (forecast engine said −0.65%, LOW confidence) |
| Expense growth | 3.35% | forecast engine (inflation proxy) |
| Stabilized occupancy | 95.0% | property data (95.15% physical, capped at 95%) |
| Exit cap | 6.25% | ai-estimate (Denver 5.48% plus about 75 bp) |
| Reserves / closing costs / disposition costs | $350/unit / 2% / 2% | default |
Other points:
Please confirm the recommendation (Approve with Conditions) or change it. If you change it, I'll update the memo and re-export it.
Files are in your library:
This is a read-only snapshot of a RealAI analysis.