markdown 💡 Inspiration We started this project after talking to volunteers at a local women's center. Every day they receive donations — food, clothing, hygiene products, baby supplies — and every day they face the same impossible question:

"We just got 10 cans of baby formula. Five families need it. Who do we give it to first?"

The answer usually came down to whoever the volunteer remembered most recently, or whoever shouted loudest. Not because anyone wanted unfairness — but because there was no system. Spreadsheets got out of date by lunch. Paper logs got lost. And the people who needed help most weren't always the loudest.

We realized this is exactly the kind of problem AI is built for: complex multi-variable decisions made under uncertainty, repeatedly, with empathy. So we built WellSpring AI.

🎯 What it does WellSpring AI is a donation management system that helps women's centers track, allocate, and report donations using natural language and Claude-powered intelligence.

Three things make it different:

  1. Natural-language intake — Volunteers don't fill forms. They type "got 3 boxes of canned food, 2 winter coats size L, and 10 cans of baby formula" and Claude parses it into structured inventory in under a second.

  2. Family-aware allocation — When supplies are scarce, the system automatically merges related beneficiaries into allocation units so a household of four doesn't displace four separate individuals. When supplies are plentiful, units split back into individuals. This is the core innovation.

  3. Privacy by design — Real names never reach the AI. All identifiers are anonymized hashes. Family relationships are stored in a separate table from personal data, following the principle of least privilege.

🧠 How we built it The priority engine
The heart of the system is a weighted scoring model that evaluates each allocation unit across four dimensions:

[ \text{Score} = 0.40 \cdot M_{\text{need}} + 0.25 \cdot W_{\text{wait}} + 0.20 \cdot U_{\text{urgency}} + 0.15 \cdot V_{\text{vulnerability}} ]

Where:

  • ( M_{\text{need}} ) — does this donation match what the unit actually needs?
  • ( W_{\text{wait}} ) — how long since their last allocation? (normalized to 30‑day window)
  • ( U_{\text{urgency}} ) — caseworker‑assigned urgency on a 1–5 scale
  • ( V_{\text{vulnerability}} ) — weighted indicator for infants, elderly, or disabled members

Family unit merging
When stock falls below 1.5× total demand, the system enters scarcity mode and runs a Union‑Find algorithm over the family relationship graph:

function buildAllocationUnits(beneficiaries, relations, scarcityMode) {
  if (!scarcityMode) {
    return beneficiaries.map(b => ({ type: 'individual', members: [b] }));
  }
  // Union-Find collapses connected components into family units
  const parent = new Map();
  beneficiaries.forEach(b => parent.set(b.id, b.id));
  relations.forEach(r => union(r.beneficiary_a, r.beneficiary_b));
  // Group by root → return as allocation units
}
So three beneficiaries A, B, C with equal individual priority — where A and B share a household — become two units: [A,B] and [C]. Each unit gets one allocation slot.

Claude as the explanation layer
Cold scores aren't enough. A volunteer needs to know why the system suggests [A,B] over [C]. So after ranking, we send the score breakdown to Claude (claude-sonnet-4-6) and ask it to write a one‑sentence justification in the voice of an experienced coordinator:

"Family unit BNF‑A3F2/B7K9 ranks first: they have an infant, were last served 3 weeks ago, and the donation matches their flagged urgent need."

This is what makes the system feel human, not algorithmic.

Stack

Frontend: Next.js 14 (App Router) + React + TypeScript + Tailwind CSS + shadcn/ui

Backend: Next.js API routes

Database: Supabase (Postgres) with Row‑Level Security policies for privacy

AI: Anthropic Claude API for NLP parsing, recommendation explanation, and report generation

Deployment: Vercel

Versatility on unseen cases
Because Claude's NLP layer handles parsing, the system works on donation categories it has never explicitly seen. "Got a gently used breast pump and three packs of nursing pads" — never in our training data, but Claude correctly tags it as baby and matches it to beneficiaries with has_infant: true. This is the generalization the AI/ML track explicitly asks for.

🧗 Challenges we ran into

Family relationships are sensitive data. Our first design stored relationships inside the beneficiary record, but that meant any field‑level access leaked household structure. We refactored to a separate family_relationships table with stricter access policies — relationships are only visible to the priority engine, not to volunteer dashboards.

The "fair to whom" problem. Should a family of four count as four allocation slots or one? We went back and forth. Counting as four meant a single household could absorb a whole donation. Counting as one penalized large families. Our compromise — dynamic merging based on scarcity — only collapses families when supplies are constrained, preserving individual access when there's enough to go around.

Row‑Level Security vs. API access. Supabase's RLS blocked our API routes from writing data because they had no JWT. We solved it by giving server‑side routes a service role key and keeping the browser on the anon key — the browser literally cannot write data, only read filtered views. This turned a deployment headache into a stronger security model: even if the public API key leaks, no one can mutate the database.

Hallucination on parsing. Early prototypes had Claude inventing quantities ("about 3 boxes" → quantity: 3.5). We fixed this with a strict system prompt requiring integer quantities and a JSON‑only output format, plus a validation layer that rejects malformed parses.

🏆 Accomplishments we're proud of

The family unit algorithm works. In testing with 5 beneficiaries and a 2‑person family unit, scarcity mode correctly collapsed them into a single allocation slot, preserving fairness for individuals.

Volunteers can use it without training. No forms, no dropdowns — just natural language. We tested with non‑technical users who logged a donation in under 30 seconds.

Claude's explanations are genuinely good. They sound like an experienced coordinator, not a chatbot. Privacy holds — no names ever reach the model.

Sub‑second latency for parsing. Real‑world usable, not a demo trick.

The reports Claude writes are stakeholder‑quality. Not bullet lists — actual analytical prose with category percentages and recommendations.

📚 What we learned

AI is at its best as a layer, not a replacement. The deterministic priority math gives us auditability; Claude gives us empathy and natural language. Neither alone would work.

Privacy is architectural, not bolted‑on. Splitting the family relationship table and routing all writes through a service‑role API was the most important design decision we made.

Real users care about edge cases more than features. "What happens when two families have the same priority?" matters more than fancy charts.

claude‑sonnet‑4‑6 is fast enough for interactive UIs. We were surprised — full parse‑and‑respond round trips averaged under a second.

🚀 What's next for WellSpring AI

We've already shipped full multi‑language support — the entire UI, Claude's allocation explanations, and generated reports switch instantly between English, 中文, and Español based on user preference. No translation layer, just native Claude reasoning.

Next on the roadmap:

SMS‑based intake — many partner organizations don't have laptops on hand at donation sites. A simple SMS gateway would let drivers text donations as they arrive.

Federated deployment — let multiple women's centers share aggregate insights ("region‑wide formula shortage incoming") without sharing personal data.

Outcome tracking — close the loop by letting beneficiaries optionally confirm what they received, feeding back into the priority model.

We hope WellSpring AI becomes infrastructure that real organizations can deploy — not just a hackathon demo. The code is open source, the data model is documented, and the AI layer is fully replaceable.

Built With

Share this project:

Updates