# 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](https://cherry.fun/learn/features/cherry-api-and-bots/) page documents in full.

## Before you start

- A project at [portal.cherry.fun](https://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:

```bash
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.

```bash
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..."]
  }'
```

```ts
// 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.

```ts
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.

```ts
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](https://cherry.fun/learn/features/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.

```ts
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](https://portal.cherry.fun/docs/guides/moderation-bot) 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.

```ts
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](https://cherry.fun/learn/features/token-gated-chat/) page lists every rule type, and [wallet identity](https://cherry.fun/learn/features/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](https://cherry.fun/learn/features/mini-apps/) rather than API integrations, so read those numbers as the audience a game reaches here.

[SOL Miner on Cherry](https://chat.cherry.fun/@solminer)

[Gib Meme on Cherry](https://chat.cherry.fun/@gibmeme)

## 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

| Symptom | Likely cause | Fix |
|---|---|---|
| `401` on every call | The key was rebuilt from parts, or sent from the browser | Send the whole `cherry_sk_` token, server-side |
| `403 INSUFFICIENT_SCOPE` | The key lacks that endpoint's scope | Grant it on the key in the portal |
| Player sees an empty embed | They are not an active member | Add them with `autoAccept: true` |
| Embed never renders in a WebGL build | The host page origin is not allowed | Add the exact origin, scheme and port |
| WebView shows chat but signing never starts | The embed loaded as the top-level document | Nest it in an iframe on a host page |
| `403 CANNOT_MODERATE_OWNER` on a mute | The target wallet owns the room | Create match rooms with a studio wallet as `ownerWallet` |

## Related guides

- [Add chat to your dApp](https://cherry.fun/learn/guides/add-chat-to-your-dapp/) for the web install without the per-match plumbing.
- [Build a Solana mini app](https://cherry.fun/learn/guides/build-a-solana-mini-app/) if the game itself should run inside Cherry.
- [Anti-spam and moderation](https://cherry.fun/learn/features/anti-spam/) for the wallet-age gate, mutes, and bans.

Create a project and a key at [portal.cherry.fun](https://portal.cherry.fun), then follow [Cherry API: create rooms and attach to embeds](https://portal.cherry.fun/docs/api/rooms).

## 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

- [Cherry API: create rooms and attach to embeds](https://portal.cherry.fun/docs/api/rooms)
- [Cherry API: authentication](https://portal.cherry.fun/docs/api/authentication)
- [Cherry API: messages and bot actions](https://portal.cherry.fun/docs/api/messages)
- [Cherry API: members and moderation](https://portal.cherry.fun/docs/api/members)
- [Cherry API: rate limits and errors](https://portal.cherry.fun/docs/api/rate-limits)
- [Guide: a room per game, match, or order](https://portal.cherry.fun/docs/guides/room-per-entity)
- [Guide: build a moderation bot](https://portal.cherry.fun/docs/guides/moderation-bot)
- [Chat Embed SDK installation](https://portal.cherry.fun/docs/embed/installation)
- [Chat Embed SDK authentication](https://portal.cherry.fun/docs/embed/authentication)
- [Chat Embed SDK on React Native and Flutter](https://portal.cherry.fun/docs/embed/mobile)
- [SOL Miner on Cherry](https://chat.cherry.fun/@solminer)
- [Gib Meme on Cherry](https://chat.cherry.fun/@gibmeme)

