Embed chat in dApps: the wallet-native chat widget

Embed chat in a dApp or website with Cherry's Chat Embed SDK: three wallet auth modes, seed-color theming, per-embed moderation, live in five minutes.

The Chat Embed SDK is the fastest way to embed chat in dApps and websites whose users already have wallets. Cherry (cherry.fun) is a wallet-to-wallet messenger and community app for crypto: people sign in with a wallet, DM any wallet address, and join token-gated, NFT-gated, and paid group chats. The embed puts one Cherry chat room inside your product as an iframe served from embed.cherry.fun, with wallet sign-in, theming, realtime delivery, unread badges, and moderation handled by Cherry. It is for developers who want a live room on the page without writing a chat backend. As of 10 September 2026 the package is @cherrydotfun/chat-embed-sdk 0.1.7 on npm, and the portal quickstart puts a public room live in about five minutes.

What the Chat Embed SDK does on Cherry

One package, one iframe, one room per embed. The facts as of 10 September 2026:

  • Install from npm (ESM, CommonJS, and TypeScript types included) or from a pinned jsDelivr <script> tag that exposes window.CherryEmbedSDK.
  • Mount inline on any element, or float the panel left or right with a built-in launcher bubble and unread badge (SDK 0.1.7 or newer).
  • Three auth modes: wallet-only, app-trusted+wallet, and zero-signature app-trusted.
  • Theming from two to four seed colors into about 60 CSS variables with contrast floors; transparency and blur included.
  • Host identity: your app’s display names and avatars in place of .sol names and shortened addresses.
  • Moderation rules per embed, enforced on the server.
  • Every embed is also a community inside Cherry, so your users are reachable in the Cherry web and mobile apps too.

How it works

The SDK creates an iframe on your page, the two talk over postMessage, and everything else happens on Cherry’s side:

  1. Create the embed in the portal. Sign in at portal.cherry.fun with a wallet, create a project, and make an embed. Copy its ID (your appId) and add your origin under Allowed origins.
  2. Load the SDK and mount. Pass appId, a roomId, and either a container (inline) or a position (floating). mount() resolves once the iframe reports ready.
  3. Sign the visitor in. In the wallet-backed modes the iframe fetches a challenge, the wallet signs it, and Cherry verifies the Ed25519 signature before issuing a session. After that the iframe re-establishes its session from a rotating refresh token on every page load.
  4. Messages flow. Realtime delivery, reactions, images and GIFs, and mentions run inside the iframe; the SDK emits unreadState, authStateChange, and message events to your page.

The three auth modes

ModeBackendWallet signatureWhat Cherry verifiesUse when
wallet-onlynoneyesthe Ed25519 signature on a challengepublic rooms, no backend, the five-minute quickstart
app-trusted+walletyesyesyour HS256 JWT (sub = wallet, app_id, about 5 minutes, unique jwtid) plus the signaturechat identity tied to your logged-in users; the most common mode for logged-in products
app-trustedyesnoyour JWT alone; sub comes from your sessioninternal apps and trusted partners; runs with server-side restrictions

The token for the two backend modes is minted on your server with the app secret from the embed’s settings, and the secret never reaches the browser:

// POST /api/cherry-embed-token, on 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() },
);
// On the page: mount with the token and a sign handler
import { CherryEmbed } from '@cherrydotfun/chat-embed-sdk';

const chat = new CherryEmbed({
  appId: 'YOUR_EMBED_ID',
  container: '#cherry-chat',
  roomId: 'YOUR_ROOM_ID',
  token,
  walletAddress,
  signChallengeHandler: async (message) => {
    const { signature } = await provider.signMessage(message, 'utf8');
    return signature; // Uint8Array, signed as-is
  },
});

await chat.mount();

Zero-signature app-trusted mode trusts whatever sub your backend asserts, so derive the wallet from your own authenticated session, never from the request body. Because the app secret alone can mint a session for any wallet, the mode runs under server-side restrictions: a fail-closed room allowlist, about 20 messages per minute per user, moderation actions from the iframe off by default, and embed sessions of up to 15 minutes that refresh on their own. A 403 or 429 from those rules is policy, not a broken integration.

Display, launcher, and theme

Inline embeds size themselves to the container you give them. Floating embeds omit the container, pin to a viewport corner, and can start collapsed. Set chatBubble: true and the SDK renders the launcher and hangs the unread badge on it ('dot', 'count' capped at 99+, or 'off'); counters accrue while the chat is hidden or collapsed.

<div id="cherry-chat" style="height: 600px"></div>

