Skip to content

Repository files navigation

City Wallet — The wallet that knows the moment

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


✨ Highlights

  • 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:en namedetails + a toLatin sanitizer), 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 the toLatin-sanitized output, plus a "Clear geocode cache" button that drops the server cache and re-runs everything live.

🧱 Architecture

┌──────────────────┐   ┌──────────────────────┐   ┌──────────────────┐
│  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      │
                                                  └──────────────────┘

Three modules · one loop

  1. Context Sensing Layersrc/server/live-context.server.ts aggregates weather, time, location, Overpass merchants and the Payone density simulator into a CityContext blob. TTL-cached (5 min) on the worker; client schedules a refresh 30 s before expiry.
  2. Generative Offer Enginesrc/server/generate-offer.ts calls the Lovable AI Gateway with the merchant rule + context as a tool-call schema. 10-second timeout, deterministic fallback, source labelled on every card.
  3. Seamless Checkoutsrc/server/offers.ts issues a signed token, src/routes/redeem.$token.tsx shows the user-facing claim page, src/routes/merchant.scan.tsx is the merchant scanner with a "Redeem now" CTA and optional auto-redeem (with confirmation).

🚀 Tech stack

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)

📂 Project layout

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

🔐 Privacy & consent model

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.

toLatin + pickEnglishName

Some Nominatim entries return the local script (e.g. "ضلع بہاولپور" for Bahawalpur, Pakistan). The pipeline:

  1. Calls Nominatim with accept-language=en&namedetails=1.
  2. pickEnglishName(["name:en", city, town, …]) walks the candidate list, running each through toLatin (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").
  3. Both raw and sanitized values are exposed in LiveContextResult.geocoding so the debug drawer can show the round-trip.

Verified by tests/sanitizer.test.ts.


🛠 Local development

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

Environment

.env is auto-managed by Lovable Cloud and contains:

  • VITE_SUPABASE_URL
  • VITE_SUPABASE_PUBLISHABLE_KEY
  • VITE_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.


🧪 Test coverage

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)

🚢 Deployment

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.

Push to GitHub

  1. Open Connectors (root sidebar in Lovable) → GitHub → Connect.
  2. Authorize the Lovable GitHub App and pick the target org.
  3. Click Create Repository — Lovable pushes the entire codebase and keeps it in two-way sync.

Publish a public URL

  1. Click Publish (top-right desktop · → Publish on mobile).
  2. Click Update in the dialog — the app goes live at your-project.lovable.app.
  3. Optionally connect a custom domain from the same dialog.

🗺 Roadmap

  • Wire the Payone density simulator to a real transaction feed
  • On-device intent classifier (TF.js) for userIntent instead 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)

📜 License

Built for the MIT Hackathon · DSV-Gruppe Challenge 01. All third-party services (OpenStreetMap, Open-Meteo, Lovable AI Gateway) used under their respective terms.

About

A privacy-first, location-aware AI wallet that turns your city into a personalized stream of merchant offers, generated on-device the moment you arrive — no tracking, no accounts.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages