# 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](https://cherry.fun/learn/features/mini-apps/) 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](https://github.com/cherrydotfun/miniapp-sdk) 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

```bash
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 path | What it holds | Solana dependency |
|---|---|---|
| `@cherrydotfun/miniapp-sdk` | core client, bridge, host detection, launch-token verification | none |
| `@cherrydotfun/miniapp-sdk/react` | provider and hooks | none |
| `@cherrydotfun/miniapp-sdk/solana` | `CherryWalletAdapter` | `@solana/web3.js`, `@solana/wallet-adapter-base` |
| `@cherrydotfun/miniapp-sdk/kit` | `createCherrySigner` | none (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.

```tsx
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:

```tsx
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:

```tsx
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:

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

| Permission | What the app may do |
|---|---|
| `wallet:connect` | read the signed-in public key |
| `inline:render` | render as a card inside a message; required for sharing results |
| `inline:eager` | mount 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](https://t.me/cherrydotfun). Keys, embeds and bots for the same project live in [Cherry Portal](https://cherry.fun/learn/guides/cherry-portal-for-developers/).

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.

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

[Tramplin.io on Cherry](https://chat.cherry.fun/@tramplin)

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](https://github.com/cherrydotfun/miniapp-sdk).

## Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| Blank frame in Cherry web | `X-Frame-Options` or a CSP without `frame-ancestors` | Send `frame-ancestors 'self' https://chat.cherry.fun` on HTML responses |
| `isInsideCherry()` false in Cherry web | the SPA router stripped `cherry_embed=1` before detection ran | Call it at module load, before routing |
| Embedded UI shows in a wallet browser | fallback detection matched `ReactNativeWebView` | Pass `{ strict: true }` to the detection APIs and the provider |
| Two connect buttons inside Cherry | the standalone wallet modal still mounts | Return `null` from that component when `isEmbedded` |
| The handshake times out after 10 seconds | the app never ran inside a Cherry container, or the origin is not registered | Launch it from a room; check the origin on the registration |
| `CherryWalletAdapter` fails to import | imported from the package root | Import it from the `/solana` entry point |
| `Buffer is not defined` | Node globals in browser code | Use `btoa`/`atob`, or add a Buffer polyfill to your bundler |

## Related guides

- [Add chat to your dApp](https://cherry.fun/learn/guides/add-chat-to-your-dapp/) and the [embeddable chat widget](https://cherry.fun/learn/features/embeddable-chat/): a Cherry room inside your product.
- [Distribute a Solana dApp](https://cherry.fun/learn/guides/distribute-a-solana-dapp/), for after registration.
- [Chat app for Solana Seeker](https://cherry.fun/learn/guides/chat-app-for-solana-seeker/), for the Seeker audience.
- [Chat for web3 games](https://cherry.fun/learn/guides/chat-for-web3-games/), if the app is a game.

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

- [Cherry Mini App SDK on GitHub](https://github.com/cherrydotfun/miniapp-sdk)
- [@cherrydotfun/miniapp-sdk on npm](https://www.npmjs.com/package/@cherrydotfun/miniapp-sdk)
- [Cherry developer docs: integration skills for AI agents](https://portal.cherry.fun/docs/ai/for-ai-agents)
- [SOL Miner on Cherry](https://chat.cherry.fun/@solminer)
- [Tramplin.io on Cherry](https://chat.cherry.fun/@tramplin)
- [Farcaster Mini Apps: getting started](https://miniapps.farcaster.xyz/docs/getting-started)

