Part 19 · 18 chapters · ~20 min

Round seventeen: “fees, the company books, and who pays for what”

Seventeen rounds of moving customer money, and almost nothing about the bank's own. Yet every transaction generates revenue we earn and costs we incur, the two are settled on different schedules with different counterparties, and the difference between them is the entire business. This round builds the revenue side and the company books, and shows why keeping customer funds and company funds separable is a design property rather than an accounting convention.

215

The pressure: revenue is not a number in a spreadsheet

interviewer

“Your CFO asks what a transfer actually earns us, net of everything we pay for it. Then the auditor asks you to prove that no customer money is funding our operations. Where do both answers come from?”

Both from the same place, which is the point of this round. If fees are booked properly, unit economics and safeguarding are the same query shaped differently.

worked numbers
          one ₦50,000 transfer over NIP:


            fee charged to the customer              +₦10.00

            NIBSS switching fee we pay             −₦3.50

            stamp duty collected for the state     ₦0.00 net, we are an agent

            VAT on our fee, collected             ₦0.00 net, we are an agent

            SMS alert we send                      −₦4.00

            infrastructure, amortised               −₦0.30


            contribution                              ₦2.20


the SMS costs more than the switching fee, which is the kind of

fact that only exists if every cost is posted to the ledger.
functional, new
  1. Every fee we charge is priced, posted and attributable to a transaction.
  2. Every cost we incur is posted, including ones billed to us monthly in arrears.
  3. Amounts we collect on behalf of others never touch our revenue.
  4. Customer funds and company funds are separable by query, at any instant.
  5. Profit per transaction, per product and per channel is derived, not tracked.
non-functional, new
  1. Fee computation adds under 2 ms to the posting path.
  2. The safeguarding check runs continuously with a hard pass or fail.
  3. Every reported revenue figure is reproducible months later, per Part 11.
  4. A pricing change is configuration, effective-dated, never a release.
216

A taxonomy of fees in a bank

Four categories, distinguished by who ultimately keeps the money. That question, rather than who pays it, is what determines the accounting.

CategoryExampleWho keeps itOur books
RevenueTransfer fee, card maintenance, FX spread, interchange receivedUsIncome. Hits the P&L
CostNIBSS switching, scheme fees, SMS, PSP chargesA supplierExpense. Hits the P&L
Pass-throughStamp duty, VAT, levies collected for a governmentA third partyLiability. Never revenue, never expense
ContraWaivers, promotional discounts, goodwill creditsNobody; forgone revenueReduces income. Booked separately, not hidden
the category people get wrong
Pass-through is not revenue, however it flows through your accounts. If you collect ₦50 of stamp duty and book it as income, your revenue is overstated, your tax position is wrong, and you owe the state ₦50 you have recorded as yours. The test is not "did money arrive"; it is "do we get to keep it". Collecting on behalf of someone creates a liability the moment it lands.
the four categories on one ₦50,000 transfer · one journal, all of it
wallet A−5006250amount + fee + VAT + duty
settle_suspense:nibss+5000000the transfer itself
fee_income:transfer:41+1000revenue, ours
vat_payable+75pass-through, owed to the revenue service
stamp_duty_payable+5000pass-through, owed to the state
nibss_fee_expense−350cost, ours
nibss_payable+350owed to NIBSS at settlement
Σ0seven entries, one journal, every category distinguishable
217

Fees we charge: pricing, waivers, and tiers

Pricing is data, effective-dated, and resolved exactly like the limits in Part 18. The same pattern, for the same reason: it changes on someone else's timetable.

