尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Real time investment portfolio monitoring for free in one static HTML file!
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
Live prices for stocks, crypto and commodities in a single static HTML file. No server, no build step, no dependencies, no account. Open index.html and it works.
Most portfolio trackers want one of three things from you: brokerage credentials, a monthly subscription, or an account. The free ones are usually delayed, ad-supported, or quietly harvesting your holdings.檢視完整 README ↗Live prices for stocks, crypto and commodities in a single static HTML file. No server, no build step, no dependencies, no account. Open index.html and it works.
Most portfolio trackers want one of three things from you: brokerage credentials, a monthly subscription, or an account. The free ones are usually delayed, ad-supported, or quietly harvesting your holdings.
The constraint I set was: a genuinely useful real-time tracker where your positions never leave your browser, using only free data. That rules out a backend (nothing to send data to), which in turn rules out the usual answer to every hard problem below — no server-side caching, no API-key proxying, no rate-limit pooling across users. Everything has to work from a static file on GitHub Pages.
Most of what's interesting here is the consequences of that constraint.
| 11 data sources, automatic failover | Picks whichever source covers the most symbols, then backfills individual missing fields from the others. Seven are keyless and direct-CORS — including stocks, spot metals and Alpaca's crypto snapshots — so the no-key path never depends on the proxy fleet for a stock or crypto price. |
| Real-time streaming | Trade-by-trade stock prices over WebSocket from Alpaca (IEX) and FinnHub, split so a portfolio with both keys streams up to 80 positions; Coinbase Exchange overlay so crypto ticks on a 2-second interval instead of waiting on CoinGecko's free API, which caches most data for 1–5 minutes. |
| Delayed data is labelled | Any price its source serves on a delay — futures, Alpaca's delayed-SIP fallback — carries a superscript D with the reason and the lag, and it clears only when a source positively confirms the price is live. A stale price is never shown as if it were live. |
| Works with zero configuration | No API key required. Keys unlock more columns and streaming, but the app is useful on first open. |
| Everything stays local | localStorage only. Positions, quantities and cost basis never leave the browser; the only thing sent out is the ticker symbols needed to fetch prices, to the data providers (and, for Yahoo, public CORS proxies). |
| 33 data columns | Sortable, reorderable, hideable, with per-symbol notes, price alerts, and multi-portfolio support. |
| Genuinely mobile | The table becomes tap-to-expand cards with inline editing, drag-reorder, and pull-to-refresh — not a squeezed desktop layout. |
1. Download index.html (or clone this repo)
2. Open it in any modern browser — desktop or mobile
3. Type a ticker, share count, and cost basis → + Add
That's it. No install, no npm, no server.
A Welcome Guide opens as a dialog on first launch, and Load Sample Data fills the table with a few well-known positions if you want to see it working before entering your own. Dismiss the guide with its close button, the backdrop, or Esc; re-open it any time from Settings → Show Welcome Guide.
Optional free API keys (Settings panel) unlock the rest: Alpaca adds a real-time trade stream for your 30 largest stock positions, real bid/ask, and a delayed-SIP fallback that covers names IEX alone misses (Alpaca's real-time crypto needs no key and runs for everyone); FinnHub adds a second stream plus P/E, EPS, beta, dividends, earnings dates, and analyst ratings; an FMP key adds a third quote fallback, but only keys issued before September 2025 (FMP retired the endpoint this app uses for newer free plans).
Generate Alpaca keys from a paper-trading account. The Basic market-data plan is identical for paper and live keys, and a paper key can't move real money if it ever leaks from your browser — the keys live in localStorage, readable by any script on the origin.
A key is optional. Without one you still get real-time stock and crypto prices, day P&L, and the 52-week range — what a key adds is the data no keyless source publishes: P/E, EPS, beta, dividends, earnings dates, analyst ratings, real bid/ask, and WebSocket streaming.
Push to GitHub → Settings → Pages → source main, folder / → visit https://<user>.github.io/<repo>/. It's a static file; there is nothing to configure.
Every refresh runs as three waves, and each wave-1 source paints the moment it lands rather than waiting for its siblings — so every row appears at the speed of its own source, not the slowest one:
flowchart TD
T([Refresh tick]) --> S["StockAnalysis · CoinGecko · Coinbase<br/>direct CORS, no proxy"]
T --> G["Tokenized commodities<br/>PAXG / XAUT for gold"]
T --> C["Commodity futures<br/>Yahoo v8 chart, proxy race per symbol"]
T --> W2["Wave 2 · full fan-out<br/>FinnHub · Alpaca · FMP · Yahoo chart via proxy<br/>(only for rows nothing direct could price or name)"]
S -->|"~400 ms"| P["Wave 1 · incremental interim paint<br/>each source paints on arrival, blank rows only"]
G -->|"~35 ms"| P
C -->|"~2 s"| P
P --> M["Merge · best-coverage source wins,<br/>the rest backfill missing fields<br/>→ authoritative paint"]
W2 --> M
M --> D["Wave 3 · deferred, per-symbol<br/>fundamentals · performance · after-hours<br/>→ final paint, cached to localStorage"]
Two scheduling rules do most of the work here, and both were learned the hard way:
Ask for the fast things first. fetch fires when the promise is constructed, so construction order is request order. Constructing wave 2 first put the slow proxied calls on the wire ahead of the fast direct-CORS ones, which then competed with them for connections (browsers cap concurrent HTTP/1.1 connections per host at six) — sources that answer in 23–72 ms took 920 ms to reach the screen. Constructing wave 1 first brought that to 230 ms.
Never put a fast source behind a barrier with a slow one. A single Promise.all around wave 1 meant the interim paint ran at the speed of its slowest member; adding the commodity racer to it pushed stocks and crypto from 230 ms to 2.4 s even though their data had already arrived. Painting per source fixed it. The same mistake, in a worse form, is what made gold take 18.5 seconds: its real-time token price was fetched in ~35 ms but awaited only after the entire Yahoo proxy chain had finished.
Four problems drove most of the design.
Free financial APIs rate-limit, go down, silently return empty results, or drop coverage for individual tickers. The app treats every source as unreliable:
SHOP.TO, BRK-B) are never sent to Alpaca, so one foreign listing can't get a whole subscription rejected. Ranking is by market value at a session-stable price — the previous close, falling back to the open, then the current price, and only for a row with no quote yet, cost basis. Cost basis alone was the original rule and was wrong twice over: it is an optional field, so a portfolio that never filled it in streamed essentially at random, and a long-held winner bought at a tenth of today's price ranked at a tenth of its true weight — the position most worth streaming looked like the least. Live market value would be the obvious fix and is the wrong one: a price wobbling either side of the 30th-place cutoff would resubscribe both streams all day. A previous close doesn't move during the session, so the ranking is as steady as cost basis was while actually reflecting size.Routing Yahoo through public proxies means an untrusted intermediary controls the response body. That shapes several decisions:
__proto__ is discarded before it can become an object key — otherwise it would pollute Object.prototype application-wide. The same guard applies to trade messages from both WebSocket streams via an explicit hasOwnProperty check.Content-Security-Policy meta tag blocks remote scripts, eval, and object/embed content, and limits connect-src to the exact hosts the app uses. referrer is no-referrer, so navigating away doesn't leak a shared-portfolio URL (which carries positions in its hash).value="" attribute, so it's both validated on ingestion (YYYY-MM-DD) and escaped on render.requestAnimationFrame. Streams connect at page load rather than after the first full refresh. A green ● Live badge shows when streaming, and its tooltip names each stream and how many stocks it carries. If trades stop arriving for 60+ seconds during the regular session, it flips to amber ● Live (no data) with a tooltip explaining the likely cause. Pre- and post-market are exempt, since thin trading on IEX makes a quiet minute normal there. REST polling continues either way.marketState, with an ET-clock fallback) so yesterday's post-market price never lingers into the next trading day.Data via FinnHub + CoinGecko | WS streaming.33 data columns — symbol, name, price, change, % change, after-hours, quantity, cost basis, purchase date, market value, day P&L, total P&L, P&L %, dividend/yield, ex-div, earnings date, YTD/6M/1Y performance, prev close, open, bid, ask, day range, 52-week range, volume, avg volume, market cap, P/E, EPS, beta, analyst rating, and notes. Seven are locked as essential (symbol, name, price, change, % change, quantity, cost basis) and can't be hidden.
Analyst ratings — Strong Buy → Strong Sell consensus from FinnHub (free key), color-tinted by score. Click for a popover with the 1–5 score on a green-to-red scale and a stacked breakdown of analyst counts.
Multiple portfolios — unlimited named portfolios with their own positions, notes, and undo history. Switching keeps loaded quotes in memory (keyed by symbol), so flipping between portfolios doesn't force a reload. The selector bar is hidden by default to save vertical space; enable it in Settings (auto-shown if you already have more than one).
Inline editing — quantity, cost basis, date, and notes edit directly in the table or card.
Smart lot merging — same-side lots average by share-weighted cost basis; an opposite-side lot is treated as a partial or full close, where the surviving side keeps its basis instead of blending a closing price into a meaningless average. Manual adds and CSV imports share one routine so they can't drift.
Short selling — negative quantities track shorts with correct P&L (gains when price falls) and carry market value as a negative in the summary.
Undo / redo — Ctrl+Z / Ctrl+Y (or Ctrl+Shift+Z) across every portfolio change, 50 levels deep, with toolbar buttons that enable and disable as the stacks change.
Price alerts — per-symbol above/below thresholds with a 🔔 marker, row pulse, sound, and browser notification on cross. Alerts are global, shared across all portfolios.
Analytics panel — three linked views that open together above the table:
Auto-detects exports from Robinhood · E*Trade · Fidelity · Charles Schwab · Webull · Vanguard, and accepts a bare Symbol, Shares, Cost, Date with no headers.
Total Cost Basis wins over a vaguer column regardless of column order.(5), or Short / Sell / Sold / SLD in a type, side or Trans Code column — the last is what Robinhood labels it, so a transaction history nets correctly instead of importing every sale as another purchase.; with the comma as the decimal mark (1.330,10), which a comma-split turned into plausible-looking nonsense rather than an error — so the separator is detected and the decimal convention follows it.BRK/B, every source here wants BRK.B, and stripping the slash produced BRKB, which resolves nowhere.Alt or Shift to scroll sideways even on a long page, with the same easing as the plain wheel — Shift is handled too because Chrome's native Shift+wheel animation crawls near the ends of the table. Firefox on Windows keeps its native Alt+wheel and Shift+wheel. It hands back to normal page scrolling once the table reaches its edge, leaves trackpad swipes to the browser, and eases between notches rather than jumping.Tab through toolbar inputs, Enter to add, Ctrl+Z/Ctrl+Y undo/redo, ? for help, Esc to close overlays.color-scheme: dark at the root, so native date pickers and scrollbars match.aria-live="polite"; the table carries an aria-label; every icon-only toolbar button has an explicit aria-label.:focus-visible outline, visible for keyboard navigation and suppressed for mouse clicks.prefers-reduced-motion.env(safe-area-inset-*), plus an extra breakpoint at 380px for iPhone SE.| Source | Key | Rate limit | Provides |
|---|---|---|---|
| StockAnalysis | — | direct CORS | Keyless real-time US equities and ETFs, with no proxy hop. Price, change, OHLC, volume, 52-week range, market state, and pre/post-market. Handles dotted tickers (BRK.B) and ETFs. 60 symbols in ~455 ms. |
| Yahoo Finance | — | via CORS proxies | v8 chart only: quotes and company names for rows nothing direct covers, after-hours candles, historical performance, and commodity futures |
| FinnHub | free | 60/min + WebSocket (app budgets itself to 50/min) | WebSocket streaming (stocks), quotes, profiles, P/E, EPS, beta, dividends, earnings, analyst ratings |
| Alpaca Markets | free | 200/min + 1 stream (30 symbols) | Real-time IEX trade stream, IEX snapshots, delayed-SIP fallback for names IEX doesn't cover, real bid/ask, avg volume, historical bars |
| Alpaca Crypto | — | 200/min | Real-time crypto snapshots with real bid/ask — no key, no feed tiers and no delay, unlike equities. Runs for every visitor; keys are sent when present. |
| Financial Modeling Prep | legacy keys only | 250/day | Quotes with after-hours, fundamentals. Uses /api/v3, which FMP restricted to subscriptions from before 31 Aug 2025; newer free keys get an error here and the other sources cover the gap. |
| CoinGecko | — | 5–15/min keyless | Crypto 24h high/low, market cap, volume, 1-year chart for 52-week range and performance |
| Coinbase Exchange | — | 10/s | Real-time crypto price/bid/ask overlay; PAXG for real-time gold |
| Binance.US | — | direct CORS | Batched real-time crypto overlay above 10 coins, with real bid/ask. 36 of the 37 supported coins. |
| Kraken | — | direct CORS | Batched crypto backstop — the only venue covering all 37 coins — plus PAXG and XAUT as gold failover when Coinbase is unavailable. |
| gold-api.com | — |
Verified 2026-09-01 against the live endpoints. Free financial APIs change access terms without notice: two sources moved behind keys since the previous audit and were replaced (see the graveyard below). Treat the dates as when each row was last checked, not when the code was written.
401 Unauthorized for everyone, and /v1/test/getcrumb returns 401 Invalid Cookie on both CDN hosts through any proxy. The crumb requires a Yahoo consent cookie issued by fc.yahoo.com, and a stateless passthrough proxy makes each request independently — it cannot carry that cookie, and the browser cannot hold a cross-origin cookie for Yahoo. Fixing this requires a stateful backend, which is the one thing this project rules out. So the app no longer calls the crumb, v7, v6 or quoteSummary at all; the only Yahoo endpoint it uses is the per-symbol v8 chart. They used to get one try per session, latched off on the first 401/404 — but measured through the proxy from the production origin they never answer 401/404. They come back as 500 or 522 after 14–20 s, so the latch never fired, and every one of them was retried all session while holding a connection to the only proxy host./quote polling for stocks that weren't streaming used to run to several hundred calls a minute. Over budget, the least-recently-quoted symbols go first next time and prices keep coming from the other sources./quote. Dividend data comes from /stock/metric, not the dedicated /stock/dividend endpoint, which is premium-only and returns 403 on every free key.Free financial APIs die, and they rarely announce it. Every source below was either used by the app and then lost, or turned out to be unusable when it was needed. Kept as a record so the same ground doesn't get re-tested every time something breaks, and because the failure modes turned out to be more instructive than the successes.
| Source | Died | What happened | Replaced by |
|---|---|---|---|
| Stooq | ~Mar 2026 | Introduced a mandatory API key. The keyless CSV endpoint now returns 404 for everyone and the site serves a JavaScript proof-of-work interstitial to non-browser clients. Direct CORS stopped being allowed at the same time. (independently reported) | StockAnalysis |
| CryptoCompare | ~2026 | Moved under CoinDesk and retired anonymous access — min-api.cryptocompare.com now answers 401 {"message":"API key required"}. | Binance.US + Kraken |
Yahoo /v7/finance/quote | 2025–26 | Returns 401 Unauthorized · User is unable to access this feature. The batch quote endpoint is simply gone for unauthenticated callers. | Yahoo v8 chart, per symbol |
Yahoo /v6/finance/quote | earlier | 404 — endpoint removed. | as above |
Yahoo crumb / quoteSummary | 2023–26 | 401 Invalid Cookie. Needs a consent cookie from fc.yahoo.com; a stateless CORS proxy can't carry one and the browser can't hold a cross-origin Yahoo cookie. Unfixable without a backend — see Known gaps. | FinnHub / Alpaca (key required) |
| CoinCap | 2025 | api.coincap.io decommissioned; the v3 host at rest.coincap.io returns 401 without a key. | Coinbase, Binance.US, Kraken |
| IEX Cloud | Aug 2024 | Shut down entirely before it could be used here. | — |
FMP /api/v3 (newer free keys) | 31 Aug 2025 | Restricted to accounts with subscriptions from before that date; newer free plans only get FMP's "stable" API, whose quote endpoint takes one symbol per call — unworkable for live quotes on 250 calls a day. The integration stays for older keys. | Alpaca, FinnHub, StockAnalysis |
Two things this table taught the design:
Test from the real origin, not localhost. Corsfix passed every local test and never once worked on the deployed site. Proxies and some APIs grant localhost privileges they deny a production domain.
A source that hangs is far worse than one that errors. A fast 403 costs ~30 ms; a hang costs a full timeout and a connection slot, and if it sits in a blocking wave it stalls the entire first paint. Stooq and codetabs were both in that category, and between them they were the reason a keyless first load could sit blank for ~30 seconds. Everything that can fail now either fails fast or backs itself off. Which of the two matters: a source that refuses with an explicit status (401/403/451) is latched off for the session, because that will not resolve itself. A source that merely stops answering gets a 60-second circuit breaker instead — from a browser, "this endpoint is gone" and "the user's wifi dropped" are the same observable event, so a permanent latch there would disable a healthy source for the rest of the session over a passing blip.
Don't fix a dead source if the replacement is better anyway. Stooq and CryptoCompare could both be revived with keys. Neither was worth it: Stooq's replacement is real-time instead of 15 minutes delayed and handles the dotted tickers Stooq skipped, and CryptoCompare's replacements are batched and carry real bid/ask, which it never did. Adding a key field to Settings would have been the smaller change and the worse outcome.
Tested for keyless + direct-CORS access and found unusable, so nobody has to re-test them: Binance.com, OKX, Bybit, KuCoin, Bitstamp, Bitfinex, Crypto.com (no CORS headers from a browser); CNBC, Nasdaq, WSJ (no CORS); Polygon, marketstack, Twelve Data, Alpha Vantage (key required, and Alpha Vantage's free tier is 25 requests/day). TradingView's scanner does work batched with CORS, but needs exchange-prefixed tickers (NASDAQ:AAPL) that the app has no way to resolve.
Also searched: a tokenized stand-in for silver, platinum or copper, the way PAXG works for gold. Coinbase, Kraken (1,450 pairs) and Binance.US between them list exactly two metal-backed tokens, PAXG and XAUT, both gold. Kinesis Silver (KAG) and Kinesis Gold (KAU) exist but turn over ~$84K and ~$65K a day against PAXG's ~$177M, and are on none of those venues; at that depth a token can sit stale or drift off spot, which is worse than an honestly-labelled delayed price. Oil-backed tokens (WTIC, XTI and similar) are newly minted, tiny, absent from every venue the app can reach, and discussed in the trade press alongside the question of whether they are scams — not something to put behind a portfolio row.
A caution from that search: MEXC lists a KAGUSDT at about $36 while Kinesis Silver trades near $65. Same ticker, different asset. Ticker collisions are exactly how a tracker ends up confidently displaying a wrong number, which is why every source is read through the app's own symbol map rather than by trusting a ticker string.
What the search did turn up is better than a token: a keyless spot-metals API with open CORS, which is how silver, platinum, palladium and copper became real-time. It carries no energy or agricultural symbols, so those remain delayed.
Everything lives in localStorage. Positions, quantities and cost basis are never transmitted; the only outbound data is ticker symbols, sent to the data providers above and, for Yahoo, through public CORS proxies. Writes are debounced on a trailing 250 ms timer and flushed on beforeunload, pagehide, and visibilitychange — the last two matter because iOS routinely skips beforeunload when you switch apps.
Every key keeps the stonks_ prefix from the project's original name. Renaming them would silently orphan every existing user's portfolio, so the prefix stays — it is a storage format, not a brand.
| Key | Contents |
|---|---|
stonks_portfolios | All named portfolios: { name: { portfolio, notes } } |
stonks_active_portfolio | Currently selected portfolio |
stonks_portfolio · stonks_notes | Active portfolio's positions and notes (mirrored for backward compatibility) |
stonks_quotes | Last-known quotes for instant paint on reload; pruned to current holdings |
stonks_alerts | Price alert thresholds, { symbol: { above, below } } — global across portfolios |
stonks_hidden_columns · stonks_column_order | Column visibility and order |
stonks_sort | Active sort column and direction |
stonks_alloc_visible | Allocation chart show/hide |
stonks_show_portfolio_bar | Portfolio selector bar visibility |
stonks_finnhub_key · stonks_alpaca_key_id · stonks_alpaca_secret · stonks_fmp_key | API keys |
stonks_alpaca_stream | Alpaca real-time streaming switch (0 = off; on by default) |
stonks_refresh_interval · stonks_auto_refresh | Refresh preferences |
stonks_welcome_dismissed | Welcome guide state |
stonks_debug | Verbose logging flag (see below) |
Corrupt entries are detected on load: the bad key is removed and the app boots with an empty fallback rather than crashing. Clearing browser data resets everything.
Being honest about what isn't done, and why:
api.allorigins.win still works, and even that is intermittent (seconds to minutes of 522s). This is also shared infrastructure: every visitor's Yahoo requests go through the same community service, which is why proxied calls are now limited to what nothing else can supply. If Yahoo-backed fields go blank, check the pool from a real deployed origin before anything else.Browsers — Chrome/Edge 103+, Firefox 100+, Safari 16+ (mid-2022 and newer). The binding constraint is AbortSignal.timeout, used on every network call to prevent a hung fetch from freezing the refresh loop. structuredClone (Safari 15.4+) and Promise.any are also required. AbortSignal.any is feature-detected and degrades gracefully on older browsers. No IE support.
Debugging — per-symbol and per-proxy fetch warnings are silenced by default, since one refresh races several proxies × symbols and most non-winners produce expected errors. To turn the verbose stream back on:
localStorage.setItem('stonks_debug', '1');
location.reload();
Source-level events — auth failures, rate limits, WebSocket connect/close, quota errors, and top-level refresh exceptions — always log regardless.
The up-front direct (no-proxy) attempt at Yahoo is off by default: Yahoo sends no CORS headers, so it can only succeed with a CORS-relaxing browser extension and otherwise logs two red errors per session. Opt in with localStorage.setItem('stonks_direct_fetch', '1').
The interesting property of this app is that it has no backend, so the attack surface is entirely client-side plus the untrusted CORS proxies in front of Yahoo. The main asset worth protecting is the API keys in localStorage: any script running on the origin can read them, so almost every mitigation below is about keeping attacker-controlled data from becoming script.
Content Security Policy — a meta-tag CSP sets default-src 'self', blocks remote scripts, eval, and object/embed content, restricts connect-src to the exact hosts the app talks to (so injected script couldn't fetch() keys to an arbitrary server — though it could still navigate the page away with them), and sets frame-src 'none' so the app cannot load third-party frames. Note that this does not prevent the app from being framed by someone else — that requires a frame-ancestors directive, which browsers ignore in a meta tag and which would need an HTTP response header. referrer is no-referrer, so a shared-portfolio URL (positions in the hash) isn't leaked on navigation.
Untrusted API and proxy responses — quote payloads can only populate symbols the app requested, so a crafted key such as __proto__ from a compromised proxy is discarded before it can pollute Object.prototype. WebSocket trade messages use an explicit hasOwnProperty check for the same reason. API keys go only to their own origins, never through a proxy.
Response values are treated the same way. Most numeric fields go through formatters built on Intl.NumberFormat, which can only emit digits. The exceptions were analyst counts and scores, concatenated directly: Yahoo's quoteSummary reader (since removed along with the endpoint) passed raw values through without a type check, so a proxy returning numberOfAnalystOpinions: {raw: "<img onerror=…>"} reached innerHTML. The render sites now accept only finite numbers there. The free-text date fields a payload controls verbatim — ex-dividend and next-earnings — are escaped and length-capped at the render site. They were previously interpolated raw into the table, which a compromised source could have turned into script execution; the CSP does not help there, since script-src has to allow 'unsafe-inline' for the app's own handlers. Company names are only taken from a proxied payload if they are strings, and the attribute escaper coerces its input, because a numeric name used to throw inside the table renderer — and since names are cached, it threw again on every reload.
Untrusted user input — imported CSV fields are escaped in the preview, and the preview caps how many rows it draws so a mis-dropped transaction log can't freeze the tab. The column picker attaches handlers via addEventListener and reads keys from data- attributes rather than interpolating them into inline onclick. External links use rel="noopener noreferrer".
Symbols are whitelisted to [A-Z0-9.\-=] on every path that can introduce one — manual entry, CSV import, shared URL, and state restored from localStorage. That last one matters because symbols are interpolated into inline onclick handlers, where attribute escaping alone is not enough: the HTML parser decodes ' back to a real apostrophe before the JS is parsed, so a stored quote could otherwise terminate the argument. Writing localStorage already requires script execution on this origin, so this is defence in depth rather than a privilege boundary — the point is that the invariant holds everywhere instead of by convention.
Settings files are treated as untrusted, because "import my layout" is an easy thing to trick someone into. Price-alert thresholds were stored exactly as imported, and the mobile cards write them into value="" attributes, so a crafted file could run script and read the stored keys. That was verified with a working payload before the fix. Alerts are now rebuilt on import and on load: symbol keys must match the whitelist, and only finite numbers survive. Key fields must be strings, and the refresh and toggle settings must match their allowed values. The export button warns that the file contains your keys.
Column layout loaded from a settings file is resolved through a Map, not a plain-object lookup. With an object, an entry of __proto__ or constructor satisfies a truthiness check and injects Object.prototype into the column list, rendering phantom headers.
Portfolio names index a plain object, so the same class of name misbehaves there too — and __proto__ is the sharp one: all['__proto__'] = {…} doesn't create a property, it reassigns the prototype, so the portfolio is silently never stored while the UI switches to it. A short reserved-word list is rejected up front, and existence checks use hasOwnProperty rather than truthiness so an inherited key can't pass as a saved portfolio.
Every source is read through the app's own symbol map, never by iterating the response. This is what keeps the response-key guard holding as the source count grows: Binance.US and Kraken responses are indexed by their pair, then looked up via our map; Kraken's internal keys (XXBTZUSD) never touch quoteData directly. None of the sources added recently contribute a free-text field to the DOM — their names come from our own tables and their numeric fields go through Number() — so the untrusted-string render surface did not grow with them.
Shared-URL imports — parsed with a symbol whitelist ([A-Z0-9.\-=]), a 10-character cap, numeric coercion, and strict YYYY-MM-DD date validation, then gated behind a confirmation dialog. The date matters specifically because it flows into an HTML value="" attribute; it's validated on ingestion and escaped at every render site.
Not investment advice. Every price, fundamental and analyst rating shown comes from free third-party sources, several of them unofficial, and can be delayed, incomplete or wrong. Check with your broker before acting on anything the app shows. Provided as-is under the MIT license.
Built by Grant · MIT licensed
522 at ~19 s. A short timeout cancels the slow-but-fine responses, and a long one stalls the refresh, so instead a second request goes to Yahoo's other CDN host if the first hasn't answered in 6 s, and the first one to arrive wins. A healthy proxy still costs exactly one request per symbol.PAXG-USD from Coinbase in wave 1 (~35 ms) — PAX Gold is a redeemable claim on one troy ounce, trades 24/7, and tracks spot within ~1%. Its 24h open doubles as a previous close, so gold shows a real day change and range even on weekends when Yahoo isn't queried. Kraken backs this up with both PAXG and XAUT (Tether Gold — the same claim-on-an-ounce structure from a different issuer), so gold survives both a Coinbase outage and a problem specific to one token's peg. I checked the full product lists of all three CORS-reachable venues for metal-backed tokens: Coinbase, Kraken (1,450 pairs) and Binance.US carry PAXG and XAUT, and nothing else. It is a liquidity story — PAXG turns over ~$177M a day, while the nearest silver token (Kinesis KAG) does ~$84K and is listed on none of them. A token that thin can sit stale or print well off spot.Access-Control-Allow-Origin: *, so they are fetched directly, never through the proxy. Measured against a live reference, its gold print read 4333.30 while PAXG traded at 4333.35, and it restamps every 30 s. It is also the third gold fallback, behind Coinbase and Kraken.
These are spot prices while the rows are dated futures contracts, so the previous close is converted onto the spot basis (using the ratio between the two prices observed at the same instant) before the day change is computed — silver spot 64.93 against "Silver Dec 26" at 65.15 is a 0.34% gap, which is $0.22 on a typical $1 daily move. If no futures quote exists to convert against, the row shows the live price and a dash for the change rather than an invented one. An anonymous free service with no SLA is strictly an overlay: any failure falls straight back to the delayed futures quote underneath, and three failures back it off for a minute.USO, BNO, UNG, CORN, WEAT, SOYB, CANE) — a pointer, not a substitution, because an ETF is a different instrument: USO trades near $162 while WTI is near $104, and the ratio drifts with every roll.All three are hover-linked and clickable: a legend or mover row jumps to that position in the table, and clicking an asset class flashes every position in it. They render from one data pass behind a shared signature gate, so they can't drift out of sync or re-render independently on a WebSocket tick, and on desktop they equalise to a single card height so the band reads as one row rather than three ragged stacks. Toggle with Show Charts in the summary bar.
Adaptive price precision — decimals scale to magnitude (2 → 4 → 6 → 8), so SHIB shows 0.00002341 instead of $0.00. Dollar aggregates stay at 2.
Export CSV — respects your current sort; re-imports cleanly into the app.
Export / import settings — API keys, column layout, alerts, and preferences as a JSON file.
Share via URL — portfolio encoded in the URL hash (up to 99 positions), gated behind a confirmation prompt on the recipient's side.
Estimated bid/ask — when Alpaca isn't available, derived from last trade with a price-appropriate spread, marked ~ and dimmed.
| direct CORS, none stated |
Real-time spot metals: silver, platinum, palladium, copper (symbol HG, not XCU) and gold. The only real-time source for the four non-gold metals, and the one that takes them off the proxy entirely. Overlay only — failures fall back to the delayed futures quote. |
FinnHub /stock/dividend | — | Not dead, but premium-only: returns 403 on every free key. It was 1 of 5 calls fired per symbol, so a fifth of the fundamentals budget bought a guaranteed error. | /stock/metric, which carries the same fields on the free tier |
thingproxy.freeboard.io | ~2026 | NXDOMAIN — the domain no longer exists. | — |
crossorigin.me | years ago | Service shut down and the domain changed hands. Requests carried portfolio symbols in the URL to an unknown party for zero chance of a response. | — |
api.codetabs.com | ~Aug 2026 | Stopped answering. Notably it does not refuse — it hangs, burning the full 10 s timeout and holding a connection slot on every rotation. | — |
corsproxy.io (keyless form) | ~2026 | Hard 403 keyless_legacy_url — the anonymous URL form was retired in favour of account API keys, which a public static file can't ship. | — |
proxy.cors.sh | ~Sep 2026 | The hostname stopped resolving (NXDOMAIN). A lapsed proxy domain is a liability, not just dead weight: whoever registers it next would receive every user's ticker symbols and control the JSON the app parses. | — |
proxy.corsfix.com | never worked in production | Free use is localhost-only. Every other origin, including the GitHub Pages site, gets 403 domain_not_registered. It measured as the fastest proxy in local testing, which is exactly why it looked healthy. | — |
MSFT / MSFT before Yahoo or FinnHub merges the real name in. It self-corrects within a few seconds once a name is learned (names are cached, so this only happens on the first load of a symbol); if the proxy is down and no key is set, the ticker is all you get, with a retry every 2 minutes. The alternative was shipping a hardcoded ticker-to-name table, which would be stale the week after it was written.