<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: autumnbuilds</title>
    <description>The latest articles on DEV Community by autumnbuilds (@autumnbuilds).</description>
    <link>https://dev.to/autumnbuilds</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4065245%2Fdf9a16d5-06de-42b2-b91b-c9302bcaa7d4.png</url>
      <title>DEV Community: autumnbuilds</title>
      <link>https://dev.to/autumnbuilds</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/autumnbuilds"/>
    <language>en</language>
    <item>
      <title>How I Modeled US Lottery Tax Withholding for 21 Countries Without a Backend</title>
      <dc:creator>autumnbuilds</dc:creator>
      <pubDate>Thu, 06 Aug 2026 07:03:41 +0000</pubDate>
      <link>https://dev.to/autumnbuilds/how-i-modeled-us-lottery-tax-withholding-for-21-countries-without-a-backend-4m9</link>
      <guid>https://dev.to/autumnbuilds/how-i-modeled-us-lottery-tax-withholding-for-21-countries-without-a-backend-4m9</guid>
      <description>&lt;p&gt;Every time a US Powerball or Mega Millions jackpot balloons enough to hit global headlines, the advertised number is misleading in a very specific way: it represents the pre-tax annuity total, paid out over 29 years. For anyone living outside the US, finding a straightforward answer to "okay, but what would actually land in my bank account, based on where I live?" is surprisingly hard to come by.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://chamtax.com" rel="noopener noreferrer"&gt;ChamTax&lt;/a&gt; to answer that question directly. The most interesting engineering challenges here weren't about the UI — they were rooted entirely in modeling the cross-border tax logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the problem
&lt;/h2&gt;

&lt;p&gt;For a non-US resident, winning a US lottery touches at least two distinct tax jurisdictions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;US non-resident withholding&lt;/strong&gt; — under IRC §871(a), a flat 30% is withheld at the source, before the money ever leaves the US.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Home-country taxation&lt;/strong&gt; — handled completely differently by every country: flat rates, progressive brackets, surcharges stacked on surcharges, and sometimes an explicit "no confirmed statutory rule" gap.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most of these interactions also involve a &lt;strong&gt;foreign tax credit (FTC)&lt;/strong&gt;: in theory, the US withholding is credited against what you owe at home, up to whichever amount is smaller, so the two taxes don't just stack. In practice, that credit disappears whenever a country has no active tax treaty with the US — then the two taxes simply add up in full.&lt;/p&gt;

&lt;h2&gt;
  
  
  Client-side only, by design
&lt;/h2&gt;

&lt;p&gt;There's no backend. No database, no user accounts, nothing to leak. All 21 country tax models and all 50 US state rates live as plain JavaScript objects bundled straight into the browser, and every calculation runs 100% client-side.&lt;/p&gt;

&lt;p&gt;That was a deliberate constraint, not a limitation I worked around — it eliminates a category of infrastructure cost and attack surface I didn't want to manage solo, and it turns the privacy story into a verifiable fact instead of a policy document.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trick that made 20 of the 21 countries easy
&lt;/h2&gt;

&lt;p&gt;For almost every supported country, the tax model reduces to one pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;flatCountry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amountUsd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ftcAvailable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usWithholding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;amountUsd&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;homeTax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;amountUsd&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;credit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ftcAvailable&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;usWithholding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;homeTax&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;additionalTax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;homeTax&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;credit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;amountUsd&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;usWithholding&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;additionalTax&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because it's a flat percentage of the payout, the math is entirely &lt;strong&gt;currency-invariant&lt;/strong&gt; — I never need a live exchange rate to compute the &lt;em&gt;rate&lt;/em&gt; of tax, only to format the final number in whatever currency the user picks. That collapsed what could've been 21 separate currency-conversion code paths into one shared function driven by a per-country &lt;code&gt;{ rate, ftcAvailable }&lt;/code&gt; config.&lt;/p&gt;

&lt;h2&gt;
  
  
  Korea: the one branch that needed real money math
&lt;/h2&gt;

&lt;p&gt;South Korea is the exception. It taxes foreign lottery winnings as ordinary income under a progressive bracket system, and the brackets are defined in absolute KRW thresholds — not percentages of the payout.&lt;/p&gt;

&lt;p&gt;Because those bracket boundaries are fixed KRW amounts, this branch has to convert the USD payout to KRW first, walk an 8-tier progressive tax table, then convert the result back. It's the only place in the entire engine where the exchange rate actually changes &lt;em&gt;which bracket&lt;/em&gt; you land in, rather than just the currency you see on screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Being honest about what isn't verified
&lt;/h2&gt;

&lt;p&gt;A handful of countries — Thailand, Sri Lanka, Cambodia, Mongolia, Laos, and Pakistan — don't have a clean, citable rule for how they tax &lt;em&gt;foreign&lt;/em&gt; lottery winnings specifically. Their domestic lottery-withholding statutes are written assuming a domestic payer, which a US lottery obviously isn't.&lt;/p&gt;

&lt;p&gt;Rather than silently picking a plausible-sounding number, those branches carry an explicit ⚠️ note describing exactly what's being approximated and why (usually the closest general-income statute, or a PwC/KPMG country tax summary), instead of pretending to a confidence the underlying law doesn't support. Getting a tax calculator's confidence-labeling wrong — in either direction, overclaiming certainty or hedging everything into uselessness — felt like the real product risk here, more than any UI decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  i18n at a scale that made me rethink "just translate it"
&lt;/h2&gt;

&lt;p&gt;The site ships in 26 languages, several of them (Uzbek, Khmer, Sinhala, Kyrgyz) with no native speaker on the team to sanity-check the output. Running everything through one machine-translation pass and calling it done produced a few embarrassing mistakes early on.&lt;/p&gt;

&lt;p&gt;What worked better: a second AI pass specifically prompted to flag anything it &lt;em&gt;wasn't confident about&lt;/em&gt;, rather than just re-translating. A short list of flagged, low-confidence lines is something you can actually act on — a second uniformly-confident-sounding translation isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;Going wide on 26 languages before there was meaningful traffic in any single market is the decision I'm least sure about in hindsight. It's the right call if you believe the audience is inherently spread across many countries from day one (which I do, for this specific product) — but it's real, ongoing maintenance weight on every future change to the calculator.&lt;/p&gt;

&lt;p&gt;If you're curious about the tax logic for a country I haven't covered here, or think one of the ⚠️-flagged approximations is wrong, I'd genuinely like to hear it — that's the one place in this project where being wrong actually costs someone money.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>i18n</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