code
CREATE TABLE fee_schedules (
  id             UUID PRIMARY KEY,
  fee_code       TEXT NOT NULL,      -- transfer.nip | card.maintenance
  -- what this schedule applies to. null = any.
  account_type   TEXT,
  kyc_tier       INT,
  channel        TEXT,                -- app | ussd | pos | api
  currency       CHAR(3) NOT NULL,
  -- the band this row prices. bands must tile the range with no gaps.
  min_amount_minor BIGINT NOT NULL DEFAULT 0,
  max_amount_minor BIGINT,
  -- flat + percentage + cap is the general form. most rows use one.
  flat_minor     BIGINT NOT NULL DEFAULT 0,
  rate_bps       INT NOT NULL DEFAULT 0,
  cap_minor      BIGINT,
  -- the regulatory instrument that constrains this price, if any
  regulatory_ref TEXT,
  effective_from TIMESTAMPTZ NOT NULL,
  effective_to   TIMESTAMPTZ
);
worked numbers
          the CBN transfer fee bands, as an example of why bands exist:


            ≤ ₦5,000                  → ₦10

            ₦5,001 to ₦50,000      → ₦25

            > ₦50,000                → ₦50


these are regulatory MAXIMA, not suggestions.

so the pricing engine must be able to prove it never charged

above the cap in force on that date, which is why regulatory_ref

and effective dating are on the row.
waivers, and why they are booked rather than skipped
  1. A waived fee is not the absence of a fee. It is a fee charged and a contra entry that offsets it.
  2. Booking it that way makes forgone revenue visible, so the business can see what promotions actually cost.
  3. Simply not charging makes the promotion free in the accounts and invisible in analysis, which is how a campaign runs for a year without anyone knowing its cost.
  4. It also keeps the customer's statement honest: they can see the fee and the waiver.
a waived transfer fee under a promotion · four entries, not zero
wallet A−1000fee charged as normal
fee_income:transfer:41+1000revenue recognised
fee_waiver_contra−1000forgone revenue, visible
wallet A+1000customer refunded, net effect zero for them
Σ0the promotion's cost is now a queryable number
218

Fees we pay: interchange, scheme, provider, rail

The costs side, which is usually worse instrumented than the revenue side and therefore where margin quietly disappears.

CostBilledKnown at transaction time?Posting approach
NIBSS switchingPer transaction, settled dailyYes, it is a published tariffPost at transaction time
Card scheme feesMonthly, itemisedApproximatelyAccrue an estimate, true up on the invoice
PSP collection feeNetted at settlementYesPost at transaction time
SMS and notificationMonthly, per messageYes, per messagePost per message, per Part 12's cost attribution
Interchange paidNetted at card settlementYes, by scheme tablePost at clearing
Cloud and infrastructureMonthlyNoAccrue monthly, allocate by driver for unit economics
the accrue-and-true-up pattern, for costs known only later
  1. At transaction time, post an estimated cost to expense and to an accrual liability.
  2. When the invoice arrives, post the difference between the accrual and the actual.
  3. Settle the invoice against the liability.
  4. Monitor the true-up variance: a consistently wrong estimate means the model needs fixing, and a sudden change means the supplier changed their pricing.
month-end true-up: accrued ₦4.1m of scheme fees, invoice is ₦4.35m · journal kind: cost.trueup
scheme_fee_expense−25000000we under-accrued by ₦250,000
scheme_fee_accrual+25000000liability topped up to the invoiced amount
Σ0a 6% variance, which is a metric worth watching
the operational value of accruing
Without accruals, a month's P&L is distorted by when invoices happen to arrive: a quiet month looks profitable and the next looks terrible. Accruing per transaction also means unit economics are available immediately rather than six weeks later, which is the difference between pricing decisions informed by data and pricing decisions informed by hope.
219

Fee posting: when, and against which account

three timing options, and when each is right
  1. Inline, same journal as the transaction. The default. The fee and the movement that caused it are atomically linked and reconcile together.
  2. Deferred, same day. For fees whose amount depends on something not yet known, such as a card fee that depends on the final cleared amount.
  3. Periodic, batch. Account maintenance, monthly card fees, minimum balance charges. The Part 5 accrual batch, with a date-scoped idempotency key.
why inline is strongly preferred
A fee posted in the same journal as its transaction is impossible to orphan. Post it separately and you have two journals that must be linked by convention, a reconciliation surface where fees can be charged for reversed transactions, and a support question nobody can answer. If the fee is caused by the transaction, put it in the transaction's journal.

Which account bears the fee

