35 min

Project: miniledger v1

Assemble everything built so far into miniledger v1, a small importable double-entry two-tier ledger whose invariants hold while an interbank payment settles in reserves.

Where you are. Lesson 1 showed you miniledger v1 finished - the whole library, end-state first - and promised the module would teach you to build it. It has. Lessons 2 to 7 built the six pieces: the ledger as a list, the balanced posting, the balance sheet and its invariant, the two tiers as typed accounts, cash as the bearer exception, and the submit/settle API that gives a payment a settlement moment you can point at. Lessons 8 to 10 put the machine to work - the interbank payment, loans creating deposits, the central bank’s levers - and lessons 11 to 14 parked the idle cash it produces. This project spends all of it at once: a full day at three banks, driven by your script, with every rule the module taught asserted as it happens.

Close of business

Monday, three small banks: Alder, Birch and Cedar. Alice banks at Alder, Bob at Birch, Carol at Cedar. Over the morning, three payments queue: Alice to Bob, 120; Bob to Carol, 80; Carol back to Alice, 50. Each one crosses banks; none is real yet. At noon the queue settles, oldest first, and reserves shuffle across the central bank’s ledger without a single unit going missing. Mid-afternoon, Birch approves Bob’s loan of 250 and writes brand-new money into his deposit without touching a reserve. Late in the day, somebody tries to push 10,000 out of Alice’s account, and the system refuses - loudly, by name, before anything moves. At the close, three trial balances print and every sheet squares. Nothing in that paragraph is new to you; that is the point. Today you make every sentence of it execute.

The idea in one paragraph

A module that ends in a library owes you a proof, and the proof of a library is a day of real use, not a reread of its source. Your script drives the canonical miniledger through everything this module taught, in order: three cross-bank payments queued and then settled, showing that submit moves nothing and settle moves everything; a refusal caught by name, showing validation happens before money; a loan minting its own deposit, showing lesson 9 was not a metaphor; and a closing trial balance per bank, printed with every invariant green. Module 0’s project put the assertions in your script because the toy had none of its own. This library asserts its invariants itself, after every posting; your script pins the day’s exact numbers on top and proves the machine does what the module claimed.

The brief

Open code/project_miniledger.py. The scaffolding inside full_day() is written: a World, three Banks admitted - each admission opening a reserve line at the central bank - three customers with deposits, and three endowments of 500, 300 and 200. The endowment is lesson 10’s honest act: fresh reserves from the central bank fund each deposit on both tiers at once, which is what sets tier 1’s total to exactly 1,000 for the day. The numbers are stylised - round on purpose, small enough to check every assertion in your head.

One import drives everything: from miniledger import World, Bank, LedgerError. Your six stage files from lessons 2 to 7 are drafts of this library; the repo ships the assembled reference at the root as miniledger/, and that is what your script exercises - the one canonical import location every later module will use.

The milestone, restated

This run has to demonstrate the module README’s spec clause by clause. miniledger v1 is an importable library - from miniledger import Ledger, Bank, CentralBank works from anywhere in the repo - with double-entry postings whose balanced legs are enforced, typed accounts (asset, liability, equity), the two-phase submit/settle API that is the finality point from lesson 7, and a two-tier world of one central bank plus at least two commercial banks; this day runs three. Invariants are asserted after every posting: each balance sheet balances, and total reserves at the central bank are conserved by any interbank flow. Later modules import this one canonical library - their rails simulations, netting, mint/burn and atomic cash-against-asset settlement are all built on it - and extend it by wrapping or subclassing in their own code/; nothing ever vendors a copy or edits v1 in place.

World one central bank, member banks, one payment queue Ledger post(): balanced legs; sheet re-checked Bank reserves + loans; deposit IOUs CentralBank tier 1: a reserve line per bank inherits inherits submit() / settle() assert_world() later modules call here from miniledger import World, Bank, CentralBank
The miniledger architecture as later modules import it: a World box wraps the Ledger base class and its two subclasses, Bank and CentralBank, and exposes submit/settle and assert_world as the ports everything later calls

Wider than the screen; scroll it sideways.

The day, assertion by assertion

The five numbered steps in the starter’s TODO(you) comment are five assertions, and each one is a lesson replayed against the running machine.

When your step 2 calls settle(), the library works the queue oldest first, and each cross-bank payment lands as three postings: the payer’s bank sheds a deposit and reserves, the payee’s bank gains both, and the central bank relabels its two reserve lines. Module 0’s project counted that payment as four edits on three ledgers; miniledger v1 marks six changed lines for the same payment, because each bank also carries its own mirror of its reserve line - the reconciliation lesson 8 walked leg by leg. And the checks never wait for the finish line: every posting re-asserts its sheet, and every settled payment re-runs assert_world, so a wrong middle state dies in the middle, not at the close.

