Chat for web3 games: a room per match with the API

Add chat for web3 games with the Cherry API: a room per match with initialMembers, the roomId in the embed, and mutes and bans from your game server.

This guide adds chat for web3 games the way games need it: a separate room for every match, lobby, or clan, created from your game backend with the Cherry API and opened in the client with the chat embed. Cherry is a wallet-to-wallet messenger and community app for crypto, so a player’s Solana wallet is the account, with no phone number, email, or KYC step in front of the lobby. It is for the engineer whose server already knows the match id and the players’ wallets.

What chat for web3 games needs from an API

Four things, all of them server-side decisions:

  • A room keyed to a unit of play, created when the match is made and closed when it ends.
  • An identity the player already carries, so nobody registers a username to trash talk.
  • A voice for your game server in the room, for results, rewards, and rule warnings.
  • Moderation your server can run, so a griefer is muted without a human in the room.

One project key covers all four, on the surface the Cherry API and bots page documents in full.

Before you start

  • A project at portal.cherry.fun , created by signing in with a wallet, and a project key from its API keys section: one opaque token shaped cherry_sk_<projectId>_<secret>, shown once. Copy it whole; the docs warn against assembling it from an id and a secret.
  • Scopes on that key: groups:create, groups:manage, members:invite, members:moderate, messages:send.
  • A chat embed in the same project, which gives you the embed appId, with your game’s origin in its allowed origins.
  • Each player’s Solana wallet address on your server.

There is no Unity or Unreal package. As of 11 September 2026 Cherry ships a REST API for servers and a browser SDK, @cherrydotfun/chat-embed-sdk (0.1.7 as of 10 September 2026), for the UI. A WebGL build puts the chat on the host page beside the canvas; a native or mobile build puts it in a WebView on a small host page.

Confirm the key:

curl https://api.cherry.fun/api/v1/apps/me \
  -H "Authorization: Bearer ${CHERRY_API_KEY}"

You know it worked when the response lists your scopes and a rateLimits object of 600 per minute and 50,000 per day.

Step 1: Create a room when the match starts

One call, from the service that made the match. Players passed as initialMembers are invited and auto-accepted in the same request, so the room is populated before the first frame renders.

curl -X POST https://api.cherry.fun/api/v1/apps/groups \
  -H "Authorization: Bearer ${CHERRY_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "ownerWallet": "7jx8aB...",
    "title": "Match #1042",
    "description": "Finals lobby",
    "initialMembers": ["walletA...", "walletB..."]
  }'
// your match service
const res = await fetch('https://api.cherry.fun/api/v1/apps/groups', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CHERRY_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    ownerWallet: HOUSE_WALLET,
    title: `Match #${matchId}`,
    initialMembers: players.map((p) => p.wallet),
  }),
});

const { roomId } = await res.json();
await matches.update(matchId, { cherryRoomId: roomId });

ownerWallet is required and becomes the room owner. Use a wallet your studio controls, not a player’s, so no single player can rename the room or walk off with it. Store the roomId on the match record.

You know it worked when the response is a single roomId and GET /api/v1/apps/groups/:roomId returns the room you just created.

Step 2: Post game events as your app identity

Your project has an app identity, a wallet of its own, and it does not need to join a room to post in it. A game running a thousand match rooms posts into all of them without adding a member to each.

await fetch(`https://api.cherry.fun/api/v1/apps/groups/${roomId}/messages`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CHERRY_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ content: `Round 1 to ${winner.name}. Score 3-1.` }),
});

Content runs 1 to 10,000 characters. Pass an optional botId to speak as one of your project’s named bots, so a referee and a rewards bot read as different characters. The sender is resolved on Cherry’s side, so no message can impersonate a player.

You know it worked when the message appears under your app’s display name, set in the portal’s developer profile.

Step 3: Mount the room in the game client

The embed is an iframe from embed.cherry.fun with wallet auth, theming, realtime, and unread badges in it. Pin it to the match room with the roomId you stored.

import { CherryEmbed } from '@cherrydotfun/chat-embed-sdk';

const chat = new CherryEmbed({
  appId: 'YOUR_EMBED_ID',
  container: '#match-chat',
  roomId,
  mode: 'single',
});

await chat.mount();

Pick the auth mode before shipping. wallet-only needs no backend: the player connects and signs. app-trusted+wallet has your backend mint a five-minute token for the connected wallet and the player sign a challenge too. app-trusted is the zero-signature path, where your server is the identity source and the token’s subject claim carries the wallet, so no prompt interrupts a match. The embeddable chat page compares the three.

One caveat: as of 11 September 2026 only single display mode is implemented, with list and external-controlled documented ahead of runtime support. When a player moves to the next match, destroy the embed and mount a new one.

You know it worked when a player from initialMembers sees the room open with your Step 2 message in it.

Step 4: Moderate the match from your server

Match chat draws trash talk, and some of it crosses the line. Because your app created the room, your project key moderates it as the room owner, with the members:moderate scope and no human moderator on shift.

const member = `https://api.cherry.fun/api/v1/apps/groups/${roomId}/members/${wallet}`;
const headers = {
  Authorization: `Bearer ${process.env.CHERRY_API_KEY}`,
  'Content-Type': 'application/json',
};

