Screener · Design & Docs

Personal stock screener — how it works, page by page

A personal stock screener for one investor. It watches a handful of lists, shows fundamentals and signals for each ticker, and helps decide what to look at next. It never places orders, never shares your portfolio, and runs entirely on free data APIs.

Last updated Nov 5, 2026 · Reflects the live app (bundle index-BeLxCA_z.js) · Live app: screener-raj.pages.dev

Overview

The screener answers three questions, in order: what do I own, what is moving, and what is worth a closer look. Everything else is a detail of one of those three.

  • Command is the home screen. A watchlist on the left, one ticker in detail on the right, and a market strip across the top.
  • Portfolio is what you own: holdings, risk, sector mix, and plain-word rebalance suggestions.
  • Brief is the morning read: earnings calendar, movers, fresh news, and what the Street is saying.
  • Signals is discovery: scans of whole indexes for technical setups, plus a fundamental filter, saved screens, and a sector heatmap.
  • Groups is the optional shared layer: small groups with per-ticker discussion and structured buy/sell calls. Your portfolio and watchlists are never visible there.

Personal, not social. The screener is built for one person. Shared features exist only inside Groups, and they are fenced off from private data by design: group endpoints require sign-in, check membership, and never return holdings or watchlists. There is no public profile, no feed, and no way for one user to see another user's lists.

Design principles

These rules were set while building the app and they still govern every change. A feature that breaks one of them does not ship.

  1. Free APIs only. Finnhub free tier, Yahoo chart data, and Alpha Vantage free tier. Anything that needs a paid plan is out of scope, including standardized statements, price targets, and options data.
  2. Calls only when needed, only for what is required. Finnhub allows 60 calls a minute on the shared key. The app caches aggressively, fetches per symbol only when a view needs it, and never polls for data nobody is looking at.
  3. No orders, ever. The screener finds candidates. It has no broker connection and no order flow. Signals are for targeting, not trading.
  4. Clean values, explanations on hover. Numbers sit on the card they belong to, with their 5-year average underneath. What a number means lives in a hover or tap, not in permanent text.
  5. Related numbers stay together. A ratio and its history are one card, not two sections.
  6. Think first, then design. Visual fixes follow design principles. No quick patches, no wasted space, nothing floating over numbers.
  7. Minimal and accessible. One slim top bar, one-line watchlist rows, real buttons with spoken labels, 24px touch targets, and all motion off under reduced-motion settings.
  8. Never lose the user's lists. A failed load never rewrites saved data. A failed symbol stays visible with a retry. Saving waits until a load has succeeded for the current identity.

Architecture

Three parts. A React single-page app served from Cloudflare Pages, a Cloudflare Worker that holds the API keys and talks to the data providers, and a Postgres database on Neon that stores lists and holdings. The browser never sees an API key, except for three Alpha Vantage keys used only for direct Congress-trades fetches from the browser.

Browser SPA React + Vite Cloudflare Pages Worker API screener-api holds the keys Finnhub Yahoo chart Neon Postgres Clerk auth Alpha Vantage Dashed line: Congress trades fetch straight from the browser (Worker path is blocked on the free tier)
Figure 1 — System architecture. The Worker is the only part that talks to Finnhub, Yahoo, and Neon.
PartWhat it isWhat it owns
SPAReact 18 + Vite 6, served as static files from Cloudflare Pages (screener-raj.pages.dev)Every screen, chart (lightweight-charts), scans, backtests, and the save guard
WorkerOne Cloudflare Worker (screener-api), ~2,700 linesAPI keys, Finnhub/Yahoo fan-out, 24-hour edge caches, JWT checks, group rules, Neon writes
NeonPostgres 18, free plan, AWS US East 2screener_state, watchlist_lists, and five group tables
ClerkHosted sign-in, no phone number requiredIdentity only. The app keys cloud rows by clerk:<user id>
FinnhubFree tier, 60 calls/min shared key, plus a second key for Portfolio and metrics fan-outQuotes, fundamentals, news, earnings, analyst, insider, peers
YahooPublic chart API, no keyPrice history, market strip, FX, extended-hours bars
Alpha VantageFree tier, three keys rotated from the browserCongress trades only (25 calls/day per key)

