Building an MTG Collection App, Part 2: The Deck Builder
Part 1 ended with every card I own in a database. Part 2 is about the fun idea: pick a format and a power level, maybe a commander, and have Claude build a deck using only those cards.
The first version took an afternoon. Making it trustworthy took a lot longer.
Rule one: trust, but validate
Language models are very good at deckbuilding and have no idea what’s in my shoeboxes. Ask for a deck, and you get a great list that includes cards I’ve never owned.
So every deck Claude returns goes through validation before it gets saved. Cards I don’t own are dropped. Quantities are capped at the copies I own. Cards outside the commander’s colors or illegal in the format are removed. And the app tells me about every change instead of fixing things quietly. It’s the same rule I use for AI at work: let the model be creative, and check its output in code you control.
The Island incident
Early builds kept coming back short on lands. It turns out that “cards I own” included basic lands, and ManaBox, quite reasonably, doesn’t track the pile of 200 Islands every Magic player has. So the validator was enforcing my scanned count of basics, and decks came out with three Islands.
The fix was a commit titled “assume unlimited basic lands,” which is also how I’d like to approach my actual cardboard.
Two decks, one commander
I added an option to let Claude pick the commander, steered away from commanders already in use in my other decks. Then I started two builds at the same time. Both read the same list of “unused” commanders. Both chose the same one. Two decks, one commander, and that one commander wasn’t around to break the tie.
It was a textbook race condition, in a hobby project, about wizards. Now the save runs under a Postgres advisory lock, and the loser asks Claude to try again. I fix this kind of bug at work too, but at work nobody’s commander gets double-booked.
Thinking costs tokens
My favorite bug was a “Scan only” deck analysis that kept failing with “hit
the token limit.” The output wasn’t long. The thinking was. Extended
reasoning counts toward max_tokens, and careful thought about a 100-card
deck plus EDHREC data used up most of a 16k budget before the JSON response
even started. The fix was raising the cap to 64k. The lesson was that I’d
been rate-limiting the model’s homework.
Getting the rules right
A few other things the deck builder learned the hard way:
- Ban lists change. Card data was cached, so a newly banned card could stay “legal” in my database for a long time. Now any card in a build that’s more than a day old gets re-checked against Scryfall.
- Brawl is two formats. I’d built Competitive Brawl against the wrong card pool and ban list. Along the way I’d also excluded planeswalker commanders like Oko, which is the most Oko thing that could happen.
- A Commander deck is exactly 100 cards. Not 97 because validation removed three. Short decks now get filled from the most popular legal cards I own, lands first.
- Vibes aren’t strategy. Builds and upgrade suggestions now draw on real EDHREC data and community deckbuilding guidelines (land counts, ramp, card draw, removal) instead of whatever the model remembers.
Where part 2 ended
The deck builder streams the list as it’s built, so you watch the cards arrive instead of staring at a spinner. It can scan a deck I already have and suggest upgrades, split into “swaps you can make from your own cards” and “a $25 shopping list.” That split exists because the first version of the upgrade feature had a much bigger idea of my budget than I do.
By then the app also handled MTG Arena imports, Moxfield checks, sealed product with price history, a wish list, an advisor chat, and partner commanders. Each one made sense when I added it. All together, the app had turned into a junk drawer.
Which brings us to part 3.