A quantitative decision framework for MIGA, IBRD, IDA, regional MDB, bilateral DFI, and ECA guarantees on infrastructure projects, implemented on the Austral Platform
Author: David Duarte Arancibia, Austral Intelligence Date: 5 May 2026 · Revised: 23 October 2026 (see Errata, Appendix F) Audience: Sponsors, project finance advisors, MDB credit officers, Ministry-of-Finance fiscal-risk teams, lenders Companion software: Austral platform —
https://platform.austral-intelligence.com
A third-party guarantee — from the Multilateral Investment Guarantee Agency (MIGA, World Bank Group), the International Bank for Reconstruction and Development (IBRD), the International Development Association (IDA), a regional multilateral development bank (MDB), a bilateral development finance institution (DFI), an export-credit agency (ECA), or a private insurer — can move an infrastructure project from "unbankable in the current market" to "investible." The cost is the guarantee fee. The decision is rarely posed as a calculation. It is usually treated as a heuristic ("this country needs Political Risk Insurance (PRI)") or a sales conversation with the lenders.
This paper takes the central planner's perspective — the Ministry of Finance or sector regulator asking whether the guarantee reduces the all-in cost paid by whoever ultimately funds the project (users in user-pays contracts, the host government in availability-payment contracts) by more than the fee. The framework runs the project finance (PF) model twice — once at market terms, once with the guarantee in place — and in each run solves for the tariff (or availability payment) that produces the sponsor's required equity Internal Rate of Return (IRR). Critically, the required IRR itself is not held fixed: when the guarantee transfers project risk to a guarantor, the sponsor's equity hurdle drops by Δhurdle = λ × Δspread (Δspread expressed as a fraction — e.g. 237 bps = 0.0237 — and product-specific λ ≈ 1.1–1.9, derived in Appendix E), so the planner captures both the lower debt cost and the lower required equity premium. The society's NPV saving — the difference in the all-in cost stream — is the right comparison to the fee. The decision rule is direct: take the guarantee if and only if society's saving is positive.
The framework distinguishes two cases. Feasibility-enabled — Run A is infeasible at any tariff: the project does not exist without the guarantee, and the decision is a cost-benefit-analysis (CBA) question outside the scope of this paper. Cost-reduction with rational pricing — both runs are feasible at some tariff: the framework computes society's saving directly. A third behavioural mode, sponsor capture, occurs only when contract structure prevents the savings from passing through; the framework flags this as a market-friction warning rather than treating it as a separate equilibrium.
The framework is implemented end-to-end on the Austral Platform. A worked example for a frontier-market US$ 500 M solar Independent Power Producer (IPP) closes the paper, ranking three candidates at 90 % coverage: an IBRD Project-Based Guarantee (society saving +US$ 527 M, the dominant verdict), MIGA Political Risk Insurance (+US$ 241 M), and an OECD-Arrangement ECA cover (+US$ 94 M, very tenor-sensitive). The planner-view society savings are dramatically larger than the sponsor-view differentials — for MIGA-PRI the sponsor view says reject (−US$ 12 M) while the planner view says take (+US$ 241 M), because in equilibrium the lower hurdle delivers a tariff reduction that the sponsor-view framing entirely misses7.
Keywords: Multilateral guarantee, MIGA, IBRD, IDA, ECA, Political Risk Insurance, project finance, bankability, guarantee fee, OECD Arrangement, audit-evidence.
Feedback & discussion. This paper is published openly so that practitioners — sponsors, MDB credit officers, Ministry-of-Finance fiscal-risk teams, advisors, lenders — can challenge it, refine it, and put it to work on real projects. We welcome corrections, counter-examples, and proposed extensions on any section.
- Comment in-page at
https://austral-intelligence.com/research(open comments on every section)- Email the author: [email protected]
- Live demo or walkthrough on request
Appendices A, B, C, D, E, F follow the page break.
Why this matters: a third-party guarantee can be the difference between a project closing and a project never breaking ground; the fee is real cash that the project pays out year after year, so the decision should not be made on instinct.
A typical situation. A solar IPP with a US$ 500 M total cost in a frontier-market jurisdiction is presented to commercial lenders. The market response is a 600 basis points (bps) spread over the local benchmark, a 12-year tenor, and a 1.40× minimum Average Debt-Service Coverage Ratio (Min ADSCR) covenant. At those terms the equity Internal Rate of Return (IRR) drops below the sponsor's 18 % hurdle and the DSCR is uncomfortably tight, so the project is unbankable. With a MIGA Political Risk Insurance (PRI) cover at 90 % of the senior debt principal, the same lender offers 350 bps, an 18-year tenor, and a 1.30× DSCR covenant. MIGA charges roughly 1.4 % per annum on the covered amount, plus a 50 bps upfront fee at financial close1. The question is direct — is the project better off taking the guarantee, or rejecting it and either restructuring or walking away?
The same question arises for IBRD Project-Based Guarantees (PBGs)2, IDA PBGs2, International Finance Corporation (IFC) Partial Credit Guarantees (PCGs)3, Asian Development Bank (ADB) PCGs4, OECD-Arrangement Export Credit Agencies (ECAs)5, and private PRI providers (Lloyd's, AIG, Sovereign, Zurich). The fee schedules differ. The counterfactual debt-term reductions differ. The decision logic is identical.
Practitioners answer this question today using one of three modes9:
The framework in this paper replaces all three modes with a deterministic, replicable, audit-tracked computation.
Why this matters: a one-shot "compare with vs without" is not enough — the variables feed back into each other, and a defensible recommendation has to handle that feedback explicitly.
Given a project specified by a Financial-Model
ModelInputspayload with construction schedule, revenue stack, operating-cost stack, tax regime, and a senior commercial debt tranche priced at the current market.Given a candidate guarantee product specified by a
GuaranteeProducttriple —(provider, instrument, coverage_pct)— and a country-risk profile specified by aCountryRiskInputstriple —(country_code, OECD Country Risk Class (CRC) ∈ [0..7], sovereign_rating).For each run (A — baseline; B — with guarantee), solve for the year-1 tariff (or availability payment)
τsuch that the sponsor's realised equity Internal Rate of Return (IRR) equals the run-specific required hurdleh:
IRR_equity(τ_A | inputs_A, h_A) = h_Awithh_A= the sponsor's market hurdle (Run A — baseline).IRR_equity(τ_B | inputs_B, h_B) = h_Bwithh_B = max(h_A − λ × Δspread, h_floor)(Run B — guaranteed).inputs_Bapplies the counterfactual changes (lower senior debt rate, longer tenor, relaxed Min ADSCR covenant) and adds the average annual fee as an OPEX line.Find society's NPV saving:
S = NPV(all-in cost stream A) − NPV(all-in cost stream B)where the all-in cost stream in each run is the year-by-year payment society makes for the delivered services (tariff × demand in user-pays contracts; the availability payment in availability-payment contracts) discounted at the public discount rate. The fee is already inside Run B's all-in cost — Run B's tariff has to cover it — so
Sis net of the fee.Subject to the bankability filters in each run:
Min ADSCR ≥ dscr_target(lender covenant)feasible = TRUE(the FM canonical engine returns a complete cash-flow waterfall and a feasible tariff exists within plausible bounds)The IRR test is by construction satisfied — the tariff bisection delivers it — so feasibility reduces to "does a feasible tariff exist?" and "does the DSCR covenant hold at that tariff?".
Apply the decision rule:
- If the scenario carries no revenue lever of the chosen contract type — no user tariff under a user-pays contract, no availability payment under an availability contract — both bisections return before either run is built:
not_applicable. Nothing was modelled, so nothing was rejected, andSis undefined rather than negative. See §7.- If
feasible(B) = FALSE:reject— the project is infeasible even with the guarantee, whatever Run A did. Not a sizing question; restructure first.- If
feasible(A) = FALSEandfeasible(B) = TRUE:take_with_caveat— the bankability flip. The project does not exist without the guarantee. The verdict is a cost-benefit-analysis question (is the project worth doing at Run B's society cost?), outside the scope of this paper. The framework flags the case and surfaces Run B's society cost for the planner.- If
feasible(A) = TRUEandfeasible(B) = TRUEandS > 0:take— the guarantee reduces society's all-in cost by more than its fee.- Otherwise:
reject.The differential equity NPV
Δ = eNPV(B) − eNPV(A) − Fis also computed and surfaced as a sponsor diagnostic. The sponsor view holds the equity hurdle fixed and so misses the channel through which risk transfer reduces the required equity premium; under the planner view that channel is captured byλ. The framework prints a market-friction warning when contract structure (e.g. directly-negotiated, fixed-tariff PPP with no re-bid) may prevent the equilibrium tariff cut from passing through to society — in that case the sponsor pockets the saving as private rent, and the planner should either reject the guarantee or fix the contract structure first.
The variables in the problem feed back into each other. No closed-form solution exists.
| Variable | Depends on | Affects |
|---|---|---|
| Guarantee fee | Outstanding covered debt amount, year by year | OPEX line in Run B |
| Outstanding covered debt amount | Senior debt amortisation schedule | Annual fee stream → fee NPV |
| Senior debt amortisation schedule | Lender terms — rate, tenor, repayment type | DSCR, equity cash flow |
| Lender terms | Whether the guarantee is present + coverage | Whether bankability holds |
| Equity hurdle in Run B | Δspread × λ, with floor h_floor |
The tariff that solves equity IRR = hurdle |
| Tariff (Run A and Run B) | Bisection target — equity IRR = run hurdle | Society's all-in cost stream |
| Coverage % of debt | Decision variable in the outer loop | Spread reduction, tenor extension, fee |
Reading the table top-to-bottom: every variable in row N depends on a variable in a later row, and every variable affects something in an earlier row. The FM model is needed twice — once at market terms (Run A, with bisection on the tariff to hit h_A), once at guaranteed terms (Run B, with bisection on the tariff to hit h_B) — and the two runs share no shortcut.
The inner loop is the Run B feasibility check. For a fixed (product, country, coverage_pct), it runs once and returns a Yes/No on bankability plus the snapshot ratios. The flow:
The loop has no recursion: each step uses the output of the previous step and never revisits an earlier one. Termination is guaranteed.
The outer loop sweeps across guarantee products and coverage levels to find the best decision. Priority ordering is enforced top-down so the sponsor and the host government look at the right candidates first.
coverage_pct ∈ {50 %, 75 %, 90 %}. The sweep is deliberately coarse — finer slicing rarely changes the decision. The worked example in §10 fixes coverage at 90 % across all candidates to keep the comparison readable; running the full sweep is a single API parameter change.S among those that pass bankability. Ties below 5 % go to the next tiebreaker.S, prefer (a) the one that delivers the larger bankability margin (DSCR headroom + Run B-tariff distance from the bracket bound), (b) the one with the more stable fee schedule (some private insurers can re-rate annually), (c) the one with the simpler underwriting process.The inner loop is deterministic. Every function in the chain — estimate_counterfactual_terms, compute_guarantee_fee, run_model — is a pure function of its inputs. There are no fixed-point iterations and no convergence tolerances to set.
The outer loop is bounded. Eligibility filtering is finite (at most ten provider × instrument pairs). The coverage sweep is finite (three values). Each (product, coverage) cell needs two tariff bisections (Run A and Run B), each ~10 FM solves at a baseline 100 ms per solve. The full sweep is therefore at most 10 × 3 × 2 × 10 = 600 FM solves, well under a minute on a single backend pod; a single decision (one product, one coverage) returns in 3–5 seconds on warm caches.
Society's saving S(coverage_pct) is well-behaved on the coverage axis:
coverage_pct^0.5 — diminishing returns above 50 % coverage match the empirical observation that lenders price the first tranche of risk transfer most generously and discount further coverage less aggressively.S is the difference between the spread-and-hurdle benefit (concave in coverage, via the square-root law) and the fee cost (roughly linear in coverage), so it is quasi-concave with a unique maximum somewhere in the 50 %–90 % range for most products. The discrete sweep finds it.
Under the planner view, the loss is the negative society saving. Minimisation maps to the decision.
def planner_loss(decision: SocietyDecision) -> float:
"""Loss to minimise — the more negative, the better the guarantee.
Conventions:
* A scenario with no revenue lever of the chosen contract type
returns +infinity: neither run was built, so there is no saving
to minimise. This is NOT a rejection of the instrument — the
optimiser must skip it rather than score it (see §7).
* Infeasible Run B returns +infinity — never take such a guarantee.
* Bankability flip (Run A infeasible, Run B feasible) is handed to
the CBA pipeline (outside this paper); the framework returns
+infinity from the planner-loss perspective so the optimiser
does not select it without explicit operator approval.
* Otherwise return -society_saving_npv.
"""
if decision.case == "not_applicable":
return float("inf") # nothing was modelled
if not decision.guaranteed_run.feasible:
return float("inf")
if decision.case == "feasibility_enabled":
return float("inf") # defer to CBA pipeline
return -decision.society_saving_npv
A guarantee is taken if and only if planner_loss < 0. The platform implements this rule inside evaluate_guarantee_central_planner, returning decision = "take" when society_saving_npv > 0, decision = "reject" when it is non-positive or Run B is infeasible, and decision = "not_applicable" when no run could be built at all. The legacy sponsor-view loss −Δ = −(eNPV(B) − eNPV(A) − F) is still computed and surfaced in the diagnostic panel; the two losses can disagree (see §10).
Each evaluation — whether the planner-view call (evaluate_guarantee_central_planner) or the sponsor-view diagnostic (evaluate_guarantee):
fm.ce_evaluated event on the workspace's WebSocket channel — every collaborator sees the result the moment it computes.Any auditor — internal compliance, supreme audit institution, MDB compliance — can re-run the same call against the saved inputs and verify the result matches the audit-chain entry to the cent.
Why this matters: the guarantee can only be assessed against an honest "what would happen if we tried to close the project at market terms today" — and that baseline has to come from a fully-built FM, not a back-of-envelope estimate.
The baseline run is the project as the lender market would price it today, with no third-party support. The senior commercial debt tranche carries the full risk premium for the country, the sector, and the contractual risk allocation. The DSCR covenant is whatever the lender market actually demands. There is no guarantee fee in OPEX.
Under the planner view the framework bisects the year-1 tariff (or availability payment) until the realised equity IRR equals the sponsor's market hurdle h_A. The output of Stage 1 is a RunSnapshot containing the solved tariff, the resulting min_adscr, avg_adscr, equity_npv, the year-by-year all-in cost stream society pays, and a feasible flag.
Either end of the bracket can be reached without a root, and the solver says which:
h_A, the solver reports infeasible_at_high_lever and Run A has no solved tariff at all.h_A — the project is over-bankable at market terms — the solver reports already_above_target and adopts the bottom of the bracket as the solved tariff, since the solver does not search below its bracket.Neither outcome short-circuits the evaluation. The reason code travels on the decision as base_tariff_solver_reason and is turned into a sentence in the decision notes for the audit chain ("Run A, without the guarantee: the project already earns the required equity return at the bottom of the solver's search range, so the lowest user tariff in that range was used."), and the framework goes on to build Run B and returns whatever the Stage-5 rule of §7 gives. In the over-bankable case that rule never returns take: Run B searches the same tariff range as Run A — the range is set from the scenario's own tariff, which the guarantee does not change — so Run B can never solve below Run A's floor. Either both runs sit on that floor and S = 0, or Run B needs a higher tariff and S < 0; the verdict is reject by the general S ≤ 0 rule, not by an early exit. The one exception is a Run A that clears h_A at the floor but does not cover its debt service there: if the guarantee's debt terms cure that, Run B is feasible where Run A is not, and the case is the bankability flip of §7, take_with_caveat. The sponsor-view fallback (no bisection, fixed market tariff input) is also computed as the legacy diagnostic.
The user builds the project in the Austral FM module — inputs, statements, charts, sensitivity pages of /workspaces/{wid}/fm/ — and saves the scenario to the workspace. The same ModelInputs payload feeds Stage 2.
The Run A bisection must converge inside the plausible tariff bounds. If even at the upper end of the bracket the project still fails the lender's DSCR covenant — Min ADSCR below 1.0 means debt service is not covered at any tariff society would tolerate — then nothing but a guarantee could make the project bankable, and the gate moves to Run B. The planner classifies on Run B, not on Run A. If Run B is feasible the case is feasibility_enabled and the verdict is take_with_caveat — the bankability flip of §7. If Run B fails too, the project is fundamentally infeasible, a guarantee will not fix it, and the framework reports case = "infeasible" with decision = "reject". The sponsor-view diagnostic has its own code for that last combination, infeasible_either_way; the planner view does not use it. In neither case does the evaluation stop early — both bisections run before any verdict is read.
Why this matters: the value of a guarantee comes from the change in the project's debt terms, and that change has to be calibrated against observed transactions, not assumed.
For a given (provider, instrument, coverage_pct) and (country_code, CRC, sovereign_rating), the framework looks up three empirical adjustments:
These adjustments are empirical — they come from observed MDB-backed transactions and from the published rate cards of the guarantors. The framework ships with a calibrated default table indexed by (provider × instrument × CRC). The defaults are mid-range estimates and can be overridden per transaction when the sponsor has lender pricing in hand.
A note on calibration: the counterfactual debt-term table is the input that drives most of the headline number, and the current values are seed estimates derived from public rate cards plus a small set of recent transactions. A formal back-test against a larger transaction sample (Senegal Senergy 2024, Bangladesh Summit 2023, Kenya Lake Turkana, IFC Mocoa 2022)8 is in progress and is tracked as an open item in §11.3 — until that lands, sponsors should treat the table as a calibrated guess and override per transaction whenever they have lender pricing.
The adjustments scale with coverage percentage on a square-root curve: bps_effective = bps_at_full_cover × sqrt(coverage_pct). The shape captures the empirical observation that the first 50 % of cover delivers most of the spread compression; further coverage has diminishing returns. Tenor extension is integer-valued and assumed flat in coverage above 50 %.
credit-enhancement analyzer (FM extension)The frontend page at /workspaces/{wid}/fm/credit-enhancement/ cascades the provider selection into a list of valid instruments (MIGA → PRI / NHSFO; IBRD → PBG-loan / PBG-payment / PBG-policy; …) and surfaces the current counterfactual values to the user before any FM run is triggered. The user can override any value with a single field.
The looked-up counterfactual values must be plausible — spread reduction in [0, 600] bps, tenor extension in [0, 15] years, DSCR relaxation in [0, 0.20]. Values outside those ranges are flagged for manual review; outside the ranges suggests the reference table needs calibration for that country class.
Why this matters: the fee is the price of the guarantee, and a fee that is too aggressive will turn an otherwise valuable guarantee into a net cost.
The framework computes the guarantee fee in two parts:
The total fee NPV is the sum of the upfront fee at t=0 plus the discounted annual fee stream over the guarantee tenor (which equals base senior debt tenor plus the counterfactual tenor extension). The discount rate defaults to the project's public discount rate and can be overridden per transaction.
The default fee tables are calibrated from public sources:
| Source | Used for |
|---|---|
| MIGA Annual Reports + published rates1 | MIGA-PRI, MIGA-NHSFO |
| WB Treasury — IBRD/IDA guarantee financial terms2 | IBRD-PBG (loan / payment / policy), IDA-PBG |
| IFC public rate disclosures3 | IFC-PCG |
| ADB / AfDB / IDB / EBRD published rates4 | Regional MDB PCGs |
| OECD Arrangement minimum reference rates5 | OECD-ECA |
| Practitioner observation | Private PRI cover |
The defaults are mid-range. Override per transaction when you have a quote in hand.
compute_guarantee_fee() engine functionThe platform's FM credit-enhancement page surfaces the upfront fee, annual fee rate, the per-year annual fee stream, and the total fee NPV next to the counterfactual-terms panel — all derived from the chosen (provider, instrument, country.risk_class, coverage_pct, tenor) combination, with a single text input letting the user override the rate when needed.
Under the planner view the fee is inside Run B's all-in cost stream — Run B's bisected tariff has to cover it. The natural gate becomes: society's saving (= Run A all-in cost − Run B all-in cost, both NPVs) must be positive. The framework returns reject whenever it is not. The legacy sponsor-view gate (fee NPV must be less than baseline equity NPV in Run A) is still computed as a diagnostic and surfaces the early-warning case where the fee alone exceeds the project's equity value — which can occur with very high country risk, very long tenor, and high coverage.
Why this matters: the guaranteed-case equity returns and bankability ratios cannot be estimated by hand — they require a full re-run of the cash-flow waterfall under the new terms.
The framework deep-copies the baseline ModelInputs, applies the counterfactual changes (lower senior debt rate, longer senior debt tenor, relaxed Min ADSCR covenant) and adds the average annual fee as an insurance_cost line in OPEX. It then bisects the Run B tariff until the realised equity IRR equals the Run B hurdle h_B = max(h_A − λ × Δspread, h_floor). Each bisection step is one call to ppp_core.engine.model.run_model; convergence is typically 6–10 iterations.
The result is a second RunSnapshot (Run B) with the solved tariff, the resulting Min ADSCR, the year-by-year all-in cost stream society pays at the new tariff, and the same equity IRR and equity NPV at the new hurdle. The same ModelInputs schema is used in both runs, so the FM canonical engine produces ratios that are directly comparable to Run A.
Three nuances matter for clean comparison:
opex.insurance_cost. This is the cleanest mapping — the fee semantically is an insurance premium, and it flows through the income statement as an operating cost (deductible from taxable income).Min ADSCR ≥ dscr_target returns True.run_model — bisected invocationInternally each Run B candidate tariff is fed through run_model(inputs_B(τ)) where inputs_B(τ) = _apply_guarantee_terms(inputs_A, counterfactual, fee, τ). The bisection loop runs ~10 times before settling on the tariff that produces equity IRR = h_B. Run A bisects analogously against h_A. The two outer bisections are independent and could be parallelised; the current implementation runs them sequentially and the full evaluation typically completes in ~1 second.
The Run B bisection must converge inside the plausible tariff bounds (a configurable interval, default 0.1× to 10× the Run A solved tariff). If the bisection bracket is exhausted — the tariff that hits h_B lies outside the plausible bounds, or the cash flows are negative throughout — Run B is treated as infeasible and the framework recommends reject. The same applies in reverse to Run A.
Why this matters: this is the step where the framework returns a recommendation a Ministry of Finance, a sector regulator, or a sponsor can defend in front of an investment committee.
The primary verdict is society's NPV saving:
S = NPV(all-in cost stream A) − NPV(all-in cost stream B)
The all-in cost stream in each run is the year-by-year payment society makes for the delivered services (tariff × demand in user-pays contracts; the availability payment in availability-payment contracts) discounted at the public discount rate. Run B's tariff already covers the guarantee fee, so S is net of the fee. The decision rule:
| Revenue lever of the chosen contract type | Run A feasible | Run B feasible | Society saving S |
case |
decision |
|---|---|---|---|---|---|
| Absent | — (no run built) | — (no run built) | None — never computed |
not_applicable |
not_applicable |
| Present | (any) | No | ignored | infeasible |
reject |
| Present | No | Yes | ignored | feasibility_enabled |
take_with_caveat |
| Present | Yes | Yes | > 0 |
cost_reduction |
take |
| Present | Yes | Yes | ≤ 0 |
cost_reduction |
reject |
The four rows that build runs are tested in that order, and the order matters: a scenario with no lever also leaves Run B infeasible, so the lever test has to come first or the two collapse into one. (One residual branch: if both runs are feasible but a cost stream cannot be recovered from one of them, S is None and the verdict is reject with a note saying the saving could not be computed. It has not been observed outside a deliberately broken model.)
The bankability-flip case is qualitatively different. When Run A is infeasible, the project does not exist without the guarantee — there is no Run A all-in cost to compare against, because society pays nothing for a project that never enters operations. The verdict in that case is a cost-benefit-analysis (CBA) question: is the project worth doing at Run B's society cost, given whatever non-financial benefits it delivers? That CBA is outside the scope of this paper. The framework reports case = "feasibility_enabled" with decision = "take_with_caveat" and surfaces Run B's society cost so the planner can hand the question to the standard CBA pipeline.
The not-applicable case is the one outcome that is not a verdict on the guarantee at all. Both tariff bisections read the revenue lever of the chosen contract type before they run anything: revenue.base_tariff under user_pays, the first availability payment's annual_amount under availability. If that lever has no positive value to scale — a user-toll scenario evaluated as an availability contract, say, or an availability scenario whose payment schedule is still empty — the solver returns no_base_lever_value immediately and neither run is ever built. There is no Run A cost, no Run B cost, no solved tariff on either side and no saving: society_saving_npv is None, not a negative number. The framework reports case = decision = "not_applicable" and says so in the decision notes, in the engine's own words — here for a user-toll scenario evaluated as an availability contract:
The tariff solver had nothing to scale: this scenario has no availability payment for the chosen contract type. Add one, or switch to the user-pays contract type.
This is not a rejection: neither run could be built, so the planner test was not run.
The distinction matters for anything that counts. reject is a verdict on an instrument that was evaluated: both runs were built, priced and compared, and the guarantee did not pay for itself. not_applicable means the comparison never happened, and the usual cause is a scenario configured for the other contract type rather than anything about the guarantee. A screening exercise that pooled the two would report a provider as having failed a test it was never put through, and a coverage sweep that minimised rejections would be optimising over an empty set — which is why the loss function of §2.6 skips the case rather than scoring it. The platform keeps the two apart end to end: the decision card prints the verdict in neutral grey rather than the red reserved for reject, both solved-tariff rows read "no lever to solve" rather than "infeasible", and the export endpoints, when sent a planner decision, label it NOT APPLICABLE (in grey on the PDF); the DOCX replaces the decision rationale with the missing-lever explanation and how to fix it.
The sponsor-view differential Δ = eNPV(B) − eNPV(A) − F is also computed and surfaced as a diagnostic. Δ is what the sponsor sees if they hold the equity hurdle fixed at h_A and ignore the equilibrium tariff cut delivered by the lower hurdle. As §10 demonstrates, Δ and S can have opposite signs for the same deal — the planner view says take; the sponsor view says reject. When Δ is negative but S is positive, the framework prints a market-friction warning: the saving only flows to society if contract structure (re-bid, regulated tariff, or competitive renewal) lets the equilibrium tariff cut pass through. In a directly-negotiated, fixed-tariff PPP with a monopoly sponsor and no re-bid, the sponsor pockets the saving as private rent, and the planner should either reject the guarantee or fix the contract structure first.
credit-enhancement decision cardThe platform renders the verdict prominently, colour-coded on the card's left border and on the verdict line itself: green for take, red for reject, amber for take_with_caveat (the bankability flip, which routes the user to the CBA pipeline rather than to a take/reject answer), and neutral grey for not_applicable — deliberately not red, because no instrument failed a test here. Beside it sit society's saving (the headline number, omitted when there is none to show), the Run A and Run B all-in cost streams, the counterfactual terms, the fee breakdown, the sponsor diagnostic Δ in a collapsible panel below, and any market-friction warning. The card is designed to be lifted directly into a Ministry-of-Finance or investment-committee paper.
The verdict + the supporting numbers (Run A snapshot, Run B snapshot, counterfactual terms, fee breakdown, society saving, sponsor diagnostic, market-friction warning) form a single, exportable record. The DOCX export endpoint produces a methodology note that includes the verdict, the rationale, and the audit-chain ID. The PDF export produces a one-page dashboard. The XLSX export produces a workbook for the deal team to inspect line by line.
Why this matters: the framework's value rests on producing the same answer regardless of who runs it, when, or where — and on being audit-traceable from end to end.
The framework operates in a single mode: a thin overlay on the FM module's canonical cash-flow engine. There is no "alternative engine" to choose between. Two entry points are exposed in ppp_core.engine.credit_enhancement:
evaluate_guarantee_central_planner(inputs, product, country, base_equity_hurdle, ...) — the primary entry. Bisects the tariff in each of Run A and Run B against the run-specific hurdle (with the λ-based hurdle adjustment in Run B), returns a SocietyDecision carrying the society saving, both runs' all-in cost streams, the counterfactual terms, the fee breakdown, the sponsor diagnostic, and any market-friction warning.evaluate_guarantee(inputs, product, country, ...) — the legacy / sponsor-view diagnostic. Holds the hurdle fixed at the sponsor's market hurdle and computes Δ = eNPV(B) − eNPV(A) − F. Still produced inside the planner-view call as the sponsor diagnostic; the standalone entry point exists for backwards compatibility with code that pre-dates the planner-view sprint.Both functions are shared by the platform module and (in principle) by any future Streamlit standalone — guaranteeing that the platform UI, the public API, and any downstream automation return identical numbers.
Per-jurisdiction awareness. The fee tables and counterfactual-term tables are indexed by OECD Country Risk Class (CRC) on the 0–7 scale, with each provider × instrument pair mapping to its own row. A change in CRC (e.g. country upgrade or downgrade) flows through to fees and to counterfactual terms automatically. Sovereign rating is captured in the CountryRiskInputs for reference but does not currently drive the lookup — it is present so users can record the rationale.
Multi-currency. When the FM model runs in dual-currency mode (financing currency vs local currency), the credit-enhancement engine inherits the currency conversion automatically. The fee is denominated in the financing currency (which is the convention all major guarantors use). The exposure of the project to local-currency depreciation, when relevant, can be captured separately via the existing FM multicurrency extension.
Audit chain. Every call — evaluate_guarantee_central_planner (planner view) or evaluate_guarantee (sponsor diagnostic) — writes an immutable entry to the workspace activity log: input hash, output hash, user identity, timestamp, server-side run identifier. The activity log is SHA-256 hash-chained — each entry's hash includes the previous entry's hash — so any tampering breaks the chain at the modified entry and downstream. Re-running the same call against the same inputs produces the same output; the audit chain ID lets a future auditor verify that without running anything.
Signed PDFs. The DOCX, XLSX, and PDF exports embed the audit-chain ID and the decision hash at the bottom of every page. When the document leaves the platform — emailed to a sponsor's investment committee, attached to an MDB submission — the receiver can verify the document has not been altered by re-running the underlying evaluation against the recorded inputs and comparing hashes. Optional cryptographic signing (PAdES for PDF) can be enabled per workspace; the signing infrastructure is shared with the existing FCCL Reporting module's signed output.
The full API and pseudocode reference is in Appendix D.
Why this matters: the framework is only useful if a practitioner can follow eight discrete steps from a blank workspace to a defensible decision in under 10 minutes.
Step 1 — Open the platform and create a workspace. Navigate to https://platform.austral-intelligence.com/. The platform creates a Guest workspace automatically; no signup is required for the smoke test.
Step 2 — Build the baseline FM scenario. Open the Financial Model module via the left sidebar. Use the Inputs page to define construction CAPEX, revenue, OPEX, tax, depreciation, the senior commercial debt tranche (rate, tenor, repayment type), and the equity contribution. Save the scenario.
Step 3 — Sanity-check Run A. Open Statements and Ratios. Confirm the project is plausibly viable at market terms — Min ADSCR not catastrophically below 1.0, cash-flow profile reasonable. The credit-enhancement page will bisect the tariff in Step 8 to hit the planner-supplied hurdle, so the question here is just whether the project deserves a guarantee analysis at all.
Step 4 — Open the Credit Enhancement page. Click "Credit Enhancement" in the FM sidebar (or navigate to /fm/credit-enhancement).
Step 5 — Pick the source FM scenario. Use the dropdown to select the scenario you saved in Step 2.
Step 6 — Configure the guarantee product. Select Provider (e.g. MIGA), Instrument (e.g. PRI), and Coverage % (e.g. 90 %). The instrument list auto-filters to those valid for the selected provider.
Step 7 — Configure the country risk profile. Enter the country code, OECD CRC (0–7), and sovereign rating. The CRC drives the fee schedule and the counterfactual terms via the reference tables.
Step 8 — Set the planner inputs and click "Evaluate (planner view)". Enter the base equity hurdle h_A and the contract type (user-pays or availability-payment), then click the primary CTA. Direct-negotiation contracts are flagged separately by the market-friction warning rather than as a contract-type value. The decision card shows the society saving as the headline number, the Run A and Run B all-in cost streams, the solved tariffs in each run, the counterfactual terms, the fee breakdown, and the verdict (take / reject / take_with_caveat for the bankability flip / not_applicable when the scenario carries no revenue lever of the contract type you chose — the usual cause is picking "availability payment" for a user-toll scenario, or the reverse). The sponsor-view differential is collapsed in a "Sponsor diagnostic" panel below the planner verdict — expand it to see Δ at fixed hurdle. A market-friction warning appears in amber when the contract type may prevent the saving from passing through. Iterate by changing provider or coverage and clicking Evaluate again — each evaluation takes ~1 second and writes a new audit-chain entry.
Why this matters: a concrete numerical example shows the framework in action and lets the reader sanity-check the recommendation against intuition.
Project parameters (baseline FM inputs):
| Variable | Value |
|---|---|
| Total CAPEX | US$ 500 million |
| Installed capacity | 500 MW (solar, ~22.8 % capacity factor) |
| Construction period | 2 years |
| Operating period | 20 years |
| Tax rate | 25 % |
| Depreciation life | 20 years (straight-line) |
| Year-1 generation | 2,740 MWh / day |
| Tariff (year 1) | US$ 100.00 / MWh |
| Annual demand growth | 3.0 % |
| Annual tariff escalation | 2.0 % |
| Operating cost (year 1) | US$ 15 million / year |
| OPEX escalation | 2.5 % |
| Senior commercial debt share | 70 % of CAPEX |
| Senior commercial debt rate | 10.0 % |
| Senior commercial debt tenor | 15 years (linear amortisation) |
| Equity | 30 % of CAPEX |
| Public discount rate | 8 % |
| Country profile | CRC 6, sovereign rating B |
| Sponsor's equity IRR hurdle (h_A) | 18 % |
| Equity-hurdle floor (h_floor) | 7 % (Appendix E) |
| Min ADSCR target | 1.30 × |
Run A — baseline (no guarantee).
The framework bisects the year-1 tariff until the realised equity IRR equals the 18 % hurdle. At that tariff:
| Metric | Value |
|---|---|
| Solved year-1 tariff | US$ 96.10 / MWh |
| Society's all-in cost (NPV @ 8 %) | US$ 1,284 million |
| Equity IRR (achieved) | 18.0 % |
| Min ADSCR | 1.59 × |
| Feasible | YES |
The project is bankable at market terms — DSCR comfortably above 1.30, equity hurdle hit by construction. The total cost society pays is US$ 1,284 million in NPV terms over the 20-year operating life. The planner now asks: would a third-party guarantee bring this number down by more than its fee?
Candidate 1 — MIGA PRI at 90 % coverage.
The framework first looks up the counterfactual debt-term changes (237 bps spread reduction, 6-year tenor extension, 0.05× DSCR relaxation). With λ = 1.30 — MIGA-PRI transfers only the four political-risk perils, not full sovereign credit — the equity hurdle drops by 1.30 × 237 / 10 000 ≈ 3.08 percentage points. The hurdle floor of 7 % is not binding. The framework solves the Run B tariff at the new 14.92 % hurdle:
| Metric | Value |
|---|---|
| Counterfactual spread reduction | 237 bps |
| Counterfactual tenor extension | 6 years |
| λ (equity-elasticity of risk transfer) | 1.30 |
| Δhurdle (= λ × Δspread) | −3.08 percentage points |
| Equity hurdle (Run A → Run B) | 18.00 % → 14.92 % |
| Solved year-1 tariff (Run B) | US$ 78.10 / MWh |
| Society's all-in cost (NPV @ 8 %) | US$ 1,043 million |
| Total fee NPV (informational) | US$ 30 million |
| Society saving | +US$ 241 million |
| Decision (planner) | TAKE |
| Sponsor's perceived NPV uplift | −US$ 12 million (diagnostic) |
The sponsor view and the planner view point in opposite directions for this deal. The sponsor view (hurdle held fixed at 18 %) sees a US$ 12 million loss because the MIGA fee swallows the equity-NPV uplift the sponsor would notice if the tariff stayed at US$ 96.10. The planner view sees a US$ 241 million saving because the lower hurdle, in equilibrium, allows a tariff cut from US$ 96.10 to US$ 78.10 — a 19 % reduction. The all-in cost society pays falls from US$ 1,284 million to US$ 1,043 million.
The sponsor view's "reject" verdict is wrong from the planner's seat. The right answer is take — and the only situation where it would be wrong is the sponsor-capture case: a directly-negotiated, fixed-tariff contract where the sponsor pockets the equilibrium savings as monopoly rent rather than passing them through to society. The framework prints a market-friction warning when that risk is present so the planner can either reject the guarantee or re-open the tariff.
Candidate 2 — IBRD PBG-loan at 90 % coverage.
IBRD's PBG transfers full sovereign-credit risk; λ = 1.90, the highest in the catalogue. The hurdle drops by 1.90 × 389 / 10 000 ≈ 7.39 percentage points — from 18.00 % to 10.61 %, well above the 7 % floor. Tariff is solved at the new hurdle:
| Metric | Value |
|---|---|
| Counterfactual spread reduction | 389 bps |
| Counterfactual tenor extension | 13 years |
| λ (equity-elasticity of risk transfer) | 1.90 |
| Δhurdle (= λ × Δspread) | −7.39 percentage points |
| Equity hurdle (Run A → Run B) | 18.00 % → 10.61 % |
| Solved year-1 tariff (Run B) | US$ 56.60 / MWh |
| Society's all-in cost (NPV @ 8 %) | US$ 757 million |
| Total fee NPV (informational) | US$ 8 million |
| Society saving | +US$ 527 million |
| Decision (planner) | TAKE |
| Sponsor's perceived NPV uplift | +US$ 64 million (diagnostic) |
The IBRD PBG is dramatically the better instrument: society saves US$ 527 million versus US$ 241 million for MIGA-PRI. Both effects compound — IBRD's spread reduction is 1.6× larger than MIGA-PRI's (389 vs 237 bps), AND λ is higher (1.90 vs 1.30) because IBRD takes full sovereign-credit risk while MIGA-PRI takes only the political-risk perils. The combined effect is a 7.4 pp hurdle reduction versus 3.1 pp, and the tariff drops from US$ 96.10 to US$ 56.60 — a 41 % reduction.
The sponsor view's diagnostic figure (+US$ 64 million) is roughly 12 % of the planner-view society saving. That asymmetry is the core insight: in equilibrium the sponsor captures none of the value (they are indifferent at hurdle = required return). All US$ 527 million flows through to whoever funds the project.
Candidate 3 — OECD-Arrangement ECA at 90 % coverage, 22-year tenor.
| Metric | Value |
|---|---|
| Counterfactual spread reduction | 171 bps |
| Counterfactual tenor extension | 7 years |
| Total guarantee tenor (base 15 + 7) | 22 years |
| Counterfactual DSCR relaxation | 0.047 × |
| λ (equity-elasticity of risk transfer) | 1.40 |
| Δhurdle (= λ × Δspread) | −2.39 percentage points |
| Equity hurdle (Run A → Run B) | 18.00 % → 15.61 % |
| Annex VIII tenor multiplier (22 yrs) | 2.50 × (cap)5 |
| MPR base (CRC 6, 5-yr baseline)5 | 1.50 % p.a. (illustrative) |
| MPR effective (after multiplier) | 3.75 % p.a. |
| Solved year-1 tariff (Run B) | US$ 89.04 / MWh |
| Society's all-in cost (NPV @ 8 %) | US$ 1,190 million |
| Total fee NPV (informational) | US$ 80 million |
| Society saving | +US$ 94 million |
| Sponsor's perceived NPV uplift | −US$ 76 million (diagnostic) |
| Decision (planner) | TAKE — but barely |
OECD-Arrangement ECA cover is the only instrument in the catalogue where the OECD's Annex VIII tenor multiplier is binding on the fee schedule5. At a 22-year guarantee tenor the multiplier hits its 2.50 × cap, lifting the effective annual MPR from 1.50 % to 3.75 % per annum on the covered amount. The fee NPV swells to US$ 80 million — bigger than the headline spread-compression gain a sponsor would notice at fixed hurdle, which is why the sponsor diagnostic is sharply negative (−US$ 76 M). The planner view recovers a positive society saving (+US$ 94 M) because the modest hurdle reduction still allows a tariff cut from US$ 96.10 to US$ 89.04 — but the margin is thin. The framework's audit-chain note for this evaluation reads: "OECD Arrangement tenor multiplier 2.50× applied (tenor 22 yrs); λ = 1.4 capped by ECA's partial credit-risk transfer (Appendix E)."
The lesson for ECA cover is structural: the Arrangement deliberately makes long-tenor ECA cover expensive so participating ECAs do not undercut the multilateral guarantor pool5. Society still nets a small saving here because the spread compression and tariff cut more than cover the inflated fee — but the margin is roughly an order of magnitude thinner than IBRD-PBG (US$ 94 M vs US$ 527 M) and tighter than MIGA-PRI (US$ 94 M vs US$ 241 M). Practitioners structuring deals with ECA backing should target tenors at or below the project's debt maturity — outside that band, the Arrangement's pricing can turn ECA cover from value-creating to value-destroying, and the verdict flips. The MPR base is shown as an illustrative CRC-6 calibration; the actual OECD-published rate for CRC 6 should be substituted whenever the live snapshot is available (see §11.2 OECD rate-snapshot infrastructure).
Iteration trace. The framework ran three evaluate_guarantee_central_planner calls — one per candidate. Each call ran two tariff-bisection sweeps (Run A and Run B) over the FM canonical engine, plus an evaluate_guarantee baseline for the sponsor diagnostic. Total: roughly 90 FM solves in ~9 seconds at the 100 ms-per-solve baseline; the platform is faster on warm caches and typically returns in 3–5 seconds. The decision ranking is unambiguous: take the IBRD PBG when available (society saving +US$ 527 M); take MIGA-PRI when IBRD eligibility does not apply (+US$ 241 M); take ECA cover only as a last-resort instrument (+US$ 94 M and very tenor-sensitive).
The deeper insight. From the central-planner's seat, the primary value of a third-party guarantee is not the equity-NPV uplift the sponsor would notice — it is the all-in cost reduction society pays for the same delivered services. That cost reduction comes from two sources, both flowing to whoever funds the project: (i) the lower commercial debt cost the lender accepts, and (ii) the lower equity premium the sponsor accepts in equilibrium because risk has been transferred away7. The framework's λ-based estimate of Δhurdle captures the second channel — a channel the sponsor-view framing entirely misses.
Two practical corollaries fall out:
The sponsor-view differential and the planner-view saving can have opposite signs. MIGA-PRI in this worked example is rejected by the sponsor view but taken by the planner view. The sponsor sees a fee that exceeds an equity-NPV uplift held artificially small by a fixed hurdle; the planner sees a tariff reduction that exceeds the fee by ~8×.
Contract structure matters as much as guarantee choice. When the contract structure prevents the equilibrium savings from flowing to society — a directly-negotiated, fixed-tariff PPP with a monopoly sponsor and no re-bid — the sponsor pockets the planner's saving as private rent. The framework prints a market-friction warning when that structure is detected. The planner's response is either to reject the guarantee or to fix the contract structure first; the response is not to evaluate the guarantee in isolation.
Why this matters: every framework has assumptions; documenting them protects users from drawing the wrong conclusions when the underlying assumptions are violated.
multicurrency extension handles the conversion before the credit-enhancement evaluation. The fee is always denominated in the financing currency (matching guarantor convention).coverage_pct^0.5. The square-root law captures empirical diminishing returns above 50 % coverage but is itself a simplification. The shape may not hold for very specific products (e.g. partial credit guarantees with insurance-style attachment points).ppp_core.engine.credit_enhancement — solver overlay on the FM canonical engine (~950 lines including the layered-stack solver and the OECD Annex VIII tenor multiplier).provider-instrument form (e.g. OECD-ECA-ECA, MIGA-PRI) — early implementations had inconsistent keying for the OECD-ECA and private-PRI rows that under-priced both by ~25 %; the calibration test suite now locks the canonical keys and asserts the legacy ones do not exist.evaluate_guarantee_central_planner() solves for the tariff (or availability payment) that produces the sponsor's required equity hurdle in each run; the hurdle itself drops in Run B by Δhurdle = λ × Δspread with product-specific λ from _HURDLE_REDUCTION_FACTOR_BY_PRODUCT (1.9 for IBRD-PBG, 1.8 for MIGA-NHSFO, 1.5 for IFC/ADB PCG, 1.4 for OECD-ECA, 1.3 for MIGA-PRI, 1.1 for private PRI). The society NPV saving (Run A all-in cost minus Run B all-in cost) is the right comparison to the fee. Decision rule: take if saving > 0. The framework also prints the sponsor diagnostic (eNPV uplift at fixed hurdle) and a market-friction warning when contract structure may prevent the savings from passing through. Endpoints: POST /fm/credit-enhancement/{evaluate-cp, projects/{pid}/eval-cp}. Frontend: a "Central-planner inputs" panel (base equity hurdle + contract type) and a primary "Evaluate (planner view)" CTA, with the sponsor view collapsed as a labelled diagnostic panel.not_applicable — a scenario with no revenue lever of the chosen contract type makes both tariff bisections return before anything is modelled. That outcome used to be reported as reject, which reads as a verdict on the instrument; it is now its own case and its own decision, carries the sentence "This is not a rejection: neither run could be built, so the planner test was not run.", and must not be counted as a rejection. The verdict, its case label, the two solver-reason notes and the "no lever to solve" cell of the decision card all carry stable ids and are translated in the platform's four locales.compute_guarantee_fee whenever the product is OECD-ECA. The note string in the fee breakdown surfaces every adjustment for the audit chain.evaluate_guarantee_stack() solves for two or more guarantees layered on the same project. Combination math: spread reductions stack with diminishing returns (each layer contributes 0.5 × what remains), tenor extension takes max + 1 yr per extra layer, DSCR relaxation takes max, fees are summed. POST /fm/credit-enhancement/evaluate-stack and /projects/{pid}/eval-stack. Frontend supports "+ Add another guarantee layer" with per-layer rows.OecdRateSnapshot table (immutable, append-only, hash-chained), services/oecd_rates.py for seed + read + write, routers/oecd_admin.py (/admin/oecd-rates/{snapshots, active, refresh}), and a Celery monthly task scheduled on the 1st of each month. The platform's CE router fetches the active snapshot's payload as the overlay argument to evaluate_guarantee / evaluate_guarantee_stack; the engine consults the snapshot first, falls back to the static reference table for any (key, CRC) tuple not present./api/v1/workspaces/{wid}/fm/credit-enhancement/: products catalogue, evaluate, evaluate-stack, evaluate-on-saved-FM-scenario, scenario CRUD, AI insights, three exports (DOCX methodology note, XLSX workbook, PDF one-page dashboard)./api/v1/batch/fm/credit-enhancement-{eval, batch, stack}, gated by the platform billing middleware./workspaces/{wid}/fm/credit-enhancement/: provider/instrument cascade, country risk inputs, two-run dashboard with side-by-side Run A vs Run B snapshots, counterfactual-terms panel, fee breakdown, decision verdict card, layered-stack mode, three download buttons.NAV_ITEMS entry; Module Cover feature card.fm.ce_evaluated, fm.ce_stack_evaluated, fm.ce_scenario_saved, fm.ce_scenario_deleted, fm.ce_ai_generated, fm.ce_exported — collaborators see decisions in real time.notes field and propagated into the DOCX / XLSX / PDF exports.backend/tests/test_credit_enhancement_calibration.py pin the current default tables (including the λ table, the hurdle floor, the OECD Annex VIII multiplier curve, and the canonical product-instrument keys); failing test = somebody changed a default without going through the calibration round documented in papers/CE_CALIBRATION_NOTES.md.papers/CE_CALIBRATION_NOTES.md.platform.austral-intelligence.com/workspaces/{wid}/fm/credit-enhancement and the canonical FM standalone URL fm-platform.austral-intelligence.com/.../fm/credit-enhancement via Host-header routing. The marketing site links to the standalone subdomain. No additional Streamlit work is required — the platform-based standalone already carries the feature.Run the worked-example evaluation against the deployed platform.
Step 1: get a guest token.
TOKEN=$(curl -s -X POST https://platform.austral-intelligence.com/api/v1/auth/guest \
-H "Content-Type: application/json" -d '{}' \
-A "Mozilla/5.0" | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
WID=$(curl -s -X POST https://platform.austral-intelligence.com/api/v1/auth/guest \
-H "Content-Type: application/json" -d '{}' \
-A "Mozilla/5.0" | python3 -c "import sys,json; print(json.load(sys.stdin)['workspace_id'])")
Step 2: post the planner-view evaluate request.
curl -s -X POST \
"https://platform.austral-intelligence.com/api/v1/workspaces/$WID/fm/credit-enhancement/evaluate-cp" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-A "Mozilla/5.0" \
-d @worked_example_body.json
The worked_example_body.json payload is the FM ModelInputs dict (matching the Stage-1 baseline parameters in §10) plus a product dict (provider, instrument, coverage_pct), a country dict (country_code, risk_class, sovereign_rating), a base_equity_hurdle (e.g. 0.18), and a contract_type ("user_pays" or "availability").
Step 3: parse the response.
import json
r = json.loads(response_body)["result"]
print(r["case"]) # "feasibility_enabled" | "cost_reduction"
# | "infeasible" | "not_applicable"
print(r["decision"]) # "take" | "reject" | "take_with_caveat"
# | "not_applicable"
print(r["society_saving_npv"]) # planner-view headline, in financing-currency millions
print(r["base_equity_hurdle"]) # h_A
print(r["guaranteed_equity_hurdle"]) # h_B (after λ × Δspread, clamped at floor)
print(r["base_tariff_solved"]) # τ_A
print(r["guaranteed_tariff_solved"]) # τ_B
print(r["sponsor_view_eNPV_uplift"]) # diagnostic Δ — sign can disagree with society_saving_npv
print(r["market_friction_warning"]) # non-null if contract structure may leak the saving
On a not_applicable response every field that would have come out of a run is null — society_saving_npv, both solved tariffs, both all-in costs and the market-friction warning — and base_tariff_solver_reason and guaranteed_tariff_solver_reason both read "no_base_lever_value". A client that treats not_applicable as a rejection is counting a test that was never run; branch on it explicitly.
The legacy sponsor-only call lives at POST .../credit-enhancement/evaluate and returns a GuaranteeDecision with differential_equity_npv instead — kept for backwards compatibility but not the recommended entry point.
The credit-enhancement engine takes three inputs and returns one structured output.
Inputs.
from ppp_core.engine.model_inputs import ModelInputs
from ppp_core.engine.credit_enhancement import (
GuaranteeProduct, CountryRiskInputs,
)
inputs = ModelInputs(...) # FM canonical schema
product = GuaranteeProduct(
provider="IBRD", # 'MIGA' | 'IBRD' | 'IDA' | 'IFC' | 'ADB' | ...
instrument="PBG-loan", # depends on provider
coverage_pct=0.90, # 0..1 share of senior debt covered
)
country = CountryRiskInputs(
country_code="ZZ",
risk_class=6, # OECD CRC 0..7
sovereign_rating="B", # S&P scale, reference only
)
Primary output — SocietyDecision (returned by evaluate_guarantee_central_planner).
@dataclass
class SocietyDecision:
case: str # 'feasibility_enabled' | 'cost_reduction'
# | 'infeasible' | 'not_applicable'
decision: str # 'take' | 'reject' | 'take_with_caveat'
# | 'not_applicable' — the planner test
# was not run; NOT a rejection
contract_type: str # 'user_pays' | 'availability'
# Hurdles
base_equity_hurdle: float # h_A
guaranteed_equity_hurdle: float # h_B = max(h_A − λ·Δspread, h_floor)
hurdle_reduction_pp: float # Δhurdle in percentage points
lambda_used: float # λ from the product table
# Run A — without guarantee
base_tariff_solved: float | None
base_tariff_solver_reason: str | None # None on a clean root; else one of
# 'no_base_lever_value' |
# 'model_failed_at_high_lever' |
# 'infeasible_at_high_lever' |
# 'already_above_target' |
# 'max_iter_reached'
base_run: RunSnapshot
total_cost_society_a_npv: float | None # NPV of revenue stream A at public discount rate
# Run B — with guarantee
guaranteed_tariff_solved: float | None
guaranteed_tariff_solver_reason: str | None # same vocabulary as Run A's
guaranteed_run: RunSnapshot
total_cost_society_b_npv: float | None # NPV of revenue stream B (fee already inside)
# Society saving + verdict
society_saving_npv: float | None # = a_npv − b_npv. None if either side infeasible.
# Counterfactual + fee (informational)
counterfactual: CounterfactualTerms
fee: GuaranteeFeeBreakdown
# Sponsor diagnostic
sponsor_view_eNPV_uplift: float | None # Δ at fixed hurdle — diagnostic only
market_friction_warning: str | None # populated when sign(Δ) ≠ sign(S)
notes: list[str] # audit-chain provenance
Legacy output — GuaranteeDecision (returned by the sponsor-view evaluate_guarantee).
@dataclass
class GuaranteeDecision:
baseline: RunSnapshot # Run A — without guarantee
guaranteed: RunSnapshot # Run B — with guarantee
counterfactual: CounterfactualTerms
fee: GuaranteeFeeBreakdown
differential_equity_npv: float # Δ = eNPV(B) − eNPV(A) − F
bankability_flip: bool # True if Run A infeasible & B feasible
decision: str # 'take' | 'reject' | 'infeasible_either_way'
binding_constraint: str # 'none' | 'min_adscr' | 'equity_irr_*' | ...
notes: list[str]
RunSnapshot carries equity_irr, min_adscr, avg_adscr, project_irr, equity_npv, and feasible. CounterfactualTerms carries spread_reduction_bps, tenor_extension_years, dscr_relaxation, and provenance notes. GuaranteeFeeBreakdown carries upfront_fee_amount, annual_fee_pct, annual_fee_stream (year by year), fee_npv, and the discount rate used.
λ × Δspread, clamped at the floor h_floor.Δhurdle = λ × Δspread. Ranges from 1.10 (private PRI) to 1.90 (IBRD/IDA PBG). Derived structurally in Appendix E.S) — The primary planner-view verdict. Defined as NPV(all-in cost stream A) − NPV(all-in cost stream B), both discounted at the public discount rate. Run B's tariff already covers the guarantee fee, so S is net of the fee. Take the guarantee if and only if S > 0.Endpoints (all under /api/v1/).
| Method | Path | Purpose |
|---|---|---|
| GET | /workspaces/{wid}/fm/credit-enhancement/products |
Catalogue of supported products |
| POST | /workspaces/{wid}/fm/credit-enhancement/evaluate-cp |
Planner-view evaluation (primary) |
| POST | /workspaces/{wid}/fm/credit-enhancement/projects/{pid}/eval-cp |
Planner-view on saved FM scenario |
| POST | /workspaces/{wid}/fm/credit-enhancement/evaluate |
Sponsor-view diagnostic (legacy) |
| POST | /workspaces/{wid}/fm/credit-enhancement/projects/{pid}/eval |
Sponsor-view on saved FM scenario |
| GET | /workspaces/{wid}/fm/credit-enhancement/scenarios |
List saved CE scenarios |
| POST | /workspaces/{wid}/fm/credit-enhancement/scenarios |
Save scenario |
| GET | /workspaces/{wid}/fm/credit-enhancement/scenarios/{sid} |
Load scenario |
| DELETE | /workspaces/{wid}/fm/credit-enhancement/scenarios/{sid} |
Delete scenario |
| POST | /workspaces/{wid}/fm/credit-enhancement/ai/insights |
Claude commentary |
| POST | /batch/fm/credit-enhancement-eval |
Public batch (single) |
| POST | /batch/fm/credit-enhancement-batch |
Public batch (up to 100) |
Pseudocode — planner view (evaluate_guarantee_central_planner).
def evaluate_guarantee_central_planner(
inputs, product, country, *,
base_equity_hurdle=0.18,
contract_type="user_pays",
discount_rate=None,
overlay=None,
):
discount_rate = discount_rate or inputs.funding.npv_discount_rate or 0.08
# Counterfactual + fee lookups (overlay first, fall back to seed table)
cf = estimate_counterfactual_terms(product, country, overlay=overlay)
fee = compute_guarantee_fee(
product, country,
covered_amount=_covered_amount(inputs, product),
fee_tenor=inputs.funding.tranches[0].total_term + cf.tenor_extension_years,
discount_rate=discount_rate,
)
# λ-based hurdle adjustment, clamped at the floor
lam = _HURDLE_REDUCTION_FACTOR_BY_PRODUCT.get(_key(product), 1.5)
delta_spread = cf.spread_reduction_bps / 10_000.0
h_B = max(base_equity_hurdle - lam * delta_spread, _EQUITY_HURDLE_FLOOR)
# Run A — bisect tariff against h_A
snap_a, tariff_a, reason_a = solve_tariff_for_target_irr(
inputs, target_hurdle=base_equity_hurdle, contract_type=contract_type,
)
a_npv = _revenue_total_npv(_with_tariff(inputs, tariff_a), discount_rate)
# Run B — bisect tariff against h_B, on guaranteed inputs
inputs_b = _apply_guarantee_terms(inputs, cf, fee)
snap_b, tariff_b, reason_b = solve_tariff_for_target_irr(
inputs_b, target_hurdle=h_B, contract_type=contract_type,
)
b_npv = _revenue_total_npv(_with_tariff(inputs_b, tariff_b), discount_rate)
# Society saving — primary verdict
saving = (a_npv - b_npv) if (a_npv is not None and b_npv is not None) else None
# Case classification, in this order. The no-lever test comes FIRST:
# it also leaves snap_b infeasible, and the two are not the same thing —
# one is a scenario that was never modelled, the other a project that was.
if reason_b == "no_base_lever_value":
case, decision = "not_applicable", "not_applicable"
elif not snap_b.feasible:
case, decision = "infeasible", "reject"
elif not snap_a.feasible:
case, decision = "feasibility_enabled", "take_with_caveat"
else:
case = "cost_reduction"
decision = "take" if (saving is not None and saving > 0) else "reject"
# Sponsor diagnostic (Δ at fixed hurdle) — for the friction warning
sponsor_uplift = (snap_b.equity_npv or 0.0) - (snap_a.equity_npv or 0.0) - fee.fee_npv
friction = _market_friction_warning(saving, sponsor_uplift, contract_type)
return SocietyDecision(
case=case,
decision=decision,
contract_type=contract_type,
base_equity_hurdle=base_equity_hurdle,
guaranteed_equity_hurdle=h_B,
hurdle_reduction_pp=(base_equity_hurdle - h_B) * 100,
lambda_used=lam,
base_tariff_solved=tariff_a, base_run=snap_a, total_cost_society_a_npv=a_npv,
guaranteed_tariff_solved=tariff_b, guaranteed_run=snap_b, total_cost_society_b_npv=b_npv,
society_saving_npv=saving,
counterfactual=cf, fee=fee,
sponsor_view_eNPV_uplift=sponsor_uplift,
market_friction_warning=friction,
notes=cf.notes + fee.notes,
)
Pseudocode — sponsor-view diagnostic (evaluate_guarantee, legacy).
def evaluate_guarantee(inputs, product, country, *, discount_rate=None):
discount_rate = discount_rate or inputs.funding.npv_discount_rate or 0.08
# Sizing: covered amount = senior debt amount × coverage_pct
capex = (
inputs.general.construction_cost
+ inputs.general.land_cost
+ inputs.general.design_cost
+ inputs.general.other_cost
) or inputs.general.total_investment
senior_debt_amount = capex * inputs.funding.debt_pct * inputs.funding.tranches[0].share
covered_amount = senior_debt_amount * product.coverage_pct
# Counterfactual + fee lookups
cf = estimate_counterfactual_terms(product, country)
base_tenor = inputs.funding.tranches[0].total_term
fee_tenor = base_tenor + cf.tenor_extension_years
fee = compute_guarantee_fee(
product, country, covered_amount, fee_tenor,
discount_rate=discount_rate,
)
# Run A — baseline
result_a = run_model(inputs)
snap_a = _snapshot(result_a, discount_rate)
# Run B — with guarantee
inputs_b = _apply_guarantee_terms(inputs, cf, fee)
result_b = run_model(inputs_b)
snap_b = _snapshot(result_b, discount_rate)
# Differential
differential = (snap_b.equity_npv or 0.0) - (snap_a.equity_npv or 0.0) - fee.fee_npv
flip = (not snap_a.feasible) and snap_b.feasible
# Decision
if not snap_b.feasible:
decision = "infeasible_either_way" if not snap_a.feasible else "reject"
elif differential > 0 or flip:
decision = "take"
else:
decision = "reject"
return GuaranteeDecision(
baseline=snap_a, guaranteed=snap_b,
counterfactual=cf, fee=fee,
differential_equity_npv=differential,
bankability_flip=flip,
decision=decision,
binding_constraint=...,
notes=cf.notes + fee.notes,
)
Reference-table seed sources. The default fee tables and counterfactual-term tables in ppp_core.engine.credit_enhancement are seeded from public sources: MIGA Annual Reports + published rate cards1, World Bank Treasury fee schedule for IBRD / IDA guarantees2, IFC public rate disclosures3, ADB / AfDB / IDB / EBRD published rates4, and OECD Arrangement minimum reference rates5. The seed values are mid-range estimates and are intentionally easy to override per transaction.
The whole framework's primary verdict turns on the relationship Δhurdle = λ × Δspread (with Δspread expressed as a fraction). This appendix derives that relationship, explains why λ is product-specific, gives the calibration table, and motivates the equity-hurdle floor h_floor = 7 %.
Let the project's required equity return decompose into a risk-free rate, a market risk premium, and a project-specific premium for project equity beta:
\[h \;=\; r_f \;+\; \beta_{equity} \cdot ERP\]
where ERP is the equity risk premium (≈ 5–6 % for emerging markets). The project's equity beta itself decomposes into the unlevered asset beta and a leverage multiplier:
\[\beta_{equity} \;=\; \beta_{asset} \cdot \Bigl[\, 1 + \tfrac{D}{E}(1 - t) \,\Bigr]\]
The senior commercial debt spread compensates lenders for the same underlying project risk, attenuated by the seniority premium and by the lender's recovery expectation. To first order:
\[\Delta\text{spread}_{debt} \;=\; \sigma \cdot \rho_{debt} \cdot \beta_{asset}\]
where σ is the asset-volatility scale and ρ_debt is the share of debt-side variance that flows through to lender pricing. The same underlying β_asset change drives equity-side compression:
\[\Delta h_{equity} \;=\; \beta_{asset, removed} \cdot ERP \cdot \Bigl[\, 1 + \tfrac{D}{E}(1 - t) \,\Bigr]\]
Dividing the equity-side compression by the debt-side compression yields:
\[\lambda \;=\; \frac{\Delta h_{equity}}{\Delta\text{spread}_{debt}} \;=\; \frac{ERP \cdot \bigl[\, 1 + \tfrac{D}{E}(1-t) \,\bigr]}{\sigma \cdot \rho_{debt}}\]
So λ is high when (i) the equity risk premium is large, (ii) leverage is high, (iii) the share of risk that flows through to the debt is small (because ρ_debt in the denominator is small). It is low when the guarantee transfers risk that primarily affects debt-service (e.g. transfer/convertibility) but leaves the equity beta nearly unchanged.
The risk a guarantee transfers is not a scalar — it is a bundle of perils. Different products transfer different bundles:
| Product | Perils transferred | Equity-side bite | λ |
|---|---|---|---|
| IBRD-PBG-loan | Full sovereign-credit risk on debt service | Very high — sovereign credit is the dominant equity-beta driver in frontier markets | 1.90 |
| IDA-PBG-loan | Same as IBRD-PBG, on IDA-eligible (lower-income) countries | Very high — same structural transfer as IBRD-PBG | 1.90 |
| MIGA-NHSFO | Non-honouring of sovereign financial obligations (debt service) | High — but more narrowly debt-side than IBRD-PBG | 1.80 |
| IFC-PCG | Partial credit on a tranche of senior debt | Medium-high — IFC's preferred-creditor umbrella also benefits equity | 1.50 |
| ADB-PCG | Partial credit on a tranche of senior debt | Medium-high — analogous to IFC-PCG | 1.50 |
| OECD-ECA | Commercial + political risk on the export-financed portion only | Medium — Annex VIII pricing reflects partial transfer | 1.40 |
| MIGA-PRI | Four political-risk perils only (transfer, expropriation, war, breach) | Lower — leaves commercial / demand risk with the sponsor | 1.30 |
| PRIVATE-PRI | Same perils as MIGA-PRI but with insurer balance-sheet risk and re-rating | Lowest — discounts for re-rating risk and counterparty | 1.10 |
The ranking is what the structural model predicts: the more of the equity-relevant risk a guarantee transfers, the higher λ. IBRD-PBG sits at the top because it transfers full sovereign credit, which in frontier markets is the single largest contributor to project equity beta. Private PRI sits at the bottom because the sponsor must reserve a discount for the insurer's own credit and re-rating risk.
The values in §10 and the table above are the platform's current seed values, locked by the calibration test suite (backend/tests/test_credit_enhancement_calibration.py). They were chosen to (i) preserve the structural ranking implied by the perils-transferred analysis, (ii) reproduce the order of magnitude of observed equity-hurdle compression in a small set of recent transactions (Senegal Senergy 2024, Bangladesh Summit 2023, Kenya Lake Turkana, IFC Mocoa 2022)8, and (iii) fall comfortably below the upper bound implied by [1 + (D/E)(1−t)] · ERP / σ, which for typical frontier-market parameters (D/E = 70/30, t = 25 %, ERP = 6 %, σ ≈ 8 %) sits around λ ≈ 2.5.
A formal back-test of λ against a larger transaction sample is the subject of the open calibration round documented in papers/CE_CALIBRATION_NOTES.md and surfaced in §11.3.
h_floorThe structural formula is linear in Δspread and in principle can drive h_B arbitrarily low. Empirically that is not what happens: even on heavily de-risked transactions, equity sponsors still demand a minimum premium for sponsor effort, illiquidity, and idiosyncratic operational risk that no guarantee covers. The framework imposes:
\[h_B \;=\; \max\bigl(\, h_A - \lambda \cdot \Delta\text{spread},\;\; h_{floor} \,\bigr), \qquad h_{floor} = 7\%\]
The 7 % figure is calibrated as r_f + minimum_equity_premium ≈ 4 % + 3 %, where the 3 % minimum equity premium is the lower bound observed across mature-market (CRC 0–2) infrastructure equity transactions. In the §10 worked example the floor never binds: IBRD-PBG drops the hurdle to 10.61 %, MIGA-PRI to 14.92 %, ECA to 15.61 %. The floor only binds in extreme cases — heavily-covered guarantees in low-risk countries — where it corrects an otherwise unrealistic Run B hurdle.
The headline society-saving figures are reasonably robust to ±20 % perturbations of λ, because society's saving depends on the combined effect of the spread compression (in the empirical counterfactual table) and the hurdle compression (via λ). For the §10 IBRD-PBG candidate, λ = 1.5 instead of 1.9 reduces society's saving from US$ 527 M to roughly US$ 410 M — still a strong TAKE; λ = 0.7 (i.e. closer to a no-equity-channel assumption) would reduce it to ~US$ 220 M. Only at λ ≈ 0 — i.e. assuming the equity hurdle does not move at all — does the planner verdict collapse onto the sponsor diagnostic. Practitioners with a credible alternative calibration of λ should override per transaction; the framework propagates the override through the audit chain.
23 October 2026 — the planner's fourth verdict, and what Stage 1 does at the ends of its bracket. Two corrections, both to the description of what evaluate_guarantee_central_planner returns. No number in this paper changes. The §10 worked example (IBRD-PBG +US$ 527 M, MIGA-PRI +US$ 241 M, OECD-ECA +US$ 94 M), the λ sensitivity of §E.5, the reference fee and counterfactual tables and every figure are unaffected; both corrections are about how outcomes are named and when the evaluation stops.
not_applicable was missing from the verdict setVersions of this paper issued before this date listed three planner verdicts (take, reject, take_with_caveat) and three cases (feasibility_enabled, cost_reduction, infeasible). There are four of each. §2.1's decision rule and §7's decision table also named two codes the planner never returns — infeasible_either_way (the sponsor view's) and bankability_flip (a sponsor-view flag; the planner reports that case as take_with_caveat). Both now show the engine's own case/decision pairs.
A scenario that carries no revenue lever of the chosen contract type — no user tariff under a user-pays contract, no availability payment under an availability contract — makes both tariff bisections return no_base_lever_value before either run is built. The platform used to report that as reject. It now reports case = decision = "not_applicable", on the ground that "reject" is a verdict on an instrument that was evaluated, and here nothing was: neither run exists, so there is no saving, no solved tariff and no comparison. The decision notes say it in words — "This is not a rejection: neither run could be built, so the planner test was not run." — the decision card prints the verdict in neutral grey rather than the red reserved for a rejection, and anything that counts rejections must exclude it.
The new verdict is described in §7 and now appears in the decision rule of §2.1, the loss function of §2.6, the decision table and card description of §7, the walkthrough step of §9, the response and dataclass listings of Appendices A and B, the pseudocode of Appendix D, and the shipped list of §11.2.
§3 used to state that when even the lower bound of the plausible tariff range produces an equity IRR above h_A, the framework "returns decision = "reject" early". It does not, and never did.
What the solver does at that end of the bracket is report already_above_target and adopt the bottom of the bracket as the solved tariff. The reason code travels on the decision and is turned into a sentence in the audit notes, but the evaluation continues: Run B is built and priced, and the verdict comes out of the Stage-5 rule like any other. When both runs end up pinned to the same bracket floor the saving is exactly zero and the verdict is indeed reject — by the general S ≤ 0 rule, not by a short circuit. It can never come out take: both runs search the same tariff range, so Run B never solves below Run A's floor and S is never positive here. (The only other outcome is the bankability flip, when Run A clears the hurdle at the floor but does not cover its debt service there and the guarantee cures that.) The old text named the right verdict and the wrong mechanism.
The same section's Gate paragraph carried two related errors. It attributed infeasible_either_way to the planner view; that code belongs to the sponsor-view diagnostic evaluate_guarantee, and the planner reports case = "infeasible" with decision = "reject" instead. And it implied the planner classifies on Run A's feasibility; it classifies on Run B's — a Run A that fails at the top of its bracket while Run B succeeds is the bankability flip, not an infeasibility.
Both corrections were found by regenerating the paper against the engine rather than against the previous draft.
Multilateral Investment Guarantee Agency (MIGA), MIGA Annual Report 2024, World Bank Group, 2024. Membership figures and Political Risk Insurance (PRI) and Non-Honoring of Sovereign Financial Obligations (NHSFO) fee structure. https://www.miga.org/annual-report. ↩↩↩↩
World Bank Treasury, IBRD Project-Based Guarantees and Policy-Based Guarantees — Financial Terms, World Bank Group, 2023. Front-end fee, annual guarantee fee, standby fee, and IDA guarantee terms for low-income countries. https://treasury.worldbank.org/en/about/unit/treasury/ibrd/ibrd-financial-products/project-based-guarantees. ↩↩↩↩↩↩
International Finance Corporation, IFC Annual Report 2024, World Bank Group, 2024. Partial Credit Guarantee (PCG) product disclosures and pricing principles. https://www.ifc.org/en/annual-report. ↩↩↩
Asian Development Bank, Guarantees — Financing Partnerships and Risk Mitigation Instruments, Asian Development Bank, 2023. Fee structure for ADB Partial Credit and Political Risk Guarantees; analogous documents from AfDB, IDB, and EBRD provide regional-MDB benchmarks. https://www.adb.org/what-we-do/financing-instruments. ↩↩↩
Organisation for Economic Co-operation and Development (OECD), Arrangement on Officially Supported Export Credits — Annex VIII (Project Finance), document TAD/PG(2024)1, OECD Trade and Agriculture Directorate, 2024. Minimum Premium Rate (MPR) methodology and the tenor multiplier capped at 2.50× for tenors above 20 years. https://www.oecd.org/trade/topics/export-credits/arrangement-and-sector-understandings/. ↩↩↩↩↩↩↩
OECD, Country Risk Classifications of the Participants to the Arrangement on Officially Supported Export Credits, OECD, updated quarterly. Eight-class scale (0 lowest risk to 7 highest) used as the index for fee schedules and counterfactual-term tables. https://www.oecd.org/en/topics/sub-issues/credit-risk-and-rating/country-risk-classification.html. ↩
Engel, E., Fischer, R., and Galetovic, A., "The basic public finance of public-private partnerships," Journal of the European Economic Association, 11 (1), 2013, pp. 83–111. Establishes the equilibrium-pricing result that the equity hurdle adjusts when project risk is transferred, so risk-allocation gains accrue to the buyer of services rather than to the sponsor. https://doi.org/10.1111/j.1542-4774.2012.01105.x. ↩↩
World Bank, Private Participation in Infrastructure (PPI) Database, World Bank Group. Used as the back-test base for counterfactual-term calibration; specific transactions cited (Senegal Senergy 2024, Bangladesh Summit 2023, Kenya Lake Turkana, IFC Mocoa 2022) are catalogued there. https://ppi.worldbank.org/. ↩↩↩
APMG International with the World Bank, Asian Development Bank, Inter-American Development Bank, European Bank for Reconstruction and Development, and Islamic Development Bank, PPP Certification Program Guide — Chapter 8: Risk Identification, Allocation, and Management, 2016. Standard reference for the practitioner modes (heuristic, lender pitch, spreadsheet comparison) discussed in §1. https://ppp-certification.com/. ↩