A House Divided A House DividedDocumentation
Changelog
Game Design/Economy & Finance

Interbank Lending and Bank Resolution

Last updated 2026-08-20
Source files

This covers three related pieces of the private banking system: the interbank market and central-bank margin line (), what happens to a bank's outstanding loan book after it fails (), and the NPP-run banks that seed into every eligible country (). All three sit inside the wider private banking system in src/lib/banking/, which also covers charters, deposits, reserves, capital adequacy, the discount window, and deposit insurance (not documented here). Every path in this doc is gated behind privateBankingEnabled on gameConfig, which the ops notes describe as currently OFF in production; the code confirms the gate exists () but this doc does not speak to current prod state beyond that.

Feature Flags#

defines three flags read off gameConfig._id: "default":

lendInterbank, repayInterbank, drawCbMargin, and repayCbMargin all check isPrivateBankingEnabled() && isBankPropTradingEnabled() and return an error if either is false. The two interbank API routes (bank/interbank/loans, bank/interbank/margin) separately gate on isBankPropTradingEnabled() and return 404 if off.

NPC bank seeding (seedNpcBanks) is explicitly NOT gated on privateBankingEnabled, charters are issued through the real issueCharter path with a skipFlagCheck bypass, so NPC banks exist in the world regardless of the flag. Runtime NPC bank behavior (processNpcBankPolicyTurn, the turn-phase entry point) IS gated: it no-ops and returns a zero summary when private banking is off.

Interbank Lending (interbank.ts)#

A deposit-taking bank (retail or universal charter) lends non-reserved cash to a bank running a prop book (investment or universal charter). This is separate from a retail bank's ordinary loan book: interbank loans are not part of totalLoans and are tracked on their own InterbankLoan documents plus a running bankCharter.interbankDebt on the borrower.

Eligibility#

Sizing constraints#

INTERBANK_MAX_SHARE_OF_LENDABLE = 0.5   // provisional

Lendable headroom is computed via getLendableHeadroom(lenderCharter, reserveRatio) (see reserves.ts), where reserveRatio = getReserveRequirement(db, currency). The lender may place at most 0.5 x headroom on the interbank market, checked as a running total across all of that lender's current interbank loans (sumLenderInterbankOutstanding, summed over status: "current"). A new loan is rejected if it alone exceeds the cap, or if adding it to already-outstanding interbank lending would exceed the cap. Separately, the lender must have enough liquid cashReserves to cover the amount.

Origination (lendInterbank)#

Cash moves lender reserves -> borrower reserves. An InterbankLoan document is created:

{
  lenderCorporationId, borrowerCorporationId, currency,
  principal, outstanding, ratePercent, originatedTurn,
  status: "current"
}

The write sequence is: insert loan -> debit lender bankCharter.cashReserves (conditioned on cashReserves >= amount) -> credit borrower cashReserves and increment bankCharter.interbankDebt. Each step is guarded and the function unwinds (deletes the loan doc, refunds the lender) if a later step's conditional update fails to match, since the code notes standalone Mongo has no transactions. A bank_interbank_lend transaction is emitted via emitTx.

Repayment (repayInterbank)#

Repays principal only, capped at min(amount, loan.outstanding). Cash moves borrower -> lender; bankCharter.interbankDebt and loan.outstanding both decrease by the repaid amount. If the lender-credit step fails after the borrower has already been debited, the code compensates by crediting the cash and debt back onto the borrower rather than losing it. Loan status flips to "repaid" once outstanding <= 0; arrearsTurns resets to 0 on any repayment.

Interest servicing (turn-phase, bankingTurn.ts)#

serviceInterbankAndCbMargin runs every banking turn (idempotent via lastProcessedTurn !== turn), independent of whether any deposit-taking bank needs a servicing pass. It processes every InterbankLoan with status: "current".

Per loan (serviceOneInterbankLoan):

