Where you are. Nine lessons have handed you, in order: the five parties behind a card tap, money as a tamper-evident record, the deposit as a goldsmith’s IOU, the clearing house that settles differences instead of trades, the wire that made a payment final in seconds, the two-tier map of whose ledger your money lives on, the tokenisation claim that would redraw that map, the reading rules that keep this course honest, and a verified Python toolkit. This project spends all of it at once: you will push one payment across both tiers with your own hands and prove, with assertions, that the map is real.
A monetary system in fifty lines
Open code/first_payment.py. Ten lines in, you are holding a complete monetary system: a central bank whose ledger carries one line, First National’s reserves of 1,000; and First National itself, whose ledger carries two, alice at 500 and bob at 200. Every unit of money in this little economy is one of those dict entries. There is nowhere else for money to be.
Run it and Alice pays Bob 100. The trace prints every line of every ledger and marks what changed: alice 500 -> 400, bob 200 -> 300. The central bank’s line prints 1000 -> 1000, unmarked. A payment happened, and the top tier never stirred.
Now move Bob across the street. Give him an account at Second City, a bank that does not exist yet, and send the same 100 from the same Alice. The moment Bob’s balance is a different bank’s IOU, two edits stop being enough - and making that same payment work again will walk you through everything this module built.
The idea in one paragraph
Inside one bank, a payment is two edits on one ledger: debit alice, credit bob, done - the bank owes one customer less and another more, and its own position never moves. Across two banks, the same payment takes four edits on three ledgers, because First National shedding an IOU while Second City takes one on leaves the two banks un-square with each other, and the thing that squares them is a reserve movement on the central bank’s ledger - tier 1 doing the one job it exists for. Your script asserts conservation on both tiers: each tier’s total unchanged by the payment, every debit paired with a credit. When those assertions stay green across your two-bank version, you have run lesson 6’s map as a program instead of reading it as a diagram.
The brief
The starter models the same-bank world: run it as shipped and Alice pays Bob inside First National - two edits, one ledger, assertions green, exit 0. Your project is to move Bob’s account to Second City, a bank you will create, and make the same payment work again, now as four edits on three ledgers, with the assertions still green and the exit code still 0.
The starter protects you from half-finishing: pay raises NotImplementedError the moment the payer’s bank and the payee’s bank differ, so an unfinished cross-bank payment fails loudly instead of pretending it worked. Your reserve leg replaces that raise.
What the assertions guarantee
The script ends with two checks:
assert reserves_after == reserves_before, "tier 1 total must be conserved"
assert deposits_after == deposits_before, "tier 2 total must be conserved"
The first says that a payment between customers can relabel which bank owns reserves but can never mint or destroy them: changing tier 1’s total is the central bank’s own act, never a side effect of Alice paying her rent. The second says the same thing one tier down: summed across every commercial ledger, deposits are conserved, because Alice’s debit is exactly Bob’s credit. Money moved; none appeared. Between them, the two assertions demand that every edit your code makes arrives with a cancelling partner - the every-debit-finds-its-credit discipline this module has been circling since the tally stick, now executable.
And this is why the project is the map. Lesson 6 drew two tiers: the central bank’s ledger, where banks settle in reserves, and commercial banks’ ledgers, where everyone else holds a bank’s IOU. The same-bank run shows tier 2 working alone: two edits cancel inside one bank, and tier 1 sleeps. The cross-bank run shows why tier 2 cannot always work alone: no set of edits confined to First National’s and Second City’s own ledgers can leave both banks square, because the only ledger where both banks hold accounts is the central bank’s, one tier up. Conservation on tier 2 forces the deposit edits to pair; conservation on tier 1 forces the reserve edits to pair; and needing both pairs for one payment is the two-tier map, restated as arithmetic your machine checks.
Wider than the screen; scroll it sideways.
What module 1 hardens
The toy is deliberately soft, and naming exactly where is the last thing this module owes you.
Nothing is typed. A reserve line and a customer deposit are both bare dict entries; nothing in the code knows they are different kinds of thing, on different tiers, under different rules. Module 1 gives the miniledger typed accounts, so the ledger itself knows what lesson 6 taught you.
The invariants run once, at the finish line. tier_totals is compared before and after the whole run, so a wrong intermediate state lives happily until the end - and one whole class of wrong program, the missing reserve leg, crosses the line unchallenged. Module 1 asserts its invariants on every posting - every individual ledger edit - and adds the per-bank check, that each bank’s own ledger stays square, which is exactly the check that would have caught it.
pay cannot say no. It commits the instant it is called. There is no notion of a payment being requested, checked against the rules, and accepted or refused - which is also why nothing stops Alice spending money she does not have.
Check yourself
1. In the same-bank run, the central bank’s line prints 1000 -> 1000. Why does a real payment leave tier 1 asleep?
Both accounts are entries on First National’s own ledger. The payment changes who the bank owes - 100 less to Alice, 100 more to Bob - but its total IOUs are unchanged, so it has nothing to square with any other bank and no reason to move reserves. Tier 1 exists for the between-banks problem, and a same-bank payment never poses it.
2. In the cross-bank run, reserves visibly move, yet the assertion demands tier 1’s total stay exactly constant. How are both true at once?
The reserve leg relabels ownership: 100 leaves First National’s line and lands on Second City’s, so the sum is untouched. A customer payment can redistribute central bank money between banks; it can never create or destroy any. Changing tier 1’s total is something only the central bank itself does.
3. A classmate’s two-bank version prints alice 400 and bob 300, exits 0, and its central bank lines never change. What did they actually build?
A system where Second City credited Bob 100 and received nothing: it now owes more, backed by nothing, while First National owes less and kept all its reserves. The banks are not square, and no assertion in the toy notices, because both tier totals sum across all banks and the two deposit edits cancel in that sum. The per-bank invariant that would catch it - each bank’s own ledger staying square - is precisely one of the checks module 1 adds.
4. Why does the script assert on tier totals instead of on expected final balances, like alice == 400?
Expected balances pin one scenario; change the amount or the route and the test is wrong rather than the code. Conservation holds for every valid payment, so the same two assertions police the same-bank run, the cross-bank run, and any experiment you try - and a failure always means the same thing: an edit went in without its pair.
Do this
The project, start to finish. Work from module-00-orientation; everything here is standard library, no toolkit needed.
Run the starter as shipped:
python code/first_payment.py
Two lines are marked <- changed, the central bank’s line is unmarked, and the final line is conservation holds on both tiers; exit 0.
Then work the three markers, in order:
TODO(you) 1- inLEDGERS, give Second City a ledger holding Bob’s 200, give the central bank aSecond City reservesline, and remove Bob from First National. Split the existing 1,000 of reserves between the two banks (the solution uses 800 and 200) rather than minting new ones: Bob changing banks is no reason for the economy’s tier 1 total to move.TODO(you) 2- inpay, replace theNotImplementedErrorwith the reserve leg: debit the payer bank’s line on the central bank ledger, credit the payee bank’s line. Two edits, one tier up.TODO(you) 3- repoint the call:pay(100, "First National", "alice", "Second City", "bob").
Run it again. Success is the module’s milestone, and it is mechanical: the script exits 0, both conservation assertions hold, and the trace names which ledger changed at each step - four <- changed lines this time (alice, bob, and both reserve lines), with the final line still conservation holds on both tiers; exit 0.
Then break it once, deliberately. Delete the debit half of your reserve leg, keep the credit half, and run: tier 1’s total rises by 100 and the script dies on tier 1 total must be conserved. Restore the line. You have now seen what the assertion catches - a half-written leg - and you know from the gotcha what it cannot catch: a leg that is missing entirely. The assertion polices pairing; the trace polices presence.
The completed version is solutions/first_payment.py. Check yours against it when you are done, not before.
What you can now do. You can trace one payment across both tiers of a ledger system and say, for every edit, whose ledger it lands on and which edit pairs with it. That is the module’s whole capability: you hold the map the next seven modules fill in. And this fifty-line toy is the ancestor of module 1’s miniledger - same two tiers, same conservation discipline, hardened with typed accounts, invariants asserted on every posting, and a submit/settle API that can refuse a payment instead of merely witnessing it. The map is yours; the machinery comes next.