<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Ethan Cole]]></title><description><![CDATA[Ethan Cole]]></description><link>https://ethancole-dev.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Ethan Cole</title><link>https://ethancole-dev.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 20 Sep 2026 22:37:12 GMT</lastBuildDate><atom:link href="https://ethancole-dev.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[A live crypto price ticker on a static site, three ways]]></title><description><![CDATA[I run a small static site on Netlify and wanted a scrolling price ticker across the top. No build step, no server, and ideally no signup. Here is what I tried and where each one hurt.
1. Fetch a publi]]></description><link>https://ethancole-dev.hashnode.dev/a-live-crypto-price-ticker-on-a-static-site-three-ways</link><guid isPermaLink="true">https://ethancole-dev.hashnode.dev/a-live-crypto-price-ticker-on-a-static-site-three-ways</guid><category><![CDATA[webdev]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[api]]></category><category><![CDATA[performance]]></category><category><![CDATA[crypto]]></category><dc:creator><![CDATA[Ethan Cole]]></dc:creator><pubDate>Tue, 15 Sep 2026 14:36:49 GMT</pubDate><content:encoded><![CDATA[<p>I run a small static site on Netlify and wanted a scrolling price ticker across the top. No build step, no server, and ideally no signup. Here is what I tried and where each one hurt.</p>
<h2>1. Fetch a public API in the browser</h2>
<p>The shortest path is a plain fetch against a public endpoint. CoinGecko still answers without a key on the simple price route:</p>
<pre><code class="language-js">const url = "https://api.coingecko.com/api/v3/simple/price"
  + "?ids=bitcoin,ethereum,solana&amp;vs_currencies=usd&amp;include_24hr_change=true";

const res = await fetch(url);
const data = await res.json();
// { bitcoin: { usd: 77642, usd_24h_change: 0.58 }, ... }
</code></pre>
<p>Rate limiting bit me first. The free tier counts calls per IP, and since each visitor brings their own IP, the ceiling is effectively per visitor. That holds until a dozen people behind one office network open a page that polls every five seconds. Poll once a minute at most, and stash the last response in <code>sessionStorage</code> so a reload does not spend a call.</p>
<p>Then there is the id mapping. The route wants CoinGecko ids rather than symbols, and <code>solana</code> only works because the two happen to match. <code>MATIC</code> is <code>matic-network</code>. <code>UNI</code> is <code>uniswap</code>. I ended up with a hardcoded lookup table, the sort of thing that quietly rots the day a project renames itself.</p>
<p>Cost: roughly 40 lines with the rendering, plus a table to maintain.</p>
<h2>2. Proxy it through a function</h2>
<p>If you need a key, or you want one cache shared by every visitor, put a function in front:</p>
<pre><code class="language-js">// netlify/functions/prices.js
let cache = { at: 0, body: null };

export default async () =&gt; {
  if (Date.now() - cache.at &lt; 60_000) {
    return new Response(cache.body, { headers: { "content-type": "application/json" } });
  }
  const r = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum&amp;vs_currencies=usd");
  cache = { at: Date.now(), body: await r.text() };
  return new Response(cache.body, { headers: { "content-type": "application/json" } });
};
</code></pre>
<p>For a real app this is the answer. One upstream call a minute regardless of traffic, the key never reaches the browser, and you get to reshape the payload on the way out.</p>
<p>What tripped me up is that the in-memory cache only lives as long as the instance stays warm. A cold start pays the upstream call again, and several concurrent instances each keep their own copy, so the real rate is a multiple of what the code suggests. Move the cache into a KV store if that number has to be exact.</p>
<p>Cost: a function, a deploy target that runs one, and a cold start on the first request of the day.</p>
<h2>3. Drop in a hosted widget</h2>
<p>On a marketing page I stopped trying to own the data. A hosted ticker is one element and one script:</p>
<pre><code class="language-html">&lt;div id="crypto-marquee" data-theme="dark" data-count="10"&gt;&lt;/div&gt;
&lt;script src="https://www.thecoinanalysis.com/widgets/crypto-marquee-widget.js"&gt;&lt;/script&gt;
</code></pre>
<p>I settled on the marquee from <a href="https://www.thecoinanalysis.com/crypto-marquee-widget">The Coin Analysis</a>, which asks for no key and no account. It weighs about 16 KB, takes its settings from data attributes, and renders a credit link back to the source. That last part is the deal you accept with any free widget.</p>
<p>Pasting someone else's script in took me less time than the three checks around it.</p>
<p><strong>Content Security Policy.</strong> The tag fails silently when your policy leaves the host out of <code>script-src</code>. The widget also calls its own price endpoint, so <code>connect-src</code> needs the host too. If it injects styles, add <code>style-src</code>. If it loads token logos, add <code>img-src</code>.</p>
<p><strong>Layout shift.</strong> The ticker renders once the script has loaded, and everything below it jumps down. Reserve the height yourself:</p>
<pre><code class="language-css">#crypto-marquee { min-height: 44px; }
</code></pre>
<p>Measure the rendered height once in devtools and hardcode it. On a page like that, it is the single biggest Cumulative Layout Shift win available.</p>
<p><strong>Motion.</strong> A scrolling marquee is a vestibular trigger for some readers. Whatever you embed, wrap it:</p>
<pre><code class="language-css">@media (prefers-reduced-motion: reduce) {
  #crypto-marquee * { animation: none; }
}
</code></pre>
<p>That rule is the only lever you have from the outside, so check that the widget renders into ordinary DOM rather than a closed shadow root before you commit to it.</p>
<h2>What I would pick again</h2>
<p>On a side project where the prices are decoration, the hosted widget wins on time and has never woken me up. When the number is the product, fetch it yourself and keep a function in front, because the day a free tier changes you want that failure landing in your logs instead of inside a script you do not control.</p>
<p>One habit carried across all three: render the stale value with a timestamp instead of a spinner. A price from four minutes ago still tells the reader something. A spinner only tells them the site is broken.</p>
]]></content:encoded></item><item><title><![CDATA[Three requests per hour: what a strict free tier taught me]]></title><description><![CDATA[Most "free tier" API posts assume a generous budget. This one is about the other case: an API that allows three requests per hour, per endpoint. I have been building a small crypto dashboard against o]]></description><link>https://ethancole-dev.hashnode.dev/three-requests-per-hour-what-a-strict-free-tier-taught-me</link><guid isPermaLink="true">https://ethancole-dev.hashnode.dev/three-requests-per-hour-what-a-strict-free-tier-taught-me</guid><category><![CDATA[api]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[webdev]]></category><category><![CDATA[node]]></category><dc:creator><![CDATA[Ethan Cole]]></dc:creator><pubDate>Tue, 15 Sep 2026 14:33:27 GMT</pubDate><content:encoded><![CDATA[<p>Most "free tier" API posts assume a generous budget. This one is about the other case: an API that allows three requests per hour, per endpoint. I have been building a small crypto dashboard against one of those, and the limit changed how I write client code more than any performance trick ever has.</p>
<p>The numbers below come from The Coin Analysis public price API, which I picked because its free tier is exactly that strict. The ideas apply to any API where calls are the scarce resource.</p>
<h2>One wide call beats twenty narrow ones</h2>
<p>The instinct is to request one token, then the next, then the next. Three calls in, you are locked out for the hour.</p>
<p>Almost every list endpoint accepts a page size, and it is usually larger than you think. Here the whole universe comes back in a single request:</p>
<pre><code class="language-bash">curl "https://www.thecoinanalysis.com/api/public/v1/prices?perPage=250&amp;order=market_cap" \
  -H "x-api-key: $KEY"
