Inspiration Every grant a small nonprofit wins arrives as two documents that never meet. The first is a PDF full of conditions: which costs are allowed, which budget line they belong to, when the money can be spent and when the report is due. The second is the bank feed, where real spending shows up as terse, messy text that follows none of those rules. Between the two sits a bookkeeper, part-time or a volunteer, translating contract language into ledger entries from memory.

That translation gap is where grants go wrong. A cost charged to the wrong line, a purchase that needed prior approval, a salary split that does not match time records: none of these require bad intent, and all of them can surface months later in an audit, when the money is already spent. Large organizations buy their way out with finance teams and enterprise software. Small nonprofits, where a single grant can be most of the budget, cannot.

We built Steward at Cal Hacks 13.0 to close that gap. The failure is not effort: the rules live in prose, the spending lives in data, and nobody re-reads forty pages of contract for every transaction. A system that reads the contract once and checks every dollar against it turns bookkeeping from a quarterly scramble into a daily habit. Because the stakes are real money and real compliance, we set one non-negotiable rule from the start: the AI proposes, deterministic rules verify, and a human approves. Steward can never approve its own work.

What it does Steward is an autonomous grants accountant for small nonprofits. It does five things, end to end. First, it reads grant agreements. Upload a grant PDF and Steward extracts the terms a bookkeeper would underline by hand: budget lines, the period of performance, unallowable categories with clause references, prior-approval thresholds, indirect cost caps and the report due date. Second, it codes the bank feed. Each transaction is proposed a grant and budget line, or marked unrestricted, with a plain-English rationale and a confidence score. Payroll is not guessed by the model: it is coded deterministically from staff time-and-effort allocations, so a salary split matches the time records behind it.

Third, it reconciles four sources that normally disagree: the bank, the bookkeeper's general ledger, the contracts and the staff allocations. Deterministic rules flag deposit mismatches between bank and ledger, duplicate charges, out-of-period costs, effort overcharges, budget lines running over their allowed variance, and costs in categories the contract prohibits. Every flag cites its evidence, including the exact clause.

Fourth, it turns exceptions into work. Findings land in an inbox as case files. Each case shows the charge, the proposed coding, the evidence chain and a suggested correction expressed as a balanced double-entry journal entry. A reviewer can open "Ask why" and Steward defends the decision from its evidence, distinguishing what the contract says from what it inferred. Fifth, it drafts the funder's budget-vs-actual report from the decisions themselves, line by line, instead of asking anyone to retype numbers at the reporting deadline.

On synthetic data in our development environment, this loop has run end to end. 69 bank transactions, 69 ledger rows, 5 staff allocations and 2 precedents loaded, both grant PDFs extracted, and the first automatic coding run finished cleanly. The demo case, a bar tab (the Black Cat deep link), resolved to a blocked decision citing clause 4.7, with a balanced $482.50 correction proposed. Approving it as a human took the inbox from 10 open cases to 9, and Undo reopened the case and returned it to 10. How we built it

Steward is a Next.js application with a Convex backend. Convex holds the data, file storage, scheduled functions and the agent actions; the Next.js interface is the reviewer workspace: overview, inbox, case files, fund breakdown, approvals, report draft and a custom upload tab, plus a command palette. The intelligence lives in one pipeline, lib/pipeline.ts, built on a single principle stated in its header: model proposes, deterministic rules verify, human approves. The pipeline runs the same way inside a Convex action, a script, or a test with a fake model.

We use two model tiers through the Anthropic API. A fast model (claude-haiku-5-5 in lib/llm.ts) codes transactions in batches, receiving each row with its ledger history and grant context. A stronger model (claude-sonnet-5-5) handles contract extraction, exception reasoning and defending decisions under questioning. Every model response is validated against a Zod schema through structured outputs. If validation fails, the call retries once with the validation error attached; if it still fails, the item is marked needs_review for a human. There is no code path where a failed model call becomes a fake success. Around the models sits the deterministic layer, lib/rules.ts, which runs with zero model calls and never changes data. It checks period, allowability against each grant's own prohibited list, prior-approval thresholds, duplicates above a $250 materiality floor, effort against time records, line overruns against budget plus variance, and bank-versus-ledger agreement. Money is integer cents everywhere internally, so arithmetic is exact and journal entries balance by construction.

