# Feed API — Mobile Integration Guide

The personalized home feed: a scored, paginated blend of posts from people you
follow, interest matches, and trending content. This doc is for the mobile
(React Native) client. Feed internals: [feed-algorithm.md](https://github.com/Axiant-Labs/adolescent_social/blob/main/docs/feed-algorithm.md).

## Endpoint

```
GET /api/feed?cursor=<opaque>&limit=<1-50>
Authorization: Bearer <accessToken>
X-Client-Type: mobile
```

| Query | Default | Notes |
| ----- | ------- | ----- |
| `limit` | `20` | Page size. Hard-capped at `50`. |
| `cursor` | — | Opaque continuation token from the previous response. Omit for page 1. |

### Response `200`

```json
{
  "success": true,
  "data": {
    "items": [ /* serialized posts, ranked */ ],
    "nextCursor": "eyJ...==",
    "mode": "warm"
  }
}
```

- `nextCursor` — pass back as `?cursor=` to load the next page. `null` = end of feed.
- `mode` — which strategy produced this feed. Informational (analytics / debug),
  safe to ignore for rendering:
  - `cold` — new user (interest/trending only).
  - `warm` — established user (follow + interest + trending blend).
  - `fallback:following` | `fallback:any-recent` | `fallback:own` | `fallback:none`
    — sparse graph; the normal pools were empty and the feed degraded gracefully.

### Item shape

Every item is a **canonical serialized post** — identical shape to
`GET /api/posts/*`, so reuse your post card component as-is. See
[Post Data Structure](../post/mobile-post-data-structure-guide.md) for the full
field list (`author`, `media`, `topics`, `isLiked`, `isSaved`, `isReposted`,
`interestStatus`, `likeCount`, `commentCount`, CTA, repost `original`, …).

Feed items add two ranking fields:

```ts
score: number;                            // frozen rank score (debug/analytics)
pool: "follow" | "interest" | "trending"; // which pool sourced this item
```

## Pagination — frozen snapshots

The ranked feed is computed **once per page-1 request**, frozen server-side, and
paged by slicing that frozen list. This matters for the client:

- **A no-cursor `GET /api/feed` rebuilds** — fresh ranking, fresh content,
  freshly applied exclusions and seen penalties. This is your **pull-to-refresh**.
- **Cursor pages are stable** — no re-ranking between pages, so **no duplicates
  and no skips** mid-scroll. Follow `nextCursor` until it's `null` for infinite scroll.
- Snapshots live ~5 minutes. An old/expired cursor doesn't error — the server
  rebuilds from the top. Rare, but don't treat a cursor as permanent.

## Consumption model — what leaves the feed, and when

The feed separates **learning** (what you'll see next) from **selection** (what's
a candidate now). Interacting with a post never re-ranks the post you just acted on.

### Liked & commented posts are removed (next build)

A post you **like** or **comment on** is *consumed* — it is excluded from **every
future feed build** for you, in every mode. It still lives on the author's
profile, your liked list, search, notifications, and its direct link — just not
the home feed. This kills the old "I liked it, refreshed, and it came back" bug.

Timing follows the snapshot rules above:
- Within the **current** frozen snapshot it will **not** resurface (dedup +
  freeze), so mid-scroll is already safe — you don't need to do anything.
- On the **next no-cursor refresh** it's gone from the list.

### Seen posts are down-ranked automatically

Posts already served to you are recorded **server-side** and gently demoted on
the next build (graded — seen once is a small nudge, seen repeatedly sinks
lower), so refreshing rotates content instead of showing the same top posts.
**Nothing for the client to send** — seen tracking is automatic today.

> **Future:** viewport-accurate impression tracking (`POST /feed/impression`
> with the ids ~70% visible) is planned but **not built**. Do not call it yet.
> Until then the server counts a post as seen when it's returned in a page.

## Recommended client UX

- **Pull-to-refresh** → `GET /api/feed` with **no cursor**. Fresh snapshot with
  liked/commented removed and seen posts rotated down.
- **Infinite scroll** → append items, follow `nextCursor`, stop at `null`.
- **On like / comment** → update the card's `isLiked` / counts optimistically and
  **leave the card in place** for the current session (the frozen snapshot won't
  duplicate or bounce it). If your design prefers it gone immediately, optimistically
  remove it from the local list — safe, because the snapshot can't resurface it.
  No page-1 refetch is required just to hide a liked post.
- **On `not_interested`** → see [Post Interest](../post/mobile-post-interested-notInterested.md)
  (harder, permanent exclusion + immediate topic-weight shift).
- Don't rebuild the feed on every interaction — refresh on explicit pull-to-refresh,
  screen re-focus, or when the list runs out.

## Examples

```bash
TOKEN=... # from POST /api/auth/login (alice@example.com / Password123)

# Page 1
curl -s localhost:3001/api/feed -H "Authorization: Bearer $TOKEN" | jq '.data.mode, (.data.items | length), .data.nextCursor'

# Next page
curl -s "localhost:3001/api/feed?cursor=<NEXT_CURSOR>" -H "Authorization: Bearer $TOKEN" | jq '.data.items[].id'

# Verify a liked post is gone after refresh — index must be null
curl -X PUT localhost:3001/api/posts/<POST_ID>/like -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -d '{"liked":true}'
curl -s localhost:3001/api/feed -H "Authorization: Bearer $TOKEN" \
  | jq '[.data.items[].id] | index("<POST_ID>")'
```

Swagger UI: `http://localhost:3001/api/docs` (Feed → `GET /api/feed`).
