<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: w Bush G</title>
    <description>The latest articles on DEV Community by w Bush G (@w_bushg_ee2cdfa6c771a866).</description>
    <link>https://dev.to/w_bushg_ee2cdfa6c771a866</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2834934%2F53a5d0c5-6cd3-4b25-95fb-57c025c93cf1.png</url>
      <title>DEV Community: w Bush G</title>
      <link>https://dev.to/w_bushg_ee2cdfa6c771a866</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/w_bushg_ee2cdfa6c771a866"/>
    <language>en</language>
    <item>
      <title>Building a Zero-API-Key Real-Time Market Data Proxy</title>
      <dc:creator>w Bush G</dc:creator>
      <pubDate>Mon, 03 Aug 2026 06:08:59 +0000</pubDate>
      <link>https://dev.to/w_bushg_ee2cdfa6c771a866/building-a-zero-api-key-real-time-market-data-proxy-4i69</link>
      <guid>https://dev.to/w_bushg_ee2cdfa6c771a866/building-a-zero-api-key-real-time-market-data-proxy-4i69</guid>
      <description>&lt;h1&gt;
  
  
  Building a Zero-API-Key Real-Time Market Data Proxy
&lt;/h1&gt;

&lt;p&gt;A one-screen market dashboard that pulls live quotes for A-shares, HK, US stocks, commodities, crypto and treasury yields — &lt;strong&gt;without a single paid API key&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: market data APIs are expensive
&lt;/h2&gt;

&lt;p&gt;When I started &lt;a href="https://github.com/theBigGavin/marketingdashboard" rel="noopener noreferrer"&gt;Market Research Cockpit&lt;/a&gt;, I priced out the usual suspects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Polygon.io&lt;/strong&gt; — $29/mo for real-time US stocks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finnhub&lt;/strong&gt; — $49/mo for websockets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alpha Vantage&lt;/strong&gt; — 25 calls/day free tier, then $49/mo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EODHD / Tiingo / TwelveData&lt;/strong&gt; — all $20–80/mo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a personal research tool that polls every 5 seconds, that's hundreds of dollars a year for data that is, frankly, public. The A-share and HK market data I needed most wasn't even available on these platforms.&lt;/p&gt;

