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
| Code | Account |
|---|---|
| 1000 | Cash and Cash Equivalents |
| 1010 | Operating Cash |
| 1020 | Customer Deposits |
| 1100 | Accounts Receivable |
| 1200 | Pending Settlements |
| 1300 | Loans Receivable |
Liabilities (2xxx) — normal credit balance
| Code | Account |
|---|---|
| 2000 | Customer Liabilities |
| 2010 | Customer Account Balances |
| 2020 | Pending Transfers Out |
| 2100 | Accounts Payable |
| 2200 | Accrued Interest Payable |
Equity (3xxx) — normal credit balance
| Code | Account |
|---|---|
| 3000 | Retained Earnings |
| 3100 | Capital |
Revenue (4xxx) — normal credit balance
| Code | Account |
|---|---|
| 4000 | Fee Income |
| 4010 | Transfer Fees |
| 4020 | Card Transaction Fees |
| 4030 | Account Maintenance Fees |
| 4100 | Interest Income |
| 4200 | Interchange Revenue |
Expenses (5xxx) — normal debit balance
| Code | Account |
|---|---|
| 5000 | Operating Expenses |
| 5010 | Processing Costs |
| 5020 | Network Fees |
| 5100 | Interest Expense |
| 5200 | Fraud 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:
| Domain | Entry source |
|---|---|
| Account create | None — accounts are records, not money. |
| Deposit / inbound transfer | Cash debit, customer-liability credit. |
| Outbound transfer | Customer-liability debit, pending-settlements credit; settled entry clears pending and credits cash. |
| Card authorisation | Hold entry (memo only — does not post until capture). |
| Card capture / settlement | Customer-liability debit, cash credit. |
| Loan origination | Cash credit, loans-receivable debit. |
| Loan payment | Cash debit, loans-receivable credit (principal portion); cash debit, interest-income credit (interest portion). |
| Interest accrual | Interest-income credit, accrued-interest-payable credit (or interest-expense debit). |
| Fees | Customer-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:
| Exception | Meaning |
|---|---|
| Unmatched on platform | Sponsor cleared a transfer the platform doesn't have a ledger entry for. Probably a stale submission or an unposted entry. |
| Unmatched at sponsor | Platform shows a posted entry the sponsor didn't clear. Probably a transfer that failed without proper ledger update. |
| Amount mismatch | Same 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
Run reconciliations
Resolve all open exceptions before closing.
- 2
Pay accrued interest
Run
POST /banking/money-market/admin/pay-interestandPOST /banking/investments/admin/accrue-cd-interestfor the period. - 3
Verify trial balance
Trial balance must net to zero — debits = credits. If not, fix before closing.
- 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
Lock the period
Future postings to closed periods should be blocked or routed through a "prior-period adjustment" code. The ledger service supports a
closedThroughdate 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.