Social Integration

Introduction

Buddo provides built-in social features that every operator app shares. Friends, presence, and chat are platform services — users maintain a single social graph, one online status, and one message history across every app on the platform.

As an operator, you don’t need to build your own friends list, presence system, or chat server. Instead, your app consumes the Buddo social API and the platform handles the rest. A user who adds a friend in your game can see that friend in every other Buddo app, and vice versa.

Platform-native, not per-app. Social features belong to the Buddo platform. Your app is a window into the user’s shared social world — it does not own or isolate any part of it.

Architecture

All social data lives at the platform level. There is one friend list per user, one presence record per user, and one set of chat channels that any app can access. The social API is authenticated with JWT bearer tokens (the same token returned by POST /api/auth/login), not OAuth tokens.

Key principles

Full endpoint reference: For complete request/response schemas and error codes, see the Social API Reference.

Friends System

The friends system supports searching for users, sending and responding to friend requests, listing friends, removing friendships, and blocking or unblocking users. All endpoints require a JWT bearer token.

Before sending a friend request, your app needs to find the target user. The search endpoint matches against usernames (minimum 2 characters) and returns up to 20 results. Blocked users and the current user are automatically excluded.

GET /api/social/users/search?q={query}
GET /api/social/users/search?q=alice
Authorization: Bearer <jwt>

// 200 OK
{
  "users": [
    { "id": "uuid", "username": "alice42" },
    { "id": "uuid", "username": "alice_wonder" }
  ]
}

Send a friend request

Once you have a user’s ID, send a friend request. The request stays in pending state until the recipient accepts or rejects it.

POST /api/social/friends
POST /api/social/friends
Authorization: Bearer <jwt>
Content-Type: application/json

{ "friend_id": "target-user-uuid" }

// 201 Created
{
  "friendship_id": "uuid",
  "status": "pending"
}
StatusMeaning
403Target user has blocked you
404User not found
409Already friends or request already pending

List pending requests

Retrieve incoming friend requests that the current user has not yet responded to.

GET /api/social/friends/pending
GET /api/social/friends/pending
Authorization: Bearer <jwt>

// 200 OK
[
  {
    "id": "sender-user-uuid",
    "friendship_id": "uuid",
    "username": "bob99",
    "sent_at": "2026-05-20T14:30:00Z"
  }
]

Accept or reject a request

Use the friendship_id from the pending list to accept or reject. The action field must be "accept" or "reject".

PUT /api/social/friends/{friendship_id}
PUT /api/social/friends/<friendship_id>
Authorization: Bearer <jwt>
Content-Type: application/json

{ "action": "accept" }

// 200 OK
{ "status": "accepted" }

List friends

Returns all accepted friends for the current user. Each entry includes the friend’s id, username, and email.

GET /api/social/friends
GET /api/social/friends
Authorization: Bearer <jwt>

// 200 OK
[
  { "id": "uuid", "username": "alice42", "email": "alice@example.com" },
  { "id": "uuid", "username": "bob99", "email": "bob@example.com" }
]

Remove a friendship

Either party can remove an accepted friendship. This is a mutual removal — neither user will see the other in their friend list afterward.

DELETE /api/social/friends/{friendship_id}
DELETE /api/social/friends/<friendship_id>
Authorization: Bearer <jwt>

// 200 OK
{ "status": "removed" }

Block and unblock

Blocking a user prevents them from sending friend requests, appearing in search results, and interacting via chat. Blocks are platform-wide and apply across all apps.

POST /api/social/block
POST /api/social/block
Authorization: Bearer <jwt>
Content-Type: application/json

{ "user_id": "target-user-uuid" }

// 200 OK
{ "status": "blocked" }

To unblock:

DELETE /api/social/block/{user_id}
DELETE /api/social/block/<user_id>
Authorization: Bearer <jwt>

// 200 OK
{ "status": "unblocked" }

Presence

The presence system tracks which users are online and what they are currently doing. Your app reports the user’s activity via a heartbeat, and can query who among the user’s friends is online. Presence data includes optional game and lobby fields, so friends can see not just that someone is online, but where they are.

Heartbeat (track presence)

Call this endpoint periodically (every 30–60 seconds is typical) to report the user as online. Include game and/or lobby to indicate what the user is doing.

