What Do You Owe Society? Building an Honest Lifetime Contribution Calculator

A calculator that weighs what society spent on you against what you gave back, year by year, with every assumption on show and no data kept.

Share
A balance scale with schooling, healthcare, shared services and social protection on the Received side, and paid work, unpaid care and volunteering on the Given side
💡
TL;DRI built Lifetime Contribution Balance. It asks six short sets of questions and estimates what society has spent on you (schooling, healthcare, shared services, social protection) against what you have given back (paid work, unpaid care, volunteering).The model is one line, LCB(A) = Contribution(0..A) − Consumption(0..A). The engine applies it to every year of a life, using published national statistics.Every number comes from a versioned "coefficient pack". Each one carries its source, its year and a label saying how it was obtained, and every result shows an honest range, not a single falsely precise figure.There is no database. Your answers stay in the browser, and the API is a pure function. The tool also says in plain words that it must never be used to make decisions about people.

The dinner-table question nobody can answer

Sooner or later, at most dinner tables, someone says it. "I've paid in my whole life and got nothing back." Or the darker version: "People like them just take." Everyone has an opinion. Nobody has a number.

I kept wondering about the honest version of that question, asked of myself. Since the day I was born, I have been given a lot: years of school, doctors, roads, streetlights, a safety net I may never have noticed. I have also given back through work, through looking after people, and through the odd Saturday helping out. How do the two actually compare?

It turns out you can estimate this. Governments publish what they spend per student, per patient and per person. Statistics offices publish how much an hour of work produces. What was missing was something that puts it all together for one life, shows its working, and refuses to pretend to be more certain than it is.

What I built

Lifetime Contribution Balance is a small web app with a calculation engine behind it. You answer six short steps (You, Learning, Work, Home, Giving, Support), and every question has a sensible default, so you can skip any of them. The engine walks through your life one age-year at a time and returns a balance to date, an honest range around it, a break-even age, and two extra accounts that are deliberately never folded into the money.

Diagram: six questionnaire steps feed an engine that walks every age-year using a versioned coefficient pack, producing a balance, an unpriced account and an environmental account
Six questions in, one honest balance out. The unpriced and environmental accounts stay separate from the money.

The welcome screen sets the tone before you answer anything: "Not a score of your worth. Not a test you pass." That line shaped almost every design decision that came later.

The model in one line

Here is the whole idea:

LCB(A) = Contribution(0..A) − Consumption(0..A)

In plain words: add up everything you have given from birth to age A, subtract everything you have received, and the gap is your balance at A. Think of a bank statement for your relationship with society, with one row per year of your life.

The engine builds that statement literally. It turns your answers into a timeline with one slice per age-year. For each slice it asks where you lived, which calendar year it was, and whether you were studying, working, caring or retired. It then runs eight "modules" and adds up the results. Years after today use projected figures plus your own retirement and salary assumptions.

Illustrative stacked bar chart: received bars dominate childhood and old age, given bars dominate working years; a running balance line dips during education, crosses zero at a break-even age and falls again in retirement
The typical shape of a life (illustrative, not real data). Childhood is mostly received, working years mostly given, and later life tips back again.

That shape is the most useful thing to understand about the tool. A negative balance at 25 is not a verdict. It is just the dip after years of school, before a working life has had time to happen. The results page puts it plainly: being short at that age is "entirely ordinary."

What counts on each side

Received (consumption)Given (contribution)
Everyday living: household spending, shaped by age, lifestyle, housing and transportPaid work: the value your hours add to the economy
Education: spending per student at each stage, in the country where you studied itUnpaid care: hours spent raising children or caring for adults
Healthcare: health spending per person, curved by ageVolunteering: hours given, priced at a replacement wage
Shared public services: policing, roads, environment, culture and so on
Benefits and pensions: social protection, plus unemployment and housing support where they apply

One detail matters a lot here. Paid work counts as value added, not as salary. The formula is roughly hours × output per hour × an occupation factor × the labour share. Labour share is the slice of output that goes to workers. Wages and taxes are just two ways of dividing up that same output, so adding either on top would count it twice. A dedicated test, test_double_counting.py, exists to fail loudly if that ever creeps back in.

How it works under the hood

Architecture diagram: browser talks to nginx on port 8080, which proxies /api to a FastAPI service on port 8000; the API reads the active pack from a packs volume; a methodology console and a build_pack tool can add new packs
Two containers and one volume. There is deliberately no database anywhere.

The whole thing runs as two Docker containers.

  • The browser app (React, Vite, Tailwind). It shows the questionnaire, a running balance that updates with every answer, and the results. Your answers live in a small in-memory store (zustand) in the page itself.
  • web, an nginx container on port 8080. It serves the built app and forwards anything under /api to the calculator. Because the browser only ever talks to one address, there's no cross-origin setup to get wrong.
  • api, a FastAPI service on port 8000, published only on localhost. Inside, a CalculationEngine walks the timeline, and a CoefficientService looks up each statistic for a given country and year.
  • The packs volume. The only thing saved on disk is a set of versioned coefficient packs: published statistics, with nothing about any person.