Why a Worker in the middle

Three reasons. First, keys stay server-side, so the shipped bundle contains no secret. Second, one Worker call can fan out to several provider calls and cache the merged result for a day, which is how the app stays inside the 60-calls-a-minute budget. Third, the Worker is where identity is checked: it verifies the Clerk token and refuses group writes from anyone who is not a member.

What runs in the browser, and why

Scans, backtests, diversification math, and the risk engine all run in the browser. They need price history, not Finnhub, so they cost zero Finnhub calls. A scan of the S&P 500 is hundreds of Yahoo chart fetches through the Worker, done six at a time, and the results live in local storage until the next scan. Congress trades also fetch from the browser, because the free tier blocks the same call from the Worker's shared egress.

How data flows

Quotes: one fetch, two sources

A quote is two facts from two places. The price and change come from Finnhub (/quote). For Toronto listings the price comes from the last Yahoo chart candle instead, because Finnhub's free quote for a TSX symbol is the US listing. The app asks the Worker for a whole list at once (/api/quotes), refreshes every 60 seconds, and shows a pulsing live dot while the market is open. A live tick stream (Finnhub websocket, proxied through the Worker, 50 symbols max) updates prices between polls during market hours.

Two guards keep quotes honest. First, a Toronto chart is only reused when its last bar is from today in Eastern time, or the chart was fetched within the last hour. That stops yesterday's close from masquerading as today's price. Second, the app resolves every symbol before it fetches: a bare TD always opens the Toronto listing TD.TO, never the New York one.

Saving: the guard that never loses a list

Lists and holdings save to the cloud, but only under strict rules. The rules exist because two real incidents wiped lists: a sign-in race and a failed load that deleted symbols it could not fetch. Both are fixed in code, and the rules below are the fix.

You edit a list add, remove, rename Wait 800 ms debounce Identity check same user as load? PUT to Worker /api/lists, /api/state Neon replace whole list A save is blocked unless a load already succeeded for this exact identity. A failed load never deletes anything: the symbol stays, marked "Couldn't load", with Retry. Signing in or out resets the guard, so a stale session can never overwrite the new one.
Figure 2 — The save flow. Every write is a full replace, so the guard in front of it has to be right.
  • Ready only after a successful load. The app flips its save flag only after the cloud load for the current identity returns. Until then it shows the local copy, saves nothing, and retries every 30 seconds.
  • Identity is captured at both ends. A load and a save each record whose identity they belong to. If the identity changed while a request was in flight, the result is discarded or the write is aborted.
  • Failed symbols stay. If quotes fail for a symbol, the row stays in the list as a failed row with Retry and a remove button. The old behaviour, deleting failed symbols, is gone.
  • One stale client can still overwrite. A phone app that resumed from a suspended session can hold an old copy and save it. The footer version stamp (app v<hash>) and the "new version available" banner exist to catch exactly that: reload before trusting a stale screen.

History and charts: Yahoo, cached a day

Charts, scans, backtests, and the strip all read Yahoo chart data through /api/history. The Worker caches each symbol for 24 hours, separately in each Cloudflare data centre. That means two data centres can briefly serve different snapshots of the same symbol, and a buy-and-hold figure can differ by a point or two between them. It is a cache vintage difference, not a calculation bug, and it heals itself as caches expire.

Fundamentals: one call per symbol, cached a day

/api/metrics returns P/E, market cap, yield, beta, ROE, net margin, debt/equity, revenue growth, and sector for up to 12 symbols per call. Each uncached symbol costs one Finnhub /stock/metric call, plus one /stock/profile2 call for the sector. Results are cached 24 hours per symbol; failed fetches are not cached. Toronto symbols short-circuit: there are no TSX fundamentals on the free tier, so they return empty rather than burning calls.

