Case study · AgriLokal · 0 → 1
~7 min read Oct – Jul Subscription · AI Pipeline · Systems Design

From a single paid call to a subscription farmers actually renew.

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.

Role
Associate Product Manager (promoted from PM Intern)
Squad
3 BE, 3 FE, 2 EMs (1 BE, 1 FE), 1 Product Analyst, 2 Design, 1 Product Intern, 1 Marketing, plus Founder
Ops
16-person ops team, agronomist review layer
Timeline
Oct – Jul, subscription pivot through growth diagnosis
TL;DR · 60 seconds
The stakes

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.

The move

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.

The outcome

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.

What this shows

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).

Peak MRR
Rs.4L
Subscription rate
33%→49%
Calendar creation time
−70%+
Farmer interviews
500+
Problem

A product with no reason to come back

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.

Research

Calling users before building anything

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.

Insights

What farmers actually said

I don't understand how trial and subscription are different, how are you helping me?

— user call, ahead of the paywall redesign

Field 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.

Hypothesis

Remove friction, make access legible, keep trust intact

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.

Experiments

Three things I tested cheap before building expensive

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.

PlanCalendar accessCalling accessResult
Rs.99Shorter windowCapped60% conversion
Rs.149Mid windowCapped, higherMid-tier positioning
Rs.399Full 90-dayUnlimitedHighest 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.

Iterations

The fixes that came from watching real usage

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.

Edge cases considered — Crop Calendar
  • Farmer growing multiple crops at once
  • Crop changed mid-season
  • Unknown or missing sowing date
  • Prompt hallucination on agronomic detail
  • Translation inconsistency across languages
  • Agronomist override of AI output
  • Model token limits on long calendars
  • Unsupported regional language requested

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.

Illustrative — v1
Generate a crop calendar for {crop}
in {region}. Define the standard
fertilizer, mineral, and quantity
schedule for this crop and stage.
Illustrative — after training on agronomist edits
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.

Cutting generation cost by changing what gets regenerated, not just which model

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.

Tradeoffs

What I gave up, and what I'm not claiming

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.

Launch

The subscription state machine

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.

Pre-Trial Trial Active Trial Cancelled Active Subscription Autopay Failed Halted Subscription Renewal Subscription Cancelled mandate paid cancels early Day 3 success Day 3 fails retry ok exhausted fixed grace expires Day 90 renewal fails
8-state subscription lifecycle: the Halted buffer is what saves recoverable users from being cancelled by a transient payment failure.
Day 0 Rs.2 mandate Day 3 Rs.399 autopay Day 87 renewal reminder Day 90 renewal attempt
One subscriber's actual 90-day journey through the state machine above.
User experienced

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.

System did

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.

I was thinking

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.

FieldTypePurpose
user_stateenumOne of the 8 states above
days_remainingintTrial days left / grace-period days left
next_charge_datedate | nullNext scheduled autopay attempt
crop_calendar_access_levelenumnone / trial_3day / full_season
advisory_access_levelenumnone / unlimited_trial / subscriber
block_flagsobjectFeature-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.

Metrics

What actually moved

One operational window, tracked separately from the headline lifecycle metrics above.

MonthMAUPaid MAUActive SubsRevenueTrial→SubsLogin→Trial
March19.5k2.2k1.01kRs.3.5L46%11.6%
April15.5k2.0k969Rs.3.7L44.4%13.1%
May12.9k1.78k807Rs.2.7L37%15%
June12k1.6k718Rs.3L38.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.

Reflection

MAU was declining. Renewal wasn't. Those are different problems.

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.

If I had three more months

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.