Available now · Senior / Staff / Lead IC · Remote, US Available · Remote IC
Selected work

Grimoire · Solo design + build · 2026

An AI-native deck builder for Magic: The Gathering. Six weeks of solo design and build, live in production.

A collection manager and deck coach that stands on parity with Moxfield and Archidekt, and wins on one wedge: the model proposes, but it never gets to be the system of record. Live at grimoirestudy.com.

RoleSolo design + build + go-to-market
TimelineSix weeks · first shipped on day 15 · still building
PlatformResponsive web · grimoirestudy.com · native app on main, not in the App Store
StackNext.js · Neon Postgres · Claude · Scryfall
StatusLive in production · open beta · creator push starting
Grimoire's deck workspace for an Atraxa, Praetors' Voice Commander deck, bracket B3, 100 cards, total value $1,425.09. The Deck Coach rail runs down the right side with a usage gauge reading 'Unlimited' and no number, and the coach's analysis in prose: counter doublers with excellent density, a solid proliferate package, draw at 14 and ramp at 11 both above floor, interaction at 16 a touch high — closing with 'Your move: want me to work through the 12 land additions and proposed cuts to make room?'. Below the deck header, a Build plan panel reads '99 / 99 cards in deck · Target reached · +6 staged' and '0/6 pillars done · now picking: Proliferate engine', with Approve all 6, Approve Proliferate engine, and Reject all beneath it.
The deck workspace. The coach reasons in the right rail against real prices and real cards; the build-plan strip and the footer independently track what's committed versus what's merely proposed. The coach can suggest. The system of record decides. (The usage gauge reads "Unlimited" with no number — that's the metering rebuild described below.)
The short version
ProblemThe dominant deck builders are deck-first: brilliant if you arrive with a list, silent about the cards you already own. Turning a real collection into a legal 100-card Commander deck is still hours of manual cross-referencing.
ApproachMatch the incumbents on table stakes — decks, collection, pricing, playtest — and spend the entire innovation budget on one wedge: an agentic Deck Coach that builds with you, pillar by pillar, where every fact it relies on is resolved by code that can be checked rather than asserted by the model.
ResultLive at grimoirestudy.com. The first shippable version took 15 days — 66,761 lines, 52 routes, 406 commits. Today main stands at 1,043 commits, 222,468 lines in the web app and 46,678 more in the native one, and the number worth pointing at is test files: 78 → 314. The tests grew faster than the code, because production kept finding things the tests didn’t. The tests grew twice as fast as the code, because production kept finding things the tests didn't.
Honest line760 AI actions, $54.06 of real Claude spend, zero failed generations across nine features, and a coach measured at 90.5% once the judge could see its own evidence. It's in open beta with a handful of users and the first marketing push days old — the build proved the velocity; distribution is the open bet.

The system

The model proposes. The system of record decides.

Every AI deck tool lets a language model assert facts about your cards. Grimoire doesn't. The bracket a deck sits in comes from the database, never from a model argument. Legality comes from a nightly reconciliation against Scryfall, not the model's memory. Mana production is read from Scryfall's produced_mana field, not inferred from the card's prose. Ownership is resolved server-side before a card is ever staged. Synergy is fetched live from EDHREC rather than frozen inside an AI response that rots. The coach is fluent, fast, and completely untrusted — every fact it leans on is resolved by code that can be checked.

The coach builds with you, not for you

Ask for a deck and the coach doesn't hand back a finished 99 to accept or reject. It lays out an ordered set of pillars specific to your commander — Connive Engine, Interaction, Card Advantage, Mana Base — and works them one at a time, staging candidates onto a Considering board beside the deck rather than into it.

Every staged card is yours to rule on — approve or reject it inline in the decklist where it sits, tinted so you can see at a glance what's real and what's still a proposal, or take a whole pillar at once with Commit 10 to deck. Either way a card enters your 99 only because you put it there. The footer tracks it live: 0 / 99 cards in deck · 99 still needed · 0/7 pillars done · now picking: Connive Engine. The deck is never in a state where you don't know what's real and what's still a suggestion.

Across 331 recommendation events in production, users have accepted 271 of the coach's cards and dropped or rejected 59 — an 82% acceptance rate on cards a human actually ruled on.

Underneath, the agentic loop runs up to twenty-four tool steps across twenty-two tools: Scryfall for card facts, legality, and price; your own collection, so it prefers what you already own and saves your most valuable printing of it; Commander Spellbook for combos; the Comprehensive Rules by retrieval; community popularity data; real tournament results; and Grimoire's own signal — what players building this commander actually keep, cut, and thumb down. A bracket gate sits at the tool boundary, so a Game Changer your bracket can't afford is never proposed in the first place, rather than argued about afterwards.

Evidence, never a verdict

The newest tool is the one that tests the thesis hardest. competitiveMeta puts real tournament results in front of the coach — and every other constraint in this product is a veto. Legality, bracket, ownership: the system says no. This one is evidence the model is supposed to reason with, which is a much harder thing to keep honest.

The discipline lives in the data structure rather than the prompt. The payload carries its own confidence denominator — the number of tournament entries behind it — and below a threshold of ten it attaches "small sample, treat as weak evidence" to itself, automatically. The prompt then adds the judgment a number can't carry: strong at brackets 4–5, weak flavor at 1–3, never cite a win rate without its sample size. And the coach's request path never touches the tournament API at all; a weekly job materializes the tables and the tool reads them.

A win rate with no n is the most common way software lies with true numbers.

The save path is idempotent, legality-checked, and ownership-aware, and it independently names the exact off-by if the list lands at 98 or 101 — because one card short was the most common way the model failed.

Guided intake → gates (rate limit · mana · turn cap) → agentic loop, 24 steps across 22 tools, bracket-gated at the tool boundary → commander-specific pillars → candidates staged to Considering → you commit each pillar → idempotent, legality-checked, ownership-aware save

The Deck Coach's build plan expanded to its pillars. A strip reads '99 / 99 cards in deck · Target reached · +6 staged · 6 checked below' and '0/6 pillars done · now picking: Proliferate engine', with Approve all 6, Approve Proliferate engine and Reject all beneath. The open pillar 'Proliferate engine' carries a 'Needs review · 6 staged' chip and a one-line rationale — 'Atraxa only untaps once, the deck needs more proliferate than her trigger' — over six checked candidates with prices: Bloated Contaminator $4.15, Brimaz Blight of Oreskos $0.42, Nesting Grounds $0.33, Bloom Tender $13.44, Contentious Plan $0.36, Skyclave Relic $1.34, and a 'Commit 6 to deck' button. The next pillar, 'Counter payoffs', sits below marked Pending.
Pillar by pillar, with the reasoning attached: every pillar carries the one-line argument for why this deck needs it. Nothing here counts toward the 99 until you commit it.
Grimoire's decklist with coach-staged cards rendered inline. A blue banner reads '+6 staged by the Coach — approve or reject them in the list below', with the pillar name PROLIFERATE ENGINE and the six card names as chips. In the list itself, staged rows are blue-tinted with a STAGED chip and their own tick and cross controls — Bloated Contaminator, Bloom Tender and Brimaz sit among the ordinary creature rows. Section headers count them separately: CREATURES (26) +3 staged, SORCERIES (11) +1 staged, ARTIFACTS (8) +1 staged. The sticky footer reads 100 Main cards, 8 to buy, 92% owned, To buy $214.85, Value $1,425.09, Legal · Commander, Bracket 3.
The same six cards, seen where you actually work. Staged cards sit inline in the list, tinted, with per-card approve and reject — and the section headers count them separately (26 creatures, +3 staged). A proposal never moves a committed number.
The board-destination dropdown open in the deck workspace, headed 'Cards you add go to…' with four options: Main deck ('goes straight into the deck'), Sideboard, Considering ('a shortlist beside the deck — nothing in the deck changes'), and Wishlist ('a card you want but don't own yet').
Every "Add" asks where. A recommendation for a card you don't own defaults to the wishlist — so the coach can never quietly put a card you'd have to buy into your 99.

Disclosure on the three shots above. They're captured from the demo fixture deck, and the coach's build session was seeded directly in the local database rather than produced by a live run — a real build costs money and about two minutes a pillar, and wouldn't reproduce the same frame twice. What was seeded: the six-pillar outline, and six staged cards, all of them real cards from the local Scryfall catalogue, verified Commander-legal, inside Atraxa's color identity, and genuinely absent from that deck. Everything else in frame — the 100-card list, the prices, the ownership percentage, the bracket, the legality, the coach's prior reasoning in the rail — is untouched fixture data rendered by the real application. No pixels were edited and no copy was substituted. Same standard as the banned Mana Crypt further down.

The detail that defines the product

When a coached card lands in your deck, Grimoire doesn't save a generic card reference. resolveOwnedPrinting resolves it to the most valuable printing you actually own — your foil, your special-art version — and falls back to a standard printing only for cards you still need. Every other builder saves an abstract list; Grimoire saves your deck, then hands you the buy list for the gap.

Cost is a design constraint — and the first version of it was wrong

An AI feature that loses money per use isn't a feature, it's a countdown. The Coach runs on Sonnet; Opus is reserved for heavy-analysis surfaces where depth is the point and users read slowly. In production that decision shows up in the data rather than the assertion: 740 Sonnet calls to 19 Opus.

The first metering model was consumable credits — a build cost 10, an analysis 3, and you watched a number go down. I replaced it three weeks ago, and the reason is worth more than the mechanism.

The unit now is mana, and it is not a token. It's a display skin over real spend, pegged in code and commented never decouple this:

1 mana = $0.0001 of actual Claude cost · allowance = (net revenue ÷ 2) ÷ $0.0001

Two properties fall out of that peg. A user who burns 100% of their allowance still returns a 100% margin — a Collector's 45,000 mana is $4.50 of Claude against $9.40 net. And because an allowance always sits below break-even by construction, a whale is mathematically impossible rather than merely unlikely. The old flat dollar backstop was guarding against a case the pricing now can't produce.

The part I'd point at in a review, though, is the price table. Every per-action cost is annotated with where it came from — M for measured against live production usage with the sample size inline, E for an unmeasured spec estimate:

"coach-build": 10_000, // M: per-thread avg $0.669, median $0.375, max $3.29 (n=33)
"card-scan": 70, // M: $0.0093 (n=69). Kept as the adoption loss-leader
"rules-qa": 110, // E: ~$0.011 Sonnet (0 prod rows) — UNMEASURED

The original prices were guesses presented to users as facts. This one marks which of its own numbers are still guesses. That is the same instinct as flagging an estimated price in the database rather than in a caption — applied to the business model instead of the schema.

Underneath, the engineering survived the change intact. A single atomic conditional UPDATE … WHERE monthly ≥ x AND banked ≥ y spends across both pools all-or-nothing, so concurrent actions can't overspend, and a failed generation is refunded as a signed ledger row so the running total stays truthful rather than merely reduced. Two rules were added that matter more than the old backstop numbers ever did. Spend order: drain the monthly allowance before the mana someone paid for — reversed, you burn a user's money while their free pool silently expires, and the spec calls this the most important correctness rule in the feature. And a named residual race: the split between pools is computed from a snapshot read, so a concurrent spend can make the WHERE miss even when the pools would have covered it. The comment states the blast radius precisely — a rare, self-healing false "insufficient" that a retry fixes. It never overspends and never partially deducts.

The posture split is deliberate and worth stating: money gates fail open, abuse gates fail closed. If the metering table itself errors, generation proceeds unmetered — a billing bug must never lock a user out of the product. The rate limiter, by contrast, is synchronous and in-memory precisely so that no infrastructure blip can knock it open.

Build the instrument before the thing it measures

The coach's quality is measured by a calibrated judge against a golden set of nineteen hand-labelled turns, each one justified — covering the failure modes that actually matter: the coach confabulating what it can see, inventing card counts or colors, shopping for cards on a turn that was supposed to diagnose, or folding sycophantically the moment a user pushes back. The pass/fail verdict is derived from its sub-scores, so it can't emit a pass its own reasoning contradicts.

Then the gate fired on me. A prompt change I wanted to ship measured +8.3 points against the baseline. Rebased onto a tool change that had landed in the meantime, the same change measured −2.4 points — the earlier PR had already absorbed exactly the headroom the prompt was exploiting. The prompt hunk was reverted, not shipped on a stale number. A second request of mine — forcing a tool call on analysis turns — the eval also rejected: grounding was already running at 82–93% and was never the binding constraint.

A coach score is only valid against the tool surface it was measured on.

Then the judge turned out to be wrong, and it was punishing the thing I built the product for

The rubric requires every factual claim the coach makes to trace back to the decklist, a tool result, or the rules. Reasonable. Except that the code assembling the judge's prompt rendered only the tool names — never the results. The judge was being asked to verify claims against evidence it was never shown.

So a reply that faithfully reported real tool output scored as fabrication. I found it because I disputed a failure instead of accepting it. The judge had marked a Muldrotha reply as inventing card prices and wrongly calling Rhystic Study a Game Changer. I checked every claim against the database:

Sylvan Library ~$28 → 25.17–31.15 · Phyrexian Arena ~$4 → from 2.00 · Rhystic Study ~$70 → from 59.71 · Guardian Project ~$15 → from 13.15 · Solemn Simulacrum ~$0.27 → 0.29 · Rhystic Study — on the Game Changer list

Every one was true. The coach had called searchCards and reported what came back. The judge's own written rationale gave the game away: "the tool results themselves are never actually displayed."

This is the part that matters. A false negative against checking the source is not a scoring nuisance — it is a gradient pointing the wrong way. Left alone, the gate would have taught the coach that answering from memory scores better than looking things up. The instrument built to keep the model honest was quietly rewarding it for guessing.

The fix renders tool results into the judge prompt, caps and marks truncation inline so that cut off can never read as absent, and adds a rubric rule with the same shape: when a tool ran but its results aren't shown, absence is unknown, not false — a fabrication finding is reserved for a claim that is visibly wrong, not merely unconfirmed.

A prompt edit invalidates the prior calibration, so I re-measured. κ went from 0.894 to 1.000 — 19 of 19, and the judge's one remaining false negative is gone.

Then the bill came. Every absolute pass rate from the old judge is void, including a "67% baseline" I had been reasoning from — it was understating the coach by failing correct, tool-grounded replies. Re-run on the honest judge, the real baseline is 90.5%. The coach had been better than the gate could see the whole time.

What the honest judge then told me to un-ship

The coach is slow — 90 to 140 seconds on a one-line question, with roughly three quarters of the output being private reasoning. The obvious lever is to turn reasoning effort down, and an earlier A/B said I could: it looked fine.

That A/B had been scored by the blind judge. Re-gated properly — n=42, three repeats, κ=1.000:

EffortPassGroundedDiagnosedOutput tokens
high38/42 · .905.929.92965,437
medium33/42 · .786.905.78645,407 (−31%)

A 31% token saving for nine points of pass rate. But the interesting part is where it went: grounding barely moved. Diagnosis fell, and it fell in clumps rather than scattering — two specific cases went 3/3 → 0/3 and 2/3 → 0/3. Reading the failing replies hands you the mechanism: at medium the coach starts saying "I can't see the land names," "I can't see all 99" and hands the analysis back to the user.

It doesn't get less correct. It gets lazier — it stops doing the synthesis from the manifest that is the product.

So the lever ships inert: default off, production sends no effort field, behaviour byte-identical to before the branch. The knob, its tests, and the measurement all stay in the codebase, so nobody re-litigates the finding from scratch in three months. And I want to be exact about what did not improve: nothing. Average coach latency in production has gone 85s → 78s → 108s across three weeks, and cost per coach action has risen from roughly $0.08 to $0.18 as the step budgets took on more work. There is no speed win to claim here and I'm not going to invent one. The latency problem is real and unsolved — effort is simply the wrong lever, because it buys speed with reasoning. Streaming the answer as it's produced attacks the perceived wait without touching the thinking, and that's next.

There's still a limit worth stating before a reader finds it: the live gate registers only the card-fact tools, because the fixture decks are synthetic. Anything reading Grimoire's own first-party data sits outside it. A gate whose scope you can state is a gate you understand; an undisclosed one is marketing.

Transferable: the pattern — constrain the model with real-data tools, keep the system of record in code the model cannot write to, put a human commit between proposal and persistence, and build the measuring instrument before you start tuning — applies anywhere "plausible" isn't "correct": config generation, compliance tooling, catalog and pricing surfaces, agent builders.

The problem

Deck-first tools ignore the shoebox

Commander is Magic's most popular format: 100 unique cards, color-identity rules, a power-bracket system, and a secondary market where a single list can price out at four figures. The established tools serve players who already know what they're building. Nobody starts from the other end — the pile of cards you actually own.

Competitive gap

ToolStrengthWhy it falls short
MoxfieldThe default deck database; deep format supportDeck-first. Collection is an afterthought; nothing builds for you
ArchidektVisual builder, rich categorizationSame deck-first assumption; steep for newer brewers
EDHRECMeta statistics for every commanderRecommendations, not a finished legal deck; blind to your cards
TCGplayer / Card KingdomPricing and purchaseNo bridge from "what I own" to "what this deck needs"

The scope call that made those first two weeks possible: match Moxfield's depth where it's table stakes; don't try to out-build it. Deck CRUD, collection tracking, pricing, sharing, and a playtest engine are parity features, built to be credible rather than superior. The innovation budget went entirely to the one job nobody does: collection → coached deck → buy list.

What that budget bought, beyond the coach itself, is an understanding layer: a deck doesn't just get built, it gets explained. Power score, mana curve, color requirements, detected combos, and a plain-language read on the deck's weak spot — every number resolved from real card data and the cards you actually own, never asserted by a model.

Grimoire's deck analysis for a mono-green Lathril elves deck: a Quick Read panel calling out resilience as the weak spot, a Deck Analysis scorecard with 100 cards, $451.08 market value and a power score of 6.1 out of 10, a Quick Fixes list flagging 'Light on Black — 5 Black sources, but your greediest Black card costs BB and wants turn 5', a playability estimate of a 65% chance of an on-curve play, a power breakdown across consistency, resilience, interaction and speed, and a mana curve chart beside manabase health and color-requirement meters.
The understanding layer. "Light on Black — 5 Black sources, but your greediest Black card costs BB" is not a model opinion: the source count is read from the card data, and the target comes from published manabase math, not invented arithmetic.
A live four-player Grimoire pod on the dark playtest board: seats for Alice, Carol, Bob, and Dave, each at 40 life with commander-damage trackers, a turn banner reading Carol's turn, Turn 1, a phase strip from Untap through End, the host's hand of seven real cards along the bottom, and battlefield lanes awaiting the first plays.
The "parity" feature that isn't: a real four-player Commander pod. The whole game is one serializable state object in a single Postgres row — no game server, no sockets — with each seat's hidden hand redacted server-side. Built and playable, but switched off in production behind a feature flag: it isn't ready to meet real players yet, and shipping it half-right would be worse than not shipping it.
Grimoire's deck editor on a phone: a full-width add-cards search with an import button, a Non-foil/Foil/Etched finish selector, board tabs for Main, Sideboard, and Considering, a Display options button beside a grid/list toggle, a type-grouped card list with quantity steppers and prices, and a sticky footer showing 99 main cards, 99 to buy, $653.53, a 1-illegal flag, and a Bracket 4 badge.
The same editor on a phone — Magic players brew at the kitchen table, not the desk.
Grimoire's rebuilt landing page on a phone: an open-beta badge reading 'Built in the open, updated July 27, 2026', a headline about not copying decklists, and real product screenshots below the fold rather than illustrations.
Zero clicks to value: featured community decks render before any signup. The front door was rebuilt around real product screenshots and a maintained inventory of what's live, what's experimental, and what's still coming — the marketing surface had been describing capabilities without distinguishing shipped from aspirational.

The design system

A colour world can be created faster than it can be found

The playtest board needed to feel warm. Instead of reaching for the parchment register the product already had, the work invented a parallel one — bespoke leather and felt hues that no token governed and nothing else in the product could reach. It looked fine. It was a second colour world, created in an afternoon, that would have taken months to find again.

That is the failure the design system exists to prevent, and it is named in the commit that hardened it: the reference document was rewritten and, in the same commit, a clause was added requiring the document be read before any UI work at all.

What it standardises, in the order it matters

  • One warm register. Warmth is the parchment register, full stop. There is no second way to make a surface feel warm, so there is no argument to have.
  • A five-step luminosity ladder instead of shadows. In dark mode separation runs surface-0surface-4 at roughly +0.025 lightness a step, and there is exactly one drop shadow in the product, reserved for genuinely floating overlays. One rule replacing an infinite number of shadow arguments.
  • A “never for” column on every token table. Not “here is mana-u” but “here is mana-u, and here is what it must never be used for.”
  • Decision tables rather than vocabularies. A classifier that tells you which bucket a surface is in before you reach for a colour. There is always an answer, which is the point.

gold = premium and value · red = interactive, live, selection · mana = data only · price = movement only

Four vocabularies that cannot borrow from each other. A blue thing on this screen means the card is blue. It does not mean “selected.”

Which is why the interesting artifact is the documented exception

Coach-staged cards needed a colour meaning provisional, awaiting your approval — a state most products have no vocabulary for at all. Every honest option was already spoken for: gold is brand chrome, red is destructive, green and amber are price movement. So mana blue was borrowed, and the borrowing was written into the system as a bounded exception with three conditions: it is named, it is bounded to exactly one meaning, and it must carry a textual “Staged” chip anyway, because colour alone is never allowed to carry state.

The rejected alternative is recorded next to it, with its price. Gold could not read as pending because gold is brand chrome everywhere — and on parchment it went nearly invisible. v1 shipped that way and failed on the exact surface it existed for.

Six passes on a provisional state

What followed is the best design work in the product. Four rounds against a real screen, where fixing the row kept breaking the control inside it. A rule that provisional cards are counted separately everywhere they appear — hence “26 creatures, +3 staged” — with a note in the grouping code that section counts deliberately do not sum to the deck size and must never be used to derive one. A scroll-navigator added once it became clear that on a 99-card list the user approves whatever happens to be on screen and never learns the rest exist. And at the end, the staged styles extracted into one module, so the next staged control inherits the fix rather than rediscovering the bug.

An AI that proposes changes to a document you own creates a state most products have no vocabulary for: this exists, and you have not agreed to it.

The system survived a platform change because a build fails

The stylesheet authors 217 oklch() values, which React Native cannot parse. The obvious move is to hand-copy a palette into the native app. The file that would have held that copy instead says do not add a hex literal to this app and re-exports a generated module, checked against source in continuous integration.

It has already caught real drift: main went red when a pull request edited the stylesheet without regenerating the tokens. The failure mode was named before it happened — a hand-copied palette on a platform nobody greps would drift silently and forever — and the gap between writing the check and wiring it up was stated in the file rather than glossed.

The honest half. The document drifts. Two separate commits in the last week existed only to bring it back into line with code that had already shipped, and outside the two places where it is mechanically enforced it is a good document that depends on being read. A design system that is documentation is a wish; the part of this one that is a build failure is the part I would defend.

Four decisions that drove the design

The non-negotiables

Collection-first, not value-first

The tempting home screen is "your collection is worth $3,936" — a number that flatters and goes nowhere. I rejected the portfolio-tracker framing; value is a secondary lens. The home surface leads with what your cards can do: decks you can build, decks that are close, what's missing.

Your most valuable printing, not a generic one

The alternative every competitor ships — resolve saved cards to a default or cheapest printing — is simpler and wrong. The deck you sleeve is made of the cards you own. resolveOwnedPrinting was more work at save-time and it's the difference between a list and your list.

Sonnet by default, Opus where it earns it

Everything-on-Opus would have made every free build a loss. The Coach runs on Sonnet; Opus is reserved for deck analysis and collection insights, where users read the output slowly. Unit economics were designed with the feature, not retrofitted after launch — and six weeks of production bears the split out: 740 Sonnet calls against 19 Opus.

Card images bypass the image optimizer — deliberately

Card art is the most-rendered asset in the product, and routing it through a metered optimization service means one viral day makes every card in production go blank when the quota exhausts. Images ship unoptimized, straight from Scryfall's CDN. Less elegant on paper; strictly fewer ways to fail.

One consequence worth stating plainly: the fastest path to value needs no account — the landing page renders featured decks at zero clicks, and a full read-only deck is one click away — but deck building itself is auth-gated. That's a deliberate trade: every Coach run costs real money, so the free tier is metered per-account rather than open to anonymous traffic.

Failure modes

What happens when the model — or the upstream — is wrong

A product built on someone else's card database and a probabilistic model needs explicit answers for the days both misbehave:

FailureWhat the system doesWhy
Scryfall down or rate-limitingBatched requests retry with backoff that honors Retry-After in full, and a failed chunk is marked unmatched rather than fatal. Imports additionally resolve against the local catalogue firstA 2,700-card import shouldn't die because request 1,900 hit a 429. (Live search has no such fallback — if Scryfall is down, search is down.)
Coach lands one card short of 99The build-plan strip live-counts N / 99 cards in deck, and the server-side save gate independently names the exact off-by — "D too few (add lands or spells)" — regardless of what any UI saysOne-card-short was the most common model failure in testing, so the gate lives in the save path, not the interface
Card image fails to loadCascade: primary URI → Scryfall by-ID redirect → text box with the card nameA deck page never renders blank
Search finds nothingAn explicit empty state with a loosen-the-filters nudgeDead ends need exits, not blank grids
The model errors mid-streamThe stream is inspected for an error part, an honest message is returned, and the mana is refunded as a signed ledger rowSee below — this one was silently broken, and the fix is the most instructive bug in the codebase
Grimoire's legality panel flagging a deck as not Commander-legal: a red banner reading '2 issues keeping this deck from being Commander-legal', a banned-card violation naming Mana Crypt outright, and a deck-size violation stating the exact card count against the required 100.
Ban-aware legality: the panel names the banned card outright and the exact size violation. Legality is reconciled nightly against Scryfall — never asserted by the model. (Staged for capture: no development deck was illegal, so a banned Mana Crypt was deliberately added. The detection and the copy are unmodified.)

The bug a try/catch cannot catch

The AI SDK's streamText does not throw when the model call fails. It emits an error part into the stream — and the text stream that gets served to the browser filters to text deltas and drops it. So a failed generation returned HTTP 200 with an empty body: the user saw a blank panel, no error, and was charged for it. No try/catch around the call could ever have caught this, because nothing threw. The only place the failure is visible is the full stream.

Eight surfaces had the same shape. The fix inspects the stream, returns an honest message, and refunds — with a judgment call written as a pure function: an errored stream is refunded no matter how much text arrived, so the user keeps the partial answer and the mana, while genuinely hitting the token ceiling is charged and says so. Thirty-five regression tests; twenty of them go red against the pre-fix code.

The bug my own validation certified

The card scanner is the second-most-used AI surface in the product — 234 scans in production — and it shipped with a subtler failure than the one above.

A scan resolves two ways at once: exact, by set code plus collector number, and fuzzy, by the printed name. The exact lookup won unconditionally, on the reasonable theory that set-plus-number pins an exact printing. It does — if both were read correctly.

On a Marvel Super Heroes card, the model read the name "Doctor Doom" correctly, at 75% confidence, then misread the set code MSH as MH3 — visually near-identical. And set:mh3 cn:394 is a real, valid card: Ocelot Pride. So the wrong card won, and walked into the user's collection carrying its own art and its own $52 price tag.

The fix is that the name is the far stronger signal — large high-contrast type versus four-point text at the card's edge — so the exact lookup is now trusted only when it corroborates the name. The lesson leaves Magic entirely: when two extraction paths disagree, precision is not authority. The more specific identifier looked more trustworthy and was more fragile, and because its failures land on valid records, nothing downstream can catch them. This one was a money bug wearing an OCR costume.

What's deliberately imperfect: if the metering table itself errors, the gate fails open and generation proceeds unmetered. A billing bug must never lock users out of the product; the cost of that stance is that a database hiccup means free AI until it heals. I'd make the same call again — and it belongs in the documentation, not hidden in a catch block.

What's not handled yet, stated plainly: connection loss mid-edit — ordinary deck edits are plain server actions with no offline queue and no optimistic draft, so a dropped connection loses the change; only the multiplayer playtest reconnects gracefully. Rate limiting exists, but on four routes only, and it is a per-user in-memory brake rather than a distributed defense: under horizontal scale each instance keeps its own counters, so the effective ceiling is a multiple of the configured one — the source file says so about itself. Bot protection does not exist anywhere in the app, and neither does throttling on the auth endpoints. All known, none invisible.

Distribution

The go-to-market is part of the build

A deck builder with no players is a database. Unlike my other projects, Grimoire shipped with its distribution mechanics in the codebase from week one:

  • A referral ladder, not a referral link. Double-sided mana rewards, with named milestones — Advocate at 3 referrals, Champion at 5, Legend at 10 — so sharing has a progression, not just a coupon.
  • Zero-touch creator onboarding. Comped Pro is an allowlist applied automatically at sign-in — no admin panel, no manual grants, no waiting. I should be honest about the scale of it: the allowlist is a three-name array in the source, so onboarding a creator today is a code edit and a deploy. It has been used zero times.
  • Every public deck is a landing page. Shared decks render read-only with no account, carry a "Build your own" call to action, and are enumerated in the sitemap for search.
A public Grimoire deck page for 'Lathril, Blade of the Elves' over full-bleed commander art: Commander badge, a B4 bracket indicator, 100 cards, the owner's @brem handle, 64 views, a Build Your Own call to action, a total value of $1,731.96, a Read the Codex panel, and the decklist grouped by card type with color pips and per-card prices. An open-beta banner across the top reads 'Built in the open, updated July 27, 2026. See what's working and what isn't.'
That third bullet, rendered: a shared link opens a full read-only deck with a "Build your own" call to action — no account needed. The banner across the top links to a hand-maintained inventory of what works, what's rough, and what's still coming.

Since then a social layer shipped on top: @handles, public profiles, followers, a native share sheet.

A public Grimoire profile page at /u/brem: the handle, follower and following counts, a Share button, and the user's public decks laid out as cards with commander art and deck values.
The social layer, and the honest measurement of it: 4 follows, 3 followers, 28 likes. Meanwhile public decks pulled 383 views. Decks are the distribution surface; the social graph is scaffolding.

Distribution is the part I have least evidence for, and I'd rather show the evidence than describe the effort. The mechanics are built and they work: a creator account was comped and claimed inside a day, the entitlement applied automatically at sign-in exactly as designed. It has built zero decks. Ten users hold referral codes; none has been redeemed. Marketing has only just started, so none of this is a verdict — but the lesson already available is that granting access is the easy half. Nothing in the product yet gives a creator a reason to open it on a Tuesday, and that's a product problem I was treating as an outreach problem.

The gap I'd close first is the same one as six weeks ago, and it got sharper rather than smaller. Per-deck social previews still don't exist: the deck page — the single most-shared URL in the product, the thing the whole "every public deck is a landing page" argument rests on — has no Open Graph block at all, and there's no social-image route anywhere in the repo. A shared deck still previews as a blank card in Discord, and Discord is where Commander players live.

What makes it worse is that the codebase now demonstrably knows how. When profiles shipped, they got a proper OG block with an image and a matching card type. So the capability exists, on the surface that matters less, and the deck page still doesn't have it. That's not a missing skill, it's a missing decision — and it's still not on the roadmap, which is the honest answer rather than the flattering one.

The second gap, and it's free: the sitemap enumerates every public deck — but not the commander hub pages, one per commander, each carrying its own metadata and naturally long-tail. The largest organic search surface in the product is sitting there unlisted. The new public profiles aren't in it either.

Reflection

What worked, what I'd do differently, when this approach is wrong

What worked

  • Parity-not-outbuild scoping. Deciding on day one which features only had to be credible freed the two weeks to be spent where the product is actually different. This is the same altitude call I make in team settings; the solo build just makes it visible.
  • AI-assisted velocity, human architecture. 66,761 lines in the first 15 days is Claude Code doing the typing. The calls that made the product work — the streaming ingest parser, the atomic spend gate, owned-printing resolution, the eval harness — came from debugging real failures, not from generation.
  • Measure before you tune. Building the calibrated judge before touching the coach prompt is the decision I'd repeat first. It is the only reason I found out that a change I was proud of was worth nothing once rebased.

What I'd do differently

  • Never let an estimate be stored where a fact goes. Grimoire couldn't reach a real buylist, so a card's buylist price was a guess — 60% of retail. The UI said so. The database didn't: the price-snapshot table had no column marking a row as estimated, so the guess persisted in the same shape as a real vendor quote, and the price-history chart plotted it as a plain "Buylist" line with no caveat — a flat 0.6× of retail rendered as if it were market data. The disclosure lived in the one place a user might not look and was missing from the one place they would. The estimate needed a column, not a caption. So it got one: an estimated boolean, the chart now draws those series dashed and badged with "about 60% of the market price, not a real vendor quote" in the tooltip and the accessible label, and real vendor buylist data now lands flagged as genuine. Of 318,906 price snapshots in production, 12,884 are marked estimated. I'm keeping this one on the page after fixing it, because the architecture lesson is the point and it was the right diagnosis before it was a fix.
  • Test where the money is. The codebase has 314 test files now, up from 78 at first ship. The deck-save path used to have zero. It has seven — added off a production incident where a decklist carrying 1 Swamp on four separate lines wrote four separate rows, so there was no single row to increment and I had to tell the owner of two real decks to fix them by hand. But read the caveat carefully: those seven tests cover a pure helper. The database write, the ownership scope, and the idempotency probe are still untested. And the atomic spend is still pinned by reading the source file and asserting on strings rather than executing it. Shape, not behavior. The thing production broke now has tests; the write itself still doesn't.
  • A flat token cap is a guess about a distribution. A batched generation was capped at 2,200 output tokens while real responses measured 2,045–2,451 — the cap sat inside the output distribution, so it failed on a coin flip and charged for the failures. If you haven't measured the distribution, your cap is sitting somewhere in the middle of it.
  • Reconcile estimates against reality. The AI-cost ledger was under-reporting by roughly threefold — one day billed $9.85 against $3.60 recorded — because it logged only the final step of a multi-step tool loop. Fixing that immediately exposed the opposite error: cached input tokens were being billed at full rate. An estimate that's never checked is a guess with a UI.
  • Email before launch, not after. Transactional email is still a console-log stub — two TODO: wire an email provider comments and a console.log of the link — which means password reset is genuinely broken for a real user. There is no email provider configured in production of any kind. This has been true, unchanged, for six weeks, across two of the busiest build weeks in the project. It is the weakest thing in the product now that the price estimate has a column, and the only reason it hasn't bitten is that eight people is few enough that I hear about it directly.
  • The quality gate that guards a fix is why the fix never ships. There's a retrieval tool wired into the coach that has been live and registered in production for weeks and has been called zero times — not a flag problem, the flag is on. The model simply never reaches for it, because the prompt names two other tools for the same job and never mentions it. The fix is one line of prompt. But the prompt is one of four eval-gated files, so it can't be a drive-by edit — and when I did run it through the gate properly, the gate couldn't see the tool at all, because the tool reads first-party data and the harness only registers card-fact tools. A dead tool stays dead because the instrument protecting it structurally cannot measure it. That is the cost of the discipline I'm proudest of, paid in full.
  • The catalogue can't be corrected. The bulk ingest inserts and skips conflicts, so it never updates the rows it already has. Oracle errata and legality changes never reach the 116,000 existing cards; a nightly cron patches the single column that matters most as a workaround. The real fix — an upsert-mode ingest — is on the board.

When I wouldn't ship this approach

The parity-plus-wedge sprint works when the incumbents' feature set is well-understood, the underlying data is public commodity (Scryfall is a gift), and the wedge is genuinely orthogonal to what they do. It's the wrong play when correctness is the product — and the Rules Judge is the proof. It shipped answering rules questions from the model's memory, which produces good answers rather than judge-grade ones. That gap was only closable by doing the unglamorous thing: vendoring the actual Comprehensive Rules and retrieving over them. It now does exactly that.

A correction I owe this page. The last version said the moat tool — the one reading Grimoire's own signal back into the coach's picks — was "built and wired, and the table is currently empty," contributing nothing. That was wrong, and I want to be precise about the direction: I under-claimed. It isn't one table. It fans out over five, and the deck-inclusion half has been live for weeks — 2,542 card rows across 28 decks telling the coach what actually gets built. The rated-turn half is real signal, but it is mine — 117 turns I graded myself while adversarially testing the coach, not a verdict from users. And the half that reads what actually wins is dark: zero games recorded, exactly as published.

And the reason that half is dark is more interesting than the status. Match outcomes accumulate from playing games inside the app. That means the multiplayer pod — which is built, playable, and switched off behind a feature flag because it isn't ready to meet real players. The moat's second half is empty because the feature that would fill it is turned off. That's a causal story, not a status line, and it's the kind of thing you only find by checking a claim you were already comfortable with.

Next on the roadmap: a native mobile app — the scanner is a phone-camera feature and people brew at the kitchen table, so the responsive web build has been carrying weight it wasn't shaped for. It is no longer specced-not-built. 46,678 lines of React Native landed on main between 28 and 31 July; the shell, auth, the five tabs, deck edit, the AI surfaces, scan, and wishlist are merged. It is also not in the App Store — no submission, no TestFlight — and only 6 of 18 screens have been seen on an actual phone. I track those two numbers separately, because a green CI has never once caught a visual defect in this app. The gate on this work is a walkthrough, not a build. Alongside it: the deck-page redesign, an upsert-mode catalogue refresh, and the arbitrage panel that tells you where to actually sell. Batch card scanning came off this list and shipped. Per-deck social previews are still the biggest known gap — still not a commitment on the board, which is itself the honest answer.

Status

On Grimoire

Grimoire is portfolio work and a real product: live in production, in open beta, with a paid tier fully built and wired to Stripe. A handful of users, the first marketing push only days old — a six-week-old product still being finished, with the selling still ahead of it. Those counts are three days into a launch, not a result, and I'd rather not dress them up as either.

The numbers I'd rather be judged on are the engineering ones: 760 AI actions, $54.06 of real Claude spend, zero failed generations across nine features, a 31.6% prompt-cache hit rate, 271 coach proposals accepted against 59 rejected — and a coach measured at 90.5% on a judge calibrated to κ = 1.000.

There's one more artifact I'd point at, and it's important to say exactly what it is. There are 117 rated coach turns in the database, and every single one is mine — graded by me, harshly, while adversarially testing prompts against my own model's output. It is not a satisfaction rate and I won't present it as one; there is no user signal in that table at all. What it is, is a hand-built corpus of 117 labels that feeds the learning loop and seeds the eval set. That's a more defensible thing to have built than a satisfaction metric, and a lot more work.

I maintain it on my own time and in small windows. I'm available for full-time senior, staff, or lead IC work. Grimoire does not compete with that.

What this one proves

That I can hold a product model steady while a language model is loose inside it. The coach is fluent, fast, and completely untrusted; the system of record stays in code the model cannot write to; a human commit sits between proposal and persistence; and the instrument that measures the whole thing was built before the tuning started. The design work is the same act at a different altitude — a colour system where four vocabularies cannot borrow from each other, and one documented exception for the state an AI product creates and most products have no word for.