# How to build a moderation bot for a crypto chat

> Build a moderation bot for a Cherry group chat: create a bot key, promote the bot to a room role, wire updates, then mute, kick, ban and delete.


A moderation bot is a named account that deletes spam, mutes offenders and removes repeat abusers from a room without a human watching the chat at 3am. This guide covers how to build a moderation bot on Cherry, the wallet-to-wallet messenger where people sign in with a wallet and join public, token-gated and paid group chats. The step that decides whether any of it works is the authority model: a bot key on its own gives your bot no power over anyone. It moderates only where it holds a role in the room, and that promotion is a separate call made with a different key.

## Before you build a moderation bot

You need a Cherry Portal project (sign in with a Solana wallet and create a project), a server to hold secrets, and a room: one your project creates, or one whose owner agrees to promote your bot. Keys are bearer tokens, so every call below runs from your backend, never from a browser.

Two key types do two jobs. A project key (`cherry_sk_`) manages rooms your project owns and acts as their owner. A bot key (`cherry_bot_`) acts as one named bot with its own wallet and avatar. Building a moderation bot uses both: the project key sets the room up once, the bot key does the work.

One design fact shapes the rest: group chats on Cherry are not end-to-end encrypted, which is what lets a bot read a room and act on what it reads. DMs are end-to-end encrypted and no bot can filter them, so rules that depend on inspecting private messages are out of reach.

## Step 1: Create the bot and copy its key

