Files
README.md

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 $AAPL cause 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-USD and 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 $AAPL gets stored as an app.bsky.richtext.facet#tag facet 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.online being 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.