Problem
On the story page (/story/[storylineId]), the first stat box (Market Cap) renders as an empty bordered box while the PLOT/USD price is loading. After a few seconds it eventually appears (e.g. "$68.84 +0.0%"), meaning all 3 price sources in the fallback chain are slow or failing before one finally succeeds.
Root Cause
Two issues:
1. Price API fallback chain is slow/unreliable
getPlotUsdPrice() in lib/usd-price.ts tries 3 sources sequentially with 3s timeouts each:
- Mint Club SDK — dynamic import + RPC call. Likely fails or times out (3s wasted)
- GeckoTerminal — HTTP fetch. May not list PLOT or may be rate-limited (3s wasted)
- CoinGecko — HTTP fetch. May not list PLOT without API key (3s wasted)
Worst case: 9 seconds of sequential timeouts before returning null. Even if one source eventually works, the user sees a blank box for several seconds.
Investigation needed:
- Which source(s) actually succeed for the PLOT token?
- Which ones consistently fail? (check server logs for
[USD Price] source=X result=miss)
- Are we hitting rate limits on GeckoTerminal/CoinGecko?
- Is the Mint Club SDK import failing on the server side?
- Is the 2-minute in-memory cache (
CACHE_TTL) working, or is it being invalidated between requests (e.g. serverless cold starts)?
2. Loading state shows blank box
MarketCapBox (src/components/MarketCapBox.tsx:30) returns null while plotUsd is loading:
if (!plotUsd) return null;
The parent always renders the box container, so null content = empty bordered box.
Fix Plan
Part A: Diagnose & fix the price source reliability
- Add structured logging to
fetchPlotUsdPrice() — log which source succeeded, how long each attempt took, and why failures occurred (timeout vs HTTP error vs missing data)
- Identify which sources actually work for PLOT token and remove/deprioritize dead sources
- Consider
Promise.any() instead of sequential fallback — try all sources in parallel, use whichever responds first
- Extend cache TTL or add stale-while-revalidate — return stale cached price immediately while refreshing in background, especially important for serverless where in-memory cache is lost on cold starts
- Consider DB-backed cache as a last-resort fallback (store last known good price in Supabase) so there's always a value to show
Part B: Loading state fallback
Show "—" with label while loading, so the box is never blank:
if (!plotUsd) {
return (
<>
<div className="text-foreground text-sm font-bold">—</div>
<div className="text-muted text-[9px]">Market Cap</div>
</>
);
}
Files
lib/usd-price.ts — price fetching logic, cache, fallback chain
src/components/MarketCapBox.tsx — loading state UI
src/hooks/usePlotUsdPrice.ts — client-side hook
src/app/api/tokens/plot-price/route.ts — API endpoint
Acceptance Criteria
Problem
On the story page (
/story/[storylineId]), the first stat box (Market Cap) renders as an empty bordered box while the PLOT/USD price is loading. After a few seconds it eventually appears (e.g. "$68.84 +0.0%"), meaning all 3 price sources in the fallback chain are slow or failing before one finally succeeds.Root Cause
Two issues:
1. Price API fallback chain is slow/unreliable
getPlotUsdPrice()inlib/usd-price.tstries 3 sources sequentially with 3s timeouts each:Worst case: 9 seconds of sequential timeouts before returning
null. Even if one source eventually works, the user sees a blank box for several seconds.Investigation needed:
[USD Price] source=X result=miss)CACHE_TTL) working, or is it being invalidated between requests (e.g. serverless cold starts)?2. Loading state shows blank box
MarketCapBox(src/components/MarketCapBox.tsx:30) returnsnullwhileplotUsdis loading:The parent always renders the box container, so
nullcontent = empty bordered box.Fix Plan
Part A: Diagnose & fix the price source reliability
fetchPlotUsdPrice()— log which source succeeded, how long each attempt took, and why failures occurred (timeout vs HTTP error vs missing data)Promise.any()instead of sequential fallback — try all sources in parallel, use whichever responds firstPart B: Loading state fallback
Show "—" with label while loading, so the box is never blank:
Files
lib/usd-price.ts— price fetching logic, cache, fallback chainsrc/components/MarketCapBox.tsx— loading state UIsrc/hooks/usePlotUsdPrice.ts— client-side hooksrc/app/api/tokens/plot-price/route.ts— API endpointAcceptance Criteria