When we started, we thought "state tax" was going to be a column in a spreadsheet. Instead, it became a maze with seven different escape hatches. Here's how we ended up structuring the tax engine behind Lottery Dreams — the data model, the edge cases, and the one question that turned out to matter more than any other.
Our first version was a flat dictionary: state code to decimal rate. California: 0.0. Florida: 0.0. New York: 0.109. Compute the cash option, multiply by the rate, subtract. The web has probably a hundred calculators that stop here. They're all wrong, or at least they're all misleading in ways that matter when the number is eight digits.
Lottery tax rules have a lot more texture than "income tax rate." A partial list of the things that actually vary:
That's the seven-scenario matrix we referenced in the marketing copy. Each one is a small rule in isolation and a nightmare in combination.
Does "state tax" mean what they hold back, or what you actually owe?
We kept hitting the same wall: the same cell in our table meant two different things depending on the conversation. The withholding number is what a winner sees the day they claim — it's tangible. The liability number is what they'll really end up paying after they file. Both are useful. Conflating them is how you lose someone $30 million in expectations.
We ended up shipping both. Every row in the breakdown carries two values under the hood — withheld and owed — and the UI is clear about which one is which. In the hero card on the homepage, we show the total liability, but in the detail view we split out the withholding line so nothing surprises anyone.
After a lot of rewrites, the tax engine boils down to a per-jurisdiction struct that looks roughly like this:
state, county, city, federal.resident, nonresident, both.When a user picks a state, we roll up the applicable rows for their situation and produce a single ordered breakdown: federal withholding, federal top-up at filing, state withholding, state top-up, county/city tax (withheld or owed). That rolled-up order is what you see in the app's tax breakdown view.
Tax law changes. We wrote the rate file so that every row carries a "last verified" date and a source URL — usually the state revenue department's own page. Before every release, we re-verify any row older than 180 days. It's a small discipline, but it's the thing that turns the app from "a clever calculator" into "a tool you can actually lean on."
We also wrote a small test harness that takes a handful of historical real-world winners — publicly reported jackpot, state, and, where we could find it, reported net — and checks that our engine reproduces their take-home within a small tolerance. When the engine drifts, we'd rather know from a regression test than from a user.
We do not try to simulate your full 1040. We do not ask for dependents, itemized deductions, or other income. The app's job is to get the prize-related number right and to make it clear how much is certain vs. how much depends on the rest of your return. If you actually win, you bring a CPA. That's in the Tax Disclaimer for good reason.
For all of the above, the app still only loads one flat data file. The complexity lives in the shape of each record and the rules that combine them. A single JSON blob, signed, shipped with the binary, and swappable over the air when something changes. If we've done our job right, you never think about any of this — you just see a number you trust.
This post describes the shape of our tax engine at launch. Rates, rules, and approximations are illustrative — see the Tax Disclaimer and consult a professional for any real-world claim.