<script src="https://cdn.jsdelivr.net/npm/@cherrydotfun/chat-embed-sdk@0.1.7/dist/index.global.js"></script>
<script>
  new window.CherryEmbedSDK.CherryEmbed({
    appId: 'YOUR_EMBED_ID',
    roomId: 'YOUR_ROOM_ID',
    position: 'floating-right',
    collapsed: true,
    chatBubble: true,
    chatBubbleBadge: 'count',
    theme: { mode: 'dark', primaryColor: '#9162FF', backgroundColor: '#0D0416', accentColor: '#FF57C1' },
  }).mount();
</script>

The theme engine takes seeds, not a hand-built palette. primaryColor drives your own bubble and the send button, backgroundColor drives surfaces and text polarity, accentColor drives links and mentions, and an optional incomingBubbleColor recolors received messages. From those it derives roughly 60 variables and enforces contrast floors: body and incoming text at 4.5:1, the send icon at 3:1. An alpha backgroundColor plus backgroundBlur (0 to 40 px on the iframe element) frosts the panel. The live builder offers presets and a “Show the integration snippet” button that emits the config, and the portal’s embed editor previews a theme live and emits it too. A separate layout object toggles the header, its title, the member count, and the composer (showInput: false makes a read-only feed).

Your users’ names

By default the widget labels people by wallet identity: a .sol domain when they own one, otherwise a shortened address. Flip Who your users appear as on the embed and the widget asks for your names instead, through a resolveUsers callback on the page (up to 50 wallets per call) or through a profile endpoint on your backend that the iframe calls directly (POST {url}/resolve, GET {url}/search, CORS allowed for https://embed.cherry.fun, no cookies, an optional bearer token via setIdentityToken()). searchUsers points @-autocomplete at your directory. Cherry stores none of the names, the wallet remains the author of every message, and every name is sanitized before it renders (48 characters, lookalike characters stripped, http(s) avatars only).

Moderation per embed

The Moderation tab in the embed editor is yours; the rules run on Cherry’s servers, so a visitor editing client code cannot skip them. All rules are off on a new embed.

RuleWhat it doesLimits (10 September 2026)
Minimum wallet ageBlocks posts from wallets younger than N days, measured from the first on-chain transaction; a wallet with no transactions has age zero0 to 365 days
Blocked wordsShadow-drops a message that contains one: the sender sees it, nobody else does, so a spammer gets no signal to retryup to 200 words, 1 to 100 characters each, case-insensitive
LinksAllow all, allowlist, or block all; an empty allowlist blocks every linkup to 100 bare hosts
Images and GIFsTwo independent toggleson by default

A separate, read-only Embed policy card shows the limits Cherry sets for embed sessions: whether kick, ban, and mute work from the iframe, message length, and message rates.

What is different from a general chat SDK

The difference is who the identity belongs to. In a general chat SDK your backend invents a user ID and mints a token for it; in Cherry the wallet is the identity. Here’s how the identity models compare, as of 10 September 2026:

SendbirdStream ChatXMTPCherry Chat Embed
Identitya user_id your backend creates through Sendbird’s server APIa user ID you define, validated against your own systemany public DID such as an ENS name or a Bluesky handle, or a device-bound passkeythe wallet
Who proves ityour server issues an access or session tokenyour server signs a JWT with the API secret; a client cannot mint onethe user’s keys, client-sidethe wallet signs a challenge in the iframe; optionally your backend also signs a JWT
Backend requiredyesyesno, but you build the appno in wallet-only; yes in the two app-trusted modes
What you shiptheir UI kit or your own UItheir UI components or your ownyour own UI on the protocola hosted iframe widget, themed
Where users also appearinside your appinside your appany XMTP clientyour page and the Cherry app, because the embed is a Cherry community

Sendbird and Stream fit products where identity is an email or phone login. XMTP is an end-to-end encrypted protocol: it gives you the transport and leaves the UI, the rooms, and the moderation to you.

The Cherry room is the same kind of community people join at chat.cherry.fun, alongside token-gated and paid rooms, and its members can DM each other wallet to wallet in the Cherry app. Builders who would rather run inside Cherry use mini apps instead; both share the same wallet identity.

Who uses it and for what

Three common integration shapes:

A DEX or launchpad front end with a room next to the chart. Wallet-only mode, no backend: the visitor already connected to trade signs one challenge and is in the room. A seven-day minimum wallet age keeps fresh-wallet spam out, and a link allowlist of your own domains removes phishing links.

A game or marketplace with a room per match or order. Your backend calls POST https://api.cherry.fun/api/v1/apps/groups with a title and initialMembers, stores the returned roomId with the match, and mounts a single-mode embed with that id on the match page. Use app-trusted+wallet so chat identity equals the logged-in player, and delete the room when the match ends.

A wallet or portfolio app with its own accounts. Users are already logged in, so app-trusted removes the wallet layer from the chat: the backend mints the token from its session and host identity shows the app’s usernames and avatars. On mobile the same flow runs in a WebView host page with no wallet plumbing at all.

Limits and honest caveats

  • One room per embed. Room switching (list, external-controlled, setRoom()) is documented ahead of runtime support and behaves like single today, so a multi-room product mounts one embed per page.
  • Group rooms are not end-to-end encrypted, by design, so moderation rules and bots can read what they moderate. Wallet-to-wallet DMs in the Cherry app are end-to-end encrypted; the embed renders a room.
  • A thin “Powered by Cherry.fun” band sits under the composer, and the built-in launcher’s labels are English only (Open chat / Close chat). Draw your own launcher if you need localized text.
  • Sign-in inside the widget is a Solana wallet signature (Ed25519); the embed authentication docs cover each mode’s wallet flow.
  • The app secret is the whole trust model of the backend modes. Keep it server-side and rotate it from the embed settings in the portal.
  • Server-to-server moderation through the Apps API is a separate path from the embed’s own rules.
  • There is no native mobile SDK. Pointing a WebView straight at embed.cherry.fun fails because the bridge requires a parent frame, and a native sign request times out after 60 seconds.
  • An embed loads only on allow-listed origins and only while it is enabled; anything else returns 401, as of 10 September 2026.

How to embed chat in dApps and websites

  1. Open https://portal.cherry.fun , press Connect Wallet, approve the SIWS signature, and press Create Project.
  2. In the project, open Chat embeds, press New embed, and name it. The name is the chat room’s visible title, so choose what visitors see.
  3. Copy the embed ID, add http://localhost:3000 (or your dev origin) and your production origin under Allowed origins, and confirm the embed is enabled.
  4. Leave the auth mode on wallet-only for a public room, or pick app-trusted+wallet and copy the app secret to your server’s environment.
  5. Install the SDK with npm install @cherrydotfun/chat-embed-sdk, or paste the jsDelivr script tag pinned to @0.1.7.
  6. Add <div id="cherry-chat" style="height: 600px"></div> and mount:
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();
  1. Theme it in the live builder or the portal editor and paste the emitted config over the theme block.
  2. Open the embed’s Moderation tab, set a minimum wallet age and a link policy, and press Save.
  3. Open the same room in the Cherry web app and invite your first members.

The portal quickstart walks the same flow.

FAQ

Can I add chat to a website without a backend?

Yes. In wallet-only mode the embed needs an appId and a public room id. Visitors connect their own wallet and sign a challenge inside the iframe; Cherry verifies the signature and runs the session. There is no token endpoint to build.

How does wallet auth work in the chat widget?

The iframe fetches a challenge from Cherry, the visitor’s wallet signs it, and Cherry checks the Ed25519 signature before issuing a session. In app-trusted+wallet mode your backend also mints a five-minute HS256 JWT for the wallet, and both proofs are verified.

Can the widget show my app’s usernames and avatars instead of wallet addresses?

Yes. Turn on the per-embed switch in the portal, then answer with resolveUsers from your page or point the embed at a profile endpoint on your backend. Names are a visual overlay: the wallet stays the author of every message and Cherry stores none of the names.

Is the Cherry chat widget white-label?

Mostly. Two to four seed colors drive about 60 CSS variables, and every slot can be overridden, but a thin “Powered by Cherry.fun” band sits under the composer and the built-in launcher’s labels are English only.

Does the Chat Embed SDK work in React Native or Flutter?

Through a WebView. The SDK is browser-only, so you load a small host page in react-native-webview or webview_flutter and bridge the wallet signature to the native layer: Mobile Wallet Adapter on Android, a deeplink wallet on iOS. The SDK repo ships runnable examples for both.

What stops spam in an embedded chat?

Per-embed rules enforced on Cherry’s servers: a minimum wallet age of up to 365 days, up to 200 blocked words that shadow-drop a message, a link policy (allow all, allowlist up to 100 hosts, or block all), and image and GIF toggles. Cherry also sets message-rate and message-length limits for every embed.

Do my users need a Cherry account?

No. Their wallet is the account. Signing the challenge inside the widget signs them in, and because every embed is also a community inside Cherry, they can open the same room in the Cherry web or mobile app later.

Is the Chat Embed SDK free?

Yes. The Chat Embed SDK, the Cherry API, bots and Cherry Portal are all free. Cherry’s only charges are a share of paid-community payments, the paid-DM fee and a referral share on in-chat swaps, and none of them apply to an embed.

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.