The problem
Online price comparison stops at the screen. In Tehran, a lot of shopping still happens in person, in small shops, arcades (پاساژ) and malls, and that's where it goes wrong:
- "I don't know which model is right."
- The listed price is old, and nobody knows whether the item is in stock.
- You cross the city and the shop is closed, or the owner never picks up.
- You find the mall but not the shop inside it.
- You can only buy on installments, and you don't know who offers them.
The challenge asked for a flow that covers four steps: choose → find stores → compare stores → contact stores, plus all five pain points.
Four steps, five places it goes wrong.
Each pain point sits under the step where it hurts · click a step to jump to it
What it does
Choose: search that understands plain Persian
Type «گوشی سامسونگ زیر ۶۰ میلیون اقساطی» ("Samsung phone under 60 million, on installments") and Bebin pulls out the budget, the brand and the payment condition as removable chips. If you're unsure, a need-finder asks short questions about budget and use, and you can compare products side by side.
Find: stores near you, honestly labelled
Sellers appear in a list and on a map, ranked from your location, with walking or taxi times. Shops that only match the category are kept in a separate group. The app never implies they carry the item.
Compare: rankings that explain themselves
Sort by cheapest, nearest, or a balance of both. Each shop shows whether it will be open when you arrive, not just now, and a «چرا این ترتیب؟» ("why this order?") sheet breaks down the score.
What "balanced" means.
Share of the score per factor, by ranking mode · real weights from the ranker
| Mode | price | distance | open on arrival | fresh | verified |
|---|---|---|---|---|---|
| balanced | 0.35 | 0.30 | 0.15 | 0.10 | 0.10 |
| cheapest | 0.60 | 0.15 | 0.10 | 0.10 | 0.05 |
| nearest | 0.15 | 0.60 | 0.15 | 0.05 | 0.05 |
Ask: «از فروشگاه بپرس»
Ask up to three shops about today's price, stock, exact location or installments. Replies arrive live. A confirmed answer gets a stamp (مهر) with the time it was given, and it replaces the old listed price.
If a shop doesn't answer, or says it's out of stock, Bebin offers alternatives in order: the same product at another shop, similar models at the same shop, then similar models nearby.
The life of a question.
Boxes = states, ink outline = the outcome you want, dashed = dead ends that hand off to alternatives
Go: a plan that knows what will be open
The visit plan orders your stops, groups the ones in the same mall, and warns you when a shop will be closed on arrival. It can reschedule («برای شنبه ۱۰:۰۰ بچین», "plan it for Saturday 10:00") or swap the stop for an alternative. Mall pages show the floor, unit and neighbouring shops, so you can find the shop once you're inside.
Also
- An assistant chat that uses the same tools: it searches, compares, finds sellers, asks stores (only after you confirm) and plans the visit. Its answers render with the same cards as the regular screens.
- A seller panel (
/seller): open it on a second phone and answer the inquiries as the shop.
Honesty, by design
The dataset has no stock column, every phone number is fictitious, and most optional fields are empty. Instead of hiding that, Bebin is built around it. These rules are enforced in the API, not only in the UI:
- Stock is never stated unless a seller answered, and the answer shows when they answered.
- Listed price, last price change and seller verification are always shown separately.
- Unknown is a first-class value: «نامشخص», drawn dashed, with a way to find out.
- Simulated seller replies are labelled «پاسخ شبیهسازیشده» ("simulated reply").
Saturation = certainty.
How Bebin draws a price, from most to least certain · drawn here in ink
How fresh are the prices?
Age of each listed price, measured against the snapshot date (2026-10-13)
| Age | Share of prices (%) |
|---|---|
| Older than two weeks | 55 |
| Newer | 45 |
How it works
Two apps, two SQLite files.
Boxes = processes, cylinders flattened to boxes = data, dashed = optional
Key decisions
- Few moving parts. Two apps and two SQLite files: no Redis, no queues, no workers. The read-only catalog and the user data live in separate files, so rebuilding the catalog never touches inquiries.
- Deterministic code ranks and filters; the LLM understands, asks, explains and writes. Rankings are reproducible and testable, and an LLM outage can't break a core flow.
- Hybrid search. SQLite FTS5 keyword search fused with vector embeddings (reciprocal rank fusion, rescaled onto each query's keyword-score range). It handles typos and synonyms: «کفش ورزشی» ("sports shoes") went from 11 results to about 70.
- Live updates over SSE, with replay after a reconnect and an in-process scheduler that survives restarts.
- The API returns view-ready data: reasons, trust fields, distances, open status. The frontend and the assistant reuse the same payloads, and the TypeScript types are generated from the OpenAPI schema.
- Built for Iran: self-hosted fonts, logical (RTL-safe) CSS only, Persian digits and Jalali dates, deployed on an Iranian host.
Design
The whole identity uses two hues: a radish's red and its leaves' green. Bright radish means "you can do this" and marks every call to action. Dark beet means "something's wrong". The «oo» in the wordmark is a pair of glasses, which doubles as the search icon.
The mascot is «تربچه», a 24×24 pixel radish that appears only at moments that matter, at most once per screen: searching, waiting for a reply, happy with the stamp, sleepy when a shop is closed, wilted when you're offline.
All 25 designed screens were built, with 44 px touch targets, visible focus rings, reduced-motion support, and colour never used as the only signal.