POST /api/social/presence
POST /api/social/presence
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "game": "slots",
  "lobby": "high-roller-room"
}

// 200 OK
{ "status": "ok" }
Heartbeat cadence. If the platform does not receive a heartbeat within its timeout window, the user’s status automatically changes to offline. Keep heartbeats consistent while the user has your app open.

Get friends' presence

Returns a map of friend IDs to their presence status. Each entry includes status (online or offline), last_seen timestamp, and optional current_game and lobby fields.

GET /api/social/presence
GET /api/social/presence
Authorization: Bearer <jwt>

// 200 OK
{
  "user-uuid-1": {
    "status": "online",
    "last_seen": "2026-05-20T15:00:00Z",
    "current_game": "slots",
    "lobby": "high-roller-room"
  },
  "user-uuid-2": {
    "status": "offline",
    "last_seen": "2026-05-20T12:30:00Z",
    "current_game": null,
    "lobby": null
  }
}

You can filter to specific users by passing a comma-separated list of UUIDs:

GET /api/social/presence?user_ids=uuid-1,uuid-2,uuid-3

Table presence

For game tables or rooms, you can query who is present at a specific table. This is separate from the friends-based presence query — it returns all users at the table, not just friends.

GET /api/social/presence/table?game={game}&ref_id={ref_id}
GET /api/social/presence/table?game=bit-battle&ref_id=table-42
Authorization: Bearer <jwt>

// 200 OK
{
  "users": [
    { "user_id": "uuid", "status": "online", "last_seen": "2026-05-20T15:01:00Z" },
    { "user_id": "uuid", "status": "online", "last_seen": "2026-05-20T15:00:30Z" }
  ]
}

Untrack presence

When the user closes your app or logs out, call this to immediately remove their presence record rather than waiting for the heartbeat to time out.

DELETE /api/social/presence
DELETE /api/social/presence
Authorization: Bearer <jwt>

// 200 OK
{ "status": "ok" }

Chat

The chat system provides messaging within shared contexts — game lobbies and tables. Chat channels are identified by a type (lobby or table) and an optional ref_id that ties the channel to a specific game room or table instance. Messages are automatically filtered for profanity and length (max 1,000 characters).

Create or join a channel

Before sending messages, ensure the channel exists. This endpoint is idempotent — if a channel with the same type and ref_id already exists, it returns the existing channel rather than creating a duplicate.

POST /api/social/chat/channels
POST /api/social/chat/channels
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "type": "table",
  "ref_id": "bit-battle-table-42"
}

// 200 OK
{
  "id": "channel-uuid",
  "type": "table",
  "ref_id": "bit-battle-table-42"
}

Send a message

Post a message to a channel. The response includes the server-assigned ID, the filtered message body, sender details, and timestamp.

POST /api/social/chat/{channel_id}/messages
POST /api/social/chat/<channel_id>/messages
Authorization: Bearer <jwt>
Content-Type: application/json

{ "body": "Good luck everyone!" }

// 201 Created
{
  "id": "message-uuid",
  "sender_id": "user-uuid",
  "sender_username": "alice42",
  "filtered_body": "Good luck everyone!",
  "sent_at": "2026-05-20T15:05:00Z"
}
StatusMeaning
400Missing message body
422Message too long or caught by profanity filter

Message history

Retrieve past messages from a channel. Messages are returned in reverse chronological order with cursor-based pagination. Use the before parameter with an ISO 8601 timestamp to page backward through history.

GET /api/social/chat/{channel_id}/messages
GET /api/social/chat/<channel_id>/messages?limit=25
Authorization: Bearer <jwt>

// 200 OK
[
  {
    "id": "message-uuid",
    "sender_id": "user-uuid",
    "sender_username": "bob99",
    "filtered_body": "gg",
    "sent_at": "2026-05-20T15:04:50Z"
  },
  ...
]

To fetch the next page, pass the sent_at of the last message as the before parameter:

GET /api/social/chat/<channel_id>/messages?limit=25&before=2026-05-20T15:04:50Z

Integration Patterns

These patterns demonstrate how to combine the social endpoints into practical features within your operator app.

Social sidebar

