DEV Community

AchieSun
AchieSun

Posted on

I Built a Tax Engine for 5 Countries on Cloudflare Workers. It Went About as Well as You'd Expect.

I have a folder on my computer called tax-shit. It contains 47 PDFs from five different tax authorities. I started it sometime last year, after I realized I'd been filing my own cross-border taxes wrong for two years.

I'm not an accountant. I was a developer at a German company, then moved to the Netherlands, spent eight months in Portugal (Lisbon, mostly), and at some point picked up a short contract in the UK, and each move added a tax authority to my life without removing any of the old ones. I'd ask a tax advisor, get an answer, ask a second advisor, get a different answer. So I did what anyone who's bad at knowing when to stop would do: I built a tax engine.

Four months of nights and weekends. Cloudflare Workers. Five countries. Zero server costs during development. Here's the stack:

Layer Choice
Runtime Cloudflare Workers
Framework Hono
Database D1 (SQLite at the edge)
Frontend SolidJS
Auth Better Auth
ORM Drizzle
Vector search Vectorize + BGE-M3
Payments Creem

It all ran on Cloudflare's free tier. I picked them because servers were the last thing I wanted to think about.


Architecture

┌──────────────────────────────────────────────┐
│              SolidJS Frontend                 │
│     Calculator │ Residency │ Strategies │ QA  │
└──────────────────┬───────────────────────────┘
                   │
┌──────────────────▼───────────────────────────┐
│            Hono API Gateway                   │
│        Auth │ Rate Limit │ Audit Log          │
└──┬──────────┬──────────┬──────────┬──────────┘
   │          │          │          │
┌──▼────┐ ┌──▼────┐ ┌──▼────┐ ┌──▼───────────┐
│  D1   │ │  KV   │ │  R2   │ │  Vectorize   │
│(SQL)  │ │(Sess) │ │(PDFs) │ │(RAG Search)  │
└───────┘ └───────┘ └───────┘ └──────────────┘
Enter fullscreen mode Exit fullscreen mode

The gateway is the only thing that knows about HTTP. The tax engine underneath doesn't know what a request is. That separation wasn't there at the start: the first version had tax logic mixed into the Hono route handlers, which felt pragmatic at midnight and indefensible a week later, so I ripped it apart over a weekend, broke half the tests doing it, and only once it was back together could I actually write tests. I wrote 977 of them. I still don't trust the thing, but the tests pass.


The German calculator shipped broken

For the first three hours it was live, the German tax calculator returned zero for every input. The BMF tariff formula was right. The threshold constant was off by one decimal place: €12,096 became €1,209.60, and every income above €1,210 fell into the wrong bracket segment.

Someone on Hacker News who actually knew German tax law posted a screenshot of their Steuerbescheid and said "this is wrong." I fixed it in twenty minutes. Three people had already used the numbers. I emailed each of them.

German income tax is a cubic spline. The law defines it as a piecewise function, §32a EStG, paragraph 1, four segments. The first two are zero and a quadratic. The last two are cubic. You implement the function or you're wrong.

// §32a EStG — pure tariff T(zvE), 2025
function tariff2025(zve: number): number {
  if (zve <= 12096) return 0;
  if (zve <= 17443) {
    const y = (zve - 12096) / 10000;
    return (932.3 * y + 1400) * y;
  }
  // ... two more cubic segments
}
Enter fullscreen mode Exit fullscreen mode

I spent two days verifying this against the BMF's published test cases. The German calculator module alone has 18 tests. The whole project has 90 test files, and at this point every threshold and rounding rule I know about is pinned to something the BMF publishes.

Portugal does something similar. The parcela a abater (the deduction amount) is derived from parliamentary bills. Nobody publishes it in a clean table, so I reverse-engineered it from the law text and cross-checked against worked examples from a Portuguese tax forum where half the posters were accountants arguing with each other. It took me a week.


Spain

I shipped the first version of the Spain calculator with four regions.

Spain has seventeen autonomous communities and two autonomous cities. Each one sets its own regional income tax rates. The régimen común covers most of them, but the Basque Country and Navarra have separate tax systems entirely: the Concierto Económico and the Convenio Económico. Andalucía taxes differently from Cataluña. Madrid's rates are lower than most of the country. I had four regions. I have no idea where I got four from. I think I was tired.

I fixed it. The Spain module is still the weakest part of the engine. I've tested the common-regime communities. The foral regimes in the Basque Country and Navarra are partially implemented, and I have not verified them against actual tax assessments. If you live in Bilbao and use this tool, your results might be wrong.


The strategy engine

I ended up with twenty-two tax optimization strategies across the five countries. The Netherlands' 30% ruling. IFICI in Portugal, the successor to the old NHR regime. Spain's Beckham Law. And the UK's Statutory Residence Test, whose tie-breaker rules have so many branches I had to draw the whole thing on paper before I could code it.

The UK test is the one I keep putting off. I've implemented the automatic tests, the ones where you're clearly resident or clearly not, but the sufficient ties test is half-finished, and the split-year day-counting has edge cases I only understood on the days I wrote them. I've verified eight of the seventeen possible outcomes. The other nine live in a file I haven't opened since March.

Each strategy has conditions. A few are deterministic: if you have a Dutch employer contract and a salary above the threshold, the 30% ruling applies. Full stop. Others need the user to answer questions first. The rest involve the LLM, though only to explain in plain language what a strategy means, never to decide whether it applies, because that part is a rules problem and rules belong in code. Any product that lets a language model decide whether a tax strategy applies to a real person's money is malpractice. Some of those products have funding.

The C-tier strategies go through a six-layer validation harness. Structured output schema, rule injection, numeric verification, a secondary audit pass, and two more layers of paranoia. The LLM is wrapped in enough guardrails that it outputs explanatory text and a list of citations, nothing more. I'm not going to be the person who got sued because a language model hallucinated tax advice.

The test suite covers the A-tier and B-tier strategies. The C-tier harness has tests for the validation layers but not for the LLM output itself. It's a gap and I don't love it, but you can't deterministically test a language model.


What is still broken

The UK Statutory Residence Test. Large parts of the Spain foral-regime calculator. The PDF form filler for the German Anlage N, which fills about 70% of the fields correctly and leaves the rest blank. The Netherlands "Box 3" wealth tax assumes a flat deemed return, which the Dutch Supreme Court threw out in 2023. I have a GitHub issue for it from February. It's still open. It's assigned to me.

The strategy engine has a bug where if you qualify for both the Netherlands 30% ruling and the UK remittance basis, the comparison output can show a negative number. It's only a display issue (the math underneath is right), but it looks bad and I haven't fixed it yet.

I built this because I was tired of not knowing what I owed. I still don't know what I owe in some edge cases. The plan is Box 3 first, the Anlage N filler after that, and the nine UK outcomes whenever I stop finding excuses not to open that file.


The project is in beta. €29 a year while it's in this state, €99 once Box 3, the UK residence test, and the Spanish foral regimes are done, and everyone who signs up now keeps the €29 price. If you're a developer working across European borders, try it: taxmora.com. If it tells you zero, it's probably the decimal point bug again, so let me know.

Top comments (0)