&lt;p&gt;So I built the proxy on top of free public endpoints instead. Here's how it works and what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data sources
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Market&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A-shares / HK quotes&lt;/td&gt;
&lt;td&gt;Tencent&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;qt.gtimg.cn/q=&lt;/code&gt; (GBK)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;US index quotes&lt;/td&gt;
&lt;td&gt;Tencent + Sina&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hq.sinajs.cn&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VIX&lt;/td&gt;
&lt;td&gt;Sina futures&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hq.sinajs.cn/list=hf_VX&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sector boards / money flow&lt;/td&gt;
&lt;td&gt;Tencent + Eastmoney&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;proxy.finance.qq.com&lt;/code&gt;, &lt;code&gt;push2delay.eastmoney.com&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crypto&lt;/td&gt;
&lt;td&gt;Binance / OKX&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v3/ticker/24hr&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Treasury yields&lt;/td&gt;
&lt;td&gt;Eastmoney datacenter&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datacenter-web.eastmoney.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Earnings calendar&lt;/td&gt;
&lt;td&gt;Eastmoney datacenter&lt;/td&gt;
&lt;td&gt;&lt;code&gt;data.eastmoney.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All of them are undocumented public JSON endpoints. No keys, no rate-limit contracts — just HTTP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser (React 19)
   │  /api/*  (batched every 5s)
   ▼
Node proxy (zero deps beyond iconv-lite)
   │  per-symbol TTL cache (1.5s) + inflight merge
   ▼
Tencent / Sina / Eastmoney / Binance / OKX
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend never talks to upstreams directly. The proxy is the single chokepoint: it caches, dedupes, decodes and degrades gracefully.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 6 engineering decisions that matter
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Per-symbol caching, not per-request-string caching
&lt;/h3&gt;

&lt;p&gt;The naive approach — &lt;code&gt;cache.set(requestUrl, data)&lt;/code&gt; — breaks the moment your frontend's watchlist changes. The dashboard's panels subscribe to different code sets dynamically, so the request string changes every poll and you miss the cache every time, hammering upstream.&lt;/p&gt;

&lt;p&gt;The fix: cache &lt;strong&gt;per symbol&lt;/strong&gt; with a 1.5s TTL, and only fetch the symbols that missed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;codes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`q:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hit&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// fetch only `missing`, in chunks of 60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Inflight merging — kill the thundering herd
&lt;/h3&gt;

&lt;p&gt;When a watchlist of 300 codes comes in and the cache is cold, 300 concurrent requests would hit upstream in one burst. The &lt;code&gt;cached()&lt;/code&gt; helper stores the &lt;strong&gt;in-flight promise&lt;/strong&gt; in the cache slot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;inflight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inflight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// share the same promise&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inflight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;cacheSet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inflight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// stale-while-error&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;cacheSet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inflight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;inflight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cold start = one upstream request per missing symbol, not N. And if upstream dies mid-flight, callers get the &lt;strong&gt;last good value&lt;/strong&gt; instead of an error — the dashboard keeps rendering stale data instead of flashing red.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. GBK decoding and referer spoofing
&lt;/h3&gt;

&lt;p&gt;Two Chinese upstreams have old-school quirks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tencent&lt;/strong&gt; returns GBK-encoded text. &lt;code&gt;fetch&lt;/code&gt; gives you UTF-8 garbage unless you decode the buffer manually with &lt;code&gt;iconv&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sina&lt;/strong&gt; returns 403 without a &lt;code&gt;Referer&lt;/code&gt; header pointing at &lt;code&gt;finance.sina.com.cn&lt;/code&gt;. The same URL, same UA, different referer — different result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both took me a debugging session each. The proxy normalizes both so the frontend only ever sees clean UTF-8 JSON.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The curl fallback
&lt;/h3&gt;

&lt;p&gt;Node's &lt;code&gt;fetch&lt;/code&gt; (undici) has a different TLS fingerprint than a browser. Some upstreams (CNBC especially) intermittently reset connections to undici while serving curl fine. The fix is brutally simple — a dual channel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchTextAny&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;curlText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// execFile("curl", ...)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fetchTextAny&lt;/code&gt; is now the default for every upstream call. Zero config, zero dependencies beyond the system &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Chunking — long URLs get rejected
&lt;/h3&gt;

&lt;p&gt;Batch endpoints like Tencent's &lt;code&gt;q=code1,code2,...&lt;/code&gt; reject URLs with too many symbols. 60 per chunk keeps every request under the limit while staying parallel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;texts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetchText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://qt.gtimg.cn/q=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Defensive caching: bounded LRU, no prototype pollution
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The cache is a &lt;code&gt;Map&lt;/code&gt; capped at &lt;strong&gt;2000 entries&lt;/strong&gt; with a 60s sweeper, so a user-typed garbage key can't grow it unboundedly.&lt;/li&gt;
&lt;li&gt;Response objects are built on &lt;code&gt;Object.create(null)&lt;/code&gt; — a symbol named &lt;code&gt;__proto__&lt;/code&gt; from upstream can't pollute the prototype chain.&lt;/li&gt;
&lt;li&gt;Every cache write does &lt;code&gt;cache.delete(key)&lt;/code&gt; before &lt;code&gt;cache.set(key, ...)&lt;/code&gt; to refresh insertion order — that's what makes the eviction a real LRU.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the frontend gets
&lt;/h2&gt;

&lt;p&gt;The React app just calls &lt;code&gt;/api/quotes?codes=sh600519,sz000001,...&lt;/code&gt; every 5 seconds. The proxy decides what's cached, what's stale, and what upstream to hit. The result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero API keys&lt;/strong&gt; — clone, &lt;code&gt;npm install&lt;/code&gt;, &lt;code&gt;npm run dev&lt;/code&gt;, done&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~0 upstream cost&lt;/strong&gt; — one public request per symbol per 1.5s at most&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resilient&lt;/strong&gt; — stale-while-error + curl fallback + multi-source (Binance → OKX) keeps the screen alive through upstream outages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~400KB gzipped bundle&lt;/strong&gt; for the whole cockpit (React 19 + Vite 7 + Tailwind)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Public endpoints are undocumented contracts&lt;/strong&gt; — pin the exact URL shape in one place, because they will break. My proxy centralizes every upstream call behind one function so a format change is a one-line fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test against the real upstream&lt;/strong&gt;, not mock data. GBK encoding and referer requirements only show up against production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits still apply, they're just invisible.&lt;/strong&gt; The 429 you don't see is the connection that silently hangs. Treat every upstream as hostile: timeout everything (&lt;code&gt;AbortController&lt;/code&gt;, 8s default), cache aggressively, degrade gracefully.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo&lt;/strong&gt;: &lt;a href="https://mrd.hermes.cc.cd" rel="noopener noreferrer"&gt;mrd.hermes.cc.cd&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repo&lt;/strong&gt;: &lt;a href="https://github.com/theBigGavin/marketingdashboard" rel="noopener noreferrer"&gt;github.com/theBigGavin/marketingdashboard&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One screen: A-shares / HK / US, commodities, crypto, treasury yields, sector heat, money flow, industry-chain watchlists, and a 7×24 news flash. Plus an Android TV shell with D-pad navigation if you want it on a wall screen.&lt;/p&gt;

&lt;p&gt;If you've built something similar — or hit a quirk in one of these upstreams — I'd love to hear about it in the comments.&lt;/p&gt;

</description>
      <category>node</category>
      <category>webdev</category>
      <category>api</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
