Where you are. Module 2’s project ended on one printed line: three rails, one ledger, one truth. You drove a seeded Monday through RTGS, the deferred-net batch and the instant rail, watched them disagree on cash moved and delay accepted, and forced them to agree on every closing balance. You leave that module with the miniledger as an importable library, the rails simulator as its proof, and finality, liquidity cost and gridlock understood domestically. This lesson takes the quiet word at the end of that sentence - domestically - and makes it the problem. It builds the two-country laboratory the whole module runs in, proves the border between the countries is real, and writes the day of payments the next fifteen lessons replay.
The question the machine cannot hear
One in the morning, Monday. A customer of Alder - alder-c2, in the code’s flat naming - owes 3,266 to a supplier in another country, a customer of a bank called Kiri. Your reflex says this is a solved problem: module 2 built three separate machines for exactly this shape - a payer at one bank, a payee at another, an amount. So type it: world.submit(3266, "Alder", "alder-c2", "Kiri", "kiri-c2"). The line does not fail the way module 2 taught payments to fail. It does not queue for reserves, miss a cutoff, or come back as a return. It raises KeyError: 'Kiri'. Your World has never heard of the payee’s bank. None of the three engines can even be reached from here, because each one lives inside a ledger that has no line for Kiri, under a central bank at which Kiri holds nothing. The payment has not been rejected. The question could not be asked.
The idea in one paragraph
A currency zone is a closed world, in exactly the sense your code gives that word. Inside one, everything you have built holds: a central bank at the top of the two-tier map, banks holding reserves below it, customers holding deposits below them, and every rail from module 2 settling, one way or another, across the central bank’s ledger. The dollar has one such world. The yen has another, with its own central bank, its own tier 1, its own banks. And no ledger spans the border: there is no account visible from both zones, no institution where a dollar bank and a yen bank both hold reserves, no shared anything for a balanced posting to balance across. Crossing that gap is this module’s whole problem, and every lesson from here to the project builds one piece of the crossing. This lesson builds the laboratory: two closed Worlds, proof that each works domestically, proof that nothing connects them, and a seeded day of forty cross-border intents that the module replays until, by the project, they finally settle.
Two countries, four banks
The workbench is code/border_workbench.py, and it builds its two countries with module 1’s own constructor. build_zone makes a fresh World per zone, and a World, remember, carries its own central bank - that was the point of the class. The dollar zone admits Alder and Birch, two banks you have known since module 1. The yen zone admits Kiri and Matsu - paulownia and pine; the tree-naming rule crosses the border even though money will not. Each bank opens three deposit customers, alder-c1 through matsu-c3, and each customer is endowed with 20,000. Every amount in this module, here and in the fixture, is stated in dollar-equivalent units: a simplification declared now and priced later, because converting one currency into another has a cost of its own, and lesson 7 gives that cost its own machinery rather than letting it blur today’s question.
Wider than the screen; scroll it sideways.
Then each country proves it still works. sanity_domestic pushes one interbank payment inside each zone - 250, from the first customer of one bank to the first customer of the other - through the same submit-and-settle you have called since module 1, and asserts the payee’s deposit reads 20,250. This is not ceremony. It pins down the scope of the module’s problem before you meet it: nothing about module 2 is broken in here. Inside either box, clearing, settlement, netting and finality all work exactly as taught. The problem lives only in the gap between the boxes.
The proof is a failed name
Before testing anything, the workbench states its premise as an assert: the set of bank names in the dollar World and the set in the yen World intersect nowhere. That sounds like pedantry until you notice what it rules out. If even one bank appeared in both Worlds, the two countries would secretly be districts of one, any payment could route through the shared name, and nothing this module builds would be necessary. The empty intersection is the border, stated as a set operation.
prove_no_bridge then attempts the hook’s payment properly, in both directions: Alder to Kiri submitted to the dollar World, Kiri to Alder submitted to the yen World. Both must raise KeyError, and the test passes only if they do. Look at how early the failure happens. Module 1’s two-phase API validates at submit, and validation walks the World’s own tables: the payer’s bank, then the payee’s account at the payee’s bank. Kiri is not in the dollar World’s tables, so the walk dies at the payee’s bank name - before the payer’s balance is checked, before anything is queued, before settlement is even a question. Every failure module 2 showed you was a payment struggling somewhere along a rail. This is different in kind: there is no rail, and the failure is the ledger telling you so at the earliest possible moment.
The day this module replays
The workbench’s last job is to write the module’s day. generate_fixture draws forty cross-border payment intents from a seeded generator: each gets an arrival minute somewhere in the day’s 1,440, a payer zone, a payer bank and customer, a payee bank and customer in the other zone, and an amount between 100 and 5,000 - household-sized, deliberately, because the people who pay the most to cross borders are the ones sending small amounts home, and the module keeps them in view. The intents are sorted by arrival and numbered 1 to 40, and the whole day totals 91,751 dollar-equivalent units. The seeded day also leans: twenty-five intents leave the dollar zone and only fifteen leave the yen, an imbalance worth remembering when the module’s closing lessons start cancelling flows against each other. The file lands next to the code as fixture_border.json, and every later lesson loads it rather than regenerating, so the whole module argues about one fixed day - module 2’s rule that a comparison is only honest on one input, carried across the border.
You will replay these forty intents until you know some by number, and that is the design. Lesson 2 splits each one into an instruction and a movement of value, and shows they travel separately. Lessons 3 to 6 build the relay of partner banks that carries them, and measure what each hop charges in fees, days and money parked in advance. Lessons 7 to 9 price the toll hidden inside the exchange rate. Lessons 10 to 12 meet the danger of paying your half first, and the both-or-neither discipline that removes it. Lessons 13 to 15 bring module 2’s netting back, at the border’s scale. The project settles all forty, end to end, and prices what the crossing cost.
Check yourself
1. The cross-zone submit raises KeyError before anything is queued. Why is failing at the name more honest than accepting the intent and failing later?
Submit’s job, since module 1, is to validate against the ledger’s own tables, and the payee’s bank is simply not in them. Accepting the intent would assert that some rail exists to carry it, which is false; every later stage - queue, cutoff, settlement - presupposes a shared ledger the two zones do not have. Failing at the name states the true missing ingredient precisely: not cash, not capacity, but connectivity.
2. Every module 2 rail assumed one thing so quietly it was never stated. Name it, and say where each of the three engines breaks without it.
All three assumed both banks hold reserves at the same central bank - one shared ledger at tier 1. RTGS breaks first: settling gross in reserves needs a reserve line for each bank, and the foreign bank has none here. The batch rail clears all day but must settle its nets over that same shared rail at the cutoff, so the nets have nowhere to land. The instant rail’s prefunding is itself a reserve balance, and there is no institution at which both sides could park it. One missing ledger, three symptoms.
3. Why does the fixture ship as intents rather than as payments submitted into some queue?
A queue belongs to a rail, and no rail spans the zones, so there is nothing to submit to - the workbench just proved that a cross-zone submit cannot even resolve the payee’s bank. Calling them intents keeps the bookkeeping honest: nothing has been validated, nothing promised, nothing is anywhere in flight. The module’s arc is exactly the work of turning an intent into a promise and a promise into settlement.
4. Both zones use the same dollar-equivalent units and the same 20,000 endowment. What real feature of a border does that hide, and why hide it now?
It hides that two currencies do not share a unit, and that converting between them is never free - a toll collected inside the exchange rate itself. Hiding it isolates this lesson’s problem, connectivity, from a different problem, price. Lesson 7 removes the simplification and builds the price machinery; until then, one problem at a time.
5. The workbench asserts that generate_fixture() run twice produces identical output, and its docstring fixes the order in which each intent’s fields are drawn. What do these two facts buy?
The same day on every machine, forever. Later lessons quote exact numbers - fees, delays, parked balances, netted totals - against this fixture, and those numbers are only checkable because the fixture cannot drift. The draw order matters because a seeded generator is deterministic only if it is consumed identically: reorder the draws and the same seed yields a different day.
Do this
Fifteen minutes, from module-03-across-borders. Open code/border_workbench.py. The scaffolding - the zone table, the deterministic customer naming, the miniledger import - is written; the TODO(you) markers are the workbench’s four jobs, in the order the file runs them. build_zone stands up one closed country: a fresh World, admit the zone’s two banks, open and endow three customers each at 20,000, then assert_world, because a country born unbalanced proves nothing. sanity_domestic pushes 250 across each zone through submit and settle and checks the payee reads 20,250. prove_no_bridge attempts the forbidden payment in both directions and passes only if both raise KeyError. generate_fixture draws the forty intents - respect the docstring’s draw order per intent (minute, payer zone, payer bank, payer, payee bank, payee, amount), because the determinism assert regenerates the fixture and diffs it against itself.
python3 code/border_workbench.py
Green is this, ending with the final line verbatim:
sanity: a domestic payment settles inside the dollar zone
sanity: a domestic payment settles inside the yen zone
sanity: neither zone can even name a bank in the other - no account bridges them
fixture written: 40 cross-border intents, total value 91751 dollar-equivalent, between a two-bank dollar zone and a two-bank yen zone - and no rail yet connects them
If prove_no_bridge reaches its AssertionError, your submit found the payee’s bank, which almost always means all four banks went into one World: you have accidentally built a planet with a single central bank, and this module’s problem dissolves - rebuild with one World per zone and rerun. If the determinism assert fires, or your total is not 91751, your draws are out of order or a field was drawn that should not be: consume the generator exactly as the docstring lists. The run writes fixture_border.json beside the code; every later lesson loads that file instead of regenerating, so this workbench runs first, once, before anything else in the module. The completed version is solutions/border_workbench.py; compare after you are green.
What you can now do. You can build a currency zone as what it actually is - a closed two-tier world with its own central bank - and prove both halves of this module’s premise with asserts rather than assertions: inside a zone, everything module 2 taught still works, and between zones there is nothing - no shared ledger, no nameable account, no rail. You hold the day the module replays: forty intents, 91,751 dollar-equivalent units that want to cross and cannot. Every lesson from here builds one piece of the machinery that moves them, and the first piece is a surprise about what actually travels: lesson 2 sends the instruction abroad and shows that nothing else goes with it - at the border, the message and the money part company.