All developer documentation

Friends

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

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.

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.

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 covers it.

Testing a friends screen#

The SDK ships no seeded mock (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.

Read this page as Markdown