import React, { useMemo, useState } from "react"; // ---- Utility helpers ---- const fmt = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 }); const pct = (n) => `${((isFinite(n) ? n : 0) * 100).toFixed(1)}%`; const num = (v, d=4) => (isFinite(v) ? Number(v.toFixed(d)) : 0); function pmt(rateAnnual, years, principal) { const r = rateAnnual / 12; const n = years * 12; if (r === 0) return principal / n; return (r * principal) / (1 - Math.pow(1 + r, -n)); } // ---- Component ---- export default function DealOptionsCalculator() { const [inputs, setInputs] = useState({ rent: 2500, piti: 1800, // underlying existing PITI for Subto / Wrap arv: 350000, price: 280000, rehab: 15000, hoaMonthly: 0, arrearsOwed: 0, // one-time reinstatement/arrears loanBalance: 0, // optional total mortgage payoff listingType: 'direct', // 'direct' | 'agent' isWholesale: false, assignmentFee: 10000, }); const [thresholds, setThresholds] = useState({ minCF: 300, // monthly minCOC: 0.15, // 15% minCap: 0.08, // 8% minFlipProfit: 20000, minFlipMargin: 0.10, // 10% minAssignmentFee: 5000, hideFails: false, }); const [adv, setAdv] = useState({ // Global assumptions vacancyRate: 0.05, mgmtRate: 0.08, maintenanceRate: 0.05, capexRate: 0.05, otherOpexRate: 0.00, fixedMonthlyOpex: 0, // taxes/ins, utilities, etc (ex-debt) buyClosingPct: 0.03, acqAgentFeePct: 0.00, // extra buy-side fee if listed w/ agent (set to e.g. 3%) sellClosingPct: 0.08, // agent + closing + concessions on resale holdMonths: 4, holdMonthlyBurn: 500, // Seller Finance sfDownPct: 0.10, sfRate: 0.055, sfTermYears: 30, // Subto subtoUpfront: 10000, // Hybrid (Subto + 2nd) underlyingLTV: 0.80, // assumes loan balance ≈ price * LTV if no explicit loanBalance secondRate: 0.08, secondTermYears: 30, // Wrap Exit (to end buyer) wrapPricePctOfARV: 1.00, // sell at 100% of ARV by default wrapDownPct: 0.10, wrapRate: 0.095, wrapTermYears: 30, wrapAssignmentFee: 0, // optional fee on top of buyer downpayment // STR (Short-Term Rental) strRevenueUplift: 1.8, // vs LTR rent strExpenseRate: 0.50, // % of STR revenue (incl. cleaning, utilities) strMgmtRate: 0.20, // professional STR manager }); const onChange = (k, v) => setInputs((s) => ({ ...s, [k]: typeof v === 'boolean' ? v : Number(v) || 0 })); const onSet = (k, v) => setInputs((s) => ({ ...s, [k]: v })); const onThreshold = (k, v, mult=1) => setThresholds((s) => ({ ...s, [k]: typeof v === 'boolean' ? v : Number(v) * mult })); const onAdv = (k, v, mult=1) => setAdv((s) => ({ ...s, [k]: Number(v) * mult })); const U = useMemo(() => { const rent = Math.max(inputs.rent, 0); const piti = Math.max(inputs.piti, 0); const arv = Math.max(inputs.arv, 0); const price = Math.max(inputs.price, 0); const rehab = Math.max(inputs.rehab, 0); const hoaMonthly = Math.max(inputs.hoaMonthly, 0); const arrearsOwed = Math.max(inputs.arrearsOwed, 0); const loanBalance = Math.max(inputs.loanBalance, 0); const listingType = inputs.listingType; const isWholesale = inputs.isWholesale; const assignmentFeePlanned = Math.max(inputs.assignmentFee, 0); // buy costs affected by listing type const buyCosts = price * adv.buyClosingPct + (listingType === 'agent' ? price * adv.acqAgentFeePct : 0); const basisCash = price + rehab + buyCosts; // cash purchase basis (your total invested at closing for cash buy) // --- Standardize NOI (monthly, pre-debt) --- const operRate = adv.mgmtRate + adv.maintenanceRate + adv.capexRate + adv.otherOpexRate; const egiMonthly = rent * (1 - adv.vacancyRate); // Effective Gross Income (after vacancy) const varOpexMonthly = egiMonthly * operRate; // % of EGI const fixedMonthly = adv.fixedMonthlyOpex + hoaMonthly; // taxes, insurance, HOA, utilities etc. const noiMonthly = Math.max(egiMonthly - varOpexMonthly - fixedMonthly, 0); const noiAnnual = noiMonthly * 12; const capRateAnnual = basisCash > 0 ? noiAnnual / basisCash : 0; // same cap rate across pre-debt scenarios function rentalMetrics(monthlyDebt) { const cashflowMonthly = noiMonthly - monthlyDebt; // CF after expenses and debt return { cashflowMonthly, capRate: capRateAnnual }; } // Gate cash offers if loan balance provided and exceeds offer const cashOfferCoversLoan = loanBalance === 0 || price >= loanBalance; // ---- Strategy calcs ---- // 0) WHOLESALE (optional card) const wholesale = { fee: assignmentFeePlanned, pass: isWholesale && assignmentFeePlanned >= thresholds.minAssignmentFee }; // 1) CASH FLIP const flipCashIn = basisCash + adv.holdMonths * adv.holdMonthlyBurn; // total cash tied up const flipSaleCosts = arv * adv.sellClosingPct; const flipCosts = flipCashIn + flipSaleCosts; const flipProfit = (cashOfferCoversLoan ? (arv - flipCosts) : Number.NEGATIVE_INFINITY); const flipMargin = cashOfferCoversLoan && arv > 0 ? (flipProfit / arv) : 0; // profit as % of resale price const flipROI = cashOfferCoversLoan && flipCashIn > 0 ? (flipProfit / flipCashIn) : 0; // profit vs cash invested const flipPass = cashOfferCoversLoan && (flipProfit >= thresholds.minFlipProfit) && (flipMargin >= thresholds.minFlipMargin); // 2) CASH RENTAL (no debt) const rentCash = rentalMetrics(0); const cocCash = basisCash > 0 ? (rentCash.cashflowMonthly * 12) / basisCash : 0; // annual CF / cash invested const rentCashPass = cashOfferCoversLoan && (rentCash.cashflowMonthly >= thresholds.minCF) && (cocCash >= thresholds.minCOC) && (capRateAnnual >= thresholds.minCap); // 3) SUBTO (use PITI as monthly debt); cash in = subtoUpfront + rehab + closing + arrears const subtoCashIn = adv.subtoUpfront + rehab + buyCosts + arrearsOwed; const subtoCF = noiMonthly - piti; const subtoCOC = subtoCashIn > 0 ? (subtoCF * 12) / subtoCashIn : 0; const subtoPass = (subtoCF >= thresholds.minCF) && (subtoCOC >= thresholds.minCOC) && (capRateAnnual >= thresholds.minCap); // 4) SELLER FINANCE const sfDown = price * adv.sfDownPct; const sfPrincipal = Math.max(price - sfDown, 0); const sfPayment = pmt(adv.sfRate, adv.sfTermYears, sfPrincipal); const sfCF = noiMonthly - sfPayment; const sfCashIn = sfDown + rehab + buyCosts; // arrears generally not relevant when paying seller-financed equity; adjust if needed const sfCOC = sfCashIn > 0 ? (sfCF * 12) / sfCashIn : 0; const sfPass = (sfCF >= thresholds.minCF) && (sfCOC >= thresholds.minCOC) && (capRateAnnual >= thresholds.minCap); // 5) HYBRID: take over underlying (prefer explicit loan balance) + 2nd for equity const underlyingAmt = loanBalance > 0 ? loanBalance : price * adv.underlyingLTV; const estSecondAmt = Math.max(price - underlyingAmt, 0); const secondPayment = pmt(adv.secondRate, adv.secondTermYears, estSecondAmt); const hybridMonthlyDebt = piti + secondPayment; // PITI covers underlying const hybridCF = noiMonthly - hybridMonthlyDebt; const hybridCashIn = adv.subtoUpfront + rehab + buyCosts + arrearsOwed; // include arrears to reinstate const hybridCOC = hybridCashIn > 0 ? (hybridCF * 12) / hybridCashIn : 0; const hybridPass = (hybridCF >= thresholds.minCF) && (hybridCOC >= thresholds.minCOC) && (capRateAnnual >= thresholds.minCap); // 6) WRAP EXIT: sell via wrap at higher rate/price; spread over underlying (use subto or seller-fin payment) const wrapPrice = arv * adv.wrapPricePctOfARV; const wrapDown = wrapPrice * adv.wrapDownPct; const wrapPrincipal = Math.max(wrapPrice - wrapDown, 0); const wrapBuyerPay = pmt(adv.wrapRate, adv.wrapTermYears, wrapPrincipal); // choose underlying payment = smallest of (piti, sfPayment) if both exist; else piti if >0 else sfPayment const underlyingPay = (piti > 0 && isFinite(sfPayment) && sfPayment > 0) ? Math.min(piti, sfPayment) : (piti > 0 ? piti : (isFinite(sfPayment) ? sfPayment : 0)); const wrapSpread = wrapBuyerPay - underlyingPay; // before servicing; end-buyer typically pays property expenses in a wrap const wrapMonthlyNet = wrapSpread; // keep simple; optional servicing reserve can be modeled later const wrapCashIn = adv.subtoUpfront + rehab + buyCosts + arrearsOwed; const wrapUpfrontNet = (wrapDown + adv.wrapAssignmentFee) - wrapCashIn; const wrapPass = (wrapMonthlyNet >= thresholds.minCF) && ( (wrapUpfrontNet >= thresholds.minFlipProfit) || (wrapMonthlyNet * 12 / Math.max(wrapCashIn,1) >= thresholds.minCOC) ); // 7) SHORT-TERM RENTAL (cash) const strRevenue = rent * adv.strRevenueUplift; // LTR baseline × uplift const strEGI = strRevenue; // vacancy typically embedded in expense rate for STR; adjust if desired const strVarOpex = strEGI * (adv.strExpenseRate + adv.strMgmtRate); const strNOIMonthly = Math.max(strEGI - strVarOpex - fixedMonthly, 0); const strCF = strNOIMonthly; // no debt in this card const strCOC = basisCash > 0 ? (strCF * 12) / basisCash : 0; const strCap = basisCash > 0 ? (strNOIMonthly * 12) / basisCash : 0; const strPass = (strCF >= thresholds.minCF) && (strCOC >= thresholds.minCOC) && (strCap >= thresholds.minCap); return { rent, piti, arv, price, rehab, buyCosts, basisCash, egiMonthly, varOpexMonthly, fixedMonthly, noiMonthly, noiAnnual, capRateAnnual, cashOfferCoversLoan, wholesale, flip: { profit: flipProfit, margin: flipMargin, roi: flipROI, cashIn: flipCashIn, pass: flipPass }, cashRental: { cf: rentCash.cashflowMonthly, coc: cocCash, cap: capRateAnnual, pass: rentCashPass }, subto: { cf: subtoCF, coc: subtoCOC, cap: capRateAnnual, cashIn: subtoCashIn, pass: subtoPass }, sellerFinance: { payment: sfPayment, down: sfDown, cf: sfCF, coc: sfCOC, cap: capRateAnnual, cashIn: sfCashIn, pass: sfPass }, hybrid: { cf: hybridCF, coc: hybridCOC, cap: capRateAnnual, cashIn: hybridCashIn, pass: hybridPass }, wrap: { price: wrapPrice, buyerPay: wrapBuyerPay, spread: wrapSpread, monthlyNet: wrapMonthlyNet, upfrontNet: wrapUpfrontNet, pass: wrapPass }, str: { revenue: strRevenue, cf: strCF, coc: strCOC, cap: strCap, pass: strPass }, }; }, [inputs, adv, thresholds]); const strategies = [ ...(inputs.isWholesale ? [{ key: 'wholesale', label: 'Wholesale (Assignment)', render: () => ( ) }] : []), { key: 'flip', label: 'Cash (Flip)', render: () => ( ) }, { key: 'cashRental', label: 'Cash (Rental)', render: () => ( ) }, { key: 'subto', label: 'Creative: Subject-To', render: () => ( ) }, { key: 'sellerFinance', label: 'Creative: Seller Finance', render: () => ( ) }, { key: 'hybrid', label: 'Creative: Hybrid (Subto + 2nd)', render: () => ( ) }, { key: 'wrap', label: 'Wrap-Around Exit', render: () => ( ) }, { key: 'str', label: 'Short-Term Rental (Cash)', render: () => ( ) }, ]; const visible = thresholds.hideFails ? strategies.filter(s => U[s.key]?.pass) : strategies; return (

