How to build a Solana mini app for Cherry chats

Turn a Solana dApp into a mini app that runs inside Cherry chats: the SDK, the wallet bridge, the CSP header, launch-token checks, and registration.

This guide is how to build a Solana mini app that opens inside a chat room, starting from a dApp you already ship. Cherry is a wallet-to-wallet messenger and community app for crypto: you sign in with a wallet, DM any address, and join token-gated, NFT-gated, and paid group chats. A mini app is your web app, loaded by Cherry in a WebView on mobile and an iframe on web, handed the signed-in wallet, the room it was opened from, and a launch token your backend can verify.

Before you start

You need four things in place:

  • A web app served over HTTPS from a domain you control, as a static build or an SPA.
  • Solana wiring you can identify: @solana/web3.js with wallet-adapter, or @solana/kit.
  • Control of the HTTP response headers on whatever serves your HTML. Step 5 depends on it.
  • A wallet signed in to Cherry at https://chat.cherry.fun for testing.

A backend is optional: add one if you keep state per player. Step 6 verifies the token there.

The SDK repository ships a runnable reference app: Vite and React 19, wired for both Solana paths, plus a small server with a token-verification endpoint and a server-rendered blink route. The SDK is free, like the rest of Cherry’s developer tools.

Step 1: Install the SDK

npm install @cherrydotfun/miniapp-sdk
npm install @solana/wallet-adapter-base @solana/web3.js   # web3.js projects
npm install @solana/signers                               # @solana/kit projects, types only (optional)

The package is @cherrydotfun/miniapp-sdk, version 0.1.21 on npm as of 11 September 2026. It has four entry points, so a kit project never pulls in web3.js:

Import pathWhat it holdsSolana dependency
@cherrydotfun/miniapp-sdkcore client, bridge, host detection, launch-token verificationnone
@cherrydotfun/miniapp-sdk/reactprovider and hooksnone
@cherrydotfun/miniapp-sdk/solanaCherryWalletAdapter@solana/web3.js, @solana/wallet-adapter-base
@cherrydotfun/miniapp-sdk/kitcreateCherrySignernone (structural typing)

You know it worked when your bundler resolves @cherrydotfun/miniapp-sdk/react.

Step 2: Detect the host and branch the UI

Cherry marks its two containers differently: the mobile WebView injects window.__cherry = true before your page loads, and Cherry web appends cherry_embed=1 to the launch URL. Read those signals at module load, before any provider mounts.

import { CherryMiniAppProvider, useCherryEnvironment } from '@cherrydotfun/miniapp-sdk/react';

function App() {
  const { isEmbedded, platform } = useCherryEnvironment({ strict: true });
  if (!isEmbedded) return <StandaloneApp />;   // 'standalone'
  return (
    <CherryMiniAppProvider strict={true}>
      <EmbeddedApp />                           // 'webview' | 'iframe'
    </CherryMiniAppProvider>
  );
}

Without strict, the SDK also accepts two fallbacks kept for older Cherry builds, ReactNativeWebView and window.parent !== window. Wallet in-app browsers are the usual false positive, because ReactNativeWebView exists in any React Native WebView and any iframe has a parent. Turn the fallbacks off once your users are on builds that set the Cherry signals. Use the same flag to hide what Cherry already provides: your connect-wallet button, header, and splash screen.

You know it worked when the app in a plain browser tab reports standalone, and the same URL with ?cherry_embed=1 reports iframe.

Step 3: Swap in the Cherry wallet adapter

Inside Cherry the wallet is the one the user signed in with, reached over the bridge. For wallet-adapter projects, replace the wallet list and select the adapter by name:

import { CherryWalletAdapter } from '@cherrydotfun/miniapp-sdk/solana';
import { isInsideCherry } from '@cherrydotfun/miniapp-sdk';

const embedded = isInsideCherry({ strict: true });
const wallets = useMemo(
  () => (embedded ? [new CherryWalletAdapter()] : [new PhantomWalletAdapter(), new SolflareWalletAdapter()]),
  [embedded],
);
// then select the adapter whose name === 'Cherry' and pass autoConnect={embedded}

For @solana/kit projects there is no adapter to register. Build a TransactionSigner instead:

import { createCherrySigner } from '@cherrydotfun/miniapp-sdk/kit';
const signer = createCherrySigner(useCherryApp());
const [signed] = await signer.signTransactions([{ messageBytes, signatures: {} }]);

Either path goes through the same bridge methods, wallet.signMessage, wallet.signTransaction and wallet.signAndSendTransaction, and signAllTransactions signs several transactions in a single batch.

You know it worked when a transaction inside Cherry raises Cherry’s signing sheet and no connect-wallet modal appears first.

Step 4: Read the user and the room

useCherryMiniApp() returns { user, room, launchToken, isReady }. The room object carries id, title, and memberCount, enough for a per-room leaderboard. The handshake times out at 10,000 ms by default, and the client emits suspended, resumed, and walletDisconnected, so pause a running game when the user returns to the message list.

You know it worked when room.title matches the room you launched from and user.publicKey is populated before your first render after isReady.

