Skip to content

docs(research): POST /api/research/events — id-keyed artist live shows - #297

Merged
sweetmantech merged 11 commits into
mainfrom
docs/research-events
Aug 10, 2026
Merged

docs(research): POST /api/research/events — id-keyed artist live shows#297
sweetmantech merged 11 commits into
mainfrom
docs/research-events

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Contract for a new read-only endpoint returning a Recoup artist's live shows.

Tracking issue: recoupable/chat#1954
Merge order: this PR first, then recoupable/api#826, the implementation built against this contract (documentation-driven development).

Revised 2026-08-10. The first version of this spec took a bandsintown_id. It now takes the Recoup artist_id, and the events source is resolved server-side from the artist's connected profile. Callers shouldn't need a third-party id to ask Recoup about their own artist, and a provider id in the request body couples the published contract to that provider. Swapping the source later would be a breaking change.

Why id-keyed

Name-based show lookup can resolve to a different performer sharing the artist's name. That is not hypothetical: it put a wrong artist's concert into a customer-facing report on 2026-08-10. Resolving through the artist's connected profile makes that failure structurally impossible.

What's in the spec

  • POST /api/research/events, body { artist_id, date? }, date one of upcoming (default) / past / all.
  • Response { status, events[] } with date, venue, city, region, country, ticket_url, sold_out, lineup[].
  • 404 and 200 [] are documented as different answers, and the 404 description says so explicitly:
    • no events profile connected produces a 404 (we cannot answer)
    • connected but nothing scheduled produces a 200 with an empty events array
      Collapsing them would let a missing profile read as "no shows".
  • country carries a warning to pair it with city when filtering by market, since London and Paris are not unique city names.
  • lineup[] is documented as the queried artist first, so a shared bill is detectable.

Conventions followed

  • Reuses the shared ResearchErrorResponse (400/401/404) and ResearchInsufficientCreditsResponse (402) schemas.
  • Registered in docs.json nav under Metrics & insights, next to career and milestones.
  • No 501, /api/research/web documents one, but this endpoint has no unsupported-source path and the spec should describe only what the API returns.

Verification

Rendered locally with mint dev and inspected through Chrome DevTools MCP, see the verification comment below for screenshots. That caught a defect invisible in the raw JSON: the composed 200 example read "city": "London" with "region": "TN", because Mintlify builds examples from per-field values. Fixed in 302338d.

Spec parses; docs.json parses; nav entry resolves. The spec file does not survive a json.loadjson.dumps round-trip byte-for-byte (key ordering differs at line 2705), so all blocks were inserted via anchored text edits with each anchor asserted unique before substitution.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an Artist Events API for looking up upcoming, past, or all events.
    • Event results include dates, venues, locations, ticket links, sold-out status, and billed lineups.
    • Added clear responses for unavailable artist profiles, no matching events, validation errors, authentication issues, and insufficient credits.
  • Documentation

    • Added the Artist Events API reference page.
    • Added the page to the Research documentation navigation.