BILT Deal Options Calculator

Enter a few numbers and compare multiple exit options side by side. Tweak constraints to instantly see what qualifies.

{/* Inputs */}
onChange('rent', v)} prefix="$" /> onChange('piti', v)} prefix="$" /> onChange('hoaMonthly', v)} prefix="$" /> onChange('arv', v)} prefix="$" /> onChange('price', v)} prefix="$" /> onChange('rehab', v)} prefix="$" /> onChange('loanBalance', v)} prefix="$" /> onChange('arrearsOwed', v)} prefix="$" />
onSet('isWholesale', e.target.checked)} />
{inputs.isWholesale && ( onChange('assignmentFee', v)} prefix="$" /> )}
onThreshold('minCF', v)} prefix="$" /> onThreshold('minCOC', v, 0.01)} suffix="%" /> onThreshold('minCap', v, 0.01)} suffix="%" /> onThreshold('minFlipProfit', v)} prefix="$" /> onThreshold('minFlipMargin', v, 0.01)} suffix="%" /> onThreshold('minAssignmentFee', v)} prefix="$" />
onThreshold('hideFails', e.target.checked)} />
onAdv('vacancyRate', v, 0.01)} suffix="%" /> onAdv('mgmtRate', v, 0.01)} suffix="%" /> onAdv('maintenanceRate', v, 0.01)} suffix="%" /> onAdv('capexRate', v, 0.01)} suffix="%" /> onAdv('otherOpexRate', v, 0.01)} suffix="%" /> onAdv('fixedMonthlyOpex', v)} prefix="$" />
onAdv('buyClosingPct', v, 0.01)} suffix="%" /> onAdv('acqAgentFeePct', v, 0.01)} suffix="%" /> onAdv('sellClosingPct', v, 0.01)} suffix="%" /> onAdv('holdMonths', v)} /> onAdv('holdMonthlyBurn', v)} prefix="$" />
onAdv('sfDownPct', v, 0.01)} suffix="%" /> onAdv('sfRate', v, 0.01)} suffix="%" /> onAdv('sfTermYears', v)} />
onAdv('subtoUpfront', v)} prefix="$" /> onAdv('underlyingLTV', v, 0.01)} suffix="%" /> onAdv('secondRate', v, 0.01)} suffix="%" /> onAdv('secondTermYears', v)} />
onAdv('wrapPricePctOfARV', v, 0.01)} suffix="%" /> onAdv('wrapDownPct', v, 0.01)} suffix="%" /> onAdv('wrapRate', v, 0.01)} suffix="%" /> onAdv('wrapTermYears', v)} /> onAdv('wrapAssignmentFee', v)} prefix="$" />
onAdv('strRevenueUplift', v)} suffix="×" /> onAdv('strExpenseRate', v, 0.01)} suffix="%" /> onAdv('strMgmtRate', v, 0.01)} suffix="%" />
{/* Results */}

Results

{visible.map((s) => (
{s.label}
{U[s.key]?.pass ? 'Pass' : 'Below Threshold'}
{s.render()}
))}
{/* Footnote */}

All outputs are estimates based on your inputs & assumptions. Not legal, tax, or investment advice. Always verify numbers for your specific deal.

); } function Card({ title, children }) { return (
{title}
{children}
); } function Field({ label, value, onChange, prefix, suffix }) { return ( ); } function Details({ title, children }) { return (
{title}
{children}
); } function MetricGrid({ rows, pass }) { return (
{rows.map(([k, v]) => (
{k} {v}
))}
); }