Gamified financial education for kids (ages 9–14). Play minigames to earn resources, build your city, take a mortgage, learn about RRSO/APR without touching real money. Pitched to PKO BP as an SKO 2.0 partnership prototype.
🏆 1st place — PKO Gaming track, ETHSilesia 2026. Post-hackathon
development continues here on main; partnership path with PKO BP is
active.
The original XP Arena prototype (9 evergreen games + AI rotation) is preserved at the
xp-arena-final-v1.0tag. Pre-merge feature branches are preserved underarchive/*tags — see "Repo history" below.
Production on https://watt-city.vercel.app. 616 / 616 vitest across 72 files, 14 Playwright specs (~600 E2E assertions), 85 API routes, 48 page routes, 4 locales (423 keys each, zero drift).
Shipped core loop
- Hourly AI rotation: fresh Sonnet/Haiku game every hour, single-flight
lock, idempotent
rotate-if-dueendpoint; three converging triggers (external pinger + Vercel Cron safety net + on-render lazy backstop). See ADRdocs/decisions/001-hourly-rotation-on-hobby.md. - Resource ledger: 7 resources (⚡ 🪙 🧱 🪟 🔩 💾 💵), append-only ledger with SADD-backed idempotency, backfill endpoint for legacy XP Arena users, nav ResourceBar honouring MVP-vs-coming-soon state.
- 20-slot city map at
/miasto: signup-gifted Domek, earn-to-unlock gating, place/upgrade/demolish (50% refund, Domek-protected), slot category restrictions, rate-limited 5 ops/min. - Hourly cashflow tick with 30-day offline catch-up cap, citywide- landmark multiplier, fires on every authenticated render behind a 30-second single-flight lock.
- Mortgage engine: amortized monthly payment (8% standard / 5% preferred), 12/24/36-month terms, cap = min(12 × monthly cashflow, 50 000 W$), credit score 0–100 (+1 on-time / −5 miss / −20 default after 3 consecutive misses), early repayment bonus.
- 9 evergreen minigames + daily AI challenge with a top-3 Hall-of-Fame.
- AI mission reviewer queue at
/admin/review— operator UI to approve / request-changes / reject every live AI mission, with a permanent audit log. Aligned with the governance flow shown in the PKO Internal Review deck §09. Role-gated (role === "admin") for UI; REST endpoint/api/admin/review[/:id]accepts ADMIN_SECRET Bearer for scripted ops. - Parent observer flow (V4.6): parent-link bridge (invite code +
consent),
/rodzicdashboard, GDPR-K gating for under-16 accounts. - Teacher/classroom flow:
/nauczycielclass dashboard, invite codes, per-class leaderboard, teacher onboarding tour. - Notifications: server ledger of tier-ups + mortgage-missed events, bell dropdown with unread badge, quiet-hours push gate.
- Web3 opt-in (default off): soulbound
WattCityMedalERC-721 on Base Sepolia, parent-gated mint, burn-on-revoke (GDPR Art. 17). - 4 langs (PL default · UK · CS · EN).
Hardening since 2026-04-19 hackathon day
- awardXP single-flight race fixed (
kvSetNXlock + 5× exp-backoff). - CSRF double-submit via
proxy.ts(Next.js 16 middleware rename) +CsrfBootstrapmonkey-patchingwindow.fetchso every mutating call picks upx-csrf-tokenwithout per-component wiring. - Cron-auth consolidated to
lib/cron-auth.ts(4 duplicates collapsed), NODE_ENV-gated dev bypass. - Per-IP rate-limit for
/api/auth/register+/login; parental-consent mail via Resend → SendGrid → log-only fallback (lib/mailer.ts). - 2026-04-22 UX/E2E sweep: unlit buildings render true silhouettes
(hero-copy parity),
/api/scoreparallelises Redis hops, notification popover repositioned so it stops bleeding through the resource-bar, onboarding tour useskeepalive+ localStorage so the modal never resurrects after the user completes it, Playwright webServer blanked Upstash env sogp_*/pr_*/… test accounts no longer leak into the production leaderboard (+ new/api/admin/purge-e2e-accountscleans up historical leakage). Seedocs/progress/2026-04-22-ux-fixes-batch.md.
Near-term, no specific date — tracked in docs/SKO-BACKLOG.md:
- PKO partnership path: audit closure for the web3 layer, handoff docs for SKO 2.0 product team. Precondition for mainnet.
- City widget iteration (home page): the "Tvé město" card's empty centre needs content — candidates are a mini-skyline preview, XP-today / streak / to-next-level stat trio, or a "today's challenge" CTA. Design still open, flagged from the 2026-04-22 user-testing pass.
- Real-device mobile matrix: the Playwright mobile-safari / mobile- chrome specs pass against bundled webkit/chromium, but full real-device coverage (actual iOS Safari class-rule parser, Android Chrome PWA install flow) is pending.
- Content expansion: more per-kind evergreen questions to stretch
the 9-game pool, native-speaker polish pass on the UK locale (calques
flagged in
docs/progress/2026-04-22-docs-review.md). - Observability: structured JSON logs already in place; external sink (Grafana / Datadog) + alerting is the next step.
- School pricing: per-class feature flag + stripe-backed plan — scope still being sized; partnership conversations are upstream.
Longer-term, subject to product direction:
- Marketplace for resource trading between kids (ADR pending — see D10).
- PKO Junior mirror: read-only parental mirror of the kid's city into the real PKO Junior app surface.
- Resource decay / seasonal events to sustain engagement past the initial build-out phase.
- Next.js 16 (App Router, Turbopack) + React 19.2
- TypeScript strict
- Tailwind CSS 4
- Upstash Redis for persistent leaderboards, user stats, AI-game storage, duel rooms (in-memory fallback for local dev)
- Zod for API-payload and AI-pipeline validation
- HMAC-signed cookie sessions + scrypt password hashes (no auth library)
- Anthropic SDK (
@anthropic-ai/sdk) for the daily AI-generated challenge - i18n across PL (default), UK, CS, EN — cookie-driven, server-rendered
- 9 evergreen minigames: Finance Quiz, Math Sprint, Memory Match, Word Scramble, Budget Balance, Currency Rush, Power Flip, Energy Dash, Stock Tap
- Daily AI challenge:
claude-sonnet-4-6generates a fresh quiz / scramble / price-guess every 24 h (Vercel Cron). Up to 3 live AI games at once; top-3 players get a permanent medal in Hall of Fame. - Duel mode: create a 6-char code, 2 players race the same seeded round, winner gets a global-leaderboard bonus.
- Level system: 9 tiers, each unlocking new building upgrades in the Katowice cityscape SVG.
app/
api/
auth/{register,login,logout}/route.ts
score/route.ts # accepts both canonical + ai-<id> game IDs
leaderboard/route.ts
duel/{create,[code]}/route.ts
cron/daily-game/route.ts # Vercel Cron entrypoint for AI pipeline
lang/route.ts # cookie-based locale switcher
games/
page.tsx # hub + CityScene
<9 evergreen games>/page.tsx
ai/[id]/page.tsx # renders an active AI game by ID
duel/, leaderboard/, sin-slavy/, o-platforme/, ochrana-sukromia/
page.tsx # landing / dashboard
components/
games/ # per-game client components + ai-*-client.tsx
city-scene.tsx # SVG panorama, wires construction site to live AI game
site-nav.tsx, dashboard.tsx, ...
lib/
ai-pipeline/
types.ts # zod schemas (GameSpec discriminated union)
research.ts # deterministic theme rotation
generate.ts # Claude call w/ prompt caching + mock fallback
publish.ts # Redis persist, TTL, portfolio dedup, eviction
redis.ts, session.ts, auth.ts, leaderboard.ts, duel.ts,
games.ts, user-stats.ts, level.ts, i18n.ts, i18n-server.ts
content/ # per-game PL/UK/CS/EN question + scenario pools
locales/ # 4-language dict
pnpm install
cp .env.example .env.local # optional; without Upstash the app runs in-memory
pnpm dev # http://localhost:3000Without ANTHROPIC_API_KEY the AI pipeline serves deterministic mock specs
(model: "mock-v1") — judges can still hit /api/cron/daily-game to mint
one, then play it at /games/ai/<id>.
- Register in
lib/games.ts(id,title,xpCap, building glyph, …). - Create
app/games/<id>/page.tsx— server component, pull session, redirect to/loginif unauthenticated, passdictto the client. - Put the game loop in
components/games/<id>-client.tsx. - On round end, POST
/api/scorewith{ gameId, xp }. The backend caps XP byxpCapand writes toxp:leaderboard:global+xp:leaderboard:game:<id>. - Add per-lang content if needed (e.g.
lib/content/<id>.tswithPL_ / UK_ / CS_ / EN_pools +*For(lang)accessor).
UPSTASH_REDIS_REST_URL+UPSTASH_REDIS_REST_TOKEN— persistent storageSESSION_SECRET— HMAC key for cookie sessions (production: required, ≥16 chars)ANTHROPIC_API_KEY— enables the real AI-game generator (omit to stay on mocks)CRON_SECRET— shared secret Vercel Cron / external pinger send inAuthorization: Bearer; omit during dev to allow any callerADMIN_SECRET— required by/api/admin/*in productionREGISTER_IP_LIMIT/LOGIN_IP_LIMIT— per-IP rate-limit ceilings for/api/auth/registerand/api/auth/login(defaults 5/h and 20/min; Playwright bumps both to1000so test runs aren't self-throttled)- Mail adapter (
lib/mailer.ts; parental-consent invites, falls back to log-only if nothing is configured):RESEND_API_KEY— preferred providerSENDGRID_API_KEY— fallback providerMAIL_FROM— defaultFromaddress (e.g.Watt City <no-reply@watt-city.vercel.app>)
APP_BASE_URL— absolute base for parent-consent links emitted from server code that has no request context (defaulthttps://watt-city.vercel.app)- Web3 (opt-in, default off — see "Web3 surface" below):
NEXT_PUBLIC_WEB3_ENABLED=trueactivates the/profileon-chain galleryNEXT_PUBLIC_WEB3_CHAIN_ID=84532(Base Sepolia testnet)NEXT_PUBLIC_WC_PROJECT_ID— WalletConnect / Reown cloud project idWEB3_CONTRACT_ADDRESS— deployedWattCityMedaladdressWEB3_RELAYER_PRIVATE_KEY— EOA funded with testnet ETH (never committed)NFT_STORAGE_API_KEY— IPFS pin service for medal metadata
Watt City ships an optional soulbound-NFT layer for ETHSilesia 2026's
Web3/Base track. Kids' off-chain achievements mirror to on-chain
WattCityMedal tokens only with parent consent (V4.6 observer flow)
and burn on consent revocation (GDPR Art. 17). Every under-16 account
without web3OptIn === true is hard-gated server-side (HTTP 403) — the
client cannot bypass it. Default path of the app has zero web3 imports
(tree-shaken behind NEXT_PUBLIC_WEB3_ENABLED); 99% of users never see
a wallet button.
| Field | Value |
|---|---|
| Chain | {{CHAIN_NAME}} (chainId {{CHAIN_ID}}) |
| Contract | WattCityMedal — soulbound ERC-721, ~200 LOC, invariant-tested |
| Address | {{CONTRACT_ADDRESS}} |
| BaseScan | {{CONTRACT_ADDRESS}} |
| Source | contracts/WattCityMedal.sol |
| Tests | contracts/test/WattCityMedal.test.ts (source-level invariants) |
| Audit | Post-pilot. Mainnet gated on external audit per docs/web3/DEPLOY.md §5 |
| Submission one-pager | docs/web3/SUBMISSION.md |
| Architecture | docs/web3/PLAN.md |
| Runbook | docs/web3/DEPLOY.md |
| Demo video (2 min) | {{VIDEO_URL}} |
Running the Web3 surface locally requires Hardhat 2.x on Node 22 LTS (or
Foundry); see docs/web3/DEPLOY.md §1–§3 for the toolchain. Do not
deploy to Base mainnet before the audit closes.
docs/SMOKE-TEST.md is the end-to-end runbook. Takes ≤ 10 min covering
auth, game → resource, build/upgrade/demolish, cashflow tick, mortgage,
rotation, language switch, mobile.
pnpm test runs the vitest suite (616/616 across 72 files as of
2026-05-15 — research bucket, rotation idempotency, resource yield math,
ledger dedupe, building place/upgrade/demolish, tick catch-up with 30-day
cap, amortization formula, default after 3 misses, cron auth matrix,
rate-limit keying, mailer fallback, awardXP lock, parent-link redeem).
Playwright E2E (16 specs): pnpm test:e2e covers smoke, prod-smoke,
api-contracts, security, data-integrity, a11y-matrix, golden-paths,
perf, production-ready, rate-limits, bot-protection (opt-in), pwa,
smoke.mobile, smoke.cross, walkthrough (full anonymous + kid + teacher
visual capture), ux-fixes (demo-review punch list — drawer focus,
landing hierarchy, anonymous demo flow, public-surface filter).
pnpm test:e2e # all specs, chromium-only
pnpm exec playwright test ux-fixes # focused: punch list smoke
pnpm exec playwright test --project=chromium walkthrough
# 56-route capture + axe scan
playwright.config.ts defaults baseURL to http://localhost:3000
and auto-starts pnpm dev for the webServer. No spec hardcodes
a port — every test calls page.goto("/...") and the config decides
the host. Override the target for preview/prod runs:
PLAYWRIGHT_BASE_URL=https://watt-city.vercel.app \
PLAYWRIGHT_WEBSERVER=0 \
pnpm exec playwright test --project=chromium ux-fixes
Setting PLAYWRIGHT_WEBSERVER=0 skips the local pnpm dev boot and
points the suite at the URL you set.
E2E test accounts never reach the production Upstash —
playwright.config.tsblanksUPSTASH_REDIS_REST_URL+_TOKENfor its webServer solib/redis.tsfalls back to its in-memory store. If you accidentally ran an older commit against prod,scripts/purge-e2e-accounts.shcleans historicalgp_*,pr_*,rl_*, … leftovers via/api/admin/purge-e2e-accounts(admin-bearer,--committo actually delete; dry-run by default).
docs/README.md is the index. Highlights:
docs/SKO-BACKLOG.md— 11-phase backlog, ~300 itemsdocs/ECONOMY.md— every number, every formuladocs/ARCHITECTURE.md— schemas, API contracts, Redis keysdocs/OPERATIONS.md— deploy, monitor, recoverdocs/decisions/— ADRsdocs/progress/<date>.md— agent session logs
Single-branch workflow (main only) since 2026-04-20. Pre-merge state is
preserved in git tags:
| Tag | What |
|---|---|
xp-arena-final-v1.0 |
XP Arena prototype final commit (f1b294a) before Watt City merge |
archive/watt-city-v3 |
21 commits of V3.1..V3.7 development trail |
archive/watt-city-cleanup |
7 commits of UX-audit cleanup |
archive/watt-city-refactor |
V2 critical polish + feature-flag admin |
archive/ux-fixes-2026-04-19 |
12 commits of UX fixes (cashflow-hud, onboarding-tour, toasts, cookie-consent) |
archive/ux-audit-2026-04-19 |
UX audit gap matrix + 6 personas |
Recover any archive: git checkout archive/<name>.