Live stream: one socket, shared

The Worker holds one Finnhub websocket and proxies trades to every connected browser. It subscribes to at most 50 symbols, only during market hours, skips Toronto symbols, and sends a ping to keep the socket alive. The app disconnects hidden tabs and reconnects with backoff, giving up after five fast failures. The 60-second poll stays as the fallback.

Sign-in & privacy

Sign-in is optional, through Clerk, and it never asks for a phone number. Signing in turns on cloud saving and unlocks the Brief, Portfolio, Signals, and Groups views. Signing out, or never signing in, shows a fixed five-stock preview (AAPL, MSFT, GOOG, TD.TO, NVDA) with the Fundamentals card only. No chart, no lists, no search, no saving.

  • Identity is the key. Cloud rows are keyed by clerk:<user id> with a verified token. Without a token the app uses a per-browser id that never leaves the device.
  • Anonymous data does not migrate. Lists made while signed out stay in that browser. The first sign-in copies the legacy watchlist into a list called Watchlist, and only that.
  • Groups are Clerk-only. No anonymous access, no fallback. Every group endpoint checks the token and the membership, and a non-member gets a 404.
  • Nothing is shared by default. A friend using the screener cannot see your portfolio or watchlist. Groups show only what you post inside them.

Command

The home screen. Watchlist on the left, one ticker in full detail in the middle, market strip on top.

Top bar

One slim sticky bar: the Screener wordmark, a pulsing live dot with the updated time, the view tabs on desktop, a search icon, and the profile avatar. The search panel hangs from the bar and opens any ticker by symbol or name, including tickers that are in no list.

Market strip

A one-row swipeable carousel of nine cards: S&P 500, Nasdaq, Nasdaq 100, TSX, VIX, US 10-year, WTI crude, gold, and silver. It advances one card about every 4.5 seconds and loops. The advance pauses on hover, touch, drag, wheel scroll, and keyboard focus, and it never runs while the strip is off screen or the tab is hidden. Gold and silver are futures (GC=F, SI=F), not spot, because true spot is not available free through Yahoo.

Watchlist

One flat list, in your order, no grouping. Each row is a single line: symbol on the left, price above the change on the right, for example AAPL $344.17, down $5.34, down 1.53%. Prices flash on tick. Rows are real buttons with spoken labels. Tapping a row opens that ticker in detail. The count sits in the tabs row ("3 names").

Adding is a button that explains itself: + Add to Watchlist. It opens a symbol input that stays open for multiple adds and closes on Escape. Removing is edit-gated: there is no remove button in normal browsing, not even on the selected row. Tap Edit next to the Add button and every row gets a remove button, the list tabs offer rename and delete, and rows stop opening tickers until you tap Done. A symbol whose quotes failed to load stays in the list as a failed row with Retry; its remove button follows the same edit rule.

Ticker detail

Selecting a ticker opens its detail page. Sections, in phone order: Chart, Fundamentals, Peers, Congress, News, Signal, Earnings, Quality, Analyst, Insider. On desktop the detail sits beside the watchlist with resizable columns. On phones a floating pill with the same section names appears once you have scrolled a little, and it stays up while you are scrolled down, so you can jump without scrolling back. It only shows sections that are actually on the page.

  • Chart. Price bars with 6-month, 1-year, and 5-year ranges, weekly and monthly aggregation, SMA, EMA, Bollinger bands, support and resistance, and Fibonacci levels. After-hours prices come from Yahoo 5-minute bars, 04:00 to 20:00 Eastern.
  • Fundamentals. The clean values view: each ratio with its 5-year average underneath, related ratios on the same card, explanations on hover. Toronto symbols show dashes where no free fundamental exists.
  • Peers. Related companies from Finnhub, with the same one-tap open.
  • Congress. Recent trades by members of Congress for that ticker, fetched from Alpha Vantage. Includes chamber, politician, party, state and district, transaction type and date, disclosure date, and amount range.
  • News. Company news from Finnhub, newest first, linking out to the source.
  • Signal. A six-factor verdict (trend, momentum, mean reversion, breakout, value, quality) scored 0 to 100, with a short "why this score". The fixed strategy ideas used to sit here; they were removed because they were too static. The card now shows only the live verdict.
  • Earnings. Reported dates with actuals, plus upcoming dates for the next week.
  • Quality. A Buffett and Munger style checklist for the business.
  • Analyst. The current recommendation trend and the consensus direction.
  • Insider. Recent insider transactions.

