01 SectionThe number that decided the architecture
A casual labourer in India earns ₹455 a day if male, ₹315 if female. That is from the Periodic Labour Force Survey Annual Report 2025. I can point at the PDF.
Now imagine someone tells that worker they qualify for a government scheme. To claim it they skip a day's work, pay bus fare both ways, stand in line at a Common Service Centre — and the operator says no, you don't qualify. They just paid ₹455 for a wrong answer, and most of them never come back for a second try.
So a wrong yes costs a day's wages. A wrong no costs an entitlement they may have been owed for years and will never ask about again. Those two errors are not the same size and not in the same unit. Everything in this project follows from that.
02 SectionHow I got here
Scheme Sathi is my second entry for the Code for a Billion hackathon. The first was Saans, an air-exposure bot on Telegram — 3,500 lines, zero dependencies, a live bot, a public repo. Finished code. I parked it on 30 August.
The reason I told myself was that the idea already existed in another app. That felt bad, so I stopped. But novelty isn't a judging criterion. The real reason was that Saans was never deployed and never used by a single person, and 25% of the score is deployment and impact. Losing on novelty was a feeling. Losing on "you have zero users" is a score. Being able to tell those two apart is the most useful thing Saans gave me.
The problem I moved to: India has about 44 crore unorganised workers — construction labour, domestic workers, drivers, vendors — and the welfare schemes for them already exist. 31.48 crore are registered on e-Shram with 14 central schemes plugged into it. The gap isn't eligibility. It's that the rules are scattered, in English, in bureaucratic language, and a failed trip costs a day's wages.
"But myScheme.gov.in exists." It does, and it's good. It's also an English web form that assumes literacy, a browser, and a user who knows what "land holding in hectares" means. Scheme Sathi is a conversation in Hindi on a ₹6,000 phone that ends in a checklist and an address to walk to. Last-mile delivery on top of the government site, not a replacement.
03 SectionFour rules, decided before any code
The language model may not touch an eligibility decision. Eligibility is a pure function — no network, no clock, no randomness. A model may help map "I lay bricks" to a job category (always confirmed back to the worker) and rephrase Hindi a human wrote. It never sees a threshold or produces a verdict. A test asserts that importing the rules package pulls in no model and no HTTP client. Running with no API key is a tested configuration, not a degraded one.
An unresearched value is the string
"TODO", never0.annual_value_inr = "TODO"— because a zero looks researched and no validator can tell the difference. Any file with a"TODO"still loads, gets flagged, and the engine returns UNKNOWN for it.Verdicts are three-valued: ELIGIBLE, INELIGIBLE, UNKNOWN. "I couldn't check this — ask this exact question at the centre" costs the worker neither a wasted day nor a lost entitlement. Most systems treat unknown as failure. Here it's a legitimate answer.
The profile has no name, phone, or Aadhaar field. Not "we don't store it" — the fields don't exist, so they can't be filled by accident. Coarse income bands in the log, a random session id unrelated to the Telegram chat id, and any statistic under five people is suppressed.
The shape that came out of this: scheme rules live in TOML files, each value carrying a deep link to the sentence in the official document it came from and the date it was read. A rule engine evaluates a profile against them. A conversation layer asks the questions in Hindi or English with buttons. Telegram and WhatsApp adapters speak the same conversation from the same engine. Python standard library only, SQLite for the event log, systemd on a small AWS box.
04 SectionThe AI part, honestly
Let me say the uncomfortable thing first. I did not type most of this code. Claude Code wrote it, I described what I wanted and read the diff. If you want to call that vibe coding, that's fair.
What I did do, and can prove from the git log: I decided the four rules above. I read every government PDF the scheme values came from — not one threshold, rupee figure or age band in this project was authored by a model. I used the bot the way a worker would and found the bugs the tests missed. I set up the Telegram bot, the AWS account, the Meta app and the WhatsApp number by hand in three consoles. I debugged the things that broke on a real server at night.
What worked:
- Plan before edit. For anything touching more than one file, the assistant read the code and produced a plan, I approved or changed it, then it wrote. Letting a model start editing from a one-line prompt gives you a diff you have to review harder than writing it yourself.
- Commit messages that name who gets hurt.
Read a tax "Yes" back before it costs someone a pension.Stop adding an accident cover to a pension. When the message has to say the consequence to the worker, you notice the fixes where you can't. - Making it break its own fix. Every new test, I'd have the fix reverted and check the test went red. If it stayed green, the test was decorative. Caught more bad tests than anything else. Costs ninety seconds.
- Two models arguing beat one model agreeing with me. I made the repo public and ran it through ChatGPT three times, then had Codex and Claude Code review each other's fixes on the same checkout. Codex's design beat Claude's twice — on double-tap handling and on age parsing (
str.isdecimal()beforeint(), which rejects3_4but still accepts Devanagari३४). Then Codex's own fix silently broke my exhaustive path test — every button tap was rejected, no session was ever created, and the test ran green against the opening screen. Claude measured that and repaired it. Next morning a second Codex pass found two bugs in Claude's fix from the night before. Neither model was reliably better. The value was that they disagreed, and every disagreement was something I had to actually understand to settle.
What did not work:
- Anything requiring a fact about the real world. A model is very good at "restructure this state machine" and structurally incapable of "what does the PM-SYM page say the contribution is at age 32." Ask it and you get a plausible number, which is the exact thing this project exists to prevent.
- Review findings that were my own comments read back to me. A meaningful fraction of the first ChatGPT round was known trade-offs from my
# ?comments, returned as discoveries. It also scored "domain correctness 7.6" without opening a single source PDF. - Advice that contradicted a constraint the reviewer had just praised. Two rounds told me to add location routing — "the CSC 4 km away" — in the same review that praised the privacy model. Location routing needs a pincode in the profile. The privacy model is that the field doesn't exist. I didn't do it, and I wrote down why. "I was told to do this and I didn't, here's the reasoning" is worth more than any claim about who typed which line.
05 SectionFive bugs worth telling
1. The README said "don't screen a real worker." The code didn't care.
The worst one, found by outside review. I'd written in bold in the README that the scheme data hadn't been checked by a second person. I'd written it in a comment at the top of every scheme file. I'd written a test asserting the warning was still there. Three places, all prose. Meanwhile Scheme.is_verified returned not self.stubs — "no TODOs left" was being treated as "verified" — so the app printed PMSBY: verified at startup and served real verdicts off data one person had transcribed from a PDF once.
Now researched and signed off by a named human are separate states, and only a scheme that is both can produce a verdict. Enforcing it made the bot tell every worker "I couldn't check this yet" for every scheme until I sat down with the sources and signed each file. A worse demo and a better product. A warning in a README protects nobody. If a rule matters, the code path that would violate it has to be the thing that stops.
Enforcing it also quietly gutted the test suite — no servable scheme, no eligible result, no application pack. The exhaustive walk dropped to a fifth of its paths and built zero packs while passing every assertion it reached. It was caught only because that test counts its own coverage and asserts on the counters.
2. Six bugs in the first hour of real use — none in the engine
I put the bot live and used it like a worker. A reply_markup: null field made Telegram reject the message with a bare 400 and silently kill the session. A free-text occupation box looped back into the menu that led to it. No income band for a worker earning nothing. English screens whose messages were English while the buttons under them were Hindi. Every test that passed while those shipped was asserting on message text; the bugs were one layer out, in the keyboards. Same lesson as Saans, almost word for word: test what the user touches, not what is convenient to assert.
3. A double tap wrote the bank answer into the tax field
On a cheap phone with weak signal, people tap twice. A stale button press from the previous question could answer the current one — a "Yes, I have a bank account" landing as "Yes, I pay income tax," which disqualifies someone from a pension. The fix that shipped was Codex's, not Claude's: remember the message_id of the keyboard currently on screen, accept a callback only from it, retire it before processing. Verified by driving a real double tap and checking the tax field stayed None.
4. My impact number was six times too big
Once real scheme values went in, the headline "annual entitlement surfaced" jumped — because I was summing PMSBY's ₹2,00,000 accident cover with PM-SYM's ₹36,000/year pension. A contingent insurance payout and a guaranteed annual pension are not the same kind of money. They're separate numbers everywhere now, and the label "entitlement surfaced, not money delivered" travels with the figure into the chat, the printed sheet and the dashboard.
The same bug came back a week later, one layer up: a 65-year-old widow qualifies for both Uttarakhand pensions but the state pays one, and while the worker-facing result collapsed them correctly, the dashboard summed both. A crash is visible. An inflated impact number survives all the way to a judge.
5. The things no assistant could do for me, and what I broke doing them
Telegram was five minutes with BotFather. AWS: I created the IAM user and pasted the console password and the access key and secret straight into the chat, because the console showed all three on one screen and I didn't know which was the secret. Everything was treated as burned, a replacement key was written straight to ~/.aws/credentials without being printed, and the console login was deleted rather than rotated. "Vibe coder leaks cloud credentials" is exactly the story people expect; the honest version is yes, the fix took thirty seconds, and now I know what an access key looks like.
Meta was slower. App, WhatsApp product, test number, verifying my own phone as a recipient — all manual, all fine. Then WhatsApp went silent about a day after it first worked, while Telegram on the same box kept answering. Nothing in the logs said "token." The temporary token Meta hands you expires after 24 hours; a permanent one needs a System User under the business with both the app and the WhatsApp account assigned as assets, four screens deep and mentioned nowhere near the screen that gives you the temporary one. That took an evening.
And deployment itself sat parked for days behind Azure student verification and a Fly.io card preauth that Indian cards decline — until I asked "can I just use AWS" and found an account already carrying $120 of credit. The whole deploy took under an hour, most of it waiting for the instance to boot. The estimate that had kept it parked was wrong by an order of magnitude the entire time. Deploy before you polish. Only one of them is measured.
06 SectionWhere it honestly stands
| Live | @YojanaSathiBot on Telegram, 24×7 on AWS EC2 (~$10.50/month) |
| Schemes | 10 signed and live, each value deep-linked to its source; 4 researched drafts that return UNKNOWN until I sign them; 1 that lost its signature when a Hindi source changed a rule |
| built, verified end to end on Meta's test number, not on a public number — production needs Business Verification and a second SIM | |
| Tests | zero dependencies, python3 check.py, exhaustive button walk in both languages plus an independent rule oracle across ~200,000 verdicts |
| Real workers screened | zero |
That last row is the honest one. Everything that's missing is person-shaped: a native Hindi speaker to read the strings, a worker to press Start, a phone call to a CSC operator about one age cap, a verification queue at Meta. None of it is "the code doesn't work."
There's also a risk above all of it I can't control: the hackathon's submission form has a required checkbox saying the project was built in AgentFoundry, the official IDE. This was built locally. I've asked whether importing a repo qualifies — publicly in a GitHub Discussion with zero replies, and by email to the address in the organiser's own footer, which hard-bounced. I'm logging every attempt with a date rather than ticking the box. If the answer is no, the project loses a hackathon and keeps everything else.
07 SectionIf you're starting something like this
- Decide what you're not willing to be wrong about before you open the editor. The ₹455 figure became a type signature. Every good decision in this codebase traces back to it.
- Let the AI type. Don't let it decide, and don't let it know facts. The moment a model supplies a real-world number, you've built a very confident liar.
- Use your own thing on a real phone before anyone else does. Every serious bug in this project was found that way, and none by the test suite that was green at the time.
- Ship before you're ready. You'll learn more from the first stranger pressing a button than from the next week of refactoring.
08 Where to goLinks
- Code: github.com/avinashnegi1999/yojana-sathi — Apache-2.0
- Try it: @YojanaSathiBot
- The site: avinashnegi.com/yojana-sathi — video, screenshots, live counters
- The long version:
docs/BUILD_LOG.mdin the repo — every bug and wrong turn, nothing tidied up - The handbook: scheme-sathi-handbook — 19 chapters on the same material