feat(api): suggest follows from a user's favorites and reposts - #1020
Merged
Conversation
Adds GET /v1/users/:userId/suggested-follows: artists whose tracks or
albums the user has favorited or reposted but whom they don't already
follow.
Distinct from /users/:userId/related, which is artist-anchored
("followers of X also follow Y"). This one is viewer-anchored, and needs
no graph traversal -- the candidates are already engaged with, so they
justify themselves.
Scoring sums engagement per artist, weighting a repost (1.5) above a
favorite (1.0) since a repost is a public endorsement, and decaying each
on a 180-day exponential so dormant taste doesn't outrank current taste.
Ties break on user_id to keep pagination stable.
Two details worth knowing:
- The engagement scan is capped at the 2000 most recent favorites and
reposts. Everything downstream joins per-row, so uncapped this would
scale with a heavy user's entire library.
- The decay clock is `now() AT TIME ZONE 'utc'`, not bare `now()`.
created_at is `timestamp without time zone`, so the plain form makes
the computed age depend on the server's timezone.
Cached separately from relatedUsersCache at a shorter 5min TTL: this
list is per-viewer and shrinks as the user acts on it, so a follow
should drop out of their suggestions promptly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dylanjeffers
added a commit
to AudiusProject/apps
that referenced
this pull request
Aug 18, 2026
Wires GET /users/:id/suggested-follows into web's empty feed and mobile's SuggestedFollows via a shared useFollowSuggestions hook: personalized when the user has favorites/reposts, existing static SUGGESTED_FOLLOW_HANDLES list when they don't. sdk.users.getSuggestedFollows is hand-written pending the next SDK regen, since npm run gen pulls the spec from a running node. Depends on AudiusProject/api#1020 (merged).
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.
Adds
GET /v1/users/:userId/suggested-follows— artists whose tracks or albums the user has favorited or reposted but whom they don't already follow.This is the viewer-anchored counterpart to
/users/:userId/related, which is artist-anchored ("followers of X also follow Y"). It needs no graph traversal: the candidates are already engaged with, so they justify themselves. "You saved three of their tracks and never followed them" is both the cheapest suggestion available and the most legible one.Today "suggested follows" on the clients is a static JSON list of handles from
SUGGESTED_FOLLOW_HANDLES. This is the first personalized source for that surface.Scoring
Engagement is summed per artist, weighting a repost (1.5) above a favorite (1.0) — a repost is a public endorsement, a favorite is private — and decaying each on a 180-day exponential so dormant taste doesn't outrank current taste. Ties break on
user_idso pagination is stable across pages.Excluded: self, already-followed, deactivated, unavailable, unlisted tracks, stems, and private playlists.
Two details worth a look in review
now() AT TIME ZONE 'utc', not barenow().created_atistimestamp without time zone, so the plain form makes the computed age depend on the server's timezone.Cached separately from
relatedUsersCacheat a shorter 5 min TTL: this list is per-viewer and shrinks as the user acts on it, so a follow should drop out of their suggestions promptly.Testing
Two tests, both against real Postgres. The first covers ranking, per-artist aggregation, album favorites, pagination, the empty-engagement case, and each exclusion rule. The second isolates recency decay — every fixture in the first shares a
created_atand would pass with the decay deleted, so it needs its own case. I verified it fails when the decay term is stubbed to1.0.Full
./api/...suite passes locally (withdbandelasticsearch-testup).Follow-ups, not in this PR
audius-protocol— merge and deploy this one first, orsdk.users.getSuggestedFollows404s there.🤖 Generated with Claude Code