Currency

Values are shown in the listing currency. A dual-listed company is converted when the data source reports in another currency: Finnhub serves some fundamentals in the home currency, so the app detects it and converts absolute and per-share values through Yahoo FX. Ratios are never converted.

Portfolio

What you own, how risky it is, and where it is concentrated.

Holdings

Each holding is a symbol, a share count, an average cost, and an optional target weight. The total is shown in dollars, and each holding shows its current value, gain or loss, and weight. Holdings save to the cloud with the same guard as lists. Adding a holding is a small form on the Portfolio tab; removing one is a tap on its remove control.

Risk engine

The risk engine derives everything from the history the app already has, so it costs no extra API calls. It reports portfolio volatility, the largest drawdown, and value at risk in plain words, and it flags holdings that dominate the total.

Diversification

Your sector weights against the S&P 500 (a static GICS snapshot, currently October 30, 2026). Concentration is stated in plain words through the Herfindahl index, translated into an "effective number of positions". The dashboard flags sectors you are overweight in and sectors you have no exposure to, and it shows a pairwise correlation table between your holdings with plain labels instead of bare numbers. Finnhub industry labels are mapped to GICS sectors; a label with no mapping is shown as-is.

Problems and rebalance suggestions

A rule-based section that reads the portfolio and states what is wrong in plain language, for example a holding far above its target weight, and what rebalancing would look like. It is suggestions only. Nothing here trades, and nothing is submitted anywhere.

What is not here

There is no balance-add or cash-top-up flow in the interface. It was removed at the owner's request; the code is kept but not rendered. Candidate suggestions from the old balance feature are also off. ETFs are supported as holdings and are bucketed under "ETF" in the portfolio; they do not appear in watchlist signals or earnings.

Brief

The morning read. What reported, what is moving, what is new, and what the Street thinks.

  • Earnings calendar. What reported, plus what is due today, tomorrow, and this week, for the symbols in your lists. Reported entries carry actuals where the provider has them.
  • Movers. The session's biggest moves among your lists, labelled by session so an after-hours move is never mistaken for a regular-session one.
  • Fresh news. News since the last 4:00 PM Eastern close, newest first.
  • Street lines. The consensus trend and the insider net for your names, so a single headline does not carry more weight than the trend behind it.

The Brief is time-aware: it knows the last close in Eastern time and works across daylight saving changes, so "since the last close" always means the real last close. Signed-out visitors see a sign-in gate instead of the Brief.

Signals

Discovery. Scan whole indexes for technical setups, filter by fundamentals, and keep the screens you like.

The scanner

Pick a universe (S&P 500, Nasdaq-100, or My lists), pick one of six conditions, set the lookback window (last 5, 10, or 30 trading days), and tap Find signals. The app fetches one year of daily bars per name through the Worker, six at a time, evaluates the condition on the last N days, and returns the hits ranked by signal date, newest first. Tapping a hit opens it on Command. Nothing re-fetches until you tap Find again, and switching tabs or reloading restores the last scan exactly.

