Real-time agricultural commodity prices from Indian mandis (markets), sourced from the government Agmarknet feed on data.gov.in. Built for farmers, traders, and anyone who wants to know what tomatoes are going for in their nearest market today.
- ๐ Search & multi-select commodities and states โ pick Tomato + Onion across Karnataka + Tamil Nadu in one query
- ๐ฐ Ranked prices per mandi: modal โน/kg, minโmax range, variety, district, "as of" date
- ๐ 8 Indian languages โ English, เคนเคฟเคจเฅเคฆเฅ, เคฎเคฐเคพเค เฅ, เฒเฒจเณเฒจเฒก, เคจเฅเคชเคพเคฒเฅ, เฎคเฎฎเฎฟเฎดเฏ, เฐคเฑเฐฒเฑเฐเฑ, เฆฌเฆพเฆเฆฒเฆพ (cookie-persisted)
- ๐ Light ยท Dark ยท System theme, warm Claude-inspired palette
- ๐ฑ Mobile-first โ sticky header, touch-sized controls, installable PWA (manifest + icons)
- ๐ Daily auto-ingest at 6 PM IST via Vercel Cron โ no manual refresh
- ๐ก JSON API for power users:
/api/prices?commodity=Tomato&state=Karnataka
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ data.gov.in Agmarknet API โ free, public, daily-updated mandi feed
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ paginated fetch (1 page = 1000 records, cap ~10k/day)
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ /api/cron/ingest โ Vercel Cron ยท 30 12 * * * UTC = 6 PM IST
โ (Next.js Route, node runtime)โ ยท Bearer CRON_SECRET auth
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ upsert in 500-row batches, dedupe by (mandi, commodity, variety, date)
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Supabase Postgres โ free tier ยท 500 MB ยท public-read RLS
โ states / districts / mandis โ
โ commodities / price_records โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ public-read via anon key
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Next.js App Router (SSR) โ picker โ /prices?commodity=X&state=Y
โ shadcn/ui + Tailwind v4 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
| Layer | Choice | Why |
|---|---|---|
| Framework | Next.js 16 App Router + TypeScript | SSR, route handlers, Vercel Cron native |
| Styling | Tailwind v4 + shadcn/ui | Mobile-first utilities, accessible primitives |
| Database | Supabase (Postgres) | Free 500 MB, instant REST, row-level security |
| Data source | data.gov.in Agmarknet | Official, free, no scraping |
| Cron | Vercel Cron | 1 job/day on free hobby tier |
| Hosting | Vercel free hobby | $0 at any reasonable traffic |
| Themes/i18n | next-themes + cookie-based dictionary |
No client routing changes |
Estimated cost at <10K daily users: $0.
# 1. Clone + install
git clone https://github.com/skalkii/mandibazar.git
cd mandibazar
pnpm install
# 2. Configure env
cp .env.example .env.local
# fill in 5 values โ see "Environment variables" below
# 3. Initialize the database
# Open Supabase SQL Editor โ paste db/schema.sql โ Run
# 4. Seed some data
pnpm seed # last 3 days (override with SEED_DAYS=N)
# 5. Start dev server
pnpm dev # โ http://localhost:3000Pick a commodity + (optional) state โ click See prices โ ranked list of mandis.
Copy .env.example to .env.local and fill these 5 values:
| Variable | Where to get | Notes |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Supabase Dashboard โ Project Settings โ API โ "Project URL" | Just the base, no /rest/v1/ suffix |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Same page โ "Project API Keys" โ anon public | Safe in browser; RLS protects data |
SUPABASE_SERVICE_ROLE_KEY |
Same page โ "Project API Keys" โ service_role | |
AGMARKNET_API_KEY |
Register at https://data.gov.in/user/register โ profile โ "API Key" | One key per account; works for any dataset |
CRON_SECRET |
Generate: openssl rand -hex 32 |
Used to auth the ingest endpoint |
Optional tuning:
| Variable | Default | What |
|---|---|---|
SEED_DAYS |
3 | Days back pnpm seed ingests |
BACKFILL_DAYS |
30 | Default for pnpm backfill with no arg |
| Command | What it does |
|---|---|
pnpm dev |
Start Next.js dev server |
pnpm build |
Production build (compiles routes + types) |
pnpm start |
Run production build locally |
pnpm lint |
ESLint |
pnpm typecheck |
tsc --noEmit |
pnpm seed |
Ingest the last $SEED_DAYS days (default 3) |
pnpm backfill [days] |
Ingest the last N days (e.g. pnpm backfill 30) |
pnpm tsx scripts/smoke.ts |
Verify Supabase connection + row counts |
states (id, name)
districts (id, state_id, name)
mandis (id, district_id, name, latitude?, longitude?)
commodities (id, canonical_name, hindi_name?, kannada_name?, category?)
price_records (id, mandi_id, commodity_id, variety, arrival_date,
min_price_per_quintal, max_price_per_quintal,
modal_price_per_quintal, ingested_at)
UNIQUE (mandi_id, commodity_id, variety, arrival_date)
All tables have public-read RLS policies. Writes only via service-role key (cron + seed scripts). See db/schema.sql.
Migrations live in db/migrations/. Run them in Supabase SQL Editor after the base schema.
Read-only JSON endpoints:
GET /api/commodities
โ { "commodities": ["Ajwan", "Apple", โฆ] }
GET /api/locations
โ { "states": ["Andhra Pradesh", "Bihar", โฆ] }
GET /api/locations?state=Karnataka
โ { "districts": ["Bagalkot", "Bangalore", โฆ] }
GET /api/prices?commodity=Tomato&commodity=Onion&state=Karnataka
โ {
"commodities": ["Tomato", "Onion"],
"states": ["Karnataka"],
"latest_date": "2026-05-14",
"count": 38,
"prices": [ { mandi, district, state, modal_per_kg, โฆ } ],
"byCommodity": { "Tomato": [...], "Onion": [...] }
}Multi-value: repeat the param key (?commodity=A&commodity=B).
- Sign in: https://vercel.com โ "Continue with GitHub".
- Add New โ Project โ import this repo.
- Click Environment Variables โ paste the same 5 keys from your
.env.local. Mark the 3 server-only ones (SUPABASE_SERVICE_ROLE_KEY,AGMARKNET_API_KEY,CRON_SECRET) as Sensitive. - Deploy. ~90 sec โ live at
https://mandibazar-<hash>.vercel.app. - Cron auto-wires from
vercel.json(30 12 * * *UTC = 6 PM IST). Vercel injectsAuthorization: Bearer ${CRON_SECRET}automatically.
Smoke test:
https://your-app.vercel.app/https://your-app.vercel.app/api/commoditieshttps://your-app.vercel.app/api/prices?commodity=Tomato&state=Karnataka
| Service | Free tier | This app's load |
|---|---|---|
| Vercel | 100 GB bandwidth/mo | JSON tiny, far below |
| Vercel cron | 2 jobs / day | Uses 1 |
| Vercel funcs | 100k invocations/mo | Page reads cheap |
| Supabase | 500 MB storage | ~10k rows/day fits easily |
| data.gov.in | ~100 req/min | 1 cron/day, ~10 calls |
app/
api/
cron/ingest/route.ts # daily ingest (Bearer-auth'd)
commodities/route.ts # GET commodity list
locations/route.ts # GET states or districts
prices/route.ts # GET filtered prices (multi)
prices/page.tsx # results SSR
page.tsx # landing (picker)
layout.tsx # theme + i18n + header + footer
globals.css # Tailwind v4 + theme tokens
components/
ui/ # shadcn primitives (button, card, select, โฆ)
Header.tsx # sticky, theme + lang switchers
Footer.tsx # data attribution + GitHub
PickerForm.tsx # commodity + state multi-select
MultiSelect.tsx # searchable, multi, chips
PriceCard.tsx # ranked mandi card
ThemeProvider.tsx # next-themes wrapper
ThemeToggle.tsx # light/dark/system
LanguageSwitcher.tsx # 8 locales, cookie
lib/
agmarknet.ts # data.gov.in API wrapper (retry, content-type guard, paginator)
ingest.ts # upsert pipeline (dedupe + 500-row batches)
prices-query.ts # shared price query (used by /api/prices + /prices SSR)
queries.ts # commodity + state list helpers
supabase.ts # browser + server clients
format.ts # INR + date formatters (Intl)
normalize.ts # DD/MM/YYYY parse, canonical names, quintalโkg
types.ts
utils.ts # cn() helper
i18n/
dictionaries.ts # 8 locale strings
server.ts # getLocale() via cookie
db/
schema.sql # full schema + RLS โ paste into Supabase
migrations/ # ordered ALTERs
scripts/
_env.ts # loads .env.local then .env
seed-locations.ts # pnpm seed
backfill.ts # pnpm backfill N
smoke.ts # connection + row count check
public/
favicon.svg, favicon.ico, apple-touch-icon.png, icon-192/512.png
site.webmanifest # PWA installable
- Open
lib/i18n/dictionaries.ts. - Add the locale code to the
LOCALEStuple andLOCALE_NAMESmap. - Copy the
endictionary, translate each value. - Add it to the
dictionariesrecord at the bottom.
That's it โ switcher picks it up automatically.
PGRST125 Invalid pathon Supabase calls โ yourNEXT_PUBLIC_SUPABASE_URLincludes/rest/v1/. Strip it; use justhttps://<ref>.supabase.co.invalid input syntax for type integerduring seed โ migration001_prices_to_numeric.sqlnot run, or you're on an oldlib/ingest.tsthat didn't round. Latest code rounds at the boundary, so any int column accepts the data.- Empty pickers in dev โ no seed yet. Run
pnpm seedonce. - Empty results for "Karnataka + Tomato" right after first seed โ first seed sometimes truncates due to the rare duplicate-in-batch issue; re-run
pnpm seed. - Cron returns 401 in Vercel โ
CRON_SECRETenv var isn't set in Vercel project settings.
- WhatsApp deep-link share button
- Geolocation auto-detect nearest district
- 7-day modal-price trendline per mandi
- Saved searches + email/WhatsApp digest
- Hindi/Kannada commodity name secondary labels (currently UI-only i18n)
- Map view
- SMS interface (Twilio India)
MIT.
- Data: data.gov.in Agmarknet ยท Ministry of Agriculture and Farmers Welfare
- UI primitives: shadcn/ui
- Icons: lucide
- Fonts: Inter, Newsreader, Geist Mono