On April 20, a tournament org running on the production app could show three different team counts on three different screens, and they could all disagree. The package tier said one number. The per-event controls said another. The credit logic, when it ran at all, said a third. Each number had been correct at some point in the product’s history. None of them were correct together.
That was the problem the ticket was supposed to fix. I had written it up as a Packages UI redesign, pulled in a Figma mockup, and planned to spend a focused day on it. That was not what the day turned out to be.
the model was lying before the UI was
The tournament org feature had been bolted onto a system built for individual team registrations. The original design assumed a single account type. A team pays, a team registers. Tournament orgs are a different thing. They are organizations that buy credit pools, spin up multiple child events, and allocate teams across those events from a shared balance.
That model existed in the codebase. It just was not named. Whether an account was a tournament org, a solo tournament, or a child event of a parent org was determined at runtime by checking which flags were set and which were not, in a specific order that nobody had written down. Org behavior was inferred from a cluster of flags, fallback conditions, and package tier strings that had accumulated over several product iterations.
Claude helped me map it. I fed it the relevant ASP files and asked it to trace how the available-team count was computed for a tournament org create flow. It produced a call graph. The call graph had five decision branches, two of which ended in the same fallback, and one of which silently returned zero. That zero had been in production for months. Nobody had noticed because zero triggered a “contact us” fallback in the UI, which looked intentional.
It was not intentional, and I had not noticed.
account types were inferred, not declared
The first commit did not touch the UI at all. It standardized the account type taxonomy so that tournament orgs, child events, and legacy solo tournaments each had an explicit type constant instead of being inferred from flag combinations.
The new constants lived in a shared tournament constants file that server-side ASP and the client-side package configuration both pull from: one file, with both sides reading from it, instead of two files that were supposed to agree.
Before this change, adding a new account type meant updating three separate places and hoping they stayed synchronized. After, it meant adding one constant and updating the routing logic that branched on it. The old way had produced the silent zero. The new way makes a missing branch a visible gap.
the economic model was also scattered
Org credits are the economic primitive for tournament orgs. A new org starts with a credit balance. Each event it creates costs credits. Adding teams costs credits. The per-pack size and prices are configuration, not logic.
None of that was true in the code I inherited. Starting credits were a hardcoded integer in the org creation handler. Event costs were a different hardcoded integer in the event creation handler. Pack sizes for purchasing additional team slots were a third set of strings in the Packages UI template itself, formatted for display but not referenced by the engine that calculated what a purchase actually bought.
The fix was not clever. I moved all three into the tournament constants file. A single starting-credit constant, event cost per creation, pack sizes as an array the UI and the engine both read. When pricing changes, one file changes. The number means the same thing everywhere it appears.
the UI fix came last
The Packages page redesign from Figma was the last thing I touched. The original design had per-event team controls, spinners that let org admins set team counts at the individual event level. The redesign replaced those with a single global add-teams counter tied to the org credit pool.
That sounds like a simplification. It was not a simplification. It was the UI finally telling the truth that the model had been telling underneath it since the taxonomy and constants changes landed. The per-event spinners had been encoding a lie, that team allocations were event-level decisions. They are not. They are org-level decisions drawn from a shared pool. The spinner had survived from before the credit model existed.
Removing it was straightforward once the model was honest. The new counter reads the org’s current credit balance, shows available teams as a derived number, and posts a single add-teams action. Claude generated the first draft of the ASP handler for that action in about three minutes. It got the credit deduction logic right on the first pass because the constants were now in one place and it could read them.
what changed in production
New tournament orgs now start with a fixed credit allocation. That number appears once in the codebase, in the constants file, and it flows through org creation, the Packages display, and the credit ledger. Event creation costs a known amount. Adding a pack of teams costs a known amount. The Packages page shows the real credit pool, not a derived guess from three separate event-level tallies.
A second ticket also shipped on April 20. An orientation-flow refactor compiled and released to staging while the first was in review. Smaller change, same principle. The orientation flow had been inferring account type from the same flag cluster. It now reads the type constant directly.
Two separate tickets ended up making the same correction: read the declared type instead of inferring it.
the agent made it faster but could not make it cleaner on its own
Claude traced the call graph, drafted handlers, and caught two places where I had not updated all the references to the old flag-cluster pattern. It was useful for the mechanical work. It could not have identified the root cause on its own because the root cause was not a bug. It was a missing abstraction.
When I first described the problem to Claude as “fix the available team count discrepancy,” it proposed patching the three places where the counts diverged. That would have worked. The counts would have agreed. The underlying model would still have been inferring type from flags, and the next developer to add a fourth account type would have created a fourth divergence.
I reframed the problem as “the account type taxonomy is implicit, make it explicit.” After that, Claude’s output was better on every pass. The call graph became readable. The handler drafts were clean. The constants file suggestion came from Claude, not from me.
The output was only ever as honest as the problem statement I gave it.
the moment you stop letting the abstraction lie
I have shipped plenty of pricing tweaks. Add a column here, change a string there, deploy, move on. This was not a pricing tweak. It was the day I admitted the tournament org model had never been named, and named it.
If the underlying model is wrong, AI just helps you ship cleaner nonsense faster. The fix is not a better prompt or a better UI. It is stopping, naming the broken abstraction, and rewriting it until every surface reads from the same source.