Credit Committee Packets
You produce a single JSON object that a lending committee will read. The grader compares
your numbers, enums, ordering, and rounding against a standard answer, so precision and
the exact procedure matter more than narrative. There is no partial credit for prose.
Everything you need comes from one read-only HTTP API and one authoritative policy
document. The policy endpoint is the single source of truth for every threshold —
never hardcode a band or formula from memory; fetch it and apply it.
0. Universal workflow (do this every task)
- Read the prompt and identify: the task type (see §1), the target
branch_id or
segment_id, the review/as-of date, and any explicit population filter (e.g. "rated
3 or worse", "current_rating 6 or worse", the two application_ids to compare).
- Read
input/payloads/answer_template.json (or the template the prompt names). It
defines the required top-level keys, every field, the enums, the ordering rule, and the
rounding precision for each field. Treat it as a contract: output exactly those keys,
nothing more, nothing less, and obey each declared ordering and precision.
- Fetch the data from the API (§2). Always fetch
/api/policies first.
- Compute using the SOPs in §3–§7 for the matching task type.
- Emit only the JSON object. No markdown fence, no commentary outside the JSON.
If the prompt mentions env/setup.sh or an "API base URL printed by setup", ignore that
plumbing — the API is already live at http://127.0.0.1:8003. Do not look for local
data, db, or env files; the HTTP API is the only source.
1. Identifying the task type
Match the prompt and the template's top-level keys to one of five families:
| Signal in prompt / template keys |
Task type |
SOP |
portfolio_regrade, "rating migration", "re-derive risk ratings", material_downgrades, npa_benchmark |
A. Regrade & migration review |
§3 |
allocation, decisions, concentration_flags, decline_reasons, pending applications, "lending capacity" |
B. Capacity allocation package |
§4 |
posture, state_metrics, peer_comparison, controls, escalation_triggers, segment_id, NCUA |
C. Credit-union segment posture |
§5 |
watch_list_summary, stress_results, workout_queue, severe_bucket_counts, "adverse rated", CDFI risk class |
D. Watch-list stress & workout |
§6 |
applications_compared, weighted_cdfi_score, recommended_path, competing CRE, conditions |
E. Competing CRE decision |
§7 |
Read references/policy_and_formulas.md for the full policy schema, every band/table,
and the exact arithmetic conventions. Read it whenever you are unsure of a threshold.
2. Using the HTTP API
Base URL http://127.0.0.1:8003. All responses are JSON; money/ratios are plain numbers.
branch_id and segment_id are matched case-insensitively. Fetch with curl or Python
urllib. The endpoints you will actually use:
GET /api/policies — authoritative thresholds. Fetch first, every task.
GET /api/branches/{id} — branch row: lending_capacity_q1, cre_policy_limit_pct,
sector_ceiling_pct, state_code, institution_type, fdic_benchmark_set.
GET /api/branches/{id}/metrics?quarter=YYYYQn — quarterly metrics. Use the 2025Q1
row unless the review date implies otherwise. Fields: total_loans_outstanding,
nonperforming_loans, delinquency_30_plus_pct, net_charge_offs,
allowance_for_loan_losses. The endpoint returns a list (newest quarters included);
filter to the right quarter.
GET /api/branches/{id}/loans — one row per loan. Key fields: loan_id,
current_rating, dscr, ltv, debt_to_asset, fico, liquidity_months,
payment_status, days_past_due, outstanding_balance, loan_type, sector,
borrower_name, collateral_value. Some factors are null — handle missing factors
explicitly (see §3). Optional filters: ?loan_type=, ?payment_status=,
?min_current_rating=.
GET /api/branches/{id}/sector-exposures — sector, current_exposure, limit_pct,
grandfathered.
GET /api/branches/{id}/applications — pending applications. Key fields:
application_id, requested_amount, dscr, ltv, fico, dti,
years_in_business, bankruptcy_months_ago, sba_guaranty_pct,
documentation_complete, loan_type, sector, plus CRE scoring inputs
(net_income, collateral_value, existing_relationship_years, co_guarantor_strength).
GET /api/benchmarks/fdic/q4-2024 — total_loans_noncurrent_pct,
total_real_estate_noncurrent_pct, construction_development_noncurrent_pct,
total_real_estate_30_89_pct, construction_development_30_89_pct.
GET /api/benchmarks/ncua/q1-2025?state_code=XX — per-state credit-union rows:
delinquency_bps, loan_to_share_pct, roaa_bps, positive_net_income_pct. Omit the
filter to get all states including a US row (needed for medians/national comparison).
GET /api/credit-union-segments/{segment_id} — segment posture inputs:
minimum_checklist, peer_states, risk_tolerance, quarterly_capacity,
current_outstanding, and internal_context (recent_delinquency_bps, control_issue,
staffing_constraint).
3. SOP A — Regrade & rating-migration review
Population: loans whose current_rating >= target_min (the prompt states the min,
usually 3 — "rated 3 or worse"). Higher rating number = worse credit.
Re-derive each loan's final rating = the worst (max) numeric rating across the
factors for which data is available (dominant_factor_rule):
- DSCR rating band (policy
risk_rating.dscr_thresholds): `>=1.5→3, >=1.25→4, >=1.05→5,
=1.0→6, <1.0→7`.
- LTV rating band (
ltv_thresholds): <=0.65→3, <=0.75→4, <=0.85→5, <=1.0→6, >1.0→7.
- Delinquency minimum (
delinquency_minimums) from payment_status: Current→none, 30 Days Past Due→4, 60 Days Past Due→5, 90+ Days Past Due→7, Nonaccrual→8. This is a
floor (the rating can be worse from another factor, never better).
final_rating = max(of the available factor ratings). If all factors are null/absent
(no DSCR, no LTV, payment Current), keep the loan's existing current_rating.
Output pieces:
target_loan_count, target_exposure = count and Σoutstanding_balance of the population.
final_rating_exposure_totals: group the population by final_rating; per group emit
{final_rating, loan_count, exposure}. Order ascending by final_rating.
migration_from_current_rating_3: only loans whose current_rating == 3, grouped by
final_rating, each with loan_ids (ascending). Order ascending by final_rating.
watch_list_action_coverage: loans that landed on a watch action after regrade. Map
final_rating → recommended_action (§8). Only watch-rated loans are covered —
loans with final_rating <= 5 get monitor and are excluded from coverage. Report
covered_loan_count, covered_exposure, and by_action groups (ascending by action,
each with ascending loan_ids).
material_downgrades: loans where final_rating - current_rating >= material_downgrade_notches
(policy value, currently 2). Per loan: {loan_id, current_rating, final_rating, downgrade_notches, exposure}. Order ascending by loan_id.
npa_benchmark: see §9.
top_problem_credit: the single worst credit — highest final_rating, breaking ties by
largest exposure. Emit loan_id, borrower_name, exposure, current_rating, final_rating, payment_status, recommended_action.
4. SOP B — Capacity allocation package
Goal: decide each pending application, allocate the branch's quarterly lending capacity to
the strongest credits, and report concentration impact.
Per-application screen. Decline (or condition) on objective red flags. Thresholds
inferred and confirmed against the policy bands — verify each against /api/policies:
weak_dscr: DSCR below the new-loan floor (~1.15; income-producing loans only — CRE,
C&I, SBA, Equipment).
high_ltv: LTV above ~0.80 for commercial/income-producing loans. Retail loans
(Consumer, Residential Mortgage) tolerate higher LTV when FICO is strong.
low_fico: FICO below ~620 (retail loans — Consumer/Residential — screen on FICO+LTV,
not DSCR).
startup_risk: years_in_business < 2.
recent_bankruptcy: bankruptcy_months_ago present and recent (<= ~24 months).
capacity_limit: an otherwise-clean credit that falls below the committee's allocation
cutoff this round (lowest-quality qualifying applicant when appetite is constrained).
sector_breach: approving would push the application's sector over its limit_pct.
Decision enum: approve, conditional_approve, decline, defer,
participation_required. Use conditional_approve when a credit is fundable only with a
mitigant (e.g. participation for a sector breach, or SBA guaranty + startup monitoring for
a young borrower). Declined apps get approved_amount = 0.0, bank_capacity_used = 0.0,
conditions = ["none"].
bank_capacity_used (the capacity charge, distinct from the booked loan amount):
- Plain approve:
= approved_amount.
- SBA guaranty (
sba_guaranty_pct present): bank retains only the unguaranteed share:
bank_capacity_used = approved_amount * (1 - sba_guaranty_pct).
- Participation required (sector breach): bank retains the maximum
R that keeps the
breaching sector at its limit, solved against a base that also includes only retained
amounts:
R = (limit_pct * (total_loans_outstanding + sum_other_retained) - existing_sector_exposure) / (1 - limit_pct)
where sum_other_retained is the Σbank_capacity_used of the other approvals.
allocation block: lending_capacity_q1 (from the branch row), gross_approved_amount
= Σapproved_amount over approved + conditionally-approved apps (full loan amounts),
committed_capacity_amount = Σbank_capacity_used, remaining_capacity =
lending_capacity_q1 - committed_capacity_amount, and priority_ranking = the approved /
conditionally-approved application_ids ordered best credit first (lowest re-derived risk
rating, then higher DSCR). Approved-and-conditional only — never list declined/deferred.
concentration_flags: one row per sector that a new approval pushes to/over its limit.
{sector, application_id, limit_pct, post_approval_pct (ratio, 4 dp), flag (bool), handling}. Order by sector then application_id.
decline_reasons: object mapping each declined application_id → a list of reason
codes sorted ascending alphabetically. Only declined apps appear.
post_approval_concentrations: one row per sector that received an approval. Use the
post-book base base = total_loans_outstanding + gross_approved_amount (full approved
amounts, not retained). exposure_after_approval = existing_sector_exposure + full_approved_amount_for_that_sector; post_approval_pct = exposure_after_approval / base
(ratio, 4 dp); limit_pct from the sector row; over_limit bool. Order ascending by sector.
5. SOP C — Credit-union segment posture
state_metrics: the NCUA row for the segment's state_code, copied as integers
exactly as reported (delinquency_bps, loan_to_share_pct, roaa_bps,
positive_net_income_pct), plus state_code and benchmark_version (ncua_q1_2025).
peer_comparison: peer_states = the segment's peer_states (ascending). For each of
the 4 metrics, give the direction of NC's value relative to (a) the US row
(nc_vs_us) and (b) the median across the peer states (nc_vs_peer_median):
higher/lower/equal. Remember the polarity: higher delinquency = worse; lower roaa /
lower positive_net_income = worse.
posture (continue_approving / continue_with_tighter_conditions /
temporarily_pause): choose continue_with_tighter_conditions when capacity is available
but external state metrics are weaker than peers/national and there are control issues;
temporarily_pause only if metrics are severely adverse or capacity is gone.
controls.required_checklist_gates = the segment's minimum_checklist (these are the
gates already mandated). added_operating_controls = the operational mitigants the
internal_context calls for, e.g. a missed-insurance-binder control issue →
pre_close_insurance_binder_verification + lien_perfection_prior_to_funding; weak
external delinquency → monthly_segment_delinquency_watch +
quarterly_state_benchmark_monitoring; staffing/control issue →
senior_underwriter_second_review. Both are sets — emit ascending/sorted, deduplicated.
escalation_triggers: ordered ET001, ET002, .... Each {trigger_id, condition, owner}
from the allowed enums. Typical mapping: delinquency breach →
segment_recent_delinquency_ge_90_bps / credit_risk_manager; insurance-or-lien gap →
missing_insurance_or_lien_exception / operations_control_manager; capacity overrun →
quarterly_capacity_exceeded_or_exception_requested / lending_committee_chair.
interpretation: capacity_status (capacity_available when quarterly_capacity
remains), external_risk_status (weaker_than_national_and_peers when NC is worse on the
risk metrics vs both US and peer median), risk_tolerance (pass through the segment's
risk_tolerance), committee_message (the enum matching the posture — e.g.
capacity_available_but_external_risk_weaker).
6. SOP D — Watch-list stress & workout
Population: "adverse rated" = loans with current_rating >= adverse_min (prompt states
it, usually 6). adverse_loan_count, adverse_balance = Σoutstanding_balance.
risk_classes (CDFI factor scoring): for each adverse loan, sum the per-factor scores
from policy cdfi_factor_scores over the available factors (skip nulls):
- FICO:
>720→0, 680-720→1, 580-679→3, <580→5.
- LTV:
<0.40→0, 0.40-0.60→2, 0.60-0.80→4, >0.80→6.
- debt_to_asset: same band structure as LTV (
<0.40→0, 0.40-0.60→2, 0.60-0.80→4, >0.80→6).
- liquidity_months:
>12→0, 6-12→1, 3-6→3, <3→5.
factor_score = that sum (report as the integer base score). Map to risk_class by band:
0-5 Prime, 6-9 Desirable, 10-13 Satisfactory, 14-18 Watch, >=19 Doubtful. Projected
Loss override: a credit with ltv > 1.0 in a loss posture (Nonaccrual) is Projected Loss even if its base score sits in the Watch band. Order risk_classes ascending by
loan_id. monitoring_cadence: monthly for an adverse/distressed population.
stress_results (+200bp watch-list shock): policy watch_list_formula
stressed_dscr = dscr / (1 + 0.18), breach_threshold = coverage_breach_threshold (1.0),
shock_label = "+200bp". Compute only for loans with DSCR available, ordered ascending
by loan_id: {loan_id, base_dscr, stressed_dscr (2 dp), breaches_threshold} where
breaches_threshold = stressed_dscr < 1.0. breach_loan_ids = the breaching ids ascending.
workout_queue: every adverse loan, ordered descending by exposure, then ascending
loan_id. {loan_id, exposure, risk_class, payment_status, recommended_action, projected_loss}. recommended_action per §8; projected_loss = (risk_class == "Projected Loss").
severe_bucket_counts: group the adverse population by (current_rating, payment_status);
{current_rating, payment_status, loan_count, exposure}. Order ascending by
current_rating, then by payment_status (alphabetical — note "90+ Days Past Due" sorts
before "Current").
7. SOP E — Competing CRE decision
Compare the named CRE applications and pick the stronger.
weighted_cdfi_score (policy cre_weighted_score): a weighted average of five "C"
sub-scores, each on a 1–5 scale where lower is better, assigned from objective factors
(capacity from DSCR, collateral_exposure from LTV / sector concentration, plus conditions,
character, capital). Weights: capacity 0.45, collateral_exposure 0.36, conditions 0.11, character 0.05, capital 0.03. Report to 1 decimal. score_class by band: <=2.0 approve_quality, <=3.0 conditional, >3.0 weak. Order applications_compared ascending by
application_id; reason_codes ascending alphabetically.
stress (CRE dual stress): policy cre_dual_stress_formula
stressed_dscr = dscr * 0.85 / (1 + 0.18); report the formula string, threshold 1.0,
and per app {application_id, base_dscr, stressed_dscr (2 dp), breaches_threshold = stressed_dscr < 1.0}. Order ascending by application_id.
concentration: CRE exposure = Σoutstanding_balance of loan_type == "CRE" loans.
cre_policy_limit_pct from the branch. existing_cre_concentration = existing_cre_exposure / total_loans_outstanding. For the selected app, selected_post_approval_cre_concentration = (existing_cre_exposure + approved_amount) / (total_loans_outstanding + approved_amount)
(denominator grows with the new loan). selected_policy_variance_bps = (post_conc - cre_policy_limit_pct) * 10000 (from unrounded values, 2 dp). FDIC piece uses
fdic_benchmark_metric = total_real_estate_30_89_pct: branch_delinquency_ratio =
delinquency_30_plus_pct; fdic_variance_ratio = branch - benchmark (4 dp);
fdic_variance_bps = (branch - benchmark) * 10000 (2 dp).
recommended_path: select the lower (better) weighted score / non-breaching stress;
path = its decision. For the unselected credit choose decline or defer and give its
unselected_reason_codes (ascending). A breaching stress + sector breach + FDIC adverse
variance on a weak-class credit typically yields defer.
conditions: the set of CRE conditions attached to the selected path, sorted ascending
alphabetically (e.g. bank_retained_exposure_cap, committee_cre_exception,
minimum_dscr_covenant_1_25, tenant_roll_and_lease_review, updated_appraisal_before_close,
quarterly_financial_reporting, no_additional_cre_without_committee_review).
8. Final-rating / risk-class → recommended_action mapping
The watch-action ladder, consistent across tasks (action enum: monitor, watchlist,
special_assets, workout, partial_chargeoff_review, legal_referral):
final_rating == 8 or payment_status == "Nonaccrual" or risk_class == "Projected Loss" → partial_chargeoff_review.
final_rating == 7, or 90+ Days Past Due, or risk_class == "Watch" →
special_assets.
final_rating == 6 (or other adverse-but-performing) → watchlist.
final_rating <= 5 → monitor (and excluded from watch-list coverage in SOP A).
9. NPA / benchmark-variance arithmetic (shared)
For a branch noncurrent/NPA comparison (npa_benchmark):
branch_npa_exposure = nonperforming_loans (metrics, target quarter).
branch_total_loans = total_loans_outstanding.
branch_npa_ratio = branch_npa_exposure / branch_total_loans, displayed 4 dp.
benchmark_metric is total_loans_noncurrent_pct for an all-loans NPA review (or the
real-estate / construction variant when the prompt scopes to that book);
fdic_benchmark_ratio is that field from the FDIC benchmark, 4 dp.
variance_ratio = branch_npa_ratio - fdic_benchmark_ratio, 4 dp.
variance_bps = (branch_ratio - fdic_ratio) * 10000, 2 dp.
10. Rounding, precision, and ordering — read before you emit
These conventions are observed in the standard answers; getting them wrong fails the grade
even when the logic is right:
- Currency/USD fields → 2 decimals.
- Ratio/concentration/percentage fields → 4 decimals (they are ratios, e.g.
0.1897,
not 18.97). limit_pct is the raw policy value (e.g. 0.19).
- bps fields → 2 decimals.
- DSCR values (base/stressed) → 2 decimals. weighted_cdfi_score → 1 decimal.
- Compute bps from the UNROUNDED ratio, not the rounded display value. E.g. NPA ratio
displays
0.1135 but variance_bps is 1037.49 (from the full-precision 0.11354…),
not 1037.00. Round only at the final emission step; never chain rounded intermediates.
- breach is strict
< 1.0 at threshold 1.0 (a stressed DSCR of 0.97 breaches; 1.00 does
not).
- Obey every
ordering clause in the template exactly: ascending loan_id /
application_id / final_rating / action, descending exposure then ascending loan_id
for workout queues, alphabetical for reason-code and condition lists. String fields sort
lexicographically (so "90+ Days Past Due" < "Current").
- Enums are closed sets. Use only the allowed values from the template/policy; never
invent a status, action, reason code, or class.
11. Common misjudgments to avoid
- Don't skip the policy fetch. Bands and formulas can change with the policy version;
read
/api/policies every run and apply the live values.
- Re-derive ratings — don't trust
current_rating. The whole point of a regrade is that
the booked rating is stale; the final rating is the worst factor band.
- Worst factor wins; delinquency is a floor only.
Current contributes no delinquency
rating; a clean payment status never improves a rating set by DSCR/LTV.
- Handle nulls per factor (skip the missing factor; never treat null DSCR as 0). If all
factors are missing, fall back to the existing
current_rating.
- Booked amount vs capacity charge are different (SBA and participation reduce
bank_capacity_used but the full loan amount hits sector exposure and
gross_approved_amount).
- Use the right concentration base: post-approval sector pct uses
total_loans + gross_approved; CRE post-approval pct uses total_loans + that one approved amount.
- Only the required population is in scope — filter to
current_rating >= min (or the
stated population) before any grouping or totals; never include out-of-scope loans.
- Emit only the JSON object, exactly the template's keys, no extra fields, no prose.
When a detail is ambiguous, prefer the interpretation that is consistent across the policy
document and the field semantics in the template, and recompute against /api/policies
rather than guessing.
1---2name: credit-committee-packets-33description: Produce committee-ready JSON answers for the shared Credit Office / lending-committee task family: branch loan-rating regrades and migration reviews, branch lending-capacity allocation packages for pending applications, credit-union segment posture pages, watch-list stress and workout packets, and competing CRE underwriting decisions. Use this skill whenever a prompt references a Credit Risk Committee, lending committee, branch_id or segment_id, risk-rating regrade/migration, watch-list/adverse-rated loans, CDFI risk classes, DSCR stress (+200bp / dual stress), NPA or delinquency benchmark variance (FDIC/NCUA), sector/CRE concentration limits, lending capacity allocation, or asks for a JSON answer matching an answer_template.json against the credit office HTTP API at http://127.0.0.1:8003. Apply it even when the prompt only hints at these (e.g. "regrade the loans rated 3 or worse", "compare the two CRE requests", "posture recommendation for the segment") and does not name the skill explicitly.4---56# Credit Committee Packets78You produce a single JSON object that a lending committee will read. The grader compares9your numbers, enums, ordering, and rounding against a standard answer, so **precision and10the exact procedure matter more than narrative**. There is no partial credit for prose.1112Everything you need comes from one read-only HTTP API and one authoritative policy13document. **The policy endpoint is the single source of truth for every threshold** —14never hardcode a band or formula from memory; fetch it and apply it.1516## 0. Universal workflow (do this every task)17181. **Read the prompt** and identify: the task type (see §1), the target `branch_id` or19 `segment_id`, the review/as-of date, and any explicit population filter (e.g. "rated20 3 or worse", "current_rating 6 or worse", the two application_ids to compare).212. **Read `input/payloads/answer_template.json`** (or the template the prompt names). It22 defines the required top-level keys, every field, the enums, the ordering rule, and the23 rounding precision for each field. Treat it as a contract: output exactly those keys,24 nothing more, nothing less, and obey each declared `ordering` and `precision`.253. **Fetch the data** from the API (§2). Always fetch `/api/policies` first.264. **Compute** using the SOPs in §3–§7 for the matching task type.275. **Emit only the JSON object.** No markdown fence, no commentary outside the JSON.2829If the prompt mentions `env/setup.sh` or an "API base URL printed by setup", ignore that30plumbing — the API is already live at `http://127.0.0.1:8003`. Do not look for local31data, db, or env files; the HTTP API is the only source.3233## 1. Identifying the task type3435Match the prompt and the template's top-level keys to one of five families:3637| Signal in prompt / template keys | Task type | SOP |38|---|---|---|39| `portfolio_regrade`, "rating migration", "re-derive risk ratings", `material_downgrades`, `npa_benchmark` | **A. Regrade & migration review** | §3 |40| `allocation`, `decisions`, `concentration_flags`, `decline_reasons`, pending applications, "lending capacity" | **B. Capacity allocation package** | §4 |41| `posture`, `state_metrics`, `peer_comparison`, `controls`, `escalation_triggers`, segment_id, NCUA | **C. Credit-union segment posture** | §5 |42| `watch_list_summary`, `stress_results`, `workout_queue`, `severe_bucket_counts`, "adverse rated", CDFI risk class | **D. Watch-list stress & workout** | §6 |43| `applications_compared`, `weighted_cdfi_score`, `recommended_path`, competing CRE, `conditions` | **E. Competing CRE decision** | §7 |4445Read **`references/policy_and_formulas.md`** for the full policy schema, every band/table,46and the exact arithmetic conventions. Read it whenever you are unsure of a threshold.4748## 2. Using the HTTP API4950Base URL `http://127.0.0.1:8003`. All responses are JSON; money/ratios are plain numbers.51`branch_id` and `segment_id` are matched case-insensitively. Fetch with `curl` or Python52`urllib`. The endpoints you will actually use:5354- `GET /api/policies` — authoritative thresholds. **Fetch first, every task.**55- `GET /api/branches/{id}` — branch row: `lending_capacity_q1`, `cre_policy_limit_pct`,56 `sector_ceiling_pct`, `state_code`, `institution_type`, `fdic_benchmark_set`.57- `GET /api/branches/{id}/metrics?quarter=YYYYQn` — quarterly metrics. Use the **2025Q1**58 row unless the review date implies otherwise. Fields: `total_loans_outstanding`,59 `nonperforming_loans`, `delinquency_30_plus_pct`, `net_charge_offs`,60 `allowance_for_loan_losses`. The endpoint returns a list (newest quarters included);61 filter to the right `quarter`.62- `GET /api/branches/{id}/loans` — one row per loan. Key fields: `loan_id`,63 `current_rating`, `dscr`, `ltv`, `debt_to_asset`, `fico`, `liquidity_months`,64 `payment_status`, `days_past_due`, `outstanding_balance`, `loan_type`, `sector`,65 `borrower_name`, `collateral_value`. Some factors are `null` — handle missing factors66 explicitly (see §3). Optional filters: `?loan_type=`, `?payment_status=`,67 `?min_current_rating=`.68- `GET /api/branches/{id}/sector-exposures` — `sector`, `current_exposure`, `limit_pct`,69 `grandfathered`.70- `GET /api/branches/{id}/applications` — pending applications. Key fields:71 `application_id`, `requested_amount`, `dscr`, `ltv`, `fico`, `dti`,72 `years_in_business`, `bankruptcy_months_ago`, `sba_guaranty_pct`,73 `documentation_complete`, `loan_type`, `sector`, plus CRE scoring inputs74 (`net_income`, `collateral_value`, `existing_relationship_years`, `co_guarantor_strength`).75- `GET /api/benchmarks/fdic/q4-2024` — `total_loans_noncurrent_pct`,76 `total_real_estate_noncurrent_pct`, `construction_development_noncurrent_pct`,77 `total_real_estate_30_89_pct`, `construction_development_30_89_pct`.78- `GET /api/benchmarks/ncua/q1-2025?state_code=XX` — per-state credit-union rows:79 `delinquency_bps`, `loan_to_share_pct`, `roaa_bps`, `positive_net_income_pct`. Omit the80 filter to get all states including a `US` row (needed for medians/national comparison).81- `GET /api/credit-union-segments/{segment_id}` — segment posture inputs:82 `minimum_checklist`, `peer_states`, `risk_tolerance`, `quarterly_capacity`,83 `current_outstanding`, and `internal_context` (`recent_delinquency_bps`, `control_issue`,84 `staffing_constraint`).8586## 3. SOP A — Regrade & rating-migration review8788**Population:** loans whose `current_rating >= target_min` (the prompt states the min,89usually 3 — "rated 3 or worse"). Higher rating number = worse credit.9091**Re-derive each loan's final rating** = the **worst (max) numeric rating** across the92factors for which data is available (`dominant_factor_rule`):9394- DSCR rating band (policy `risk_rating.dscr_thresholds`): `>=1.5→3, >=1.25→4, >=1.05→5,95 >=1.0→6, <1.0→7`.96- LTV rating band (`ltv_thresholds`): `<=0.65→3, <=0.75→4, <=0.85→5, <=1.0→6, >1.0→7`.97- Delinquency minimum (`delinquency_minimums`) from `payment_status`: `Current→none,98 30 Days Past Due→4, 60 Days Past Due→5, 90+ Days Past Due→7, Nonaccrual→8`. This is a99 **floor** (the rating can be worse from another factor, never better).100101`final_rating = max(of the available factor ratings)`. If **all** factors are null/absent102(no DSCR, no LTV, payment `Current`), keep the loan's existing `current_rating`.103104**Output pieces:**105- `target_loan_count`, `target_exposure` = count and Σ`outstanding_balance` of the population.106- `final_rating_exposure_totals`: group the population by `final_rating`; per group emit107 `{final_rating, loan_count, exposure}`. Order **ascending by final_rating**.108- `migration_from_current_rating_3`: only loans whose `current_rating == 3`, grouped by109 `final_rating`, each with `loan_ids` (ascending). Order ascending by `final_rating`.110- `watch_list_action_coverage`: loans that landed on a watch action after regrade. Map111 `final_rating → recommended_action` (§8). **Only watch-rated loans are covered** —112 loans with `final_rating <= 5` get `monitor` and are excluded from coverage. Report113 `covered_loan_count`, `covered_exposure`, and `by_action` groups (ascending by `action`,114 each with ascending `loan_ids`).115- `material_downgrades`: loans where `final_rating - current_rating >= material_downgrade_notches`116 (policy value, currently 2). Per loan: `{loan_id, current_rating, final_rating,117 downgrade_notches, exposure}`. Order ascending by `loan_id`.118- `npa_benchmark`: see §9.119- `top_problem_credit`: the single worst credit — highest `final_rating`, breaking ties by120 largest `exposure`. Emit `loan_id, borrower_name, exposure, current_rating, final_rating,121 payment_status, recommended_action`.122123## 4. SOP B — Capacity allocation package124125Goal: decide each pending application, allocate the branch's quarterly lending capacity to126the strongest credits, and report concentration impact.127128**Per-application screen.** Decline (or condition) on objective red flags. Thresholds129inferred and confirmed against the policy bands — verify each against `/api/policies`:130- `weak_dscr`: DSCR below the new-loan floor (~1.15; income-producing loans only — CRE,131 C&I, SBA, Equipment).132- `high_ltv`: LTV above ~0.80 for commercial/income-producing loans. Retail loans133 (Consumer, Residential Mortgage) tolerate higher LTV when FICO is strong.134- `low_fico`: FICO below ~620 (retail loans — Consumer/Residential — screen on FICO+LTV,135 not DSCR).136- `startup_risk`: `years_in_business < 2`.137- `recent_bankruptcy`: `bankruptcy_months_ago` present and recent (<= ~24 months).138- `capacity_limit`: an otherwise-clean credit that falls below the committee's allocation139 cutoff this round (lowest-quality qualifying applicant when appetite is constrained).140- `sector_breach`: approving would push the application's sector over its `limit_pct`.141142**Decision enum:** `approve`, `conditional_approve`, `decline`, `defer`,143`participation_required`. Use `conditional_approve` when a credit is fundable only with a144mitigant (e.g. participation for a sector breach, or SBA guaranty + startup monitoring for145a young borrower). Declined apps get `approved_amount = 0.0`, `bank_capacity_used = 0.0`,146`conditions = ["none"]`.147148**bank_capacity_used (the capacity charge, distinct from the booked loan amount):**149- Plain approve: `= approved_amount`.150- **SBA guaranty** (`sba_guaranty_pct` present): bank retains only the unguaranteed share:151 `bank_capacity_used = approved_amount * (1 - sba_guaranty_pct)`.152- **Participation required** (sector breach): bank retains the maximum `R` that keeps the153 breaching sector at its limit, solved against a base that also includes only retained154 amounts:155 `R = (limit_pct * (total_loans_outstanding + sum_other_retained) - existing_sector_exposure) / (1 - limit_pct)`156 where `sum_other_retained` is the Σ`bank_capacity_used` of the other approvals.157158**allocation block:** `lending_capacity_q1` (from the branch row), `gross_approved_amount`159= Σ`approved_amount` over approved + conditionally-approved apps (full loan amounts),160`committed_capacity_amount` = Σ`bank_capacity_used`, `remaining_capacity` =161`lending_capacity_q1 - committed_capacity_amount`, and `priority_ranking` = the approved /162conditionally-approved `application_id`s ordered best credit first (lowest re-derived risk163rating, then higher DSCR). Approved-and-conditional only — never list declined/deferred.164165**concentration_flags:** one row per sector that a new approval pushes to/over its limit.166`{sector, application_id, limit_pct, post_approval_pct (ratio, 4 dp), flag (bool),167handling}`. Order by sector then application_id.168169**decline_reasons:** object mapping each **declined** `application_id` → a list of reason170codes **sorted ascending alphabetically**. Only declined apps appear.171172**post_approval_concentrations:** one row per sector that received an approval. Use the173post-book base **`base = total_loans_outstanding + gross_approved_amount`** (full approved174amounts, not retained). `exposure_after_approval = existing_sector_exposure +175full_approved_amount_for_that_sector`; `post_approval_pct = exposure_after_approval / base`176(ratio, 4 dp); `limit_pct` from the sector row; `over_limit` bool. Order ascending by sector.177178## 5. SOP C — Credit-union segment posture179180**state_metrics:** the NCUA row for the segment's `state_code`, copied **as integers181exactly as reported** (`delinquency_bps`, `loan_to_share_pct`, `roaa_bps`,182`positive_net_income_pct`), plus `state_code` and `benchmark_version` (`ncua_q1_2025`).183184**peer_comparison:** `peer_states` = the segment's `peer_states` (ascending). For each of185the 4 metrics, give the **direction of NC's value** relative to (a) the `US` row186(`nc_vs_us`) and (b) the **median across the peer states** (`nc_vs_peer_median`):187`higher`/`lower`/`equal`. Remember the polarity: higher delinquency = worse; lower roaa /188lower positive_net_income = worse.189190**posture** (`continue_approving` / `continue_with_tighter_conditions` /191`temporarily_pause`): choose `continue_with_tighter_conditions` when capacity is available192but external state metrics are weaker than peers/national and there are control issues;193`temporarily_pause` only if metrics are severely adverse or capacity is gone.194195**controls.required_checklist_gates** = the segment's `minimum_checklist` (these are the196gates already mandated). **added_operating_controls** = the operational mitigants the197internal_context calls for, e.g. a missed-insurance-binder control issue →198`pre_close_insurance_binder_verification` + `lien_perfection_prior_to_funding`; weak199external delinquency → `monthly_segment_delinquency_watch` +200`quarterly_state_benchmark_monitoring`; staffing/control issue →201`senior_underwriter_second_review`. Both are **sets** — emit ascending/sorted, deduplicated.202203**escalation_triggers:** ordered `ET001, ET002, ...`. Each `{trigger_id, condition, owner}`204from the allowed enums. Typical mapping: delinquency breach →205`segment_recent_delinquency_ge_90_bps` / `credit_risk_manager`; insurance-or-lien gap →206`missing_insurance_or_lien_exception` / `operations_control_manager`; capacity overrun →207`quarterly_capacity_exceeded_or_exception_requested` / `lending_committee_chair`.208209**interpretation:** `capacity_status` (`capacity_available` when `quarterly_capacity`210remains), `external_risk_status` (`weaker_than_national_and_peers` when NC is worse on the211risk metrics vs both US and peer median), `risk_tolerance` (pass through the segment's212`risk_tolerance`), `committee_message` (the enum matching the posture — e.g.213`capacity_available_but_external_risk_weaker`).214215## 6. SOP D — Watch-list stress & workout216217**Population:** "adverse rated" = loans with `current_rating >= adverse_min` (prompt states218it, usually 6). `adverse_loan_count`, `adverse_balance` = Σ`outstanding_balance`.219220**risk_classes (CDFI factor scoring):** for each adverse loan, sum the per-factor scores221from policy `cdfi_factor_scores` over the available factors (skip nulls):222- FICO: `>720→0, 680-720→1, 580-679→3, <580→5`.223- LTV: `<0.40→0, 0.40-0.60→2, 0.60-0.80→4, >0.80→6`.224- debt_to_asset: same band structure as LTV (`<0.40→0, 0.40-0.60→2, 0.60-0.80→4, >0.80→6`).225- liquidity_months: `>12→0, 6-12→1, 3-6→3, <3→5`.226227`factor_score` = that sum (report as the integer base score). Map to `risk_class` by band:228`0-5 Prime, 6-9 Desirable, 10-13 Satisfactory, 14-18 Watch, >=19 Doubtful`. **Projected229Loss override:** a credit with `ltv > 1.0` in a loss posture (Nonaccrual) is `Projected230Loss` even if its base score sits in the Watch band. Order `risk_classes` ascending by231`loan_id`. `monitoring_cadence`: `monthly` for an adverse/distressed population.232233**stress_results (+200bp watch-list shock):** policy `watch_list_formula`234`stressed_dscr = dscr / (1 + 0.18)`, `breach_threshold = coverage_breach_threshold` (1.0),235`shock_label = "+200bp"`. Compute only for loans **with DSCR available**, ordered ascending236by `loan_id`: `{loan_id, base_dscr, stressed_dscr (2 dp), breaches_threshold}` where237`breaches_threshold = stressed_dscr < 1.0`. `breach_loan_ids` = the breaching ids ascending.238239**workout_queue:** every adverse loan, ordered **descending by exposure, then ascending240loan_id**. `{loan_id, exposure, risk_class, payment_status, recommended_action,241projected_loss}`. `recommended_action` per §8; `projected_loss = (risk_class == "Projected242Loss")`.243244**severe_bucket_counts:** group the adverse population by `(current_rating, payment_status)`;245`{current_rating, payment_status, loan_count, exposure}`. Order ascending by246`current_rating`, then by `payment_status` (alphabetical — note "90+ Days Past Due" sorts247before "Current").248249## 7. SOP E — Competing CRE decision250251Compare the named CRE applications and pick the stronger.252253**weighted_cdfi_score** (policy `cre_weighted_score`): a weighted average of five "C"254sub-scores, each on a 1–5 scale where **lower is better**, assigned from objective factors255(capacity from DSCR, collateral_exposure from LTV / sector concentration, plus conditions,256character, capital). Weights: `capacity 0.45, collateral_exposure 0.36, conditions 0.11,257character 0.05, capital 0.03`. Report to **1 decimal**. `score_class` by band: `<=2.0258approve_quality, <=3.0 conditional, >3.0 weak`. Order `applications_compared` ascending by259`application_id`; `reason_codes` ascending alphabetically.260261**stress (CRE dual stress):** policy `cre_dual_stress_formula`262`stressed_dscr = dscr * 0.85 / (1 + 0.18)`; report the `formula` string, threshold 1.0,263and per app `{application_id, base_dscr, stressed_dscr (2 dp), breaches_threshold =264stressed_dscr < 1.0}`. Order ascending by application_id.265266**concentration:** CRE exposure = Σ`outstanding_balance` of `loan_type == "CRE"` loans.267`cre_policy_limit_pct` from the branch. `existing_cre_concentration = existing_cre_exposure /268total_loans_outstanding`. For the **selected** app, `selected_post_approval_cre_concentration269= (existing_cre_exposure + approved_amount) / (total_loans_outstanding + approved_amount)`270(denominator grows with the new loan). `selected_policy_variance_bps =271(post_conc - cre_policy_limit_pct) * 10000` (from unrounded values, 2 dp). FDIC piece uses272`fdic_benchmark_metric = total_real_estate_30_89_pct`: `branch_delinquency_ratio` =273`delinquency_30_plus_pct`; `fdic_variance_ratio = branch - benchmark` (4 dp);274`fdic_variance_bps = (branch - benchmark) * 10000` (2 dp).275276**recommended_path:** select the lower (better) weighted score / non-breaching stress;277`path` = its decision. For the unselected credit choose `decline` or `defer` and give its278`unselected_reason_codes` (ascending). A breaching stress + sector breach + FDIC adverse279variance on a weak-class credit typically yields `defer`.280281**conditions:** the set of CRE conditions attached to the selected path, sorted ascending282alphabetically (e.g. `bank_retained_exposure_cap`, `committee_cre_exception`,283`minimum_dscr_covenant_1_25`, `tenant_roll_and_lease_review`, `updated_appraisal_before_close`,284`quarterly_financial_reporting`, `no_additional_cre_without_committee_review`).285286## 8. Final-rating / risk-class → recommended_action mapping287288The watch-action ladder, consistent across tasks (action enum: `monitor`, `watchlist`,289`special_assets`, `workout`, `partial_chargeoff_review`, `legal_referral`):290291- `final_rating == 8` **or** `payment_status == "Nonaccrual"` **or** `risk_class ==292 "Projected Loss"` → `partial_chargeoff_review`.293- `final_rating == 7`, **or** `90+ Days Past Due`, **or** `risk_class == "Watch"` →294 `special_assets`.295- `final_rating == 6` (or other adverse-but-performing) → `watchlist`.296- `final_rating <= 5` → `monitor` (and excluded from watch-list coverage in SOP A).297298## 9. NPA / benchmark-variance arithmetic (shared)299300For a branch noncurrent/NPA comparison (`npa_benchmark`):301- `branch_npa_exposure` = `nonperforming_loans` (metrics, target quarter).302- `branch_total_loans` = `total_loans_outstanding`.303- `branch_npa_ratio` = `branch_npa_exposure / branch_total_loans`, **displayed 4 dp**.304- `benchmark_metric` is `total_loans_noncurrent_pct` for an all-loans NPA review (or the305 real-estate / construction variant when the prompt scopes to that book);306 `fdic_benchmark_ratio` is that field from the FDIC benchmark, 4 dp.307- `variance_ratio` = `branch_npa_ratio - fdic_benchmark_ratio`, 4 dp.308- `variance_bps` = `(branch_ratio - fdic_ratio) * 10000`, 2 dp.309310## 10. Rounding, precision, and ordering — read before you emit311312These conventions are observed in the standard answers; getting them wrong fails the grade313even when the logic is right:314315- **Currency/USD** fields → 2 decimals.316- **Ratio/concentration/percentage** fields → 4 decimals (they are ratios, e.g. `0.1897`,317 not `18.97`). `limit_pct` is the raw policy value (e.g. `0.19`).318- **bps** fields → 2 decimals.319- **DSCR** values (base/stressed) → 2 decimals. **weighted_cdfi_score** → 1 decimal.320- **Compute bps from the UNROUNDED ratio, not the rounded display value.** E.g. NPA ratio321 displays `0.1135` but `variance_bps` is `1037.49` (from the full-precision `0.11354…`),322 not `1037.00`. Round only at the final emission step; never chain rounded intermediates.323- **breach is strict `< 1.0`** at threshold 1.0 (a stressed DSCR of 0.97 breaches; 1.00 does324 not).325- **Obey every `ordering` clause in the template** exactly: ascending `loan_id` /326 `application_id` / `final_rating` / `action`, descending exposure then ascending loan_id327 for workout queues, alphabetical for reason-code and condition lists. String fields sort328 lexicographically (so `"90+ Days Past Due"` < `"Current"`).329- **Enums are closed sets.** Use only the allowed values from the template/policy; never330 invent a status, action, reason code, or class.331332## 11. Common misjudgments to avoid333334- **Don't skip the policy fetch.** Bands and formulas can change with the policy version;335 read `/api/policies` every run and apply the live values.336- **Re-derive ratings — don't trust `current_rating`.** The whole point of a regrade is that337 the booked rating is stale; the final rating is the worst factor band.338- **Worst factor wins; delinquency is a floor only.** `Current` contributes no delinquency339 rating; a clean payment status never improves a rating set by DSCR/LTV.340- **Handle nulls per factor** (skip the missing factor; never treat null DSCR as 0). If all341 factors are missing, fall back to the existing `current_rating`.342- **Booked amount vs capacity charge are different** (SBA and participation reduce343 `bank_capacity_used` but the **full** loan amount hits sector exposure and344 `gross_approved_amount`).345- **Use the right concentration base:** post-approval sector pct uses `total_loans +346 gross_approved`; CRE post-approval pct uses `total_loans + that one approved amount`.347- **Only the required population is in scope** — filter to `current_rating >= min` (or the348 stated population) before any grouping or totals; never include out-of-scope loans.349- **Emit only the JSON object**, exactly the template's keys, no extra fields, no prose.350351When a detail is ambiguous, prefer the interpretation that is consistent across the policy352document and the field semantics in the template, and recompute against `/api/policies`353rather than guessing.