ScenarioCharged toNote
Standard transferThe senderGross model from Part 5: recipient gets the full amount
Merchant collectionThe merchantNetted from settlement, so the payer sees the round number
Card purchaseNobody visibleWe receive interchange. The cost is on the acquiring side
Failed transactionNobodyCharging for a failure is a complaint and often a regulatory issue
Reversed transactionDepends on policyMust be explicit. If the reversal is our fault, refund the fee
Corporate bulk fileThe corporate, per line or per fileA contractual term, and both models exist
code
// fee reversal follows the transaction reversal, by policy, and it is
// ALWAYS a new journal linked to the original. never an edit.
async function reverseWithFee(original: Journal, reason: Reason) {
  const refundFee = policy.refundFeeOn(reason);
  // our fault → refund. customer changed their mind → usually not.

  await ledger.post({
    idempotencyKey: `rev-${original.id}`,
    kind: JournalKind.TRANSFER_REVERSAL,
    reversesJournalId: original.id,
    entries: [
      ...reverseEntriesOf(original, { includeFee: refundFee })
    ]
  });
}
220

Recognition: earned now, deferred, or amortised

When money arrives and when it becomes revenue are different questions, and conflating them misstates the P&L.

FeeCash arrivesRevenue recognisedWhy
Transfer feeNowNowThe service is complete at the moment of transfer
Card annual feeNowOver 12 monthsWe owe 12 months of service, so 11 months is a liability
Loan origination feeAt disbursementOver the loan termUnder the effective interest method in most standards
Account maintenanceMonthlyMonthlyCharged for the period just served
InterchangeAt settlementAt transactionEarned when the transaction occurs, received later
card annual fee of ₦6,000 collected up front · journal kind: fee.deferred
wallet A−600000customer pays the full year now
deferred_income:card_annual+600000a liability: we owe 12 months of service
Σ0note that no revenue has been recognised yet
monthly release, one twelfth · journal kind: fee.recognition · run by the Part 5 batch
deferred_income:card_annual−50000liability reduces
fee_income:card_annual+50000revenue recognised for the month served
Σ0keyed cardfee-recog-{cardId}-{yearMonth}, so a re-run is a no-op
why an engineer should care about this
It looks like an accounting nicety and it is a system requirement: deferred income needs a schedule, a monthly batch and a balance that must unwind to exactly zero by the end of the period. If the batch is not idempotent, or the rounding does not sum to the original, the liability never clears and someone finds a stubborn ₦3 balance in an account two years later. The Part 5 splitExact function exists for precisely this.
221

Pass-through versus absorbed cost

worked numbers
pass-through  we collect it and owe it onward

            customer pays ₦50 duty → stamp_duty_payable +₦50

            we remit ₦50 to the state → stamp_duty_payable −₦50

            net effect on revenue: zero, correctly


absorbed     we pay it and do not recover it

            we pay NIBSS ₦3.50 → nibss_fee_expense −₦3.50

            net effect on revenue: a real cost


recovered    we pay it and charge it on

            expense −₦3.50 and income +₦10.00

            net: +₦6.50 margin, both sides visible
the modelling rule
Never net a cost against revenue in a single entry. Charging ₦10 and paying ₦3.50 is two entries, not one entry of ₦6.50. The netted version destroys the ability to answer "what is our gross revenue" and "what do we pay NIBSS", both of which finance and the regulator will ask separately. Gross both sides, always, and let the query do the netting.
the test to apply to any charge
  1. Do we keep it? Yes → income. No → liability.
  2. Did we choose to incur it? Yes → expense. It was imposed on the customer → pass-through.
  3. Is there a statutory remittance deadline? Then it is definitely a liability, and a late one carries penalties.
  4. Would the regulator consider it ours? The safest test, and the one that settles arguments.
one fee, four destinations
who actually keeps each naira
swipe the figure sideways, or tap expand for full screen
1/8
one charge
A customer is charged 62 naira 50 on top of their transfer. One number on their statement, four different things underneath.
222

The company chart of accounts, properly

Part 5 introduced GL classes. Here is what the bank's own accounts actually look like, and the structure matters because reporting is generated from it.