Approval is a separate, guarded step. Proposed entries carry a state machine from proposed to approved or rejected, and the server rejects approval attempted under Steward's identity, rejects approvers approving their own ledger entries, and writes every change to an audit log with who, when, what, why and the evidence references. In the interface, acting as Steward disables the review actions entirely, so the segregation of duties is visible, not just enforced.

All demo data is synthetic: a fictional organization, two fictional grants, generated bank and ledger files, and prior human rulings stored as precedents that the pipeline can cite. A visible "Synthetic demo data" badge appears whenever demo data is loaded.

Challenges we ran into The hardest problem was trust boundaries. A language model is an excellent reader and a careless accountant, so we had to decide, line by line, what it was allowed to decide. Extraction could be a model task because its output is schema-shaped and checkable. Coding could be a model task because deterministic rules re-verify every proposal. Arithmetic could never be a model task, so totals, splits and balancing are computed in code. That boundary took several redesigns of the pipeline before it stopped leaking.

The Black Cat case taught us the boundary from the other side. Our deterministic rules deliberately do not catch a bar tab: the keyword heuristics for prohibited categories miss it, and it falls to the model's exception reasoning to connect a venue name to an alcohol prohibition and cite the clause. In our offline rules test it is the one miss in a recall of 11 out of 12, the clearest illustration of why the system needs both layers, not either one alone.

Approval integrity was subtler than expected. A QA pass found that undoing an approval restored the journal entry but left the inbox case resolved, because the two records are separate. The fix, now in the codebase, makes undo clear the approval-only inbox resolution while preserving independent resolutions and the audit history. A related set of async race fixes makes deep links, approval results and Undo feedback survive slow mutations instead of resolving against stale screen state. Finally, honesty in tooling bit us twice. A fresh checkout failed TypeScript until the framework's type generation step ran, which our check list initially omitted. And with only a development deployment behind us, we learned to state exactly which results came from synthetic development data and which, like a public production deployment, had not happened yet.

Accomplishments we are proud of We are proud that the full loop actually runs, on synthetic development data, with no stage faked: contracts extracted from real PDF files by the model, a complete automatic coding run finishing with no errors, a rules engine verifying proposals independently, a blocked bar tab defended in plain English with its clause cited, and a human approval and undo that move the inbox exactly as the audit log says they should.

We are proud of the segregation of duties, because it is enforced in three places a judge can test: the interface disables review actions while acting as Steward, the server rejects Steward as an approver, and it rejects a person approving their own ledger entry. The system demos its own refusal. We are also proud of the evidence chain. Every decision carries its sources: a contract clause, a ledger row, a time record, a precedent, or a human. "Ask why" is not a chatbot bolted on; it answers from that chain, and in testing it correctly separated what the contract stated from what the model inferred about a venue. That distinction is the difference between a tool a bookkeeper can defend to a funder and one they cannot.

What we learned We learned that generative AI in a financial workflow is a translation layer, not an authority. It is superb at turning a clause into a structured rule and a cryptic bank memo into a probable coding. It is the wrong tool for adding columns of money, deciding what is allowed, or approving anything. Systems that respect that split get the model's reading ability without inheriting its confidence.

We learned that failure has to be a first-class state. needs_review is not an error screen; it is the product working as designed, routing uncertainty to the human it belongs to. Confidence thresholds, one schema retry and visible error states did more for trustworthiness than any prompt tuning.

And we learned that in compliance software, the explanation is the interface. A flag without a clause citation is just an accusation. A correction without a balanced entry is just a suggestion. The work that mattered most was the unglamorous plumbing that connects a decision back to its evidence.

What is next for Steward The immediate next step is production: a guarded deployment workflow for Vercel and Convex production is written, and the remaining work is verifying production access, deploying, and running the full smoke checklist against a public URL, including the bank feed, report and scheduled deadline checks that have not yet been exercised live. A demo passcode is not authentication, so real user accounts and per-organization data isolation come before any real nonprofit touches it. After that: journal posting is out of scope today, Steward proposes entries and stops, and closed-period enforcement is in our principles but not yet implemented or verified. Both are prerequisites, along with a completed sealed evaluation, before Steward should be trusted near real books. Then the work widens: more bank CSV formats, scanned and imperfect PDFs, recurring grant calendars, and a pilot with one small nonprofit whose bookkeeper reviews every proposal while we measure what the review actually catches.

Built With

Share this project:

Updates

Submission history