>_SPENCERLABSECURITY · INFRASTRUCTURE SERVING FROM THE HOMELAB|0 INBOUND PORTS
GitHub
Built

The Real-Life ATM — my backend, Claude Code's frontend

A three-bank ATM in Python: I wrote the object-oriented banking backend, Claude Code built the Pygame arcade machine on top of it — and the same game now runs in your browser, compiled to WebAssembly.

4 min read

Play it

Try the machine

This is the actual Pygame game, not a remake. It's compiled to WebAssembly and runs entirely in your browser. The first launch downloads a Python runtime, so give it a few seconds, then click once to start.

The ATM's attract screen: THE REAL-LIFE ATM, press OK to begin

Click the keypad or use your keyboard: number keys, Enter = OK, Backspace = CLEAR, Esc = CANCEL. Balances live in the page's memory and reset when you reload.

BankType digitsPINNameBalance
CIBC0011234John Smith$1,000
CIBC0022345Sarah Johnson$2,500
RBC0014567David Wilson$1,500
BMO0017890Robert Taylor$2,000

Demo accounts only. Get the PIN wrong three times and the machine keeps your card, just like a real one.

The idea

The bank is the backend; the machine is just the interface

I started with a plain terminal ATM to learn classes and methods: state and behaviour. The design question that made it worth writing up came later. Can the same banking code drive a completely different front end without changing?

It can. The logic sits behind one small service layer that never prints and never asks for input. The terminal program and the arcade game both call the same methods, so neither one knows how accounts or balances actually work.

  atm.py       (terminal) ─┐
                           ├─▶  atm_service.py  ──▶  Bank ─▶ Account ─▶ Customer
  atm_game.py  (Pygame)   ─┘     clean API, no I/O       OOP logic + flat-file storage
Two front ends, one backend. The browser build is the Pygame front end again.

My part

The backend

Three classes, each owning one idea:

  • Customer: identity and the PIN check.
  • Account: the balance, deposit and withdraw, and writing every transaction to the bank's log with a timestamp, amount and new balance. Invalid amounts and overdrafts raise an ATMError rather than printing, so each interface decides how to show the reason.
  • Bank: loads its customers from a flat file and writes changed balances back.

Storage is deliberately simple: one pipe-delimited file per bank (ID|Name|PIN|Account|Balance) and one transaction log per bank. Three banks (CIBC, RBC and Bank of Montreal) share one ATM network, each with its own customers and log.

PIN entry screen: Welcome, John Smith, enter PIN, two digits masked as asterisks
PIN entry. Three attempts, then the machine retains the card, matching the original terminal logic.

Claude Code's part

The frontend

Claude Code built the machine around that backend: atm_game.py, a drawn ATM kiosk with a card slot, a numeric keypad, a phosphor-green CRT screen, an animated cash dispenser and a receipt printer. It also generates its sound effects in code, with no audio files.

Main menu: Welcome John Smith, 1 Check Balance, 2 Deposit, 3 Withdraw Cash, 4 Exit
The main menu, driven by the same service calls as the terminal version.

Withdrawals are enforced in multiples of $5 and broken into real bills ($100, $50, $20, $10, $5), which drop into the tray one by one:

Take your cash: $185 dispensed, with $100, $50, $20, $10 and $5 bills in the tray
$185, dispensed as one of each bill.

Getting there meant a small refactor of my code, which is worth being specific about:

  • It extracted atm_service.py, the clean seam both interfaces now sit on.
  • It added balance persistence. My original never wrote balances back, so every run started from the same amounts. Bank.update_balance now rewrites only the matching account's field, through a temp file, so a crash can't leave half a file.
  • It fixed my transaction log. I'd used ( ) instead of { } inside an f-string, so the log recorded the literal text instead of the amounts.

What I took from it

Splitting the work this way (I own the domain model, the AI builds the interface against it) only worked because the backend had a clean boundary. Once nothing in the logic printed or read input, a whole new front end could be built on it without touching how money moves.

Into the browser

Pygame on a web page

The game above is compiled with pygbag, which packages a Pygame project for CPython running in WebAssembly. The browser needs control back every frame, so the only new code is a small main.py that drives the game's own handle, update and draw from an async loop. The game file itself is unchanged.

Gotcha

A pygbag build produces an .apk and a .tar.gz. The first test shipped only the .apk: the Python runtime loaded, then stopped on a 404 for the archive. Ship both.

Honest notes

What it isn't

It's a learning project. PINs are stored in plain text in the demo data files. That's fine for a simulation, and not how a real ATM stores credentials. Hashing them and wrapping atm_service in a small web API are the obvious next steps.

The source is on GitHub: Python_for_IT_Automation / Bank_ATM / The_Real_Life_ATM.

More in Built →