1000ASSETSdebit increases
1100Cash at central bankRTGS account
1200Nostro accountsper correspondent
1300Loans receivableper loan
1310Interest receivableaccrued, unpaid
1400PSP and acquirer receivablesper counterparty
1500FX positionper currency
2000LIABILITIEScredit increases
2100Customer depositsevery wallet
2110Interest payableaccrued to customers
2200Stamp duty payablepass-through
2210VAT payablepass-through
2300Deferred incomeunearned fees
2400Supplier accrualsscheme, SMS, cloud
2900Suspense accountsall of them
3000EQUITYcredit increases
3100Share capital
3200Retained earnings
4000INCOMEcredit increases
4100Fee incomeby product, sharded
4200Interest incomeloans, overdraft
4300FX spread incomeper corridor
4400Interchange incomecard issuing
4900Fee waivers (contra)reduces income
5000EXPENSESdebit increases
5100Interest expensepaid to savers
5200Rail and switching feesNIBSS, SEPA, SWIFT
5300Scheme and interchange paidcards
5400Notification costsSMS, push, email
5500Loan loss provisionsexpected credit loss
5600Fraud and dispute losseswritten off
the two lines an engineer should remember
2100 customer deposits is a liability, so every customer wallet balance is money the bank owes. And 2900 suspense accounts are liabilities too, because every naira sitting in one belongs to somebody. That second point makes the Part 10 ageing report a balance sheet concern rather than an engineering curiosity, which is usually what gets it prioritised.
223

Customer funds versus company funds

The distinction the auditor cares about most, and it must be answerable by query rather than by explanation.

worked numbers
customer funds  money we hold that belongs to someone else

            = Σ customer deposits (2100)

            + Σ suspense balances (2900)

            + Σ pass-through payables (2200, 2210)


company funds  money that is ours

            = assets − liabilities − customer funds

            = equity + retained earnings


the safeguarding assertion:

            liquid assets held ≥ customer funds owed, at all times


if this ever fails, the bank is using customer money to

fund itself, which is the failure regulators exist to prevent.
code
-- the single most important query in the company books. it is the
-- Part 10 machinery pointed at solvency rather than at drift.
WITH owed AS (
  SELECT e.currency, SUM(e.amount) AS customer_funds
    FROM entries e JOIN accounts a ON a.id = e.account_id
   WHERE a.gl_code LIKE '21%'     -- customer deposits
      OR a.gl_code LIKE '29%'     -- suspense: somebody's money
      OR a.gl_code LIKE '22%'     -- pass-through payables
   GROUP BY e.currency
), held AS (
  SELECT e.currency, -SUM(e.amount) AS liquid_assets
    FROM entries e JOIN accounts a ON a.id = e.account_id
   WHERE a.gl_code LIKE '11%' OR a.gl_code LIKE '12%'
   GROUP BY e.currency
)
SELECT o.currency, h.liquid_assets, o.customer_funds,
       h.liquid_assets - o.customer_funds AS buffer
  FROM owed o JOIN held h USING (currency)
 WHERE h.liquid_assets < o.customer_funds;   -- must return ZERO rows
why this must be a continuous check rather than a monthly one
A safeguarding shortfall is not a reporting problem, it is an event. If it appears at 14:00 because a treasury transfer went to the wrong account, finding out at month end means three weeks of breach. This belongs on the Part 13 correctness dashboard next to the zero-sum invariant, with the same severity, because they are the same class of assertion: something that must never be false.
224

Safeguarding, and proving segregation daily

what segregation requires in practice
  1. Customer funds sit in a designated account at a separate institution, legally identified as client money.
  2. That account is not used for operational expenses, payroll, or anything of ours.
  3. The balance is reconciled daily against our sum of customer obligations.
  4. A shortfall must be funded immediately from company money, and reported.
  5. An excess is also a problem, because company money sitting in a client account is commingling in the other direction.
worked numbers
          the daily assertion, both directions:


            segregated account balance   ₦4,821,340,000

            sum of customer obligations ₦4,821,336,500

                                              ──────────

            buffer                                ₦3,500


          buffer < 0   → SHORTFALL. fund it today. report it.

          buffer large → commingling. sweep the excess out.

