Inspiration

Ramesh is a smallholder farmer in Latur, India — two acres of cotton, and a season that lives or dies by timing. He knows his crop better than any app does; what he can't always catch is the moment. Spray before the wind starts, not after. Irrigate in that dry window, only when it holds. The advice for these moments is there — the hard part is getting Ramesh to act on it, right when it matters. And he's one of millions of smallholders in exactly the same spot.

One extension officer is spread thin across thousands of farmers like him. SMS blasts and apps go out, then… into the void. Nobody can easily tell you if a single farmer actually sprayed or irrigated or skipped it. That gap — between advice sent and advice acted on — is where Ramesh's season, and his income, quietly leaks away, little by little.

Aren't there already enough agri-advisory tools? Plenty that send. Almost none that close the loop — confirm who did what, and follow up with who didn't. And the farmer-facing ones keep running into the same wall: install an app on a low-end phone, on patchy data, with yet another login. So in the end they don't get installed, and the whole chain breaks early. Every farmer, though, already has the one channel that just works — WhatsApp — free, familiar, nothing to download.

Here's the bit that ties impact to a business, because that's the real pressure. The organizations backing this advice — an NGO running a cotton program, an agri-input company, a Krishi Vigyan Kendra, that farm-science center — can't prove it worked either. Without a follow-through count, there's no clean way to improve, and no solid way to report to a donor. Then there's no way to justify next season's budget. Accountability isn't just paperwork here, it's what lets good advice stay funded, and funding is what lets it reach more farmers. Make follow-through visible, and a partner doesn't only help one cohort act; they earn the case to expand into the next ten districts.

I already had the half that talks to farmers — a WhatsApp engine that watches a district's weather, sends a plain-language nudge when the moment is right, and records the farmer's "Done." It works in the field today: an AWS AIdeas award winner, with a two-minute demo of the farmer side. What it was missing was everything around it: a way for a partner to say who they're advising, pay for it, watch who acted, and chase the ones who didn't. That product is Outturn — the accountable, monetizable control plane that turns a one-way advisory blast into a loop a partner can see, fund, and grow.

The name is deliberate. Outturn is the trade term for what a crop actually yields at delivery — the realized result, not the projected one. That's exactly what this measures: not the advice that went out, but whether the farmer acted on it. The outturn of a nudge.

What it does

Outturn is the dashboard a partner signs into — a partner being an NGO, an agri-input company, a co-op or FPO, or a government extension network like a KVK. From there they can:

  • create a cohort — a district + crop group (cotton in Latur, say);
  • set the advisory rules (the safe-to-spray thresholds) and the reminder cadence;
  • activate it through Stripe — or a one-click demo activation, so a judge isn't stuck behind a paywall;
  • enroll farmers by phone number;
  • run an advisory cycle on demand and watch it go out over WhatsApp;
  • see follow-through per cohort, live — 28 of 42 acted, 67% — rolled up from real replies, not delivery receipts;
  • catch cohorts going quiet and re-nudge the farmers who haven't acted;
  • and read a tenant-scoped log of every action taken.

The whole thing runs on one sentence: provision → activate → poll → nudge → reply → roll up → act. The dashboard is the steering wheel for that loop. And the goal is simple: more farmers actually act on the advice, and a partner can prove it — well enough to fund the next district.

The closed loop — provision → activate → poll → nudge → reply → roll up → act, with re-nudge closing it.

Most advisory stops at "nudge." Outturn measures the reply, rolls it up, and acts on whoever didn't.

How I built it

The front end and every control-plane API route run on Vercel (Next.js, TypeScript). The operational data lives in Amazon DynamoDB. The field engine — Lambda, Step Functions, EventBridge, API Gateway, WhatsApp via Meta's Cloud API — was already there. H0 was about building the control plane on top of it, and the AWS-database-plus-Vercel plumbing that makes the two read as one product.

Architecture — the H0 control plane (green) on the pre-existing AgriNexus engine (gray), meeting at one DynamoDB table. Green is the H0 build; gray is the proven engine — they meet at one DynamoDB table.

🔍 Every claim below is backed by live AWS consoles — two accounts, keyless OIDC, Streams, the audit table — at outturn.vercel.app/proof. Not a mockup.