Universe S&P 500 / NDX / lists 1 year of daily bars 6 at a time, via Worker Six conditions last 5 / 10 / 30 days Ranked hits newest first Open on Command The scan runs in the browser and costs zero Finnhub calls. Results persist until the setup changes. Each row can backtest its own preset on 5 years of bars, long side only, costs on, next-bar fills.
Figure 3 — The scan flow. Discovery only: no orders, no automation.

The six conditions

ConditionWhat it looks forDefaults
SMA crossoverFast average crossing above the slow one6-day vs 30-day
EMA crossoverFast exponential average crossing above the slow one20-day vs 50-day
RSI bounceRSI dipping to oversold and turning back up14-day, 30 / 70
MACD crossoverMACD line crossing above its signal line12 / 26 / 9
BreakoutPrice closing above the 20-day Donchian high20 days
Price + volume breakoutBreakout with volume above its average20 days, 1.5x volume

Parameters are typed freely and clamped on blur or Enter. Every field shows its range and its cross rule underneath, and an invalid combination (fast above slow, oversold above overbought) disables Find with a message. The six conditions are the whole set: no new signals are added.

Preset backtests

Every signal row has a Backtest button. One click runs that exact preset on 5 years of daily bars, evaluated over 4 years, long side only. Entry is the bullish event, exit is the bearish event, no stop, no hold cap, costs on, next-bar fills, and the same gates as the main engine. A bearish row backtests the same setup's long side and the panel says so. The old Backtest tab and its fixed strategy ideas are gone; the engine and its 22 strategy definitions remain in code but have no interface.

Fundamentals mode

Flip the Signals tab to Fundamentals to filter an index by numbers instead of price action. Eight fields, each with a min and a max: P/E, market cap, yield, beta, ROE, net margin, debt/equity, and revenue growth. All set filters combine with AND. At least one filter must be set; a filter-free run is blocked by design. Results stream in progressively as the runner works through the universe in chunks, and a Stop button ends the run. The runner paces uncached chunks 24 seconds apart to stay inside the Finnhub budget, so a cold S&P 500 pass takes about 20 minutes and a second run is fast because the metrics are cached.

Results table

Results can be shown as rows or as a sortable table. The table has the fixed columns (symbol, direction, date, price, signal, backtest) plus six columns you pick: P/E, yield, market cap, beta, distance from the 52-week high, and sector. Clicking a header sorts ascending, then descending, then back to scan order, with missing values last. Metric columns load per hit from /api/metrics. Distance from the 52-week high needs no fetch: it is computed from the scan's own one-year bars.

Saved screens

Name a scan setup and save it. Tapping a saved screen loads the setup and runs the scan in one tap. Deleting takes two taps. Twenty screens max, duplicate names rejected. Saved screens live in the browser's local storage, not in the cloud, by design.

Sector heatmap

Eleven S&P 500 sector tiles at the top of the Signals tab, sized by market-cap weight and coloured by the one-day or one-week change, with a toggle between the two. Full tint at 3 percent. Changes come from Yahoo chart data, eleven parallel calls when the tab opens, no key, no Finnhub. Tapping a tile shows its three largest holdings; tapping a holding opens it on Command. One failed tile renders grey and never blocks the rest.

Groups

The only shared part of the app. Small groups, per-ticker discussion, and structured calls with a paper scoreboard.

A group is a named room with up to 20 members, created by a signed-in user. Up to five groups per user. Inside a group there are per-ticker threads and a way to post a structured call: buy or sell, on a ticker, with a price and a thesis. Calls are not the Signal card's signals. A call is a person's stated position; a signal is the app's computed verdict.

  • Calls and the scoreboard. Each call is tracked against the market as a paper outcome, and the group keeps a scoreboard of whose calls worked. Nothing here trades. A call is a note with a price attached, and the scoreboard is the record of it.
  • Membership is checked on every write. Joining, posting, approving, and removing all go through Clerk-only endpoints that verify membership. A non-member gets a 404, and removing a member cuts their access.
  • Rate limits. Ten group writes per minute per user, enforced in the Worker.
  • Invite codes. Groups are joined by invite code. An old code stops working once it is replaced.

