25 min

An interbank payment, leg by leg

When payer and payee bank at different banks, the deposit edits alone leave the banks out of square; they settle one tier up by moving reserves at the central bank.

Where you are. Seven lessons have assembled the machinery: money as ledger entries, postings whose legs must balance, the bank as a balance sheet, the two tiers built into the miniledger’s World, physical cash at the edge of the map, and lesson 7’s submit/settle API, which can refuse a payment and names the exact moment one becomes final. What you have not yet done is watch the whole machine perform its title event: the payment that crosses banks. This lesson runs the very payment you built by hand in module 0’s project through v1, reads every line the settlement changes, and squares v1’s count with module 0’s.

The same payment, run again

You have made this payment before. In module 0’s project you pushed 100 from alice to bob across two banks with your own hands: you wrote the reserve leg yourself, and the trace marked four lines changed on three ledgers. Now let the miniledger make it. The amounts are stylised - round numbers chosen so every diff reads at sight - and the setup is module 0’s world reborn under new names:

from miniledger import World, Bank

world = World()
for name in ("Alder", "Birch"):
    world.admit(Bank(name))
world.banks["Alder"].open_deposit("alice")
world.banks["Birch"].open_deposit("bob")
world.endow("Alder", "alice", 500)
world.endow("Birch", "bob", 200)

world.submit(100, "Alder", "alice", "Birch", "bob")
world.settle()

Snapshot every balance before the submit, diff after the settle, and count: six lines changed. Module 0 marked four. Same amount, same route, same two tiers, and the count grew by two. Nothing about the payment grew - total reserves are 700 before and 700 after - so the extra lines are not extra money. They are the subject of this lesson.

The idea in one paragraph

Inside one bank a payment is two edits on one ledger; across banks, the deposit edits alone leave the banks out of square - Alder has shed an IOU, Birch has absorbed one, and nothing has yet paid Birch for taking it on - so the payment settles one tier up, by moving reserves at the central bank. v1 performs that as three balanced postings: a payer leg (alice’s deposit down, Alder’s reserves down), a payee leg (Birch’s reserves up, bob’s deposit up) and a reserve leg at the central bank (Alder’s line down, Birch’s line up). Diff the world and six lines changed. Module 0 counted the same payment as four edits on three ledgers, and both counts are honest: the two extra lines are exactly the tier mirror each bank now carries - its own reserves line, recording on its own books the position the central bank also records on its. Four facts, six lines, no new money.

Three postings, one payment

Here is the cross-bank branch of World.settle, verbatim; every payment that crosses banks lands as these three postings:

payer_bank.post(
    [(p.payer, -p.amount), ("reserves", -p.amount)],
    memo=f"payment {p.id} payer leg",
)
payee_bank.post(
    [("reserves", p.amount), (p.payee, p.amount)],
    memo=f"payment {p.id} payee leg",
)
self.central_bank.post(
    [
        (self.central_bank.line(p.payer_bank), -p.amount),
        (self.central_bank.line(p.payee_bank), p.amount),
    ],
    memo=f"payment {p.id} reserve leg",
)

The payer leg is Alder’s whole experience of the payment: alice’s deposit down 100, reserves down 100. Read it with lesson 4’s lens and it balances on its own: the deposit is a liability, the reserves an asset, so dAssets (-100) equals dLiabilities (-100) plus dEquity (0) - in words, Alder owes a customer 100 less and holds 100 less central-bank money, and its sheet shrinks without tipping.

The payee leg is the same shape with the signs flipped: Birch’s reserves up, bob’s deposit up. Birch’s sheet grows by 100 on both sides; it absorbed a new IOU and was paid in reserves for absorbing it.

The reserve leg is the squaring itself, on the one ledger where both banks hold accounts: Alder’s line down 100, Birch’s line up 100. Two liabilities of the central bank, one falling and one rising by the same amount, so the tier 1 total cannot move. This is the settlement you first met in module 0 lesson 5, now one balanced posting among three - and all three land inside settle, at the finality point lesson 7 named, with the world’s invariants re-checked after the batch.

Six lines, four facts

Now diff every balance and number the changed lines in the order the money travels:

  1. alice’s deposit at Alder: 500 -> 400
  2. Alder’s own reserves line: 500 -> 400
  3. the central bank’s Alder reserves line: 500 -> 400
  4. the central bank’s Birch reserves line: 200 -> 300
  5. Birch’s own reserves line: 200 -> 300
  6. bob’s deposit at Birch: 200 -> 300

The numbering follows the money, not the code: the payer leg wrote lines 1 and 2, the reserve leg wrote 3 and 4, the payee leg wrote 5 and 6.

tier 1 tier 2 Central bank ledger 3 Alder reserves 500 -> 400 4 Birch reserves 200 -> 300 one reserve position, written on two books Alder ledger 2 reserves 500 -> 400 1 alice 500 -> 400 Birch ledger 5 reserves 200 -> 300 6 bob 200 -> 300
The six changed lines of the stylised 100 payment, numbered 1 to 6 across three ledger boxes: the two deposit legs in gold at the outer edges, and the reserve legs in green, each bank's own reserves line joined by a dashed mirror line to its matching line on the central bank's ledger

Wider than the screen; scroll it sideways.