a small deliberate buffer is normal and is itself policy.
the reconciliation that is genuinely two-sided
Most reconciliation in Part 10 compares our record of a counterparty against their record of the same thing. Safeguarding compares our record of what we owe customers against a bank's record of what we hold, which are different quantities that must agree for a reason rather than by construction. It is the one reconciliation where both sides are meaningful on their own, and that is why it is the regulator's favourite.
225

Reconciling user funds: the two-sided check

the layers of user-funds reconciliation, innermost first
  1. Internal consistency. Every journal sums to zero per currency. Part 10, continuous.
  2. Sum of wallets. Σ customer deposit accounts equals the customer-funds figure used everywhere else. Catches classification errors.
  3. Wallets versus the bank. Customer obligations versus the segregated account. The safeguarding check.
  4. Wallets versus rails. What we believe settled versus what NIBSS, the schemes and the PSPs say. Part 10's external matching.
  5. Wallets versus statements shown. What the customer sees equals what the ledger says. Catches serving-layer drift.
layer five, which almost nobody builds
The balance the customer sees comes from Bigtable and the cache, not from the ledger. If the serving layer drifts, the customer sees a wrong number even though the ledger is perfect, and they will call support rather than an auditor. Sampling a few thousand accounts nightly and comparing the served balance to the computed one is cheap and catches an entire class of incident that no other check sees.
code
-- layer 5: sample and compare. cheap, and it catches cache and
-- projection bugs that every other reconciliation is blind to.
async function servingDriftCheck(sampleSize = 5000) {
  const accounts = await sampleRandomAccounts(sampleSize);
  const drifts = [];

  for (const a of accounts) {
    const served   = await bigtable.currentBalance(a.id);     // what they see
    const computed = await ledger.balanceFromEntries(a.id); // the truth
    if (served !== computed) drifts.push({ a, served, computed });
  }

  // any drift at all is a bug. it is not a tolerance.
  if (drifts.length) await alerts.page('serving.balance_drift', { drifts });
}
226

Reconciling company accounts and internal transfers

Company accounts are reconciled differently from customer accounts, because their counterparty is usually an invoice or a statement rather than a rail.

Company accountReconciled againstFrequency
Fee incomeTransaction count × applicable tariff, recomputed independentlyDaily
Rail fee expenseThe rail's own billing fileDaily or monthly
Scheme fee accrualThe monthly invoiceMonthly true-up
Interchange incomeScheme settlement reportsDaily
Stamp duty payableRemittance advice from the stateOn remittance
Nostro accountsCorrespondent bank statementsDaily
FX positionTreasury's own position recordsIntraday
the independent recomputation, which is the strongest check available
For fee income, do not merely sum the fee entries. Independently recompute what the fees should have been from the transaction log and the tariff table, then compare. Summing the entries proves the ledger is self-consistent; recomputing proves the pricing engine charged correctly, which is a different and more valuable assertion. A pricing bug that undercharges every transaction by ₦0.50 is invisible to every other check in this module.
internal transfers, which need their own discipline
  1. Moving money between two company accounts is still a journal with balanced entries, never an adjustment.
  2. It requires four-eyes, because it is the classic path for internal misappropriation.
  3. It must carry a reason code and a reference, because "internal transfer" with no narrative is exactly what an auditor flags.
  4. Transfers between customer-funds accounts and company-funds accounts are the highest-risk operation in the bank and should require finance approval, not engineering.
227

Unit economics: profit per transaction, derived

The payoff for posting every revenue and cost line. Because they all carry the journal id, unit economics is a join rather than a model.

code
-- contribution per transaction: every income and expense entry that
-- shares a journal_id with the transaction, summed.
SELECT j.kind, j.metadata->>'channel' AS channel,
       COUNT(DISTINCT j.id)                          AS txn_count,
       SUM(CASE WHEN a.gl_code LIKE '4%' THEN e.amount ELSE 0 END) AS income,
       SUM(CASE WHEN a.gl_code LIKE '5%' THEN e.amount ELSE 0 END) AS expense,
       SUM(CASE WHEN a.gl_code LIKE '4%' OR a.gl_code LIKE '5%'
                THEN e.amount ELSE 0 END)             AS contribution
  FROM journal j
  JOIN entries e ON e.journal_id = j.id
  JOIN accounts a ON a.id = e.account_id
 WHERE j.created_at >= $from AND j.created_at < $to
 GROUP BY 1, 2
 ORDER BY contribution;   -- the loss-makers first. always look here.
