Cashtags #
Hover over a cashtag — a stock ticker written as $AAPL, $TSLA, $BTC-USD
… well, anything matching $ + 1–6 letters — in any post to see a live quote
and an intraday chart. Click the cashtag for a detail view with more time
ranges (1D/5D/1M/6M/1Y/5Y), day and 52-week stats, and a link to the
symbol's Yahoo Finance page.
How it works #
The plugin registers a rich-text transform: every cashtag in a post becomes
an inline node containing the visible $SYM text plus a hidden card that
pure CSS (:hover) reveals. No host or SDK changes are involved — the
sandbox only allows click/change/input event bindings, so the hover
itself is CSS, and the card's content is whatever the cache knew when the
post rendered. Clicking a cashtag opens a modal with freshly fetched data,
which doubles as the path for touch devices where there is no hover.
Quotes come from Yahoo Finance's public chart endpoint,
https://query1.finance.yahoo.com/v8/finance/chart/{symbol} — the same
endpoint the
yahoo_finance_api Rust
crate uses for its get_quote_range/get_latest_quotes — fetched through
cashtagsapi, a small Rust proxy in
front of it. The proxy exists purely because Yahoo's endpoint sends no
Access-Control-Allow-Origin header, so a browser-context fetch straight to
it is blocked by CORS regardless of plugin permissions; a same-origin-
friendly proxy is the only fix. It passes Yahoo's response through verbatim
(status + body, after validating the symbol/interval/range against a
whitelist and gating on a shared-secret token), so everything below
describes Yahoo's own response shape unchanged. One response carries both
the quote snapshot (chart.result[].meta) and the price series
(timestamp + indicators.quote[].close) the chart is drawn from. The
plugin's request and response handling mirrors the crate's, including its
"too many requests" detection (HTTP 429 or the message in the body).
Rate limiting #
Yahoo doesn't publish official limits, so the plugin stays deliberately conservative:
- One request at a time, with at least half a second between request starts.
- Per-symbol deduplication — ten posts on screen mentioning
$AAPLcause exactly one request. - Device-local cache with a 60-second freshness window. A stale cached quote still renders (marked "as of HH:MM … refreshing") instead of spinning, while one background refresh runs per symbol.
- Negative caching — symbols Yahoo rejects as unknown/delisted are cached for 15 minutes so they can't trigger a request on every render.
- Backoff — any 429/"too many requests" response pauses the entire request queue for 60 seconds, doubling on consecutive hits (capped at 15 minutes), and the per-session request count is capped outright.
- The detail modal's range buttons reuse the quote's own intraday series for 1D instead of refetching, and each range selection is a single request through the same queue.
Privacy #
The only permission is fetch for cashtagsapi.7778777.online/chart/*. Every
request also carries a fixed X-Api-Token header — a low-effort-abuse
deterrent baked into this public plugin source, not a hard secret; real
protection against abuse is cashtagsapi's own daily quota and rate
limiting, not this string's secrecy. The quote cache lives in device-local
plugin storage (loadLocalData), never in your account preferences —
nothing syncs across devices and nothing about what you hover over is
stored server-side by the plugin or the proxy (which doesn't log per-request
identity beyond a daily call count). Requests are made with
credentials: omit (the SDK enforces this), so no cookies are sent.
Known limitations #
- Cashtags are
$+ 1–6 ASCII letters:$BRK.B,$BTC-USDand friends don't match. (The chart endpoint accepts them fine — the detector is the limit, kept intentionally conservative to avoid wrapping things that aren't tickers.) - Symbols inside link/mention facets, or inside an ordinary hashtag facet
(
#something), are never matched, so a$inside a URL can't pop a card. The one exception is Bluesky's own native cashtag support (shipped in v1.114): typing$AAPLgets stored as anapp.bsky.richtext.facet#tagfacet rather than plain text — the plugin recognizes those by their underlying tag value and wraps them the same as plain-text cashtags, instead of leaving them as Impro's generic hashtag-search link. - The hover card renders at transform time; if a first-ever fetch for a symbol takes longer than ~3.5 s, the card shows "Loading quote…" until the post next renders (scroll away/back, open the thread, or click the cashtag). The fetch keeps running in the background either way.
- The card positions itself below the tag with plain CSS; in containers that clip overflow it can be cut off.
- Quotes depend on
cashtagsapi.7778777.onlinebeing up (not a regression — a direct-to-Yahoo fetch was already 100% broken by CORS before this proxy existed). If the proxy is down, the plugin degrades the same way it does for any other fetch failure: a transient error card, retried on the next render.
Local development #
See ../impro/plugins.md for the general workflow. In short:
npm install
npm start # watches src/main.js and rebuilds main.js
main.js is symlinked into impro/plugins-local/, so changes appear after
a page reload in a locally running Impro instance
(cashtags__LOCAL under Settings → Community Plugins).
Unit tests (no test framework, just node:test):
npm test
They cover cashtag detection, the Yahoo response parsing/rate-limit detection, the rate limiter's spacing/backoff/cap behavior, the cache's TTL/LRU/persistence, and the chart geometry.
Requirements #
Like the other workspace plugins, this builds against the local SDK
(package.json points @impro.social/impro-plugin at
file:../impro/impro-plugin) rather than a published release. Everything
it uses (registerRichTextTransform, Modal incl. update(), Setting,
whitelist-gated fetch) exists in the local SDK source.
- JavaScript 96%
- CSS 4%