Build a social app
06 · Home feed

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.

bash
curl -H "Authorization: Bearer pcft_live_..." \
  "https://social.productcraft.co/v1/communities/<c>/actors/<actor-uuid>/feed?order=chronological&limit=20"
response.json
{
  "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.

bash
# 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.

bash
# 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.

bash
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) while has_more is still true — keep following next_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.