Check yourself

1. Step 1 asserts Bob still holds 300 after all three payments have been submitted. What is the assertion actually testing?

That submit validates and queues but moves nothing: a submitted payment is a promise in motion, and the ledger edits wait for settle - the commit point lesson 7 built. If the balance had already moved, payments would become facts the moment they were requested, and refusal would arrive too late to mean anything.

2. Reserves visibly moved during settlement, yet step 2 asserts total reserves still equal exactly 1,000. Why must both be true?

Settlement relabels reserves across the central bank’s lines - Alder’s down, Birch’s up - so the sum across banks cannot change. The one act all day that changed tier 1’s total was the endowment at setup, which minted the 1,000: that is the central bank’s own act, never a side effect of customers paying each other.

3. A classmate decides the loan step looks wrong and edits their copy of lend so it moves 250 of reserves into Bob’s deposit. What happens on their next run?

LedgerError, immediately. The posting reserves -250, bob +250 shrinks Birch’s assets while growing its liabilities: unbalanced legs, refused before a single balance changes. The real posting grows both sides at once - loans +250, bob +250 - because the loan asset funds the deposit IOU, and no reserves are needed at the moment of lending. Lesson 9’s claim, enforced by lesson 3’s rule.

4. The overdraft attempt sits in a try/except with raise AssertionError in the else clause. What wrongness does that else catch?

A library that forgets to refuse. If submit accepted the 10,000, no exception would be raised, and without the else the script would sail on and print its proud closing line while proving nothing. The else makes acceptance itself the failure: this leg of the day passes only if the refusal arrives, by name, before anything moves.

5. Birch’s closing line reads assets 590 = liabilities 590 + equity 0. How much of Bob’s 590 is money Birch actually received?

340: the endowed 300, plus 120 settled in from Alice’s payment, minus 80 settled out to Carol - which is exactly Birch’s closing reserves. The other 250 exists because Birch wrote it, backed by Bob’s promise to repay, sitting on the asset side as a loan. One printed line, both halves of the module: money that moved in, and money that was made.

Do this

The project, start to finish; standard library only. Work from module-01-money-at-rest - the script adds the repo root to its own path, so the miniledger import resolves on its own.

Run the starter as shipped:

python3 code/project_miniledger.py

The world builds - three banks admitted, three customers endowed - and the day stops at NotImplementedError. Everything above the raise is scaffolding you keep; everything below is yours, following the five numbered steps in the TODO(you) comment:

  1. Submit the three payments - alice to bob 120, bob to carol 80, carol to alice 50 - and assert nothing has moved yet.
  2. Settle. Assert every final balance - alice 430, bob 340, carol 230 - and that world.total_reserves() is 1000.
  3. Have Birch lend bob 250; assert his deposit grew and Birch’s reserves did not.
  4. Attempt an overdraft - 10,000 from alice will do - and catch the refusal by name: except LedgerError, assert "REFUSED" is in the message, and raise in the else if no refusal came.
  5. Call world.assert_world() and print a closing trial-balance line per bank.

While the queue is still full - after your three submits, before settle - add one extra line and run it once: world.submit(400, "Alder", "alice", "Birch", "bob"). Alice holds 500, but 120 of it is already promised, and the refusal in the traceback says so: owes 120 from the queue. Delete the line once you have seen it; the day must end green.

Green looks like this - the refusal prints itself mid-day, and the run ends:

Alder    assets   430 = liabilities   430 + equity     0
Birch    assets   590 = liabilities   590 + equity     0
Cedar    assets   230 = liabilities   230 + equity     0
miniledger v1 proven: postings, tiers, refusal, loans, conservation

Exit code 0 and that final line, exactly, are the module’s milestone, met. The completed script is solutions/project_miniledger.py; check yours against it when the day is green - and only then take the gotcha’s diff of your six stages against miniledger/ledger.py.

What you can now do. You can drive miniledger v1 through a full day - three payments queued and settled in order, a refusal caught by name, a loan minting its deposit - with every invariant green after every posting. That is this module’s capability in one sentence: money at rest is entries under invariants, and you now hold a working, importable machine that enforces them, assembled from six pieces you wrote and proved against a day you scripted. It is also the last time you build the machine; from here you drive it. Module 2 wires rails onto this library - the same submit and settle you called today, industrialised into the systems real economies run, queues with schedules, batches with netting, the clearing house pattern from module 0 running as a program - and every module after that extends the same import. The map was module 0’s; the machine is now yours.

What you can now do

You can drive miniledger v1 through a full day - payments, refusal, a loan - with every invariant green.