@cherrydotfun/miniapp-sdk
v0.1.18
Published
SDK for building mini-apps embedded in Cherry messenger
Readme
@cherrydotfun/miniapp-sdk
SDK for building mini-apps embedded in Cherry messenger. Provides wallet integration, user/room context, and navigation — works in both WebView (mobile) and iframe (web).
Supports both @solana/web3.js (legacy wallet-adapter) and @solana/kit (modern TransactionSigner).
Install
npm install @cherrydotfun/miniapp-sdkPeer dependencies — install only what you need:
# For @solana/web3.js (legacy wallet-adapter)
npm install @solana/wallet-adapter-base @solana/web3.js
# For @solana/kit (modern)
npm install @solana/signers
# For React hooks
npm install reactPackage Exports
| Entry Point | Description | Solana Dependency |
|-------------|-------------|-------------------|
| @cherrydotfun/miniapp-sdk | Core client, bridge, env detection, token verification | None |
| @cherrydotfun/miniapp-sdk/react | React provider and hooks | None |
| @cherrydotfun/miniapp-sdk/solana | CherryWalletAdapter for wallet-adapter ecosystem | @solana/web3.js + @solana/wallet-adapter-base |
| @cherrydotfun/miniapp-sdk/kit | TransactionSigner for @solana/kit | None (structural typing) |
Quick Start — @solana/web3.js
import { CherryMiniAppProvider, useCherryMiniApp, useCherryWallet } from '@cherrydotfun/miniapp-sdk/react';
import { CherryWalletAdapter } from '@cherrydotfun/miniapp-sdk/solana';
// Drop-in for @solana/wallet-adapter-react
const wallets = [new CherryWalletAdapter()];
function MyGame() {
const { user, room, launchToken, isReady } = useCherryMiniApp();
const { publicKey, signTransaction, signAllTransactions, signMessage } = useCherryWallet();
if (!isReady) return <div>Loading...</div>;
return (
<div>
<p>Welcome, {user.displayName}!</p>
<p>Room: {room.title} ({room.memberCount} members)</p>
</div>
);
}Quick Start — @solana/kit
import { CherryMiniApp } from '@cherrydotfun/miniapp-sdk';
import { createCherrySigner } from '@cherrydotfun/miniapp-sdk/kit';
const cherry = new CherryMiniApp();
await cherry.init();
// TransactionSigner — use with @solana/kit transaction builders
const signer = createCherrySigner(cherry);
// Sign transactions
const [signed] = await signer.signTransactions([{ messageBytes, signatures: {} }]);
// Sign messages
const [signature] = await signer.signMessages([messageBytes]);Quick Start — React + Kit
import { CherryMiniAppProvider, useCherryApp } from '@cherrydotfun/miniapp-sdk/react';
import { createCherrySigner } from '@cherrydotfun/miniapp-sdk/kit';
function MyGame() {
const app = useCherryApp(); // CherryMiniApp instance
const handleSign = async () => {
const signer = createCherrySigner(app);
const [signed] = await signer.signTransactions([{ messageBytes, signatures: {} }]);
};
}Environment Detection
Check if running inside Cherry before initializing:
import { isInsideCherry, getCherryEnvironment } from '@cherrydotfun/miniapp-sdk';
if (isInsideCherry()) {
// Running inside Cherry — SDK will work
} else {
// Standalone — show regular wallet connect
}
const env = getCherryEnvironment();
// env.platform: 'webview' | 'iframe' | 'standalone'
// env.isEmbedded: booleanReact hook (no provider needed):
import { useCherryEnvironment } from '@cherrydotfun/miniapp-sdk/react';
function App() {
const { isEmbedded, platform } = useCherryEnvironment();
if (!isEmbedded) return <StandaloneApp />;
return <CherryMiniAppProvider><EmbeddedApp /></CherryMiniAppProvider>;
}Strict Mode
By default the SDK uses fallback heuristics for backward compatibility with older Cherry builds:
ReactNativeWebView— present in any React Native WebView, not just Cherry'swindow.parent !== window— true inside any iframe, not just Cherry's
This can cause false positives (e.g. wallet in-app browsers). Once your users are on a Cherry version that injects window.__cherry (WebView) or appends cherry_embed=1 (iframe), enable strict mode to rely only on Cherry-specific signals:
// Standalone functions
isInsideCherry({ strict: true });
getCherryEnvironment({ strict: true });
detectPlatform({ strict: true });
// React hook
const { isEmbedded } = useCherryEnvironment({ strict: true });
// Provider (passes strict to CherryMiniApp internally)
<CherryMiniAppProvider strict={true}>...</CherryMiniAppProvider>
// CherryMiniApp
const cherry = new CherryMiniApp({ strict: true });In strict mode only these signals are accepted:
- Mobile WebView:
window.__cherry === true(injected by Cherry before page load) - Web iframe:
cherry_embed=1query parameter (appended by Cherry host to the URL)
Web Embedding — CORS & CSP
When your mini-app runs inside the Cherry web client (iframe), the browser enforces standard cross-origin policies. Two things must be configured on your mini-app's server for the embed to work.
1. Allow Cherry to frame your app (frame-ancestors)
By default many frameworks set X-Frame-Options: SAMEORIGIN or a restrictive Content-Security-Policy, which blocks any iframe embedding. You need to explicitly allow Cherry's origin.
Option A — CSP header (recommended):
Content-Security-Policy: frame-ancestors 'self' https://chat.cherry.funOption B — X-Frame-Options (legacy, less flexible):
X-Frame-Options: ALLOW-FROM https://chat.cherry.fun
X-Frame-Options: ALLOW-FROMis ignored by Chrome/Firefox — prefer the CSP header.
Framework examples:
// Next.js — next.config.ts
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: "frame-ancestors 'self' https://chat.cherry.fun",
},
],
},
];
},
};// Express / Node.js
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy', "frame-ancestors 'self' https://chat.cherry.fun");
next();
});# Nginx
add_header Content-Security-Policy "frame-ancestors 'self' https://chat.cherry.fun" always;2. CORS on your backend API
When your mini-app frontend (served from https://yourgame.example) calls its own backend API, the browser sends the request with Origin: https://yourgame.example — same as outside the iframe, so no additional CORS config is needed if your API already allows that origin.
The one case that requires attention: if your API validates the Referer header or only allows requests when Origin exactly matches a whitelist, make sure https://yourgame.example is in that list. The Cherry host page is never the origin of your API calls — the iframe is its own browsing context.
If your mini-app calls any Cherry API endpoints directly (not via the SDK bridge), add the appropriate Access-Control-Allow-Origin on your side or proxy through your own backend.
Checklist
| | Requirement |
|---|---|
| ✅ | Content-Security-Policy: frame-ancestors … https://chat.cherry.fun on all HTML responses |
| ✅ | No X-Frame-Options: DENY or X-Frame-Options: SAMEORIGIN without override |
| ✅ | Backend API allows Origin: https://yourgame.example (usually already true) |
| ✅ | No Referer-based origin checks that would break inside an iframe |
Navigation
Open Cherry screens from your mini-app:
import { useCherryNavigate } from '@cherrydotfun/miniapp-sdk/react';
function MyComponent() {
const navigate = useCherryNavigate();
// Open user profile — accepts wallet address, domain, or @handle
await navigate.userProfile('alice.sol');
await navigate.userProfile('@alice');
// Open room — accepts roomId or @handle
await navigate.openRoom('@solminer');
await navigate.openRoom('roomId123');
}Sharing Results (Blinks)
Hand a read-only "result" snapshot to the Cherry host so the user can share
it into a DM or group as an interactive blink card. The host opens a recipient
picker with a preview; on send, the result carries the new message's unique
messageId.
import { useCherryShare } from '@cherrydotfun/miniapp-sdk/react';
function ShareButton() {
const share = useCherryShare();
const onClick = async () => {
const res = await share({
route: '/result', // route the receiver opens (default '/')
params: { score: 9000 }, // snapshot rendered read-only (≤ 4 KB JSON, depth ≤ 8)
height: 'medium', // 'compact' | 'medium' | 'tall'
caption: 'I scored 9000 points!', // optional caption shown by the card
});
if (res.shared) {
// res.roomId — where it was shared
// res.messageId — unique id of the created blink message (record it to
// correlate later callbacks / bot:blink_update events)
}
};
return <button onClick={onClick}>Share</button>;
}Vanilla JS: await cherry.share({ params: { score: 9000 } }).
How it works / guarantees
- A mini-app can only share itself. You never name the mini-app — the host
derives the identity from your current session's launch token.
route,params,heightandcaptionare the only things you control. - Read-only snapshot. Shared blinks are non-interactive (no callback
buttons) — there is no bot behind them to answer callbacks. The
paramsyou pass are the data the receiver's mini-app renders. - Authored by the user. The resulting message's sender is the user's wallet
(not a bot), with
metadata.senderType = 'user_share'. - The mini-app must declare the
inline:renderpermission to be shareable.
Launch Token (Backend Verification)
The SDK provides a JWT launch token signed by Cherry's server. Verify it on your backend:
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
});
// Always present:
// payload.sub — wallet address
// payload.room_id — room where app was opened
// Embed / fullscreen handshake token also carries:
// payload.user — { display_name, avatar_url }
// payload.room — { title, member_count }
// Inline / blink launch tokens also carry (all optional in the type):
// payload.message_id — unique id of the blink message this token is bound to
// payload.mini_app_id — the mini-app being rendered
// payload.route — route to open
// payload.params — the snapshot payload (signed → tamper-proof)
// payload.height — 'compact' | 'medium' | 'tall'
// payload.interactive — false for read-only shared snapshots
// payload.source — 'user_share' for user-shared snapshotsServer-Side Rendering (SSR)
The launch token rides in the launch URL's query string
(/inline?token=...), so it reaches your mini-app's server. That means you can
render a blink server-side and bind per-message state before the client
mounts — keyed by the token's message_id:
// Express-style handler for GET /inline?token=...
import { verifyLaunchToken } from '@cherrydotfun/miniapp-sdk';
app.get('/inline', async (req, res) => {
const payload = await verifyLaunchToken(String(req.query.token), {
expectedAppId: 'your-app-id',
});
const messageId = payload.message_id; // stable key for this blink
const params = payload.params ?? {}; // signed snapshot data
await bindStateFor(messageId, params); // your pre-render binding
res.send(renderBlinkHtml(params)); // SSR the card
});Keep snapshot data inside the signed token's
params— do not pass raw params as separate query fields, or they become forgeable. The token keeps them signed (RS256) and verified.
See example/server.ts for a runnable SSR endpoint.
Vanilla JS (No React)
import { CherryMiniApp } from '@cherrydotfun/miniapp-sdk';
const cherry = new CherryMiniApp();
await cherry.init();
cherry.user.publicKey; // wallet address
cherry.room.title; // room name
cherry.launchToken; // JWT for backend
const sig = await cherry.wallet.signMessage(new TextEncoder().encode('hello'));
const signed = await cherry.wallet.signAllTransactions([tx1, tx2, tx3]); // batch sign
await cherry.navigate.userProfile('alice.sol');
const res = await cherry.share({ params: { score: 9000 } }); // share a result snapshot
cherry.on('suspended', () => console.log('App suspended'));
cherry.on('resumed', () => console.log('App resumed'));API Reference
React Hooks
| Hook | Description |
|------|-------------|
| useCherryMiniApp() | { user, room, launchToken, isReady, error } |
| useCherryApp() | CherryMiniApp instance (for kit signer etc.) |
| useCherryWallet() | { publicKey, connected, signTransaction, signAllTransactions, signMessage, signAndSendTransaction } |
| useCherryNavigate() | { userProfile(id), openRoom(id) } |
| useCherryShare() | (opts?) => Promise<{ shared, roomId?, messageId? }> — share a read-only result snapshot |
| useCherryEnvironment(opts?) | { isEmbedded, platform } — no provider needed; pass { strict: true } to disable fallbacks |
CherryMiniApp (Core)
| Property/Method | Description |
|-----------------|-------------|
| new CherryMiniApp(opts?) | opts.initTimeout (ms, default 10 000); opts.strict (disable fallback detection) |
| init() | Wait for Cherry host handshake |
| user | { publicKey, displayName, avatarUrl } |
| room | { id, title, memberCount } |
| launchToken | JWT string for backend verification |
| wallet.signTransaction(tx) | Sign a transaction (returns Uint8Array) |
| wallet.signAllTransactions(txs) | Sign multiple transactions in a single batch (returns Uint8Array[]) |
| wallet.signMessage(msg) | Sign an arbitrary message |
| wallet.signAndSendTransaction(tx) | Sign and submit transaction |
| navigate.userProfile(id) | Open user profile (wallet/domain/@handle) |
| navigate.openRoom(id) | Open room (roomId/@handle) |
| share(opts?) | Share a read-only result snapshot → { shared, roomId?, messageId? } |
| on(event, handler) | Listen to suspended, resumed, walletDisconnected |
| destroy() | Cleanup listeners |
CherryWalletAdapter (solana/)
import { CherryWalletAdapter } from '@cherrydotfun/miniapp-sdk/solana';Drop-in BaseWalletAdapter for @solana/wallet-adapter-react. Handles connect, signTransaction, signAllTransactions, signMessage, sendTransaction.
createCherrySigner (kit/)
import { createCherrySigner } from '@cherrydotfun/miniapp-sdk/kit';Returns a TransactionSigner compatible with @solana/kit. Supports signTransactions and signMessages.
Bridge Protocol
The SDK communicates with Cherry via postMessage. The protocol is versioned (v2) and uses JWT launch tokens for authentication.
| Message | Direction | Description |
|---------|-----------|-------------|
| cherry:init | Host → App | Handshake with JWT token |
| cherry:ready | App → Host | App acknowledges init |
| cherry:request | App → Host | Wallet/navigate/share operations (e.g. host.share, wallet.signTransaction) |
| cherry:response | Host → App | Operation result |
| cherry:event | Host → App | Lifecycle events |
App→Host request methods include wallet.signMessage, wallet.signTransaction, wallet.signAndSendTransaction, navigate.userProfile, navigate.openRoom, and host.share. Prefer the typed hooks/methods above over calling the bridge directly.
Privy Integration
If your mini-app uses Privy for authentication or embedded wallets, you can use Cherry's launch token as a custom auth provider — giving users zero-click login inside Cherry.
Setup
Privy Dashboard → Settings → Custom Auth → Add Provider:
- JWKS URL:
https://chat.cherry.fun/.well-known/jwks.json - Issuer:
https://chat.cherry.fun - User ID field:
sub
- JWKS URL:
Code — dual-mode login (Cherry + standalone):
import { CherryMiniAppProvider, useCherryApp, useCherryEnvironment } from '@cherrydotfun/miniapp-sdk/react';
import { usePrivy } from '@privy-io/react-auth';
function AuthGate({ children }) {
const { isEmbedded } = useCherryEnvironment();
const cherry = useCherryApp();
const { loginWithCustomAccessToken, authenticated, ready } = usePrivy();
useEffect(() => {
if (!ready || authenticated) return;
if (isEmbedded && cherry?.launchToken) {
loginWithCustomAccessToken(cherry.launchToken); // transparent login
}
}, [ready, authenticated, isEmbedded, cherry]);
if (!authenticated && !isEmbedded) return <PrivyLoginButton />;
return <>{children}</>;
}- Helper — get auth config programmatically:
import { getCherryCustomAuthConfig } from '@cherrydotfun/miniapp-sdk';
const { token, jwksUrl, issuer } = getCherryCustomAuthConfig(cherry);| Environment | Login Method | User Action |
|-------------|-------------|-------------|
| Inside Cherry | loginWithCustomAccessToken(launchToken) | None — automatic |
| Standalone | Standard Privy UI (email, social, wallet) | User clicks login |
See the integration skill for a complete step-by-step guide.
AI-Assisted Integration
This package includes a Claude Code / Codex skill that automates SDK integration into existing web3 apps. After installing the SDK, copy the skill to your AI assistant and say "Integrate Cherry Mini-App SDK" — it will analyze your codebase and guide you step by step.
License
MIT