Privacy, restated. Groups never see your portfolio, your watchlist, or your holdings. The only things inside a group are the messages and calls you post there.

Watchlists & lists

There is one active list at a time, shown on Command. You can keep up to 20 named lists with up to 200 symbols each. The first list is called Watchlist. Switching lists switches the symbols the watchlist shows, the quotes it fetches, and the universe the Brief and Signals use when "My lists" is selected. Only the open list loads quotes.

  • Creating. The + button in the tabs row opens a name input. Duplicate names are rejected.
  • Renaming and deleting. Only in Edit mode: tap a tab to rename it inline, or tap its delete button twice to delete it. At least one list always remains.
  • Adding. The + Add button opens a symbol input. A successful add keeps the input open so you can add several in a row. A symbol that is already in the list, or that cannot be resolved, shows a hint instead of failing silently.
  • Removing. Only in Edit mode, as described on the Command page. There is no gesture, shortcut, or automatic path that removes a symbol.
  • Symbols and listings. US symbols are bare (AAPL). Toronto symbols carry .TO (TD.TO). A bare symbol that matches a Toronto listing resolves to the Toronto one. A symbol with no data anywhere (a delisted or bad ticker) can still sit in a list; it shows as a failed row, never as an empty slot.

Worker API

Every call goes to the Worker. The browser never calls Finnhub, Yahoo, or Neon directly, except for Congress trades. Base URL: https://screener-api.rajppatel-aiapp.workers.dev.

EndpointWhat it returnsCache
GET /api/quotes?symbols=…Price, change, and change percent for a list of symbolsNone (live)
GET /api/stock?symbol=…Quote plus profile, metrics, news, and earnings for one symbolMinutes, per symbol
GET /api/history?symbol=…&range=…Yahoo chart bars for a symbol and range24 h per symbol
GET /api/metrics?symbols=…Fundamentals for up to 12 symbols (P/E, cap, yield, beta, ROE, margin, debt/equity, growth, sector)24 h per symbol
GET /api/marketThe nine market-strip values (indexes, VIX, 10-year, oil, gold, silver)Minutes
GET /api/moversSession movers for the BriefMinutes
GET /api/earnings?symbols=…Earnings calendar and reported actualsHours
GET /api/analyst?symbol=…Recommendation trend for one symbolHours
GET /api/peers?symbol=…Peer companies for one symbol24 h
GET /api/candidate?symbol=…Portfolio candidate detail, on the second Finnhub keyHours
GET /api/congress?symbols=…Congress trades. Currently returns a note: the Worker path is blocked on the free tier, so the browser fetches Alpha Vantage directly.N/A
GET /api/streamWebsocket proxy for live Finnhub trades (50 symbols max)Live
GET /api/state · PUT /api/stateHoldings (and the legacy watchlist field). Keyed by identity.None
GET /api/lists · PUT /api/listsNamed watchlists. PUT replaces the whole set. First GET migrates the legacy watchlist into a list called Watchlist.None
/api/groups…Group rooms, members, threads, calls, and the scoreboard. Clerk JWT required on every call; membership checked.None

Two hard limits shape the Worker. One invocation fans out about four subrequests per uncached symbol against a cap of 50, so fan-out endpoints chunk small (earnings uses 6 symbols, metrics uses 12). And any provider error, including a rate limit, returns null for that symbol rather than an error page, so one bad symbol never breaks a whole list.

Storage & saving

Cloud (Neon)

TableWhat it holds
screener_stateHoldings and the legacy watchlist, keyed by owner id
watchlist_listsNamed watchlists, keyed by owner id
Five group tablesGroups, members, threads, calls, and scoreboard rows

There is no daily snapshot storage. Nothing is versioned: a PUT replaces the whole list or state. That is why the save guard matters so much.

Browser (local storage)

