AgriLokal gives Indian farmers crop advisory and agronomist access in Hindi, Marathi, and English. When I joined, it was a pay-per-call product with no reason for anyone to come back. This is the full arc of turning it into a subscription business, including the parts that didn't work on the first try.
A pay-per-call farming app doing ~Rs.5,000/month, with no reason for anyone to come back and no subscription infrastructure to build one on.
Pivoted to a full subscription model, then designed the 8-state lifecycle machine (BE/FE contract) that made "what can this user access right now" answerable for the first time, backed by an AI-assisted, human-reviewed crop calendar pipeline.
Subscription rate 33%→49%, trial-to-paid 9%→18%, D0 cancellations 55%→under 35%, renewals sustained at ~45%, scaling to Rs.4L MRR from a ~Rs.5,000/month starting point.
Systems thinking under real constraints, disciplined AI/human tradeoffs on a farmer-facing product, and honest attribution when a metric's cause was ambiguous (the paywall lift, the MAU decline).
The app launched as pay-per-call: a wallet check gated every agronomist call, and monthly revenue sat around Rs.5,000. There was no ongoing relationship, just isolated transactions.
Before proposing a fix, I ran direct user calls. The consistent theme: farmers wanted help across a whole crop cycle, not a one-off answer. Nine months later, I went further: 19 in-person interviews in Maharashtra (8 app users across active/trial/churned, plus 11 non-users), to understand what was still missing once the subscription existed.
I don't understand how trial and subscription are different, how are you helping me?
— user call, ahead of the paywall redesignField research added a second insight: farmers already paid up to Rs.5,000/year to consultants for some crops, so willingness to pay clearly existed above our Rs.399/90-day price. But it was crop-cycle dependent, a 10-month grape cycle justified that spend in a way a 3-month crop didn't.
If we removed the wallet-check friction (unlimited calls, autopay), made subscription state genuinely legible to both users and our own frontend, and kept a human in the loop on anything AI-generated for a farmer, renewal would hold even while we experimented on price and acquisition channels.
Home teaser: a static "start your 3-day trial" image on home, no real backend, just to gauge intent. 14-15% click-through, cleared the bar to build the real flow.
Pricing A/B: consolidated subscription plans and built an n8n-hosted AI chatbot to run a 3-arm pricing experiment (Rs.99/Rs.149/Rs.399) across Hindi/Marathi cohorts, reframing success around net revenue per assigned user rather than raw conversion alone.
| Plan | Calendar access | Calling access | Result |
|---|---|---|---|
| Rs.99 | Shorter window | Capped | 60% conversion |
| Rs.149 | Mid window | Capped, higher | Mid-tier positioning |
| Rs.399 | Full 90-day | Unlimited | Highest revenue per renewed user |
Read: the cheapest plan converted best (60%), with cancellations held under 30% overall across all three arms. Framing success as net revenue per assigned user, not just conversion rate, is what kept the Rs.99 plan from looking like a win on volume alone while quietly eroding revenue per user.
Paywall redesign: designed and shipped it myself, directly from the confusion surfaced in user calls above.
The in-app crop calendar input (a 4-page flow in simple Hindi with image-led fields) landed at ~50% D0 completion. Page 4 was the specific leak: a soil-type field was greyed out and hard to read. Rebuilt it as an image field to match the rest of the flow, and added a summary screen at the end.
Model usage was deliberately split: a more conservative model for first-pass calendar generation (so agronomic data doesn't hallucinate), a lighter model for translation only, where the risk is lower. Agronomist review stayed permanently in the loop, a design choice, not a gap.
Generate a crop calendar for {crop}
in {region}. Define the standard
fertilizer, mineral, and quantity
schedule for this crop and stage.
Generate a {crop} calendar for {region},
{soil_type}. Standard fertilizer/mineral/
quantity norms are well-documented, keep
those as-is. Flag only where local practice
or {soil_type} would deviate from the
generic norm, and do not assert a sowing
date without a farmer-provided start date.
Recreated for illustration, not the verbatim production prompt. V1 was already reasonably accurate on the well-documented, static parts, standard fertilizer/mineral/quantity schedules per crop and stage. Agronomist edits weren't correcting that baseline; they were mostly catching regional deviations and sowing-date assumptions the model had no way to know. That's what the refinement actually targeted, not general accuracy.
Early on, every calendar (trial and subscription, generation and translation) ran on GPT-5.1 end to end, each full generation cost real money per calendar, on every cycle. The fix wasn't only swapping to a cheaper model, it was realizing the same 90-day calendar didn't need to be regenerated every time. Generate one 90-day calendar on 5.1, have agronomists review and extract it into 7-day chunks, then run only the translation layer on the cheaper 4.1 model for each chunk as it's needed.
[Real figure to insert once checked against actual API billing: cost per calendar dropped from ~$20 on the old full-regeneration approach to $__ under the chunked-extraction + 4.1-translation approach.]
This same one-calendar-per-cycle structure is what made monthly plans possible without extra generation cost: since the underlying 90-day calendar doesn't change, a renewing user isn't asked to re-enter crop calendar inputs unless the calendar's own end date has actually been reached.
The daily task tab runs on the same structure. Each translated chunk is sent to the backend as a JSON of that period's activities, tasks, precautions, input requirements, and weather-risk flags. The daily task tab simply matches today's date against that JSON: a match renders that day's full task card, no match (a null date field) renders nothing for today. The UI never has logic of its own, it's a thin reader over structured data the pipeline already produced.
Final calendars are stored in a Scylla table for write-throughput at scale. The cost: that data isn't queryable through BigQuery, so deeper analytics need a separate export step. Unsolved, named honestly.
The paywall redesign moved conversion 2-3 percentage points, but the same window saw Meta CAC swing between Rs.150 and Rs.400. I can't cleanly separate the redesign's effect from cheaper traffic that week, so I hold this as directional, not a clean causal win.
The centerpiece: there was no BE/FE contract for "what state is this user in right now." I rejected the simplest option, treating any failed autopay as immediate cancellation, because it would silently lose users over a transient card or balance issue. Instead, 8 explicit states, with a dedicated Halted buffer: a retry window plus grace period before permanent cancellation.
A banner appears the day before renewal. If payment fails, the app still shows the last calendar, just read-only, with a clear fix-payment prompt, not a dead end.
On Day 90, a retry job fires the mandate charge, updates user_state, and recalculates block_flags so the frontend never has to re-derive access logic itself.
A binary "paid or cancelled" model punishes recoverable failures. The extra Halted state costs more to build and test, but it's the difference between losing a user to a bounced card and losing them to an actual decision to leave.
| Field | Type | Purpose |
|---|---|---|
| user_state | enum | One of the 8 states above |
| days_remaining | int | Trial days left / grace-period days left |
| next_charge_date | date | null | Next scheduled autopay attempt |
| crop_calendar_access_level | enum | none / trial_3day / full_season |
| advisory_access_level | enum | none / unlimited_trial / subscriber |
| block_flags | object | Feature-level booleans, so FE never re-derives access logic |
Alongside the state machine: the in-app input flow, a home-screen redesign and S2 re-entry fixes (Profile→Paid +3pp, subscription rate +4pp, post-renewal engagement +6pp), the 115-crop multilingual content library (n8n + GPT pipeline generating in-app HTML pages in Hindi/Marathi/English, pre-trial vs active-subscriber access built directly into the output, served from Postgres with the frontend purely rendering), and an n8n-hosted AI chatbot as a lower-friction entry point before trial.
One operational window, tracked separately from the headline lifecycle metrics above.
| Month | MAU | Paid MAU | Active Subs | Revenue | Trial→Subs | Login→Trial |
|---|---|---|---|---|---|---|
| March | 19.5k | 2.2k | 1.01k | Rs.3.5L | 46% | 11.6% |
| April | 15.5k | 2.0k | 969 | Rs.3.7L | 44.4% | 13.1% |
| May | 12.9k | 1.78k | 807 | Rs.2.7L | 37% | 15% |
| June | 12k | 1.6k | 718 | Rs.3L | 38.5% | 13% |
Renewals sustained at ~45% overall. Subscriber week-over-week retention decays from 34.7% (week 1) to 12.4% (week 4), then stabilizes around 14-19% through week 13, rather than trailing to zero.
MAU dropped by a third, March to June, which reads as alarming in isolation. But renewal held flat and login-to-trial actually improved over the same period. If the product were leaking users, the funnel's later stages would degrade too, they didn't. The likelier explanation is acquisition volume and CAC volatility, upstream of the product, not inside it. The lesson I keep re-applying: segment acquisition from retention before you diagnose a topline number, the same aggregate can mean two opposite things.
Build confidence scoring on AI-generated calendar output, so agronomists review by exception instead of reviewing everything equally.
Personalize calendars using a farmer's historical behavior (crop switches, engagement patterns), not just crop and region.
Introduce automated prompt evaluation, so accuracy regressions surface before an agronomist catches them manually.
Reduce manual review safely, tied to the confidence scoring above, not as a blanket cut.
Improve multilingual consistency, the translation layer still trails the English/Hindi generation quality.