…s (chat#1954)

Contract-first spec for a new read-only endpoint that returns an artist's
live shows keyed on a stable Bandsintown artist id instead of a name string.

Name-based show lookup can resolve to a different performer who shares the
artist's name; an id lookup makes that impossible. The `country` field is
documented with an explicit warning to pair it with `city` when filtering by
market, since London and Paris are not unique city names.

- add `/api/research/events` path to the research OpenAPI spec
- add `ResearchEventsRequest` / `ResearchEventsResponse` schemas
- reuse the shared `ResearchErrorResponse` and
  `ResearchInsufficientCreditsResponse` schemas (400/401/402)
- add `api-reference/research/events.mdx` and register it in docs.json nav

No 501 is documented: unlike `/api/research/web` this endpoint has no
unsupported-data-source path, and the spec should describe only what the
API will actually return.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 10, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sweetmantech, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 22 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1e1623fe-9cd7-439d-b8e7-a4c78fcbd1c8

📥 Commits

Reviewing files that changed from the base of the PR and between 1089132 and f7f02b0.

📒 Files selected for processing (2)
  • api-reference/openapi/releases.json
  • api-reference/openapi/research.json
📝 Walkthrough

Walkthrough

The PR documents POST /api/research/events, including request and response schemas, date filters, event details, and error responses. It also adds the Artist Events reference page and Research navigation entry.

Changes

Research Events API

Layer / File(s) Summary
Research Events API contract
api-reference/openapi/research.json
Defines the endpoint, artist UUID request field, date filter, event response fields, and 400, 401, 402, and 404 responses.
Reference page and navigation
api-reference/research/events.mdx, docs.json
Adds the Artist Events OpenAPI reference page and places it in the Research documentation navigation.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

  • recoupable/chat#1954 — Covers the same POST /api/research/events endpoint and artist event request and response contract.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the documented POST /api/research/events endpoint and its purpose of returning artist live shows.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/research-events

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api-reference/openapi/research.json`:
- Around line 4644-4647: Update the bandsintown_id schema property in the
research API definition to retain its string type while adding a validation
pattern that accepts only one or more decimal digits, so nonnumeric IDs are
rejected.
- Around line 4661-4718: Update the ResearchEventsResponse schema to require the
events property, while preserving its existing array definition and empty-array
behavior when no events match. Add the required-property declaration at the
response object level.
🪄 Autofix

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 Plus

Run ID: 1f6e8fd0-9cf3-47fc-a3b1-469bf30a3078

📥 Commits

Reviewing files that changed from the base of the PR and between 38001ef and 01e993e.

📒 Files selected for processing (3)
  • api-reference/openapi/research.json
  • api-reference/research/events.mdx
  • docs.json

Comment thread api-reference/openapi/research.json Outdated
Comment on lines +4644 to +4647
"bandsintown_id": {
"type": "string",
"description": "Bandsintown numeric artist id — the digits in a Bandsintown artist URL (`bandsintown.com/a/{id}-{slug}`). Identifies the artist exactly, so results cannot drift to a same-named performer.",
"example": "1590132"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Constrain bandsintown_id to digits.

Line 4645 accepts any string, but Line 4646 defines a numeric Bandsintown artist ID. Generated clients can send invalid values that the endpoint must reject. Add a digits-only pattern.

Proposed fix
 "bandsintown_id": {
   "type": "string",
+  "pattern": "^[0-9]+$",
   "description": "Bandsintown numeric artist id — the digits in a Bandsintown artist URL (`bandsintown.com/a/{id}-{slug}`). Identifies the artist exactly, so results cannot drift to a same-named performer.",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"bandsintown_id": {
"type": "string",
"description": "Bandsintown numeric artist id — the digits in a Bandsintown artist URL (`bandsintown.com/a/{id}-{slug}`). Identifies the artist exactly, so results cannot drift to a same-named performer.",
"example": "1590132"
"bandsintown_id": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "Bandsintown numeric artist id — the digits in a Bandsintown artist URL (`bandsintown.com/a/{id}-{slug}`). Identifies the artist exactly, so results cannot drift to a same-named performer.",
"example": "1590132"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api-reference/openapi/research.json` around lines 4644 - 4647, Update the
bandsintown_id schema property in the research API definition to retain its
string type while adding a validation pattern that accepts only one or more
decimal digits, so nonnumeric IDs are rejected.

Comment thread api-reference/openapi/research.json

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 3 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread api-reference/openapi/research.json
Comment thread api-reference/openapi/research.json
Comment thread api-reference/openapi/research.json Outdated
The composed 200 example read `"city": "London"` with `"region": "TN"` and
`"country": "United Kingdom"` — Mintlify builds the example from per-field
examples, so a Nashville state code landed inside the London/Brixton event.
Caught by rendering the page locally rather than reading the spec.

Region is now exemplified as the empty string, which is what UK venues
actually return, and the description carries the "TN" case in prose instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sweetmantech

sweetmantech commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

Local preview verification (Chrome DevTools MCP)

Ran this branch locally with mint dev and drove the rendered page through Chrome DevTools MCP rather than reading the spec. That was worth doing, it caught a defect the JSON review missed.

🐛 Found and fixed while rendering: incoherent region example

Mintlify composes the 200 example from per-field example values. My original spec gave region a Nashville-flavoured "TN" while every other field exemplified the London/Brixton show, so the rendered example read:

"city": "London",
"region": "TN",
"country": "United Kingdom"

A reader copying that example would have gotten a venue in London, Tennessee, United Kingdom. Fixed in 302338d: region is now exemplified as the empty string (what UK venues actually return), with the "TN" case moved into prose. This is only visible once rendered, the raw JSON looks fine field-by-field.

Verified on the rendered page

Check Result
Page renders at /api-reference/research/events ✅ title Artist Events - Recoup
Nav placement POST Artist Events under Metrics & insights, between Milestones and Similar Artists
Method badge ✅ blue POST (siblings in group are GET)
Response code tabs ✅ exactly 200 · 400 · 401 · 402, no 501, matching the deliberate decision not to copy the sibling's unsupported-source code
bandsintown_id string, required, example "1590132"
date enum<string>, default: upcoming, options upcoming, past, all
Empty-result contract ✅ "An artist with no matching events returns an empty events array, not an error" renders on the Response block
All 8 child attributes date, venue, city, region, country, ticket_url, sold_out, lineup with descriptions
region after fix Example: "", description carries the US "TN" case in prose
Pagination ✅ ← Milestones / Similar Artists →
cURL sample ✅ targets https://api.recoupable.dev/api/research/events

Nav placement + response tabs

nav and example

Full page, child attributes expanded

full page


Screenshots live on the throwaway branch pr-297-verification-assets (commit 89bc25a) purely so they render here, not part of this PR and safe to delete after merge.

One caveat worth stating: Mintlify caches the OpenAPI spec, so the region fix did not hot-reload. The screenshots above are from a restarted dev server confirmed to be serving the patched spec.

Callers should not have to hold a Bandsintown id to ask Recoup a question
about their own artist, and publishing a provider id as a request parameter
couples the contract to that provider. The endpoint now takes the Recoup
`artist_id` and resolves the connected live-events profile server-side.

- `ResearchEventsRequest.bandsintown_id` -> `artist_id` (uuid)
- add 404 for "artist has no live-events profile connected", documented as
  explicitly distinct from a 200 with an empty `events` array (connected but
  not touring). Collapsing those two would let a missing profile read as
  "no shows", which is the same silent-wrong-answer failure this endpoint
  exists to prevent.
- reword the operation description to describe resolution via the connected
  profile rather than a provider id

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sweetmantech added a commit to recoupable/api that referenced this pull request Aug 10, 2026
…server-side

Rewrites the endpoint to the contract in recoupable/docs#297.

Callers should not have to hold a Bandsintown id to ask Recoup about their own
artist — and the sandboxed agent that consumes this could not obtain one
anyway, since resolving it needs a credential buildRecoupExecEnv deliberately
withholds. The request now takes `artist_id` and the provider id is resolved
from that artist's connected socials.

- add lib/research/getArtistBandsintownId.ts — parses the numeric id out of a
  connected bandsintown profile URL. Case-insensitive because profile_url is
  lowercased by a DB trigger, and tolerant of a missing www. and of trailing
  query strings.
- validator takes `artist_id` (uuid) and carries accountId + orgId through
- handler scopes the roster lookup via getArtists(accountId, orgId) before
  reading the artist's socials. Without this any authenticated account could
  read any artist's connected profile and events.
- 404 when the artist has no bandsintown profile, with the exact URL format
  and a link to the field that accepts it

Two negative cases stay deliberately distinct: no profile connected is a 404,
while connected-but-not-touring is a 200 with an empty array. Collapsing them
would let a missing profile read as "this artist has no shows" — the same
silent-wrong-answer failure this endpoint exists to prevent. Both are tested.

605 tests pass across the research, apify and artists suites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ample

The 404 reused ResearchErrorResponse, whose generic example reads
"Missing required parameter: artist" — a validation message. Rendered on the
404 tab that is actively misleading, since the whole point of this 404 is the
connection instructions the caller needs to act on.

Overrides the example at the response level so the schema stays shared (DRY)
while the 404 shows what the API actually returns. Caught by rendering the
page, not by reading the spec.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sweetmantech

sweetmantech commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

Local preview re-verification, artist_id contract

Re-ran mint dev against the revised spec and drove it through Chrome DevTools MCP. It caught a second defect, same class as the first: something invisible in the raw JSON but wrong on the rendered page.

🐛 Found and fixed: the 404 example was a validation message

The 404 reuses the shared ResearchErrorResponse schema, whose generic example is "Missing required parameter: artist". Rendered on the 404 tab, that reads as a 400, the opposite of what this 404 means, and it hides the connection instructions that are the entire point of the response.

Fixed in 9eb025e with a response-level example override, so the schema stays shared (DRY) while the 404 shows what the API actually returns:

{
  "status": "error",
  "error": "Error: no bandsintown ID connected to this artist. Please connect the bandsintown ID in this format: bandsintown.com/a/{id}-{slug} Docs here: https://docs.recoupable.dev/api-reference/artists/update#body-profile-urls"
}

Worth noting the general lesson for reused error schemas: a shared $ref gives you a shared example too, and a generic example on a specific status code can be actively misleading.

Verified on the rendered page

Check Result
Request body artist_id string<uuid> required, example 123694f2-1dab-40b4-8a75-84d39571c0bc
Provider removed from params ✅ no bandsintown_id anywhere in the request schema or cURL sample
cURL sample {"artist_id": "123694f2-…", "date": "upcoming"}
Response code tabs 200 · 400 · 401 · 402 · 404 (404 is new; still no 501)
404 example ✅ renders the real connection-instructions message
404 description ✅ states it is distinct from a 200 with an empty events array
Operation description ✅ "identified by their Recoup artist_id … Returns 404 when the artist has no live-events profile connected"
date enum<string>, default: upcoming, options upcoming, past, all
Anchor #body-artist-id (was #body-bandsintown-id)
region fix from the first round ✅ still Example: "", no regression
Nav placement POST Artist Events under Metrics & insights, between Milestones and Similar Artists

Separately verified that the URL in the 404 message resolves: the artist PATCH body param is profileUrls, and Mintlify kebab-cases it to body-profile-urls, which is present on the live docs page. The link and the instruction are both correct.

Request body, artist_id, no provider id

artist_id body

404 tab showing the real error body

404 example

Full page

full page


Screenshots live on the throwaway branch pr-297-verification-assets (commit 5558bc5) purely so they render here, not part of this PR, safe to delete after merge. The earlier round's screenshots were replaced since they showed the superseded bandsintown_id contract.

Same caveat as last time: Mintlify caches the OpenAPI spec, so the 404 example fix did not hot-reload. These screenshots are from a restarted dev server confirmed to serve the patched spec.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="api-reference/openapi/research.json">

<violation number="1" location="api-reference/openapi/research.json:2364">
P1: Custom agent: **Flag AI Slop and Fabricated Changes**

The 404 example hard-codes a bandsintown-specific error message into a contract that was deliberately redesigned to be provider-agnostic. The description says 'no live-events profile connected' (which is correct), but the example below it names bandsintown explicitly and prescribes its URL format—re-introducing the provider coupling the PR sought to remove. Because the backend hasn't been implemented yet, this example also documents behavior that may not match the actual 404 response. Replace the example with a provider-neutral message that matches the description, or remove the example if the schema shape is already self-explanatory.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

},
"example": {
"status": "error",
"error": "Error: no bandsintown ID connected to this artist. Please connect the bandsintown ID in this format: bandsintown.com/a/{id}-{slug} Docs here: https://docs.recoupable.dev/api-reference/artists/update#body-profile-urls"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Custom agent: Flag AI Slop and Fabricated Changes

The 404 example hard-codes a bandsintown-specific error message into a contract that was deliberately redesigned to be provider-agnostic. The description says 'no live-events profile connected' (which is correct), but the example below it names bandsintown explicitly and prescribes its URL format—re-introducing the provider coupling the PR sought to remove. Because the backend hasn't been implemented yet, this example also documents behavior that may not match the actual 404 response. Replace the example with a provider-neutral message that matches the description, or remove the example if the schema shape is already self-explanatory.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api-reference/openapi/research.json, line 2364:

<comment>The 404 example hard-codes a bandsintown-specific error message into a contract that was deliberately redesigned to be provider-agnostic. The description says 'no live-events profile connected' (which is correct), but the example below it names bandsintown explicitly and prescribes its URL format—re-introducing the provider coupling the PR sought to remove. Because the backend hasn't been implemented yet, this example also documents behavior that may not match the actual 404 response. Replace the example with a provider-neutral message that matches the description, or remove the example if the schema shape is already self-explanatory.</comment>

<file context>
@@ -2358,6 +2358,10 @@
+                },
+                "example": {
+                  "status": "error",
+                  "error": "Error: no bandsintown ID connected to this artist. Please connect the bandsintown ID in this format: bandsintown.com/a/{id}-{slug} Docs here: https://docs.recoupable.dev/api-reference/artists/update#body-profile-urls"
                 }
               }
