Express service that implements Archwest Capital's Fix & Flip (FNF) loan sizing per the internal pricing PDF and sheets. Designed for forms, CRMs, and voice agents to return a provisional loan and indicative rate range in-call.
Phase‑1 supports FNF with Borrower Levels A/B, state tiering, and loan amount tiers 1–4. The sizing flow is row‑driven from the pricing matrix.
npm start
# or
npm run dev
Server will listen on http://localhost:3000
.
Required
- productKey: "FNF"
- data.loanPurpose: "purchase" | "refi"
- data.propertyState: 2‑letter code (state tier)
- data.purchasePrice (or current propertyValue for refi)
- data.rehabBudget
- data.afterRepairPropertyAmount (ARV)
- data.borrowerFico
- data.borrowerExperienceMonths
Optional
- data.requestedAmount (caps Final Note)
- data.borrowerLevel (defaults to A; A/B currently supported)
Success response:
{
"ok": true,
"data": {
"productKey": "FNF",
"requestedAmount": 2700000,
"afterRepairPropertyAmount": 7000000,
"rehabBudget": 310000,
"propertyValue": 2700000,
"purchasePrice": 2700000,
"propertyState": "CA",
"borrowerFico": 740,
"borrowerExperienceDeals": 7,
"borrowerExperienceMonths": 84,
"loanPurpose": "purchase",
"stateTier": 1, // State pricing tier (1=CA, 2=FL/GA/TX, 3=others)
"loanAmountTier": 3, // Loan size tier (1-4)
"borrowerLevel": "A", // Borrower qualification level
"policy": {
"maxLTV": 0.85, // Max loan-to-value
"maxLTARV": 0.8, // Max loan-to-ARV
"maxLTC": 0.9, // Max loan-to-cost
"minFico": 740,
"minExperienceMonths": 7,
"minLoan": 2000000,
"maxLoan": 3499999
},
"constraints": {
"byLTARV": 5600000, // Max by ARV constraint
"byLTC": 2709000, // Max by total cost constraint
"byLTV": 2295000, // Max by property value constraint
"totalProjectCost": 3010000
},
"sizing": {
"maximumEligibleLoan": 2295000,
"provisionalLoanAmount": 2295000
},
"pricing": {
"noteRate": "8.904%", // Interest rate
"noteRateDecimal": 0.08904,
"originationFee": 0.0075, // 0.75%
"term": 12 // Months
},
"outcome": "eligible", // eligible|ineligible|recontact
"reason": null
}
}
Error response (examples):
{ "ok": false, "error": "arv_gt_rehab_value", "message": "ARV must be greater than rehab budget" }
{ "ok": false, "error": "arv_gt_property_value", "message": "ARV must be greater than property value" }
{ "ok": false, "error": "insufficient_experience", "message": "Minimum 36 months experience required" }
{ "ok": false, "error": "no_qualifying_product", "message": "No qualifying loan product found" }
{ "ok": false, "error": "invalid_fnf_amount", "message": "Loan amount outside acceptable range" }
- State gate: reject if state not enabled (via state tier table)
- Borrower level/experience screen (A/B supported; mins enforced)
- Choose pricing row by FICO band (highest row minFICO ≤ borrower FICO) and loan‑amount tier
- Purpose caps from row (Purchase vs Refi)
- Projected Note = min( ARV × LTARV, (Purchase + Rehab) × LTC )
- UPB @ Close = Purchase × LTV
- Final Note (provisional loan) = min( Projected Note, UPB + Rehab, RequestedAmount if provided )
- Snap to the row whose Min/Max contains Final Note and re‑calculate (caps, UPB, Final Note)
- Price by State Tier (Tier1/2/3) and display a +30 bps range
- Return eligibility outcome and explanation
Always
- ok (boolean), data.outcome: eligible | ineligible | recontact
- data.reason when not eligible (e.g., state_not_enabled, low_fico, sizing_outside_row_range)
If eligible
- data.finalNoteAmount, data.upbAtClose, data.holdback
- data.displayLTV, data.displayLTC
- data.rateLo, data.rateHi, data.termMonths (12)
- data.stateTier, data.loanAmountTier
-
Borrower Level A (Experienced)
- Experience: ≥ 7 months (from database rows)
- FICO bands supported: 740, 720, 700, 680
- Typical caps (example, Purchase): at 740 FICO, caps often align to
LTV 85%
,LTARV 80%
,LTC 90%
; lower FICO bands reduce caps per pricing row
-
Borrower Level B (Mid‑Experienced)
- Experience: ≥ 5 months (from database rows)
- FICO bands supported: 740, 720, 700, 680
- Typical caps: slightly tighter than A for the same tier/FICO (see JSON rows for exact caps used by the API)
Levels C (≥ 3 months) and D (≥ 1 month) will be added. Until then, callers with < 5–7 months should expect “ineligible” or “recontact” outcomes depending on FICO.
-
Property
propertyState
(2‑letter code; drives state tier/rate)propertyValue
(as‑is value)afterRepairPropertyAmount
(ARV)purchasePrice
(if applicable)rehabBudget
-
Borrower
borrowerFico
borrowerExperienceMonths
(required; min enforced is 36 months)borrowerExperienceDeals
(optional, informational)
-
Loan details
loanPurpose
(purchase
orrefi
)requestedAmount
(optional; if omitted, API returns the maximum eligible sizing)
- ARV must be greater than rehab budget; otherwise sizing is invalid
- ARV must be greater than current property value
- Minimum experience of 36 months is enforced in the API today
- FICO below pricing row minimum → ineligible; FICO < ~640 → “recontact” recommendation
- Sizing respects the minimum/maximum loan amounts per loan‑amount tier
Given the inputs above, the API computes:
- Caps from database by borrower level, FICO, loan‑amount tier, and purpose (Purchase vs Refi)
- Constraint amounts:
byLTARV
,byLTC
,byLTV
maximumEligibleLoan = min(byLTARV, byLTC, byLTV)
provisionalLoanAmount = min(maximumEligibleLoan, requestedAmount || maximumEligibleLoan)
Result also includes the effective noteRate
based on state tier (CA = Tier1, FL/GA/TX = Tier2, others = Tier3).
Endpoint: POST /v1/sizer/fixflip/quote
curl -s -X POST http://localhost:3000/api/loan-details \
-H 'Content-Type: application/json' \
-d '{
"productKey": "fix_and_flip",
"data": {
"afterRepairPropertyAmount": 500000,
"rehabBudget": 100000,
"propertyValue": 300000,
"requestedAmount": 250000
}
}' | jq
- Added new purpose-aware sizer endpoint:
POST /v1/sizer/fixflip/quote
.- Implements PDF sequence: Projected Note (min of LTARV and LTC), UPB @ close via LTV, Final Note = min(Projected, UPB+Rehab), re-selects pricing row by Final Note, and prices by State Tier with a +0.30% range.
- Returns:
projectedNote
,upbAtClose
,holdback
,finalNoteAmount
,displayLTV
,displayLTC
,rateLo
,rateHi
,termMonths
,stateTier
,loanAmountTier
,caps
.
- Added
src/sizer/fnf.js
sizing module (purchase/refi caps, UPB/holdback, snap-to-row, rate range). - Kept existing endpoint
POST /api/loan-details
(basic sizing) for backward compatibility. - Documented borrower personas (Levels A/B), voice-agent input checklist, and guardrails.
- Health endpoints available:
GET /
andGET /health
. - Data ingestion updated: added XLSX-based builder to extract caps/rates from original workbook (no CSV header ambiguity). A/B caps now fully populated except 1 pending source row (B, FICO 680, Tier 4), which is gated with a clear config_missing_caps outcome.
- Data dependency: Borrower B, FICO 680, Tier 4 caps are pending confirmation from Abhiram Deshpande (Codiot). Until confirmed, this single combo is disabled via outcome=config_missing_caps to preserve accuracy.
- Known gaps (to be added next): remaining C/D cleanup, points adjustments, judicial/non-judicial linkage, additional products (Bridge/GUC/DSCR).
- Fix & Flip with Rehab sizing logic
- Borrower Levels A and B (experience mins enforced)
- Loan amount tiers 1–4 and state tier mapping (CA=Tier1; FL/GA/TX=Tier2; others=Tier3)
- FICO banding at 740/720/700/680
- Returns: Final Note (provisional loan), UPB @ Close, Holdback, display LTV/LTC, rate range (+30 bps), 12‑month term, outcome
Pricing and tiering rules are loaded from a JSON generated from Archwest’s CSV/Excel sheets:
archwest_fnf_database.json
This JSON contains:
- Loan amount tiers (min/max)
- FICO minimums
- Experience requirements (A=7, B=5 in 36 months)
- State → Tier mapping
- Pricing rows with LTV/LTARV/LTC caps and note rates
Endpoint
POST /v1/sizer/fixflip/quote
Request example
{
"productKey": "FNF",
"data": {
"loanPurpose": "purchase",
"propertyState": "CA",
"purchasePrice": 1100000,
"rehabBudget": 75000,
"afterRepairPropertyAmount": 1400000,
"borrowerFico": 740,
"borrowerExperienceMonths": 84,
"borrowerLevel": "A",
"requestedAmount": 1200000
}
}
Response example
{
"ok": true,
"data": {
"qualified": true,
"outcome": "eligible",
"finalNoteAmount": 1010000,
"upbAtClose": 935000,
"holdback": 75000,
"displayLTV": 0.85,
"displayLTC": 0.9,
"rateLo": 0.0868,
"rateHi": 0.0898,
"termMonths": 12
}
}
-
Eligible (A/B): “Based on your details, you’re eligible for a provisional loan of about $1.01M, with $935k advanced at closing and $75k reserved for rehab. Your rate is estimated between 8.7% and 9.0% for a 12‑month term.”
-
Recontact: “You’re not eligible at this time. If your credit improves over the next six months, you may qualify. I’ll set a reminder to reach out then.”
-
Ineligible (state gate): “We’re not able to offer a loan right now because the property state isn’t eligible for our programs.”
- Borrower Levels C/D (with complete cap validation).
- Judicial vs Non‑Judicial and explicit Able‑to‑Lend integration (single policy section).
- Additional products (Bridge, DSCR, GUC).
- Pricing adjustments (origination/size/credit/tier spreads, buffers).
- CRM integrations (Salesforce/HubSpot).
This API provides front‑end sizing only and is not a credit decision engine. All outputs are indicative and subject to Archwest’s final underwriting and compliance rules.