ProductIncomeDirect costContributionWhat it says
Transfer, app₦25.00₦7.80₦17.20The core earner
Transfer, USSD₦25.00₦24.10₦0.90Session charges nearly erase it
Card purchase₦120.00₦34.00₦86.00Why banks push cards
Bill payment₦0.00₦11.20−₦11.20A loss leader. Is that deliberate?
Balance enquiry, USSD₦6.98₦6.98₦0.00Exactly break-even by regulation
the two rows that justify the whole round
USSD transfers earning ₦0.90 and bill payments losing ₦11.20 are the kind of facts that change product strategy, and they are invisible unless every cost is posted against the journal that caused it. A bank that tracks revenue in the ledger and costs in a spreadsheet knows its revenue and guesses its margin.
228

VAT, withholding, and stamp duty on the ledger

TaxBasisOur roleLedger treatment
VAT on feesA percentage of our feeCollecting agentLiability on collection, cleared on remittance
Stamp dutyFlat, on qualifying credits above a thresholdCollecting agentLiability, remitted per schedule
Withholding taxOn interest paid to customersWithholding agentDeducted from the customer's interest, held as a liability
Company income taxOn our profitTaxpayerAn expense and a liability, computed periodically
savings interest of ₦1,000 with 10% withholding · the customer receives ₦900
interest_expense−100000our full cost is still ₦1,000
wallet A+90000the customer receives the net
withholding_tax_payable+10000we owe this to the revenue service
Σ0the customer's tax certificate is generated from this entry
three rules that keep tax handling defensible
  1. A separate liability account per tax type. Netting them makes remittance reconciliation impossible and each has its own deadline.
  2. Never recognise collected tax as income, even briefly. The moment it lands it is a liability.
  3. Store the rate that was applied, not just the amount, because rates change and a 2024 transaction must be explicable under the 2024 rate. Same reasoning as Part 7's stored FX rates.
the customer-facing consequence
Customers need tax certificates, showing interest earned and tax withheld per period. If withholding is modelled properly as its own entry against its own account, that certificate is a query. If it was netted into the interest posting, somebody will reconstruct it from a spreadsheet every January, and the numbers will not quite tie.
229

Month-end: accruals, provisions, and the P&L

the month-end sequence, extending Part 10's daily close
  1. Cut off at a declared timestamp, and freeze it.
  2. Complete accruals: costs incurred but not invoiced, income earned but not received.
  3. Release deferred income for the month, per chapter 220.
  4. Compute provisions: expected credit loss on loans, and a provision for open disputes.
  5. Revalue FX positions at month-end rates, posting unrealised gain or loss.
  6. Run the trial balance and confirm it proves out per currency.
  7. Generate the P&L and balance sheet from the GL classes.
  8. Finance reviews and signs, or the close is held open with named exceptions.
  9. Freeze the reported figures, per Part 10 chapter 120.
worked numbers
          the P&L, entirely derived from GL classes:


            income (4000)                          ₦1,240,000,000

            less waivers (4900, contra)          −₦38,000,000

                                                       ──────────────

            net income                         ₦1,202,000,000

            expenses (5000)                      −₦810,000,000

                                                       ──────────────

            profit before tax                   ₦392,000,000


no profit table. no separate reporting database. one query over

the same entries that moved the money, which is the dividend from

round one that has now paid out for the last time.
provisions, which are the one genuinely estimated number
Everything else in the P&L is derived from facts. Loan loss provisions are a model output: expected credit loss over a horizon, based on staging and probability of default. That makes them the number auditors scrutinise most, and the one where the engineering requirement is reproducibility: the same inputs and the same model version must produce the same provision, which is Part 11's four-way pinning applied to a number rather than a report.
230

Fee disputes, refunds, and goodwill credits

