Where you are. Six lessons have built the machinery of record: money as entries in a list, postings whose legs must balance, the bank as a balance sheet under one invariant, the two tiers rebuilt in code, and cash as the one central-bank money the public can hold. Everything so far shares one silence: no payment has ever been judged. Whatever you posted, posted. Module 0 coined settlement - the moment a payment’s ledger edits become final - and promised that this module would build the machinery of the moment itself. This lesson is that machinery: a payment split into a promise and a fact, and a ledger that can refuse the promise before anything moves.
The ledger that could only watch
At the end of module 0’s project you broke the toy on purpose. Alice held 500; you told pay to move 600; it did. She landed on -100, both conservation assertions stayed green, and the lesson named the reason: conservation is not solvency. The amounts moved in matching pairs, so the invariants were satisfied; nobody had asked whether Alice was good for it, because there was no moment at which anybody could. pay was a raw write. It committed the instant it was called, and a rule that wanted to inspect the payment first had nowhere to stand. The toy could not refuse a payment, only witness one.
The project’s last page made a promise about this module: refusal is what the submit/settle API adds. This is that lesson. And the cure is not a cleverer pay with an if bolted on the front. It is admitting that “make a payment” was never one operation. It is two - a request and a commit - they happen at different times, and everything a serious ledger owes the world lives in the gap between them: the right to say no, and a moment of no-going-back.
The idea in one paragraph
miniledger v1 splits a payment into two phases with different jobs. submit is the request: it validates the payment against the rules and against everything the payer has already promised, then either queues it or refuses it - loudly, with a raise - before a single balance changes. settle is the commit: it applies each queued payment atomically, every leg or none, and stamps it final. Between the phases the payment is a promise in motion: it exists, it binds the payer, but no ledger shows it. At settle it crosses the one line that matters, the point after which nobody can unwind it - a property with a name this lesson gives you, finality. Refusal lives entirely before the line; finality entirely after; and holding the two apart is what this lesson builds.
Submit: a promise in motion
The miniledger’s World is module 0’s two-tier map as a data structure - one central bank, the member banks admitted to it - and, from this lesson on, a payment queue. Here is the first phase, on the real API:
p = world.submit(120, "Alder", "alice", "Birch", "bob")
p.status # "submitted"
world.banks["Birch"].balance("bob") # 100 - unchanged
submit names the whole route - amount, payer bank, payer, payee bank, payee - and runs three checks: the amount must be positive, the payee’s account must exist at the payee’s bank, and the payer must be able to cover the amount on top of everything they have already promised. Fail any check and it raises LedgerError with a message that starts REFUSED:, and nothing - not one balance on one ledger - has changed. A refusal costs the system nothing to issue, precisely because it happens before anything moves; that placement is the design decision the whole lesson turns on.
Pass every check and submit still moves nothing. It builds a Payment - an id, an amount, the route - stamps it "submitted", appends it to world.queue, and hands it back. The class docstring says exactly what the object is: a submitted-but-unsettled payment, a promise in motion, not yet a fact. Module 0’s settlement callout warned about “a balance displayed but not yet safe to spend”; a submitted payment is that object, given a type. Bob’s app might well show the 120 as incoming. No ledger holds it.
The queue counts against you
The third check is subtler than it looks, and it is what makes the promise real:
pending = sum(
p.amount for p in self.queue
if p.payer_bank == payer_bank and p.payer == payer
)
if bank.balance(payer) < amount + pending:
raise LedgerError(...)
The payer is not measured against their balance. They are measured against their balance minus everything of theirs already waiting in the queue. A promise you have made is money you no longer have, even though every ledger still shows it in your account; banks surface the same subtraction to you as the gap between a “current” and an “available” balance.
Settle: promises become facts
settled = world.settle() # applies the queue, oldest first
settle drains the queue in order. For each payment it posts every leg atomically: a same-bank payment is one balanced posting on one ledger, while a payment that crosses banks lands postings on three ledgers - the payer’s bank, the payee’s bank, and the central bank, where a reserve movement squares the two banks. Lesson 8 walks that settlement leg by leg; today it is one atomic step. Then come the three small lines this lesson exists for:
p.status = "final"
self.settled.append(p)
self.assert_world()
The stamp is the event. Before that line, the payment was a promise that could still fail; after it, the edits are on the books and the world’s invariants have been re-asserted around them - after every payment, not once at the finish line. Module 0’s toy checked only at the end, and a whole class of wrong programs crossed the line unchallenged; this closes that gap too. And the property the stamp confers has a name:
Build on it how? Bob ships the goods, because the 220 is his even if Alice regrets the purchase, and his next payment can be submitted against it. Birch counts the reserves that arrived as its own, with no asterisk. Real systems draw the same line with law as well as code - a settled payment stays settled even when a participant later fails - and module 2 puts a price on reaching the line quickly, because finality in seconds and finality at end-of-day cost very different amounts of reserves. You met that trade in module 0’s wire lesson; now you have the word for what it buys.
Here is the whole lifecycle as one drawing; the dashed box is the phase this lesson added.
Wider than the screen; scroll it sideways.
Check yourself
1. submit has returned a Payment and Bob’s balance is unchanged. Where does the payment exist, and what has it already changed?
It exists in exactly one place: world.queue, as a Payment object with status "submitted" - a promise in motion. No ledger carries it. But it has already changed one thing: the payer’s capacity to promise. Every further submit of Alice’s is measured against her balance minus this queued 120, so the promise binds her before it touches a single account.
2. Alice holds 300 with 120 already queued. Why does a submit of 200 get refused, when 300 covers 200 comfortably?
Because 120 of the 300 is already spoken for. Keeping both promises would take 320, and the ledger can see that at submit time by adding the queue to the check: refuse when balance is less than amount plus pending. What is refused is a double-spend - not of money Alice lacks, but of money she has promised twice. Banks show you the same subtraction as your “available” balance.
3. The same checks could run at settle instead. Why must refusal happen at submit?
At submit, a no is free: nothing has moved, nobody has relied on anything, there is no cleanup to do. Validate only at settle and the queue can hold promises that cannot all be kept - the torn write rebuilt one level up, between payments instead of within one - so the refusal still happens, but late, landing on a payee who has watched the payment as incoming and may have acted on it. The rule: validate before anything moves; commit only what validation has already blessed.
4. Settlement and finality arrive in the same line of code, p.status = "final". What is the difference between the two words?
Settlement is the event: the moment the payment’s ledger edits are applied and become final - the word module 0 coined. Finality is the property that moment confers: from it onward, no party can unwind the payment, so other decisions - shipping goods, counting the money as yours, making the next payment - may safely build on it. One names the crossing; the other names being on the far side.
Do this
Ten minutes, nothing beyond the standard library. Work from module-01-money-at-rest and open code/submit_settle.py. The world is assembled for you: two banks, Alder and Birch; Alice endowed with 300 at Alder and Bob with 100 at Birch (endow funds an opening balance honestly, on both tiers at once - lesson 10 names the real-world operation it mirrors); and one payment of 120 already submitted. The TODO(you) block asks you to prove the phase gap with four assertions, in order:
- after submit,
p.statusis"submitted"and Bob still holds 100 - the promise has touched no ledger; - a second submit of 200 raises
LedgerErroreven though Alice’s balance alone could cover it - the queue counts; world.settle()returns 1 andp.statusbecomes"final";- Bob now holds 220 and
world.assert_world()stays quiet.
Print one line per phase as you go.
python3 code/submit_settle.py
Run unmodified, the starter stops at NotImplementedError. Completed, it prints one line for the promise, one for the refusal - let the ledger’s own REFUSED: message do the talking - and ends with the line
settled: the edits are facts on three ledgers
If your second submit fails to raise, check the order of events: settle first and Alice’s 300 really does cover 200, and the refusal you were proving never had a queue to count. The completed version is in solutions/submit_settle.py; check yours against it once the run is green.
What you can now do. You can split a payment into a promise and a fact and put your finger on the line between them: submit validates and queues, settle applies atomically, and the stamp p.status = "final" is the finality point - the property the apparatus exists to manufacture. You can make a ledger refuse instead of witness, and refuse for the right reason: not only money the payer lacks, but money already promised. That is the exact hardening module 0’s project asked for. What this lesson treated as one atomic step - the cross-bank settle whose edits land on three ledgers - is the next lesson’s whole subject: the deposit edits alone leave the banks out of square, and squaring them takes a reserve movement one tier up.