← All work

ourmoney

Where did the reported rupee for my place go?

Live
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
Role · Solo (product, domain model, React, AI grounding)Timeline · 2026-presentRead · ~16 min (skim or deep)
  • 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

  1. 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.
  2. 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”.
  3. 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.
  4. 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.
  5. Read the standing. Every office shows what it received, what it sent to named offices, what it used itself, and what is left.
  6. 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.

Why this diagram: Sources normalize into one canonical model. Domain rules are framework-free. The AI only sees what the domain hands it.

Layer map: the UI never reads a source’s raw schema
  1. 1. Resolve intent

    Pick scheme and place from the question. Unknown real places get an honest miss, never invented rupees.

  2. 2. Build the corridor

    Path from Centre to each named office, the transfers between them, and a few related forks.

  3. 3. Compute standing

    Integer paise per office. Status from the share of received that is still open.

  4. 4. Ask the model

    gpt-4o-mini via a Cloudflare Pages Function. Cite only corridor offices. No misconduct words.

  5. 5. Render and link

    Answer with a path artifact that opens Explore on the same office.

  6. 6. Optional RTI

    Record points from standing and reconciliation, routed to the PIO for that level.

One question mapped to the steps above

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

received = sent to named offices + used here + what's left sent to named offices = sum of named child offices what's left = received − sent − used here # "next office not named"

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

next office not named ≥ 8% of received → needs explanation next office not named < 8% → watch still on this ledger ≥ 3% of received → watch otherwise → clear
citizen-standing.ts
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;
}
// ...
}
Named children, used here, and the remainder: the same math for every parent office

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:

resolve-question-nodes.ts
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);
}
Longest span wins, so a child office is never swallowed by its parent’s name

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.

fund-flow-source.ts
export interface IFundFlowSource {
loadCatalog(): Promise<readonly ISchemeSummary[]>;
loadScenario(schemeId: string): Promise<ISchemeScenario>;
}
Every data source, synthetic or public record, fills the same canonical model

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:

  • SyntheticScenarioSource holds 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.
  • PublicRecordSource holds 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:

  1. 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.
  2. Name the gap, not the culprit. Precise words get records released. Accusations get a tool ignored.
  3. Ground on what the citizen asked. A small corridor with citations beats a big prompt that answers about the wrong office.
  4. 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.