Panen Pas — Garuda Hacks 7 Writeup

A Telegram bot (Agria) that tells Indonesian chili farmers when to sell, by combining real Bank Indonesia price data with live detection of local harvest gluts — the market trap no price app can see.


Inspiration

Every harvest season, Indonesian roadsides fill with rotting chili. It looks like a story about waste, but it's really a story about timing and coordination.

Smallholder farmers plant and harvest with almost no market visibility. So an entire district ends up harvesting the same crop in the same week — and the local market floods. Supply spikes, the local price collapses, and a season of work sells for a fraction of its worth. The cruel part: the farmer often had no way of knowing that four of their neighbors were harvesting on the same day.

We chose cabai rawit merah (bird's-eye chili) deliberately, because the data made it undeniable. Using Bank Indonesia's PIHPS price history, we measured the coefficient of variation of daily prices:

$$ CV = \frac{\sigma}{\mu} \times 100\% $$

For cabai rawit in Jawa Barat and Jawa Tengah, approximately 32% - 37% (Bank Indonesia's own "High Volatility" band), with prices swinging 3–4× across a single year. For comparison, rice (beras) sat at 2% — "Very Stable." Chili is perishable (≈5-day shelf life) and violently volatile: exactly the crop where mistiming a sale hurts most.

The insight that became our thesis: oversupply is local, but price information is provincial. A national price ticker tells every farmer in a province the same number — so they all react the same way and crash their local market together. We wanted to see the glut before it forms.


What it does

A farmer opens a chat with Agria and, in a short guided conversation, reports their crop, district, expected harvest date, quantity, and contact number. No app to install — it lives inside a messaging app they already use.

Behind that simple chat, the engine combines two signals no price app puts together:

  1. Price trend — rising, flat, or falling, computed from real Bank Indonesia price history for the crop's province.
  2. Local harvest cluster — how many farmers are harvesting the same crop in the same district within an estimate of 2-day window.

It returns one of three recommendations — Sell now / Hold / Wait — each shown with the real market price (e.g. "Harga stabil ➖ Rp56.150/kg"). The killer case: the price looks stable, but Agria sees five neighbors about to harvest at once and warns sell now, before the local glut crashes the price — advice a plain price ticker would never give.

When the answer is Sell, the farmer's offer is routed to an anchor buyer. On the buyer's side, Agria closes the loop:

  • Browse harvest reports per district, with live remaining stock and "sold out" markers.
  • Place a bulk order — say 200 kg — and the system aggregates it across farmers using Earliest-Deadline-First allocation (fill from whoever's crop spoils soonest), returning each farmer's cut and phone number.
  • On confirmation, every seller is notified how much sold, what's left, and the current price.

There's also a coordinator view of all matches and an admin tool to simulate price shocks and broadcast alerts.


How we built it

We split the work contract-first, so two tracks could build in parallel against an agreed data shape and one entry function, process_harvest_report(...).

Core (data & logic) — pure Python, no Telegram:

  • core/models.py — canonical vocabulary (crops, district→province map) and data shapes.
  • core/price_trend.py — the trend signal. We anchor windows to the latest date in the data (not the wall clock), so a stale cache still computes honestly. With a recent window of nr days and a previous window of np days:

  • core/clustering.py — counts reports of the same crop+district within the harvest window. A cluster is "crowded" when

$$ N_{\text{cluster}} = \big|{\, r : \text{crop}(r)=c,\ \text{region}(r)=g,\ |\,d(r)-d_0\,| \le 2 \,}\big| \ge 3 $$

  • core/rules.py — a deterministic rule table combining the two signals. We chose deterministic over a black box on purpose: a farmer betting their harvest needs a reason they can verify.

$$ \text{decide}(\text{trend}, N_{\text{cluster}}) = \begin{cases} \textbf{sell} & \text{trend} = \text{falling} \ \textbf{sell} & \text{trend} = \text{flat} \wedge N_{\text{cluster}} \ge 3 \ \textbf{hold} & \text{trend} = \text{flat} \wedge N_{\text{cluster}} < 3 \ \textbf{hold} & \text{trend} = \text{rising} \wedge N_{\text{cluster}} \ge 3 \ \textbf{wait} & \text{trend} = \text{rising} \wedge N_{\text{cluster}} < 3 \end{cases} $$

  • core/matching.py — anchor-buyer matches and the buyer-side aggregation. Given a target $T$ and farmers sorted by harvest date (earliest deadline first), we greedily fill:

$$ a_i = \min!\left(s_i,\ \ T - \sum_{j<i} a_j\right), \qquad \text{shortfall} = \max!\left(0,\ T - \sum_i a_i\right) $$

where si is farmer i's remaining stock and ai the amount taken.

  • data/loader.py + data/sources.py — a pluggable price-source chain behind a PriceSource protocol, so we can swap data sources without the rule engine ever knowing which one answered. The terminal fallback is a cached JSON of real Bank Indonesia data.

Adapter (bot) — python-telegram-bot: thin by design. It collects input via inline buttons (with typed-text fallback), calls the one core write path, and renders friendly Bahasa Indonesia messages. Business rules never live here.

Data: real PIHPS (Bank Indonesia) price history for cabai rawit and beras across Jawa Barat and Jawa Tengah, captured and normalized into the cache.

Infra: migrated from SQLite to PostgreSQL on Supabase, fully dockerized (bot + Postgres + the promo site behind nginx), with a DB_TARGET toggle between local and cloud. 54 unit tests cover the engine directly — the whole thing is demoable from a plain Python script before any bot exists.

Promo site: React + Vite + Tailwind, served as a static container.


Challenges we ran into

  • Rural devices are low-end, so a downloaded app was never viable. Research on technology access in rural Indonesia shows that many farming households run entry-level smartphones with limited storage, constrained data plans, and low digital literacy — conditions under which installing, updating, and learning a dedicated app is a genuine barrier, not minor friction. This constraint shaped one of our earliest decisions: rather than ship yet another download, we built Agria inside a messaging app farmers already have open every day. Turning that limitation into a design principle — meet farmers where they are — is what makes the tool actually reachable.

  • We wanted WhatsApp, but couldn't stand it up in time. WhatsApp is where ~90% of Indonesian farmers already are, so it was our first choice of channel. But the WhatsApp Business Platform requires Meta business verification and a billing setup, and the proactive notifications at the heart of our product — price alerts, sold-out confirmations — are charged per message. That isn't feasible to provision in a hackathon weekend. Telegram's Bot API, by contrast, is free and instant, so we prototyped there — and kept the adapter layer channel-agnostic, so moving to WhatsApp later is a swap, not a rewrite.

  • The price data was behind anti-bot walls. Both PIHPS (Bank Indonesia) and Panel Harga (Badan Pangan) block plain HTTP clients — reCAPTCHA, a WAF that 302-redirects data calls, and a runtime-signed x-api-key. We reverse-mapped every endpoint and parameter, then accepted reality: capture once from a real browser and convert to a cache. It keeps the demo off the live network too.

  • Synchronous DB calls silently stalled the async bot. Our handlers make synchronous Postgres calls to a Tokyo Supabase pooler inside python-telegram-bot's event loop. Under latency, a blocked loop caused Telegram sends to hit the library's default 5-second timeout and throw TimedOut. We diagnosed it from the traceback and raised the HTTP timeouts; the deeper fix (offloading DB work) is on the roadmap.

  • Toolchain landmines. python-telegram-bot 20.x breaks on Python 3.13+; we moved to 22.x. Docker Compose's env_file silently $-interpolated and corrupted our Supabase password until we set format: raw. Container DNS occasionally failed to resolve api.telegram.org when the host network flapped, which we hardened with pinned public resolvers.

  • Designing an honest, explainable engine. The counterintuitive core — "the price is stable, but sell anyway" — had to be both correct and reassuring in plain language, without ever pretending to be an AI oracle it isn't.


Accomplishments that we're proud of

  • It runs on real government data, end to end — not a mock. We can point at the screen and say "that Rp56.150 is Bank Indonesia's actual price yesterday."
  • We built the thing nobody else has: local-glut detection. Every competing app answers "what's the price?"; only Agria answers "your neighbors are all harvesting this week — sell before the crash."
  • Buyer-side aggregation with Earliest-Deadline-First allocation — turning a scattered glut into one fillable order.
  • A distribution strategy, not just a demo: no app to download, meeting farmers inside a chat app they already open every day.

What we learned

  • Adoption beats features. For rural smallholders, the barrier isn't capability — it's behavior change. A chat they already use will out-reach any beautiful app they'd have to install and learn.
  • The economics are sharper than the tech. Coefficient of variation, perishability windows, and synchronized-supply gluts explained the problem far more convincingly than any model.
  • Real data is honest, not dramatic. Live prices were mostly flat — which is exactly why the cluster signal, not the price alone, carries the value. Scoping around that truth made the story stronger, not weaker.
  • Async and infrastructure bite quietly. Blocking calls, DNS, env-file interpolation, and version drift each cost us hours and taught us to verify end-to-end, not just in tests.

What's next for Panen Pas

  • Ship on WhatsApp. Telegram was the prototype; WhatsApp is where ~90% of Indonesians already are. Once business verification and billing are in place, the deliberately channel-agnostic adapter makes this a swap, not a rewrite.
  • From reacting to preventing — harvest staggering. Today we ration the glut; next we spread it. Nudging farmers to stagger harvest dates across the window so the local price never spikes — the full "coordinate or perish" vision.
  • Automated, resilient price ingestion across more crops and regions.
  • A farmer↔buyer negotiation relay, optionally LLM-assisted — but strictly in the adapter layer, never in the deterministic decision path, always with a non-LLM fallback.
  • A real pilot with a koperasi or farmer group, where the anchor buyer is the wedge that solves the cold-start problem.

Panen pas, harga pas.


Team

Built With

Share this project:

Updates