A sponsor is requesting acquisition financing at 70% LTV for The Modern Apartments in San Diego. Stress test the deal: what happens to DSCR if rent growth comes in at the bear-case forecast and exit cap rates widen 50 bps?
Found it — id `80d05e54ab2a7d65d995d49dd30ef223`, 3201 Annrae St, San Diego 92123. Let me pull the full pre-fill set.
The sponsor's request and the collateral are enough to pre-fill this — but a stress/scenario ask like this always gets one form so you can correct terms and attach documents before I run the numbers.
I found The Modern Apartments (3201 Annrae St, San Diego 92123 — 369 units, built 1959/renovated 2020) and pulled its in-place financials and current agency quotes to pre-fill the loan terms below. If you have an actual term sheet or T12, attach it — the loan terms below are current market quotes, not necessarily what's being offered.
mkdir -p agent_files && ls agents/01a087be-a40c-70fb-88a9-40a61b0703c2/ 2>/dev/null || echo "not found"
cd /vercel/sandbox && python skills/forecasting/scripts/forecast.py --version
cd /vercel/sandbox && cat > rent_payload.json << 'EOF'
{
"metric": "in_place_rent",
"family": "rent_or_occupancy",
"geography_level": "market",
"geography_name": "San Diego, CA (CBSA 41740)",
"as_of": "2026-09-09",
"history": [
{"period": "2024-07", "value": 2729.0},
{"period": "2024-08", "value": 2738.0},
{"period": "2024-09", "value": 2745.0},
{"period": "2024-10", "value": 2747.0},
{"period": "2024-11", "value": 2746.0},
{"period": "2024-12", "value": 2745.0},
{"period": "2025-01", "value": 2740.0},
{"period": "2025-02", "value": 2745.0},
{"period": "2025-03", "value": 2747.0},
{"period": "2025-04", "value": 2747.0},
{"period": "2025-05", "value": 2747.0},
{"period": "2025-06", "value": 2749.0},
{"period": "2025-07", "value": 2750.0},
{"period": "2025-08", "value": 2750.0},
{"period": "2025-09", "value": 2750.0},
{"period": "2025-10", "value": 2745.0},
{"period": "2025-11", "value": 2741.0},
{"period": "2025-12", "value": 2745.0},
{"period": "2026-01", "value": 2744.0},
{"period": "2026-02", "value": 2745.0},
{"period": "2026-03", "value": 2746.0},
{"period": "2026-04", "value": 2750.0},
{"period": "2026-05", "value": 2751.0},
{"period": "2026-06", "value": 2759.0},
{"period": "2026-07", "value": 2771.0},
{"period": "2026-08", "value": 2778.0}
],
"history_check": {"count": 26, "last_value": 2778.0},
"requested_lookback_years": 2.2,
"horizon_years": 10,
"caller_disclosures": {
"signals_omitted": ["supply_pipeline", "employment", "migration"],
"peer_selection_basis": "no peer series supplied; single-market history only"
}
}
EOF
python skills/forecasting/scripts/forecast.py --file rent_payload.json > rent_forecast.json
cat rent_forecast.jsoncd /vercel/sandbox && sed -n '1,250p' skills/forecasting/references/request.md
cd /vercel/sandbox && cat > rent_payload.json << 'EOF'
{
"metric": {"name": "mf_rent_ts.in_place_rent_latest_median", "units": "$", "family": "rent_or_occupancy"},
"subject": {"entity_type": "market", "entity_id": "41740", "label": "San Diego, CA (CBSA 41740)"},
"horizon": {"years": 10, "intervals": "annual"},
"as_of": "2026-09-09",
"requested_lookback_years": 2.2,
"history": [
{"period": "2024-07-01", "value": 2729.0, "source": "datamart"},
{"period": "2024-08-01", "value": 2738.0, "source": "datamart"},
{"period": "2024-09-01", "value": 2745.0, "source": "datamart"},
{"period": "2024-10-01", "value": 2747.0, "source": "datamart"},
{"period": "2024-11-01", "value": 2746.0, "source": "datamart"},
{"period": "2024-12-01", "value": 2745.0, "source": "datamart"},
{"period": "2025-01-01", "value": 2740.0, "source": "datamart"},
{"period": "2025-02-01", "value": 2745.0, "source": "datamart"},
{"period": "2025-03-01", "value": 2747.0, "source": "datamart"},
{"period": "2025-04-01", "value": 2747.0, "source": "datamart"},
{"period": "2025-05-01", "value": 2747.0, "source": "datamart"},
{"period": "2025-06-01", "value": 2749.0, "source": "datamart"},
{"period": "2025-07-01", "value": 2750.0, "source": "datamart"},
{"period": "2025-08-01", "value": 2750.0, "source": "datamart"},
{"period": "2025-09-01", "value": 2750.0, "source": "datamart"},
{"period": "2025-10-01", "value": 2745.0, "source": "datamart"},
{"period": "2025-11-01", "value": 2741.0, "source": "datamart"},
{"period": "2025-12-01", "value": 2745.0, "source": "datamart"},
{"period": "2026-01-01", "value": 2744.0, "source": "datamart"},
{"period": "2026-02-01", "value": 2745.0, "source": "datamart"},
{"period": "2026-03-01", "value": 2746.0, "source": "datamart"},
{"period": "2026-04-01", "value": 2750.0, "source": "datamart"},
{"period": "2026-05-01", "value": 2751.0, "source": "datamart"},
{"period": "2026-06-01", "value": 2759.0, "source": "datamart"},
{"period": "2026-07-01", "value": 2771.0, "source": "datamart"},
{"period": "2026-08-01", "value": 2778.0, "source": "datamart"}
],
"history_check": {"count": 26, "sum": 71455.0, "last_value": 2778.0},
"caller_disclosures": {
"peer_selection_basis": "no peer series supplied; single-market history only, San Diego CBSA is the subject's own market",
"signals_omitted": [
{"signal": "supply_pipeline", "reason": "not gathered"},
{"signal": "migration", "reason": "not gathered"},
{"signal": "employment", "reason": "not gathered"}
],
"lookback_note": "full available monthly history (26 months, Jul-24 through Aug-26); market series starts 2024-07"
},
"scenarios": ["base", "upside", "downside"]
}
EOF
python skills/forecasting/scripts/forecast.py --file rent_payload.json > rent_forecast.json
cat rent_forecast.jsoncd /vercel/sandbox && python3 -c "
import json
d=json.load(open('rent_payload.json'))
vals=[h['value'] for h in d['history']]
print(len(vals), sum(vals))
"
cd /vercel/sandbox && sed -i 's/"sum": 71455.0/"sum": 71450.0/' rent_payload.json python skills/forecasting/scripts/forecast.py --file rent_payload.json > rent_forecast.json cat rent_forecast.json
cd /vercel/sandbox && pip show openpyxl >/dev/null 2>&1 || pip install openpyxl -q
python3 << 'EOF'
import openpyxl
wb = openpyxl.load_workbook('agents/01a087be-a40c-70fb-88a9-40a61b0703c2/RealAI_Credit_Underwriting_Template_v3_clean.xlsx', data_only=False)
print(wb.sheetnames)
ws = wb['Assumptions']
for row in range(1, 60):
for col in ['B','C','E','F']:
cell = ws[f'{col}{row}']
if cell.value is not None:
print(f'{col}{row}: {repr(cell.value)}')
EOFcd /vercel/sandbox && python3 << 'EOF'
import json
d = json.load(open('rent_forecast.json'))
base = [x['pct_change'] for x in d['base_case']]
down = [x['pct_change'] for x in d['downside_case']]
up = [x['pct_change'] for x in d['upside_case']]
print("base avg", sum(base)/len(base))
print("down avg", sum(down)/len(down))
print("up avg", sum(up)/len(up))
# 10-yr term average matches loan term used later (Fannie 10yr)
EOFcd /vercel/sandbox && python3 << 'EOF'
import openpyxl
wb = openpyxl.load_workbook('agents/01a087be-a40c-70fb-88a9-40a61b0703c2/RealAI_Credit_Underwriting_Template_v3_clean.xlsx', data_only=False)
ws = wb['Assumptions']
for dv in ws.data_validations.dataValidation:
print(dv.sqref, dv.formula1)
EOFcd /vercel/sandbox && python3 << 'EOF'
import openpyxl
wb = openpyxl.load_workbook('agents/01a087be-a40c-70fb-88a9-40a61b0703c2/RealAI_Credit_Underwriting_Template_v3_clean.xlsx', data_only=False)
ws = wb['Pro Forma']
for row in range(1, 55):
vals=[]
for col in ['A','B','C','D','E','F','G','H','I','J']:
c=ws[f'{col}{row}']
if c.value is not None:
vals.append(f'{col}{row}={repr(c.value)}')
if vals: print(' | '.join(vals))
EOFcd /vercel/sandbox && python3 << 'EOF'
import openpyxl
wb = openpyxl.load_workbook('agents/01a087be-a40c-70fb-88a9-40a61b0703c2/RealAI_Credit_Underwriting_Template_v3_clean.xlsx', data_only=False)
ws = wb['Assumptions']
for addr in ['C14','C9','C10','C13','F8','F9','F10','F11','F12','F14','F18','F19','F20','F21','F22','F24','F25','C28','C29','C30','C31','F29','F30','F32','F33','F36','F37','F38','C34','C35','C36','C37','C38','C41','C18','C19','C20','F47','F55','F56','F57','F58']:
c = ws[addr]
print(addr, repr(c.number_format), c.font.color.rgb if c.font and c.font.color else None)
EOFcd /vercel/sandbox && cat > populate.py << 'PYEOF'
import openpyxl, shutil, sys
def populate(src, dst, noi_growth, cap_add_bps, requested_proceeds=None):
shutil.copy(src, dst)
wb = openpyxl.load_workbook(dst)
A = wb['Assumptions']
PF = wb['Pro Forma']
PP = wb['Prepayment']
# Collateral
A['C5'] = "The Modern Apartments"
A['C6'] = "3201 Annrae St"
A['C7'] = "San Diego, CA 92123"
A['C8'] = "Multifamily"
A['C9'] = 369
A['C10'] = 367155
A['C11'] = "Units"
A['C13'] = 1959
A['C14'] = 0.9675
A['C15'] = "T12 Actuals"
# Sponsor & guarantee - not provided
A['C20'] = "No"
# Growth & exit
A['C28'] = noi_growth
A['C29'] = 0 # no organic cap drift; the widening is via refi stress cap add
A['C30'] = 0.02 # selling costs, typical disposition cost estimate
A['C31'] = "Deep" # San Diego core MF sale-exit liquidity
# Takeout terms at maturity (proxy: current agency quote, no forward quote available)
A['C34'] = 0.0621
A['C35'] = 1.25
A['C36'] = 0.75
A['C37'] = 0.095
A['C38'] = 30
A['C41'] = "Deep"
# Loan request
A['F5'] = "Origination"
A['F6'] = "Acquisition-Term"
A['F7'] = "2026-10-01"
A['F8'] = requested_proceeds if requested_proceeds else 1000 # placeholder, updated pass 2
A['F9'] = 0.0621
A['F10'] = 10
A['F11'] = 0
A['F12'] = 30
A['F14'] = 0.01
A['F15'] = "Non-Recourse"
# Credit box
A['F18'] = 0.75
A['F19'] = 1.25
A['F20'] = 1.20
A['F21'] = 0.095
A['F22'] = 0.10
A['F23'] = "user-provided"
A['F24'] = 50
A['F25'] = cap_add_bps
# Market benchmarks
A['F29'] = 0.0727 # Kearny Mesa submarket vacancy (1 - 0.9273)
A['F30'] = 0.05
A['F32'] = 0.4045 # San Diego market total opex ratio (no submarket-level benchmark)
A['F33'] = 0.42
A['F35'] = "Yes"
A['F36'] = 0.0493 # San Diego MF market cap rate, 2Q26
A['F38'] = 3540.36 # Kearny Mesa submarket asking rent avg (market rent proxy)
A['F44'] = "Heavy" # Kearny Mesa has several 400+ unit deliveries in 2026
# Rollover - not applicable, multifamily
A['F47'] = "No"
# Flag thresholds - template defaults
A['F55'] = 0.90
A['F56'] = 0.30
A['F57'] = 0.10
A['F58'] = 0.03
# Pro Forma T12 column (E)
PF['E5'] = 12485572.08 # GPR
PF['E6'] = 482471.36 # Vacancy
PF['E7'] = 0 # Concessions (not broken out)
PF['E8'] = 0 # Bad debt (not broken out)
PF['E9'] = 1067470.15 # Other income
PF['E13'] = 792020.57 # Payroll
PF['E14'] = 297758.65 # R&M
PF['E15'] = 813046.50 # Utilities
PF['E16'] = 0 # Contract services (not broken out)
PF['E17'] = 0 # Turnover & make-ready (not broken out)
PF['E18'] = 213359.88 # Marketing & administrative (advertising + G&A)
PF['E19'] = 326764.27 # Management fee
PF['E20'] = 1306170.80 # RE taxes
PF['E21'] = 320833.83 # Insurance
PF['E22'] = 128677.91 # Replacement reserves (capex)
PF['C39'] = 0.025 # Management fee % of EGI
PF['C40'] = 348.85 # Replacement reserves $/unit/yr (128,677.91/369)
# Prepayment - Treasury curve (anchored to platform 10Y), no assumed exit month
PP['D27'] = 0.0400 # 6mo
PP['D28'] = 0.0418 # 1yr
PP['D29'] = 0.0439 # 2yr
PP['D30'] = 0.0446 # 3yr
PP['D31'] = 0.0455 # 5yr
PP['D32'] = 0.0466 # 7yr
PP['D33'] = 0.0477 # 10yr - platform national_metrics_daily.ten_year_treasury_pct
PP['D34'] = 0.0527 # 20yr
PP['D35'] = 0.0527 # 30yr
PP['C36'] = "2026-09-03"
PP['C8'] = 24 # lockout months
PP['C9'] = 3 # open period months
PP['C10'] = "Yield Maintenance"
wb.save(dst)
if __name__ == "__main__":
scenario = sys.argv[1]
src = "agents/01a087be-a40c-70fb-88a9-40a61b0703c2/RealAI_Credit_Underwriting_Template_v3_clean.xlsx"
if scenario == "base":
populate(src, "base_pass1.xlsx", noi_growth=0.0149, cap_add_bps=0)
elif scenario == "stress":
populate(src, "stress_pass1.xlsx", noi_growth=-0.0258, cap_add_bps=50)
PYEOF
python3 populate.py base
python3 populate.py stress
echo donecd /vercel/sandbox && python skills/xlsx/scripts/recalc.py base_pass1.xlsx 2>&1 | tail -20 python skills/xlsx/scripts/recalc.py stress_pass1.xlsx 2>&1 | tail -20
cd /vercel/sandbox && cp agents/01a087be-a40c-70fb-88a9-40a61b0703c2/RealAI_Credit_Underwriting_Template_v3_clean.xlsx orig_check.xlsx python skills/xlsx/scripts/recalc.py orig_check.xlsx 2>&1 | tail -20
Confirmed pre-existing in the unpopulated template — not something this run introduced. Proceeding.
cd /vercel/sandbox && python3 << 'EOF'
import openpyxl
for f in ['base_pass1.xlsx','stress_pass1.xlsx']:
wb = openpyxl.load_workbook(f, data_only=True)
PF = wb['Pro Forma']
print(f, "Lender NOI G28=", PF['G28'].value, "Lender Value C44=", PF['C44'].value)
EOFcd /vercel/sandbox && python3 << 'EOF' lender_value = 147512872.056282 proceeds = round(0.70 * lender_value) print(proceeds) EOF
cd /vercel/sandbox && python3 << 'EOF'
import openpyxl
for f in ['base_pass1.xlsx','stress_pass1.xlsx']:
wb = openpyxl.load_workbook(f)
A = wb['Assumptions']
A['F8'] = 103259010
wb.save(f)
EOF
cp base_pass1.xlsx "The Modern Apartments_Credit_UW_2026-09-09_v1.xlsx"
cp stress_pass1.xlsx "The Modern Apartments_Credit_UW_Bear_Stress_2026-09-09_v1.xlsx"
python skills/xlsx/scripts/recalc.py "The Modern Apartments_Credit_UW_2026-09-09_v1.xlsx" 2>&1 | tail -20
python skills/xlsx/scripts/recalc.py "The Modern Apartments_Credit_UW_Bear_Stress_2026-09-09_v1.xlsx" 2>&1 | tail -20Now let's read back all the mapped outputs and integrity checks for both scenarios.
cd /vercel/sandbox && python3 << 'EOF'
import openpyxl, json
def readback(f):
wb = openpyxl.load_workbook(f, data_only=True)
A = wb['Assumptions']; PF = wb['Pro Forma']; LS = wb['Loan Sizing']
AM = wb['Amortization']; RA = wb['Refi Analysis']; SB = wb['Stress & Break-Even']
REC = wb['Recovery']
out = {}
out['lender_noi'] = PF['G28'].value
out['lender_value'] = PF['C44'].value
out['noi_haircut_pct'] = PF['G30'].value
out['dscr_at_request'] = LS['C17'].value
out['debt_yield_at_request'] = LS['C18'].value
out['ltv_at_request'] = LS['C19'].value
out['supportable_proceeds'] = LS['C29'].value
out['binding_constraint'] = LS['C30'].value
out['gap_vs_request'] = LS['C35'].value
out['balance_at_maturity'] = AM['N4'].value
out['noi_at_maturity'] = RA['C21'].value
out['stressed_rate'] = RA['C22'].value if RA['C22'].value else None
out['stressed_cap'] = RA['C23'].value if RA['C23'].value else None
out['stressed_refi_proceeds'] = RA['C28'].value
out['refi_gap'] = RA['C30'].value
out['refi_gap_pct'] = RA['C31'].value
out['refi_result'] = RA['C33'].value
out['noi_breakeven'] = SB['C18'].value
out['noi_cushion_pct'] = SB['C21'].value
out['breakeven_occ'] = SB['C23'].value
out['breakeven_rent'] = SB['C24'].value
out['recovery_pct_neg20'] = REC['E14'].value
# annual DSCR grid D4:S16 - year row 8 is governing NOI presumably; let's grab whole grid labels
grid = {}
for row in range(4, 17):
label = RA[f'B{row}'].value or RA[f'C{row}'].value
vals = [RA.cell(row=row, column=c).value for c in range(4, 20)]
grid[row] = (label, vals)
out['refi_grid_rowlabels'] = {k: v[0] for k,v in grid.items()}
return out, grid
for f in ["The Modern Apartments_Credit_UW_2026-09-09_v1.xlsx", "The Modern Apartments_Credit_UW_Bear_Stress_2026-09-09_v1.xlsx"]:
out, grid = readback(f)
print("=====", f)
print(json.dumps(out, indent=2, default=str))
EOFcd /vercel/sandbox && python3 << 'EOF'
import openpyxl, json
def grid(f):
wb = openpyxl.load_workbook(f, data_only=True)
RA = wb['Refi Analysis']
years = [RA.cell(row=5, column=c).value for c in range(4, 14)]
dscr = [RA.cell(row=11, column=c).value for c in range(4, 14)]
noi = [RA.cell(row=8, column=c).value for c in range(4, 14)]
return years, dscr, noi
for f in ["The Modern Apartments_Credit_UW_2026-09-09_v1.xlsx", "The Modern Apartments_Credit_UW_Bear_Stress_2026-09-09_v1.xlsx"]:
y,d,n = grid(f)
print(f)
print("years", y)
print("dscr", [round(x,3) if x else x for x in d])
print("noi", [round(x) if x else x for x in n])
print()
# Also read integrity checks
def checks(f):
wb = openpyxl.load_workbook(f, data_only=True)
A = wb['Assumptions']
res = {}
for row in range(23,48):
label = A[f'H{row}'].value
val = A[f'I{row}'].value
if label:
res[row] = (label, val)
return res
for f in ["The Modern Apartments_Credit_UW_2026-09-09_v1.xlsx", "The Modern Apartments_Credit_UW_Bear_Stress_2026-09-09_v1.xlsx"]:
print("=====", f)
for k,v in checks(f).items():
print(k, v)
EOFcd /vercel/sandbox && python3 << 'EOF'
import openpyxl
wb = openpyxl.load_workbook("The Modern Apartments_Credit_UW_2026-09-09_v1.xlsx", data_only=True)
LS = wb['Loan Sizing']
for addr,label in [('C36','counter-offer'),('C37','reduction $'),('C38','reduction %'),('C39','DSCR@counter'),('C40','DY@counter'),('C41','LTV@counter'),('C42','coverage position'),('C43','amort to hold floor')]:
print(label, addr, LS[addr].value)
REC = wb['Recovery']
for r in range(5,25):
vals = [REC.cell(row=r,column=c).value for c in range(2,7)]
if any(v is not None for v in vals):
print(r, vals)
EOFcd /vercel/sandbox && python3 << 'EOF'
import openpyxl
wb = openpyxl.load_workbook("The Modern Apartments_Credit_UW_2026-09-09_v1.xlsx", data_only=True)
A = wb['Assumptions']; PF = wb['Pro Forma']
print("Vacancy floor applied F31:", A['F31'].value)
print("OpEx ratio applied F34:", A['F34'].value)
print("T12 vacancy rate E32:", PF['E32'].value)
print("Lender vacancy $ G6:", PF['G6'].value)
print("T12 NOI E28:", PF['E28'].value)
print("Lender OpEx total G25:", PF['G25'].value)
print("T12 OpEx E23:", PF['E23'].value)
print("Verdict cap C21:", A['C21'].value)
EOFcd /vercel/sandbox && python skills/xlsx/scripts/workbook_integrity_scan.py "The Modern Apartments_Credit_UW_2026-09-09_v1.xlsx" 2>&1 | tail -40; echo "EXIT: $?"
cd /vercel/sandbox && python3 << 'EOF'
import openpyxl
wb = openpyxl.load_workbook('agents/01a087be-a40c-70fb-88a9-40a61b0703c2/RealAI_Credit_Underwriting_Template_v3_clean.xlsx')
A = wb['Assumptions']
for addr in ['C47','C48','C49','C50','C51','C52','C53','C54','C55','C56']:
print(addr, A[addr].value)
EOFcd /vercel/sandbox && python3 << 'EOF'
import openpyxl
wb = openpyxl.load_workbook("The Modern Apartments_Credit_UW_2026-09-09_v1.xlsx")
A = wb['Assumptions']
for addr in ['C47','C48','C49','C51','C52','C53','C54']:
print(addr, A[addr].value)
REC = wb['Recovery']
print("C11 formula:", REC['C11'].value)
print("C6 formula:", REC['C6'].value)
EOFcd /vercel/sandbox && python skills/xlsx/scripts/workbook_integrity_scan.py "The Modern Apartments_Credit_UW_2026-09-09_v1.xlsx" --json scan1.json 2>&1 | tail -5
python3 -c "
import json
d = json.load(open('scan1.json'))
highs = [f for f in d['findings'] if f['severity']=='high']
print(len(highs))
cats = {}
for f in highs:
cats.setdefault(f['category'], []).append(f)
for c, items in cats.items():
print(c, len(items))
for it in items[:3]:
print(' ', it.get('sheet'), it.get('cell'), it.get('references'), it.get('reason','')[:150])
"
cd /vercel/sandbox && python3 << 'EOF'
import openpyxl
for f in ["The Modern Apartments_Credit_UW_2026-09-09_v1.xlsx", "The Modern Apartments_Credit_UW_Bear_Stress_2026-09-09_v1.xlsx"]:
wb = openpyxl.load_workbook(f)
A = wb['Assumptions']
A['C53'] = 0.05 # Foreclosure & legal, % of value - standard MF distressed-sale convention
A['C54'] = 12 # Workout carry, months - standard convention
wb.save(f)
EOF
python skills/xlsx/scripts/recalc.py "The Modern Apartments_Credit_UW_2026-09-09_v1.xlsx" 2>&1 | tail -10
python skills/xlsx/scripts/recalc.py "The Modern Apartments_Credit_UW_Bear_Stress_2026-09-09_v1.xlsx" 2>&1 | tail -10Now let's re-read the updated recovery figures and finalize.
cd /vercel/sandbox && python3 << 'EOF'
import openpyxl
for f in ["The Modern Apartments_Credit_UW_2026-09-09_v1.xlsx", "The Modern Apartments_Credit_UW_Bear_Stress_2026-09-09_v1.xlsx"]:
wb = openpyxl.load_workbook(f, data_only=True)
REC = wb['Recovery']
print(f, "Recovery%Par@-20%:", REC['E14'].value, "LossSeverity@-20:", REC['E16'].value)
EOFGood, unchanged (foreclosure costs don't move the recovery ratio at this cushion level). Now let's save both workbooks to the library and register everything.
DECLINE at the requested $103.3M / 70% LTV — restructure to ≈$76.6M (52% LTV) to clear. The debt yield test binds, not a market assumption: at Day 1, before any rent-growth or cap-rate stress is applied, this loan covers debt service at only 0.96x , already below breakeven. Layering in the bear-case rent forecast and a 50 bps exit-cap widening doesn't create the problem — it turns a thin, deteriorating position into an outright refinance failure at maturity.
Acquisition financing on The Modern Apartments, 369 units in San Diego's Kearny Mesa submarket , at 70% LTV — sized here against a lender value of $147.5M , producing a request of $103.26M at 6.21% (Fannie Mae Conventional, 10-yr, 75% LTV tier, quoted 9/2/26), 30-year amortization, non-recourse.
| Line | Value |
|---|---|
| Lender-case NOI (vs. T12 $9.00M) | $7.27M — an 18.0% haircut |
| DSCR at request (Day 1) | 0.96x — below the 1.25x floor and below 1.00x |
| Debt yield at request | 7.04% vs. 9.5% floor |
| Supportable proceeds (binding: Debt Yield Test) | $76.55M (52% LTV) |
| Gap vs. request | ($26.71M) |
The haircut is real, not conservative modeling: Kearny Mesa's submarket vacancy runs 7.27% — nearly double this property's T12 actual vacancy of 3.9% — and the submarket is absorbing a heavy 2026 delivery wave (Broadstone Soren, Alexan Camellia, Centrum Phase 6 — several 400+ unit projects within the submarket) , so the higher vacancy floor is the right lens, not a haircut for haircut's sake. At the $76.55M counter-offer, DSCR recovers to 1.29x and debt yield lands exactly at the 9.5% floor — that's the number a lender can actually support here.
I ran the RealAI forecasting engine off 26 months of San Diego in-place rent history: base-case rent growth averages +1.5%/yr over a 10-year hold, bear-case averages -2.6%/yr — the engine flags the read as low-confidence because the whole history sits inside one post-2020 regime with no multi-cycle anchor . Run through the model as the NOI growth assumption, alongside the requested 50 bps exit-cap widening at the takeout:
The rent-growth assumption is the swing factor — Day-1 basis and the balance schedule don't move with it, but the entire maturity outcome flips from "clears by a hair" to "$29M short" on that one input.
Recovery cushion is deep on value alone — even at a 30% value decline, net recovery proceeds ($101.2M) still exceed the workout balance, so loss severity is 0% and lender recovers 100% of the outstanding balance . But recovery as a % of the original $103.26M par is only 84.2% , below the 90% threshold — a function of how little the loan amortizes by year 10, not of the rent scenario. Basis cushion (30%) protects the lender; it doesn't protect the sponsor's equity.
At 70% LTV / $103.26M this fails on Day 1 — decline as requested. The sponsor's real ask should be $76.55M, where the deal clears the debt yield floor with a 1.29x DSCR. Even there, don't take comfort in the base case's refinance math: it clears by 15 basis points of surplus, and the bear-case rent forecast alone is enough to turn that into a $29M shortfall. The single diligence item most likely to change this verdict: a signed rent roll showing in-place rents already tracking above the submarket's stressed vacancy assumption, which would tighten the vacancy floor and lift lender NOI closer to the T12 figure.
This is a read-only snapshot of a RealAI analysis.