Portfolio-intelligence dashboard — live prices, watchlist, threshold alerts, a deterministic LLM briefing, and an AI "Oracle" analysis tab, shipped as an installable PWA with a full synthetic demo mode.
Live demo → (synthetic data, no login)
VAULTS is a single-page PWA for tracking a multi-asset portfolio (equities + crypto). It pulls live prices and news, fires threshold alerts, assembles a deterministic markdown briefing designed to paste into an LLM for deep analysis, and offers an Oracle tab that calls the Anthropic API for a structured per-asset assessment. A first-class demo mode (?demo=true) serves a synthetic portfolio and never touches the paid API.
The repo ships with an example USD portfolio and a synthetic demo dataset — no real holdings.
Next.js 14 App Router (no separate backend; API routes are the backend)
Client (SWR hooks, PWA)
│
├─ /api/prices · /api/ticker · /api/btc-chart · /api/news · /api/fng
│ live market data — Yahoo / CoinGecko / Finnhub / Alternative.me
│ fallback chains that surface an error card, never a fake number
│
├─ /api/portfolio · /api/alerts hybrid localStorage + Upstash Redis
│ namespaced vaults:* , backup snapshot before every overwrite
│
├─ /api/briefing pure deterministic markdown assembly
│ (golden-snapshot tested; no LLM call)
│
└─ /api/analyze ── Anthropic API ──▶ structured JSON assessment ("Oracle")
the only paid route; guarded so demo mode never reaches it
middleware.ts → per-IP sliding-window rate limiting via Redis sorted sets
- Hybrid localStorage + Redis with a
wasSeededhandshake — instant first paint from the local cache, server-authoritative persistence in Upstash, and a schema-version check that re-seeds cleanly so users never see ghost data after a shape change. - Only
/api/analyzecosts money, and demo mode is guarded at the top of the handler — every other feed is free. A unit test asserts the demo code path never imports the Anthropic SDK, so a synthetic request can't silently bill tokens. - The briefing is a pure function with a golden-snapshot test — the exact markdown format is the product (it primes an LLM), so
buildBriefing()is deterministic and a snapshot test fails on any format drift. - Per-IP sliding-window rate limiting in middleware — implemented on Redis sorted sets (
ZADD/ZREMRANGEBYSCORE/ZCARD), fail-open if Redis is unreachable, so the paid endpoint is protected without locking users out during an outage. - Price fetchers never fabricate a value — each has a fallback chain (Yahoo
query1→query2→Finnhub for equities; CoinGecko→Finnhub for BTC) and returns an explicit error the UI renders as a card, rather than a misleading number.
- Framework: Next.js 14 (App Router), TypeScript, React 18
- UI: Tailwind CSS, framer-motion, Recharts, Geist, next-pwa
- Data / state: SWR, Upstash Redis (
@vercel/kv), hybrid localStorage cache - GenAI: Anthropic API (structured JSON "Oracle" analysis)
- Testing: Vitest (64 tests: kv layer, alert conditions, rate limiting, demo-mode guard, briefing snapshot)
Prerequisites: Node 18+.
npm install
cp .env.example .env.local # fill in what you need (all optional for a basic run)
npm run dev # http://localhost:3000Environment variables (see .env.example):
ANTHROPIC_API_KEY— enables the Oracle tab; without it the app returns a hand-written fallback analysisCOINGECKO_API_KEY,FINNHUB_API_KEY— higher rate limits / fallback price paths (optional)KV_REST_API_URL,KV_REST_API_TOKEN— Upstash Redis for cross-device persistence (optional; the app runs on localStorage alone)
npm test # vitest
npm run build # production build + type checkTry demo mode locally at /?demo=true.
Live: https://vaults-tracker.vercel.app?demo=true · (screenshots: ./docs/*.png placeholder)
MIT — see LICENSE.