For engineers

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 websiteWeb
Shipping iOS or AndroidMobile
Calling the API from any shellHTTP API
Wiring sign-in or StripeAuth · Entitlements
Matching the white / cyan deskDesign

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

ForbiddenWhy
Order routing, broker APIs, wallets, custodyWe do not execute or hold funds
“Guaranteed” or unsourced accuracy claimsRetired 2017 language. Illegal and false
30-day / any-asset / commodity predictionsOut of scope
IBM Watson, legacy partner logosRetired
Personalized investment adviceFooter says research only
Editing or deleting a published predictionThe record is the product
Showing Free users ranks 1–10The 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.

SignalCorrect when
BUY5-session return >= +1.0%
SELL5-session return <= −1.0%
HOLDabs(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_quota

One 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 PORT

Docker: docker compose up --build. Migrations: npm run db:migrate (migrations/0001_*.sql0006_chat.sql). Add a new numbered file; do not edit applied ones.

Routes to keep stable

These are deep links for desktop and mobile.

PathScreen
/Weekly ranking (Free: 11–15 only)
/stock/$symbolName + 5-day (gated)
/worldLicensed World Monitor map + layer
/chatBeginner desk chat
/scoreboardPublic record
/deskPaper book
/handoffSend picks to your broker — files only, no execution
/watchlistNames
/method1% rule
/pricing /signup /login /accountCommercial
/t/$idPublic ticket
/alertsPro alerts
/deckInvestor pitch (12 slides)
/handbookThis handbook, in-app

Where code lives

PathRole
src/routes/*Pages
src/routes/api/*HTTP JSON — the contract for every client
src/lib/entitlements.tsTiers, plans, 1% rule, chat caps
src/lib/predict.tsAdvice types + trading calendar
src/lib/market.server.tsLive tape + ensemble (server only)
src/lib/equity-layer.tsWorld Monitor feature classes + mapped book
src/lib/chat/*Desk chat: topics, context, Grok, quota
src/lib/billing/stripe.server.tsCheckout, portal, webhooks
src/lib/auth/*Better Auth + Feynman session
src/lib/paper.tsMock book, $100,000 starting cash
src/lib/api-client.tsFetch wrappers — copy these into native until @watstock/sdk exists
src/styles.cssDesign 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>.tsx with createFileRoute("/name").
  • Vite regenerates src/routeTree.gen.ts — do not hand-edit it.
  • Link from src/components/app-shell.tsx (desktop) and/or mobile-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.

PathWhen to choose
Expo (React Native)One team, fastest parity with web types
Native Swift + KotlinApp 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 / tier from 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/me says tier: "pro".

Screens

Match the design comps (WatstockDesign.pdf, Watstock App.dc.html) and the live web.

ScreenNotes
Ranking (Free)Banner “Ranks 1–10 · this week's top ten”, then 11–15 unlocked
Ranking (Pro)Full book, official rank
Full callHuge ticker, BUY/HOLD/SELL tag, paper-trade (Pro)
Desk chatSame /api/chat as web. Starters: Hormuz, HOLD, energy stress
WorldWKWebView / WebView of the official embed + /api/geo cards
Paper trade sheetBUY / SELL qty, notional — mock only
TicketShare sheet + copy instruction + full footer
PaywallMonthly featured (cyan), weekly outline
ScoreboardPublic. “Insufficient history” until 10 resolved
AccountManage Stripe in the system browser
  • 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=success and 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&center=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 + fiveDayPct on each card; tap → /stock/{symbol}.
  • Guest cap 4 / day, Free 8, Pro 40. Surface remaining under 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.

URLScreen
https://<host>/Ranking
https://<host>/stock/AAPLApple
https://<host>/chatDesk chat
https://<host>/t/{id}Public ticket
https://<host>/account?checkout=successReturn from Stripe
watstock://stock/AAPLSame, custom scheme

Stores

StoreBundleNotes
Apple App Storeapp.watstock.ios (placeholder)Export compliance: HTTPS only
Google Playapp.watstockData safety: account email, no bank numbers
Feynman App Storeslug watstock2Web 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, Paywall

Keep 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 strips days.
  • 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

MethodPathAuthNotes
GET/api/statusnoFeed health
GET/api/tape?ranking=1sessionFull-universe ranking, gated
GET/api/tape?symbols=AAPL,MSFTsessionWatchlist slice (max 50)
GET/api/stock?symbol=AAPLsessionOne name. Free: locked: true
GET/api/scoreboardnoPublic record
GET/api/geo?country=USnoWorld Monitor layer
GET/api/ticket/:idnoPublic 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

MethodPathNotes
GET/api/chat{ remaining, limit, used, tier, signedIn, aiReady }
POST/api/chatBody { 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

MethodPathNotes
GET/api/me{ authenticated, user }
*/api/auth/*Better Auth
GET/api/billing/meAlways 200. Anonymous → Free
POST/api/billing/checkout`{ "plan": "monthly" \"weekly" }{ url }`
POST/api/billing/portalStripe Customer Portal URL
POST/api/billing/webhookStripe 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.

MethodPath
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&notional=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.

SourceHow the API sees the userId format
Watstock Better AuthSession cookie or Authorization: BearerBetter Auth user id
Feynman Labs App Storefl_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.

  • POST /api/auth/sign-in/email with { email, password }.
  • Persist session.token in 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.

CapabilityGuestFreePro
Daily sentiment, starter watchlist of 10yesyesyes
Weekly rankingranks 11–15ranks 11–15full book, including top 10
5-day BUY / HOLD / SELL + pathlockedlockedunlocked
Desk chat4 / day, calls locked8 / day, calls locked40 / day, live calls
Alertsnonoyes
Watchlistcap 10unlimited
Public scoreboardyesyesyes
5-day tickets with verification URLnonoyes

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_BOOK in equity-layer.ts) — Hormuz → USO, XOM, CVX, FRO, airlines as the other side.
  • Pull live World Monitor scores + predictMany on up to 8 covered names.
  • Redact 5-day fields unless Pro.
  • Answer with Grok (grok-4.5) when XAI_API_KEY is 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.

TokenHexUse
paper#FFFFFFPage
ink#071018Headlines
fg#141A20Body
muted#5C6772Kickers
faint#8B959EPlaceholders
line#E2E7ECRules
accent#00A4D1Actions, 3px masthead rule
accent-deep#0086ADPressed
buy#0F8A4BBUY, up
sell#CC2F3ASELL, down
hold#6B7280HOLD

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

VariableRequiredPurpose
MASSIVE_API_KEY / POLYGON_API_KEYlive tapeUS prints and bars
HF_TOKENrecommendedFinBERT
WORLD_MONITOR_API_KEYlayerCII + features
XAI_API_KEYchat (optional)Grok; fallback still answers
DATABASE_URLprodPostgres
BETTER_AUTH_SECRET / BETTER_AUTH_URLprod authSession
STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRETbillingServer only
STRIPE_PRICE_WEEKLY / STRIPE_PRICE_MONTHLYbillingPrice 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_SECRETprod/api/cron/resolve
WATSTOCK_FORCE_TIERstagingpro or free
PORT / HOST / BASE_PATHApp StoreStore 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.

NameAPI originStripe
Localhttp://127.0.0.1:8080Off or test keys
StagingFeynman watstock2 or a staging hostTest + optional force-tier
ProductionPublic Watstock hostLive 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/AAPL as 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 typecheck and npm run build pass.

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/me and 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.