interestDue = outstanding x (ratePercent / 100) / TURNS_PER_YEAR   // TURNS_PER_YEAR = 48
payment = min(interestDue, borrower's available cashReserves)

Interest payments are logged via emitTx/payInterbankInterest moving cash on both bank charters' cashReserves.

Lender-side failure (bankSolvencyTurn.ts)#

If the LENDER on an interbank loan fails, writeOffLenderSideInterbankOnFailure marks every InterbankLoan where that corp is lenderCorporationId and status: "current" as "defaulted". The code is explicit that the borrower keeps the cash and the loss is absorbed as an unrecoverable write-off on the failed lender's estate; chasing the borrower for early repayment is treated as compounding one failure into a second. This function does NOT touch claims against the failed bank (i.e. where it is the borrower), those are settled separately, in priority order, by returnDepositBook during the resolution sweep.

CB margin line#

A separate facility: prop-book-collateralized borrowing directly from the country's central bank, available to the same investment/universal charters.

CB_MARGIN_SPREAD_PP = 1.5                 // provisional
cbMarginRatePercent(primeRate) = max(0, primeRate + 1.5)

CB_MARGIN_COLLATERAL_FRACTION = 0.5       // provisional
maxDebt = 0.5 x propBookMarkValue

drawCbMargin: collateral check is cbMarginDebt + cbMarginArrears + amount <= maxDebt, i.e. unpaid interest arrears count against the line's headroom too (the code notes this is deliberate, a bank that cannot service the margin loses headroom rather than silently borrowing its own arrears). On draw, cash is CREATED into bankCharter.cashReserves (+amount) and bankCharter.cbMarginDebt increases by the same amount; the country's centralBanks.netMoneyCreatedLifetime increases correspondingly. This mirrors the discount window: originating the loan does not debit any CB pool.

repayCbMargin: repay amount is min(amount, cbMarginDebt, cashReserves). Cash is DESTROYED from the bank's reserves (mirror of creation on draw) and netMoneyCreatedLifetime decreases by the same amount.

Both draw and repay emit bank_cb_margin_draw / bank_cb_margin_repay transactions with the counterparty recorded as "{country} central bank".

API surface#

Dead Bank Loans (deadBankLoans.ts)#

Handles loans a FAILED or REVOKED bank made as a LENDER that were never resolved when the charter died. The module's own comment states the prior behavior explicitly: the banking turn only services banks with an active charter, so once a bank's charter left that state its outstanding loan book simply stopped being serviced, borrowers stopped paying, the asset sat at full value on a dead charter forever, and any recovery reached nobody.

Which banks qualify#

findDeadBanksWithLoans selects every corporation with bankCharter.status in ["failed", "revoked"]. Each is marked resolved: true if either:

Where recovered cash goes (recoveryTargetFor)#

Servicing loop (processDeadBankLoans, called from bankingTurn.ts)#

For each dead bank, it queries bankLoans for that bankCorporationId where borrowerType is character or corporation, status in ["current", "arrears"], and lastProcessedTurn !== turn (idempotency guard). If there are matching loans, it resolves the target (estate or insurer) and then services each loan SERIALLY within that bank (not in parallel), because two loans from the same borrower must see each other's debit exactly as the live per-bank servicing path does. The actual per-loan collection math (interest, arrears, principal) is injected as a serviceLoan callback from bankingTurn.ts rather than owned by this module, specifically so this file does not depend on the turn file that depends on it.

Returned summary: { loansServiced, recoveredToEstate, recoveredToInsurer }, all zero if there are no dead banks with loans.

NPC Banks (npcBanks.ts)#

NPP-owned (non-player-party) retail banks seeded into eligible countries to give the private banking system counterparties and market depth without requiring player-run banks in every country.

Seeding (seedNpcBanks)#

NPC_BANKS_PER_COUNTRY = 2                        // provisional
NPC_BANK_CAPITAL_BUFFER_MULTIPLIER = 3            // provisional

For every country in ALL_COUNTRY_IDS:

Seeding itself is NOT gated on privateBankingEnabled (comment: "gameConfig is never mutated", the bypass is scoped to charter issuance only, not a flag flip). Runtime turn behavior is gated (see below).

Runtime rate policy (runNpcBankPolicy / processNpcBankPolicyTurn)#

Turn-phase entry point processNpcBankPolicyTurn (registered as phase npcBankPolicyTurn in ) no-ops with a zero summary ({ banksChecked: 0, banksUpdated: 0 }) if isPrivateBankingEnabled() is false. It does NOT seed banks; seeding is a separate admin/bootstrap-time operation (, ).

When enabled, runNpcBankPolicy iterates every corp with ceoType: "npp", active bankCharter, and charter type retail or universal. For each, it fetches the country's rate corridors (getRateCorridors) and computes the midpoint of the deposit and lending offset ranges. If the bank's current depositOffset/lendingOffset has drifted from that midpoint by more than 1e-9, it resets both to the midpoint via setBankRates. This is described as keeping NPC banks pinned to the corridor center; it does not set the reserve-holding or loan-book behavior, which the comment says comes from bankingTurn's NPC flows (not covered by this file).

Key Files#

Connected pages

← Referenced by
None