</file context>

sweetmantech and others added 2 commits August 10, 2026 15:07
Reads less like generated copy. Only the sentences authored for this endpoint
are changed; the shared 401 and 402 boilerplate keeps its em dash so this
endpoint still matches the 24 sibling occurrences of the same string on main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
POST /api/research/events returns a 404 telling callers to connect a
bandsintown URL at this field, but BANDSINTOWN was not in the recognized-keys
list, so the instruction pointed at a page that did not mention it.

Adds it to the recognized keys and the example, and states the URL form the
events endpoint parses.

The API already accepts arbitrary keys (profileUrls is validated as
z.record(z.string(), z.string())), so this documents existing behaviour rather
than requesting new behaviour. See the PR discussion for a related defect in
updateArtistSocials that this does not fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread api-reference/openapi/research.json Outdated
},
"/api/research/events": {
"post": {
"description": "List a Recoup artist's live shows. Returns one row per event with venue, city, country, ticket link, and lineup. The artist is identified by their Recoup `artist_id` and resolved through the live-events profile connected to that artist, so the lookup cannot return a different performer who happens to share the artist's name. Use this instead of web search when you need shows for a specific artist. Returns 404 when the artist has no live-events profile connected.",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

KISS

Suggested change
"description": "List a Recoup artist's live shows. Returns one row per event with venue, city, country, ticket link, and lineup. The artist is identified by their Recoup `artist_id` and resolved through the live-events profile connected to that artist, so the lookup cannot return a different performer who happens to share the artist's name. Use this instead of web search when you need shows for a specific artist. Returns 404 when the artist has no live-events profile connected.",
"description": "List an artist's live shows. Returns one row per event with venue, city, country, ticket link, and lineup.",

Per review on #297. The field-level docs already carry the artist_id
resolution and 404 behaviour, so repeating them in the operation description
was redundant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sweetmantech

Copy link
Copy Markdown
Collaborator Author

Review applied + bot triage

Your comment: KISS on the operation description

Applied verbatim in efe0886:

"List an artist's live shows. Returns one row per event with venue, city, country, ticket link, and lineup."

Agreed. The artist_id field description already carries the resolution behaviour and the 404 condition, and the 404 response has its own description, so the operation description was repeating both.

Heads up: your review is still in pending state, so that comment is only visible to you until you submit it. I could read it through the API but nobody else can see it yet.

cubic P1: "404 example hard-codes a bandsintown-specific message into a provider-agnostic contract"

Valid observation, but intentional, so not fixing. This is a real tension and the bot spotted it correctly.

The request contract is deliberately provider-agnostic (artist_id, no provider id). The 404 message names Bandsintown because it has to be actionable: telling someone "no live-events profile connected" without saying what to connect or where leaves them stuck. That trade was raised and decided explicitly, with the wording specified by the repo owner.

If we swap providers later, the request contract survives unchanged, which was the point. The error string changes. That is a much cheaper migration than a breaking parameter change.

Stale findings: three comments target a property that no longer exists

CodeRabbit and cubic both flagged bandsintown_id (add a ^[0-9]+$ pattern, etc.) against lines 4644-4647. That property was removed in 9b9661f when the contract moved to artist_id. Those reviews ran against the earlier revision.

Worth noting the underlying point was already honoured in the replacement: artist_id is "format": "uuid" in the spec and validated with z.string().uuid() in recoupable/api#826, with tests asserting a 1590132-style value is rejected with a 400.

cubic P3: region example "TN" on a London venue

Already fixed in 302338d, before the bot ran. Found the same way it did, by noticing the composed example read London / TN / United Kingdom. region now exemplifies the empty string that UK venues actually return.

CodeRabbit: require events in ResearchEventsResponse

Reasonable, not taking it. required on a response object is weakly enforced by most generators and adds no safety here, while the empty-array-not-error behaviour is already stated in both the 200 description and the events description. Happy to add it if you'd rather the schema be strict.

Same paragraph the BANDSINTOWN key was added to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sweetmantech

sweetmantech commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

Local preview verification, round 3 (final state)

Restarted mint dev on 416220f and re-checked through Chrome DevTools MCP. This round covers the review feedback, the em dash pass, and the profileUrls addition. No new defects found.

Events page, shortened description

The KISS suggestion applied. The page now opens with what the endpoint does instead of a five sentence paragraph:

List an artist's live shows. Returns one row per event with venue, city, country, ticket link, and lineup.

Check Result
Operation description ✅ two sentences, matches the suggestion verbatim
artist_id string<uuid>, required, uuid example
Resolution + 404 behaviour still documented ✅ retained on the artist_id field and the 404 response, which is why removing it from the operation description loses nothing
Response tabs 200 · 400 · 401 · 402 · 404
region example ✅ still "", no regression
Em dashes in authored prose ✅ none

events page

Artists page, BANDSINTOWN added to profileUrls

Check Result
Anchor #body-profile-urls ✅ resolves and scrolls to the field, so the 404 message's deep link lands correctly
Recognized keys … THREADS, BANDSINTOWN
URL form documented bandsintown.com/a/{id}-{slug}, cross-referenced to POST /api/research/events
Example object ✅ includes "BANDSINTOWN": "https://www.bandsintown.com/a/1590132-loreen"
Em dash in that paragraph ✅ removed (case-sensitively, so lowercase keys…)

profileUrls field

profileUrls example

Round by round

Round Outcome
1 Found the region: "TN" example on a London venue
2 Found the 404 rendering the shared schema's "Missing required parameter" example
3 Clean, no new defects

Known gap, not fixed here

Documenting BANDSINTOWN does not make the write path idempotent. getSocialPlatformByLink has no bandsintown branch, so it returns "NONE" for those URLs, and updateArtistSocials matches an existing social by getSocialPlatformByLink(url) === type, which can never equal "BANDSINTOWN". First submit inserts; every re-submit inserts again rather than replacing, producing duplicate socials. This affects any platform outside that hardcoded list. Tracked for a separate api change, flagged here so it is not mistaken for something this PR fixed.


Screenshots live on the throwaway branch pr-297-verification-assets (commit 63256ea), not part of this PR, safe to delete after merge. Earlier rounds' images were replaced since they showed superseded contracts.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread api-reference/openapi/releases.json Outdated
Comment thread api-reference/openapi/releases.json Outdated
"profileUrls": {
"type": "object",
"description": "Map of uppercase platform identifier to social profile URL. Each entry replaces the existing social for that platform; platforms not included are preserved. Recognized keys: SPOTIFY, INSTAGRAM, TIKTOK, TWITTER, YOUTUBE, APPLE, FACEBOOK, THREADS. Keys are matched case-sensitively — lowercase keys will create duplicate socials instead of replacing the existing entry.",
"description": "Map of uppercase platform identifier to social profile URL. Each entry replaces the existing social for that platform; platforms not included are preserved. Recognized keys: SPOTIFY, INSTAGRAM, TIKTOK, TWITTER, YOUTUBE, APPLE, FACEBOOK, THREADS, BANDSINTOWN. A BANDSINTOWN URL takes the form `bandsintown.com/a/{id}-{slug}` and is what `POST /api/research/events` resolves an artist's live shows through. Keys are matched case-sensitively — lowercase keys will create duplicate socials instead of replacing the existing entry.",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

OCP / KISS / YAGNI

  • actual: new inline "BANDSINTOWN URL takes the form" section in the profile profileUrls description.
  • required: dedicated example to show all profile url examples outside of the profileUrls object.
Suggested change
"description": "Map of uppercase platform identifier to social profile URL. Each entry replaces the existing social for that platform; platforms not included are preserved. Recognized keys: SPOTIFY, INSTAGRAM, TIKTOK, TWITTER, YOUTUBE, APPLE, FACEBOOK, THREADS, BANDSINTOWN. A BANDSINTOWN URL takes the form `bandsintown.com/a/{id}-{slug}` and is what `POST /api/research/events` resolves an artist's live shows through. Keys are matched case-sensitively — lowercase keys will create duplicate socials instead of replacing the existing entry.",
"description": "Map of uppercase platform identifier to social profile URL. Each entry replaces the existing social for that platform; platforms not included are preserved. Recognized keys: SPOTIFY, INSTAGRAM, TIKTOK, TWITTER, YOUTUBE, APPLE, FACEBOOK, THREADS, BANDSINTOWN. Keys are matched case-sensitively — lowercase keys will create duplicate socials instead of replacing the existing entry.",

Comment thread api-reference/openapi/releases.json Outdated
sweetmantech and others added 3 commits August 10, 2026 15:30
… prose

Per review on #297 (OCP / KISS / YAGNI). The description no longer explains
the BANDSINTOWN URL shape inline; the example now demonstrates the shape for
every recognized key instead, so adding a platform later means adding one
example line rather than another sentence of prose.

All nine keys are the same artist with real URLs, verified rather than
invented: Spotify 1Xyo4u8uXC1ZmMpatF05PJ resolves to The Weeknd via the
Spotify API, bandsintown.com/a/1371750-the-weeknd and the Apple artist id
479756766 both resolve. A mixed-artist example is how the earlier
London/"TN" defect on this PR happened.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Per cubic review on #297. Reverses my earlier call to skip this.

I judged it in isolation and said required adds no safety. Checked against the
file instead: ResearchErrorResponse and ResearchInsufficientCreditsResponse,
both referenced by this endpoint, declare required, and 27 of 50 *Response
schemas here do.

It also reinforces the contract this endpoint cares about most. `events` is
always present and is empty when nothing matches, rather than being omitted,
so declaring it required is the accurate description, not just a convention.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sweetmantech

Copy link
Copy Markdown
Collaborator Author

Correction to my earlier triage

Earlier I declined the "make events required" finding, saying it was weakly enforced and added no safety. That was wrong, and I have applied it in f7f02b0.

I judged it in isolation instead of against the file. Checking properly:

  • ResearchErrorResponse declares required: ["status", "error"]
  • ResearchInsufficientCreditsResponse declares required: ["error", "remaining_credits", "required_credits", "checkoutUrl"]
  • 27 of 50 *Response schemas in this file declare required

Both schemas this endpoint references use it, so omitting it made this endpoint the outlier.

There is also a substantive argument I missed. This endpoint's central contract is that events is always present, and empty when nothing matches, rather than omitted. Declaring it required is the accurate description of that behaviour, not merely a house convention. It is the schema-level statement of the same thing the 404-vs-200 distinction expresses at the status-code level.

ResearchWebResponse, the sibling I modelled this endpoint on, does not declare required, which is probably why I did not. That looks like a gap in that schema rather than a pattern worth copying.

@sweetmantech
sweetmantech merged commit b10eecf into main Aug 10, 2026
3 checks passed
sweetmantech added a commit to recoupable/api that referenced this pull request Aug 10, 2026
…st_id (#826)

* feat(research): POST /api/research/events — id-keyed artist live shows (chat#1954)

Implements the contract specified in recoupable/docs#297.

Looking shows up by artist *name* can resolve to a different, more
search-prominent performer who shares that name. This endpoint takes a
numeric Bandsintown artist id instead, so the result set cannot drift to the
wrong artist. The validator constrains `bandsintown_id` to digits for the
same reason: accepting free text would quietly reintroduce the ambiguity the
endpoint exists to remove.

- lib/apify/bandsintown/fetchBandsintownEvents.ts — actor call + normalization
- lib/research/validatePostResearchEventsRequest.ts — auth, body, credit gate
- lib/research/ensureEventsResearchCredits.ts — 1 credit, priced like web search
- lib/research/postResearchEventsHandler.ts — handler
- app/api/research/events/route.ts — thin route, maxDuration 60

Calls Apify synchronously via `.call()`, following fetchSpotifyAlbumPlayCounts
rather than the `.start()` + webhook scrapers: this is a read-only fetch whose
caller needs the data in its response and which persists nothing. Measured p95
5.7s against the 60s route budget (N=12, concurrency 6).

An artist with no events returns 200 with an empty array, not a 404 — "not
touring" is a valid answer to the question asked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(research): key events on Recoup artist_id, resolve provider server-side

Rewrites the endpoint to the contract in recoupable/docs#297.

Callers should not have to hold a Bandsintown id to ask Recoup about their own
artist — and the sandboxed agent that consumes this could not obtain one
anyway, since resolving it needs a credential buildRecoupExecEnv deliberately
withholds. The request now takes `artist_id` and the provider id is resolved
from that artist's connected socials.

- add lib/research/getArtistBandsintownId.ts — parses the numeric id out of a
  connected bandsintown profile URL. Case-insensitive because profile_url is
  lowercased by a DB trigger, and tolerant of a missing www. and of trailing
  query strings.
- validator takes `artist_id` (uuid) and carries accountId + orgId through
- handler scopes the roster lookup via getArtists(accountId, orgId) before
  reading the artist's socials. Without this any authenticated account could
  read any artist's connected profile and events.
- 404 when the artist has no bandsintown profile, with the exact URL format
  and a link to the field that accepts it

Two negative cases stay deliberately distinct: no profile connected is a 404,
while connected-but-not-touring is a 200 with an empty array. Collapsing them
would let a missing profile read as "this artist has no shows" — the same
silent-wrong-answer failure this endpoint exists to prevent. Both are tested.

605 tests pass across the research, apify and artists suites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(research): name the field when artist_id is missing

Found by testing the preview deployment rather than the unit suite. Omitting
artist_id returned Zod's default "Invalid input: expected string, received
undefined", which never tells the caller which field is missing. Supplying a
non-uuid returned the helpful message, so the gap only showed on the
omitted-field path.

Adds the type-level error message and a test asserting the response names the
field.

Note the same gap exists on validatePostResearchWebRequest (`query`), which is
untouched here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (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