# Friends

Your game can read **who the signed-in player's accepted friends are** — display identity only —
with the token your frame already holds:

```js
const page = await gem.friends.list();
for (const friend of page.friends) {
  render(friend.displayTag ?? 'a friend', friend.avatarIconUrl);
}
```

That one read is the whole surface, and the boundary around it is the thing to understand before
you design against it.

## What the read is, and is not

`gem.friends.list()` returns the player's **accepted** friends: for each one, a `playerId` and —
when the friend has set them — `displayName`, `discriminator`, `displayTag` and an avatar icon
(`avatarIconId` plus a ready-to-render `avatarIconUrl`). Every field beyond `playerId` is
optional, and absent means absent: render what is there and fall back per field, because a friend
with no display name is still a friend.

Everything else about the friend graph stays on the Arcade, by design:

- **No mutations.** There is no add, accept, remove or block here. The graph is managed on
  platform screens with the player's own account credential — a game frame holds a play token,
  and a play token cannot touch the graph.
- **No states.** Pending requests, blocks and relationship history do not appear. An accepted
  friend appears; everyone else does not exist on this surface.
- **No presence, no invites.** Nothing here says who is online or delivers an invite — an invite
  is something happening *in* the graph, and it is rendered where the graph lives. See the
  deliberate-absence notes in [`rooms-and-matches.md`](rooms-and-matches.md).

Two properties worth designing around rather than discovering:

- **Every accepted friend appears, whether or not they have played your game.** That is the
  point, not an oversight — an invite-a-friend flow needs exactly the friends who have *not*
  played it. If you only want friends with progress in your game, join the list against your own
  data (a friend's `playerId` is the same id leaderboard rows and transfers carry).
- **It is a player surface only.** On a dedicated server there is no `gem.friends`, and the route
  refuses a game-server token — a server has no friend list. A server that needs a friend fact is
  handed it by a client, and must treat it as a claim, not a fact.

## Pagination

The list is paginated behind an opaque cursor: a page carries `nextCursor` exactly when another
page exists, and you pass it back verbatim.

```js
let page = await gem.friends.list();
render(page.friends);
while (page.nextCursor) {
  page = await gem.friends.list({ cursor: page.nextCursor });
  render(page.friends);
}
```

Pages default to 50 friends and cap at 200; a player holds at most 1000 friends, so the whole
list is never more than five max-size pages. Ordering is newest friendship first. Mint nothing:
a cursor the server did not hand you is refused with `invalid`, not treated as page one.

## Friends and leaderboards

If what you want is a **friend standings screen**, do not build it from this list. Every declared
board answers a friend view — the same board, re-ranked server-side over the viewer and their
accepted friends — and that view is the right tool, because ranks are positions within the
subset and the global board's size cap would make client-side filtering silently lossy.
[`leaderboards.md`](leaderboards.md) covers it.

## Testing a friends screen

The SDK ships no seeded mock ([`what-a-game-must-do.md`](what-a-game-must-do.md) says why), so if
you stub this surface in your own tests, stub the states the type does not force on you: a friend
with no display fields at all, the friendless empty state — every friends screen must survive a
player who knows nobody — and a list long enough to page, because the cursor loop is code that
otherwise first runs in production against the one player with 300 friends.