KeyWhat it holds
screener.lists.v1 · screener.activeList.v1Local copy of the lists and which one is open
screener.holdings.v1Local copy of the holdings
screener.signals.v1Scan settings, last scan results, and Signals vs Fundamentals mode
screener.filters.v1Fundamental filter results
screener.screens.v1Saved screens
screener.results-table.v1Rows vs Table view and picked columns
screener.panels.v1 · screener.cols.v1Which detail sections are open, and desktop column widths
screener.congress.v2Congress trades cache, 7 days per symbol
screener.backtests.v5Dormant backtest cache from the removed Backtest tab

Signed-in and signed-out data are kept in separate stores. The service worker (screener-v1) precaches the app shell and serves it offline, with network-first navigation, so the app opens even with no connection but never serves stale data as fresh.

Limits & known quirks

  • Finnhub free tier: 60 calls a minute. One shared key for most calls, a second key for Portfolio candidates and metrics. The app paces, caches, and chunks to stay under it. A cold S&P 500 fundamentals pass takes about 20 minutes by design.
  • No Toronto fundamentals. No free provider has them. Toronto symbols show dashes for P/E, market cap, yield, beta, and the rest. That is a data limit, not a bug.
  • No standardized statements, price targets, or options data. All premium on the free tier. The app does not try to fetch them.
  • History cache vintages differ by data centre. Two Cloudflare data centres can serve different 24-hour snapshots of the same symbol, so a buy-and-hold figure can differ by a point or two. Check the snapshot before suspecting the engine.
  • Gold and silver are futures. True spot is not available free through Yahoo.
  • Some tickers have no data. A delisted or bad symbol renders as a failed row with Retry. It is never silently dropped.
  • A resumed phone app can be stale. A phone that wakes a suspended page may run an old bundle. The footer version stamp and the update banner exist for that: reload when the banner appears.
  • Two-identity group checks are unverified. Join approve and deny, non-member 404s, member removal cutting access, and old invite rejection need a second identity to test, and there is no approved way to do that.

Versions & deploys

The app and the Worker deploy separately, and that split is deliberate.

  • App (SPA). Built with Vite and deployed to Cloudflare Pages (screener-raj). Every build is guarded: the bundle must reference the Worker URL and must not contain the string "Demo data", or the deploy does not go out. The footer shows the running bundle hash (app v<hash>), and the app checks for a newer bundle on load, every five minutes, and on foregrounding, showing a reload banner when one exists.
  • Worker. Updated by pasting into the Cloudflare Worker dashboard only. The stored API token cannot edit Worker scripts, and an API upload could wipe secret bindings whose values are unrecoverable. A copy page serves the current worker.js with a copy button for the paste. After a paste, the endpoints are verified with curl and the UI is verified in a real browser.
  • Data mode. The production environment must carry VITE_DATA_MODE=live and VITE_API_BASE pointing at the Worker. Losing those once shipped demo data to production; the build guard above exists because of it.

Changelog

WhenWhat changed
Oct 31, 2026Live Finnhub and Yahoo integration, Portfolio tab with risk engine, Neon persistence, Nasdaq-100 strip
Nov 1, 2026Clerk sign-in, ETF support (portfolio only), Paper & Ink theme, Command split layout, signals dashboard, overnight-price experiment reverted
Nov 2, 2026Named watchlists, live trade stream via Worker websocket, Morning Brief, flat watchlist, second Finnhub key
Nov 3, 2026Groups system (privacy enforced), NVO currency fix, social calls with paper scoreboard
Nov 4, 2026Congress trades via browser-side Alpha Vantage, signed-out gating, save guard, TD dual-listing fix, balance-add UI removed
Nov 5, 2026Signals tab (six conditions, preset backtests, persistence), fundamental filter, diversification dashboard, saved screens, sortable results table, sector heatmap, spinning market strip with gold and silver, floating section nav, list-wipe race and drop fixes, version stamp and update banner, minimalist redesign, edit-gated removal