Add GET /v1/users/{id}/feed/for-you endpoint#787
Merged
dylanjeffers merged 3 commits intomainfrom May 8, 2026
Merged
Conversation
…lgorithm Implements a personalized For You feed modeled on Twitter's 2023 open-sourced timeline pipeline (candidate generation -> ranking -> filtering -> diversity). Candidate sources (4): - in-network: recent uploads from artists the viewer follows - weekly trending (track_trending_scores, time_range=week) - underground trending (sub-1500 follower/following artists) - similar-artist 1-hop CF: artists co-saved by users who saved my saved artists' tracks Ranking (SQL-side): - 48h half-life recency: EXP(-LN(2) * hours_old / 48) - engagement: LN(1 + 3*saves + 2*reposts + plays) (saves > reposts > plays) - social affinity: 1 + min(LN(1 + my_engagement_count) / 4, 1) - source weight: in-network 1.20, trending 1.00, underground 0.95, similar 0.90 Filtering / diversity: - hard filters mirror the v1_events_remix_contests.go pattern: is_delete=false, is_unlisted=false, is_available=true, stem_of IS NULL, no access_authorities, owner not deactivated - excludes tracks the viewer has already saved - 3-per-artist cap via ROW_NUMBER() OVER (PARTITION BY owner_id) - Go-side greedy diversity pass with a 5-track lookahead to avoid consecutive same-artist tracks without disturbing global rank Pagination: user_id (required), limit (1-100, default 25), offset (0-200). Consumed by apps#14237. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the OpenAPI entry for the new endpoint so it shows up in /v1 (swagger UI) and the SDK codegen pipeline. Documents the four query params (user_id required; limit, offset, max_per_artist optional with min/max bounds matching the handler's validate tags) and points the 200 response at the existing "tracks" component schema. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The path-param convention matches every other personalized track
endpoint in the API (/v1/users/{id}/feed, /v1/users/{id}/recommended-tracks,
etc.), is what apps#14237's description calls out as the consumer-facing
path, and lets requireUserIdMiddleware do the hash-id validation that
the handler had been duplicating.
What moved:
- Route: g.Get("/feed/for-you", ...) → g.Get("/users/:userId/feed/for-you", ...)
registered under the same requireUserIdMiddleware group as the rest of
/users/:userId/...
- File: v1_feed_for_you.go → v1_users_feed_for_you.go
- Func: v1FeedForYou → v1UsersFeedForYou
- Param struct: GetFeedForYouParams → GetUsersFeedForYouParams
- Swagger entry moved next to /users/{id}/feed and reshaped to take
`id` as a path parameter; query `user_id` is now optional and used
only for the caller's viewer-relative track fields, mirroring the
rest of the user-scoped endpoints.
What stayed (intentionally — this is what the PR is for):
- All four candidate sources: in_network, trending, underground,
similar (1-hop CF on saves graph)
- Cross-source ranking: 48h half-life × engagement × social_boost ×
source_weight, all the same coefficients
- Per-artist row_number cap + Go-side greedy 5-track lookahead
- Filter set: live tracks, owner liveness, access_authorities,
exclude already-saved, exclude path-user's own uploads
- Pool size, max_per_artist param, default limit of 25
Handler signature change: the path id is now the user being
personalized for (drives the SQL @userid), and the optional
?user_id= caller is myId for track shape — the same split every
other /v1/users/{id}/... endpoint uses.
Tests: same fixtures and assertions, URLs rewritten to
/v1/users/{id}/feed/for-you. The "requires user id" test now
asserts an invalid hash id returns 400 (from the middleware)
rather than a missing query param.
Supersedes #797.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
GET /v1/feed/for-you, a personalized track feed modeled on Twitter's open-sourced 2023 algorithm (the-algorithm/the-algorithm-ml). The pipeline is candidate-retrieval → ranking → filtering+diversity, the same three-stage shape Twitter uses on top of a learned heavy ranker. Audius doesn't yet have a trained ranker, so the heavy ranker is approximated by a hand-tuned linear blend; the candidate retrieval and diversity passes carry over directly so a learned model can drop in later.Client consumer: AudiusProject/apps#14237.
Endpoint
user_idis required (the handler 400s without it — "For You" without a "you" degenerates into trending+underground).limitdefaults to 25 (max 100),offsetto 0 (max 200),max_per_artistto 3 (max 10).Algorithm
1. Candidate retrieval (UNION across 4 capped sources)
in_networktrendingtrack_trending_scores(mirrors/tracks/trending)undergroundsimilarDISTINCT ON (track_id) ORDER BY track_id, priokeeps the strongest source for each track, so an in-network track that's also trending keeps the in-network weight.2. Ranking
3. Filters (applied once after the union)
is_current,is_delete=false,is_unlisted=false,is_available=true,stem_of IS NULLis_current,is_deactivated=false,is_available=true(same shape asv1_events_remix_contests.go)access_authorities(matches thev1_users_feedauthed-wallet pattern)4. Diversity
ROW_NUMBER() OVER (PARTITION BY owner_id ORDER BY score DESC, track_id DESC)filtered to<= max_per_artist(default 3) — prevents a single hot artist from filling the page.Pagination is
offset/limitapplied on the diversity-ordered list, so pages are stable as long as underlying scores haven't shifted.Test plan
TestV1FeedForYou_Basic— in-network + trending + underground all surface; deleted/unlisted/deactivated/own/saved tracks are excludedTestV1FeedForYou_RequiresUserId— 400 withoutuser_idTestV1FeedForYou_ExcludesAlreadySavedTracks— already-saved exclusion worksTestV1FeedForYou_MaxThreePerArtist— 3-per-artist cap enforcedTestV1FeedForYou_DiversityPassNoConsecutiveSameArtist— Go greedy pass interleaves artistsTestV1FeedForYou_PaginationDoesNotRepeat— pagination doesn't repeat ids across pagesTestV1FeedForYou_InvalidParams— limit/offset out-of-range → 400TestV1FeedForYou_RecencyAndEngagementRanking— fresh+engaged outranks low-engagement and old (joint signal test)/v1/feed/for-you?user_id=…for a real account, eyeball the mix of in-network vs trending vs underground🤖 Generated with Claude Code