docs: tighten /research/curator spec to match Chartmetric contract#137
Conversation
- platform enum: spotify | applemusic | deezer (remove amazon + youtube; Chartmetric's curator endpoints only support these three) - id is documented as a numeric Chartmetric curator ID with a pattern (^[0-9]+$) and an example value (2 — Spotify's own account) - Add 404 response for unknown curator id - Description clarifies how to discover curators via /research/playlists Surfaced while testing api PR #366 on preview — callers were passing string handles like "spotify" / "filtr" and getting opaque upstream 400s. Docs now correctly specify numeric id + restricted platform set. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughUpdates the OpenAPI specification for the Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@api-reference/openapi/research.json`:
- Around line 1249-1255: Update the ResearchErrorResponse schema description to
reflect that it is used for 400, 401 and 404 responses (not just 400/401) so
generated docs aren't misleading; locate the ResearchErrorResponse component
(the schema referenced by responses including the 404 in the endpoint diff) and
change its description text to list 400, 401 and 404 (and any brief context
about curator not found if present).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 77beaf18-8795-47d8-b35f-fbbade22be2c
📒 Files selected for processing (1)
api-reference/openapi/research.json
| "404": { | ||
| "description": "Curator not found for the given platform/id.", | ||
| "content": { | ||
| "application/json": { | ||
| "schema": { | ||
| "$ref": "#/components/schemas/ResearchErrorResponse" | ||
| } |
There was a problem hiding this comment.
ResearchErrorResponse description is stale after adding 404.
This endpoint now uses ResearchErrorResponse for 404 as well, but Line 5002 still says the schema is for 400/401 only. Please update that description to avoid misleading generated docs.
✏️ Suggested wording update
- "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).",
+ "description": "Error response returned by research endpoints for validation failures (400), authentication errors (401), and resource-not-found errors where applicable (404).",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@api-reference/openapi/research.json` around lines 1249 - 1255, Update the
ResearchErrorResponse schema description to reflect that it is used for 400, 401
and 404 responses (not just 400/401) so generated docs aren't misleading; locate
the ResearchErrorResponse component (the schema referenced by responses
including the 404 in the endpoint diff) and change its description text to list
400, 401 and 404 (and any brief context about curator not found if present).
Chartmetric's /curator/:platform/:id endpoint only supports spotify, applemusic, and deezer and requires a numeric curator id. Callers were passing string handles like "spotify" / "filtr" and getting opaque upstream 400s back. Now reject up front with helpful messages: - "Invalid platform. Must be one of: spotify, applemusic, deezer" - "id must be a numeric Chartmetric curator ID (e.g. 2 for Spotify)" Matches the tightened docs spec in recoupable/docs#137. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add 30 research API endpoints, 28 MCP tools, Zod validation, and tests
Research primitive — provider-agnostic music industry research:
- 30 REST endpoints under /api/research/ (Chartmetric, Perplexity, Exa, Parallel)
- 28 MCP tools with proper auth (resolveAccountId) and credit deduction
- 2 shared handlers (handleArtistResearch, handleResearchRequest) for DRY
- Zod validation on discover endpoint
- 10 test files (token, proxy, artist resolution, charts, lookup, similar, search, track, web, discover)
- Source param allowlist on metrics to prevent path injection
- proxyToChartmetric wrapped in try/catch for consistent error contract
- All 1767 tests passing, 0 lint errors in research files
Made-with: Cursor
* feat: add GET /api/research/track/playlists endpoint
Track-level playlist lookup — returns editorial, indie, and algorithmic
playlists for a specific track. Accepts Chartmetric track ID or track
name (resolved via search). Proxies to Chartmetric
/track/{id}/{platform}/{status}/playlists.
Includes route handler, domain handler, MCP tool, and 8 unit tests.
Made-with: Cursor
* fix: use Spotify-powered track search for reliable q= resolution
Adds resolveTrack() — searches Spotify first (accurate matching with
artist: filter), maps Spotify track ID to Chartmetric ID, falls back
to Chartmetric search if Spotify fails. Adds optional artist= param
to track/playlists endpoint and MCP tool.
Made-with: Cursor
* fix: resolve tracks via ISRC for reliable Chartmetric ID mapping
Spotify search returns ISRC, which maps to Chartmetric more reliably
than Spotify track ID. Tries /track/isrc/{isrc} first, then
/track/spotify/{id}, then falls back to Chartmetric text search.
Made-with: Cursor
* fix: resolve track ID via artist playlists + tracks matching
Spotify search finds exact track name, then we match against the
artist's Chartmetric playlists/tracks by name to get the cm_track ID.
Avoids Chartmetric's broken text search and unreliable ID mapping.
Made-with: Cursor
* fix: use Chartmetric /track/:type/:id/get-ids for track resolution
Maps ISRC → chartmetric_ids via the correct endpoint path. Falls back
to Spotify track ID if ISRC lookup fails. Platform-agnostic.
Made-with: Cursor
* style: fix formatting in research track playlists files
Made-with: Cursor
* fix(lint): remove unused getCorsHeaders import
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(review): SRP on chartmetric token cache; drop MCP research tools
Per review feedback on this PR:
SRP — resetTokenCache in its own file:
- Extract shared cache state into lib/chartmetric/chartmetricTokenCache.ts
- Move resetTokenCache to lib/chartmetric/resetTokenCache.ts
- getChartmetricToken now reads/writes the shared cache module
Remove MCP research tools to keep this PR focused on the API surface:
- Delete lib/mcp/tools/research/ (29 register* files + index)
- Remove registerAllResearchTools import/call from lib/mcp/tools/index.ts
- MCP tools can land in a follow-up PR once the HTTP endpoints ship
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: replace handleArtistResearch orchestrator with small composable helpers
Ports 15 artist-scoped research handlers off the template-method
orchestrator (handleArtistResearch) onto three focused helpers:
requireArtist, getArtistResearch, and jsonSuccess/jsonError. Each
handler now reads top-to-bottom in ~10 lines and explicitly names
its response key (no more magic { status } spreading collision).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: rename getArtistResearch → handleArtistResearch (no functional change)
Pure rename: file, symbol, test file, and all 15 handler call sites.
Keeps the new composable-helper implementation — only the name now
matches the legacy naming.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: consolidate JSON response helpers (DRY)
errorResponse was duplicated across lib/networking/errorResponse.ts
(existing) and a new lib/networking/jsonResponse.ts helper added with
this PR's research refactor. Merge them:
- Update errorResponse to return { status: "error", error } (was just
{ error }) so success/error envelopes are symmetric.
- Move jsonSuccess into a dedicated successResponse.ts file (one
exported function per file per repo convention).
- Delete lib/networking/jsonResponse.ts.
- Point the 15 research handlers + requireArtist at the unified
helpers.
The one pre-existing caller of errorResponse (validateAgentVerifyBody)
now gets status: "error" in its body. Its tests only assert status
codes, not body shape — no test changes needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: align research handler naming + shape with codebase conventions
- validateArtistRequest (was requireArtist) — matches validate*Request family
- Composed validators for metrics/playlists/similar endpoints whose inline
validation was non-trivial
- Add try/catch + explicit Promise<NextResponse> return type to all 15
artist-scoped research handlers
- Rename `gate` local to `validated` to match the rest of the codebase
No behavior change.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(kiss): spread validated into handleArtistResearch call
The handlers were manually re-assembling { artist, accountId } from
validated into handleArtistResearch's params object — pure ceremony
since validated already carries exactly those fields. Switch to
`{ ...validated, path, ... }` across the 12 simple handlers, and use
destructure-into-rest for the 3 that carry endpoint-specific extras.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: align charts + discover handlers with validator/helper conventions
- New handleResearchProxy({ accountId, path, query?, credits? })
returns { data } | { error, status } — same shape as
handleArtistResearch. No auth (validators own that now).
- New validateGetResearchChartsRequest.ts and renamed
validateDiscoverQuery -> validateGetResearchDiscoverRequest; both
now do auth + param validation per the validate*Request convention.
- Charts + discover handlers: try/catch, Promise<NextResponse>,
successResponse / errorResponse, compose validator + proxy helper.
- handleResearchRequest is left in place — still used by radio,
genres, curator, and festivals handlers (out of scope here).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: port final 4 research handlers; delete handleResearchRequest
- getResearchRadioHandler
- getResearchGenresHandler
- getResearchCuratorHandler
- getResearchFestivalsHandler (non-artist-scoped: /festival/list is a
global Chartmetric endpoint, so it uses handleResearchProxy)
Each now uses its own validateGet<X>Request validator and
handleResearchProxy. None of the four were artist-scoped — all four
hit global Chartmetric paths (/radio/station-list, /genres,
/curator/{platform}/{id}, /festival/list). Deletes the last
template-method orchestrator.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(kiss): rename handleResearchProxy → handleResearch, proxyToChartmetric → fetchChartmetric
Drop the "proxy" terminology per review feedback:
- handleResearchProxy → handleResearch
- proxyToChartmetric → fetchChartmetric
Renames the symbols, file names, and test file names to match.
No behavior change.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: move fetchChartmetric to lib/chartmetric
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(dry): share CHARTMETRIC_BASE between token exchange and fetchChartmetric
Extracts the base URL into lib/chartmetric/chartmetricBase.ts so both
getChartmetricToken and fetchChartmetric reuse the same const.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: port 5 non-artist research GET handlers to validator+handleResearch pattern
- getResearchLookupHandler
- getResearchSearchHandler
- getResearchTrackHandler
- getResearchTrackPlaylistsHandler
- getResearchPlaylistHandler
Each now has a dedicated validateGetResearch<X>Request.ts (auth + param
validation) and composes handleResearch + errorResponse / successResponse
inside try/catch. Removes hand-rolled auth + direct deductCredits calls
from the handlers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: charge credits for each Chartmetric hop in track+playlist lookups
Both getResearchTrackHandler and getResearchPlaylistHandler do a
name-resolution search before the detail fetch. Previously the resolve
step used fetchChartmetric (no credit charge), so a name-based lookup
cost the same as a direct-ID lookup (5 credits) despite hitting the
upstream twice.
Swap the resolve step to handleResearch so each successful upstream
hit deducts 5 credits. Failed searches still deduct nothing
(handleResearch skips deduction on non-200).
Cost mapping:
- Direct ID → 1 call, 5 credits (unchanged)
- Name lookup → 2 calls, 10 credits (was 5)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(review): address 9 review comments
- Rename `gate` -> `validated` in 3 composed validators (KISS)
- Spotify artist URL regex only appears in validateGetResearchLookupRequest;
no duplication, left as-is
- resolveTrack now takes accountId and calls handleResearch so
Chartmetric searches deduct credits (was hand-rolled fetchChartmetric)
- Extract validator files for 4 POST research handlers:
validatePostResearchWebRequest, ...PeopleRequest, ...ExtractRequest,
...EnrichRequest. Handlers now compose validator + existing AI-provider
call inside try/catch with Promise<NextResponse> return types.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(review): tighten validateGetResearchCuratorRequest with clear errors
Chartmetric's /curator/:platform/:id endpoint only supports spotify,
applemusic, and deezer and requires a numeric curator id. Callers were
passing string handles like "spotify" / "filtr" and getting opaque
upstream 400s back.
Now reject up front with helpful messages:
- "Invalid platform. Must be one of: spotify, applemusic, deezer"
- "id must be a numeric Chartmetric curator ID (e.g. 2 for Spotify)"
Matches the tightened docs spec in recoupable/docs#137.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(research/track): add artist disambiguation + better match ranking
Addresses PR #366 review (#366 (comment)):
every common track query ("God's Plan", "Hotline Bling", "Flowers",
"Sicko Mode", "Thriller") returned the wrong track because the resolver
called Chartmetric /search?type=tracks&limit=1 and took tracks[0] with
no ranking and no artist filter.
Changes:
- Raise the upstream limit to 25 (was 1) so real candidates make it into
the pool.
- New pickBestTrackMatch(tracks, q, artist?) — pure ranking function:
1. If artist supplied, drop candidates whose artist_names don't
contain artist (case-insensitive substring).
2. Within the remaining pool, prefer a track whose name equals q
(trimmed, case-insensitive) — fixes "God's Plan" vs "God's".
3. Otherwise fall back to the first remaining track.
- Validator accepts an optional `artist` query param.
- Handler returns 404 with both q and artist in the message when the
artist filter yields nothing (matches the OpenAPI 404 added in
recoupable/docs#138).
Docs PR (now merged): recoupable/docs#138 tightens the /research/track
spec to advertise the new `artist` param, the 404 response, and the
actual response shape (artists[], albums[], etc. — not the wrong
album_name/artist_names the spec claimed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(research/search): pass through beta, platforms, offset to Chartmetric
Chartmetric's default search engine returns exactly 1 result for every
track query (verified against preview: q=Hotline Bling → only T703R
'Bling', q=Flowers → only a Yuda cover, q=Drake&type=tracks → 1 track).
Its docs describe beta=true as "improved beta search engine for higher
relevance and accuracy", and platforms[] is beta-only.
This adds diagnostic passthrough so we can observe the beta response
shape live, and unblocks follow-up work on /api/research/track. All
three params are optional and only forwarded when explicitly set, so
default behavior is unchanged.
Handler also falls back to `suggestions` (beta response shape) when no
artists/tracks/albums array is populated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(research): /track and /playlist become pure ID proxies (KISS)
The original handlers chained /search → /track/:id (and /search → /playlist/:platform/:id
on non-numeric ids). The compound behavior produced wrong results silently,
unpredictable credit charges, and asymmetric error paths that we kept having
to patch (see #366 review). We're reverting both endpoints to
single-call proxies. Discovery is the caller's job via GET /api/research —
which already exposes beta=true for high-relevance search ranking
(a28deb2).
/api/research/track
- Takes a numeric Chartmetric track `id`, single upstream call to /track/:id
- Drops the `q` search param, the `artist` disambiguation param, and the
client-side pickBestTrackMatch ranking helper (not needed — search is the
caller's job)
- 400 when `id` is missing or non-numeric
/api/research/playlist
- Unchanged public shape: still takes platform + id
- Drops the non-numeric-id name-search fallback inside the handler
- Single upstream call to /playlist/:platform/:id for all inputs
Tests: 146 research tests green. `pickBestTrackMatch` and its 8 unit tests
deleted (never shipped; only lived on this branch in fda5592).
Follow-up docs PR will align /research/track's OpenAPI contract with the
new shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(research): /albums id-proxy + /charts enum validation (KISS)
Addresses the remaining two items from the PR #366 review comment.
/api/research/albums
- Replaces the composite name→artist_id→discography flow (same shape
that was returning "DJ Mix" feeds for artist=Drake and duplicated
"Elizabeth Taylor" albums for artist=Taylor Swift) with a thin
/artist/:id/albums proxy.
- New dedicated validator validateGetResearchAlbumsRequest.ts takes a
numeric artist_id; drops the old name/UUID fuzzy-resolve path.
- Discovery is the caller's job via GET /api/research?type=artists&beta=true.
- Note: the shared validateArtistRequest + handleArtistResearch
composites still power ~17 other artist-scoped endpoints (similar,
metrics, cities, audience, career, etc.). Those carry the same risk
class and should get the same treatment in follow-up PRs — out of
scope here.
/api/research/charts
- Validator now enforces the documented enums at our layer:
type ∈ {regional, viral}
interval ∈ {daily, weekly}
latest ∈ {true, false}
- This converts opaque upstream 400s (e.g. type=top → Chartmetric 400)
into specific 400s from us that name the valid values. Nothing is
silently ignored: params not in the spec (date, artist) were never
forwarded and now surface clearly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(research/albums): default isPrimary=true to exclude features
Per Chartmetric docs, /artist/:id/albums has an isPrimary query param
that defaults to false, meaning the response includes every album where
the artist is *featured* — not just their own discography. That's why
canonical ids were returning wrong data on the preview:
artist_id=3380 (Drake) → "nila: welcome to the aprtment (DJ Mix)" etc.
artist_id=3963 (Ariana Grande) → "Wicked: For Good" soundtrack etc.
Our handler now defaults to isPrimary=true so "albums" means the
artist's own discography by default. Callers who actually want features
and compilations can opt in with is_primary=false.
Also exposes limit and offset for pagination (Chartmetric defaults to
limit=100, offset=0). Sort params (sortColumn, sortOrderDesc) not
exposed — upstream defaults (release_date desc) are sensible; can
add later if a caller asks.
Validator rejects:
- is_primary values other than "true"/"false" (400)
- non-positive-integer limit (400)
- negative offset (400)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Recoupable <sidney@recoupable.com>
Co-authored-by: Sweets Sweetman <sweetmantech@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Why
Surfaced while testing recoupable/api#366 on preview. Callers passing string handles (`spotify`, `filtr`, etc.) got opaque upstream 400s. The docs previously described a looser contract than Chartmetric actually supports.
Follow-up
Test plan
🤖 Generated with Claude Code
Summary by cubic
Tightens the /research/curator OpenAPI docs to match Chartmetric’s contract and cut down on confusing 400s. Only Spotify, Apple Music, and Deezer are allowed, and the endpoint now documents a numeric curator ID requirement and a 404 for unknown curators.
platformenum tospotify,applemusic,deezer; clarify YouTube/Amazon are unsupported.idwith^[0-9]+$and example2./research/playlistsfor curator discovery.Written for commit 6dfcc51. Summary will update on new commits.
Summary by CodeRabbit