Module 0’s four edits are lines 1, 3, 4 and 6: the two deposits and the two lines on the central bank’s ledger. Lines 2 and 5 are the ones module 0 never had, and here is the reconciliation, stated once and precisely: module 0 counted four edits on three ledgers; v1 prints six changed lines for the same payment; and the difference is exactly the tier mirror each bank now carries - its own record of its reserve balance, which module 0’s toy kept in one place only, on the central bank’s ledger.

The mirror is not decoration; double entry forces it. Alice’s deposit falling 100 must pair with a leg on Alder’s own books - lesson 3 allows no lonely edits - and the pairing line is Alder’s reserves asset. Module 0’s toy had no per-bank books to balance, which is exactly why its missing-reserve-leg bug could exit 0. v1 closes that hole twice over: each bank’s sheet must balance after every posting, and the two tellings of each reserve position must agree. That second check is assert_world, verbatim:

mirror = self.central_bank.balance(self.central_bank.line(bank.name))
if mirror != bank.balance("reserves"):
    raise LedgerError(
        f"tier mismatch: {bank.name} carries reserves "
        f"{bank.balance('reserves')} but the central bank's line says {mirror}"
    )

The tracer animates this world: choose payer, payee and amount, and watch which lines change. What it shows for the stylised 100 payment: routed across banks, from alice at Alder to bob at Birch, six lines change - the six numbered above. Routed inside one bank, between two customers of the same ledger, exactly two lines change, both deposits, and every reserve line in the system sits still. Total reserves are unchanged in both cases: routing decides how many lines a payment touches; it never changes how much central-bank money exists.

The line that is not there

One more read of the diff, this time for what is missing. Module 0 lesson 1 put five parties behind a single card tap, and one of them - the network - carried every message in the story. Messages flowed here too: submit is an instruction arriving, and in the real system that instruction crosses a wire between the banks. Now scan the six lines for the carrier.

Check yourself

1. v1 prints six changed lines where module 0’s trace marked four. Which two lines are new, and how much new money do they represent?

The banks’ own reserves lines: Alder’s and Birch’s, on their own books. They represent zero new money, because each mirrors a line the central bank also changed: the same reserve position, written on two books. Deposits moved once, reserves moved once, and assert_world raises tier mismatch the moment either bank’s telling drifts from the central bank’s.

2. The payer leg posts alice -100 and reserves -100: both fall. What keeps Alder’s balance sheet square through that?

The two lines sit on opposite sides of the sheet. Alice’s deposit is a liability, reserves are an asset, so both sides shrink by 100 together: dAssets equals dLiabilities plus dEquity, -100 = -100 + 0. The sheet got smaller, not lopsided - and post checked exactly that before applying the legs.

3. Re-route the same 100 between two customers of one bank. Which of the six lines still change, and why do the other four sleep?

Only the two deposit lines, both on that one bank’s ledger. The bank’s total IOUs are unchanged - it owes one customer less and another more - so there is nothing to square with any other bank, no reserve movement, and therefore no work for the bank’s own reserves line or for any line at the central bank. Reserves settle between banks, and a same-bank payment never leaves the bank.

4. The instruction “alice pays bob 100” crossed a network between the banks. Why does no line in the diff belong to the carrier?

Because a ledger line is a liability - somebody’s IOU - and the carrier neither owes nor is owed any of this money. The six lines are two bank IOUs to customers and four tellings of central-bank IOUs to banks. The message asked for the payment; only ledger edits performed it. The carrier is a party to the payment and a stranger to the money, which is module 0 lesson 1’s point, now visible as an absence.

Do this

Fifteen minutes, from module-01-money-at-rest. Open code/legs_of_a_payment.py: the harness is the hook’s world verbatim - Alder and Birch admitted, alice endowed 500, bob 200 - and it ends by calling narrate(world, 100), which is yours to write. Work the TODO(you): snapshot every balance on all three ledgers, submit alice to bob for amount, settle, then diff the snapshots into one printable row per changed line - ledger, account, signed change, in any order you like; what matters is six rows and correct signs. The two asserts are the spec: exactly six rows, and total reserves still 700.

python code/legs_of_a_payment.py

Done right, it prints your six rows - the ledger narrating its own payment - and ends with the line

the same four edits, seen as six lines once each bank mirrors its reserves

If the six-row assert fires at four, you almost certainly diffed the two banks and forgot the central bank: the rows you kept include both mirrors, and the originals at tier 1 are missing. Add world.central_bank to the ledgers you snapshot and run again. The completed version is solutions/legs_of_a_payment.py; compare after you are green, not before.

What you can now do. You can narrate an interbank payment line by line: name the three balanced postings it settles as, and say for each of the six changed lines which ledger it lives on, which side of the sheet it sits on, and which leg it belongs to. You can hold both honest counts of the same event at once - module 0’s four edits on three ledgers, and v1’s six changed lines once each bank carries its own mirror of its reserve position - and say exactly what the difference is and why it is not money. Everything here moved money that already existed; alice’s 500 was endowed at setup. Lesson 9 asks where deposits come from when nobody endows them, and the answer - the bank’s loan book creates them - is where the module goes next.

What you can now do

You can narrate every changed line of an interbank payment and reconcile module 0's count with v1's.