Two side doors exist for whoever maintains the numbers. A build_pack tool fetches fresh statistics from the World Bank, the OECD, ILOSTAT and Eurostat. It prints a diff and writes nothing unless you pass --publish. A small methodology console under /api/admin/packs can validate, publish and activate packs, and it is locked unless an admin token is set.

Follow one answer through the system

Here is what happens when you change a single answer, say your occupation, in the Work step.

Sequence diagram: browser posts the profile to /api/calculate through nginx; the engine builds a timeline, resolves coefficients for each age-year, runs eight modules, sums and adds a band, then returns JSON; the browser keeps only the newest reply
One answer, one round trip, nothing stored.
  1. The store updates. The page merges your change into the profile it holds in memory.
  2. It asks for a fresh estimate. The whole profile goes to POST /api/calculate. Only current_age and birth_country are required. Everything else falls back to a default.
  3. The engine builds your timeline. Your answers become one slice per age-year: where you lived, whether you were studying, working or caring.
  4. Each year looks up its numbers. For every statistic it needs, the engine asks the pack for a value for that country in that year. Back comes the number, its source, and a label saying how solid it is.
  5. Eight modules run, and the year's "given" and "received" are added to running totals.
  6. The totals get an honest range. The headline figures come with a 10th to 90th percentile band.
  7. The result comes back as JSON. Nothing has been written anywhere on the server.
  8. The newest reply wins. If you type fast, an older, slower reply is thrown away by a simple counter, so the meter never jumps backwards.
Questionnaire Work step with the running balance meter at the top
Step 3 of 6: the running balance at the top updates as you answer.

Four design decisions worth stealing

1. The cheapest way to protect data is not to hold it

The choice: no database, no accounts, no server-side copy of anyone's answers. The questionnaire lives in the browser, and the API is a pure function: the same profile in always gives the same result out.

Why: the questions cover health, benefits, caring, migration and income. Put together, that is close to the most sensitive kind of personal data there is. Rather than build retention policies and access controls around it, I chose never to have it. A locker you never fill can't be broken into.

The trade-off: close the tab and your answers are gone. There's no "pick up where you left off" and no shareable link yet. The decision log notes that later phases might add opaque result snapshots that expire, but never profile data attached to a person.

2. Put every assumption in versioned data, not in code

The choice: every statistic the engine uses lives in a coefficient pack, a JSON file with a snapshot id. Fetched values carry the dataset id, the exact request URL and the retrieval date, down to individual cells. Packs are immutable: you can't overwrite a version, only publish a new one and switch to it. Rolling back just means activating the previous id.

Each country-year value also gets a vintage label that says how it was obtained:

LabelMeaningEffect on confidence
L1_observedA published value for that country and yearFull
L2_interpolatedFilled in between two published valuesOne step lower
L3_backcastReconstructed from that era's income levelTwo steps lower
L4_assumedBorrowed from a peer country, scaledThree steps lower

Why: the uncertainty band widens on its own when the evidence is weaker. A 25-year-old's life sits mostly on recent, well-measured years. A 60-year-old's childhood sits on reconstructed ones, and the result shows it.

The trade-off: honesty is more work than it sounds. Beyond the per-country statistics there are roughly 220 further numbers, such as age curves, lifestyle multipliers and occupation factors, that nobody fetched. So GET /api/methodology marks every source credit as either fetched or typed to match, and names those hand-set blocks openly. As the decision log puts it, this does not close the gap. It makes it visible.

🔎
Key idea: a number without its source, its year and its strength of evidence is just an opinion in a nice font. Make the provenance part of the data, and the honesty takes care of itself.

3. When a choice is unavoidable, make it a switch

The choice: some questions have no single right answer, so the tool exposes them rather than hiding them. How much of defence or debt interest should one extra resident "use"? The engine splits government spending into three tiers. Health, education and social protection are modelled per person. Congestible services, which have queues, roads and bin rounds behind them, are shared per person at half weight. Pure public goods like defence count at zero by default. Three presets (marginal, congestible_only, average) are available, and the results page shows what each one would say.

Unpaid care gets the same treatment, with three named ways to price an hour: a generalist replacement wage, a specialist one, or your own forgone earnings.

Why: these choices move the result, and quietly picking one would be dishonest. The default allocation in particular avoids handing everyone in a high-defence-spending country a fixed deficit that nothing they do could change.

The trade-off: more knobs mean more explaining, so the interface leans on short help texts.

4. Write tests that guard honesty, not just code

The choice: some of the most important tests check claims, not functions. One fails if the double count of wages comes back. One asserts that marking a question as "answered" only narrows the uncertainty band and never moves the estimate itself. Another records every pack value the engine actually touches and compares that list with what the methodology page says it uses. It fails in both directions.

Why: documentation drifts silently. Nobody writes anything false. Things just stop being true. A failing build is the only reviewer that never gets tired.

The trade-off: these tests are fiddly to write. Two of them exist purely to prove the guard itself can fail, because a check you've never seen fail looks exactly like one that can't.