Step 5: Let Cherry frame your app

On web, Cherry loads you in an iframe, so every HTML response from your server needs one header:

Content-Security-Policy: frame-ancestors 'self' https://chat.cherry.fun

Remove any X-Frame-Options: DENY or SAMEORIGIN you send. Your own API calls stay same-origin inside the iframe and need no CORS change.

You know it worked when the app renders inside Cherry web with no “Refused to frame” error in the console.

Step 6: Verify the launch token on your backend

The client receives an RS256 JWT signed by Cherry. Verify it before you trust any wallet address:

import { verifyLaunchToken } from '@cherrydotfun/miniapp-sdk';

const payload = await verifyLaunchToken(token, { expectedAppId: 'your-app-id' });
// jwksUrl defaults to https://chat.cherry.fun/.well-known/jwks.json
// payload.sub: wallet address
// payload.room_id: the room the app was opened from

The token expires five minutes after issue, so verify it on arrival rather than holding it for a session. For inline cards the token rides in the launch URL query string, so you can render the card server-side before the client mounts. Keep any shared snapshot inside the token’s signed params; raw query fields are forgeable.

You know it worked when the address your backend reads from payload.sub matches the one the client shows.

Step 7: Register the app and claim its room

A registration is what lets Cherry open your app from a room: a manifest, the origins you serve from, an icon, and a permission set. The permission set includes:

PermissionWhat the app may do
wallet:connectread the signed-in public key
inline:renderrender as a card inside a message; required for sharing results
inline:eagermount a non-interactive card without a tap

Registration goes through the Cherry team as of September 2026, and the team sets the manifest, allowed origins, icon, and permissions with you. Ask for a miniAppId in Cherry’s public Telegram group . Keys, embeds and bots for the same project live in Cherry Portal .

Ask for a public room with a handle too. That room is where your players talk, and it doubles as an indexable storefront page. SOL Miner’s room held 4,388 members and 4,993 messages on 11 September 2026; Tramplin.io, a third-party Solana staking app, 1,292 members the same day.

One registration reaches Cherry users on web, iOS, Android, and the Seeker build from the Solana dApp Store. The contrast with Farcaster is the launch surface: Farcaster’s docs describe mini apps that “can be discovered and used within Farcaster clients” and, among the next steps it lists for distribution, “make it sharable in feeds.” A Cherry mini app opens from a conversation and keeps a member list you can post into afterwards.

Build a Solana mini app with one prompt

The SDK package ships an AI skill, cherry-miniapp-integration, under skills/. Copy it to ~/.claude/skills/ for Claude Code or ~/.agents/skills/ for Codex, then say “Integrate Cherry Mini-App SDK into this project”. It reads the codebase and its wallet setup, asks what to hide when the app runs inside Cherry, then works through steps 2 to 6. Read the diff: it cannot register your app or set a header on your production host. The SDK and its example app are on GitHub .

Troubleshooting

SymptomCauseFix
Blank frame in Cherry webX-Frame-Options or a CSP without frame-ancestorsSend frame-ancestors 'self' https://chat.cherry.fun on HTML responses
isInsideCherry() false in Cherry webthe SPA router stripped cherry_embed=1 before detection ranCall it at module load, before routing
Embedded UI shows in a wallet browserfallback detection matched ReactNativeWebViewPass { strict: true } to the detection APIs and the provider
Two connect buttons inside Cherrythe standalone wallet modal still mountsReturn null from that component when isEmbedded
The handshake times out after 10 secondsthe app never ran inside a Cherry container, or the origin is not registeredLaunch it from a room; check the origin on the registration
CherryWalletAdapter fails to importimported from the package rootImport it from the /solana entry point
Buffer is not definedNode globals in browser codeUse btoa/atob, or add a Buffer polyfill to your bundler

FAQ

Is there a mini app SDK for Solana?

Yes. Cherry publishes @cherrydotfun/miniapp-sdk on npm (0.1.21 as of 11 September 2026) with React hooks, a CherryWalletAdapter for @solana/web3.js, and createCherrySigner for @solana/kit. Farcaster’s @farcaster/miniapp-sdk covers mini apps inside Farcaster clients instead.

Can I turn an existing dApp into a mini app without forking it?

Yes. The integration is additive: one environment check decides whether your app renders its standalone shell or the embedded one, and the Cherry wallet adapter replaces your wallet list only when the app is running inside Cherry.

Do I need a native app to reach Solana Seeker users?

No. A registered mini app opens inside Cherry on web, iOS, Android, and the Seeker build from the Solana dApp Store, so one web deployment covers all four surfaces.

Why does my mini app load on mobile but stay blank in Cherry web?

Cherry web loads the app in an iframe, so your server must send Content-Security-Policy with frame-ancestors 'self' https://chat.cherry.fun on every HTML response and must not send X-Frame-Options: DENY or SAMEORIGIN.

Which permissions does a mini app declare?

Permissions live in the app’s registration, which the Cherry team sets up with you. They include wallet:connect to read the signed-in wallet, inline:render to render as a card inside a message (required for sharing results), and inline:eager to mount a non-interactive card without a tap.

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.