<100 subscribers

App installs, cold-start social graphs, and clunky wallets kill growth. Traditional apps fight for app-store rankings while your users bounce before the first tap.
Today we’ll build a Base Mini App — a lightweight, social-native web app that launches instantly inside clients like the Base App and Farcaster. Using MiniKit (for frames + native hooks) and OnchainKit (for polished UI + onchain utilities), you’ll ship a “Social Trivia” app that feels at home in the feed and scales via sharing.
By the end, you’ll have a working Mini App with a clean viral loop, context-aware UI, smooth native controls, and an optional gasless onchain leaderboard on Base.
Launch a Next.js Mini App with a signed Farcaster manifest
🧭 Use MiniKit hooks for context, auth, share, and navigation
Render identity with OnchainKit’s Avatar/Name components
📣 Create a viral loop with dynamic embeds + useComposeCast
⛽ Store scores onchain with a paymaster-sponsored flow (gasless UX)
(Watch the Video here)
User → Mini App (WebView in client): Tap-to-open from a cast or DM.
MiniKit Context: Provides device safe areas, user FID, client info.
UI & Logic (Next.js + OnchainKit): Trivia flow, identity display, sharing.
Smart Contract (Base): Optional leaderboard storage, paymaster-sponsored.
Social Client: Native share sheets, in-app browser, profile/cast deep links.
MiniKit
Harnesses the in-client runtime: safe area insets, native buttons, closing flows, social navigation, and sharing.
OnchainKit
UI primitives (Avatar, Name, …) and helpers that match ecosystem UX conventions.
Blockchain (Base)
Leaderboard contract on Base; optional paymaster for gasless writes.
Mini Apps flip the model. Instead of dragging users to an app store, the app goes to the user — living inside the feed, casting UI as content. You inherit an onchain social graph (Farcaster), skip installs, and unlock viral distribution.
Farcaster account (for testing + manifest signing)
Node.js v18+
A tunneling service (e.g., ngrok) for public dev URL
Bootstrap the project
# Scaffold a new Mini Appnpx create-onchain --miniCreate the Farcaster manifest at /public/.well-known/farcaster.json:
function withValidProperties(
properties: Record<string, undefined | string | string[]>,
) {
return Object.fromEntries(
Object.entries(properties).filter(([key, value]) => {
if (Array.isArray(value)) {
return value.length > 0;
}
return !!value;
}),
);
}
export async function GET() {
const URL = process.env.NEXT_PUBLIC_URL;
return Response.json({
accountAssociation: {
header: process.env.FARCASTER_HEADER,
payload: process.env.FARCASTER_PAYLOAD,
signature: process.env.FARCASTER_SIGNATURE,
},
frame: withValidProperties({
version: "1",
name: process.env.NEXT_PUBLIC_ONCHAINKIT_PROJECT_NAME,
subtitle: process.env.NEXT_PUBLIC_APP_SUBTITLE,
description: process.env.NEXT_PUBLIC_APP_DESCRIPTION,
screenshotUrls: [],
iconUrl: process.env.NEXT_PUBLIC_APP_ICON,
splashImageUrl: process.env.NEXT_PUBLIC_APP_SPLASH_IMAGE,
splashBackgroundColor: process.env.NEXT_PUBLIC_SPLASH_BACKGROUND_COLOR,
homeUrl: URL,
webhookUrl: `${URL}/api/webhook`,
primaryCategory: process.env.NEXT_PUBLIC_APP_PRIMARY_CATEGORY,
tags: [],
heroImageUrl: process.env.NEXT_PUBLIC_APP_HERO_IMAGE,
tagline: process.env.NEXT_PUBLIC_APP_TAGLINE,
ogTitle: process.env.NEXT_PUBLIC_APP_OG_TITLE,
ogDescription: process.env.NEXT_PUBLIC_APP_OG_DESCRIPTION,
ogImageUrl: process.env.NEXT_PUBLIC_APP_OG_IMAGE,
}),
baseBuilder: {
allowedAddresses: ["base-builder-address-here"],
},
});
}Dev tip: Farcaster clients can’t access
localhost. Expose your app with ngrok and validate your URLs using the official Farcaster Manifest and Embed validators before debugging anything else.
app/components/AppLayout.tsx
/// app/components/AppLayout.tsx
'use client';
import { useMiniKit } from '@coinbase/onchainkit/minikit';
import { ReactNode } from 'react';
export default function AppLayout({ children }: { children: ReactNode }) {
const { context } = useMiniKit();
const style = {
paddingTop: `${context?.client?.safeAreaInsets?.top ?? 0}px`,
paddingBottom: `${context?.client?.safeAreaInsets?.bottom ?? 0}px`,
minHeight: '100vh',
display: 'flex',
flexDirection: 'column' as const,
};
return (
<main style={style} className="bg-gradient-to-b from-slate-900 to-slate-800">
{children}
</main>
);
}app/page.tsx
// app/page.tsx
// ⚠️ SECURITY CRITICAL:
// The context object is for display and personalization ONLY.
// It is unverified and must not be used to authorize actions on your backend.
// For secure actions, you must use the useAuthenticate hook.
// app/page.tsx
"use client";
import { useEffect } from "react";
import { useMiniKit } from "@coinbase/onchainkit/minikit";
import AppLayout from "./components/AppLayout";
import TriviaGame from "./components/TriviaGame";
export default function App() {
const { setFrameReady, isFrameReady, context } = useMiniKit();
const userFid = context?.user?.fid;
const username = context?.user?.username;
const pfpUrl = context?.user?.pfpUrl;
const displayName = context?.user?.displayName;
useEffect(() => {
if (!isFrameReady) {
setFrameReady();
}
}, [setFrameReady, isFrameReady]);
return (
<AppLayout>
<div className="flex-1 flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-4 bg-black/20 backdrop-blur-sm">
<div className="flex items-center gap-3">
{userFid ? (
<>
{pfpUrl && (
<img
src={pfpUrl}
alt={displayName || username || 'User'}
className="h-10 w-10 rounded-full"
/>
)}
<div>
<h1 className="text-xl font-bold text-white">
<span>{displayName || username || 'Anonymous'}</span>
</h1>
</div>
</>
) : (
<h1 className="text-xl font-bold text-white">Social Trivia</h1>
)}
</div>
</div>
{/* Game */}
<div className="flex-1">
<TriviaGame userFid={userFid as number} username={username as string} />
</div>
</div>
</AppLayout>
);
}Security note:
contextis not authentication. It’s for personalized display only. UseuseAuthenticatefor verified, server-authorized actions.
A. Progressive Auth with useAuthenticate
Let users play first. Only prompt auth when saving a score or calling your backend.
B. Dynamic Embeds (Share Previews)
Generate a share URL like /share?score=95&user=vitalik and render dynamic <meta> tags server-side so the cast preview becomes a personalized challenge card.
C. Native Share with useComposeCast
// app/components/ResultsScreen.tsx
const { composeCast } = useComposeCast();
const handleShare = () => {
const shareUrl = `https://your-trivia-app.xyz/share?score=${score}&user=${username}`;
composeCast({
text: `I scored ${score}/100! Can you beat me?`,
embeds: [shareUrl],
});
};D. Deep Social Navigation
// Leaderboard row → tap to view Farcaster profile
const viewProfile = useViewProfile(playerFid);
return <button onClick={viewProfile}><Name address={`fid:${playerFid}`} /></button>;E. The Linking Rule: useOpenUrl (never raw <a>)
const openUrl = useOpenUrl('https://en.wikipedia.org/wiki/Trivia');
return <button onClick={openUrl}>Source</button>;Primary Action with usePrimaryButton
usePrimaryButton({ text: 'Submit Answer' }, () => {
handleSubmitAnswer();
});Graceful Exit with useClose
const closeFrame = useClose();
return <button onClick={closeFrame}>Done</button>;Solidity Contract (simplified tutorial version)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract TriviaLeaderboard {
mapping(address => uint256) public scores;
function addScore(uint256 score) public {
// NOTE: In production, verify a server signature before accepting scores.
scores[msg.sender] = score;
}
}Production Security Tip
Never trust client-submitted scores. Verify on a server first, then sign {userAddress, score} with your server key. Have the contract check that signature before addScore updates storage. Pair with a Paymaster (Coinbase Developer Platform) to sponsor gas so the UX feels Web2-smooth.
Frontend flow
Configure MiniKitProvider with your Paymaster URL. Use standard wagmi hooks (useWriteContract) to store scores; MiniKit handles gasless plumbing under the hood.
Deploy to Vercel (or similar).
Update the manifest URLs to your final prod domain.
In Base Build (base.dev): import your app, connect the wallet tied to your Farcaster account, and follow the verification prompts.
Merge the provided baseBuilder object into your manifest, for example:
{
"frame": {
"name": "Social Trivia",
"homeUrl": "https://your-final-app-url.xyz",
"noindex": false
},
"baseBuilder": {
"allowedAddresses": [
"0x...your...verified...address"
]
}
}Redeploy with this final manifest, then submit your production URL on Base Build to complete verification.
Registering your app unlocks:
Directory discovery in the Base App
Analytics for installs, usage, and retention
You built a social-native Mini App with MiniKit + OnchainKit: context-aware UI, native controls, viral sharing, deep social navigation, and an optional gasless onchain leaderboard on Base.
Tweak copy and visuals on your dynamic share previews.
Add useAddFrame (save to favorites) and useNotification (re-engagement) as they roll out.
Harden your onchain writes with server-verified signatures and a paymaster.
Explore Base Docs to go deeper on Mini Apps.
Ship your v1, share it in the Base community, and tag @HeimLabs with what you build!
Base Docs (Mini Apps):
OnchainKit Documentation:
Farcaster Docs:
Base Discord:
Demo Codebase:
HeimLabs:
Clap if this saved you hours, and share what you’re building with the CDP stack!
Follow HeimLabs for unapologetically practical Web3 dev content.
Twitter, LinkedIn.
Happy Building 🚀

