# Cherry developer docs: full text > Every Cherry developer documentation page, concatenated into one file for AI > agents and no-JS clients. Individual pages: append `.md` to any docs URL > (e.g. https://portal.cherry.fun/docs/quickstart.md), or see https://portal.cherry.fun/llms.txt for the index. > > Machine-readable API: the OpenAPI 3.1 spec for the Cherry Apps + Bot APIs is > at https://portal.cherry.fun/openapi.json (import into Postman / codegen / agents). --- # Cherry developer docs Cherry lets you add real chat to your product, and drive it from your backend. There are two integration surfaces, designed to work together: ## Chat Embed SDK A drop-in widget that renders Cherry chat inside your web app. Install one npm package, mount it onto a `
`, and your users are chatting. It handles wallet auth, theming, and realtime updates for you. - **Package:** `@cherrydotfun/chat-embed-sdk` - **Use it for:** putting a chat room (or a list of rooms) on your site, themed to match your brand. - [Quickstart →](https://portal.cherry.fun/docs/quickstart.md) > **Tip:** Want to see it first? Try the [live embed demo and theme builder](https://cherry.fun/chat-embed-example/), a no-signup, no-backend playground where you can switch themes, pick a display mode, and copy a ready-to-paste integration snippet. ## Cherry API A REST API you call from **your backend** to manage chat programmatically: create rooms on the fly, send messages, and moderate members. It has two key types, each a single opaque token from the portal: a **project key** (`cherry_sk_`) for rooms and messages your project owns, and a **bot key** (`cherry_bot_`) for a named bot that acts with its own in-room identity (including [moderating rooms it's promoted in](https://portal.cherry.fun/docs/guides/moderation-bot.md)). - **Base URL:** `https://api.cherry.fun/api/v1/apps` (project/Apps API) · `https://api.cherry.fun/api/v1/bots` (Bot API) - **Use it for:** creating a room per game, match, order, or community automatically, then surfacing it in an embed. - [Cherry API authentication →](https://portal.cherry.fun/docs/api/authentication.md) ## How they fit together The two surfaces compose into the most common pattern: your backend **creates a room** with the Cherry API, and your frontend **embeds** it with the SDK. For example, a game spins up a room per match: the server calls the Cherry API to create the room and add the players, then the web client mounts the embed pointed at that room. The same flow works for a marketplace order, a token-gated community, or a support thread. ## Where to go next - New here? Start with the [Quickstart](https://portal.cherry.fun/docs/quickstart.md), a public chat embed in about five minutes. - Building the full experience? The **Chat Embed SDK** and **Cherry API** sections cover authentication, theming, dynamic rooms, and the complete reference. > **Tip:** Building with an AI coding assistant? Point it at our [`llms.txt`](https://portal.cherry.fun/llms.txt), a single plain-text file that indexes and links every page in these docs in a format LLMs read well. Paste the URL into your assistant (or let it fetch it) and it has the whole SDK and API at hand. See [For AI agents](https://portal.cherry.fun/docs/ai/for-ai-agents.md) for that plus ready-made integration skills. --- # Quickstart Get a live, public Cherry chat embed running on your site in about five minutes, no backend required. This uses **wallet-only** auth, where visitors connect their own Solana wallet to chat in a public room. > **Tip:** Want to see it first? Try the [live embed builder](https://cherry.fun/chat-embed-example/), a no-signup demo where you can theme the widget in real time and copy a ready-to-paste integration snippet. ## 1. Sign in with your wallet Cherry signs you in with your Solana wallet, so there's no email or password to set up. 1. Open the [portal](https://portal.cherry.fun/login) and click **Connect Wallet**. 2. Choose your Solana wallet, then approve the signature request. This is Sign In With Solana (SIWS): the signature proves you own the wallet and starts your session. Nothing goes onchain and there's no gas. 3. You land on your dashboard, signed in. ## 2. Create a project A project holds your embeds, bots, and API keys, so you need one before you can create an embed. 1. On the [dashboard](https://portal.cherry.fun/dashboard), click **Create Project**. On your first visit the empty state offers the same button. 2. Enter a **Name**. That's the only required field; description, notification email, and teammate invites are optional and editable later. 3. Click **Create Project**. Cherry opens the new project, where **Chat embeds** live. ## 3. Create an embed In your project, open **Chat embeds** → **New embed** and give it a name. Then: 1. Copy the **embed ID** (your `appId`). 2. Under **Allowed origins**, add the origin you'll embed from (e.g. `http://localhost:3000` for local dev, and your production URL). 3. Make sure the embed is **enabled**. > **Note:** The name you give an embed is also the visible title of its chat room, so pick something visitors should see. Renaming the embed later renames the live chat too; the new title reaches already-open widgets within a few minutes. > **Warning:** An embed only loads on origins you've allow-listed, and only while it's enabled, otherwise it returns `401`. Add your dev and production origins before testing. ## 4. Install the SDK ```bash npm install @cherrydotfun/chat-embed-sdk ``` ## 5. Mount the widget Point the embed at a public room and mount it onto a container element: ```ts import { CherryEmbed } from '@cherrydotfun/chat-embed-sdk'; const chat = new CherryEmbed({ appId: 'YOUR_EMBED_ID', container: '#cherry-chat', roomId: 'YOUR_ROOM_ID', mode: 'single', theme: { mode: 'dark', primaryColor: '#FF5BA8' }, }); await chat.mount(); ``` ```html
``` That's it: a themed chat room, live on your page. ## No build step? Use a script tag For a plain HTML site, load the SDK from npm via jsDelivr. The bundle exposes `window.CherryEmbedSDK`: ```html
``` ## Next steps - **Authenticate your own users** (tie chat identity to your users' wallets via your backend) → [Chat Embed SDK › Authentication](https://portal.cherry.fun/docs/embed/authentication.md) - **Theme it to your brand:** presets and the full theme reference → [Chat Embed SDK › Theming](https://portal.cherry.fun/docs/embed/theming.md) - **Create rooms from your backend** and surface them in the embed → [Cherry API › Create rooms & attach to embeds](https://portal.cherry.fun/docs/api/rooms.md) --- # Guide: Add public chat to your app The fastest way to ship chat: a public room any visitor can join by connecting their wallet. No backend required. **You'll need:** an embed created in [the portal](https://portal.cherry.fun/dashboard) (in `wallet-only` mode), its `appId`, your origin allow-listed, and a public room's `roomId`. ## 1. Install ```bash npm install @cherrydotfun/chat-embed-sdk ``` ## 2. Add a container ```html
``` ## 3. Mount ```ts import { CherryEmbed } from '@cherrydotfun/chat-embed-sdk'; const chat = new CherryEmbed({ appId: 'YOUR_EMBED_ID', container: '#cherry-chat', roomId: 'YOUR_ROOM_ID', mode: 'single', theme: { mode: 'dark', primaryColor: '#FF5BA8' }, }); await chat.mount(); ``` That's a complete, themed, public chat room. Visitors connect their own wallet to post. > **Tip:** Want to see it before you wire it up? Try the [live embed builder](https://cherry.fun/chat-embed-example/), a no-signup, no-backend demo where you can switch themes and display modes in real time and copy the matching integration snippet. ## Make it a floating widget Drop the `container`, switch `position`, and start collapsed. The widget doesn't render its own launcher button, so wire your own control to open it (see [display modes](https://portal.cherry.fun/docs/embed/display-modes.md) for all options): ```ts const chat = new CherryEmbed({ appId: 'YOUR_EMBED_ID', roomId: 'YOUR_ROOM_ID', position: 'floating-right', collapsed: true, }); await chat.mount(); // collapsed starts hidden: open it from your own UI document.querySelector('#open-chat')?.addEventListener('click', () => chat.toggle()); ``` ## Where next - Tie chat identity to *your* users → [Authenticated chat](https://portal.cherry.fun/docs/guides/authenticated-chat.md). - Theme it → [Theming](https://portal.cherry.fun/docs/embed/theming.md). --- # Guide: Authenticated chat with your users' wallets Tie chat identity to your users by minting short-lived tokens on your backend and having users sign a wallet challenge. This is the `app-trusted+wallet` mode, the default for production embeds. **You'll need:** an embed in `app-trusted+wallet` mode, its `appId` and **app secret** (open your project in [the dashboard](https://portal.cherry.fun/dashboard) → Chat embeds and API keys), and a backend you control. ## 1. Mint tokens on your backend Sign a JWT with your app secret. The user's wallet is `sub`, your app is `app_id`. Keep the secret server-side. ```ts // POST /api/cherry-embed-token import jwt from 'jsonwebtoken'; import { randomUUID } from 'node:crypto'; export function mintCherryToken(walletAddress: string) { return jwt.sign( { sub: walletAddress, app_id: process.env.CHERRY_APP_ID }, process.env.CHERRY_APP_SECRET, { algorithm: 'HS256', expiresIn: '5m', jwtid: randomUUID() }, ); } ``` > **Note:** A full runnable example is in the public [`chat-embed-sdk`](https://github.com/cherrydotfun/chat-embed-sdk/tree/main/example/app-trusted+wallet) repo, under `example/app-trusted+wallet` (its `server.js`). ## 2. Connect the wallet and mount Register `signChallengeHandler` before `mount()`: the iframe calls it to verify wallet ownership. ```ts import { CherryEmbed } from '@cherrydotfun/chat-embed-sdk'; const provider = window.phantom?.solana; const { publicKey } = await provider.connect(); const walletAddress = publicKey.toString(); const { token } = await fetch('/api/cherry-embed-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ walletAddress }), }).then((r) => r.json()); const chat = new CherryEmbed({ appId: 'YOUR_EMBED_ID', container: '#cherry-chat', roomId: 'YOUR_ROOM_ID', token, walletAddress, signChallengeHandler: async (message) => { // message: Uint8Array. Sign the bytes as-is, don't re-encode them const { signature } = await provider.signMessage(message, 'utf8'); return signature; // Uint8Array }, }); await chat.mount(); ``` ## 3. Session renewal is automatic The embed token expires in ~5 minutes, so mint it fresh right before `mount()`; don't cache it. After the initial exchange the iframe renews its session silently via a rotating refresh token; there's nothing to refresh on a timer. To force a fresh exchange (e.g. after an account switch), mint a new token and call `chat.setToken(token)`. ## Where next - Create rooms from your backend → [A room per game / match / order](https://portal.cherry.fun/docs/guides/room-per-entity.md). - Full auth detail → [Embed › Authentication](https://portal.cherry.fun/docs/embed/authentication.md). --- # Guide: A room per game / match / order Spin up a dedicated chat room for each unit of activity in your app (a game match, a marketplace order, a support ticket) and drop your users straight into it. This combines the [Cherry API](https://portal.cherry.fun/docs/api/rooms.md) (server) with the [embed SDK](https://portal.cherry.fun/docs/embed/display-modes.md) (client). ## 1. Create the room from your backend When the activity starts (a match is made, an order is placed), call the Cherry API. Add the participants as `initialMembers` so they're active immediately. ```ts // your server - CHERRY_API_KEY is your project key: the one opaque // `cherry_sk__` token copied from the portal. 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: hostWallet, title: `Match #${matchId}`, initialMembers: [playerA, playerB], }), }); const { roomId } = await res.json(); // persist roomId alongside your match/order record ``` ## 2. Greet them with the bot (optional) See the [Messages API](https://portal.cherry.fun/docs/api/messages.md) for the full send-message endpoint. ```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: 'Match starting - good luck! 🎮' }), }); ``` ## 3. Open the room in the embed Pass the `roomId` to a `single`-mode embed, or, if the user moves between activities, mount once in `external-controlled` mode and call `setRoom()`: ```ts // single room new CherryEmbed({ appId: 'YOUR_EMBED_ID', container: '#chat', roomId, mode: 'single' }); // or, switch rooms as the user navigates chat.setRoom(roomId); ``` ## 4. Clean up When the activity ends, delete the room (it must be app-created): ```ts await fetch(`https://api.cherry.fun/api/v1/apps/groups/${roomId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.CHERRY_API_KEY}` }, }); ``` ## Where next - [Cherry API › Create rooms & attach to embeds](https://portal.cherry.fun/docs/api/rooms.md) for the full endpoint detail. - [Moderation bot](https://portal.cherry.fun/docs/guides/moderation-bot.md) to keep rooms healthy. --- # Guide: Build a moderation bot Run a bot that keeps a room healthy (delete spam, mute offenders, ban repeat abusers), acting as **its own identity** in the room, not as the room owner. This uses the **two-key** model. A bot can only moderate a room where it holds a privileged **role**, so the flow has two halves: 1. A **project key** (`cherry_sk_`) sets the room up: creates it, invites the bot's wallet, and promotes it to `moderator`. 2. The **bot key** (`cherry_bot_`) does the moderating via the `/api/v1/bots/*` endpoints. **You'll need:** - A **bot** in your project with a **bot key** that has the [`bots:groups:moderate` scope](https://portal.cherry.fun/docs/api/scopes.md). Note its **wallet**: from the portal's bot section, or `GET /api/v1/bots/me`. - A **project key** with `groups:create`, `members:invite`, and `members:moderate` to set the room up. ## 1. Give the bot a moderator role (project key) The bot's authority comes from its in-room role. Create a room your project owns (or reuse one), invite the bot's wallet, activate it, then promote it. ```ts // CHERRY_PROJECT_KEY is your `cherry_sk__` token. const OWNER = { Authorization: `Bearer ${process.env.CHERRY_PROJECT_KEY}`, 'Content-Type': 'application/json', }; // create a room the project owns const { roomId } = await ( await fetch('https://api.cherry.fun/api/v1/apps/groups', { method: 'POST', headers: OWNER, body: JSON.stringify({ ownerWallet: HOST_WALLET, title: 'General' }), }) ).json(); // invite the bot's wallet and activate it immediately await fetch(`https://api.cherry.fun/api/v1/apps/groups/${roomId}/members`, { method: 'POST', headers: OWNER, body: JSON.stringify({ wallets: [BOT_WALLET], autoAccept: true }), }); // promote it to moderator, now the bot can moderate THIS room await fetch( `https://api.cherry.fun/api/v1/apps/groups/${roomId}/members/${BOT_WALLET}/role`, { method: 'POST', headers: OWNER, body: JSON.stringify({ role: 'moderator' }) }, ); ``` ## 2. Moderate as the bot (bot key) Now switch to the **bot key**. Every call is authorized by two independent checks: the `bots:groups:moderate` scope *and* the bot's moderator role in `roomId`. ```ts // CHERRY_BOT_KEY is your `cherry_bot__` token. const BOT = { Authorization: `Bearer ${process.env.CHERRY_BOT_KEY}`, 'Content-Type': 'application/json', }; // delete a spam message await fetch('https://api.cherry.fun/api/v1/bots/deleteGroupMessage', { method: 'POST', headers: BOT, body: JSON.stringify({ roomId, messageId }), }); // mute an offender (shadow-ban: they can't post, existing messages stay) await fetch('https://api.cherry.fun/api/v1/bots/muteGroupMember', { method: 'POST', headers: BOT, body: JSON.stringify({ roomId, wallet }), }); // ban a repeat abuser and wipe their messages await fetch('https://api.cherry.fun/api/v1/bots/banGroupMember', { method: 'POST', headers: BOT, body: JSON.stringify({ roomId, wallet, deleteMessages: true }), }); ``` `kickGroupMember`, `unbanGroupMember`, and `unmuteGroupMember` follow the same `{ roomId, wallet }` shape - see the [full list](https://portal.cherry.fun/docs/api/members.md). ## Watching for messages to act on Give the bot something to react to: - **Bot key:** long-poll `GET /api/v1/bots/getUpdates` (scope `bots:updates:poll`) or configure a webhook (`bots:webhook:manage`) to receive new messages as they arrive. - **Project key:** poll `GET /api/v1/apps/groups/:roomId/messages` (scope `messages:read`) for room history - respect the [rate limits](https://portal.cherry.fun/docs/api/rate-limits.md) and poll at a sane interval. ## Why two keys: the layered permission model The scope and the role are **separate gates**, and both must pass: - The **scope** (`bots:groups:moderate`) authorizes the API *call*. - The bot's **in-room role** (owner / admin / moderator) authorizes the *action on others*. > **Warning:** A scope alone is not permission to act. A bot with `bots:groups:moderate` that is only a plain member, or isn't in the room, is **denied** (`403 INSUFFICIENT_ROOM_ROLE`). This is why step 1 (promote to moderator) is required. ## Rules to remember - The bot **can't moderate the room owner**, and a moderator bot can't act on admins. - Bots **can't set roles**: only a project key (or a human owner/admin) can promote the bot in step 1. - **Mute the bot** to instantly disable it: a muted bot's moderation calls are rejected. A working kill-switch for a misbehaving bot. - **Ban** with `deleteMessages: true` purges the member's messages; **mute** leaves existing messages in place. ## Where next - [Members & moderation](https://portal.cherry.fun/docs/api/members.md), both moderation models and every endpoint. - [Scopes](https://portal.cherry.fun/docs/api/scopes.md) · [Rate limits & errors](https://portal.cherry.fun/docs/api/rate-limits.md). --- # For AI agents These docs are built to be consumed by AI coding assistants. Point your agent at them and it can [scaffold a Cherry integration end to end](https://portal.cherry.fun/docs/quickstart.md). ## llms.txt [`llms.txt`](https://portal.cherry.fun/llms.txt) is a single plain-text file that indexes and links every page in these docs, plus the key facts (package name, API base URL, token format) up front, structured the way LLMs read best. It follows the [llms.txt convention](https://llmstxt.org). ``` https://portal.cherry.fun/llms.txt ``` **How to use it:** paste that URL (or the file's contents) into your AI assistant before asking it to integrate Cherry - in Cursor, Claude, or similar, it becomes context the model can pull the right API and SDK details from. Agents that can browse can fetch it directly. It's the fastest way to give an assistant accurate, current knowledge of the whole API surface without copying pages one by one. ## Integration skills Cherry ships ready-made **agent skills** (`SKILL.md` files for tools like Claude Code) that turn "add Cherry chat to my app" into a guided, correct integration: the right auth mode, backend token signing, theming, events, and security rules. | Skill | Use it for | Source | |---|---|---| | `cherry-embed-integration` | Embedding Cherry chat with `@cherrydotfun/chat-embed-sdk` (web, React Native, Flutter) | [cherrydotfun/chat-embed-sdk](https://github.com/cherrydotfun/chat-embed-sdk/tree/main/skills) | | `cherry-miniapp-integration` | Running an existing dApp inside Cherry with `@cherrydotfun/miniapp-sdk` | [cherrydotfun/miniapp-sdk](https://github.com/cherrydotfun/miniapp-sdk/tree/main/skills) | If your assistant supports skills, install the one that matches your task and ask it to integrate Cherry: it will follow the same patterns documented here. ## Setup prompt The embed editor in the [dashboard](https://portal.cherry.fun/dashboard) generates a ready-to-paste setup prompt tailored to your embed (its `appId`, auth mode, and theme). Copy it into your AI assistant to scaffold the mount code for your exact configuration. ## Tips for accurate output - The npm package is **[`@cherrydotfun/chat-embed-sdk`](https://portal.cherry.fun/docs/embed/installation.md)**; the class is **`CherryEmbed`**. - The [Cherry API](https://portal.cherry.fun/docs/api/authentication.md) has two surfaces: the **Apps API** at **`https://api.cherry.fun/api/v1/apps`** (auth **`Bearer cherry_sk__`**, a project key) and the **Bot API** at **`https://api.cherry.fun/api/v1/bots`** (auth **`Bearer cherry_bot__`**, a bot key). Each key is **one opaque token** copied from the portal, never assembled from an app ID plus a secret. `cha__` is a deprecated legacy format. - Bot moderation is gated by **two** layers: the `bots:groups:moderate` scope **and** the bot's in-room role (owner/admin/moderator) - see [Members & moderation](https://portal.cherry.fun/docs/api/members.md). - Keep API keys and the **app secret** server-side: the secret signs embed tokens; API keys authenticate the Cherry API. --- # Installation The Chat Embed SDK renders Cherry chat inside your web app. It's published on npm and works in any modern bundler (Vite, Next.js, Webpack) or straight from a ` ``` > **Tip:** Pin the version in the CDN URL (`@0.1.5`) so a future release can't change behavior under you. With npm, your lockfile already does this. ## Mobile (React Native / Flutter) The SDK is browser-only, so it can't run directly in a mobile JS runtime. On mobile you run it inside a WebView on a small host page and bridge wallet signing to the native layer: see [React Native & Flutter](https://portal.cherry.fun/docs/embed/mobile.md). ## Requirements - A Cherry **embed** (created in your [project's](https://portal.cherry.fun/dashboard) **Chat embeds** section), it gives you the `appId`. - Your site's origin added to the embed's **Allowed origins**. - A mount target: an element on your page (for inline embeds) or nothing (for floating widgets). ## Next steps - [Configuration](https://portal.cherry.fun/docs/embed/configuration.md), every option `CherryEmbed` accepts. - [Authentication](https://portal.cherry.fun/docs/embed/authentication.md), wallet-only vs. backend-signed tokens. --- # Configuration You configure the widget by passing a config object to the `CherryEmbed` constructor: ```ts import { CherryEmbed } from '@cherrydotfun/chat-embed-sdk'; const chat = new CherryEmbed({ appId: 'YOUR_EMBED_ID', container: '#cherry-chat', roomId: 'YOUR_ROOM_ID', mode: 'single', theme: { mode: 'dark', primaryColor: '#FF5BA8' }, }); await chat.mount(); ``` > **Tip:** Want to try options before writing code? The [live embed builder](https://cherry.fun/chat-embed-example/) lets you toggle display mode, theme, and colors in real time and copies out a ready-to-paste config snippet, no signup or backend required. ## Options | Option | Type | Default | Notes | |---|---|---|---| | `appId` | `string` | | **Required.** Your embed's ID, from [the portal](https://portal.cherry.fun/dashboard). | | `container` | `string \| HTMLElement` | | **Required for inline embeds.** CSS selector or element to mount into. Omit for floating widgets. | | `roomId` | `string` | | The room to open. Required in `single` mode; optional in `list` / `external-controlled`. | | `mode` | `'single' \| 'list' \| 'external-controlled'` | `'single'` | How rooms are presented; only `single` is available today. See [Display modes](https://portal.cherry.fun/docs/embed/display-modes.md). | | `position` | `'inline' \| 'floating-right' \| 'floating-left'` | `'inline'` | Inline mounts into `container`; floating pins the chat panel to a viewport corner. | | `collapsed` | `boolean` | `false` | For floating widgets, start hidden; open from your own UI via `show()` / `toggle()`. | | `token` | `string` | | Short-lived JWT for `app-trusted` / `app-trusted+wallet` auth. See [Authentication](https://portal.cherry.fun/docs/embed/authentication.md). | | `walletAddress` | `string` | | The user's Solana address, for wallet-backed auth. Not needed in pure `app-trusted` mode: the token's `sub` claim carries it. | | `signChallengeHandler` | `(message: Uint8Array) => Promise` | | Signs the wallet challenge for wallet-backed auth. Register **before** `mount()`. Not used in pure `app-trusted` mode. | | `theme` | `EmbedTheme` | | Colors and typography. See [Theming](https://portal.cherry.fun/docs/embed/theming.md). | | `layout` | `EmbedLayout` | | Toggle UI chrome (header, avatars, input, …). See below. | | `embedUrl` | `string` | `'https://embed.cherry.fun'` | Origin the chat iframe is served from. Only override for self-hosted deployments. | ## Layout `layout` controls which pieces of UI the iframe renders: ```ts const chat = new CherryEmbed({ appId: 'YOUR_EMBED_ID', container: '#cherry-chat', layout: { showHeader: true, headerTitle: 'Support', showMemberCount: true, showAvatars: true, showTimestamps: true, showReactions: true, showInput: true, maxHeight: '600px', }, }); ``` | Field | Type | Notes | |---|---|---| | `showHeader` | `boolean` | Show the room header bar. | | `headerTitle` | `string` | Override the header title. | | `showMemberCount` | `boolean` | Show the member count. | | `showAvatars` | `boolean` | Show sender avatars. | | `showTimestamps` | `boolean` | Show message timestamps. | | `showReactions` | `boolean` | Allow message reactions. | | `showInput` | `boolean` | Show the composer (set `false` for a read-only feed). | | `maxHeight` | `string` | CSS max-height for the widget. | You can change layout after mount with `chat.setLayout({ … })`. ## App-trusted (zero-signature) Embeds set to pure `app-trusted` auth pass only `token`; there is no wallet connect on the page, so `walletAddress` and `signChallengeHandler` are omitted entirely: ```ts // The backend derives the wallet from its own authenticated session and puts // it in the token's `sub` claim; the page never learns or sends the address. const { token } = await fetch('/api/cherry-embed-token', { method: 'POST' }) .then((r) => r.json()); const chat = new CherryEmbed({ appId: 'YOUR_EMBED_ID', container: '#cherry-chat', roomId: 'YOUR_ROOM_ID', token, }); await chat.mount(); ``` Because your backend's word is the only identity proof, the mode runs with server-side restrictions (a fail-closed room allowlist, message rate limits, moderation off by default). Turn it on yourself in the portal's auth-mode selector. Trust model, token contract, and the full restrictions list: [Authentication](https://portal.cherry.fun/docs/embed/authentication.md). ## Next steps - [Authentication](https://portal.cherry.fun/docs/embed/authentication.md) · [Display modes](https://portal.cherry.fun/docs/embed/display-modes.md) · [Theming](https://portal.cherry.fun/docs/embed/theming.md) · [SDK API reference](https://portal.cherry.fun/docs/embed/api-reference.md) --- # Authentication Cherry chat identifies users by their Solana wallet. The SDK supports three auth modes: you pick one when you [configure the embed](https://portal.cherry.fun/docs/embed/configuration.md) in the portal, and wire the matching client code. Choose based on whether you run a backend and whether you want users to prove wallet ownership. | Mode | Backend? | Wallet signature? | Use when | |---|---|---|---| | `wallet-only` | No | Yes (user signs) | No backend; users bring their own wallet. Public rooms. | | `app-trusted+wallet` | Yes | Yes (user signs) | You run a backend and want verified wallet identity. **Most common.** | | `app-trusted` | Yes | No | Zero-signature: your backend is the sole identity source. Self-serve: pick it in the portal's auth-mode selector. | ## wallet-only No backend. The user connects a wallet and signs a challenge; the iframe handles the flow. Just mount: ```ts const chat = new CherryEmbed({ appId: 'YOUR_EMBED_ID', container: '#cherry-chat', roomId: 'YOUR_ROOM_ID', mode: 'single', }); await chat.mount(); ``` ## app-trusted + wallet Your backend mints a short-lived token for the connected wallet, and the user signs a challenge to prove they own it. This is the default for the public API. ### 1. Mint tokens on your backend The token is a JWT signed with your embed's **app secret** (from the portal), with the user's wallet as `sub` and your `appId` as `app_id`. Keep the secret server-side only. ```ts // POST /api/cherry-embed-token - your server import jwt from 'jsonwebtoken'; import { randomUUID } from 'node:crypto'; const token = jwt.sign( { sub: walletAddress, app_id: process.env.CHERRY_APP_ID }, process.env.CHERRY_APP_SECRET, { algorithm: 'HS256', expiresIn: '5m', jwtid: randomUUID() }, ); ``` > **Note:** A complete, runnable token server lives in the public [`chat-embed-sdk`](https://github.com/cherrydotfun/chat-embed-sdk/tree/main/example/app-trusted+wallet) repo, under `example/app-trusted+wallet` (its `server.js`). ### 2. Mount with the token + a sign handler Fetch a token, then mount. Register `signChallengeHandler` **before** `mount()`: the iframe calls it to get the user's signature. ```ts const provider = window.phantom?.solana; const { publicKey } = await provider.connect(); const walletAddress = publicKey.toString(); const { token } = await fetch('/api/cherry-embed-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ walletAddress }), }).then((r) => r.json()); const chat = new CherryEmbed({ appId: 'YOUR_EMBED_ID', container: '#cherry-chat', roomId: 'YOUR_ROOM_ID', token, walletAddress, signChallengeHandler: async (message) => { // message: Uint8Array. Sign the bytes as-is, don't re-encode them const { signature } = await provider.signMessage(message, 'utf8'); return signature; // Uint8Array }, }); await chat.mount(); ``` ### 3. Session renewal is automatic The embed token is short-lived (~5 min) by design: mint it fresh right before `mount()`, don't cache it. After the initial exchange the iframe holds a short-lived Cherry session and silently re-establishes it from a rotating refresh token whenever the page (re)loads, so there's nothing for you to refresh on a timer. If you ever need to force a fresh exchange (e.g. the user switches accounts in your app), mint a new token and call `chat.setToken(token)`. ## app-trusted (zero-signature) The same backend-minted token as above, but with no wallet signature at all: your backend is the sole source of identity truth, and the user never connects a wallet or signs anything on your page. This mode is self-serve: pick it in the portal's auth-mode selector when you configure the embed. ### How it works 1. Your backend authenticates the user through its own login session. 2. It mints the exact same JWT as in `app-trusted+wallet`: `sub` is the user's wallet, `app_id` is your embed ID, HS256 with the app secret, ~5 min expiry, unique `jwtid`. 3. The page fetches the token and mounts with it. No `walletAddress`, no `signChallengeHandler`, no wallet UI: the token's `sub` claim carries the identity. ```ts // Your backend derives the wallet from ITS OWN session. Never accept a // wallet address from the request body: with no signature to check, a // body-supplied address would let any client impersonate any wallet. const { token } = await fetch('/api/cherry-embed-token', { method: 'POST' }) .then((r) => r.json()); const chat = new CherryEmbed({ appId: 'YOUR_EMBED_ID', container: '#cherry-chat', roomId: 'YOUR_ROOM_ID', token, }); await chat.mount(); ``` ### The trust model In the wallet-backed modes the user proves wallet ownership with an Ed25519 signature that Cherry verifies. Here Cherry only verifies that the token was signed with your app secret; whatever `sub` your backend asserts is accepted as-is. Cherry cannot tell a real wallet owner from an impersonated one, which has two consequences: - `sub` must come only from your authenticated session, never from client input (request body, query, headers you don't control). - The mode runs with server-side restrictions, below. ### Server-side restrictions - **Allowed rooms are mandatory and fail-closed.** The chat works only in the rooms allowed for your embed; any other room returns `403`. - **Message rate limits** apply (about 20 messages per minute per user by default); exceeding them returns `429`. - **Moderation from the embed is off by default**, governed by the embed's server-side policy. - **Embed sessions live up to 15 minutes** and refresh automatically, so there is nothing to refresh on your side. Treat `403` and `429` responses from these rules as policy, not as bugs in your integration or a broken token. > **Note:** Turn this mode on yourself: open the embed in the portal and pick **App-trusted (zero-signature)** in the auth-mode selector. Make sure the rooms you need are allowed for the embed. ## Security notes - The **app secret** signs tokens: keep it on your server, never ship it to the browser. - Tokens are short-lived by design; mint them fresh right before `mount()`. Session renewal afterwards is automatic. - `walletConnectRequested` fires when a user clicks "connect" in a preview state: re-arm auth by calling `setToken()` and `setWalletAddress()`. (Wallet-backed modes only; it never fires in pure `app-trusted`.) ## Next steps - [Display modes](https://portal.cherry.fun/docs/embed/display-modes.md) · [SDK API reference](https://portal.cherry.fun/docs/embed/api-reference.md) - Full walkthrough: [Guides › Authenticated chat](https://portal.cherry.fun/docs/guides/authenticated-chat.md) --- # Display modes Two independent axes control how the widget appears: **mode** (how rooms are presented) and **position** (how it mounts on the page). ## Room modes Set with `mode`. ### `single` (default) Locks the embed to one room (`roomId`). No room switcher: ideal for a support widget or a single community room. ```ts new CherryEmbed({ appId: 'YOUR_EMBED_ID', container: '#cherry-chat', roomId: 'YOUR_ROOM_ID', mode: 'single', }); ``` > **Warning:** Only `single` is available today. `list` and `external-controlled` (and the `roomChanged` event) are documented ahead of runtime support; selecting them currently behaves like `single`. ### `list` When available, `list` will render Cherry's room-list UI so the user picks among the rooms they belong to (omit `roomId`). Not wired up yet: currently behaves like `single`. ```ts new CherryEmbed({ appId: 'YOUR_EMBED_ID', container: '#cherry-chat', mode: 'list', }); ``` ### `external-controlled` When available, your app will drive navigation: mount once, then call `setRoom()` whenever your UI changes, e.g. the user opens a different match, order, or channel, and listen for `roomChanged`. Not wired up yet: currently behaves like `single`, and `roomChanged` does not fire. ```ts const chat = new CherryEmbed({ appId: 'YOUR_EMBED_ID', container: '#cherry-chat', mode: 'external-controlled', }); await chat.mount(); // later: switch rooms from your own UI chat.setRoom('room_abc123'); chat.on('roomChanged', ({ roomId }) => { console.log('active room is now', roomId); }); ``` > **Tip:** Pair `external-controlled` with the [Cherry API](https://portal.cherry.fun/docs/api/rooms.md): create a room on your backend, then call `setRoom(newRoomId)` to drop the user straight into it. ## Position > **Tip:** Want to see the modes side by side? Toggle floating vs. inline live in the [Cherry embed builder](https://cherry.fun/chat-embed-example/), no signup or backend required. Set with `position`. - **`inline`** (default): mounts into your `container` element and flows with your layout. - **`floating-right` / `floating-left`:** pins the chat panel to a viewport corner. Omit `container`. Use `collapsed: true` to start hidden (the widget doesn't render its own launcher button), and open it from your own UI via `chat.show()` / `chat.toggle()`. ```ts const chat = new CherryEmbed({ appId: 'YOUR_EMBED_ID', roomId: 'YOUR_ROOM_ID', position: 'floating-right', collapsed: true, }); await chat.mount(); // collapsed starts hidden: wire your own button to open it document.querySelector('#open-chat')?.addEventListener('click', () => chat.toggle()); ``` Toggle a floating widget programmatically with `chat.show()`, `chat.hide()`, and `chat.toggle()`. ## Next steps - [Theming](https://portal.cherry.fun/docs/embed/theming.md) · [SDK API reference](https://portal.cherry.fun/docs/embed/api-reference.md) --- # Theming The widget is fully themeable to match your brand: colors, typography, and message styling. Pass a `theme` object at mount, or update it live with `setTheme()`. ```ts new CherryEmbed({ appId: 'YOUR_EMBED_ID', container: '#cherry-chat', roomId: 'YOUR_ROOM_ID', theme: { mode: 'dark', primaryColor: '#FF5BA8', accentColor: '#7C5CFF', fontFamily: 'Inter', fontSize: 'md', }, }); ``` Every field is optional: set only what you want to override; the rest fall back to sensible defaults derived from `mode` and `primaryColor`. > **Tip:** Want to see these colors change in real time? The [live embed builder](https://cherry.fun/chat-embed-example/) lets you tweak presets, brand hue, and per-side accents with no signup, then copies a ready-to-paste integration snippet. ## Updating the theme at runtime ```ts chat.setTheme({ primaryColor: '#22C55E' }); // merges with the current theme chat.resetTheme(); // clear back to defaults before applying a full new theme ``` > **Note:** When switching between full themes (e.g. a light/dark toggle), call `resetTheme()` first so stale derived colors don't leak into the new theme. ## Theme reference `mode`, `fontSize`, and the colors below make up the `EmbedTheme` type. Colors accept any CSS color string (hex, rgb, rgba). ### Base | Field | Type | Notes | |---|---|---| | `mode` | `'dark' \| 'light'` | Base variant; defaults the rest of the palette. | | `primaryColor` | `string` | Primary brand color. | | `accentColor` | `string` | Secondary/accent color. | | `backgroundColor` | `string` | Widget background. | | `surfaceColor` | `string` | Cards / surfaces. | | `borderColor` | `string` | Borders / dividers. | ### Text | Field | Notes | |---|---| | `textColor` | Primary text. | | `textSecondaryColor` | Muted / secondary text. | | `linkColor` | Hyperlinks. | | `linkColorOwn` | Hyperlinks inside your own messages. | ### Message bubbles | Field | Notes | |---|---| | `incomingBubbleColor` | Incoming bubble fill. | | `incomingBubbleBorderColor` | Incoming bubble border. | | `ownBubbleColor` | Your own bubble fill (defaults to a primary→accent gradient). | | `ownBubbleTextColor` | Your own bubble text. | ### Header & input | Field | Notes | |---|---| | `headerColor` / `headerTextColor` | Header bar background / text. | | `inputColor` / `inputTextColor` | Composer background / text. | | `sendButtonColor` / `sendButtonTextColor` | Send button fill / text. | ### Accents & chrome | Field | Notes | |---|---| | `messageOwnAccentColor` / `messageOwnAccentSoftColor` | Reaction chips on your messages (active / passive). | | `messageOtherAccentColor` / `messageOtherAccentSoftColor` | Reaction chips on others' messages. | | `ownEmbedBgColor` / `otherEmbedBgColor` | Rich embed card backgrounds. | | `messageActionsColor` / `messageActionsTextColor` | Hover message-actions menu. | | `tooltipColor` / `tooltipTextColor` | Tooltips. | | `emojiPickerColor` | Emoji picker surface. | | `iconButtonColor` / `iconButtonHoverColor` | Icon buttons. | | `avatarHoverColor` | Avatar hover ring. | | `loaderColor` | Loading spinner. | | `roleBadgeColor` | Role badge tint. | | `modalOverlayColor` | Modal backdrop. | | `dangerColor` | Destructive actions. | ### Typography | Field | Type | Notes | |---|---|---| | `fontFamily` | `string` | Font name, e.g. `'Inter'`, `'Space Grotesk'`, `'system-ui'`. | | `fontSize` | `'sm' \| 'md' \| 'lg'` | Base text size. | > **Tip:** The interactive embed editor in the [dashboard](https://portal.cherry.fun/dashboard) previews these live and emits a ready-to-paste config, the fastest way to dial in a theme. ## Next steps - [SDK API reference](https://portal.cherry.fun/docs/embed/api-reference.md), `setTheme`, events, and the full method surface. --- # Moderation Every embed ships with a built-in moderation layer you configure in the editor's **Moderation** tab (open the embed in the [dashboard](https://portal.cherry.fun/dashboard), then **Moderation**). The rules are enforced by Cherry's servers on the message send path, not in the browser, so a visitor cannot bypass them by editing client code or calling the API directly. Moderation is off by default: a new embed allows everything. Turn on only the rules you need, then **Save** (moderation is stored with the rest of the embed's settings). > **Note:** This is different from the read-only **Embed policy** card in the editor. The Moderation tab is your own self-serve setting. The separate Embed policy (whether embed users can perform moderation actions like kick, ban, or mute, plus rate and message-length limits) is set by Cherry and shown read-only. See [Authentication](https://portal.cherry.fun/docs/embed/authentication.md) for the policy limits. ## Minimum wallet age Require a visitor's Solana wallet to be at least a given number of days old, measured from its first on-chain transaction, before they can post. It is a low-friction filter against throwaway wallets and freshly minted bots. - Range: **0 to 365 days**. `0` turns the check off (the default). - A wallet with no transactions has an age of zero, so any non-zero threshold blocks it. ## Blocked words A list of words that silently hide a message. When a posted message contains a blocked word, it is shadow-dropped: the **sender still sees their own message**, but no one else receives it. There is no visible rejection, so a spammer gets no signal to adjust and retry. - Matching is **case-insensitive** (words are stored lowercased and de-duplicated). - Up to **200 words**, each **1 to 100 characters**. ## Links Choose how links posted in the chat are handled: | Policy | Behavior | |---|---| | **Allow all** (default) | Links are never touched. | | **Allowlist** | Only links whose host is in your allowed-domains list pass; every other link is blocked. An empty allowlist blocks every link. | | **Block all** | Every link is blocked. | When you pick **Allowlist**, add the domains you trust: - Enter **bare hosts** like `example.com`, lowercased, with no scheme, port, or path (`https://example.com/path` is rejected). - Each host is a valid domain (labels of `a-z`, `0-9`, and `-`), up to 253 characters. - Up to **100 domains**. ## Images and GIFs Two independent toggles, both **on** by default. Turn **Images** off to block image messages, or **GIFs** off to block GIFs. They are separate switches, so you can allow one and block the other. ## Limits at a glance | Rule | Limit | |---|---| | Minimum wallet age | 0 to 365 days | | Blocked words | up to 200, each 1 to 100 characters | | Allowlist domains | up to 100 bare hosts, each up to 253 characters | The portal rejects values outside these bounds before saving, and the server enforces the same limits, so an out-of-range value never reaches a live embed. ## Next steps - [Configuration](https://portal.cherry.fun/docs/embed/configuration.md) · [Authentication](https://portal.cherry.fun/docs/embed/authentication.md) · [Theming](https://portal.cherry.fun/docs/embed/theming.md) - [SDK API reference](https://portal.cherry.fun/docs/embed/api-reference.md) --- # React Native & Flutter The Chat Embed SDK is browser-only: it creates an `