In [Cherry Portal](https://portal.cherry.fun), open **Bots** and click **Create bot**. Under **Bot identity**, set the **Display name**: the bot's public face, next to its avatar in every room it posts in. Self-serve names are checked at creation, so a bot cannot call itself after a brand or an authority role. "Cherry Support" is refused, "Supportive Trading Bot" passes.

Create a key in the bot's **Keys** section, then open **Edit scopes** and grant what the bot needs: `bots:groups:moderate` for the moderation calls, plus `bots:updates:poll` or `bots:webhook:manage` for the update transport in step 3. Scopes are per endpoint, and the ones a moderation bot needs are self-serve. A scope granted on the wrong surface does nothing: Apps API scopes belong on project keys, `bots:*` scopes on bot keys. The token appears once, under the line "Copy your key now. It won't be shown again."

Copy the bot's wallet too, from **Bot wallet address** or from `GET /api/v1/bots/me`. Step 2 needs it, and you know this step worked when that call returns the bot's identity, wallet and granted scopes. Projects, keys and team access are covered in the [Cherry Portal guide for developers](https://cherry.fun/learn/guides/cherry-portal-for-developers/).

## Step 2: Promote the bot to a role in the room

Two independent gates stand in front of every bot moderation call. The scope authorizes the API request. The bot's role in that specific room authorizes acting on another member. A bot holding `bots:groups:moderate` that is a plain member of the room, or not in the room at all, is refused with a 403 on every call, which is the usual reason a new bot does nothing.

With a project key, create the room, invite the bot's wallet and set its role:

```ts
const OWNER = {
  Authorization: `Bearer ${process.env.CHERRY_PROJECT_KEY}`,
  'Content-Type': 'application/json',
};
const apps = 'https://api.cherry.fun/api/v1/apps';

const { roomId } = await (
  await fetch(`${apps}/groups`, {
    method: 'POST',
    headers: OWNER,
    body: JSON.stringify({ ownerWallet: HOST_WALLET, title: 'General' }),
  })
).json();

await fetch(`${apps}/groups/${roomId}/members`, {
  method: 'POST',
  headers: OWNER,
  body: JSON.stringify({ wallets: [BOT_WALLET], autoAccept: true }),
});

await fetch(`${apps}/groups/${roomId}/members/${BOT_WALLET}/role`, {
  method: 'POST',
  headers: OWNER,
  body: JSON.stringify({ role: 'moderator' }),
});
```

For a room your project did not create, the owner does the same by hand: invite the bot's wallet, open it in the member list and pick the **Moderator** role, described in the app as "Deletes messages, mutes and kicks members". Either path ends the same way.

Three rules survive the promotion. A bot cannot moderate the room owner, and a moderator bot cannot act on admins. A bot cannot set roles, so none promotes itself or mints an admin. And a bot always acts as its own wallet, never inheriting the owner's authority. You know this step worked when a mute call against a test wallet succeeds.

## Step 3: Choose long-poll updates or a webhook

Your bot needs something to react to. Long polling is `GET /api/v1/bots/getUpdates` with the `bots:updates:poll` scope, Telegram-style: your process asks for new updates and waits. It needs no public URL, which suits local development and a single long-lived worker.

A webhook is `POST /api/v1/bots/setWebhook` with `bots:webhook:manage`. Cherry pushes events to your URL, which fits serverless handlers and anything horizontally scaled where no process can hold a poll open. The portal's **Webhook** panel puts the choice in one line: "Configure where Cherry pushes bot events. Leave blank to use long-poll (getUpdates) instead." Its URL field adds the constraint: "Must be HTTPS. Cherry will POST bot events as JSON."

While a webhook is set, the long-poll endpoint is closed: calls to it are rejected with a conflict error until you delete the webhook. Configure one transport, not both. Both transports draw on one budget: 600 requests per minute and 50,000 per day per app as of September 2026, counted on rolling windows, reads and writes alike.

## Step 4: Mute, kick, ban and delete

Four calls cover almost every rule a crypto room needs. Each takes a JSON body, needs `bots:groups:moderate`, and acts as the bot's wallet.

| Action | Endpoint | Body | What the member gets |
|---|---|---|---|
| Delete a message | `POST /bots/deleteGroupMessage` | `{ roomId, messageId }` | The message is gone for everyone in the room. |
| Mute | `POST /bots/muteGroupMember` | `{ roomId, wallet }` | The app describes it as "Can read, can't write". Their messages are dropped without an error, so a spammer types into a room nobody else sees. |
| Kick | `POST /bots/kickGroupMember` | `{ roomId, wallet }` | Removed from the room, and "Can rejoin later". |
| Ban | `POST /bots/banGroupMember` | `{ roomId, wallet, deleteMessages }` | Removed and "Can't rejoin until unbanned". With `deleteMessages: true` their message history goes with them. |

```ts
const BOT = {
  Authorization: `Bearer ${process.env.CHERRY_BOT_KEY}`,
  'Content-Type': 'application/json',
};
const bots = 'https://api.cherry.fun/api/v1/bots';

await fetch(`${bots}/deleteGroupMessage`, {
  method: 'POST',
  headers: BOT,
  body: JSON.stringify({ roomId, messageId }),
});

await fetch(`${bots}/muteGroupMember`, {
  method: 'POST',
  headers: BOT,
  body: JSON.stringify({ roomId, wallet }),
});
```

`unbanGroupMember` and `unmuteGroupMember` take the same `{ roomId, wallet }` shape. When the bot speaks, the message carries the bot's name and avatar. A project key posts as the project's own app identity unless you name a bot with `botId`, which is how one backend keeps an announcement identity and a moderator apart. The full endpoint list and the OpenAPI 3.1 document are on the [Cherry API and bots page](https://cherry.fun/learn/features/cherry-api-and-bots/) and in the portal reference below.

## What the room already handles before your bot

Write your bot for the gap. Cherry runs several [anti-spam layers](https://cherry.fun/learn/features/anti-spam/) underneath every group, and the generic attacks are mostly theirs to absorb.

A wallet with no successful on-chain history cannot post in a public group or leave a review, and its cold DMs land in a hidden requests folder: the fresh-burner flood is handled before your code sees a message. A group message containing a blocked word never reaches the room, and the sender sees it in their own view, so the spammer gets no signal to tune against. Any member can also report a message or a profile from the app, choosing Spam, Scam or fraud, Harassment, Inappropriate content, Bot or Other, which puts a human moderator in the loop.

What is left is the part only you know: your community's rules and the scam pattern aimed at your token this week. That is the bot worth writing.

## Where to stop automating

Cherry's own layers follow one principle: automation drops or limits, humans ban. Copy that. Give your bot deletes, mutes and rate limits, the reversible actions where a false positive costs a member ten minutes, and route bans through a person: post the evidence into a moderators-only room with an inline button (the `bots:interactive` scope) and let a moderator decide.

A wrong mute annoys someone. A wrong ban on a large holder mid-launch costs you a community. Human-run rooms use the same rule of thumb, covered in the guide to [moderating a crypto group chat](https://cherry.fun/learn/guides/moderate-a-crypto-group-chat/).

## Troubleshooting

Every moderation call returns 403 although the scope is granted: the bot holds no privileged role in that room. Re-run step 2 and check that the promotion landed on the bot's wallet.

The bot acts on most members but not one: no bot moderates the room owner, and a moderator bot cannot touch admins. Promote it to admin for a longer reach.

The poller is rejected with a conflict error: a webhook is configured for that bot. Delete the webhook, or retire the poller and serve the events.

429s during a raid: back off for the seconds in `Retry-After`. The budget is per app, so a tight poll loop and your moderation calls compete for the same 600 per minute.

The bot behaves badly: mute it. A muted bot's calls are rejected, and an owner or admin can mute from the app faster than you can redeploy. If the key is exposed, rotate it in place from the bot's **Keys** section; the old secret dies the moment the new one is issued.

## Related guides

- [Add chat to your dApp](https://cherry.fun/learn/guides/add-chat-to-your-dapp/) for the embeddable widget and its own per-room moderation rules.
- [Cherry Portal for developers](https://cherry.fun/learn/guides/cherry-portal-for-developers/) for projects, keys and team access.
- [Discord vs Telegram for crypto communities](https://cherry.fun/learn/compare/discord-vs-telegram-for-crypto-communities/) if the room itself is still up for debate.

## FAQ

**Can a bot moderate a room my project does not own?** Yes. A bot key moderates any room where the bot itself holds an owner, admin or moderator role, including rooms your project never created. The room's human owner invites the bot's wallet and promotes it, and the bot can act from then on.

**Can a moderation bot read direct messages?** No. Direct messages are end-to-end encrypted, so nothing server-side can inspect them. A bot sees group messages in rooms it belongs to, and the DM history of conversations with the bot itself.

**How do I stop a misbehaving bot quickly?** Mute the bot in the room. A muted bot's moderation calls are rejected, so muting works as a kill-switch any owner or admin can hit from the Cherry app without touching your deployment.

**What are the Cherry API rate limits for a bot?** 600 requests per minute and 50,000 per day per app as of September 2026, on rolling windows, with reads and writes counting the same. The budget is shared by every key in the app, so a busy poller eats into your moderation calls.

Create a bot and its first key at [portal.cherry.fun](https://portal.cherry.fun).

## Sources

- [Cherry Portal: build a moderation bot](https://portal.cherry.fun/docs/guides/moderation-bot)
- [Cherry API: members and moderation](https://portal.cherry.fun/docs/api/members)
- [Cherry API: scopes](https://portal.cherry.fun/docs/api/scopes)
- [Cherry API: rate limits and errors](https://portal.cherry.fun/docs/api/rate-limits)
- [Cherry API: authentication](https://portal.cherry.fun/docs/api/authentication)
- [Cherry API: endpoint reference](https://portal.cherry.fun/docs/api/reference)
- [Cherry API OpenAPI 3.1 specification](https://portal.cherry.fun/openapi.json)

