Inspiration
Small nonprofits sit on the exact data that unlocks grant funding. Families served. Pounds distributed. Cost per meal. Year over year growth. Funders ask for these numbers on every application.
The problem is where that data actually lives. It lives in a spreadsheet where dates are written three different ways, where a family count says "roughly forty," where the same week appears twice with two different numbers and nobody remembers which one is right. The correction lives in a volunteer's text file, written in plain English, months after the fact.
A two person food bank cannot afford a grant writer to dig those numbers out. They also cannot afford to get them wrong, because an unverified figure in front of a funder is worse than never applying at all. Once a program officer catches one number that does not hold up, the whole application is suspect.
That gap, between having the impact and being able to prove it, is the gap between getting funded and not.
What it does
Three agents run in one direction, each trusting only what the one before it verified.
The Analyst reconciles the mess. It cleans inconsistent dates, sites, and number formats. It resolves duplicated and conflicting weekly counts using the volunteer's notes as the source of truth. It drops rows that fall outside the reporting period. When something is genuinely ambiguous, it flags it for a human instead of guessing. Then it derives the funder grade metrics from what survived.
The Writer takes those same verified metrics and reshapes them for one specific funder, leading with whatever that funder cares about most. A hunger relief foundation sees families served. A logistics funder sees cost per meal. Same numbers, different lead.
The Matcher checks the organization's profile and metrics against a database of real grant programs, criterion by criterion, and sorts every program into four honest outcomes: match, every criterion we can verify passed. needs_review, nothing failed, but something could not be verified from the records. reject, a specific nameable criterion failed. indirect, real money, but routed through an intermediary or paid in kind rather than granted directly.
The idea underneath all three is analyze once, then reshape and check against it as many times as needed. The funder name and the organization profile enter at the Writer and the Matcher, never at the Analyst, so the numbers get computed a single time and every retelling traces back to that one verified pass.
How we built it
Built on the Strands Agents SDK, with a provider seam that switches the whole system between the Anthropic API and Amazon Bedrock on one environment variable. The agent code does not change between them.
The architectural decision that shaped everything: no language model ever decides what a number is.
Every metric comes from a deterministic engine written in the Python standard library. Every grant outcome comes from an eligibility module that evaluates each funder's published criteria against the record in code. The agents decide when to call these tools and they narrate why, but the values themselves are computed, never generated. The engine is covered by tests that check it against a hand computed ground truth, so the validation script either prints ALL CHECKS PASSED or the build is wrong.
The tools pass work to each other through small JSON artifacts on disk rather than through the model's context, which keeps every figure traceable to a source row.
On top of that sits a FastAPI upload interface so anyone can bring their own records, deployed as a container on Amazon ECS Express Mode, with the image in ECR and optional report hosting in S3.
Challenges we ran into
Stale state could have quietly served the wrong numbers. The tools read intermediate JSON files from disk without knowing which run wrote them. Someone uploading their own records could have received a report built from a previous run's data, presented as verified fact, on a project whose entire promise is that it refuses to fabricate. The fix was an isolated scratch workspace, wiped before every run, wired through environment variables read at import time.
The web layer nearly broke the demo path. The upload interface originally wrote straight into the repository's own data directory, which meant testing the web app would overwrite the sample data and break the ground truth check. Isolating the web workspace from the repository data solved both problems at once.
Null is not missing data. In the grant engine, a null value on a field like years operating does not mean "no." It means "we could not verify this," and it deterministically routes that grant to needs_review with an explanation of what would settle it. When we built the upload form, the obvious move was to force yes or no answers. That would have silently destroyed the honesty guarantee. The form instead offers "Not recorded" as a first class answer, and says so in the interface.
Deployment. The original plan targeted Bedrock AgentCore, but AgentCore Runtime serves an invocation endpoint rather than HTML, so it could not host a browser facing upload interface. We moved to ECS Express Mode and pinned the service to exactly one task, because the job store lives in memory in a single process and a second container would answer status polls for jobs it never ran.
Accomplishments that we're proud of
The moment that defines this project is a grant the Matcher refused to approve.
It did not say yes. It did not say no. It said the partner food bank affiliation required by that program is not recorded in our profile, that applications for it route through partner agencies, so the affiliation has to be confirmed first, and that the next step is contacting the regional food bank to confirm partner agency status.
Most tools would have guessed yes or quietly dropped it from the list. Being able to say precisely what is missing and precisely what would resolve it is more valuable to a small nonprofit than a longer list of maybes.
What we learned
Trust is an architectural property, not a prompt instruction. You can tell a model not to fabricate numbers, and it will mostly comply. But "mostly" is not a foundation to hand a nonprofit whose funding depends on it. Moving every figure into deterministic code and leaving the model to explain rather than calculate is what makes the guarantee real.
We also learned that the honest answer needs somewhere to live. Every layer, the engine, the agent prompts, the upload form, and the report itself, has to preserve the distinction between "no" and "we could not verify this." Collapse that distinction anywhere in the stack and the whole promise collapses with it.
What's next for Development Office
Surfacing the grant eligibility results inside the report document itself, so the needs_review reasoning travels with the numbers. Expanding the verified grant database beyond its current programs. Adding support for the messier real world formats that small nonprofits actually keep their records in, and broadening it past food banks to any small nonprofit sitting on data it cannot prove.
Built With
- amazon
- amazon-bedrock
- amazon-s3-buckets
- amazon-web-services
- aws-fargate
- boto3
- claude-api
- css
- docker
- ecs
- fastapi
- git
- github
- html
- javascript
- python-package-index
- strands-agents-sdk
- tavily-api
- uvnicorn

Log in or sign up for Devpost to join the conversation.