// silence a griefer for the rest of the match
await fetch(`${member}/mute`, { method: 'POST', headers });

// ban a repeat offender and wipe what they posted
await fetch(`${member}/ban`, {
  method: 'POST',
  headers,
  body: JSON.stringify({ deleteMessages: true }),
});

A mute is a shadow-ban: the player can no longer post, their earlier messages stay, and it lasts until you call unmute. A ban with deleteMessages: true also removes the player’s messages, and the response carries a deletedCount. DELETE .../members/:wallet kicks a player who quit mid-match. The room owner cannot be moderated through the API (403 CANNOT_MODERATE_OWNER), one more reason the studio wallet from Step 1 owns every room.

To react to what players type, read the room with GET /groups/:roomId/messages and the messages:read scope: newest first, up to 100 per page, paged back with before. For a bot that answers in real time, invite one of your project’s bots into the room and let its bot key long-poll GET /api/v1/bots/getUpdates or register a webhook with POST /api/v1/bots/setWebhook, as the moderation bot guide shows.

You know it worked when the muted player’s next message never shows up for the rest of the lobby and the ban response returns a deletedCount.

Step 5: Close the room when the match ends

Delete the room, or keep it as a rematch thread.

await fetch(`https://api.cherry.fun/api/v1/apps/groups/${roomId}`, {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${process.env.CHERRY_API_KEY}` },
});

Only rooms your app created can be deleted; one assigned to your app returns 403 CANNOT_DELETE_ASSIGNED_ROOM. If you keep it, PATCH /groups/:roomId to retitle it with the result, and a player’s room list becomes a match history.

You know it worked when the room no longer appears in GET /groups.

Clan rooms and token-gated guilds

A clan room is the same POST /groups call with a longer life: store the roomId on the clan record, add recruits with POST /groups/:roomId/members and autoAccept: true, promote officers with the member role endpoint.

Attach a gatingRule at creation and membership answers to the chain instead of to a spreadsheet. A rule on your game token or your item collection is checked when a player joins and re-checked in the background, so a wallet that sells out of the collection loses the room. Nobody sends a verify link, nobody screenshots a balance. The token-gated chat page lists every rule type, and wallet identity covers the name and badges beside each message.

Game audiences already sit in Cherry rooms of this shape. SOL Miner’s community room had 4,012 members and Gib Meme’s had 1,887 as of September 2026. Both are mini apps rather than API integrations, so read those numbers as the audience a game reaches here.

Limits to plan for

  • Rate limits are per app and rolling: 600 requests per minute and 50,000 per day by default, as of 10 September 2026. A three-call match (create, greet, delete) leaves about 200 match starts in any minute and roughly 16,600 a day. Reads count like writes.
  • A 429 returns {"error":"RATE_LIMITED"} with Retry-After in seconds. Honor it.
  • Group chats are not end-to-end encrypted, by design, so moderation and bots can work. Player DMs are, with keys derived from the wallet.
  • Your app manages only rooms it created; another app’s room returns 403 ROOM_NOT_MANAGED_BY_APP.
  • The project key is a server secret: ship it in a WebGL bundle and anyone can delete your rooms.

Troubleshooting

SymptomLikely causeFix
401 on every callThe key was rebuilt from parts, or sent from the browserSend the whole cherry_sk_ token, server-side
403 INSUFFICIENT_SCOPEThe key lacks that endpoint’s scopeGrant it on the key in the portal
Player sees an empty embedThey are not an active memberAdd them with autoAccept: true
Embed never renders in a WebGL buildThe host page origin is not allowedAdd the exact origin, scheme and port
WebView shows chat but signing never startsThe embed loaded as the top-level documentNest it in an iframe on a host page
403 CANNOT_MODERATE_OWNER on a muteThe target wallet owns the roomCreate match rooms with a studio wallet as ownerWallet

Create a project and a key at portal.cherry.fun , then follow Cherry API: create rooms and attach to embeds .

FAQ

Is there a Unity or Unreal SDK for Cherry chat?

No. As of 11 September 2026 Cherry ships a REST API for your game backend and a browser SDK, @cherrydotfun/chat-embed-sdk, for the UI. A WebGL build runs the chat on the host page beside the canvas; a native or mobile build runs it in a WebView on a small host page.

How do I add a chat room to every game match?

Call POST /api/v1/apps/groups when the match is made, pass the players’ wallets as initialMembers so they are active without accepting an invite, store the returned roomId on the match record, and pass it to the embed.

Can clans and guilds have their own chat rooms?

Yes. A clan room is the same POST /groups call with a longer life: keep the roomId on the clan record, add members with POST /groups/:roomId/members, and attach a gatingRule so only holders of your game token or NFT stay.

How do I add chat to a web3 game without a backend?

Use wallet-only auth against a public room you created once in the Cherry app. You lose per-match rooms: creating rooms on demand needs a project key, and a project key must never reach a client.

Do players need a Cherry account before they can chat?

No separate account. The player’s Solana wallet is the identity, with no phone number, email, or KYC step. If your backend is already the identity source, use app-trusted mode: your server mints a short-lived token and the player never signs a challenge.

Sources

Build on Cherry

Add chat to your dApp, ship a mini app, or run bots and rooms through the API. Self-serve at the developer portal.