feat(feeds): per-watchlist subscribe feed — GET /api/v1/feeds/watch?ids= - #8471
Conversation
Stateless by construction (requirement #1): the watchlist travels IN the URL, not server-side. `?ids=` is a compact, kind-prefixed comma list (s<netuid>/v<hotkey>/a<ss58>), capped at 50 entities -- a malformed token is a 400, more than 50 is a 413. The cap is deliberately its own constant (WATCH_MAX_IDS) rather than reusing FEED_MAX_ITEMS: "how many entities a URL may name" and "how many items a feed page may return" are independently-tuned numbers that only happen to both be 50 today. A real gap this issue's own text didn't anticipate, documented on the issue before implementing: there is no change-class or incident data source for validators or accounts anywhere in this codebase's feeds infrastructure (registryItems/incidentItems are both keyed on subnet netuid only). validator/account ids are still accepted and counted toward the 50-id cap -- so a URL built from a mixed real watchlist never silently exceeds it -- but produce zero items, and the feed's own `description` says plainly how many ids produced nothing rather than staying silent about the gap. Reuses registryItems/incidentItems per unique watched netuid (requirement #2's "same lane discipline" -- literally the same functions the existing per-subnet feed already calls), so there's exactly one registry+incident item-building path in this codebase, not a second one for watch. workers/api.ts: `ids` added to the edge-cache key composition alongside the existing tag/since/until/limit params -- omitting it would have let two different watchlists share one cached response, since the cache key is the composed query string, not the raw request URL. Covered by a dedicated regression test (different ?ids= -> different cache entries; same ?ids= again -> the existing entry, not a third). apps/ui/content/docs/feeds.mdx: a "Watchlist feed" section -- the ?ids= encoding table, the validator/account limitation stated plainly, and the issue's own required privacy line ("anyone with this URL sees which entities it tracks") both in the docs prose and baked into the feed's own `description` field, so it reaches a feed reader, not just this page. 23 new tests (17 in the parseWatchIds/watchFeedItems/handleFeedRequest suites, 1 dedicated cache-key regression, plus assertions folded into existing blocks) -- 502 test files / 12380 tests passing repo-wide. feeds.ts patch coverage 98.87%/95.45% (stmts/branches); the one gap (line 777) is pre-existing, unrelated defensive code. Path split (noted on the issue): this PR is the backend route only. Requirement #3's "Subscribe to this watchlist" UI affordance is a apps/ui/ Path C change with its own screenshot-table requirement -- follow-up PR once this endpoint exists to point it at. Closes #8376
|
Warning ⏸️ LoopOver review result - manual review recommendedReview updated: 2026-07-28 04:05:34 UTC
Review summary Nits — 7 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionPartially addressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. Decision record
Visual preview
Click any thumbnail to open the full-size screenshot. Before = production · After = this PR's preview deploy. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
metagraphed-registry-sync-api | b0e6b0e | Jul 28 2026, 03:50 AM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
metagraphed-data-api | b0e6b0e | Jul 28 2026, 03:49 AM |
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #8471 +/- ##
=======================================
Coverage 97.82% 97.82%
=======================================
Files 419 419
Lines 29126 29171 +45
Branches 10944 10956 +12
=======================================
+ Hits 28492 28537 +45
Misses 143 143
Partials 491 491
🚀 New features to boost your workflow:
|
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
metagraphed-ui | 20874fd | Commit Preview URL Branch Preview URL |
Jul 28 2026, 03:25 AM |
token is always non-empty (filtered by .filter(Boolean) beforehand), so token[0] can never be undefined -- the fallback was dead code and the uncovered branch codecov/patch was flagging.
Closes #8376 — with a documented scope note posted first (#8376 (comment)): a real data-model gap this issue's own text didn't anticipate, plus a deliberate Path split.
The gap
There is no change-class or incident data source for validators or accounts anywhere in this codebase's feeds infrastructure —
registryItems/incidentItemsare both keyed on subnet netuid only (the changelog tracks subnet/artifact/coverage deltas; incidents track per-surface, i.e. per-subnet, downtime).WatchlistExporthas carriedvalidator/accountkinds since #8248, but nothing downstream has ever produced a feed-shaped event for either.Handled honestly, not silently:
validator/accountids in?ids=are still accepted and counted toward the 50-id cap (so a URL built from a real mixed watchlist never silently exceeds it), but produce zero items — and the feed's owndescriptionfield says plainly how many ids produced nothing, rather than leaving a gap between what was asked for and what came back.What shipped
Stateless by construction (requirement #1):
GET /api/v1/feeds/watch.{rss,atom,json}?ids=<compact set>— the watchlist travels in the URL, never server-side.?ids=is kind-prefixed (s<netuid>/v<hotkey>/a<ss58>), comma-joined, capped at 50 — a malformed token is a400, over 50 is a413.Content (requirement #2): reuses
registryItems/incidentItemsper unique watched netuid — literally the same functions the existing per-subnet feed (/api/v1/feeds/subnets/{netuid}) already calls, so there's exactly one registry+incident item-building path in this codebase, not a second one for watch.Cache posture (requirement #4): a real bug I caught while wiring this —
workers/api.ts's edge-cache key composition builds a canonical query string fromtag/since/until/limit, andidswasn't in it. Without the fix, two different watchlists would have shared one cached response. Fixed, with a dedicated regression test proving different?ids=→ different cache entries, and identical?ids=→ the same entry reused.Privacy line (requirement #5): "anyone with this URL sees which entities it tracks" appears both in the new docs section (
apps/ui/content/docs/feeds.mdx, a<Callout type="warn">) and baked into the feed's owndescriptionfield, so it reaches a feed reader that never visits the docs page at all.Not in this PR (Path split, noted on the issue)
Requirement #3's "Subscribe to this watchlist" UI affordance is a
apps/ui/Path C change (its own component, its own screenshot-table requirement) — shipping as a focused follow-up once this endpoint exists to point it at, matching how #8364→#8365→#8366 split backend-then-UI on the chain-firehose work this session.Validation
src/feeds.tspatch coverage: 98.87%/95.45% (stmts/branches) — the one gap (line 777) is pre-existing, unrelated defensive code (readData's catch block). Docs page verified rendering in a live dev server (screenshot below viaget_page_text— table + Callout render correctly).No OpenAPI/schema surface touched —
/api/v1/feeds/*sits outside the JSON contract already (matches every existing feed route). Not a frontend visual PR — Path B (route + tests) + a docs content addition, noapps/ui/srccomponent/route changes.