Documentation

Ledger and reconciliation

Double-entry bookkeeping, journal entries, trial balance, and period close.

Every money movement in Banking posts a balanced journal entry. The ledger is the source of truth for financial state — account balances are derived from ledger entries, not stored as standalone numbers. Trial balance, balance sheet, and reconciliation services keep the books honest.

Double-entry basics

Every entry is balanced: total debits equal total credits. A simple example — a customer deposits $100:

Dr. Cash and Cash Equivalents (1000)         100.00
    Cr. Customer Account Balances (2010)         100.00

The platform's cash position increases (debit to an asset). The platform's liability to the customer increases by the same amount (credit to a liability). Sum-of-debits = sum-of-credits = $100. Books stay in balance.

A wire out of $50:

Dr. Customer Account Balances (2010)          50.00
    Cr. Pending Settlements (1200)                50.00

When the wire settles:

Dr. Pending Settlements (1200)                50.00
    Cr. Cash and Cash Equivalents (1000)          50.00

Cash drops; the temporary suspense account clears.

Chart of accounts

The platform bootstraps a standard chart at the org level. Every code is a known ledger account.

Assets (1xxx) — normal debit balance

CodeAccount
1000Cash and Cash Equivalents
1010Operating Cash
1020Customer Deposits
1100Accounts Receivable
1200Pending Settlements
1300Loans Receivable

Liabilities (2xxx) — normal credit balance

CodeAccount
2000Customer Liabilities
2010Customer Account Balances
2020Pending Transfers Out
2100Accounts Payable
2200Accrued Interest Payable

Equity (3xxx) — normal credit balance

CodeAccount
3000Retained Earnings
3100Capital

Revenue (4xxx) — normal credit balance

CodeAccount
4000Fee Income
4010Transfer Fees
4020Card Transaction Fees
4030Account Maintenance Fees
4100Interest Income
4200Interchange Revenue

Expenses (5xxx) — normal debit balance

CodeAccount
5000Operating Expenses
5010Processing Costs
5020Network Fees
5100Interest Expense
5200Fraud Losses

Per-holder accounts (one asset + one liability pair) are created by createHolderLedgerAccounts() at onboarding, scoped to the holder ID.

Journal entries

A journal entry has a header (date, description, source) and at least two line items. Line items reference specific ledger accounts and carry either a debit or a credit amount.

Shape

{
  entryDate: '2026-04-25',
  description: 'Wire out: order ORD-1234',
  source: 'transfer',
  sourceId: 'tr_abc',
  status: 'pending' | 'posted',
  lines: [
    { accountCode: '2010', accountId: 'liab_holder_abc', debit: 50.00, credit: 0 },
    { accountCode: '1200', accountId: 'asset_pending',    debit: 0,     credit: 50.00 },
  ]
}

Entries start pending while the underlying operation is in flight (e.g., a wire submitted but not yet settled). On settlement they're posted and become immutable. Reversals create a new entry with opposite signs — historical entries are never edited.

Where entries come from

Each domain posts entries automatically:

DomainEntry source
Account createNone — accounts are records, not money.
Deposit / inbound transferCash debit, customer-liability credit.
Outbound transferCustomer-liability debit, pending-settlements credit; settled entry clears pending and credits cash.
Card authorisationHold entry (memo only — does not post until capture).
Card capture / settlementCustomer-liability debit, cash credit.
Loan originationCash credit, loans-receivable debit.
Loan paymentCash debit, loans-receivable credit (principal portion); cash debit, interest-income credit (interest portion).
Interest accrualInterest-income credit, accrued-interest-payable credit (or interest-expense debit).
FeesCustomer-liability debit, fee-income credit.

Direct journal-entry creation is reserved for adjustments and exceptional cases. The domain services post automatically.

Reading the books

The ledger service exposes derived views:

Trial balance

getTrialBalance(orgId, asOfDate) — returns the balance of every account as of the date, organized by account type, with column totals for debits and credits. The two totals must match. If they don't, you have a posting bug; the trial balance is the canary.

Balance sheet

getBalanceSheet(orgId, asOfDate) — assets, liabilities, equity. Assets - Liabilities = Equity. Useful for reporting and audit.

Income statement

Aggregates revenue and expense accounts over a period — the platform's P&L. Net income flows into Retained Earnings on period close.

These services are not currently exposed as REST endpoints — call them from inside the platform via the LedgerService injection. To surface them externally, add a thin controller or expose via the discovery API.

Account balance correctness

A bank account's balance.current is computed at read time from the sum of posted ledger entries against that account's liability code (2010 family). available subtracts active fund holds. pending adds pending journal entries that haven't yet posted.

This means: account balances cannot drift from the ledger. They're a view, not a stored number. If an entry posts incorrectly, the account balance is wrong; fixing the entry fixes the balance.

Reconciliation

Reconciliation matches the platform's ledger against external sources of truth:

Sponsor bank reconciliation

The sponsor bank produces daily settlement reports listing every cleared ACH, wire, and card transaction. The ReconciliationService ingests these and matches each line against a corresponding ledger entry on AppEngine's books.

A match requires:

  • Same amount and currency.
  • Same date (within tolerance).
  • Same external reference (trace number, IMAD, payment ID).

Mismatches generate an exception report:

ExceptionMeaning
Unmatched on platformSponsor cleared a transfer the platform doesn't have a ledger entry for. Probably a stale submission or an unposted entry.
Unmatched at sponsorPlatform shows a posted entry the sponsor didn't clear. Probably a transfer that failed without proper ledger update.
Amount mismatchSame reference, different amounts. Almost always a fee discrepancy — the sponsor's fee differs from what the platform booked.

Run reconciliation daily. Investigate exceptions same-day where possible — old exceptions are harder to chase.

Card network reconciliation

Card networks (Visa/Mastercard) settle interchange and fees monthly. Match interchange revenue from the network's settlement file against the platform's Interchange Revenue (4200) postings. Differences typically reflect chargeback adjustments or rate-tier changes.

Cash position reconciliation

The platform's Cash and Cash Equivalents (1000) total should match the sponsor's reported balance for the operating account. Daily check; any drift is a red flag.

Period close

At month-end (and quarter, year):

  1. 1

    Run reconciliations

    Resolve all open exceptions before closing.

  2. 2

    Pay accrued interest

    Run POST /banking/money-market/admin/pay-interest and POST /banking/investments/admin/accrue-cd-interest for the period.

  3. 3

    Verify trial balance

    Trial balance must net to zero — debits = credits. If not, fix before closing.

  4. 4

    Snapshot

    Write a period snapshot record (balance sheet + income statement) for audit. The platform retains journal entries permanently; the snapshot is for fast reporting.

  5. 5

    Lock the period

    Future postings to closed periods should be blocked or routed through a "prior-period adjustment" code. The ledger service supports a closedThrough date you can check before posting.

What the ledger doesn't do

  • No GAAP / IFRS adjustments — accruals, deferrals, reclassifications are your accounting team's job; post them as adjustment entries.
  • No tax provisioning — federal/state income tax is computed and posted by your tax service.
  • No FX revaluation — multi-currency support is per-account; consolidated reporting requires you to run the conversion.

The platform gives you a clean, balanced, auditable transaction log. Higher-order accounting sits on top.