App installs, cold-start social graphs, and clunky wallets kill growth. Traditional apps fight for app-store rankings while your users bounce before the first tap.
Today we’ll build a Base Mini App — a lightweight, social-native web app that launches instantly inside clients like the Base App and Farcaster. Using MiniKit (for frames + native hooks) and OnchainKit (for polished UI + onchain utilities), you’ll ship a “Social Trivia” app that feels at home in the feed and scales via sharing.
By the end, you’ll have a working Mini App with a clean viral loop, context-aware UI, smooth native controls, and an optional gasless onchain leaderboard on Base.
Launch a Next.js Mini App with a signed Farcaster manifest
🧭 Use MiniKit hooks for context, auth, share, and navigation
Render identity with OnchainKit’s Avatar/Name components
📣 Create a viral loop with dynamic embeds + useComposeCast
⛽ Store scores onchain with a paymaster-sponsored flow (gasless UX)
(Watch the Video here)
User → Mini App (WebView in client): Tap-to-open from a cast or DM.
MiniKit Context: Provides device safe areas, user FID, client info.
UI & Logic (Next.js + OnchainKit): Trivia flow, identity display, sharing.
Smart Contract (Base): Optional leaderboard storage, paymaster-sponsored.
Social Client: Native share sheets, in-app browser, profile/cast deep links.
MiniKit
Harnesses the in-client runtime: safe area insets, native buttons, closing flows, social navigation, and sharing.
OnchainKit
UI primitives (Avatar, Name, …) and helpers that match ecosystem UX conventions.
Blockchain (Base)
Leaderboard contract on Base; optional paymaster for gasless writes.
Mini Apps flip the model. Instead of dragging users to an app store, the app goes to the user — living inside the feed, casting UI as content. You inherit an onchain social graph (Farcaster), skip installs, and unlock viral distribution.
Farcaster account (for testing + manifest signing)
Node.js v18+
A tunneling service (e.g., ngrok) for public dev URL
Bootstrap the project
# Scaffold a new Mini Appnpx create-onchain --miniCreate the Farcaster manifest at /public/.well-known/farcaster.json:
function withValidProperties(
properties: Record<string, undefined | string | string[]>,
) {
return Object.fromEntries(
Object.entries(properties).filter(([key, value]) => {
if (Array.isArray(value)) {
return value.length > 0;
}
return !!value;
}),
);
}
export async function GET() {
const URL = process.env.NEXT_PUBLIC_URL;
return Response.json({
accountAssociation: {
header: process.env.FARCASTER_HEADER,
payload: process.env.FARCASTER_PAYLOAD,
signature: process.env.FARCASTER_SIGNATURE,
},
frame: withValidProperties({
version: "1",
name: process.env.NEXT_PUBLIC_ONCHAINKIT_PROJECT_NAME,
subtitle: process.env.NEXT_PUBLIC_APP_SUBTITLE,
description: process.env.NEXT_PUBLIC_APP_DESCRIPTION,
screenshotUrls: [],
iconUrl: process.env.NEXT_PUBLIC_APP_ICON,
splashImageUrl: process.env.NEXT_PUBLIC_APP_SPLASH_IMAGE,
splashBackgroundColor: process.env.NEXT_PUBLIC_SPLASH_BACKGROUND_COLOR,
homeUrl: URL,
webhookUrl: `${URL}/api/webhook`,
primaryCategory: process.env.NEXT_PUBLIC_APP_PRIMARY_CATEGORY,
tags: [],
heroImageUrl: process.env.NEXT_PUBLIC_APP_HERO_IMAGE,
tagline: process.env.NEXT_PUBLIC_APP_TAGLINE,
ogTitle: process.env.NEXT_PUBLIC_APP_OG_TITLE,
ogDescription: process.env.NEXT_PUBLIC_APP_OG_DESCRIPTION,
ogImageUrl: process.env.NEXT_PUBLIC_APP_OG_IMAGE,
}),
baseBuilder: {
allowedAddresses: ["base-builder-address-here"],
},
});
}Dev tip: Farcaster clients can’t access
localhost. Expose your app with ngrok and validate your URLs using the official Farcaster Manifest and Embed validators before debugging anything else.
app/components/AppLayout.tsx
/// app/components/AppLayout.tsx
'use client';
import { useMiniKit } from '@coinbase/onchainkit/minikit';
import { ReactNode } from 'react';
export default function AppLayout({ children }: { children: ReactNode }) {
const { context } = useMiniKit();
const style = {
paddingTop: `${context?.client?.safeAreaInsets?.top ?? 0}px`,
paddingBottom: `${context?.client?.safeAreaInsets?.bottom ?? 0}px`,
minHeight: '100vh',
display: 'flex',
flexDirection: 'column' as const,
};
return (
<main style={style} className="bg-gradient-to-b from-slate-900 to-slate-800">
{children}
</main>
);
}app/page.tsx
// app/page.tsx
// ⚠️ SECURITY CRITICAL:
// The context object is for display and personalization ONLY.
// It is unverified and must not be used to authorize actions on your backend.
// For secure actions, you must use the useAuthenticate hook.
// app/page.tsx
"use client";
import { useEffect } from "react";
import { useMiniKit } from "@coinbase/onchainkit/minikit";
import AppLayout from "./components/AppLayout";
import TriviaGame from "./components/TriviaGame";
export default function App() {
const { setFrameReady, isFrameReady, context } = useMiniKit();
const userFid = context?.user?.fid;
const username = context?.user?.username;
const pfpUrl = context?.user?.pfpUrl;
const displayName = context?.user?.displayName;
useEffect(() => {
if (!isFrameReady) {
setFrameReady();
}
}, [setFrameReady, isFrameReady]);
return (
<AppLayout>
<div className="flex-1 flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-4 bg-black/20 backdrop-blur-sm">
<div className="flex items-center gap-3">
{userFid ? (
<>
{pfpUrl && (
<img
src={pfpUrl}
alt={displayName || username || 'User'}
className="h-10 w-10 rounded-full"
/>
)}
<div>
<h1 className="text-xl font-bold text-white">
<span>{displayName || username || 'Anonymous'}</span>
</h1>
</div>
</>
) : (
<h1 className="text-xl font-bold text-white">Social Trivia</h1>
)}
</div>
</div>
{/* Game */}
<div className="flex-1">
<TriviaGame userFid={userFid as number} username={username as string} />
</div>
</div>
</AppLayout>
);
}Security note:
contextis not authentication. It’s for personalized display only. UseuseAuthenticatefor verified, server-authorized actions.
A. Progressive Auth with useAuthenticate
Let users play first. Only prompt auth when saving a score or calling your backend.
B. Dynamic Embeds (Share Previews)
Generate a share URL like /share?score=95&user=vitalik and render dynamic <meta> tags server-side so the cast preview becomes a personalized challenge card.
C. Native Share with useComposeCast
// app/components/ResultsScreen.tsx
const { composeCast } = useComposeCast();
const handleShare = () => {
const shareUrl = `https://your-trivia-app.xyz/share?score=${score}&user=${username}`;
composeCast({
text: `I scored ${score}/100! Can you beat me?`,
embeds: [shareUrl],
});
};D. Deep Social Navigation
// Leaderboard row → tap to view Farcaster profile
const viewProfile = useViewProfile(playerFid);
return <button onClick={viewProfile}><Name address={`fid:${playerFid}`} /></button>;E. The Linking Rule: useOpenUrl (never raw <a>)
const openUrl = useOpenUrl('https://en.wikipedia.org/wiki/Trivia');
return <button onClick={openUrl}>Source</button>;Primary Action with usePrimaryButton
usePrimaryButton({ text: 'Submit Answer' }, () => {
handleSubmitAnswer();
});Graceful Exit with useClose
const closeFrame = useClose();
return <button onClick={closeFrame}>Done</button>;Solidity Contract (simplified tutorial version)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract TriviaLeaderboard {
mapping(address => uint256) public scores;
function addScore(uint256 score) public {
// NOTE: In production, verify a server signature before accepting scores.
scores[msg.sender] = score;
}
}Production Security Tip
Never trust client-submitted scores. Verify on a server first, then sign {userAddress, score} with your server key. Have the contract check that signature before addScore updates storage. Pair with a Paymaster (Coinbase Developer Platform) to sponsor gas so the UX feels Web2-smooth.
Frontend flow
Configure MiniKitProvider with your Paymaster URL. Use standard wagmi hooks (useWriteContract) to store scores; MiniKit handles gasless plumbing under the hood.
Deploy to Vercel (or similar).
Update the manifest URLs to your final prod domain.
In Base Build (base.dev): import your app, connect the wallet tied to your Farcaster account, and follow the verification prompts.
Merge the provided baseBuilder object into your manifest, for example:
{
"frame": {
"name": "Social Trivia",
"homeUrl": "https://your-final-app-url.xyz",
"noindex": false
},
"baseBuilder": {
"allowedAddresses": [
"0x...your...verified...address"
]
}
}Redeploy with this final manifest, then submit your production URL on Base Build to complete verification.
Registering your app unlocks:
Directory discovery in the Base App
Analytics for installs, usage, and retention
You built a social-native Mini App with MiniKit + OnchainKit: context-aware UI, native controls, viral sharing, deep social navigation, and an optional gasless onchain leaderboard on Base.
Tweak copy and visuals on your dynamic share previews.
Add useAddFrame (save to favorites) and useNotification (re-engagement) as they roll out.
Harden your onchain writes with server-verified signatures and a paymaster.
Explore Base Docs to go deeper on Mini Apps.
Ship your v1, share it in the Base community, and tag @HeimLabs with what you build!
Base Docs (Mini Apps):
OnchainKit Documentation:
Farcaster Docs:
Base Discord:
Demo Codebase:
HeimLabs:
Clap if this saved you hours, and share what you’re building with the CDP stack!
Follow HeimLabs for unapologetically practical Web3 dev content.
Twitter, LinkedIn.
Happy Building 🚀
Share Dialog
Share Dialog
HeimLabs
HeimLabs
No comments yet