Show the user’s friends and their online status alongside your app’s main content. This is the most common integration pattern.

  1. On app load, fetch the friend list with GET /api/social/friends.
  2. Immediately follow up with GET /api/social/presence to get each friend’s online status and current activity.
  3. Render friends sorted by status: online friends (with their current_game shown) at the top, offline friends below.
  4. Poll GET /api/social/presence every 30–60 seconds to keep the sidebar current.
  5. Check GET /api/social/friends/pending periodically to show a notification badge for incoming requests.
// Pseudocode: social sidebar initialization
const friends = await fetch('/api/social/friends', { headers: auth });
const presence = await fetch('/api/social/presence', { headers: auth });

friends.forEach(friend => {
  const status = presence[friend.id];
  renderFriendRow(friend.username, status?.status, status?.current_game);
});

Invite to game

Use presence data to see which friends are available, then invite them to join your game table. This pattern combines presence queries with your app’s own invitation logic.

  1. Fetch presence with GET /api/social/presence and filter for friends whose status is "online".
  2. Show an “Invite” button next to each online friend.
  3. When the user clicks invite, use your app’s own messaging or notification system to deliver the invite. The Buddo social API provides the social graph and presence — your app handles the invitation delivery.
  4. Optionally, use the friend’s current_game field to customize the invite message (e.g., “Leave Slots and join my Bit Battle table!”).
No built-in invite endpoint. Buddo provides the social graph and presence. Your app is responsible for its own invitation mechanism (in-app notifications, push, etc.).

Chat in-game

Embed a chat panel in your game UI using the platform’s chat channels. This works for both lobby-wide conversations and per-table chatter.

  1. When the user enters a game table, create or join the channel: POST /api/social/chat/channels with type: "table" and ref_id set to your table’s unique identifier.
  2. Load recent history with GET /api/social/chat/{channel_id}/messages.
  3. Render messages in a scrollable panel. Show sender_username and filtered_body.
  4. Provide a text input that posts to POST /api/social/chat/{channel_id}/messages.
  5. Poll for new messages periodically, or use GET ...messages?before=... for infinite scroll into history.
// Pseudocode: joining a table chat
const channel = await fetch('/api/social/chat/channels', {
  method: 'POST',
  headers: auth,
  body: JSON.stringify({ type: 'table', ref_id: 'my-game-table-7' })
});

const messages = await fetch(
  `/api/social/chat/${channel.id}/messages?limit=50`,
  { headers: auth }
);

renderChatPanel(messages);

OAuth Scopes

The social endpoints use JWT bearer authentication, not OAuth scopes. This means social features are available to any authenticated user without requiring specific OAuth scope grants.

However, if your operator app uses OAuth tokens to authenticate users (the standard operator pattern), the user will already have a JWT from logging in to authorize your app. The social API calls use that underlying JWT, not the OAuth access token.

Session tokens. When you exchange an authorization code via POST /api/oauth/token, the response includes a session_token alongside the OAuth access token. The session token can be refreshed via POST /api/session/refresh to maintain a heartbeat. See the OAuth PKCE guide for the full flow.

For your OAuth app registration, the following scopes are relevant to social-adjacent features:

ScopePurpose
profile:read Read user profile — needed to identify the user and display their username in social contexts
points:read Read point balance — useful if your social features involve gifting or wagering
points:transfer Transfer points between users — enables friend-to-friend gifting

Endpoint Summary

All social endpoints at a glance. Every endpoint requires a JWT bearer token via the Authorization header.

MethodPathDescription
GET /api/social/users/search?q={query} Search users by username
GET /api/social/friends List current user’s friends
POST /api/social/friends Send a friend request
GET /api/social/friends/pending List pending incoming requests
PUT /api/social/friends/{id} Accept or reject a friend request
DELETE /api/social/friends/{id} Remove a friendship
POST /api/social/block Block a user
DELETE /api/social/block/{user_id} Unblock a user
POST /api/social/presence Track presence (heartbeat)
GET /api/social/presence Get friends’ online presence
DELETE /api/social/presence Untrack presence
GET /api/social/presence/table Get presence for a game table
POST /api/social/chat/channels Create or join a chat channel
GET /api/social/chat/{channel_id}/messages Get messages from a channel
POST /api/social/chat/{channel_id}/messages Send a message to a channel