DEV Community

Cover image for Before you give: hear what a nonprofit's tax filings say, and what they can't
Vinicius Pereira
Vinicius Pereira

Posted on

Before you give: hear what a nonprofit's tax filings say, and what they can't

DEV Weekend Challenge: Generosity Edition Submission πŸ’œ

This is a submission for Weekend Challenge: Generosity Edition

What I Built

Type the name of a US nonprofit and hear, in a minute or two, what its public tax filings say: how much money moved, whether it spent more than it took in, how many months of reserves it holds, where the money comes from, and how that moved over the last decade. Then, in the same breath, what those filings can't tell you.

There's no score. That was the whole design decision.

Every charity rating site ends in a number, and the number has to weigh things the filing doesn't measure. A Form 990 records what came in and what went out. It doesn't record whether the work is any good, whether the reserves are locked to a purpose, or what happened after the fiscal year closed. A score built on it is a guess wearing a number, and a donor who trusts the number is trusting the guess.

So the unit here isn't a score. It's a fact with a limit attached:

Reserves about 12 months
Net assets of $3 billion would cover about 12 months of spending at the current pace.
limit: Net assets include buildings, equipment and gifts restricted to a purpose,
so the part it could actually spend is smaller. Treat this as an upper bound.

The limit is a required field. The code can't produce a fact without one.

The generosity angle is simple: give better. Most of us donate on impulse or on a friend's word. This gives you one minute of what's actually on file, in words you can take in on the way to deciding, and it's free, keeps no account, and rates nobody.

Demo

Live: https://before-you-give.vercel.app

Try "American Red Cross", then "Doctors Without Borders", then a small food pantry in your state. The three feel completely different once you hear them.

A real session: search Feeding America, open the reading, press play, scroll to the year by year chart

The American Red Cross reading

The voice runs on a spending cap I set for the weekend. If it tells you the budget is used up, the full narration is written out under "Read the narration" and everything else still works.

Code

GitHub logo vinimabreu / before-you-give

Hear what a nonprofit's public tax filings say before you donate. No score, no guessing.

before-you-give

ci python license tests

Type the name of a US nonprofit and hear, in a minute or two, what its public tax filings say: how much money moved, whether it spent more than it took in, how many months of reserves it holds, where the money comes from, and how that has moved over a decade. Then, in the same breath, what those filings cannot tell you.

No score. No ranking. No language model writing the numbers.

Live: https://before-you-give.vercel.app

Searching for Feeding America, opening the reading, pressing play, and scrolling to the year by year chart

A real session on the live site: search, pick the organization, listen, follow the narration, scroll to the chart. The same clip as an MP4: assets/demo.mp4.

Screens

Landing Reading
Landing page The American Red Cross reading
Year by year Phone
Year by year chart and table A food pantry on a phone, narration playing

Why there is no score

Every charity rating site ends in a number, and the number has to weigh things the filing does not measure. A Form 990 records what an organization took in and what it spent. It does…

MIT. Python, FastAPI, one HTML page with no build step. 82 tests that run offline against real API responses, no key needed.

How I Built It

Data first, code second. Every number comes unchanged from the ProPublica Nonprofit Explorer API, which republishes IRS Form 990 extracts. Before writing any logic I pulled six real organizations and saved the responses as fixtures: a big 990 filer with 13 years of history, a private foundation (990-PF) that reports the same lines under different names, a food pantry on the short 990-EZ form, another on the full form, and one organization with no filings at all. The code was shaped by what the feed actually returns, not by what I assumed it would. Every field is optional and every fact only exists if its inputs do.

Facts with limits. reading.py is the whole arithmetic. A Fact has a display value, a plain sentence, the same sentence written for a voice, and a limit. The limit is what stops the page from lying by layout. When the feed doesn't carry the program / admin / fundraising split (it's on the filing PDF, not in the API), the page says so instead of pretending. When an organization has no numbers, the page explains that small organizations file a postcard and churches don't have to file at all, because absence of data is not a warning sign and a blank page would imply it is.

Two renderings of every number. $3.2 billion for the eye, 3.2 billion dollars for the voice. A text to speech engine reads $3.2B five different ways and none of them is what a listener wants. There's a test that walks every narration and asserts there is no $ and no % anywhere in it.

The narration is templates, not a language model. The script is assembled from the facts by plain string code, so it can't drift from the data even a little. It ends with the source line every time.

ElevenLabs, with the guardrails written before the first paid call. The narration renders with eleven_turbo_v2_5. Two rules sit in front of it:

  • cache by text: the mp3 is stored by the hash of the script, so the same organization never costs twice;
  • a hard cap on calls and characters, checked before every request. Past the cap the server answers 429 and keeps serving what's already cached. A small per-address limiter sits in front of that.

Without a key the app runs text-only and says so on the page. Nothing else changes.

On the voice itself: I spent part of this week tuning ElevenLabs settings on a different project, and the lesson carried over. The voice you pick matters less than stability and style. Defaults sound fast and flat; too much style and it starts oscillating between moods mid-sentence. For narration I landed on stability 0.6, style 0.2, speed 0.95, and it reads a balance sheet like a calm person, which is the point.

What the data told me that I didn't expect. The American Red Cross's fundraising events lost money in fiscal 2023: $3.1 million in, $4 million to run. My first version printed "kept -27%", which is true and useless. Now it says the events cost more than they raised, and the limit line notes that events often exist to build a community as much as to raise money. Also: a $3 billion organization holding about 12 months of reserves, and Doctors Without Borders USA running an $89 million deficit with 4 months of reserves and 97% of revenue from donations. None of that is a verdict. It's what's on file, and now you can hear it.

What I didn't build. No scraping of the filing PDFs for the expense split, no score, no "recommended" list. A weekend is enough to be honest about a data feed; it isn't enough to be right about a cause.

Prize Categories

Best use of ElevenLabs. The voice isn't decoration here, it's the accessibility layer. The people this is for are not the people who read walls of numbers. Every figure is formatted for speech, the script is deterministic so the audio can't say something the page doesn't show, and the cache and cap are what make it responsible to leave a public demo running on a personal key.

If you try it on a nonprofit you care about and the page gets something wrong, open an issue. The numbers are ProPublica's; the sentences are mine, and I'd rather fix a sentence than defend it.

Top comments (0)