Bank Job
in plain words
Talk Penny, an AI bank teller, into wiring you money. Each of the five levels adds one real security control, and you can see exactly how far every con got. Underneath, it shows that keeping an AI agent safe is mostly about what it's allowed to do, not how it's told to behave.
under the hood
How it works
- The browser sends a level and four tile ids, never text. game-api checks them against an allowlist and builds Penny's prompt itself.
- The agent gets a signed per-attempt token, and every bank tool Penny calls goes back to game-api over MCP, where the policy engine authorizes it for the level.
- Money only moves as balanced double-entry postings in Postgres, and the outcome comes from the ledger, never from what Penny says.
- Each change commits with its event through a transactional outbox. Kafka consumers build Level 5's anomaly signals, the leaderboard and the audit log.
- The browser starts one OpenTelemetry trace per attempt, and Under the hood reads it back through every MCP call, SQL query and consumer.
- If the API or the model budget is away, the game plays recorded results from static files.
Stack
- Java
- game-api on Java 25 and Spring Boot, built as a GraalVM native image: sessions, prompt assembly, the ledger, the policy engine and an MCP server
- Python
- Penny and a second-model guardrail agent that fails closed, plus the attack-matrix evals
- MCP
- Every bank tool the agent calls goes back to game-api to be authorized
- Anthropic API
- Penny and the guardrail; live-model eval runs go through the Message Batches API
- PostgreSQL
- Double-entry ledger with balance enforced by a deferred constraint trigger, append-only postings, optimistic locking and idempotency keys
- Kafka
- Transactional outbox relay, idempotent consumers and dead-letter topics, with a test that stops Kafka mid-transfer and loses nothing
- OpenTelemetry
- One trace per attempt from the browser through each MCP call, query and consumer, collected by Jaeger
- React
- Hand-drawn SVG scene with tap and keyboard alternatives to drag and drop, checked by axe
- k3s
- Runs on a homelab VM behind a Cloudflare Tunnel that exposes only the API and its health check
- Terraform
- The Cloudflare side: tunnel, routes and DNS
- GitHub Actions
- Plays all 6,048 graded attack combos against a committed baseline and builds multi-arch images
- k6
- Player load tests: 94 ms p95 at 20 steady players against a 150 ms SLO, with no errors
The longer version
Each level adds one control, enforced in code outside the model:
- Front Desk. Penny’s instructions only.
- Teller Limits. Transfers capped at $1,000 per call in the policy engine.
- Policy Engine. Transfers need the account owner to pass identity verification.
- Step-Up. An out-of-band confirmation text to the phone on file.
- The Vault. Step-up on contact changes, Kafka-fed velocity rules and a guardrail agent.
With a scripted teller that falls for everything, 600 of 864 cons win on Level 1 and none win on Level 5 from a fresh start.