One table, both worlds. The control plane and the delivery engine meet in a single DynamoDB table (agrinexus-data), with a single-table layout that's plain on purpose:

Partition key (PK) Sort key (SK) What it holds
TENANT#<id> COHORT#<id> partner-owned cohort config
TENANT#<id> LICENSE#<id> billing / activation state
TENANT#<id> SUMMARY#<id>#<period> materialized follow-through
PHONE#<phone> MEMBERSHIP phone → tenant / cohort join
USER#<phone> PROFILE field-engine farmer profile
USER#<phone> NUDGE#<ts> field-engine advisory events

The phone number is the join key between the two systems. Tenant isolation isn't a WHERE clause I have to remember to add every time — a partner's rows physically live under TENANT#<id>, so there's nothing to forget.

Activation is a data contract, not a boolean somewhere in code. A draft cohort is just a COHORT# item, and the loop can't see it. Activate it, and Outturn writes a LICENSE# row and stamps the cohort with a secondary-index key (STATUS#active). A poller queries that index for active cohorts, pulls each district's weather, and checks it against the rules — wind under the threshold, humidity under the threshold, the window dry. If it's safe, it kicks off the engine's existing Step Functions workflow. This demo poller is on-demand and walled off from the production schedule, but it calls the real nudge workflow — so the loop a judge runs is the actual loop, not a mock.

Fast dashboards, not scans. I didn't want the dashboard recomputing follow-through by scanning every event on each page load, so the numbers come from DynamoDB Streams. An OutcomesAggregator Lambda watches inserts and status changes — a new nudge bumps nudgesSent, a DONE bumps nudgesCompleted, an EXPIRED bumps nudgesExpired — and keeps one SUMMARY# row per cohort per month. The dashboard reads that one row.

That's also where I lost an evening. Streams are at-least-once, and Lambda will happily retry a batch, so even an atomic ADD can double-count — and suddenly a cohort is showing 110% follow-through. The fix: the aggregator claims each record with a DEDUPE# marker before it increments, and skips anything already claimed. Atomic and idempotent, it turns out, are two different problems.

Keyless on Vercel. Production reaches DynamoDB through OIDC federation — Vercel mints a per-deployment token, the app assumes an AWS role via STS, the SDK gets short-lived credentials — and AWS_SECRET_ACCESS_KEY never shows up in the production path.

There are actually two keyless federations, into two different AWS accounts — which surprised me too. One goes to my own account, where the live agrinexus-data table lives. The other goes to the Vercel Marketplace integration's managed account, where the partner audit log is its own DynamoDB table with its own OIDC role. Open that audit table from Vercel and you land in a VercelConsoleAccessRole session in an account that isn't mine. That's intentional: live data in my account, the audit trail on managed Marketplace infrastructure, no shared blast radius.

Consent at the last step. The invariant that actually worried me wasn't billing or the charts — it was making sure that enrolling a farmer can never quietly message them. Enrollment writes the MEMBERSHIP row and seeds a PROFILE with consent=pending only if one doesn't already exist, so an opted-in farmer never gets reset. The sender has the last word: no granted, no send.

The "act" step. Reporting on its own is passive, so a cohort with weak follow-through gets flagged "needs attention," and an admin can re-nudge it — tenant-scoped, admin-only, audited, through the same workflow, with a gate that won't double-send to a farmer who already has a nudge in flight.

A note on white-label, because it's the first thing people ask: today the demo runs on one shared WhatsApp number, with farmers routed to their partner by phone (the MEMBERSHIP row) so a message carries the right partner's name. The number is shared, though. The production white-label path is Meta Embedded Signup: each partner connects their own WhatsApp Business number, and Outturn sends on their behalf — so GreenHarvest's farmers see GreenHarvest's verified number, and AgriInput's see theirs. The phone→tenant join I already have plugs right into it.

A deterministic system, on purpose

There's no model guessing here, and for this I think that's the right call. "Is today a good day to advise spraying?" is a rule a partner can read for themselves — wind, humidity, temperature, a dry window. "Did the farmer act?" is a literal DONE reply, not an inference. A field officer can trace the whole chain end to end — the rule, the send, the reply, the roll-up — and keep the record to decide what to try next season. The farmer always decides. Outturn just makes the nudge land at the right time and the follow-up impossible to lose track of.

