Entitled: An AI Agent That Tells Indians Which Government Schemes They're Owed (Built on Sanity)
AI Summary
*This is a submission for the [Sanity Challenge, Path One: Ship an Agent That Queries Real Content](https://dev.to/challenges/sanity-2026-09-16)* ## What I Built > **Entitled** (adj.): having a legal right to something. India runs thousands of welfare schemes across central and state governments....

This is a submission for the Sanity Challenge, Path One: Ship an Agent That Queries Real Content
What I Built
Entitled (adj.): having a legal right to something.
India runs thousands of welfare schemes across central and state governments. Help often doesn't reach people, and the reason isn't that they don't qualify. The eligibility rules are spread over portals and PDFs, written in legal language, and they change by state. The people who need the help most are the ones with the least time and the least second-language English to dig through all that.

Entitled turns that pile into a conversation. Describe your life in plain words, in English or Kannada:
"I'm a widowed farmer in Karnataka with two school-age daughters. We own 1 acre of land."

You get one card per scheme, each with:
- a verdict: Likely eligible, Worth checking or Probably not
- the reason, tied to the specific rules that decided it
- the benefit amount, and a running total of what you're likely owed per year
- the documents to carry
- numbered application steps
- an Apply now link and an Official source link
Why this needs structured content
Eligibility is a chain of conditions: age, income, occupation, land, residence. Then come the exclusions, where you match every positive rule and one clause still disqualifies you. Search over PDFs returns documents. It doesn't evaluate a person against rules.
So in Entitled every rule is a typed object in Sanity, and exclusions are first-class. The clearest demo is the last example pill in the app, "Farmer with a government job":
"I am a farmer with 2 acres of land in Karnataka, but I also work as a government clerk."
This farmer matches every positive PM-KISAN rule: a farmer, with cultivable land in their name. The card still comes back Probably not, with a red Disqualifying rule callout: "Government employees are not eligible." That callout isn't model intuition. It's a document in the dataset:
{ "attribute": "occupation", "operator": "notHas", "value": "government employee", "plainLanguage": "Government employees are not eligible", "isExclusion": true }

The agent fetches that rule with a query and reports it. That's how it earns the right to say "you qualify, but...".
What else is in the app
- Kannada end to end. The interface and the generated answer are both in Kannada, and your language choice persists between visits.
- Follow-up questions. You can ask "what about scholarships for my daughters?" and the agent reuses your earlier context, so you don't start over.
- One-tap WhatsApp share. The result is formatted as plain text (✅ / 🟡 / ❌ per scheme, with amounts and apply links), because WhatsApp is how this information actually travels between families in India.
- Visible progress. A run takes 30 to 45 seconds. Three labelled stages (reading the schemes, checking your situation against the rules, preparing results) show that the system is working.

Demo
- Live app: https://entitled.varshithvhegde.in/
- No login needed. Click an example pill and press the button.
A 60-second path for judges:
- Click Widowed farmer, Karnataka. You get the likely schemes, the total annual benefit and the documents.
- Expand a card and press Apply now. The link comes from the dataset, not from the model.
- Switch to ಕನ್ನಡ and run the same example. The answer comes back in Kannada.
- Click Farmer with a government job and look at the red Disqualifying rule callout.
- Ask a follow-up, for example "What about my daughters' education?"
Video walkthrough (about 60 seconds):
{% embed https://www.youtube.com/watch?v=Pr54BLrXDrc %}
Code
{% embed https://github.com/Varshithvhegde/duecourse %}
studio/ Sanity Studio: schema, deployed to duecourse.sanity.studio scripts/ seed pipeline: API Mitra list → myScheme detail → hand-structured flagship rules web/ Next.js app: /api/check agent route + card UI (framer-motion, EN/ಕನ್ನಡ)
How I Used Sanity
The schema is the product
| Type | Role |
|---|---|
scheme | name, level (Central/State), state, ministry, categories, benefitAmountAnnual, eligibility[], documentsRequired[] (references), applicationSteps[], sources[], status, supersededBy, lastVerified |
eligibilityRule | attribute (age, income, occupation, landOwnership, bplStatus, residence...), operator, value, plainLanguage, isExclusion |
identityDocument | Aadhaar, ration card and so on, stored once and referenced by many schemes |
applicationStep | ordered steps with a channel (online, office, automatic) and a URL |
source | an official URL and a label, so every claim can be cited |

Four modelling choices carry the project:
eligibilityRuleis a typed object, not prose. The attribute list is a fixed set, so the agent compares the same vocabulary against what the citizen said. It also keepsplainLanguage, so every rule can be shown to the user in words they understand.isExclusionis a first-class flag. The system prompt makes the agent check every exclusion, because matching every positive rule means nothing if one exclusion matches.- Documents are references, not strings. Aadhaar is one document that many schemes point to. The agent dereferences it in the query with
documentsRequired[]->{name}. - Lifecycle lives in the data.
status(active, closed, superseded),supersededByandlastVerifiedare modelled so a closed scheme can point to its replacement and a reviewer can mark when a human last checked a rule against the official source.
What the agent does
The agent runs on Inception's Mercury (a diffusion LLM, OpenAI-compatible) through the Vercel AI SDK. It connects over MCP, using @ai-sdk/mcp, to a Sanity Context endpoint in GROQ mode on my dataset. Here is the whole flow, from the question to the cards:

Step by step:
- Extract. It pulls the citizen's attributes out of the plain-language message: state, age, gender, occupation, income, category, land, family.
- Orient. It uses the Context tools (
initial_context,schema_explorer) to see the deployed schema. - Fetch. One
groq_queryretrieves the schemes that have structured rules, with references resolved:
*[_type == "scheme" && defined(eligibility) && length(eligibility) > 0]{ name, shortTitle, level, state, benefitAmountAnnual, brief, eligibility, documentsRequired[]->{name}, applicationSteps, sources, categories }
- Reason. It goes rule by rule against the citizen's attributes and checks every
isExclusionrule. The prompt sets a budget of 2 to 4 tool calls, so an answer isn't a crawl. - Format. A second pass with tools off turns the answer into a strict JSON shape, and that JSON becomes the cards.
The links come from the data, not the model
Early on, the formatting pass kept dropping applyUrl and source. A card with no "Apply" button is useless, and prompting harder didn't fix it. So I moved the responsibility out of the model. After the agent answers, the API route looks each scheme up in the dataset and attaches the real apply and source URLs from applicationSteps[].url and sources[].url. The model can reason about eligibility, but it never gets to invent a link.
What I'd want a judge to know (the honest part)
1. I started with a Knowledge Base and moved to GROQ mode.
I built a Knowledge Base from the dataset first, and the agent ended up calling knowledge_base_read one entry at a time. It burned through 20 tool calls and ran out of steps before it could answer. The eligibility rules are already structured, so I pointed the Context endpoint at the dataset directly and switched the agent to groq_query. The same question now takes a handful of calls. The challenge brief allows both routes ("point your agent at your full dataset through a Context MCP endpoint"), and I'd rather say plainly which one I ended up on. I kept the dataset curated anyway, at 125 schemes, so it stays inside the Knowledge Base budget if I switch back.
2. GROQ mode requires a deployed Studio.
Before I deployed one, every call failed with -32004: Only datasets with deployed Studio applications are supported. The fix is sanity deploy and sanity schema deploy. The error message doesn't make that obvious.
3. Mercury returned 503s when I replayed tool history. Sending the full tool-call transcript into a tools-off formatting request made the provider return server errors. The formatting pass now receives only the agent's final text plus the format instructions, and if it still fails the user gets the answer as readable markdown, not an error screen.
4. Kannada broke the JSON. When writing Kannada values, the model sometimes emitted unquoted array items and trailing commas. I wrote a tolerant extractor, tested it against a real broken response, and tightened the formatting prompt.
5. Tokens created from my Sanity account kept failing.
Project and org tokens I created returned "Session not found" against both the core API and the Context endpoint, while my CLI session token worked. The deployed app therefore authenticates with a session token, which will expire. Rotating it means re-running sanity login and updating one environment variable. A proper org token is the right fix, and I'm taking it to the Sanity team.
Limits you should know about:
- Depth varies. The dataset has 125 schemes (118 Central, 7 State), each with at least one official source. Ten are flagships with hand-structured eligibility: 33 typed rules, 11 of them exclusions. They cover PM-KISAN, Ayushman Bharat, PMAY-U, Ujjwala 2.0, PMJDY, PMMVY, e-Shram, Gruha Lakshmi, Yuva Nidhi and Anna Bhagya. The other 115 carry summary data and sources only, and I haven't turned them into rules.
- I haven't verified every rule line by line. I structured the flagship rules from public scheme pages, and
lastVerifiedis there for a human to confirm each one against the official notification. Treat every verdict as guidance. The app says so, and the administering office makes the final call. - The annual total needs care.
benefitAmountAnnualmixes kinds of value. PM-KISAN pays cash every year, while Ayushman Bharat is insurance cover and PMAY-U is a one-time subsidy, so adding them into one "per year" figure can overstate what someone receives. AbenefitTypefield (cash, cover, in-kind, one-time) is the next schema change. - Scope. Karnataka plus Central only. Another state needs its own data.
- The Context endpoint is read-only. Curation happens in Studio, and the agent reads but doesn't edit.
Sanity Project Details
- Project ID:
qldtw72y - Dataset:
production(public, so you can query it without a token) - Studio: https://duecourse.sanity.studio/ (deployed; it may ask for a Sanity login)
- Context MCP: GROQ mode on
qldtw72y/production
Try it yourself, with no auth:
https://qldtw72y.api.sanity.io/v2024-01-01/data/query/production?query=*[_type=="scheme" && length(eligibility)>0]{name,eligibility}
Agent Session
This is the session I built Entitled in, trimmed to the parts that matter: the Context endpoint problems (Knowledge Base crawl, then GROQ mode and the deployed-Studio fix), the UI and Kannada work, the Mercury 503 and JSON fixes, the exclusion demo and the apply-link fix. I removed the idea research and the API-key debugging turns.
{% agent_session claude-code-session-2gyu1p %}
What's next
- A
benefitTypefield and an honest annual total. - A verification workflow: a scheme update is drafted, a human checks it against the official notification and stamps
lastVerified, and only then does it go live. That is the Sanity Workflows pattern, and it fits here because wrong rules harm real people. - Rules for all 125 schemes, and then more states.
Entitled is an information tool. It isn't legal or financial advice.