</code></pre>
<p>That returned 220 tokens and <code>"pages": 1</code> in the meta block. One call, the entire dataset, and filtering afterwards costs nothing. If you only care about a handful, ask for them by name rather than one at a time:</p>
<pre><code class="language-bash">curl ".../prices?symbols=BTC,ETH,SOL" -H "x-api-key: $KEY"
</code></pre>
<p>Three tokens, one call. The rule is simple: every loop that contains an HTTP request is a bug waiting for a 429.</p>
<h2>Refresh on a timer</h2>
<p>Three calls per hour leaves one refresh every twenty minutes. Refreshing when a user asks means the third visitor of the hour gets an error page.</p>
<p>So the fetch belongs on a timer, and the request path only ever reads the last good value:</p>
<pre><code class="language-js">let cache = { at: 0, data: null };
const WINDOW = 20 * 60 * 1000;

async function prices() {
  if (Date.now() - cache.at &lt; WINDOW &amp;&amp; cache.data) return cache.data;
  const r = await fetch(`${BASE}/prices?perPage=250`, {
    headers: { "x-api-key": process.env.KEY },
  });
  if (r.status === 429) return cache.data;   // serve stale, never throw
  cache = { at: Date.now(), data: await r.json() };
  return cache.data;
}
</code></pre>
<p>The important line is the 429 branch. Stale prices are a small inconvenience. An exception in a request handler is a broken page.</p>
<h2>The server already did the maths</h2>
<p>The history endpoint returns the series and the derived numbers together:</p>
<pre><code class="language-json">{ "history": [ { "ts": "2026-08-16T15:00:00.000Z", "price": 63044 } ],
  "volatility": 0.5958, "maxDrawdown": 39.0, "athUsd": 126080 }
</code></pre>
<p>Volatility and maximum drawdown for that window, already computed. Pulling raw candles to calculate them yourself costs the same single call and adds code you now have to test. When a rate limit is tight, prefer the endpoint that answers the question over the one that returns the ingredients.</p>
<h2>Where the 429 actually applies</h2>
<p>This one cost me an afternoon. When the list endpoint was exhausted, I backed off everything. But the window is counted per endpoint, so detail and history were still available the whole time.</p>
<p>Track the reset per route:</p>
<pre><code class="language-js">const blocked = new Map();   // route -&gt; timestamp

async function call(route) {
  if (Date.now() &lt; (blocked.get(route) ?? 0)) throw new Error("cooling down");
  const r = await fetch(BASE + route, { headers: { "x-api-key": KEY } });
  if (r.status === 429) {
    blocked.set(route, Date.now() + 60 * 60 * 1000);
    throw new Error("rate limited");
  }
  return r.json();
}
</code></pre>
<p>Blanket backoff throws away quota you still have.</p>
<h2>About that wildcard CORS header</h2>
<p>The API answers with <code>access-control-allow-origin: *</code>, so the browser will happily call it. That does not mean you should: doing it from the front end ships your key to everyone who opens the devtools, and a shared key burns three requests in one page load.</p>
<p>Keep the key on a server or an edge function, cache there, and let the browser read your own endpoint. With a single call feeding every visitor for twenty minutes, the strict tier stops mattering.</p>
<h2>What I would keep</h2>
<p>A tight free tier turned out to be a good teacher. Batching instead of looping, caching on a timer, preferring the computed answer to the raw series, scoping the backoff to one route: none of that is specific to a stingy quota. It makes a generous API better too, which is the part I did not expect.</p>
<p>The endpoints and the free key are documented at <a href="https://www.thecoinanalysis.com/developers">The Coin Analysis</a> if you want to try the same exercise.</p>
]]></content:encoded></item></channel></rss>