Watstock 2 — Developer Handbook
One book for the web desk and the mobile app. Read this before you open a ticket or add a screen.
The product is an advisory research desk. It scores a fixed US universe five trading sessions ahead. It never executes, never holds funds, and never acts as a broker.
| You are… | Jump to |
|---|---|
| Shipping the website | Web |
| Shipping iOS or Android | Mobile |
| Calling the API from any shell | HTTP API |
| Wiring sign-in or Stripe | Auth · Entitlements |
| Matching the white / cyan desk | Design |
Companion pages in this folder: product · architecture · API · auth · entitlements · client apps · design · compliance · local setup.
Application name: Watstock 2. GitLab: https://gitlab.com/feymanlabs/watstock2.git. Feynman slug: watstock2.
1. What we are building
Every published call is one of three words — BUY, HOLD, SELL — scored over five trading sessions.
The engine blends:
- Live tape (Massive / Polygon, Yahoo fallback)
- FinBERT + FinLex headline sentiment
- A small hybrid technical ensemble
- A 10% World Monitor equity-layer sleeve (licensed globe + CII)
Users paper-trade the call, then send a ticket to their own bank. The scoreboard is public. Nothing is backfilled. Misses keep a forensic line.
What we are not
| Forbidden | Why |
|---|---|
| Order routing, broker APIs, wallets, custody | We do not execute or hold funds |
| “Guaranteed” or unsourced accuracy claims | Retired 2017 language. Illegal and false |
| 30-day / any-asset / commodity predictions | Out of scope |
| IBM Watson, legacy partner logos | Retired |
| Personalized investment advice | Footer says research only |
| Editing or deleting a published prediction | The record is the product |
| Showing Free users ranks 1–10 | The top of the book is Pro |
If a feature needs execution, funds, or a suitability conversation, it does not ship.
Horizon and correctness (methodology_v1)
Fixed. Public on /method. Never tuned retroactively.
| Signal | Correct when |
|---|---|
| BUY | 5-session return >= +1.0% |
| SELL | 5-session return <= −1.0% |
| HOLD | abs(return) < 1.0% |
A name with fewer than 10 resolved calls shows “insufficient history”, never a percentage. Changing the rule requires methodology_v2 and forward-only scoring.
Universe
Fixed list in `src/lib/universe.ts`. Clients must not invent tickers. Adding a name is a backend change so the tape, layer map, and scoreboard stay aligned.
Sectors: Technology, Communication, Consumer, Financials, Healthcare, Energy, Industrials. Listed hedges include GLD, USO, FRO.
Voice
Editorial, precise, short. White paper, cyan #00A4D1, ink #071018. No hype. No emoji in product chrome.
Compliance footer (verbatim, every prediction surface and ticket):
WATSTOCK provides research and analytics, not personalized investment advice. Markets involve risk, including loss of capital. Past performance of our scoring does not guarantee future results. WATSTOCK does not execute orders, hold funds, or act as a broker-dealer.
Source: `src/lib/compliance.ts`. Do not paraphrase.
2. System map
Web (this repo) Desktop shell iOS Android
TanStack Start Tauri / Electron Swift or Expo Kotlin or Expo
\ | | /
\ | | /
+----------------+--------+--------+---------------+
|
HTTPS + session
|
Watstock 2 API
/api/tape /api/stock /api/chat /api/billing
/api/paper /api/geo /api/auth /api/scoreboard
|
Massive/Yahoo Stripe World Monitor Pro
FinBERT/FinLex Checkout CII + licensed map
|
Postgres / PGLite
predictions · subscription · paper · watchlist
chat_query · chat_quotaOne API. Four shells. Do not fork the scoring engine into a client.
Gate on the server. A blur on the client is decoration. Official rank is assigned after sorting by expected 5-day return — clients display row.rank, never index + 1 of a visible slice.
3. Web (this repository)
This GitLab project is the web product. Ship it at the marketing domain and at https://www.feynmanlabs.com/apps/watstock2.
Stack
React 19 · TypeScript · Vite · TanStack Start / Router / Query · Tailwind v4 · Better Auth · Stripe · PGLite (preview) / Postgres (prod).
How to run
npm install
# optional: set -a && source .secrets && set +a
npm run dev # 0.0.0.0:8080
npm run typecheck
npm run build
npm start # scripts/serve.mjs — honors PORTDocker: docker compose up --build. Migrations: npm run db:migrate (migrations/0001_*.sql … 0006_chat.sql). Add a new numbered file; do not edit applied ones.
Routes to keep stable
These are deep links for desktop and mobile.
| Path | Screen |
|---|---|
/ | Weekly ranking (Free: 11–15 only) |
/stock/$symbol | Name + 5-day (gated) |
/world | Licensed World Monitor map + layer |
/chat | Beginner desk chat |
/scoreboard | Public record |
/desk | Paper book |
/handoff | Send picks to your broker — files only, no execution |
/watchlist | Names |
/method | 1% rule |
/pricing /signup /login /account | Commercial |
/t/$id | Public ticket |
/alerts | Pro alerts |
/deck | Investor pitch (12 slides) |
/handbook | This handbook, in-app |
Where code lives
| Path | Role |
|---|---|
src/routes/* | Pages |
src/routes/api/* | HTTP JSON — the contract for every client |
src/lib/entitlements.ts | Tiers, plans, 1% rule, chat caps |
src/lib/predict.ts | Advice types + trading calendar |
src/lib/market.server.ts | Live tape + ensemble (server only) |
src/lib/equity-layer.ts | World Monitor feature classes + mapped book |
src/lib/chat/* | Desk chat: topics, context, Grok, quota |
src/lib/billing/stripe.server.ts | Checkout, portal, webhooks |
src/lib/auth/* | Better Auth + Feynman session |
src/lib/paper.ts | Mock book, $100,000 starting cash |
src/lib/api-client.ts | Fetch wrappers — copy these into native until @watstock/sdk exists |
src/styles.css | Design tokens |
migrations/ | Schema |
Secrets never leave the server. Read them with runtimeEnv() in src/lib/env.server.ts.
Adding a page
- Create
src/routes/<name>.tsxwithcreateFileRoute("/name"). - Vite regenerates
src/routeTree.gen.ts— do not hand-edit it. - Link from
src/components/app-shell.tsx(desktop) and/ormobile-nav.tsx(tabs). - Keep the page usable at 390×844. Tap targets ≥ 44px.
Web navigation (current)
- Desktop masthead: Hotstocks · Chat · World · Scoreboard · Paper desk · Watchlist · Methodology · Deck
- Mobile tabs: Hotstocks · Chat · Desk · Watch
App Store mount
The Feynman store mounts at /apps/watstock2. Middleware strips that prefix so routes stay /, /api/tape, /stock/AAPL. Honor PORT / HOST / BASE_PATH from the store.
4. Mobile (iOS / Android)
Two implementation paths. Pick one per team. Do not mix scoring logic into the binary.
| Path | When to choose |
|---|---|
| Expo (React Native) | One team, fastest parity with web types |
| Native Swift + Kotlin | App Store polish, platform charts |
Suggested packages later: @watstock/sdk exporting types + fetch client from src/lib/api-client.ts. Until it exists, copy api-client.ts, entitlements.ts, predict.ts, paper.ts, compliance.ts. Do not rewrite the 1% rule or rank math.
Shared contract (every binary)
- Call the HTTP API. Never scrape HTML.
- Honor entitlements. UI follows
locked/tierfrom the server. - Print the compliance footer on every prediction and ticket.
- World Monitor: official embed or
GET /api/geo. No unofficial scrape. - Paper-trade only. Tickets say send this to your bank. No broker SDK.
- Same account as the web. A Pro bought on the site unlocks the app the moment
GET /api/billing/mesaystier: "pro".
Screens
Match the design comps (WatstockDesign.pdf, Watstock App.dc.html) and the live web.
| Screen | Notes |
|---|---|
| Ranking (Free) | Banner “Ranks 1–10 · this week's top ten”, then 11–15 unlocked |
| Ranking (Pro) | Full book, official rank |
| Full call | Huge ticker, BUY/HOLD/SELL tag, paper-trade (Pro) |
| Desk chat | Same /api/chat as web. Starters: Hormuz, HOLD, energy stress |
| World | WKWebView / WebView of the official embed + /api/geo cards |
| Paper trade sheet | BUY / SELL qty, notional — mock only |
| Ticket | Share sheet + copy instruction + full footer |
| Paywall | Monthly featured (cyan), weekly outline |
| Scoreboard | Public. “Insufficient history” until 10 resolved |
| Account | Manage Stripe in the system browser |
Navigation
- Bottom tabs: Hotstocks · Chat · Desk · Watch
- Stack: ranking → stock → trade sheet → ticket
- Modal: upgrade sheet
- Chat is a first-class tab. Beginners start here.
Auth on device
- Email / password → Better Auth bearer (see Auth).
- “Continue with …” →
ASWebAuthenticationSession/ Chrome Custom Tabs. - Store the token in Keychain / EncryptedSharedPreferences.
- After Stripe Checkout (Safari View / Custom Tab), return via universal link
https://<host>/account?checkout=successand refetch entitlements.
In-app purchase vs Stripe: v1 stays Stripe Checkout in the system browser so web, desktop, and mobile share one subscription. Apple’s IAP rule applies if you unlock digital content only inside the iOS binary. Legal/finance must review before an App Store submission. Do not implement a second price in StoreKit without that review.
World Monitor on mobile
<iframe
src="https://www.worldmonitor.app/embed?layers=conflicts,earthquakes,weather,waterways,economic¢er=48.809,0.421&zoom=1.63&theme=light&variant=full"
title="World Monitor live map"
allowfullscreen>
</iframe>Native: WKWebView / WebView with that URL. Height ≥ 420pt. Theme light on the white desk. Do not invert the licensed map. Server-side CII goes through WORLD_MONITOR_API_KEY only — the app never ships that key.
Desk chat on mobile
POST /api/chat with { question, history }. Show related[] as tappable names.
- Free: names + geo why-line; 5-day call locked. Tapping a locked card opens the upgrade sheet.
- Pro:
advice+fiveDayPcton each card; tap →/stock/{symbol}. - Guest cap 4 / day, Free 8, Pro 40. Surface
remainingunder the composer. - Never invent a HOLD from a locked row.
Offline
Tape is live. If the device is offline, show the last cached ranking with a stale dateline. Never pretend a cached 5-day call is new. Issued tickets may live in local storage.
Push (later)
Not wired. The server already stores user_pref.alerts_enabled. Register a device token against a future POST /api/alerts/device. Until then, in-app alerts on World are enough.
Deep links
| URL | Screen |
|---|---|
https://<host>/ | Ranking |
https://<host>/stock/AAPL | Apple |
https://<host>/chat | Desk chat |
https://<host>/t/{id} | Public ticket |
https://<host>/account?checkout=success | Return from Stripe |
watstock://stock/AAPL | Same, custom scheme |
Stores
| Store | Bundle | Notes |
|---|---|---|
| Apple App Store | app.watstock.ios (placeholder) | Export compliance: HTTPS only |
| Google Play | app.watstock | Data safety: account email, no bank numbers |
| Feynman App Store | slug watstock2 | Web build; native apps are separate |
Privacy nutrition: we store email, paper book, watchlist, subscription id, chat questions. We do not store bank credentials.
Suggested app repo layout
apps/mobile/
app/ # Expo Router or native coordinators
src/api/ # copied api-client + types
src/theme/ # tokens from src/styles.css
src/screens/ # Ranking, Stock, Chat, World, Desk, Ticket, PaywallKeep the mobile repo thin. New scoring logic lands here (watstock2), not in the app.
Build order for the mobile team
- SDK copy + golden tests: Free ranking JSON never contains
rank <= 10; stock lock stripsdays. - Ranking + stock + paywall — the conversion path.
- Desk chat — Hormuz starter must return mapped names.
- Paper desk + ticket share — the “send to bank” moment.
- World Monitor tab — official embed +
/api/geo. - Sign-in + Stripe return + entitlements refresh.
5. Desktop
Tauri 2 wrapping the production web origin (Electron is acceptable).
- Window 1280×800 default, min 390×640.
- Persist cookies for the API origin.
- Stripe, World Monitor, and bank links open in the system browser.
- About box: “Advisory only. We do not execute.”
- Deep link
watstock://stock/AAPL→/stock/AAPL. - Never embed
MASSIVE_API_KEY,XAI_API_KEY, or Stripe secrets in the binary.
6. HTTP API
Base URL: local http://127.0.0.1:8080, production the public host, App Store https://www.feynmanlabs.com/apps/watstock2.
All JSON. Accept: application/json. Web: credentials: "include". Native: Authorization: Bearer <token> plus optional Feynman identity headers.
Errors: { "error": "human message" } with 4xx/5xx.
Market
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /api/status | no | Feed health |
| GET | /api/tape?ranking=1 | session | Full-universe ranking, gated |
| GET | /api/tape?symbols=AAPL,MSFT | session | Watchlist slice (max 50) |
| GET | /api/stock?symbol=AAPL | session | One name. Free: locked: true |
| GET | /api/scoreboard | no | Public record |
| GET | /api/geo?country=US | no | World Monitor layer |
| GET | /api/ticket/:id | no | Public verification |
Free ranking: ranks 1–10 omitted from JSON, 11–15 unlocked, 16+ locked: true. When locked is true: days is [], fiveDayPct is 0, advice is "HOLD", drivers stripped. Do not display that as a real HOLD. Show a lock + upgrade.
Accuracy: accuracy.ready === false → “insufficient history”. Never print % on total < 10.
Chat
| Method | Path | Notes |
|---|---|---|
| GET | /api/chat | { remaining, limit, used, tier, signedIn, aiReady } |
| POST | /api/chat | Body { question, history? } |
Response:
{
"reply": "Yes — the World Monitor sleeve is marking the Strait…",
"model": "grok-4.5",
"related": [
{
"symbol": "XOM",
"name": "Exxon Mobil",
"locked": true,
"why": "Chokepoints: Hormuz disruption 70"
}
],
"topics": ["hormuz"],
"remaining": 3,
"limit": 4,
"unlockCalls": false,
"compliance": "WATSTOCK provides research…"
}On Pro, locked is false and advice / fiveDayPct are present. 429 when the daily cap is used. Guests get an HttpOnly cookie ws_chat_sid. Questions are written to chat_query (desk intelligence).
If XAI_API_KEY is missing, the server still answers from the live book + World Monitor (model: "desk-fallback"). Do not mock a second chat in the app.
Identity & billing
| Method | Path | Notes | |
|---|---|---|---|
| GET | /api/me | { authenticated, user } | |
| * | /api/auth/* | Better Auth | |
| GET | /api/billing/me | Always 200. Anonymous → Free | |
| POST | /api/billing/checkout | `{ "plan": "monthly" \ | "weekly" } → { url }` |
| POST | /api/billing/portal | Stripe Customer Portal URL | |
| POST | /api/billing/webhook | Stripe only. Clients never call this |
401 if signed out of checkout. 503 if Stripe is not connected — show “Checkout is being connected”, never fake success.
Paper desk (authenticated)
Starting cash $100,000. Mock fills at last print.
| Method | Path |
|---|---|
| GET | /api/paper |
| POST | /api/paper/order |
| POST | /api/paper/ticket |
| POST | /api/paper/reset |
On the Feynman host, send X-Feynman-User-Id (and email/name) because the proxy may strip fl_session.
Watchlist, alerts, cron
Watchlist today is session server-functions (listWatchlist, addWatch, removeWatch). Free add past 10 names returns { ok: false, error: "limit" }. A thin /api/watchlist REST wrapper can be added for native; until then ask backend for the alias.
GET/POST /api/alerts — 402 on Free.
POST /api/cron/resolve + x-cron-secret — ops only.
Handoff (no execution)
GET /api/handoff?source=paper|ranking¬ional=2500&holds=1
Returns { pack } where pack.kind === "watstock.handoff.v1" and executes: false. Ranking honors Free/Pro gates (no top 10 on Free). Clients download IBKR basket CSV or the JSON — they never POST an order to Watstock.
7. Auth & identity
One member, two doors.
| Source | How the API sees the user | Id format |
|---|---|---|
| Watstock Better Auth | Session cookie or Authorization: Bearer | Better Auth user id |
| Feynman Labs App Store | fl_session and/or X-Feynman-User-* | fl:<store-user-id> |
GET /api/me returns source: "watstock" | "feynman". Paper book and subscriptions key off that id. Do not create a second paper book if the same human uses both doors in v1.
Native session (recommended)
POST /api/auth/sign-in/emailwith{ email, password }.- Persist
session.tokenin the Keychain. - Send
Authorization: Bearer <token>on every/api/*call. - OAuth: system browser + claimed HTTPS redirect. Never embed Google in a WebView.
Sign out: POST /api/auth/sign-out and drop the bearer. Feynman logout is a store concern — stop sending X-Feynman-User-*.
8. Entitlements & gating
Single source of truth: `src/lib/entitlements.ts`.
No client may infer Pro from “they signed in”. Always call GET /api/billing/me.
| Capability | Guest | Free | Pro |
|---|---|---|---|
| Daily sentiment, starter watchlist of 10 | yes | yes | yes |
| Weekly ranking | ranks 11–15 | ranks 11–15 | full book, including top 10 |
| 5-day BUY / HOLD / SELL + path | locked | locked | unlocked |
| Desk chat | 4 / day, calls locked | 8 / day, calls locked | 40 / day, live calls |
| Alerts | no | no | yes |
| Watchlist | — | cap 10 | unlimited |
| Public scoreboard | yes | yes | yes |
| 5-day tickets with verification URL | no | no | yes |
Plans: $24.99 / month (watstock_pro_monthly) or $7.99 / week (watstock_pro_weekly).
WATSTOCK_FORCE_TIER=pro unlocks the book on staging only. Never in production.
Upgrade sheet reasons: five-day · ranking · watchlist · alerts. Cancel only through the Stripe portal.
9. Desk chat (web + app)
Beginners ask the desk. The book answers. This is a product surface and an intelligence feed (chat_query).
Flow:
- Parse the question for tickers and topics (Hormuz, energy, defense, cyber…).
- Map onto the licensed book (
MAPPED_BOOKinequity-layer.ts) — Hormuz →USO,XOM,CVX,FRO, airlines as the other side. - Pull live World Monitor scores +
predictManyon up to 8 covered names. - Redact 5-day fields unless Pro.
- Answer with Grok (
grok-4.5) whenXAI_API_KEYis set; otherwise the desk fallback, which still uses the live layer.
Do not build an on-device LLM. Do not let the model invent tickers outside the universe. Do not leak ranks 1–10 or a 5-day path to Free.
10. Design system
White paper + cyan. Not cream newsprint. Not a dark terminal.
Copy these tokens into Swift / Compose / RN. Do not approximate.
| Token | Hex | Use |
|---|---|---|
paper | #FFFFFF | Page |
ink | #071018 | Headlines |
fg | #141A20 | Body |
muted | #5C6772 | Kickers |
faint | #8B959E | Placeholders |
line | #E2E7EC | Rules |
accent | #00A4D1 | Actions, 3px masthead rule |
accent-deep | #0086AD | Pressed |
buy | #0F8A4B | BUY, up |
sell | #CC2F3A | SELL, down |
hold | #6B7280 | HOLD |
Type: Newsreader (display) · IBM Plex Sans (UI) · IBM Plex Mono (prices, %, ranks). Kickers: 11px, 0.1em, uppercase.
Components: newspaper tags for signals (not neon pills); one locked-top-10 banner (no ghost tickers); monthly plan featured with a cyan border; ticket is print-like.
Motion: short fades. Respect prefers-reduced-motion. No count-up casinos on PnL.
Wordmark: watstock™ in ink on white. Store name: Watstock 2.
11. Environments & secrets
| Variable | Required | Purpose |
|---|---|---|
MASSIVE_API_KEY / POLYGON_API_KEY | live tape | US prints and bars |
HF_TOKEN | recommended | FinBERT |
WORLD_MONITOR_API_KEY | layer | CII + features |
XAI_API_KEY | chat (optional) | Grok; fallback still answers |
DATABASE_URL | prod | Postgres |
BETTER_AUTH_SECRET / BETTER_AUTH_URL | prod auth | Session |
STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRET | billing | Server only |
STRIPE_PRICE_WEEKLY / STRIPE_PRICE_MONTHLY | billing | Price ids |
Live webhook (Feynman): https://www.feynmanlabs.com/apps/watstock2/api/billing/webhook
Events: checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.payment_failed. Signing secret is STRIPE_WEBHOOK_SECRET. Checkout still unlocks Pro from the return URL if the webhook is late.
CRON_SECRET | prod | /api/cron/resolve |
|---|---|---|
WATSTOCK_FORCE_TIER | staging | pro or free |
PORT / HOST / BASE_PATH | App Store | Store assigns |
Until Stripe keys exist, every visitor is Free.
Set these as masked, protected GitLab CI/CD variables and on the Feynman App Store env API. Never commit .secrets. Never put keys in a mobile binary.
| Name | API origin | Stripe |
|---|---|---|
| Local | http://127.0.0.1:8080 | Off or test keys |
| Staging | Feynman watstock2 or a staging host | Test + optional force-tier |
| Production | Public Watstock host | Live keys |
Point every unreleased binary at staging. A leaked Massive key from a debug APK is an incident.
12. QA before you merge or ship
Web
- Home as anonymous: ranks 11–15 visible; ranks 1–10 absent from the JSON.
/stock/AAPLas Free:locked: true, no forecast path./chat→ “Strait of Hormuz”: World Monitor numbers + mapped names; 5-day locked for Free.- Sign in → Subscribe → Stripe Checkout (or 503 if keys missing).
- Paper desk: one BUY; ticket text includes “does not execute”.
/t/{id}loads from the immutable row.- 390×844: no horizontal scroll; tab bar and chat composer clear the home indicator.
npm run typecheckandnpm run buildpass.
Mobile
- Same ranking JSON test as web (golden fixture).
- Locked stock does not render a fake HOLD.
- Hormuz chat starter maps to energy names; lock affordance opens paywall.
- Stripe return refreshes
/api/billing/meand unlocks the book. - Ticket share includes the full compliance footer.
- World Monitor embed loads
theme=light. - Offline: stale banner, no “fresh” 5-day claim.
- VoiceOver / TalkBack on the three signal tags.
13. What not to build
- A second predictor (on-device FinBERT, “offline AI”).
- Brokerage connectors (Plaid invest, Alpaca, IBKR).
- Editable history of calls.
- A Free ranking that “just hides” the top 10 in CSS while the JSON still has them.
- Dark or newsprint themes that fight the white + cyan desk unless product asks.
- StoreKit / Play Billing prices that disagree with Stripe.
- Accuracy percentages on fewer than 10 resolved calls.
- Chat answers that invent tickers or 5-day paths for Free users.
- Order routing or custody. Handoff files only until a licensed broker-hosted confirm exists.
The scoreboard’s credibility is the company. If a client ships execution, custody, or a fabricated accuracy number, pull the build and notify product.