Why DynamoDB (not Aurora)

Fit, honestly. The workload is high-write and event-shaped — profiles, cohorts, advisory events, replies, summaries, audit rows — and the access patterns are known and key-driven, not relational: this tenant's cohorts, this cohort's members, all active cohorts, attribute a phone's event to a cohort, read one summary. On Vercel everything is serverless and bursty; with Aurora I'd be back to connection pooling, or reaching for RDS Proxy. DynamoDB is already a stateless HTTPS API — the right shape for per-request functions — and one well-designed table plus two GSIs covers every screen.

The calls I made — and what they cost

Every one of these was a fork with a real downside. Naming the trade-off is the point:

Decision Why What it costs
Single-table design One round-trip per screen, no joins, cheap reads Opaque to a newcomer; access patterns fixed up front
Activation as a GSI key (not a status flag) Poller finds active cohorts in one query, no scan You model the lifecycle as data, not a code branch
Materialized roll-ups via Streams (not on-read scans) Fast dashboard reads — one summary row A few seconds of eventual consistency
Idempotent aggregation (a dedupe claim) Streams are at-least-once; atomic ≠ correct An extra conditional write per event
Two keyless OIDC federations (two accounts) Live data and audit log can't share a blast radius Two trust relationships to wire and reason about
Consent as a separate state (gated at the sender) Enrolling a farmer can never accidentally message them Two states to track: "enrolled" vs "may message"
Demo poller isolated from the prod schedule A judge runs the real loop, safely A parallel entry point to maintain

Challenges I ran into

The hard parts weren't features — they were the boundaries between two systems that were never built to meet. Consent lives in the engine but gets triggered from the control plane. Counts have to stay correct across at-least-once Streams. The old workflow has to keep running while new code drives it. The demo has to be genuinely live without touching the production schedule. Every one came down to the same discipline: find exactly where the seam is, and put one unambiguous gate there. Most of my bugs, looking back, were a seam I'd drawn in the wrong place.

Accomplishments I'm proud of

A judge can drive the whole loop from the dashboard in about two minutes:

  • create → activate → run a cycle → see follow-through per cohort, rolled up from real reply events;
  • on a Vercel front end over DynamoDB, with a Marketplace-provisioned audit trail;
  • and not one stored AWS key — everything keyless via OIDC.

The farmer side — the WhatsApp nudge and the "Done" — runs on the proven engine. The architecture is easy to read because the loop is the product.

What I learned

Productizing a working engine goes beyond a rewrite. A few things that stuck:

  • Draw the boundary first. The real win was deciding exactly where the engine stops and the control plane starts.
  • Turn behavior into a row. Activation became a GSI key; the join, a phone number; outcomes, a summary; consent, a profile state — so the UI just mirrors the truth instead of inventing it.
  • Put billing, audit, and consent at the seam, not scattered through the code — that's what made the boundary monetizable, observable, and safe.

What's next

Three directions, all the same accountable loop — not a feature laundry list:

  • More of what moves a farmer's income, not just agronomy. The same nudge-and-follow-through loop fits input-credit and microfinance reminders, market-price alerts, and irrigation / sowing / pest-scouting programs — each a configurable rule set on the engine that already exists.
  • Sensor-driven nudges. Partner with soil-moisture, soil-quality, and irrigation IoT providers, roll their readings into the AWS engine, and turn them into the same accountable nudges — soil-based, not just weather-based, so the advice gets more precise per field.
  • Reach, made self-serve. Per-partner WhatsApp numbers via Meta Embedded Signup (true white-label), plus QR self-onboarding so a partner can stand up a new district and let farmers join by scanning a poster.

The pattern generalizes, but the engine is agriculture-specific today — so the honest next step is careful productization, not pretending every vertical is already a toggle.

And underneath the cohorts, the roll-ups, and the keyless plumbing, the whole point is simple: Ramesh gets the advice in time — and the people paying to reach him can finally prove it.


See the live infrastructure proof — real AWS consoles, two accounts, keyless OIDC — at outturn.vercel.app/proof.

Built With

Share this project:

Updates