Fast price discovery for the Iranian mobile market. Pick a phone, choose colour + warranty, and VWare crawls multiple Iranian retailers live to show the real, in-stock prices side by side — Persian, RTL, mobile-first.
Lineage. This is the FastAPI 0→1 MVP that validated the price-aggregation pipeline before it grew into the full multi-tenant .NET VWare platform (separate repo). Read it as the "make it work" prototype: a deliberately small stack doing one job well, and heavily tested. 0 → 1 → scale.
Iranian shoppers compare phone prices across a dozen marketplaces — Snappshop, Digikala, HamrahTel, Farnaa and more — each with its own catalogue, stock levels and per-vendor pricing. There is no clean, fast, mobile-first way to answer "this exact model + colour + warranty, cheapest in-stock, right now." VWare does that.
- Type-ahead search over a ~2,900-model catalogue — tokenised, order-independent, Persian and English.
- Guided wizard — budget and use-case in, ranked recommendations out (scored on chipset gaming tier, camera, battery, brightness, …).
- Paste-a-link — drop a product URL from another store and it resolves to the canonical model in the catalogue.
- Live price crawl across suppliers on demand, with a 30-minute price cache.
- Cross-supplier matching — one canonical phone anchored to many retailers' listings (brand + model + storage + RAM + colour, with a fuzzy fallback).
Persian, RTL, mobile-first; the UI is HTMX-rendered partials — no SPA, no build
step. Two shots are worth a look: the mobile search + price table, and the
auto-generated OpenAPI page at /docs. Capture recipe (2 min) in
docs/screenshots/.
| Live price crawler | Search-link matched | Spec source |
|---|---|---|
| Snappshop · Digikala · HamrahTel · Farnaa | + 10 more (Kalamik, GooshiOnline, Mobile140, Zitro, HamrahSeeb, Lipak, …) | GSMArena |
14 supplier profiles in total: 4 with live-crawl price integration, the rest
matched via search-link discovery. See docs/SCRAPING.md for how
data is sourced and the rate-limiting / Terms-of-Service stance.
The catalogue is a durable cache built once; only prices are fetched live, and even those are cached for 30 minutes.
data/dataset.xlsx
│ scripts/probe_mobile.py → live Snappshop API; keep in-stock models
▼
data/picked_models.json + api_samples/*.json
│ app/importer.py (+ gsmarena_catalog_*.json, digikala catalog)
▼
data/vware_mvp.sqlite ── categories · brands · products · variants · guarantees
│ · supplier_product_links (cross-supplier match)
│ app/link_matcher/* anchors 1 canonical product → many suppliers
▼
FastAPI + Jinja2 + HTMX ── user picks product + variant
│
▼ POST /api/prices
LIVE crawl per supplier ──► 30-min PriceCache (SQLite)
│
▼ price table rendered
Design calls worth noting (see PLAN.md / PLAN_UX.md):
- Probe-then-crawl. Everything derivable is imported once into SQLite; the request path only fetches the price of the exact item asked for — never bulk-crawls.
- 30-minute cache trades a little price staleness for a big drop in supplier load and sub-second repeat lookups.
- HTMX over an SPA. Server-rendered partials keep the stack tiny and the mobile payload small — no client framework, no build step.
Front and centre, because correctness here is verifiable:
pytest -q- 351 tests pass against a seeded catalogue DB (27 files: Persian text normalisation, variant/colour extraction, spec parsing, the cross-supplier link matcher, the wizard ranker, and every HTTP endpoint with the live crawler mocked).
- 298 of them run on every push in CI from a clean checkout (no seeded data
required) — that's what the green badge above reflects. The rest are catalogue-
dependent integration tests that need a seeded DB and are run locally (they skip
cleanly in CI — see
tests/conftest.py).
docker compose up # app behind Caddy on http://localhostBrings up two containers: the Uvicorn app and a Caddy reverse proxy (auto-HTTPS
when you point a domain at it — see Caddyfile).
The product catalogue lives in a gitignored SQLite DB, so a fresh checkout starts empty. Seed it with
python -m scripts.probe_mobile && python -m app.importer(needs access to the Iranian supplier APIs). To verify the engine without live access, run the test suite above — it needs no network.
python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
uvicorn app.main:app --reload # → http://127.0.0.1:8000Interactive OpenAPI docs at /docs, health check at /health.
Runs as docker compose (app + Caddy) on a low-spec Iran VPS. Because the target
network is isolated from GitHub/PyPI, scripts/deploy.sh rsyncs
code, runs idempotent migrations, re-imports supplier profiles, restarts the service,
and smoke-tests the key routes — all driven by $VWARE_SSH_* env vars (see
.env.example).
- Python 3.12, FastAPI, Uvicorn
- Jinja2 + HTMX — server-rendered, no JS framework, no build step
- SQLite + SQLAlchemy 2 (async via
aiosqlite) - httpx async client for the supplier crawlers
- pytest + pytest-asyncio
- Docker + Caddy for deployment
app/
main.py FastAPI routes (search, wizard, prices, ingest)
config.py db.py settings + SQLAlchemy models
crawler/ one file per supplier: snappshop, digikala, hamrahtel,
farnaa, gsmarena (+ base.py retry/backoff)
link_matcher/ cross-supplier anchor matching, scoring, colour match
services/ wizard ranking + canonical search
text_utils.py Persian normalisation + variant extraction
templates/ static/ Jinja partials + Idealo-style design system
data/ JSON/xlsx seed corpus (catalog, specs, personas)
scripts/ probe, importers, idempotent migrations, deploy.sh
tests/ pytest suite (27 files)
docs/ DESIGN_SYSTEM.md, SCRAPING.md
An agentic, spec-driven loop — not vibe coding:
- Spec first.
PLAN.md(scope) andPLAN_UX.md(UX + pricing model) are the curated specs the build was driven from; a Persian UX walk-through (docs/user_journey1.pdf) pinned the flow before any code. - Vertical slices. Development ran as ~26 phase branches (
phase-a→phase-zand beyond), each a thin end-to-end slice: a test, an implementation, repeat. - Verify, then trust. Every supplier parser is tested offline against captured fixtures; endpoints are tested with the live crawler mocked. The AI wrote code fast; the human owned the calls that matter — probe-then-crawl over live-scrape, the 30-minute cache trade-off, HTMX over an SPA, and which suppliers earned a full crawler vs. a search-link.
Status: working demo. Search, wizard, cross-supplier matching, live pricing and error-reporting all function; 351 tests green.
Known limits / next: live prices require access to the Iranian supplier APIs; catalogue seeding is a manual step; only 4 of 14 suppliers have full live crawlers. The natural next steps — user accounts, wallet/billing, watchlists, more crawlers — are where the project graduated into the full .NET VWare platform.
MIT © Mahdi Aghakhani.