DEV Community

AION ANALYTICS for Aion Analytics (India)

Posted on Originally published at aionanalytics.in

A Lithuanian tax calculator that cites the law on every line, and declines when it cannot

At AION Analytics (India) we build software that shows its working, and our newest public demo is for Lithuanian personal income tax. Many Lithuanian residents file their own income tax return every year, the law is public and well structured, and the tools we tried either hide where the numbers come from or quietly guess when the law gets complicated. We wanted to build the opposite. Every figure cites the article of the law it comes from, and when we cannot cite something, the tool says so and stops.

The free demo is live at aionanalytics.in/lt-tax-demo.html, no sign-up needed. Open it in another tab and follow along. This post explains what it does, how to use it, and a few of the decisions behind it that we think other developers will find interesting, especially if you build anything that handles money or regulation.

What it does

You type in your 2026 income by type. The page works out the personal income tax under the Lithuanian Law on Personal Income Tax (GPMĮ) and shows the result as a table. Each row has three things: what the line is, the amount in euro, and the article it rests on.

Here is the default example, an employee earning EUR 30,000 a year who paid EUR 1,000 in life insurance premiums and EUR 800 in pension contributions:

Line EUR Article
Aggregated annual income 30,000.00 GPMĮ 6(1)(1)
Non-taxable amount (NPD) 1,043.64 GPMĮ 20(1)
Deductible expenses 1,500.00 GPMĮ 21(3)
Taxable aggregated income 27,456.36
General income, 20% (band EUR 0.00 to 83,237.40) 5,491.27 GPMĮ 6(1)(1)
Total tax (effective rate 18.30% of total income) 5,491.27

Two things are worth pointing out in that table.

The insurance and pension figures add up to EUR 1,800, but only EUR 1,500 is deducted. That is the sub-cap in article 21(3). A tool that silently drops EUR 300 from what you typed is confusing. A tool that drops it and shows you the article that requires it is useful.

The NPD (the tax-free amount) is EUR 1,043.64, not the headline EUR 8,964. The NPD tapers away as income rises: it shrinks by 49 cents for every euro above twelve times the minimum monthly wage. At EUR 30,000 most of it is gone. People are often surprised by this, and seeing the article next to the number tends to end the argument quickly.

How to use it

  1. Open the demo. It starts in Lithuanian; there is a language switch at the top for English.
  2. Enter your annual 2026 income in the fields that apply: employment income, property lease income, agricultural activity profit, and income taxed at 15 percent (dividends and certain benefits). Leave the rest empty.
  3. If you have a recognised participation or disability level, pick it and enter how many months it applied. Article 20(6) replaces the normal NPD with a fixed monthly amount for those months, and the page handles the split year.
  4. Enter life insurance premiums, pension contributions and any other article 21 expenses you paid.
  5. Read the table. Every row names its article, so you can look it up yourself on e-seimas.lrs.lt.
  6. Press "Prepare the full report" to get everything in one document: what you entered, the calculation, the filing deadline, the documents you must keep and which GPM311 appendix each figure belongs on. Use "Print or save as PDF" to keep a copy.

The calculation runs entirely in your browser. Nothing you type into the tax fields is sent to a server. You can open the network tab in your developer tools and check.

The part we care about most: it declines

There is a section on the page called "What it will not tell you". Two real examples:

How much will I get back?
This assistant reports what the law says and cites it. For the amount of a refund, contact VMI or a licensed tax adviser.

The deadline falls on a public holiday. Does it move?
This date is a non-working day. Whether the deadline moves as a result is not confirmed in any source we hold, so please check vmi.lt before relying on a later date.

The second one is a good example of the kind of trap we mean. The filing deadline in article 27(1) is 1 May, which is a public holiday in Lithuania every year. It is very tempting to write "so the deadline moves to the next working day", and it may well be true. But we could not find that rule in a source we could cite, so the tool does not say it.

This week we applied the same rule to something bigger. Income from individual activity (self-employment) has a 2026 tax credit that brings the effective rate well below the headline 20 percent for smaller incomes. The earlier version of the demo did not apply that credit, so it could overstate the tax for smaller freelancers. We have not yet ingested the credit from the official text and signed it off, so the demo no longer calculates that line at all. If you enter individual activity profit, you now get this:

