Case study
ourmoney
Where did the reported rupee for my place go?
- Hackathon
- Build What Moves India
- Shortlisted from
- ~5,000
- Data
- Mock + live extract
- Languages
- English, Hindi
- RTI
- Drafts, never files
- Government API
- None, by design
- React
- TypeScript
- Vite
- Cloudflare Pages
- OpenAI via OpenRouter
- PWA
90-second story
I want a future where public money is accountable and transparent at every level, where anyone can see what a district office or a municipal body did with a reported rupee. Today that data sits in portals built for agencies, not citizens. For Build What Moves India (OpenAI × Varun Mayya) I built ourmoney: ask a plain question in English or Hindi, get a grounded answer, follow the rupee office by office, and draft an RTI for the records. No scraping, no government connection, and no gap is ever called corruption. It was shortlisted from about 5,000 participants. After the hackathon I added one cited MGNREGA extract next to the synthetic demo.
The story
I want a future where public money is accountable and transparent at every level. Anyone should be able to see what happened to a reported rupee, whether it sat with a district office or a municipal body.
We are far from that. A villager hears that money was sanctioned for the road. She cannot tell whether it is still at the state nodal account, was spent as admin, was sent onward, or is waiting on a late utilisation report. PFMS and scheme MIS portals publish fund-flow data, but they are built for agencies. Six acronyms, no data dictionary, inconsistent field names.
ourmoney is my first slice of that future. It answers one question in under a minute: where did the reported rupee for my place go?
I built it for Build What Moves India, the hackathon run by OpenAI and Varun Mayya. It was shortlisted from about 5,000 participants, then went through a Stage 2 resubmission with a fuller Ask, Explore, and RTI flow. It is live at ourmoney.anireco.app.
Constraints I designed around:
- Do not connect to, scrape, or probe government systems. The competition build ran on synthetic data only.
- A difference between money received and money reported is not a crime. Timing, balances, and late reports explain most gaps.
- It has to work on a phone on a slow connection, in Hindi as well as English.
- Be honest in the UI about what is simulated and what production would need.
Approach: what I considered
- Scrape live MIS and publish itRejected
Breaks the competition rules and becomes an unauthorized government mirror. Fragile the day a portal changes.
- Dashboard-first explorer as the landingRejected
Fine for a judge who wants to drill. A WhatsApp-native citizen does not start at a four-pane workbench, so it moved to Explore.
- Canonical fund-flow model, replaceable adapters, Ask-firstChosen
One citizen equation for every office. Any source that can fill the model plugs in without touching the UI.
How it works
In one sentence: You ask about your place, the app finds that office in a fund-flow tree, answers from that office’s numbers only, and lets you open the path, check the math, and draft a records request.
The flow in plain language
- Ask. Type or speak a question in English or Hindi, like “roads money at Uttar Raital”. Voice drops into the draft first, so you review it before sending.
- Find the place. The app matches the place you named against offices in the scheme tree. The longest matching name wins, so “Uttar Raital” never collapses into its parent “Raital”.
- Answer from the record. Only the path to that office, plus its transfers, goes to the model. It answers in plain words and cites the offices it used. If the AI is down, a template writes the answer from the same numbers.
- Open the path. Jump to Explore: a flow map from Centre down to the last mile, with a ledger and an inspector for each office.
- Read the standing. Every office shows what it received, what it sent to named offices, what it used itself, and what is left.
- Ask for the records. If something needs explanation, the RTI composer drafts a request to the right public authority. You file it yourself. The app never does.
The map below shows where each piece lives. The chapters after explain how the standing math, the grounding, and the adapters work.
Citizen UI
Ask
EN / हि, voice, typeahead
Explore
Flow map, ledger, inspector
RTI composer
Draft only, never files
Domain
Citizen standing
Received = sent + used + left
Reconciliation
Clear / watch / needs explanation
Place resolver
Longest span wins
Services
Ledger
Paths, transfers, standing
Explain
Corridor slice, template fallback
Edges
Source adapters
Synthetic or public record
Pages Function
OpenRouter key stays server-side
Why this diagram: Sources normalize into one canonical model. Domain rules are framework-free. The AI only sees what the domain hands it.
1. Resolve intent
Pick scheme and place from the question. Unknown real places get an honest miss, never invented rupees.
2. Build the corridor
Path from Centre to each named office, the transfers between them, and a few related forks.
3. Compute standing
Integer paise per office. Status from the share of received that is still open.
4. Ask the model
gpt-4o-mini via a Cloudflare Pages Function. Cite only corridor offices. No misconduct words.
5. Render and link
Answer with a path artifact that opens Explore on the same office.
6. Optional RTI
Record points from standing and reconciliation, routed to the PIO for that level.
Standing, not accusation
Abstract: A citizen needs finger math they can check. A reconciliation gap needs a name that is precise and fair. So every office gets the same equation, and every gap gets a status, never a verdict.
Early ledgers showed Received 140, Sent 140, Left 5. That looks broken. The fix was to make every parent row obey one rule, and to give the office’s own admin spend a place to live:
Citizen standing for one office
On a leaf (a gram panchayat, say), reported utilisation is “used here”. Anything left is still on that ledger. A test checks that every scenario obeys the rule before it can load.
Status comes from how much of what the office received is still open:
Reconciliation status bands
export function citizenStanding(scenario: ISchemeScenario, node: IFundingNode): ICitizenStanding {
const receivedPaise = node.receivedPaise;
const children = scenario.nodes.filter((candidate) => candidate.parentId === node.id);
const namedChildrenPaise = children.reduce((sum, child) => sum + child.receivedPaise, 0);
if (children.length > 0) {
sentOnwardPaise = Math.min(namedChildrenPaise, receivedPaise);
usedHerePaise = node.usedHerePaise ?? 0;
unnamedNextPaise = Math.max(0, receivedPaise - sentOnwardPaise - usedHerePaise);
} else {
// Leaf: reported utilisation is "used here".
sentOnwardPaise = 0;
usedHerePaise = node.usedHerePaise ?? node.reportedPaise;
}
// ...
}Ground the model on a corridor
Abstract: A real scheme tree is far too big for a prompt, and a model shown a parent office will happily answer about the parent. So the model sees only the corridor to the offices the citizen named.
The first version grounded on whatever node was selected on the map. Ask about “Uttar Raital” while “Raital” sat on the visible path, and the answer quoted Raital. Nested names made it worse (Raital, Raital Sadar, Uttar Raital).
The resolver now matches whole-word phrases against every office name, then keeps the longest non-overlapping spans:
return matches.sort((left, right) => {
const lengthDelta = (right.end - right.start) - (left.end - left.start);
if (lengthDelta !== 0) return lengthDelta;
return left.start - right.start;
});
function selectNonOverlappingMatches(matches: readonly IPhraseMatch[]): readonly IPhraseMatch[] {
const selected: IPhraseMatch[] = [];
for (const candidate of matches) {
const overlaps = selected.some(
(picked) => candidate.start < picked.end && picked.start < candidate.end
);
if (!overlaps) selected.push(candidate);
}
return selected.sort((left, right) => left.start - right.start);
}From those matches the explain service builds a slice: the union of paths from Centre to each named office, transfers along that corridor, and a few related offices for ranking questions. It sends mentionedNodeIds with an explicit rule: do not substitute a parent for a named office. The key lives in a Cloudflare Pages Function; the browser never sees it.
What the model gets to see
- The whole scheme treeRejected
Does not fit at real scale, costs more per ask, and invites the model to wander.
- Only the selected map nodeRejected
Answers drift to whichever parent is on screen, not the place the citizen named.
- Corridor to the named offices, with citationsChosen
Small, fast, and checkable. Every cited office is one the citizen can open.
Adapters and the line I do not cross
Abstract: The product has to reach real data one day without ever becoming a scraper. So every source sits behind one interface, and the UI cannot tell them apart.
export interface IFundFlowSource {
loadCatalog(): Promise<readonly ISchemeSummary[]>;
loadScenario(schemeId: string): Promise<ISchemeScenario>;
}The canonical model is Scheme, FundingNode, Transfer, Report, Reconciliation, and Evidence. Money is stored in integer paise and formatted only at the edge.
Two adapters ship today, switched by a Mock / Live toggle next to the theme control:
SyntheticScenarioSourceholds four fictional scheme archetypes on a shared fictional geography: works funding, demand wages, direct benefit transfer, and a matching society. Every rupee is labelled synthetic.PublicRecordSourceholds one reconstructed extract: MGNREGA FY 2025-26, every state named at Centre, and a deep corridor through 12 Himachal districts down to gram panchayats around Mashobra. It refuses to load if the standing math does not reconcile.
The official MIS hosts were returning 503 while I built the extract. So the figures are reconstructed from the public Financial Statement layout, and the evidence drawer lists official links as “verify here”, not as something fetched at runtime.
What I took away
ourmoney is live at ourmoney.anireco.app. It was shortlisted from about 5,000 participants at Build What Moves India. For me it is a step toward a bigger goal: public money that is accountable and transparent at every level, from a district office to a municipal body.
What I keep coming back to:
- Vision first, data second. The equation and the adapter seam do not care which office they describe. That is what makes “every level” possible later.
- Name the gap, not the culprit. Precise words get records released. Accusations get a tool ignored.
- Ground on what the citizen asked. A small corridor with citations beats a big prompt that answers about the wrong office.
- Draw the line in code. No scraper, no auto-filing, synthetic labels on every rupee. The constraints are the product, not a disclaimer.
What production still needs: licensed extracts for scheme MIS first, then municipal budgets and ward-level spending through the same adapters. Legal review of the RTI helper and authority directory. A moderated way for nodal officers and RTI volunteers to correct the record.