CaseTreatmentBooked to
Fee charged in errorReverse it fullyContra against the original income account
Fee correct, customer unhappyGoodwill credit, not a reversalA separate goodwill expense account
Duplicate chargeReverse the duplicateContra, and investigate the cause
Service failed, fee chargedRefund automaticallyContra, and it should never have happened
Regulatory refund orderRefund, possibly with interestContra plus a penalty expense
why goodwill must not be booked as a fee reversal
A goodwill credit is a cost of retaining a customer, not evidence that the fee was wrong. Booking it as a reversal understates fee income and, worse, makes the fee-accuracy reconciliation from chapter 226 look like it is failing. Keep them in separate accounts, because one measures a pricing defect and the other measures a service recovery, and conflating them hides both.
what the refund path must guarantee
  1. Idempotent, keyed on the original journal, so a support agent clicking twice refunds once.
  2. Bounded: a refund may never exceed what was charged, enforced server-side.
  3. Attributed: who approved it and under which reason code, per Part 18's four-eyes rules.
  4. Rate-limited per agent, because refunds are a classic insider-abuse vector and the Part 9 monitoring should see the pattern.
231

What finance actually asks for, and why

The askWhat they need underneathWhere it comes from
"Daily revenue by product"Income accounts, grouped by journal kindGold table, Part 11
"Why was yesterday down 12%?"Drill from the aggregate to individual journalsLineage, Part 15 chapter 188
"Prove the trial balance"Per currency, per GL class, summing to zeroPart 10 chapter 113
"What do we owe NIBSS today?"The payable account balance, liveA ledger balance, Part 6
"Are client funds segregated?"The safeguarding assertion, continuouslyChapter 224
"Cost per transaction by channel"Expense entries joined on journal metadataChapter 227
"Re-run last March's return"Byte-identical outputFour-way pinning, Part 11 chapter 131
what makes an engineering team good to work with, from finance's side
  1. Numbers that do not change when the same report is re-run.
  2. Drill-down that reaches individual transactions, because an aggregate nobody can explain is useless in an audit.
  3. Advance warning of changes that will move a number, such as a new fee or a reclassification.
  4. A named owner for every GL account, so a question has an addressee.
  5. Self-service, so routine questions do not require an engineer.
the sentence worth saying in an interview
"Finance is a first-class consumer of the ledger, not a downstream reporting problem. If the chart of accounts is right and every cost is posted against the journal that caused it, the P&L, unit economics and the safeguarding assertion are all the same query shaped differently. If it is wrong, every one of those becomes a spreadsheet somebody maintains by hand, and they will disagree with each other."
232

Sketch v17: the revenue and books layer

What changed, and the cost accepted

ChangeDriven byCost accepted
Pricing as effective-dated dataRegulatory caps change without noticeAnother policy store, and band coverage must be validated
Fees inline in the transaction journalA fee posted separately can be orphanedThe posting call carries more entries
Waivers booked as contraForgone revenue must be visibleFour entries instead of none for a waived fee
Costs posted per transactionUnit economics is otherwise guessworkAccrual and true-up machinery for monthly-billed costs
Pass-through as liabilitiesCollected tax is not revenueA liability account and a remittance cycle per tax type
Deferred income with a release scheduleCash timing is not revenue timingA monthly batch that must unwind to exactly zero
Continuous safeguarding assertionA shortfall is an event, not a month-end findingIt joins the correctness dashboard at the same severity
Independent fee recomputationSumming entries cannot detect a pricing bugA second implementation of pricing, deliberately
how to close round seventeen
"v17 adds the bank's own money to a ledger that previously only knew the customers'. Three things I would defend: fees post inline in the transaction's own journal, so a fee can never be orphaned from what caused it; pass-through amounts are liabilities rather than income, because the test is not whether money arrived but whether we keep it; and the safeguarding assertion is continuous, because customer money funding operations is an event rather than a reporting line. And the payoff is that the P&L, unit economics and segregation are the same query shaped three ways, which is the last dividend from choosing double-entry in round one."
architecture v17
pricing in, books out, one ledger
swipe the figure sideways, or tap expand for full screen
1/8
a transaction
A transaction is about to post. Before it does, two things need to be priced.