Line EUR Article
Individual activity: not calculated declined GPMĮ 6(8)
Total tax (excluding individual activity) ...

plus a note telling you to check that part with VMI or a licensed adviser. An empty cell with a reason is less satisfying than a number. It is also not wrong, which matters more when the person reading it is about to file a legal return.

How it is built

It is deliberately boring.

One constants object, with sources. Every rate, threshold and cap lives in a single object at the top of the page: the average wage (VDU, EUR 2,312.15 for 2026), the minimum monthly wage (MMA, EUR 1,153), the three bands at 36 and 60 times VDU with their rates and articles, the NPD cap and taper, the article 21 caps. The VDU and MMA entries carry the vmi.lt URLs they came from. When a figure changes, there is one place to change it and one source to check it against.

The article travels with the number. Each result row is built as [label, amount, article]. The renderer cannot print an amount without also printing where it came from, because they are the same array. That sounds trivial, but it is what stops citations from drifting away from figures as the code grows.

Comments explain the law, not the code. The tricky part of Lithuanian income tax is that two different lists of "what counts" are used in different places. The NPD taper runs on all taxable income (article 20(7)), while the banded tax runs on aggregated income, which excludes agricultural profit and income taxed at 15 percent. Use the wrong list for the taper and you under-taper, which gives people a bigger tax-free amount than they are entitled to. The code has a comment at exactly that line saying so, because the next person to touch it will not have the statute open.

A declined value is null, not zero. The individual activity row carries null as its amount, and the renderer prints "declined" for null. Zero would be a claim (you owe nothing on this income). null is an honest absence. The total is labelled as excluding it, and the effective rate is computed only on the income we actually calculated, so the percentage is not diluted by income we did not tax.

Bilingual from the start. All user-facing text is in one object with Lithuanian and English keys, and Lithuanian is the default. Tax terms are the hard part. The English labels are renderings, not translations of record, and the page says that.

What is free and what is not

The demo is the free tier: the calculation, the citations and the filing instructions. The paid version we are working towards adds keeping your receipts through the year and finding deductions you have missed. The page is honest about one limit of that, too: article 35 requires you to keep the original document, or a notarised copy, for ten years if you deduct an expense. A photo in an app is neither, and we say so rather than imply otherwise.

Tell us what is wrong

There is now a feedback box at the bottom of the demo page. Up to 2,000 characters, an optional email if you want a reply, and a Send button. It goes straight to us. That box is the only thing on the page that sends anything anywhere, and it only sends what you type into it.

We would especially like to hear from:

  • people who file in Lithuania and found a case the demo gets wrong, or a refusal that should have been an answer;
  • Lithuanian speakers who can tell us where the Lithuanian wording reads like a translation;
  • developers who have built rule engines for tax or benefits and have opinions on how to keep statute and code in sync.

The demo is not tax advice, and it does not replace VMI or a licensed adviser. It is a calculator that shows its working and knows where to stop.

Try it, then try the others

Run your own numbers at aionanalytics.in/lt-tax-demo.html, and tell us in the box at the bottom what it got wrong. If you are interested in the other things we build, including the free daily NIFTY 50 band from Veritas and the live demo of our market intelligence API, everything is on aionanalytics.in.

Top comments (1)

Collapse
 
danorie profile image
MinSoo Kim •

The 1 May deadline example is the best part. Writing "it moves to the next working day" would have been easy. Probably right, too. You still didn't.

I ran into a smaller version of this with a flooring calculator. Everyone "knows" you add 10 percent for waste, so we went looking for where the rule comes from. Shaw's vinyl planning page, read on 2026-09-10, gives no waste percentage at all. Nothing. So the calculator labels the allowance as the user's own input, not a recommendation, and the assumption line in the result says so in plain words, with the page repeating it, because a footnote is exactly where a rule like that goes to hide and get mistaken for advice.

One question. Does the "What it will not tell you" section grow from real user questions, or did you write it up front? I'd expect the best entries to come from people asking the thing you deliberately left out.