Home, discover, and list feeds.
`GET /actors/:actorId/feed` serves the home tab — chronological for a strict timeline, ranked for an algorithmic one. `GET /discover-feed` powers an explore tab over every public post. Same response shape, same cursor pagination, same block + visibility filtering.
1
Chronological feed
Time-ordered. Returns posts authored by the actor or by anyone they follow (plus posts using followed hashtags), excluding any actor involved in a block with them and any actor they've muted. private-visibility posts are surfaced only when the requester is the author.
curl -H "Authorization: Bearer pcft_live_..." \
"https://social.productcraft.co/v1/communities/<c>/actors/<actor-uuid>/feed?order=chronological&limit=20"{
"data": [
{ "id": "...", "kind": "image", "actor_id": "<carol>", "title": "Mercado Benito Juárez at golden hour", ... },
{ "id": "...", "kind": "link", "actor_id": "<bob>", "title": "Database schema migrations without downtime", ... },
{ "id": "...", "kind": "text", "actor_id": "<alice>", "body": "Just shipped the Glow style guide v0.4 ...", ... }
],
"pagination": { "next_cursor": null, "has_more": false }
}2
Ranked feed (default)
order=ranked blends four signals into a single SQL-side score:
- Recency — exponential decay with a configurable half-life.
- Engagement — log-scaled count of comments + reactions.
- Follow-graph bias — a flat additive boost for in-graph posts. On the home feed every candidate is already from your follow graph, so it cancels out; on the discover feed (below) it's zeroed, which is what pushes stranger content down when you blend the two surfaces client-side.
- Self-author boost — keeps the actor's own posts visible.
Each signal's weight is read from community.settings.ranking at query time. Defaults work out of the box.
# Ranked is the default; you can omit ?order=ranked
curl -H "Authorization: Bearer pcft_live_..." \
"https://social.productcraft.co/v1/communities/<c>/actors/<actor-uuid>/feed?limit=20"3
Tuning ranking
Per-community weights live on community.settings.ranking — per-signal weights under weights, plus half_life_hours and candidate_window_days beside it. Lower the recency weight if you want more evergreen content; bump engagement to push high-comment threads up; adjust the half-life for how fast posts cool off. Every key is optional and falls back independently — see the community settings reference for defaults and bounds. Note that PATCH replaces the whole settings blob, so read-modify-write.
# Update the ranking weights from the admin lane (PAK bearer,
# PlatformUser bearer, or session cookie all work).
curl -X PATCH -H "content-type: application/json" \
-H "Authorization: Bearer pcft_live_..." \
-d '{
"settings": {
"ranking": {
"weights": {
"recency": 0.6,
"engagement": 0.3,
"follow_bias": 0.05,
"self_boost": 0.05
},
"half_life_hours": 12,
"candidate_window_days": 7
}
}
}' \
https://social.productcraft.co/v1/workspaces/<ws>/communities/<c>4
Discover feed (explore tab)
GET /communities/:c/discover-feed is the explore surface: the same engagement-blended scoring, run over every public post in the community with no follow-graph constraint — Instagram Explore, or X's "For You" without the follow pool. viewer_actor_id is optional: pass it to filter the viewer's blocks, apply their muted terms, and give their own posts a small boost; omit it for the anonymous global slice (logged-out landing pages work). Visibility is locked to public — followers-only and close-friends posts never leak here.
curl -H "Authorization: Bearer pcft_live_..." \
"https://social.productcraft.co/v1/communities/<c>/discover-feed?viewer_actor_id=<actor-uuid>&limit=20"There's a third feed shape too: list feeds (GET /lists/:listId/feed) — posts authored by the members of a curated actor list, chronological by default with order=ranked available. Same envelope, same cursor rules.
5
What gets filtered out
Beyond visibility and blocks, two per-viewer filters shape every feed page:
- Mutes — authors the viewer has muted (stage 5) are excluded from the home feed in both orders.
- Muted terms — posts whose body/title contain a muted keyword are dropped from home + discover pages after the page is fetched. That means a page can return fewer rows than
limit(even zero) whilehas_moreis stilltrue— keep followingnext_cursor; don't treat a short page as the end. Details in the guide.
6
Cursors
Cursors are not interchangeable between modes — passing a chronological cursor with order=ranked returns 400 INVALID_CURSOR. A malformed cursor is silently treated as page-1 (no error, just a restart). Always re-paginate from has_more=true + next_cursor within a single mode.
7
Empty state
When the actor follows no-one and has no posts, both modes return { "data": [], "pagination": { "next_cursor": null, "has_more": false } } with a 200. The endpoint is the right one to wire up your empty-feed CTA against — it doesn't 404 on a no-content actor.