Paths confirmed. The engine apply forms exist at
sooner-underwriting-engine/apps/web/pages/apply/
(lead/index/preapproval/result/documents.tsx — note
preapproval.tsx not index.tsx is the main
form, and there’s no result.tsx listed wait it is). I have
enough to write the contract. The cluster outputs are self-contained;
I’ll consolidate now without re-reading the 145KB index.html (the
clusters already cite exact line numbers).
Here is the canonical document.
Version 1.0 · Compiled 2026-06-07 · Owner: Gabriel Garcia Leyva (lead underwriter) · Implementer: engine + site team
This is the single source of truth for every finding raised against the Sooner underwriting engine and reference site. It supersedes the four cluster outputs. Section 3 (change specs), Section 4 (new artifacts), and Section 5 (architecture) are implementation-ready. Section 6 lists what still needs founder/data/legal input.
Target files referenced throughout: - Reference site:
/Users/gabo/Desktop/Projects/sooner-mini-apps/apps/underwriting-criteria/index.html
- Engine forms:
/Users/gabo/Desktop/Projects/sooner-underwriting-engine/apps/web/pages/apply/{lead,preapproval,index,documents,result}.tsx
- Data dictionary:
/Users/gabo/Desktop/Projects/sooner-mini-apps/apps/data-dictionary/
- Rent-vs-buy benchmark:
https://sooner-rent-vs-buy.pages.dev/
Disposition legend: fixed = recalibration already produced, drop-in ready · resolved = answered by reframe or existing artifact, no/cosmetic logic change · reframed = critique was wrong or mis-scoped; answer documented · planned = real defect, change spec written, not yet coded · open = genuinely needs data/calibration/legal/founder input.
| ID | Disposition | One-line resolution |
|---|---|---|
| P2 | reframed | Margin is strongly positive (ROE 53.4%, gross IRR 41.5%, net IRR 22.4%, NIM 12.15%); OID commissions cut net capital to ~112,600 and the bundle clears 15% CoF by 7–22pts. |
| P3 | reframed | Flat pricing is not circular once the EL model is built independently; premium validated at 22.4% net IRR after CoF + opex + EL. |
| P4 | reframed | Owner never loses equity/title; Sooner’s recourse is the master lease, recovers from re-lease rent then exits — structurally opposite of equity-stripping. |
| P5 | reframed | Buyer pays nothing extra for Home Discovery/Brokerage; seller pays 21k commission, bank pays 16k referral — bundle lowers buyer price via third-party revenue. |
| P7 | resolved | Total-cost benchmark already exists: the rent-vs-buy calculator. Link it from Pricing + KFS + topnav. |
| P8 | planned | Engine implements no pricing/EL/disclosure today; port
soonerMonthly/effectiveAPR/bundle-select into
the engine, add expectedLoss(), emit KFS. |
| F-b / C7 | planned | Affordability tested once → add continuous Lean monitor + annual AECB re-pull + savings gate + stressed-DBR. |
| F-c | reframed | High effective APR is real and disclosed, but appreciation (~+5%/yr) + avoided rent net the entry cost positive; show value framing after the mandatory APR disclosure. |
| F-d | resolved | Early-termination schedule exists (6%→1% declining, 6-month floor), Shariah actual-loss framed; surface on Pricing + KFS, no logic change. |
| F-g | reframed | Product necessity validated: personal loan breaches 50% DBR, card costs more, wait-and-save forgoes appreciation. |
| C3 / R15 | planned | KFS content written (Section 4); build as .docx +
in-app tab, driven by engine constants. |
| C6 / L5 / R11 / R17 | planned | EL model spec written (Section 4); reconciles 4% PD vs 1% provision (see Section 6). |
| ID | Disposition | One-line resolution |
|---|---|---|
| S-a | decided (D1) | SIMPLIFIED 2026-06-08: no capacity cap. Re-weight the key levers (employment 15→24, re-leasing 10→15; §3.2), keep J in the additive pool, and expose the override as published reason codes (§3.1). Score = honest sum-of-parts; vetoes come from gates + the D5 weak+weak rule. |
| S-b | planned | Score is decorative for capped segments → relabel score “one input”, publish the waterfall as the decision surface; 70/55 cutoffs apply only when no cap binds. |
| S-c | planned | Employer tier triple-counted (J score + G7 + waterfall) → de-dup to exactly two loci: G7 tenure + decision-band cap/factor. |
| S-d | planned | Recourse tier counted 4× → de-dup to three distinct questions: G12 eligibility, G15 coverage, Category H + recourse cap. |
| S-e / R10-affordability-floor | planned | R10 recourse-rescue has no affordability floor → require raw.A≥82 AND raw.B≥82 (DBR≤45% + 6mo buffer) before recourse can rescue a weak employer. Same change, implement once. |
| S-f | fixed | Employer model recalibrated across 6 axes (Section 3.6); closes R2 (ADNOC→T1). |
| S-g | planned | Thin-file substitute too generous (62) → drop to 35 conservative floor. |
| S-h | planned | Income haircuts set: additional 50%, commission 50% (rental 70% per one cluster variant — reconcile, Section 6). |
| S-i | planned | CATS weights sum to 105 → rebase to 100 after removing J as an additive slot. |
| S-j | planned | 4-rung ladder creates cliffs/dead-zones → piecewise-linear interpolation between published band breakpoints. |
| G-k / G6↔︎G9 | planned | 25k income floor lets structurally-unaffordable files pass G6 then
fail G9 → compute
requiredIncomeFloor = (mortgage + Sooner monthly)/0.50,
fail fast. |
| ID | Disposition | One-line resolution |
|---|---|---|
| R-b | resolved | Built on lease-level DLD rental data we hold (every new lease + ~50% renewals; price/duration/unit-type/beds/building/location). Depth = new-lease registration count (primary); rent = actual median registered lease price; turnover/void from lease duration + renewal gaps; sales volume = tie-breaker only; reports for supply. Dated manual-override register on top; join by PSL code. Our own measured void days layered in as re-leases accumulate (3.13). |
| R-c | reframed | Yield sign is wrong in the tail → score net-of-void achievable yield as an inverted-U (peak 5.5–7%, penalize >8.5%). |
| R-d | open | Ranking still render-only; engine takes tier as a set input. Build
resolveRecourseTier() + wire R10 + emit audit signals —
AFTER rubric ratified. |
| R-e | planned | Cut-offs/weights uncalibrated → 4-stage staged calibration, re-baseline off the post-Iran-shock window, back-test at 30–50 realised re-leases. |
| R-f | reframed/resolved | Ranking must key off rental-recovery primitives, not sales volume;
subsumed by R-b. Feed same voidDays/net-rent into
recoveryModel() and provision. |
| R-h | reconciled | Concentration policy exists AND is reconciled with the
community-tiering artifact: community-classification.json
v2026-04-30 (tier A/B/C + cluster per community) is the canonical
tiering source; Section 3.15 cluster caps reference the SAME clusters.
Cluster membership = the classification file; cap numbers = 3.15
(pending Gabriel ratify, R22). No separate concentration doc owed. |
| L6 | planned | Owner-occupier vs investor recovery undifferentiated → add
propertyUsage, longer carry + Cat H haircut for
investor_tenanted. |
| F-e | reframed | Displacement critique mis-reads recovery as frictionless; the ~2–4mo carry + void is already priced; owner regains control on recovery. |
| Gov-f | resolved | Named Section-A carve-outs (Hartland Greens=B, Wilton Park=B, The Crest=C) override the aggregate “Sobha Hartland” ranking row; encode precedence. |
| ID | Disposition | One-line resolution |
|---|---|---|
| Gov-e | planned | No demographic capture → monitoring-only, hard-walled capture + adverse-action reason codes + quarterly AIR<0.80 test + champion-challenger. |
| Gov-g | resolved | Concentration policy exists but lives in docs → encode
CONCENTRATION + checkConcentration() as code;
apply R-h updates. |
| Gov-h | reframed/resolved | BNPL via Lean bank-statements = sufficient (often better than AECB); add as DBR input. PDPL = a consent/retention compliance workstream, not a bank-statement check. |
| G2 | planned | Age check is origination-only → enforce age-at-maturity:
age≥21 AND age+5≤65. |
| G3 | planned | Thin-file bypasses AECB → AECB pull mandatory; thin-file is set BY the pull result, not a free toggle. |
| G8 | resolved | 6–11% range is correct (actual fees, not a fixed %); reframe as guardrail and drop absolute floor 50k→30k. |
| G9 / R9 | planned | Compute DBR from primitives with bank mortgage counted once;
Category A consumes the same dbrTotal. |
| G-f | resolved | RERA rent caps / sitting-occupier protections never bind (eviction is non-payment-only); one clarifying line, no engine change. |
| F-f / R18 | planned | All-at-ceiling files can auto-approve → thin-affordability committee trigger + hard savings gate G16 + stressed-DBR check. |
| Gov-f (Sobha) / R23 | resolved | Page = canonical spec; engine catches up; split the aggregate Sobha ranking row by sub-community. |
| R2 | resolved | ADNOC ratified to T1 (sovereign state-energy), closed by S-f. |
| ID | Disposition | One-line resolution |
|---|---|---|
| canonical-page-design | planned | Extract inline logic into underwriting-contract.js
(versioned, deterministic ES module); page renders FROM it; add EL / KFS
/ Concentration / Ratification-ledger tabs. |
| forms-embed-plan | planned | Publish @sooner/underwriting-contract workspace
package; forms run advisory client checks, engine runs the identical
module server-side as the binding decision; CI golden-profile parity
gate. |
| R-d (engine parity) | open | Engine still hard-declines T4/T5 and takes commTier as input; parity ships structurally when engine imports the contract module. |
Ratification ledger (needs Gabriel’s sign-off to flip
resolved): R2 (ADNOC→T1 ✔ decided), R5
(additional-income haircut 50%), R7 (self-employed→manual), R9 (DBR from
primitives), R10 (employer×recourse + affordability floor), R15 (KFS/APR
actual-loss framing), R16 (recovery data source), R17 (1%/yr provision —
see Section 6), R18 (60mo affordability gates), R19 (score
honesty/caps), R20 (calibration + fair-lending + concentration), R21
(fair-lending pack), R22 (concentration re-baseline), R23 (Sobha
sub-community reconciliation).
These are the narrative answers, each a tight paragraph, ordered as they surfaced. Drop these onto the canonical page so funders and auditors read the answer in context.
P2 — “the margin barely clears cost of funds.” Wrong as written. The model shows 53.4% annualized ROE, 41.5% gross IRR, 22.4% net IRR, and 12.15% NIM, with ~76,320 five-year net profit per deal. The bridge: on the base case the buyer pays an 80,745 premium (50% of the 161,490 principal, 10%/yr flat). On top of that, OID commissions of ~37,000 (21,000 seller-side sale commission + 16,000 bank referral on the 1.6M mortgage) arrive at origination, cutting net capital deployed from 161,490 to ~112,600 and equity-at-risk to ~28,440. Gross IRR (41.5%) exceeds net IRR (22.4%) precisely because of the 15% facility drag, and it still clears it by 7–22 points. The flat 10%/yr looks thin only if you ignore the OID; the OID is the whole point of the bundle.
P3 — “flat pricing is circular because EL is never computed.” Decoupled once the EL model exists separately. The new EL model computes PD × LGD × EAD independently from the recovery economics, and the premium is checked against it, not derived from it. Reassessed: 10%/yr flat = 18.7% effective APR; subtract a ~12% blended funding cost (15% CoF on 80% debt), ~1%/yr opex, and the ~1–2.4%/yr EL the model produces, and the residual is 22.4% net IRR. Pricing is flat by design (bundle-driven, Shariah-framed lease rental, not risk-priced per deal), and that is defensible because the master-lease recourse compresses LGD dispersion across deals. The EL model proves the flat premium is adequate; it does not set it. Risk-based pricing is deferred until portfolio data calibrates the EL tail.
P4 — “predatory equity-stripping optics.” Factually wrong for this structure. Sooner finances closing fees (~8% of value), not the property. The buyer holds the freehold and the bank mortgage throughout. Sooner’s recourse is a master lease over the unit, not a charge on the buyer’s equity. On default Sooner takes vacant possession via the fast Article 25(1) non-payment route, re-leases to recover ~9–12 months of rent, then EXITS the master lease — the owner regains full possession with mortgage and equity intact. The owner can lose at most the rent Sooner collects during recovery, never their ownership stake. This is structurally the opposite of equity-stripping.
P5 — “the bundle inverts price against the buyer.” It does not. The buyer pays nothing extra for Home Discovery or Mortgage Brokerage. Sooner earns the 21,000 sale commission from the seller (1.05% of 2M) and the 16,000 mortgage referral from the bank (1% of the 1.6M mortgage). The cheaper-looking full bundle (10%/yr vs 15%/yr CFF-only) is cheaper for the buyer because Sooner is being paid by third parties on that deal, not because Sooner is repricing the buyer. The “inversion” critique assumed the buyer funds all three services; they do not. The discount is third-party revenue passing through, which aligns interests rather than inverting them.
F-c — “expensive credit on a sunk cost.” The sunk-cost/high-APR framing ignores the appreciation offset. On a 2M home the financed fee is ~60–161k. Dubai prices have averaged ~+5%/yr, so the home gains ~100k/yr. A buyer who uses Sooner buys now instead of spending 1–2 years saving the closing stack; over that wait they would forgo ~100–200k of appreciation plus pay rent. The premium (~80k over 5 years base case) is smaller than the appreciation captured by buying earlier, before netting avoided rent. The honest framing is “a financed entry that pays for itself in captured appreciation plus avoided rent,” not “expensive credit on a sunk cost.” The 18.7% effective APR is real and must be disclosed (KFS), but APR-in-isolation is the wrong lens for a one-time entry cost that unlocks an appreciating, rent-replacing asset.
F-g — “is this product even necessary?” Validated by the alternatives being strictly worse or blocked. A buyer who lacks the closing cash has three options: (1) a personal loan for ~8% of value, which adds a monthly obligation pushing most buyers over the 50% DBR cap (G9), so the bank declines the mortgage and the purchase dies; (2) a credit card at ~30–40% effective APR (worse than Sooner’s 18.7%) with far shorter tenor; or (3) wait 1–2 years saving, forgoing ~100–200k appreciation. Sooner’s master-lease structure keeps the financed amount tested inside G9 with the Sooner monthly included rather than as a separate bank loan, so it does not blow the mortgage. The product exists because the obvious substitutes either breach the DBR ceiling or cost more.
F-e — “owner-occupier displacement.” The critique mis-reads recovery as a frictionless seizure. The sub-lease ends only for non-payment; Sooner then takes vacant possession via Article 25(1) (~2–4 months) plus a re-lease void — a real cost already carried in the recovery model and the 1%/yr provision. The owner does not lose equity: Sooner re-leases for the remaining term to recover the ~8%-of-value financed amount, then exits and the owner regains control. The only case that looks like displacement is an owner-occupier who has already stopped paying — that is the recovery mechanism functioning, and it is slow and costly enough that it is never the cheap option the critique assumes. The genuinely-slower path is the investor-tenanted case, differentiated per L6.
F-d — “no early-exit / refinance path.” Resolved: an early-termination schedule exists, Shariah-framed as actual-loss compensation (not riba): months 1–6 6%, 7–12 5%, 13–24 4%, 25–36 3%, 37–48 2%, 49–60 1%, with a 6-month minimum floor (greater of 6% of remaining balance or six months of premium). Sooner’s IRR by termination timing (M3 38.1%, M6 23.4%, M12 16.2%, M24 14.4% worst, M36 15.1%, M48 16.7%, no-prepay 18.7%) confirms the schedule protects the worst-case mid-life prepayment while staying competitive — UAE personal loans charge 1–3% flat; Sooner’s declining schedule is in the same neighbourhood at the back end and only steeper early, where actual loss is largest. Base-case CPR 8%/yr costs only ~−0.5% gross yield. Needs disclosing on the KFS; it is not a gap.
G-f — “RERA rent caps constrain recovery.” Moot. The owner is Sooner’s sub-tenant; the sole eviction trigger is non-payment, the fast Dubai Law 26/2007 Article 25(1) in-term route (30-day notice, corporate-lessor-eligible, no 12-month notarised notice, no personal-use natural-person ground, no 2-year re-letting ban). RERA’s rent-cap index and sitting-occupier protections only matter if Sooner were raising rent on or removing a performing tenant — which the product never does. On default there is no protected sitting tenant; Sooner takes vacant possession and re-leases at market. The concern dissolves on the product mechanics.
G8 — “the 6–11% closing-fee range is a defect.” It is correct, not a defect. The closing stack (DLD 4%, agency 2%, bank arrangement 1%, registration/trustee/valuation/NOC/conveyancing + 5% VAT on services) lands ~6–11% of value depending on whether agency is paid, mortgage size, and VAT-able mix. Sooner finances the actual invoiced number, not a fixed percentage; the % band is an indicative guardrail. Separately, the AED 50,000 absolute floor is wrong: at a 500k property an 8% fee is only 40,000, so the floor would auto-decline in-band facilities on smaller properties. Drop the floor to 30,000.
Gov-h — “are bank statements via Lean enough for BNPL/PDPL?” Necessary but not fully sufficient, and the honest answer says so. Bank-statement parsing reliably catches BNPL/PDPL obligations that settle through the monitored account (Tabby, Tamam, postpay debits). The gap: (1) not all BNPL providers report to AECB, and an obligation paid from a different account or in cash is invisible to a single-account Lean feed; (2) a BNPL line taken after origination is only caught if Lean monitoring is continuous and an alert is wired. Canonical position: bank statements via Lean are the primary detection layer, backstopped by (a) the mandatory AECB pull (G3), (b) borrower self-declaration in Form 1, and (c) a continuous Lean monitor flagging new recurring debits mid-term. Note separately that “PDPL” is a data-protection compliance obligation, not a bank-statement check — it is handled by a consent gate at Lean-connection time plus a retention policy, not by parsing statements.
R-b — “what source for the recourse ranking? We do not have a rental-liquidity API.” Corrected 2026-06-08: Sooner has no PropSearch-grade void / time-to-let feed, but it DOES hold lease-level DLD rental data — every new lease + ~50% of renewals, each with price, duration, unit type, beds, building/sub-community and location. So the ranking is built on near-direct rental data, not sales: (1) PRIMARY DEPTH — the COUNT of new registered leases per (community × unit-type × beds), trailing 12m, normalised to stock; how many leases actually get signed is the cleanest rental-depth signal and it comes straight from DLD, no vendor. (2) ACHIEVABLE RENT — the actual median registered lease price, by unit-type × beds (not an estimate). (3) TURNOVER / VOID proxies — derived from lease duration + re-registration gaps. (4) DLD SALES volume drops to a tie-breaker only (sales liquidity ≠ rental liquidity — that was the original R-b bug). (5) Public reports (DLD Rental Index, ValuStrat VRI, Asteco / CBRE) for the supply overlay. Recovery yield = median registered rent ÷ median sale price, entirely from DLD. A thin dated manual-override register sits on top for the named communities and post-event corrections. The one signal the lease data lacks is OUR OWN realised re-lease void days, so tiers stay committee-overridable and measured void days are layered in as we operate (R-e stages). Caveat: renewals are ~50% under-registered, so the data over-weights tenant turnover vs stable renewals — favourable for measuring re-lease depth, noted for the occupancy read. Buildable today, no vendor dependency.
R-c — “yield sign is wrong.” Corrected 2026-06-08: do NOT penalise yield directly — yield IS positive recovery cash flow, and a genuinely good high-yield community must not be assumed un-re-leasable. The penalty is CONDITIONAL, not monotone. Score achievable net-of-void yield as an inverted-U for the default case, but GATE the top-band markdown on the depth proxy: if a community shows high yield AND deep Ejari registration volume AND no heavy 12m supply pipeline, it is a genuinely good high-yield re-lease market — score recovery at full (90–100), no haircut. Apply the >8.5% → 55 markdown ONLY when high yield co-occurs with WEAK depth or HEAVY supply (the oversupply / distress signature). The penalty is on the distress signature, never on the yield number itself. Coverage (G15) legitimately still uses gross rent (in-house re-lease, no agent fee); only the TIER ranking uses the conditional net-of-void yield. The two uses of “yield” are different by design — document the distinction to pre-empt the apparent contradiction.
R-f — “no void / time-to-lease / rent-level data.” Same root cause as R-b and R-c: the ranking ran on sales-market data because that is what was available, but the recovery mechanism is the rental market. At launch the proxy panel (Ejari registration volume, listing absorption, rent index, supply pipeline) supplies the depth/trend/rent-level signals R-f asks for, so R-f is not a separate workstream — it rides on the R-b proxy source. The void/time-to-lease signals it really wants come from Sooner’s OWN realised re-leases at Stage 2 (3.13); once measured void days are available, feed median void directly into the LGD model — early-default LGD is driven by exactly how many months of void + re-lease the recovery window spans, so this data sharpens the expected-loss number, not just the tier.
Gov-f — “engine contradicts the spec; Sobha Hartland is inconsistent.” Reconcile by making the page the canonical spec. The Sobha inconsistency is concrete: COMMUNITIES carries Hartland Greens=B, Wilton Park=B, The Crest=C, while the ranking exhibit scores one aggregate “Sobha Hartland” row (sales −58.7%) deriving a lower tier. Resolution precedence: the named Section-A row WINS, because it reflects a building-specific re-lease judgment the master-level volume cannot see. So Hartland Greens=B, Wilton Park=B, The Crest=C stand; the single ranking row is a master-level fallback for unnamed buildings only. Split the aggregate row by sub-community where data allows; where missing, floor depth to the conservative 50 rather than carrying the −59% master aggregate onto Wilton Park.
Gov-e — “you cannot test for disparate impact.” The risk is structural: community auto-declines (JVC District 5, excluded enclaves) and employer-tier declines (T4/T5 SME, brokerage, cabin-crew) are not protected characteristics, but in Dubai’s labour market they map almost one-to-one onto nationality. Today Sooner captures no demographic field, so it literally cannot run the four-fifths/adverse-impact-ratio test a funder will demand. “We don’t collect it” is not a defence — the funder inherits the conduct risk. The fix is monitoring-only demographic capture, hard-walled from scoring, plus quarterly AIR testing and adverse-action reason codes so every decline is explainable.
All scoring/gate specs target
apps/underwriting-criteria/index.html (and,
post-extraction, underwriting-contract.js). Implement
S-a/S-b/S-c/S-d/S-i together — they interlock.
Reconciliation note — superseded by the D1 founder decision (2026-06-08). The earlier cluster debate over the capped-score mechanism (capacityFactor multiplier vs hard
min()caps) is moot: BOTH score-capping designs are DROPPED. D1 keeps the score an additive sum-of-parts (J retained), re-weights the key levers (§3.2), and makes the honesty fix a visible reason-coded override (§3.1) plus the existing gates + the D5 weak+weak rule. Thin-file is 70 average-to-good (D3), not 35. Weight rebasing keeps all ten categories summing to 100 (§3.2).
Founder decision: the multiplicative capacity-factor / hard-score-cap is DROPPED. The score stays an additive sum-of-parts; the honesty fix is re-weighting + exposing the override. Three parts:
1. Re-weight (§3.2). Lift the two key levers — employment stability (C tenure + J employer) 15→24 and re-leasing (H) 10→15 — and rebase to 100. J (employer sector tier) STAYS in the additive pool — the earlier “remove J” instruction is REVERSED (employer capacity is a legitimate weighted signal, per founder; tenure + seniority + employer is a valid sum-of-parts, not a double-count).
2. Expose the override as a published “Decision Rules” layer
(founder 2026-06-08). Today decide() returns
decline/committee in early branches before it reaches the
score’s own total>=70 line, so the published score can
read 85 next to a “decline” with nothing connecting them. Fix: formalise
those branches as a NAMED, published Decision Rules
layer — a sibling to the gates — and surface it the same way the gates
are surfaced. Three visible layers:
| Layer | What it does | On fail/fire |
|---|---|---|
| Gates G1–G16 | hard pass/fail eligibility + affordability minimums | decline / insufficient_data |
| Decision Rules DR1–DRn | for gate-passing files, route the final band on multi-factor interactions, each with a published reason code | approve / committee / manual / decline + reason |
| Score | creditworthiness gradient, labelled “creditworthiness if the gates pass,” NOT “the decision” | shown alongside the band + reason |
Initial Decision Rules (each maps to a §3.16 adverse-action code):
const DECISION_RULES = [
{ id:'DR1', when:p=>p.weakEmp && p.weakRecourse, band:'decline', rejectType:'soft', reason:'AA10 weak employer + weak recourse' }, // soft on the recourse leg -> location path
{ id:'DR2', when:p=>p.weakEmp && p.strongRecourse, band:p.empTier==='T5'?'manual':'committee', reason:'AA10 recourse rescue of a weak employer' }, // D5
{ id:'DR3', when:p=>p.empTier==='T3', band:'committee', reason:'AA10 T3 employer ceiling' }, // optional policy ceiling
{ id:'DR4', when:p=>p.anyManualGate, band:'committee', reason:'AA-gate manual-review gate present' },
{ id:'DR5', when:p=>p.commTier==='needs_verification', band:'manual', reason:'AA09 recourse community unverified' },
];
Every Gate and Decision Rule carries rejectType (§3.9).
DR1 is soft on the recourse leg: a weak-employer +
weak-recourse file is not a dead-end — moving to a strong-recourse area
triggers DR2 (rescue to manual/committee), so the counter-offer is “you
would go to manual review in {strong-recourse areas}.”
decide() returns
{ band, score, ruleId, reason }; the worst band across the
fired rules + the score band wins, and the fired rule’s reason is
published. So the output reads “Score 82 · declined · DR1: weak employer
+ weak recourse (AA10),” never a silent flip. The true-veto cases (no
employment, ineligible recourse community) are caught by the hard gates
+ DR1; the score does NOT reproduce them, so no multiplicative cap is
needed. (Label “Decision Rules” is cosmetic — could be “Routing Rules”;
the structure is what matters.)
3. Weights sum to 100 (§3.2), not 105.
Published sensitivity (honest version): “Employer affects a file in three legitimate, distinct ways — tenure (Cat C), seniority + sector (Cat J), and the employment tenure gate — which sum because they are different predictors of staying employed over five years; plus a published decision ceiling. Re-leasing affects it in three — eligibility (G12), coverage (G15), recovery-quality (Cat H). Neither is double-counted; every contribution is named and the override is visible.”
New w values (Gabriel 2026-06-08), J kept in the
additive pool:
| Cat | Factor | Old | New | Δ |
|---|---|---|---|---|
| A | Payment Load (DBR incl. Sooner) | 20 | 17.5 | −2.5 |
| B | Savings Buffer | 20 | 15 | −5 |
| C | Employment Stability (tenure) | 5 | 10 | +5 |
| D | Household Income | 10 | 7.5 | −2.5 |
| E | UAE Residency | 10 | 7.5 | −2.5 |
| F | Credit Relationship | 10 | 5 | −5 |
| G | AECB Score | 5 | 5 | 0 |
| H | Re-Leasing Recourse | 10 | 15 | +5 |
| I | Lifestyle Alignment (LTV) | 5 | 5 | 0 |
| J | Sector Tier (employer) | 10 | 12.5 | +2.5 |
| Total | 105 | 100 |
Stability (C+J) 15→22.5; re-leasing (H)
10→15 — the two key levers, lifted. Affordability
(A+B+D) 50→40; credit (F+G) 15→10; residency E 10→7.5; LTV I held at 5.
Thin-file path: F 5→0, its weight folds into G (G 5→10)
on the 70 average-to-good substitute (§3.11) → thin sum = 100. Remove
the activeWeight divisor (total is already out of 100);
keep a guard asserting each path sums to 100. Update the “Weights at a
glance” table.
UAE Residency (E) = 7.5% — CONFIRMED 2026-06-08, captured as a 3-year step band. Founder rationale (accepted): on a visa-linked 60-month product, flight risk is a first-order concern — a fled borrower is the worst default mode, and 3+ years of UAE residency is a genuine threshold of stickiness (roots, schooling, banking history, more durable visa). So the weight stays at 7.5 (not lowered). The “3+ years is meaningfully different” effect is a THRESHOLD, not linear, so it is encoded in the BAND SHAPE rather than more raw weight (raw weight would double-count rootedness already present in tenure C + credit F/G). Residency band anchors (piecewise-linear per §3.4, with a steep step at 36 months): 12mo→35, 24mo→55, 36mo→85, 60mo→100. Crossing the 3-year mark is where the flight-risk benefit lands.
Correction (founder): the earlier “double-counted” framing was wrong. Tenure (Cat C) + seniority (a Cat J modifier) + employer-tier (Cat J) summing in the score is a legitimate sum-of-parts — each is a distinct predictor of staying employed over five years (a 10-year senior at a strong employer is genuinely more stable than a 1-year junior at the same employer). A factor also appearing in a minimum gate (the tenure floor; G12 recourse eligibility) is also legitimate — a hard floor plus a continuous score, exactly like DBR has both a 50% cap and a graded score. So: - Employer stays in the score (Cat C tenure + Cat J sector, now weighted 9 + 15) AND the tenure gate. Not de-duplicated — each contribution is named. - Recourse stays in the score (Cat H, weighted 15) AND G12 eligibility AND G15 coverage. Three distinct questions, each named. - The ONLY real fix is exposing the override (§3.1.2): publish a reason code wherever a gate or decision-ceiling overrides the score, and relabel the score “creditworthiness if the gates pass.” Publish one honest sensitivity sentence per factor (§3.1). No deletion of the score contributions, no multiplicative cap.
Replace highIsGood/lowIsGood with
piecewise-linear interpolation between published breakpoints.
Higher-is-better with anchors
[(excellent,100),(good,82),(min,62),(floor,35)]:
const r1 = x => Math.round(x*10)/10;
function highIsGood(v,e,g,m){
if(v>=e) return 100;
if(v>=g) return r1(82 + (v-g)/(e-g)*(100-82));
if(v>=m) return r1(62 + (v-m)/(g-m)*(82-62));
const fl = m*0.7; // publish this explicit floor breakpoint per category
if(v>=fl) return r1(35 + (v-fl)/(m-fl)*(62-35));
return 35;
}
Mirror for lowIsGood (floor 20). Round to 1 decimal.
Lookup categories H and J are unchanged (discrete tiers). Each band
table must publish both endpoints (e.g. “DBR 35% scores 82, DBR 50%
scores 62, linear between”). Removes the 20-point cliff at 45.0%/45.1%
and the 35–45% dead zone.
function decide(p, gates, total, raw){
const hardFail = GATES.some(g=>g.type==='hard'&&gates[g.id]==='fail');
// D5: commission-ONLY income is ineligible at the door (no base salary => decline).
if (hardFail || p.commTier==='auto_decline' || p.commissionOnly) return 'decline';
const manual = Object.keys(gates).filter(k=>gates[k]==='manual_review');
const weakEmp = p.empTier==='T4'||p.empTier==='T5';
const strongRec = p.commTier==='A'||p.commTier==='B';
const weakRec = p.commTier==='C'||p.commTier==='needs_verification';
let band = total>=70 ? 'approve' : total>=55 ? 'committee' : 'decline';
if (weakEmp){
// D5: a strong-recourse community RESCUES a weak-employer file to review in ANY DBR band,
// because every hard gate (incl. G9 DBR<50%) already passed above. NO extra 45% floor.
if (strongRec) band = (p.empTier==='T5' ? 'manual' : 'committee');
else if (weakRec) band = 'decline';
else band = 'manual'; // unknown recourse -> manual
} else {
band = worse(band, EMP_MAX_BAND[p.empTier] ?? 'committee'); // T1/T2 approve-capable, T3 committee
}
band = worse(band, REC_MAX_BAND[p.commTier] ?? 'manual'); // recourse eligibility ceiling
if (manual.indexOf('G13')!==-1) band = worse(band,'manual');
if (manual.length>0) band = worse(band,'committee');
return band;
}
function worse(a,b){ return BAND_RANK[a] <= BAND_RANK[b] ? a : b; }
Caller: decide(p, gates, sc.total, sc.raw). Two
D5 changes from the earlier draft: (1) the
affordOK (DBR≤45% + 6mo) floor is REMOVED — the hard 50%
DBR gate is the only affordability line for the recourse rescue; the
60-month stress gates (§3.12) are the backstop. (2) The
commission-broker 24-month-statement rescue path is REMOVED entirely —
commission-only income is simply ineligible
(p.commissionOnly short-circuits to decline). The thin-file
standalone committee cap is also dropped (§3.11).
D1 visible-override requirement. Every branch in
decide() that returns a band other than the pure score
outcome
(total>=70?approve:total>=55?committee:decline) MUST
attach a published adverse-action reason code (§3.16) and return it
alongside the score,
e.g. { band:'decline', score:82, reason:'AA10 — T5 employer in a weak-recourse community' }.
The displayed score is labelled “creditworthiness if the gates pass,”
NOT “the decision,” so an 82-next-to-a-decline is self-explaining rather
than a silent flip. No total multiplier / capacity cap is
applied to the score (§3.1) — the score is the raw re-weighted sum
(§3.2); the override is the visible, reason-coded layer on top.
Six edits to index.html. Full rows below.
(1) EMPLOYERS array (lines 854–856):
{ name:'ADNOC', aliases:['ADNOC','Abu Dhabi National Oil Company'], tier:'T1', industry:'State energy', macro:{peace:'T1','crisis-iran':'T1'} }, // T2->T1, closes R2
{ name:'SEHA', aliases:['SEHA','Abu Dhabi Health Services','SEHA Hospital'], tier:'T1', industry:'Public healthcare (counter-cyclical)' }, // T2->T1
{ name:'DHA / Dubai Health', aliases:['DHA','Dubai Health Authority','Dubai Health','Latifa Hospital','Rashid Hospital','Dubai Hospital'], tier:'T1', industry:'Public healthcare (counter-cyclical)' }, // NEW
{ name:'Microsoft UAE', aliases:['Microsoft','Microsoft UAE','Microsoft Gulf'], tier:'T2', industry:'Big Tech (UAE-domiciled, local contract)', seniority:[
{ match:'l7|director', tier:'T1', reason:'Big Tech Director (L7+) on a UAE contract resolves to T1-adjacent capacity.' },
{ match:'senior engineer l6|principal', tier:'T2', reason:'Senior engineering at L6+ holds T2.' } ] },
{ name:'FAANG Remote', aliases:['FAANG Remote','Remote FAANG','Meta Remote','Google Remote','Amazon Remote','Apple Remote'], tier:'T3', industry:'Foreign employer, remote worker', note:'Foreign-regional/remote: 2025-26 AI-layoff exposed; no UAE EOSB. Caps at committee.' },
{ name:'Mediclinic / Aster / NMC (clinical)', aliases:['Mediclinic','Aster','Aster DM Healthcare','NMC','NMC Health','Mediclinic Middle East'], tier:'T3', industry:'Private healthcare (clinical staff)', seniority:[
{ match:'consultant|head of department|medical director', tier:'T2', reason:'Senior consultant / medical director resolves to T2.' } ] },
{ name:'Better Homes', aliases:['Better Homes','Betterhomes'], tier:'T5', industry:'Residential real estate brokerage', commissionBroker:true, note:'A commission-ONLY broker is INELIGIBLE (D5) — no base salary => decline at the door. An applicant from a brokerage who ALSO holds a base salary is assessed on that base salary (commission at 50%, §3.7); the recourse rescue (§3.5) then applies normally on the base-salary file.' }
(2) EMP_INDUSTRY array (replace 888–903): State energy T2→T1; Public healthcare T2→T1 (“most counter-cyclical UAE sector”); ADD “Healthcare (private / clinical staff)” T3; SPLIT Big Tech → “Big Tech (UAE-domiciled entity, local contract)” T2 and “Big Tech (foreign-regional / remote / offshore contract)” T3 (“AI-layoff exposed; no UAE severance”). Brokerage/commission/cabin-crew/gig stays T5 with the 24mo precondition note. Self-employed stays manual review (R7).
(3) G_INDUSTRY array (replace 921–935):
state_energy T1;
public_health T1; ADD
{v:'healthcare_clinical', l:'Healthcare (private / clinical)', tier:'T3'},
{v:'bigtech_uae', l:'Big Tech (UAE-domiciled)', tier:'T2'},
{v:'bigtech_remote', l:'Big Tech (foreign / remote)', tier:'T3'}.
(4) EMP_SENIORITY array (add to 904–912):
{ where:'Big Tech (UAE-domiciled)', match:'Director (L7+), Principal / Senior Engineer (L6+)', tier:'T1/T2' },
{ where:'Private healthcare (clinical)', match:'Consultant, Head of Department, Medical Director', tier:'T2' }.
(5) EMP_DOCS T5 row: “Manual underwriting; strong re-leasing recourse required. Commission-ONLY applicants are ineligible (D5) — a base salary is mandatory. An applicant who holds a base salary plus brokerage commission is assessed on the base salary (commission counted at 50%, §3.7); a strong-recourse community can then carry the weak-employer file per §3.5 as long as all hard gates pass.”
(6) NEW EMP_GROWTH array (forward affordability projection — never project growth on volatile income):
const EMP_GROWTH = [
{ tier:'T1', annual:0.03, note:'Public/sovereign payroll: +3%/yr.' },
{ tier:'T2', annual:0.02, note:'Blue-chip / state-adjacent: +2%/yr.' },
{ tier:'T3', annual:0.01, note:'International / professional services: +1%/yr.' },
{ tier:'T4', annual:0.00, note:'Local SME / mainstream private: flat.' },
{ tier:'T5', annual:-0.01, note:'Commission / brokerage / gig: -1%/yr conservative decline.' }
];
J_LOOKUP mapping (T1=100, T2=86, T3=62, T4=0, T5=0) is
unchanged — only the employer→tier mapping moves. Update RECON R2 →
“resolved: ADNOC ratified to T1.” Whole table flagged
needs-ratification, owner=Gabriel.
// RESOLVED 2026-06-08: rental from a registered Ejari lease = 70% (lease-evidenced, low volatility); all other additional/commission = 50%.
qualifyingIncome = baseSalary + 0.70*ejariRentalIncome + 0.50*additionalIncome + 0.50*commissionIncome;
// D5: commission-ONLY income is ineligible — a base salary is mandatory.
const commissionOnly = baseSalary < MIN_BASE_SALARY && commissionIncome > 0; // MIN_BASE_SALARY = the G6 base floor
// commissionOnly short-circuits decide() to 'decline' (§3.5).
Use qualifyingIncome (not gross declared) in: Category D
bands, gate G6, and the DBR denominator for Category A / G9 /
soonerDbr. Reasoning for the haircuts:
additional and commission income are volatile and not contractually
guaranteed over a 60-month horizon, so half-weighting is the standard
lender treatment — it credits the income without letting one good bonus
year qualify a file base salary alone cannot carry. Rental income from a
registered Ejari lease is lease-evidenced and
materially less volatile than commission, so it counts at
70%, not 50% — this reconciles the two cluster variants
(50%-flat vs 70%-rental) into one rule. Critical: the
G6 household-income FLOOR (≥25,000) must be tested on base
salary alone — haircut income cannot lift a borrower over the
G6 hard gate, only improve the Category D score above it. Add a
base/rental/additional/commission split in the playground so the haircut
is visible. Publish: “Base salary counts in full. Rental income from a
registered lease counts at 70%. All other additional and commission
income counts at 50%.”
const soonerMo = soonerMonthly(p.principal, p.flatRatePct);
const dbrTotal = (p.existingNonMortgageDebtService + p.bankMortgageInstalment + soonerMo) / p.grossMonthlyIncome * 100;
// assert bankMortgageInstalment is NOT also inside existingNonMortgageDebtService (single-count guard)
r.G9 = (dbrTotal < 50 && p.totalDebt < 60) ? 'pass' : 'fail';
Category A consumes the SAME dbrTotal (single source of
truth). Add detected BNPL obligations (Lean parse, Gov-h) as a line in
existingNonMortgageDebtService. Resolve R9 ratify→resolved
on wiring.
rejectType is a first-class property of every
Gate and Decision Rule —
'hard' | 'soft' | 'mixed': - hard —
failing input is about WHO the applicant is, immutable near-term →
dead-end (reapply when it changes). Gates/rules: G1/G11 residency, G2
age, G3/G4 credit (AECB score + defaults), G7 tenure, G16 savings
buffer, employer-tier / commission-only, and the ABSOLUTE income floor.
- soft — failing input is about WHAT the applicant
chose, changeable now → return a counter-offer, never a dead-end.
Gates/rules: G8 facility-size/fee% (property-driven), G10 ready-freehold
(property choice), G12 community eligibility, G13 rental depth, G15
coverage (property+area), and DR1 weak-employer+weak-recourse (the
recourse leg is a CHOICE → location path). - mixed —
depends on the cause; the engine runs the test “would a cheaper
property or a different area clear this gate, holding the applicant
fixed?” → if yes, treat as soft: - G9 DBR: soft on
TWO levers — (a) the chosen property drives it → reduce budget; (b)
existing non-mortgage debt drives it AND the applicant has the savings
to settle that debt → deleverage (settle AED Y of debt, which removes it
from the DBR numerator). Guardrail: lever (b) only stands if
savings ≥ debtToSettle + requiredBuffer (the 3-month G16
reserve must survive the paydown), else it just trades a DBR fail for a
savings-gate fail. HARD only when neither lever works (debt too large to
settle from savings AND already at the floor property). - G6
income: soft against the per-deal structural floor (cheaper
property → lower required income); hard below the absolute AED 25,000
floor.
Precedence (founder call 2026-06-08): hard
dominates. If ANY hard reject fires → hard decline, and the
soft counter-offer is SUPPRESSED (do not tell a credit-declined
applicant “you’d qualify cheaper” — false hope + a fair-lending hazard).
Only a file whose failures are ALL soft gets the path-to-acceptance
below. The engine computes
rejectClass = anyHard ? 'hard' : (anySoft ? 'soft' : 'pass').
Soft reject → counter-offer. A soft decline returns the controllable levers (the original D7 path, now the soft-reject handler):
Evaluated immediately after G6, before scoring:
const requiredIncomeFloor = (p.bankMortgageInstalment + soonerMo) / 0.50;
r.G6 = (p.qualifyingIncome>=25000 && p.qualifyingIncome>=requiredIncomeFloor) ? 'pass' : 'fail';
// fail reason distinguishes 'below 25k backstop' vs 'below structural floor for this facility'
Publish: “The AED 25,000 floor is a backstop; the binding floor is computed per deal as (mortgage + Sooner monthly)/0.50, which is what G9 enforces. The two are reconciled so no borrower passes the income floor only to be declined later on DBR.”
Path-to-acceptance (founder 2026-06-08) — controllable declines return a counter-offer, not a dead-end. Affordability misses driven by a CONTROLLABLE input (the borrower’s property budget) must not be flat-declined. Split decline reasons: - Uncontrollable (residency G1/G11, age G2, AECB defaults G3/G4, employer tier) → hard decline. - Controllable (budget too high for income, i.e. G6/G9 fails only because the chosen property price drives mortgage + Sooner monthly over 50% DBR) → conditional decline WITH A PATH. Back-solve the 50% DBR ceiling to the maximum supportable property price and financed fee, and return a counter-offer instead of “declined”:
// max total monthly debt service at the 50% ceiling, net of existing non-mortgage debt
const maxDebtService = 0.50*p.grossMonthlyIncome - p.existingNonMortgageDebtService;
// Sooner monthly scales with the financed fee (~8% of price); mortgage instalment scales with the mortgage (price - downpayment).
// Solve maxDebtService = mortgageInstalmentPerAed(price) + soonerMonthlyPerAed(0.08*price) for price:
const maxAffordablePrice = solveMaxPrice(maxDebtService, p); // monotonic; bisection on price
const maxFinancedFee = 0.08*maxAffordablePrice; // indicative; actual = invoiced closing stack
if (controllableFail) { r.decision='conditional'; r.maxAffordablePrice=maxAffordablePrice; r.maxFinancedFee=maxFinancedFee; r.reasonCode='AA04-PATH'; }
// DBR deleveraging lever (founder 2026-06-08): if existing non-mortgage debt is what pushes DBR over 50%,
// and the applicant can settle enough of it from savings while keeping the 3-month buffer, offer that path too.
const requiredBuffer = 3 * (p.bankMortgageInstalment + soonerMo); // the G16 reserve, post-deal
const dbrOver = Math.max(0, dbrTotal - 50); // pp over the ceiling
const debtToSettle = (dbrOver/100) * p.grossMonthlyIncome; // monthly service to remove; map to principal via the debt's factor
if (dbrFailFromExistingDebt && p.savings >= debtToSettle + requiredBuffer) {
r.decision='conditional'; r.settleDebtToQualify=debtToSettle; r.reasonCode='AA07-PATH';
}
Publish/UX: “You qualify at up to AED X property / AED Y financed fee
at your current income (reduce your budget), OR by settling AED Z of
existing debt (you have the savings to do so and keep your reserve).”
New output fields maxAffordablePrice +
maxFinancedFee (budget lever, AA04-PATH)
and settleDebtToQualify (deleverage lever,
AA07-PATH, only when
savings ≥ debtToSettle + requiredBuffer).
Second controllable lever — LOCATION (D7, extended 2026-06-08). A decline can also be driven by the community the borrower chose (an auto-decline community per G12, or a community-tier that fails the recourse rules). That is controllable — the same borrower may qualify in a different area. So when the ONLY failing reason is the community:
// re-run the community-dependent gates (G12 + recourse cap) across the eligible community set, holding the rest of the profile fixed
const eligibleCommunities = COMMUNITIES.filter(c => c.tier!=='auto_decline')
.filter(c => decideWithCommunity(p, c) !== 'decline')
.map(c => ({ name:c.master, tier:c.tier }));
if (communityOnlyFail && eligibleCommunities.length) {
r.decision='conditional'; r.eligibleCommunities=eligibleCommunities; r.reasonCode='AA09-PATH';
}
Publish/UX: “Your profile qualifies — but not in {chosen community}.
You would qualify in {eligible areas}.” New output field
eligibleCommunities + reason code
AA09-PATH. For everything Sooner controls (budget, what
they buy, AND where they buy) the default is rejection-with-a-path,
never a dead-end no.
r.G2 = (p.age>=21 && p.age+5<=65) ? 'pass' : 'fail';
Card: “age ≥ 21 at origination AND age + 5 ≤ 65 at maturity (60-month
tenor).”if(!p.aecbPulled) return 'insufficient_data'; r.G3 = (p.aecb>=550 || p.thinFileConfirmedByPull) ? 'pass' : 'fail';
New inputs aecbPulled,
thinFileConfirmedByPull. Thin-file is set BY the pull, not
a free toggle; relabel the playground toggle “AECB pull returned thin
file.” Closes the G4 (no-defaults) bypass.const feePctOfValue = p.price>0 ? (p.principal/p.price)*100 : 0; r.G8 = (feePctOfValue>=6 && feePctOfValue<=11 && p.principal>=30000 && p.principal<=750000) ? 'pass' : 'fail';
Floor 50k→30k. Card copy: “Sooner finances the ACTUAL invoiced closing
fees, indicatively 6–11% of value (a guardrail, not a fixed assumption),
AED 30,000 to 750,000.”raw.G = thin ? 70 : highIsGood(p.aecb,740,680,550);
Substitute 70 (average-to-good), NOT a conservative 35.
Founder decision: Sooner is not an unsecured lender — the master-lease
property recourse offsets unknown credit history, so a
thin file is not the elevated risk it is for an unsecured product. CATS
G band label: “thin-file substitute 70 (average-to-good — property
recourse offsets absent history).” Keep F at 0% and G at 15% on the
thin-file path. No standalone committee cap on thin
files (the earlier conservative cap is dropped); the recourse
decision ceiling (REC_MAX_BAND, §3.1) already independently caps a thin
file that also sits in a weak re-lease community, so a thin file in a
thin re-lease market does not get a free pass without double-penalising
thin files in strong-recourse communities.
// NEW hard savings gate
r.G16 = p.savingsMonths >= 3 ? 'pass' : 'fail'; // auto-decline below 3 months
// NEW stressed-DBR check (route to committee on fail)
const haircut = {T1:0.05,T2:0.05,T3:0.10,T4:0.15,T5:0.15}[p.empTier];
const stressedDbr = dbrTotal / (1 - haircut);
if (stressedDbr >= 55) band = worse(band,'committee');
// NEW thin-affordability-corner waterfall step (before auto-approve)
if (p.qualifyingIncome<=27500 && dbrTotal>=47 && p.savingsMonths<=3) band = worse(band,'committee'); // reason AA13
Data reality (founder 2026-06-08): Sooner holds
lease-level DLD rental data — a record of every NEW
lease plus ~50% of renewals (renewals are widely under-registered), each
with price, duration, unit type, beds, building/sub-community and
location. This is near-direct rental data, not a vague proxy and not a
paid vendor. The one thing it does NOT contain is OUR OWN realised
re-lease void days, so the rubric is staged: lease-data signals now →
our measured void days layered in as we operate. Retire the SALES-volume
dldVol field. Fields per (community × unit-type × beds),
joined by PSL code, refreshed monthly: - DLD rental panel
(available today): newLeaseVol12m (count of new
registered leases, trailing 12m, normalised to stock — the PRIMARY depth
signal: how many leases actually get signed = how deep the re-lease
market is), medianRegisteredRent (actual achievable rent,
by unit-type × beds), leaseDurationMix +
renewalGap (turnover/void proxies derived from lease terms
and re-registration gaps), medianSalePrice (for the yield),
newSupplyPipeline12m (public reports).
dldSalesVol drops to a tie-breaker only. - Measured
panel (Stage 2+, layered in as it arrives):
voidDays (median days-to-let from our own realised
re-leases), timeToFirstOffer. - Caveat to
publish: renewals are ~50% under-registered, so the data
over-represents tenant TURNOVER vs stable renewals. This is fine — even
favourable — for measuring re-lease DEPTH (turnover IS the re-lease
market we would tap on default); note it so the occupancy/stability read
is not over-stated.
Signals & scoring: 1. DEPTH
(≈0.45) — at launch on newLeaseVol12m percentile
within unit-type × beds: top-quartile→100; 2nd→82; 3rd→62; bottom→42;
null→50 (conservative floor). Once a community has ≥5 realised
re-leases, layer in measured voidDays: <30→100;
30–45→85; 45–60→70; 60–90→55; 90–120→42; >120→30.
dldSalesVol only breaks ties; it is never the primary depth
driver (using sales for rental depth was the original R-b bug). 2.
STABILITY (≈0.35) on rent trend + supply: trend
>−2%→88; −2 to −5%→72; −5 to −9%→55; <−9%→40. Subtract up to 15pts
if newSupplyPipeline12m > 15% of stock. null→50. 3.
RECOVERY (≈0.20) = NET-OF-VOID achievable yield
= medianRegisteredRent*(1 − voidProxy/365)/medianSalePrice,
scored as a CONDITIONAL inverted-U (R-c): <4%→50; 4–5.5%→75;
5.5–7%→100; 7–8.5%→80. High-yield band (>8.5%) is
conditional, NOT auto-penalised: if
depth>=70 && newSupplyPipeline12m<=15%
(genuinely good high-yield, deep, not oversupplied) → score
90 (high yield is real cash flow, do not assume it
cannot be re-leased); else (high yield + thin depth or heavy supply =
oversupply signature) → 55. The markdown lands on the
distress signature, never on the yield number itself.
recourseScore = 0.45*depth + 0.35*stability + 0.20*recovery.
deriveTier: ≥79 A; 65–78 B; 50–64 C; <50
needs_verification; hardBlock → auto_decline. Coherence
(R-f): the SAME void figure (proxy then measured) feeds
recoveryModel.evictionCarryMonths/collectionWindowMonths
(replace flat 3/12 with community void) and the correlated-void
concentration clusters — one rental-liquidity surface drives ranking +
recovery carry + provision + concentration. Manual override register per
row: {tier, reason, effectiveDate, expiry, setBy}.
Staged calibration (R-e) — event-triggered on realised re-lease COUNT, not a calendar date (we cannot calibrate on re-leases we have not had): - Stage 0 (now → launch): proxy rubric live; every tier committee-overridable. - Stage 1 (~10 realised re-leases OR 2 quarters, whichever first): sanity-check the Ejari-volume depth proxy vs observed void on the first defaults; override obviously-wrong tiers. - Stage 2 (~30 realised re-leases): swap the volume proxy for MEASURED median void days where ≥5 observations exist; re-baseline stability bands off the post-Iran-shock stabilisation window (not the trough). - Stage 3 (~50 realised re-leases / ~200 originations): full back-test of tier rank-ordering vs realised LGD; promote calibrated cut-offs; run AIR/fair-lending back-test + champion-challenger. - After: quarterly proxy refresh; annual full re-calibration. Until Stage 3 the rubric stays flagged “uncalibrated, committee-overridable.” At current expected phase volumes Stage 3 lands roughly when the book crosses ~200 contracts.
Founder decision: Sooner NEVER evicts a paying party — not a paying tenant, not a paying owner-occupier. Eviction happens ONLY on non-payment. A unit is vacant only when (a) it was bought vacant and is being leased, or (b) a tenant leaves before a replacement is found. So recovery cost is not a function of occupancy type — it is the re-lease VOID (days to re-lease at a competitive price), which Sooner minimises by pricing competitively (the operating incentive is to avoid vacancy). A paying sitting tenant is the BEST recovery case: cash flow continues uninterrupted, zero void.
Drop the earlier investor_tenanted Category-H
haircut — it was backwards. Model recovery on void duration
instead: - recoveryModel(p) carry = voidMonths
(community-measured, from the DLD rental data in §3.13; default to the
community’s median void), NOT a flat per-occupancy number. - A unit with
a paying occupant at default → Sooner collects/redirects that rent,
voidMonths ≈ 0 until the lease naturally ends, then the
community void applies. - A non-paying owner-occupier or a vacated unit
→ the community void applies (time to re-lease at competitive market
rent). - propertyUsage is still captured (for
ops/reporting) but no longer drives a Cat H haircut; it only informs
whether a void applies now (vacant/non-paying) or later (paying
occupant).
F-e framing for the recourse section (no new gate): eviction is
non-payment-only; the void + the ~2–4mo possession carry are already in
recoveryModel and the 1%/yr provision; the owner retains
equity and regains control on recovery; competitive re-lease pricing
keeps the void short.
Caps as % of outstanding book per phase (P1 = contracts 0–100; P2 = 101–300; P3 = 301+). Daily automated pre-origination check; any breach routes to committee.
Unchanged single-name + borrower-risk caps: single community 50/25/15; single building 20/10/5; single developer 50/40/30; single borrower 10/5/2; self-employed 20/15/10; AECB 550–620 15/10/10; DBR 45–50% 20/15/10. Credit-tier: A(740+) no limit; B(680–739) 40; C(620–679) 20; D(550–619) 10; <550 ineligible.
Cluster changes: | Cluster | Old (P1/P2/P3) | New (P1/P2/P3) | Reason | |—|—|—|—| | Marina + JBR + JLT | 30/25/20 | 22/18/14 | Premium rents −~15%, JLT vol −60%, Marina −40%; rent-based recovery impaired | | Meydan + Sobha Hartland + Creek Harbour (MBR City belt) | 25/20/15 | 18/12/8 | Sobha Hartland sales −59%, supply-heavy, The Crest tier C; largest correlated-supply pocket | | Al Furjan + Discovery Gardens (JVC removed) | 25/20/15 (was JVC+AF+DG) | 20/15/12 | Al Furjan sales −44%; JVC pulled | | Springs + Greens + Mira | — | 30/25/18 (held generous) | Resilient owner-occupier, ValuStrat positive QoQ, low voids; book should lean here |
New sub-limits: JVC District 5 → 0/0/0 (matches G12 auto-decline). Aggregate recourse-tier-C exposure → 12/8/5. Low-yield-community aggregate (recovery band “low”/“low-mid”) → 25/20/15.
Macro overlay: under a declared “crisis” regime (matching the employer-tier crisis-iran switch), ALL community caps tighten by 5 percentage points automatically. Add correlated-default stress to the daily check (flag if >X% of book sits in communities that moved together in the last correction).
Reconciliation rules: every cluster member must
exist in COMMUNITIES; cluster allowance ≤ single-community cap; any G12
auto_decline community carries an explicit 0% cluster allowance; the
cluster list and ranking must not disagree on direction. Encode as
CONCENTRATION object +
checkConcentration(book, deal, phase). RECON rows R22
(re-baseline) + Gov-g (as-code), status ratify.
demographics
table, no FK the scorer reads. Engine invariant:
scoreProfile/decide input objects contain NONE
of these keys — enforced by a unit test asserting their absence.
Monitoring-only consent line; never shown on the decision screen.decline ~ demographic + AECB + income + DBR + employer-tier + community-tier
to report whether the gap survives credit controls.Add RECON R21, status ratify. Governance callout on the page: monitoring-only wall, AIR<0.80 trigger, reason codes, quarterly cadence. PDPL (Gov-h): add a PDPL consent gate at Form 2 (Lean-connection / OTP step) + a data-retention policy — a compliance deliverable, not an underwriting check.
Structure: EL = PD × LGD × EAD.
EAD by default month m:
outstanding(m) = principal × (1 − min(1, m/60)). Base
principal = 161,490 (8.07% of 2,000,000). OID lifts net capital deployed
from 161,490 to ~112,600, equity-at-risk to ~28,440.
PD: annual 4% base; monthly hazard
h = 1−(1−0.04)^(1/12) = 0.339%/mo. Tier-adjustable
multiplier: T1 0.6×, T2 0.8×, T3 1.0×, T4/T5 or thin-file 1.5×; AECB
550–620 1.4×. Front-load the default-month distribution using the hazard
to show the early-default tail.
LGD (recovery model):
recovery_window = collectMonths × gross_monthly_rent,
collectMonths = 12 − evictionCarry(3) = 9 effective months
(in-house re-lease, no agent fee, Art.25(1) eviction).
gross_monthly_rent = property_value × achievable_net_of_void_yield / 12.
LGD(m) = max(0, 1 − recovery_window/outstanding(m)). Worked
at m=9: outstanding ~136,667; mid-yield (6%) 9mo = 90,000 → LGD 34%;
high-yield (8%) → 12%; low-yield (5%, soft community) → 45%. Late
default (m≥24) LGD → ~0 (outstanding amortised below one year of
rent).
Per-recourse-tier EL: Tier A ~0.5–0.8%/yr; Tier B ~1.0–1.5%/yr; Tier C ~2.0–3.0%/yr. This is what risk-based pricing would key off if introduced.
Net-margin bridge (the P2/P3/R17 answer): premium income (10/12.5/15%/yr flat) + OID (21,000 seller + 16,000 bank) − 15% CoF on 80% debt (~12% blended) − opex (~1%/yr) − EL (1.0%/yr net base) = net spread → net IRR 22.4%, ROE 53.4%, NIM 12.15%.
HTML calculator page —
apps/el-model/index.html, Sooner-branded static, reusing
the exact tokens/CSS from underwriting-criteria/index.html:
- Inputs (sliders/selects): property_value (2,000,000);
fee_pct_of_value (6–11%, 8.07%); pricing_bundle (10/12.5/15);
achievable_yield_band; recourse_tier (A/B/C); annual_PD (4%,
tier-adjustable); eviction_carry_months (3); collection_window_months
(12); cost_of_funds (15%); debt_share (80%); opex_pct (1%);
seller_commission (21,000); bank_referral (16,000); default_month
(1–60). - Formulas (JS): mirror the engine’s
recoveryModel + effectiveAPR +
soonerMonthly. outstanding(m),
windowRecovery, LGD(m),
EL%/yr = PD × portfolio-LGD, net-margin bridge. -
Outputs (right column): decision-style banner “EL
1.0%/yr net | 2.4%/yr gross reserve | net IRR 22.4%”; per-default-month
LGD/EL curve (inline SVG, no deps); per-recourse-tier EL table (A/B/C);
net-margin waterfall (premium → +OID → −CoF → −opex → −EL → net);
effective-APR readout matching the Pricing tab. - Build
notes: AED formatting “AED 161,490”, no em dashes, no
exclamation marks. Add SKIP_PRE_PR_REVIEW=1
mockup-static-html marker + app.json +
README.md per repo rules.
Excel companion — el-model.xlsx (all
cells formula-driven off Inputs; build via the xlsx skill /
openpyxl): - Inputs — labelled parameter block (named
ranges). - Amortisation — 60 rows: month,
principal_outstanding, premium_accrued, cumulative_buyer_payment, EAD. -
PD-LGD — month, monthly_hazard, gross_rent,
window_recovery (9mo), LGD(m), EL_contribution(m); totals row = EL%/yr.
- Tier-EL — recourse tier A/B/C × achievable-yield →
tail-LGD → EL%/yr. - Margin-bridge — per-bundle
(10/12.5/15) columns; rows = premium, +seller 21,000, +bank 16,000, net
capital 112,600, −CoF (15%×80%), −opex, −EL (1%), = net profit, net IRR,
ROE, NIM. Tie-out cells must reproduce 53.4% ROE / 41.5% gross
IRR / 22.4% net IRR / 12.15% NIM / 76,320 5y net profit. -
Stress — swap EL to 2.4% gross reserve + yield band to
“low/soft”; show stressed net IRR (cross-check the M24-worst 14.4% from
the termination-timing table).
Product: Sooner Closing Fee Financing —
Shariah-compliant Forward Ijara (master lease + re-lease).
Provider: Sooner Real Estate LLC under a UAE
property-management licence. Compliance: UAE Federal
Decree-Law No. 6 of 2025 cost-of-credit transparency; ijara language,
not riba/interest. Format: Sooner-branded one-pager
(Manrope, paper/ink palette, yellow accent), AED formatting, no em
dashes, no exclamation marks. Produce as .docx for
any external/customer surface per the external-document-format
rule; an in-app HTML version mirrors the Pricing tab. All AED figures
driven by the same constants as the engine (principal, rate,
effectiveAPR, soonerMonthly) so the KFS never
drifts.
Section 1 — What you are getting. Sooner pays your closing fees up front (DLD 4%, agency 2%, bank arrangement 1%, registration trustee, mortgage registration, valuation, developer NOC, conveyancing + 5% VAT on services), typically 7–10% of value. You do not take a loan; Sooner takes a master lease on your property and re-leases it back to you. Your monthly payment is lease rental, not loan interest.
Section 2 — The numbers (illustrative, AED 2,000,000 base case). | Field | Value | |—|—| | Total amount financed | AED 161,490 (8.07% of value) | | Tenor | 60 months | | Premium (flat on original principal) | 10%/yr (full bundle); up to 15%/yr (CFF only) | | Total premium over term | AED 80,745 (full bundle) | | Total you repay | AED 242,235 | | Total cost of credit | AED 80,745 | | Monthly lease rental | AED 4,037 | | EFFECTIVE PROFIT RATE (APR-equivalent, reducing balance) | ~18.7%/yr (full bundle); up to ~27.7% (CFF only) |
Mandatory disclosure: the flat 10–15%/yr headline is NOT the effective cost. Because you repay in equal instalments while the premium is charged on the original amount, the true reducing-balance rate is roughly 1.85× the flat headline. The effective figure is the one to compare against any other financing.
Section 3 — What you pay for, and what you do not. You pay the lease premium on the financed closing fees only. You do NOT pay extra for Home Discovery or Mortgage Brokerage — Sooner is paid for those by the seller (sale commission) and the bank (referral). That is why bundling more services LOWERS your premium.
Section 4 — Early settlement (Forward Ijara actual-loss compensation, not a penalty). Months 1–6: 6% of remaining balance; 7–12: 5%; 13–24: 4%; 25–36: 3%; 37–48: 2%; 49–60: 1%. Minimum floor: greater of 6% of remaining balance or six months of premium. Context: UAE personal loans typically charge 1–3% flat; Sooner’s schedule declines to 1% in the final year.
Section 5 — What happens if you do not pay. You keep your property, title, and bank mortgage at all times; Sooner’s recourse is the master lease, NOT a charge on your ownership or equity. If you stop paying, Sooner may take possession under Dubai Law 26/2007 Article 25(1) (30-day notice) and re-lease to recover the amount outstanding. Once Sooner has recovered the outstanding balance from re-lease rent, Sooner EXITS the master lease and you regain full possession, with your mortgage and equity intact. You never lose your ownership stake.
Section 6 — Is this worth it (illustrative, not
guaranteed). Dubai property has averaged ~+5%/yr, so a
2,000,000 home gains ~100,000/yr; buying earlier through Sooner captures
that appreciation and avoids continued rent, generally exceeding the
premium over a typical hold. Compare your total cost using the
rent-vs-buy calculator:
https://sooner-rent-vs-buy.pages.dev/. Property values can
fall as well as rise; the Section 2 figures are the guaranteed terms,
the value figures here are not.
Section 7 — Your obligations and our monitoring. Affordability is assessed at origination including this monthly rental in your total debt-burden ratio (must stay under 50%). Sooner maintains a continuous Lean Tech (UAE open banking) connection to your nominated account for the life of the facility.
Section 8 — Governing terms. A civil master lease and re-lease under a property-management licence; not a finance-company consumer loan. Full terms in the Master Lease Agreement and Re-Lease Agreement, which prevail over this summary. Cost-of-credit transparency per Federal Decree-Law No. 6 of 2025.
buildKFS(p) computes Sections 2/4/5–7 from the actual
financed amount and selected bundle; Sections 5/8 are fixed copy;
effective APR from effectiveAPR(ratePct). The KFS must be
shown BEFORE any customer commits (regulatory), generated per-deal at
offer, and stored.
Recommendation: one shared contract module, two consumers
(canonical page + forms), one server-side authority (engine). No logic
fork is possible because all three import the same
underwriting-contract.js. This is the structural
fix for R-d engine-parity and Gov-f.
apps/underwriting-criteria/Layer 1 — underwriting-contract.js (the
source of truth; plain ES module, zero DOM, deterministic):
export const CONTRACT = {
version: '1.0.0', ratifiedAt: null,
product: { name:'Closing Fee Financing', structure:'Forward Ijara / 5-year master lease',
tenorMonths:60, premiumFlatPctRange:[10,15], feePctOfValueRange:[6,11] },
gates: GATES, pdGates: PD_GATES,
categories: CATS, // A-J re-weighted to sum 100 (J RETAINED; D1/S-i, §3.2)
decisionSteps: DECISION_STEPS,
pricing: PRICING, // bundle -> rate + effectiveAPR column
employers: EMPLOYERS, empRules: EMP_RULES, empIndustry: EMP_INDUSTRY, empGrowth: EMP_GROWTH, // S-f
communities: COMMUNITIES, ranking: RANKING, rankWeights: RANK_WEIGHTS, // Gov-f precedence
concentration: CONCENTRATION, // Gov-g / R-h, as code
el: EL_MODEL, // C6 / L5
haircuts: { additionalIncome:0.5, rentalIncome:0.7, commission:0.5 } // S-h (reconcile rental, Section 6)
};
export function buildProfile(formState){...}
export function evalGates(p){...}
export function scoreProfile(p){...} // S-a caps + S-j interpolation
export function decide(p,gates,score,raw){...} // simplified waterfall, S-b
export function recoveryModel(p){...} // L6 propertyUsage differential
export function expectedLoss(p){...} // 4.1
export function buildKFS(p){...} // 4.2
export function checkConcentration(book,deal,phase){...}
export function resolveCommunityTier(building){...} // named carve-out > data rank > manual (Gov-f)
export const FIELD_MAP = {...}; // promoted from the data-dictionary feeds[] inventory
Layer 2 — index.html renders FROM the
module. Replace the ~700 lines of inline consts with
import { CONTRACT, scoreProfile, decide, ... } from './underwriting-contract.js';
every tab iterates CONTRACT.*, no duplicated literals. The
playground calls the SAME scoreProfile/decide
the forms and engine call.
Layer 3 — versioning + ratification ledger. Header
version badge (CONTRACT.version), ratifiedAt,
git SHA in footer. New “Ratification ledger” tab rendering every RECON
item with status==='ratify' as
{id, title, recommendation, owner:'Gabriel', ratify/hold toggle}.
Signing an item sets CONTRACT.ratifiedAt and flips it to
resolved.
Layer 4 — new tabs. “Expected loss” (renders
EL_MODEL), “Key Facts Statement” (renders
buildKFS(playgroundProfile)), “Concentration” (renders
CONCENTRATION by phase + cluster map). Plus a “Download
contract JSON” button serializing CONTRACT →
underwriting-contract.json so the engine team can diff the
deployed config against the canonical truth.
FIELD_MAP is promoted from the data-dictionary
feeds[] inventory (it already exists as data — this makes
it the contract), keyed
formFieldId → {gates:[], cats:[], step, requiredFor:[]}: -
Form 1 (lead.tsx): budget→{G8,G10}; areas→{G12,catH};
residency→{G1,G11,catE}; fixedIncome→{G5,G6,catD}; employmentType→{G14};
propertyType→{catH}. - Form 2 (preapproval.tsx):
serviceBundle→{pricing}; income→{G5,G6,catD}; additionalIncome→{catD,
haircut 0.5}; employerName→{catJ,G7}; roleSeniority→{catJ};
tenure→{G7,catC}; mortgage/personalLoan/carLoan/cards→{G9,catA};
savings→{catB}; aecbBand→{G3,catG}; propertyUsage→{catI, extend to catH
per L6}. - Form 2.5 (co-applicant): mirrors F2
employment + liabilities → joint G6/G9/catD recompute. - Form
2.7 (property): community/building→{G12,catH};
listingPrice→{G8,G10,catA,catI}; ready→{G10}; freehold→{G10}. -
Form 3 (documents.tsx): each upload VERIFIES a declared
value, flipping collect:'declared'→'verified' and
triggering the authoritative engine pass.
budget×8%×premium/60, warn >50%); Cat A/B/D preview;
employer-tier resolve (J). Show an indicative Likelihood
chip (approve/committee/decline) from the SAME
decide(), labelled “indicative — final after
documents.”buildProfile(verifiedState) → evalGates → scoreProfile → decide
on VERIFIED inputs. This is the only binding decision. Client output is
never trusted.underwriting-contract.js from
the canonical page (Layer 1). Publish as workspace package
@sooner/underwriting-contract. Apply all Section-3 fixes IN
the module.decide() output on the page.sooner-underwriting-engine/apps/web); wire
FIELD_MAP-driven client checks per 5.3; add the Likelihood
chip.decide() output across (a)
canonical playground, (b) form client chip, (c) engine server pass. A
diff fails the build. This is the structural guarantee of one source of
truth.Why this order: the contract module ships FIRST so every later consumer imports rather than re-implements. Forms get instant-feedback UX for free (same fns). Engine parity (R-d) becomes structural, not a perpetual reconciliation chore. The canonical page stops being a mockup and becomes the literal code the product runs. Track engine parity as a single Linear ticket so “is it fixed” has one answerable status; until the engine imports the module, add a banner to the Reconciliation tab: “ENGINE PARITY: the live engine still hard-declines T4/T5 and takes commTier as input. This page shows the ratified target. Parity ships when the engine imports underwriting-contract v1.0.”
Recourse ranking is render-only (R-d). Still an
offline exhibit; the engine takes tier as a set input. Blocked on:
ratify the R-b/R-c/R-f rubric → then build
resolveRecourseTier(propertyPSL, unitType), wire R10 into
the live waterfall, emit the three audit signals. Sequence AFTER rubric
ratification.
Rubric calibration (R-e) — “when?” answered. Event-triggered on realised re-lease COUNT, not a calendar date (impossible to back-test without realised outcomes). Stage 1 at ~10 re-leases (or 2 quarters): sanity-check the Ejari-volume depth proxy vs observed void; override obviously-wrong tiers. Stage 2 at ~30 re-leases: swap the proxy for measured median void days (≥5 obs/community); re-baseline bands off the post-Iran-shock stabilisation window (not Feb-2026 pre-shock or the trough). Stage 3 at ~50 re-leases / ~200 originations: regress realised LGD/void on each signal, drop non-discriminating signals, re-weight survivors, set A/B/C cut-offs at the LGD breakpoints, run AIR + champion-challenger. After: quarterly proxy refresh + annual full re-calibration. At current expected phase volumes Stage 3 lands ~when the book crosses 200 contracts. Until Stage 3 the rubric stays “uncalibrated, committee-overridable.” Full schedule in 3.13.
Engine parity (R-d engine). Three queued, none
merged: R4 ranking→engine wiring, R10 employer×recourse offset, the
deriveTier resolver replacing the PSL lookup. Closes
structurally when the engine imports the contract module (Section
5).
Data source (R-b/R-f) — corrected 2026-06-08. No rental-liquidity API is available, so the rubric runs on PROXIES we already hold: DLD Ejari rental-contract registration volume (primary depth), listing absorption (Bayut/PF, optional), DLD sales volume (secondary), DLD Rental Index + ValuStrat/Asteco/CBRE reports (trend/supply), and DLD-derived gross yield (Ejari rent ÷ sale price). Build task: assemble the DLD/Ejari + reports panel per (community × unit-type); no vendor contract blocks launch. The measured void/time-to-lease signals arrive from Sooner’s OWN realised re-leases at Stage 2 and sharpen the EL LGD tail then — they are not a pre-launch dependency.
Founder ratification (the ledger). Needs Gabriel’s sign-off to flip resolved: R2 (✔ ADNOC→T1 decided), R5 (additional-income haircut 50%), R7 (self-employed→manual), R9 (DBR from primitives), R10 (employer×recourse + affordability floor), R15 (KFS/APR actual-loss framing), R16 (recovery data source), R17 (provision — see #8), R18 (60mo affordability gates), R19 (score caps/honesty), R20/R21/R22/R23.
Spec variants — ALL RESOLVED 2026-06-08. (a)
Capped-score mechanism: DROPPED (D1). No
capacityFactor, no min() cap; the score is an additive
sum-of-parts (J retained), re-weighted (§3.2), with a visible
reason-coded override (§3.1). (b) Thin-file substitute: 70
average-to-good (D3), not 35 — property recourse offsets
unknown credit history. (c) Rental-income haircut: rental from a
registered Ejari lease = 70%, all other additional +
commission = 50% (D4/§3.7). None open.
Legal — early-settlement framing (R15). Confirm the flat-on-original-principal premium and the declining early-termination schedule against the Forward Ijara actual-loss-compensation / compound-interest rules with counsel before the KFS is customer-facing. Also confirm PDPL obligations on the Lean open-banking connection (explicit consent to pull/store bank data for the facility life, data-minimisation, right-to-erasure on decline) — a separate compliance workstream, not an underwriting check.
The 4% vs 1% reconciliation — CANONICAL FIGURE.
They are NOT contradictory: 4% is the gross annual PD
(probability of default); 1% is the net annual expected-loss provision
after recovery; implied portfolio LGD ≈ 25%. The bridge:
EL%/yr = PD × LGD, so
4% PD × ~25% blended LGD ≈ 1.0% net EL/yr. The model’s
monthly default reserve (~328.5 ≈ 2.4% of principal/yr) is the GROSS
tail reserve, sized to the worst early-default tail
(4% PD × ~60% tail-LGD).
Publish three numbers, stop conflating them:
DEFAULT_PROVISION_PCT = 1 in code is the NET figure.Cross-check: 5-year EL ≈ cumulative-PD(18%) × LGD(25%) × weighted-EAD(~80,745) ≈ AED 3,633/deal ≈ 1%/yr of book over 5y — the two methods reconcile, validating the 1%/yr provision, and ~3,633 EL is comfortably covered by the 12.15% NIM. R17 status: validated, not owed.
Files to create:
apps/underwriting-criteria/underwriting-contract.js;
apps/el-model/{index.html, app.json, README.md};
el-model.xlsx; the KFS .docx (external) +
in-app HTML tab; @sooner/underwriting-contract workspace
package. Files to edit:
apps/underwriting-criteria/index.html (all Section-3
specs);
sooner-underwriting-engine/apps/web/pages/apply/{lead,preapproval,documents}.tsx
(FIELD_MAP client checks + Likelihood chip); engine server scorer
(import the contract module).