A tool with a warning label

A number like this could be misused, and I'd rather say that up front than pretend otherwise. The welcome screen, the methodology page and the PDF export all carry the same sentence: this estimate must not be used for hiring, lending, insurance, immigration or any other decision about a person.

To be clear, that guard is a stated rule, not a technical lock. A calculator can't know who is using it. What the design can do is avoid making misuse easy. It holds no data to mine and no profiles to look up, and its defaults start neutral rather than flattering or punishing. The methodology page says it directly: illness, caring, study and unemployment all move the number, "and none of them makes a person a cost."

Welcome screen with What it does, What it needs and What it is not
The welcome screen says what the tool is not before you start, and that nothing is stored.

What was hard, and what I learned

Double counting hides in plain sight. My first version multiplied an occupation factor by an education premium. But the published degree premium already includes the fact that graduates hold more productive jobs. A graduate professional came out at 1.35 × 1.40 = 1.89 times the average worker. Now the occupation sets the level, and education only nudges it where it differs from what that job usually needs.

One wrong label, a 43% error. Going back in time, money figures get rescaled by real income per head. That is right for spending, but I had also applied it to hours worked. Since output already carried the ratio, it landed twice: a German working year in 1970 was valued at 43% of what it should have been. The fix was a single set, NOT_INCOME_SCALED. Finding it took asking what "borrow from a peer country" really meant.

The model penalised a carer for being educated. Imagine someone who spent twenty-two years as a full-time carer and holds a master's degree. The model counted the cost of the degree and never credited anything back, because the education premium only raises paid work, and they had none. I fixed the opportunity-cost convention so it works without a salary on record. I also added a second, unpriced account that reports hours of care, years and people raised, never converted into money. The deeper lesson is that the model only sees flows, not what people become. That is still open.

People move. "Where did you study?" used to cost the whole school chain in one country. Primary spending per student is roughly ten times higher in Germany than in India, so anyone who moved got a figure wrong by multiples. Now each stage is costed where you were actually living at the time, read straight off the residence timeline.

Six countries, each chosen to break something. Germany, the Netherlands, the UK, the US, India and Argentina. The UK sits outside Eurostat, the US has private-majority healthcare, India has wide data gaps, and one of Argentina's price converters stops in 2021. Similar countries would have agreed with each other and proved nothing.

Results page with the headline balance, cards and the year-by-year chart
The results page: headline balance with its honest range, then given and received, year by year.

Try it yourself

The quickest way is the live version at play.sanyal.net/life-balance. There is nothing to install and no sign-up, and nothing you enter is stored.

To run your own copy instead, you only need Docker. No database, no environment file, no accounts.

docker compose up --build

Then open http://localhost:8080 for the app. The interactive API docs are at http://localhost:8000/api/docs. You can also call the calculator directly. Only two fields are required:

curl -s -X POST localhost:8000/api/calculate \
  -H "Content-Type: application/json" \
  -d '{"profile": {"current_age": 38, "birth_country": "DE"}}'

To see every source and assumption behind a result:

curl -s localhost:8000/api/methodology

For development, there are Make targets:

make up          # build and run everything
make test        # backend + frontend test suites
make dev-api     # FastAPI with reload on :8000
make dev-web     # Vite dev server on :5173

To open the methodology console, set a token first. Without one, every admin route returns 401, so the door is closed by default:

export LCB_ADMIN_TOKEN=$(openssl rand -hex 24)
docker compose up --build
curl -s -H "Authorization: Bearer $LCB_ADMIN_TOKEN" localhost:8000/api/admin/packs

What's next

The decision log keeps an honest "still open" list. In the order they will start to hurt:

  • Pricing an hour of care. The generalist wage is now sourced, but which skill level fits care work is still a judgement call. The specialist multiplier needs an activity-to-occupation mapping before time-use survey data can replace it.
  • The environmental shadow price. Footprints are reported in kilograms, cubic metres and square metres, with optional monetising via the US EPA social cost of carbon. Whether that is the right price is still open.
  • A default discount rate. Discounting works as a scenario, but the choice between roughly 1% and 3% moves the break-even age by decades, so no default is picked yet.
  • Bias review criteria. Deciding what "materially changes the result" means, before any data can influence that decision.
  • Human capital as a stock. Letting the model see what people become, not just what flows through them.

Closing thought

This project began as a philosophical conversation with my wife and a few very close friends. What do we owe to society? Have we done our share for the environment and for the people around us, or are we, honestly, net consumers? All of us come from different backgrounds, with different educations and different paths through life, and the question turned into one of the most engaging conversations we've had. Lifetime Contribution Balance was born from that evening.

It is not an exact calculation, and it never will be. But whatever the numbers say for any one person, my own sincere takeaway is this: most of us have taken more from society than we have given back to it so far. For me, that isn't a reason for guilt. It's a reason to keep giving.

If you try it, I'd love to hear what surprised you, or which assumption you'd argue with. Leave a comment below, and subscribe if you'd like the next build in your inbox.