Skip to content

docs: tighten /research/curator spec to match Chartmetric contract#137

Merged
sweetmantech merged 1 commit into
mainfrom
docs/curator-enums
Apr 15, 2026
Merged

docs: tighten /research/curator spec to match Chartmetric contract#137
sweetmantech merged 1 commit into
mainfrom
docs/curator-enums

Conversation

@sweetmantech
Copy link
Copy Markdown
Collaborator

@sweetmantech sweetmantech commented Apr 15, 2026

Summary

  • Restrict `platform` enum to `spotify | applemusic | deezer` (drops `amazon` and `youtube` — Chartmetric doesn't support curator metadata on those)
  • Document `id` as a numeric Chartmetric curator ID with a `^[0-9]+$` pattern and an example (`2` — Spotify's own account)
  • Add 404 response for unknown curator id
  • Description now points devs at `/research/playlists` for curator discovery

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

  • JSON spec validates
  • `npx mintlify@latest dev` — confirm the updated param table renders on /api-reference/research/curator

🤖 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.

  • Bug Fixes
    • Restrict platform enum to spotify, applemusic, deezer; clarify YouTube/Amazon are unsupported.
    • Require numeric id with ^[0-9]+$ and example 2.
    • Clearer 400 for invalid/missing params and new 404 when curator not found.
    • Description links to /research/playlists for curator discovery.

Written for commit 6dfcc51. Summary will update on new commits.

Summary by CodeRabbit

  • API Updates
    • Curator endpoint now validates numeric curator IDs using stricter validation rules
    • Removed Amazon and YouTube from supported platforms; endpoint now supports Spotify, Apple Music, and Deezer only
    • Added explicit 404 response when requested curator is not found

- 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>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

📝 Walkthrough

Walkthrough

Updates the OpenAPI specification for the /api/research/curator endpoint, restricting supported platforms to spotify, applemusic, and deezer, adding numeric validation to the curator ID parameter, and introducing a 404 response for missing curators.

Changes

Cohort / File(s) Summary
Research Curator Endpoint Specification
api-reference/openapi/research.json
Updated GET /api/research/curator endpoint documentation: narrowed platform enum by removing amazon and youtube, strengthened id parameter with numeric pattern validation, and added explicit 404 response for curator not found.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Poem

🐰 A curator's profile now gleams so bright,
With spotify, apple, and deezer in sight,
Numbers are validated with patterns so tight,
Four-oh-four errors make missing curators right! 🎵

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: tightening the /research/curator OpenAPI specification to align with Chartmetric's actual contract by restricting platform values and adding validation constraints.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/curator-enums

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between eafc09d and 6dfcc51.

📒 Files selected for processing (1)
  • api-reference/openapi/research.json

Comment on lines +1249 to +1255
"404": {
"description": "Curator not found for the given platform/id.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ResearchErrorResponse"
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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).

@sweetmantech sweetmantech merged commit 5d7d83c into main Apr 15, 2026
3 checks passed
sweetmantech added a commit to recoupable/api that referenced this pull request Apr 15, 2026
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>
sweetmantech added a commit to recoupable/api that referenced this pull request Apr 16, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant