Inspiration
Donors who want to give beyond the same two or three brand-name charities have no real way to vet the hundreds of smaller nonprofits that might fit them better. We wanted to see if an agentic system could do that vetting the way a thoughtful friend would: not a keyword search, but real reasoning about whether a specific nonprofit's mission actually matches what a specific donor cares about. Jac's graph-native model felt like the right fit, since donor-to-cause-to-nonprofit is naturally a graph traversal problem, not a flat lookup.
What it does
AlmsMatch takes a donor's profile (cause areas, budget, geography, values, exclusions) and spawns a Jac walker that traverses a graph of nonprofits by cause. At each candidate, a byLLM-powered ability scores fit and writes a rationale grounded in that nonprofit's actual mission text, not generic knowledge about the org. The donor gets a ranked shortlist with honest caveats, not just a score, and can commit a pledge that's written back into the graph.
How we built it
AlmsMatch is built almost entirely in Jac, an AI-native graph language from Jaseci Labs. We modeled the problem as a graph from the start: Investor, Nonprofit, CauseHub, Region, and Pledge nodes, connected by typed edges (CaresAbout, TaggedAs, OperatesIn, PledgedTo). A MatchFinder walker spawns at the donor node and physically traverses two hops — donor to cause, cause to nonprofit — gathering candidates as it goes.
We deliberately split the scoring into two stages. First, plain Jac arithmetic runs a cheap deterministic pass on every candidate: cause-tag overlap, geography containment, and a leverage score computed as basis points (how much of the org's actual annual budget a $10K gift represents). Only candidates that clear a floor on that score get an expensive by llm() call — a typed FitAssessment object where the model reads the nonprofit's real mission text and writes a grounded score, rationale, and honest concern. We blend the two signals 60/40 in favor of the LLM read, and show both numbers in the UI so the graph work stays visible, not hidden behind a single AI-generated verdict.
The frontend is a single static HTML file that talks to a jac start REST API — no separate backend framework, which keeps the Jac percentage of the codebase high and the architecture simple.
Challenges we ran into
Our Jac knowledge going in was stale relative to the installed version (0.16.7) — several syntax patterns we expected turned out to be wrong in ways that would have quietly broken the demo. disengage halts an entire walker and drops its queue, so using it in our prefilter would have silently killed the traversal after the first rejected candidate instead of continuing to the rest. We used skip instead, and reserved disengage correctly for our other walker, PledgeCommitter, which is a genuine find-first-match pattern.
The scarier bug was invisible until we went looking for it: our API server had been running since before we finished wiring in our Anthropic API key, so it was silently serving the deterministic-only fallback on every request — 0 of 6 candidates were getting real LLM calls, with no error surfaced anywhere except a quiet "AuthenticationError, kept X/100" note buried in the results. A stale process with a stale environment is exactly the kind of failure that looks like a working demo until a judge asks "wait, is this actually using the model?" We caught it by manually clicking through our own product rather than trusting our automated tests, which had passed the whole time — a good reminder that unit tests and a live click-through catch different classes of bugs.
We also chased a phantom bug: our demo page appeared to reset itself a fraction of a second after loading test data. We initially suspected a form-submission bug in our own HTML. It turned out to be our local dev server's auto-reload feature reacting to file edits, not our code at all — confirmed once we opened the page via a plain file:// URL and the problem vanished.
Accomplishments that we're proud of
The clearest one: a $80M-budget, brand-name nonprofit (Rainforest Trust) gets demoted out of our donor persona's top 5, purely because the model reads its actual mission text and recognizes it's about land acquisition and legal protection, not carbon reduction — while a $3M food-rescue nonprofit with a genuinely matching operational mission rises to the top. That's not a scripted outcome; it's the system doing exactly what we set out to build: reasoning grounded in real mission text over reputation.
We're also proud that every score in our results is auditable, not a black box — a judge (or a donor) can see the graph-computed score, the LLM-computed score, and the exact blend between them for every single match.
What we learned
That the gap between "backend tests pass" and "the live product actually works" is real and worth respecting even under time pressure — our two worst bugs today (the stale-server auth issue and the reload mystery) both hid completely from our automated test suite and only surfaced once we did a manual, end-to-end click-through. We also learned to trust installed, version-matched documentation over recalled training knowledge or web-search results, since Jac's syntax had moved meaningfully since our initial assumptions.
What's next for AlmsMatch
Persistent donor history — right now every match run starts fresh, but we already write Pledge nodes into the graph, so surfacing a donor's full giving history and reasoning about their next gift in that context is a natural next step. We'd also like to add a portfolio-level view: instead of ranking nonprofits one at a time, help a donor see whether their top matches are actually diversified across cause areas and operating regions, or quietly concentrated in one theory of change.
Built With
- anthropic
- byllm
- claude
- css
- graph-database
- html
- jac
- jaclang
- javascript
- litellm
- python
- rest-api