MIT Hackathon · Challenge 01 · DSV-Gruppe An AI-powered city wallet that detects context — weather, time, location, transaction density — and generates the perfect local offer for the moment.
City Wallet turns the corner café into Amazon. It composes live signals (temperature · time of day · nearby merchants · simulated Payone transaction density · user intent) and asks an LLM to write a brand-new offer for this exact user, this exact minute. The offer streams in as a generative-UI widget, the user taps Claim, the merchant scans the QR — the loop closes in under a minute and the dashboard updates live.
Live demo (preview): https://id-preview--4b0f6f1c-876b-4260-95a9-3e87a345688e.lovable.app
- Real context, not a coupon database. Open-Meteo for live weather,
Nominatim for reverse-geocoded city names (forced to English via
accept-language=en+name:ennamedetails + atoLatinsanitizer), Overpass API for nearby cafés/bakeries/restaurants, and a seeded Payone density simulator that shapes "txns/hour" by hour-of-day and weekday. - Generative offers in ~10 s. Lovable AI Gateway (Gemini / GPT-5) writes headline, body, item, discount, validity and a "why this offer" reason. Falls back to a deterministic offer if the gateway is rate-limited, out of credits, times out or returns no tool call — every card is labelled with its source.
- Loop closes end-to-end. Tap Claim → signed token + dynamic QR → merchant scan page → mark redeemed → live merchant dashboard with shown / accepted / redeemed funnel and revenue uplift.
- Privacy by design. Granular consent toggles (location · weather · movement · telemetry). Each toggle visibly skips the corresponding external request on the server. Anonymized mode never echoes coordinates. Movement intent stays on-device. No PII stored.
- Debuggable. Built-in debug drawer shows every regeneration attempt,
cache hit/miss, the raw Nominatim response (
address+namedetails) and thetoLatin-sanitized output, plus a "Clear geocode cache" button that drops the server cache and re-runs everything live.
┌──────────────────┐ ┌──────────────────────┐ ┌──────────────────┐
│ Browser (React) │ │ Worker (SSR + RPC) │ │ Upstream APIs │
│ TanStack Start │──▶│ TanStack server-fn │──▶│ Open-Meteo │
│ Tailwind v4 │ │ computeLiveContext │ │ Nominatim │
│ shadcn/ui │ │ generateOffer │──▶│ Overpass (OSM) │
│ React Query │ │ recordOffer │ │ Lovable AI GW │
│ Zustand-free │ │ acceptOffer │ └──────────────────┘
│ localStorage │ │ redeemOffer │ │
└──────────────────┘ │ clearGeocodeCache │ ▼
▲ └──────────┬───────────┘ ┌──────────────────┐
│ │ │ Lovable Cloud │
└────── live updates ◀───┴──────────────▶│ (Postgres + RLS)│
│ offers, events │
│ event_logs │
└──────────────────┘
- Context Sensing Layer —
src/server/live-context.server.tsaggregates weather, time, location, Overpass merchants and the Payone density simulator into aCityContextblob. TTL-cached (5 min) on the worker; client schedules a refresh 30 s before expiry. - Generative Offer Engine —
src/server/generate-offer.tscalls the Lovable AI Gateway with the merchant rule + context as a tool-call schema. 10-second timeout, deterministic fallback, source labelled on every card. - Seamless Checkout —
src/server/offers.tsissues a signed token,src/routes/redeem.$token.tsxshows the user-facing claim page,src/routes/merchant.scan.tsxis the merchant scanner with a "Redeem now" CTA and optional auto-redeem (with confirmation).
| Layer | Choice |
|---|---|
| Framework | TanStack Start v1 (React 19, file-based routing, SSR) |
| Build | Vite 7 + Cloudflare Workers Vite plugin |
| Styling | Tailwind v4 via src/styles.css (oklch design tokens, no tailwind.config.js) |
| UI primitives | shadcn/ui + Radix |
| State | React state + localStorage (wallet, history, debug log, consent) |
| Server runtime | Cloudflare Worker with nodejs_compat |
| Backend | Lovable Cloud (managed Supabase) — Postgres + RLS + edge functions |
| AI | Lovable AI Gateway (Gemini 2.5 Flash by default, no API key needed) |
| Maps / geo | OpenStreetMap (Nominatim + Overpass), Open-Meteo |
| Tests | Bun-runnable Node scripts (no test framework) |
src/
├─ routes/ TanStack file-based routes
│ ├─ __root.tsx HTML shell, error/404 boundaries
│ ├─ index.tsx Homepage (LiveOffersExperience + hero + modules)
│ ├─ demo.tsx Zeeshan's lunch break — narrated 60-second demo
│ ├─ wallet.tsx Client-side wallet (localStorage)
│ ├─ merchant.tsx Merchant portal (live stats panel)
│ ├─ merchant.scan.tsx QR scanner + redeem CTA + auto-redeem modal
│ ├─ redeem.$token.tsx User-facing claim page
│ ├─ privacy.tsx GDPR / privacy-by-design page
│ └─ api/health.ts Health endpoint
├─ components/
│ ├─ LiveOffersExperience.tsx The main live-context + offer feed (homepage hero)
│ ├─ MeetZeeshanWalkthrough.tsx 4-step floating walkthrough on the homepage
│ ├─ MerchantStatsPanel.tsx 24h funnel + per-hour chart + activity detail
│ ├─ PrivacyByDesign.tsx Granular consent toggles
│ ├─ WorldMapPicker.tsx Click-anywhere map fallback
│ ├─ SiteHeader.tsx Header + footer
│ └─ ui/ shadcn primitives
├─ server/
│ ├─ live-context.ts Thin server-fn shells (RPC wrappers)
│ ├─ live-context.server.ts computeLiveContext + toLatin + pickEnglishName
│ ├─ generate-offer.ts Lovable AI Gateway tool-call + fallback
│ ├─ offers.ts Token issue / accept / redeem / stats
│ └─ log-event.ts event_logs writer
├─ lib/
│ ├─ context-types.ts CityContext, NearbyMerchant, GeneratedOffer
│ ├─ debug-log.ts localStorage-backed debug log
│ ├─ wallet-storage.ts wallet, history, consent, request-status
│ ├─ compose-signals.ts Client-side signal composition (consent-gated)
│ └─ utils.ts cn() helper
├─ integrations/supabase/ Auto-generated client + server admin
├─ styles.css Tailwind v4 + design tokens
└─ router.tsx, routeTree.gen.ts (generated), main.tsx-equivalent shell
tests/
├─ sanitizer.test.ts toLatin + pickEnglishName (Bahawalpur case)
├─ consent-gating.test.ts Per-flag external-call gating
└─ demo.ssr.test.tsx SSR smoke test for /demo
scripts/
└─ check-server-imports.mjs Scans for forbidden @/server/* alias usage
supabase/
└─ config.toml + migrations/ Schema, RLS policies, seed
Each external API call on the server is gated by a granular consent flag passed in from the client:
| Flag | Gates | When OFF |
|---|---|---|
location |
Nominatim reverse-geocode + Overpass nearby merchants | Skips both, falls back to a deterministic city-wide pool ("Café Stuttgart Mitte") |
weather |
Open-Meteo current weather | Returns neutral 14 °C / "warm" |
movement |
On-device intent classifier | userIntent: "stationary" |
telemetry |
(Reserved) | — |
The requestStatus returned by computeLiveContext reports ran /
skipped / failed / fallback per request so the UI can show
per-toggle indicators while requests are in-flight. See
tests/consent-gating.test.ts for the
full contract.
Some Nominatim entries return the local script (e.g. "ضلع بہاولپور"
for Bahawalpur, Pakistan). The pipeline:
- Calls Nominatim with
accept-language=en&namedetails=1. pickEnglishName(["name:en", city, town, …])walks the candidate list, running each throughtoLatin(NFKD decomposition + non-ASCII strip + whitespace collapse). Returns the first one with at least 2 Latin word chars, otherwise""(caller falls back to"your city").- Both raw and sanitized values are exposed in
LiveContextResult.geocodingso the debug drawer can show the round-trip.
Verified by tests/sanitizer.test.ts.
bun install
bun dev # Vite dev server (SSR + HMR)
bun run build # Production build (Cloudflare Worker)
bun run typecheck # tsc --noEmit
bun run lint # eslint
bun run test:ssr # SSR smoke test on /demo
bun run test:consent # Per-flag consent gating contract
bun run ci # typecheck + lint + tests + build
bun run tests/sanitizer.test.ts # toLatin + pickEnglishName.env is auto-managed by Lovable Cloud and contains:
VITE_SUPABASE_URLVITE_SUPABASE_PUBLISHABLE_KEYVITE_SUPABASE_PROJECT_ID
No third-party API keys are required — Open-Meteo, Nominatim and Overpass are all keyless, and Lovable AI Gateway authenticates via Cloud.
| File | What it asserts |
|---|---|
tests/sanitizer.test.ts |
toLatin strips diacritics, drops non-Latin scripts, returns "" for pure Urdu/Japanese; pickEnglishName(["Bahawalpur", "ضلع بہاولپور"]) === "Bahawalpur" |
tests/consent-gating.test.ts |
For every consent flag, the matching external HTTP call is skipped server-side and requestStatus reflects it; "why this offer" chips drop client-side too |
tests/demo.ssr.test.tsx |
/demo renders ≥ 10 KB of HTML containing the expected hero strings (no client JS needed) |
The project ships on Lovable Cloud with auto-deployed edge functions and database migrations. Frontend changes go live by clicking Publish → Update in the Lovable editor; backend changes (migrations, edge functions) deploy automatically on save.
- Open Connectors (root sidebar in Lovable) → GitHub → Connect.
- Authorize the Lovable GitHub App and pick the target org.
- Click Create Repository — Lovable pushes the entire codebase and keeps it in two-way sync.
- Click Publish (top-right desktop ·
…→ Publish on mobile). - Click Update in the dialog — the app goes live at
your-project.lovable.app. - Optionally connect a custom domain from the same dialog.
- Wire the Payone density simulator to a real transaction feed
- On-device intent classifier (TF.js) for
userIntentinstead of the current stub - Multi-merchant tenant onboarding (today's merchant portal is single-tenant for the demo)
- Real cashback settlement via Payone
- i18n beyond English city names (UI copy is currently English-only)
Built for the MIT Hackathon · DSV-Gruppe Challenge 01. All third-party services (OpenStreetMap, Open-Meteo, Lovable AI Gateway) used under their respective terms.