koala73 / koala73/worldmonitor

perf(markets): cache stablecoin gap lookups per coin, not per request shape

Open
#6,321 0 comments 0 reactions 0 assignees View on GitHub
enhancement P2
Dominant language
TypeScript
Stars
86.6k
Forks
13.1k
Avg merge
8h 4m
Merged PRs (30d)
825

Description

## Problem

#6308 gave `ListStablecoinMarkets` a bounded provider lookup for coin IDs the seeded snapshot does not carry. That lookup is cached under a key derived from a hash of the **whole request's gap-ID set**:

```
server/worldmonitor/market/v1/list-stablecoin-markets.ts
const cacheKey = `${GAP_CACHE_KEY_PREFIX}${await sha256Hex([...ids].sort().join(','))}`;
```

So reuse is per request *shape*, not per coin:

- `?coins=frax` → key = hash("frax")
- `?coins=frax,paxos-standard` → key = hash("frax,paxos-standard")

Both miss independently, and both fetch `frax` from CoinGecko — even a second apart. The same applies to negative results: an ID that does not exist re-triggers a provider call every time it appears in a different combination.

## Why it was left this way

Deliberate, and documented in the handler. Nothing sends a non-empty `coins` today — `src/components/StablecoinPanel.ts` sends `coins: []`, which is served from the snapshot alone and never reaches a provider. Per-coin keys would optimise a traffic pattern that does not exist yet, and they carry real complexity: the `PROVIDER_ERROR` vs `NOT_FOUND` split (`cacheFetcherErrors: false`) has to survive per-ID, so an outage must not be written as a per-coin negative.

Until a real caller with overlapping ID sets exists, the cost is bounded by the 25-ID cap and the endpoint's 60/min fail-closed rate policy.

## When to do this

When something actually starts sending overlapping `coins` sets — the deferred MCP tool in #4525 is the likely first one, since agent callers request arbitrary subsets.

## Sketch

1. Per-ID keys: `market:stablecoins:rpc:v1:`. IDs are already validated against `^[a-z0-9][a-z0-9-]{0,63}$` before reaching the lookup, so they are safe as raw key suffixes — no hashing needed.
2. Batch-read candidates with `getCachedJsonBatch` (single pipelined round-trip).
3. Fetch only the still-missing subset, still in one batched provider call.
4. Write each resolved coin back under its own key.

## Acceptance criteria

- [ ] `?coins=frax` followed by `?coins=frax,paxos-standard` makes exactly one provider call for `frax` — with a test, since the current cache-reuse test repeats an identical request and cannot catch this.
- [ ] A provider outage is still never written as a per-coin negative; `PROVIDER_ERROR` and `NOT_FOUND` stay distinguishable.
- [ ] The empty-`coins` request still performs zero provider calls under every Redis state.

## Related

- #6308 (introduced the lookup and this caching granularity)
- #4525 (the deferred MCP tool, the likely first caller to care)

Found by an efficiency review pass during #6308.

Contributor guide

Open the contributing guide

Research direction

Start in server/worldmonitor/market/v1/list-stablecoin-markets.ts and inspect the existing cache-reuse test, getCachedJsonBatch, and provider error handling. Verify the empty-coins path and the current PROVIDER_ERROR/NOT_FOUND distinction before changing the cache flow. Done means overlapping requests fetch each coin only once, outages are not cached as negatives, and empty requests make zero provider calls.

Written by the indexing model from the issue text.

Assessment

Tech stack
redis, typescript
Domain
api, backend, performance
Issue type
Refactor
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.