<100 subscribers

Onchain social commerce has been too click-heavy: break out of the feed, connect a wallet, fight a checkout. Conversion drops.
Miniapps on TBA (The Base App) + the x402 protocol fix this. Miniapps are lightweight web apps that run directly inside a TBA feed. x402 revives HTTP’s dormant 402 Payment Required to turn any API route into a vending machine for paid content—seamlessly funded by onchain payments via Coinbase Developer Platform.
In this tutorial, we’ll build a fun, monetized Onchain Fortune Cookie Miniapp end-to-end:
Bootstrap with the official x402 template
Add a backend API and protect it with an x402 paywall
Ship a mobile-ready React UI for the full flow inside TBA
Upgrade to a premium tier with different pricing
By the end, you’ll have a working Miniapp you can share in a TBA cast and charge for — no janky redirects.
🧰 Clone and run the official x402 TypeScript full-stack template
🔐 Add /api/fortune protected by x402 ($0.01)
💳 Add a premium /api/golden-fortune tier ($0.10)
A mobile-friendly Next.js page with two purchase buttons
🚀 Test in TBA using an ngrok URL that resolves into a Frame + “Use App” button
(Watch the Full Video here)

Key pieces:
Miniapp UI (Next.js/React): Simple mobile layout with buttons that call /api/fortune and /api/golden-fortune. The fetch looks “normal”; x402 handles the payment handshake behind the scenes.
x402 Middleware: Wraps routes in paymentMiddleware, pricing in fiat strings (e.g., "$0.01"), and selects Base or Base Sepolia. It triggers the onchain payment flow when a protected route is hit.
Protected API Routes: Ordinary Next.js route.ts handlers—your business logic only runs after a successful payment.
CDP + OnchainKit: Provides wallet infra, payment facilitation, and a smooth purchase flow.
TBA + Frames: Your public HTTPS URL resolves into a Frame; the “Use App” button launches the Miniapp inline.
We’ll start from the official x402 template to skip boilerplate.
git clone https://github.com/coinbase/x402.git
cd x402/examples/typescript/fullstack/farcaster-miniapp
# Install all deps for the example
pnpm install
# Build the monorepo packages (critical)
cd ../../
pnpm install
pnpm build
# Go back to the example
cd fullstack/farcaster-miniapp# Copy the example env
cp env.example .env.localEdit .env.local:
# where payments land
RESOURCE_WALLET_ADDRESS=0xYourReceivingAddress
# base-sepolia for testing; switch to base for prod
NETWORK=base-sepolia
# CDP portal > OnchainKit
NEXT_PUBLIC_ONCHAINKIT_API_KEY=your_onchainkit_key
NEXT_PUBLIC_ONCHAINKIT_PROJECT_NAME=Onchain Fortune Cookie
# Use emoji placeholders - no image files needed
NEXT_PUBLIC_APP_HERO_IMAGE=https://emojicdn.elk.sh/🥠?style=apple&size=600
NEXT_PUBLIC_APP_ICON=https://emojicdn.elk.sh/🥠?style=apple&size=200
NEXT_PUBLIC_APP_SPLASH_IMAGE=https://emojicdn.elk.sh/🥠?style=apple&size=200
NEXT_PUBLIC_ICON_URL=https://emojicdn.elk.sh/🥠?style=apple&size=200Tip: For production, move to
NETWORK=baseand swapRESOURCE_WALLET_ADDRESSaccordingly.
Create a simple paid content endpoint.
File: app/api/fortune/route.ts
import { NextResponse } from "next/server";
const fortunes = [
"Your next transaction will be blessed with low gas fees.",
"A 100x gem is in your near future. DYOR.",
"The blockchain whispers secrets of prosperity to you.",
"You will successfully avoid a rug pull this week.",
"GM will bring you good luck today.",
"Diamond hands will lead to diamond rings.",
"A wild airdrop appears! It's super effective.",
];
export async function GET() {
// Runs only AFTER a successful x402 payment
const ix = Math.floor(Math.random() * fortunes.length);
return NextResponse.json({ fortune: fortunes[ix], timestamp: new Date().toISOString() });Wrap the route with paymentMiddleware and set the price.
File: middleware.ts
import { facilitator } from "@coinbase/x402";
import { Address } from "viem";
import { paymentMiddleware } from "x402-next";
const payTo = process.env.RESOURCE_WALLET_ADDRESS as Address;
const network = (process.env.NETWORK || "base-sepolia") as "base" | "base-sepolia";
export const middleware = paymentMiddleware(
payTo,
{
"/api/fortune": {
price: "$0.01",
network,
config: { description: "Your onchain fortune cookie!" },
},
},
facilitator,
);
export const config = {
matcher: ["/api/fortune"],
runtime: "nodejs",
};Any request to /api/fortune now triggers a $0.01 payment before the handler executes.
Create a minimal, mobile-first page with two buttons (standard + premium). If your template already includes a full page, you can replace it; otherwise add this as app/page.tsx.
File: app/page.tsx
"use client";
import { useState } from "react";
export default function FortuneCookieApp() {
const [loading, setLoading] = useState<null | "basic" | "golden">(null);
const [result, setResult] = useState<string>("");
async function fetchFortune(path: "/api/fortune" | "/api/golden-fortune", tier: "basic" | "golden") {
try {
setLoading(tier);
setResult("");
const res = await fetch(path, { method: "GET" });
// If payment is needed, the x402 middleware will orchestrate it.
// From the client, this remains a normal fetch that resolves after purchase.
if (!res.ok) {
const text = await res.text();
throw new Error(`Request failed: ${res.status} ${text}`);
}
const data = await res.json();
setResult(data.fortune ?? JSON.stringify(data));
} catch (e: any) {
setResult(e?.message ?? "Something went wrong.");
} finally {
setLoading(null);
}
}
return (
<main className="min-h-screen bg-[#0c0c10] text-white flex flex-col items-center px-4 py-10">
<div className="w-full max-w-md">
<div className="flex flex-col items-center gap-3 mb-8">
<div className="text-6xl">🥠</div>
<h1 className="text-2xl font-semibold text-center">Onchain Fortune Cookie</h1>
<p className="text-sm opacity-80 text-center">
Grab a fortune inside TBA. Pay per request using x402 on Base Sepolia.
</p>
</div>
<div className="grid grid-cols-1 gap-3">
<button
onClick={() => fetchFortune("/api/fortune", "basic")}
disabled={loading !== null}
className="rounded-2xl px-4 py-4 bg-white/10 hover:bg-white/15 transition border border-white/10"
>
{loading === "basic" ? "Purchasing…" : "Buy Fortune - $0.01"}
</button>
<button
onClick={() => fetchFortune("/api/golden-fortune", "golden")}
disabled={loading !== null}
className="rounded-2xl px-4 py-4 bg-yellow-500/20 hover:bg-yellow-500/25 transition border border-yellow-500/30"
>
{loading === "golden" ? "Purchasing… " : "Buy GOLDEN Fortune - $0.10"}
</button>
</div>
<div className="mt-6 min-h-[96px] rounded-2xl border border-white/10 bg-white/5 p-4">
<div className="text-xs uppercase tracking-wide opacity-60 mb-1">Result</div>
<p className="text-base leading-6">{result || "-"}</p>
</div>
<p className="mt-6 text-xs opacity-60 text-center">
Network: {process.env.NEXT_PUBLIC_NETWORK ?? "base-sepolia"} · Powered by x402
</p>
</div>
</main>
);
}Note: The payment UX is handled by x402 when your protected route is hit. Your client code stays simple: call
fetchand display the response.
We’ll expose your dev server to the internet so TBA can frame it.
Run dev server
Expose with ngrok (new terminal)
ngrok http 3000Update your env
Copy the public https://… URL from ngrok to .env.local:
NEXT_PUBLIC_URL=https://your-ngrok-id.ngrok-free.appRestart dev:
# stop then
pnpm devOpen in TBA
In TBA, create a new cast with your NEXT_PUBLIC_URL.
The URL resolves to a Frame with a Use App button.
Tap it to launch your Miniapp inline and complete a purchase.
File: app/api/golden-fortune/route.ts
import { NextResponse } from "next/server";
const goldenFortunes = [
"You've found the legendary Diamond Hand Pepe. Hodl for unimaginable gains.",
"The memecoin you ape into next will not be a rug. This is financial advice.",
"Satoshi's ghost will reveal the next big narrative to you tonight.",
];
export async function GET() {
const ix = Math.floor(Math.random() * goldenFortunes.length);
return NextResponse.json({ fortune: goldenFortunes[ix], isGolden: true, timestamp: new Date().toISOString() });
}File: middleware.ts (updated)
import { facilitator } from "@coinbase/x402";
import { Address } from "viem";
import { paymentMiddleware } from "x402-next";
const payTo = process.env.RESOURCE_WALLET_ADDRESS as Address;
const network = (process.env.NETWORK || "base-sepolia") as "base" | "base-sepolia";
export const middleware = paymentMiddleware(
payTo,
{
"/api/fortune": {
price: "$0.01",
network,
config: { description: "Your onchain fortune cookie!" },
},
"/api/golden-fortune": {
price: "$0.10",
network,
config: { description: "Your GOLDEN onchain fortune cookie!" },
},
},
facilitator,
);
export const config = {
matcher: ["/api/fortune", "/api/golden-fortune"],
runtime: "nodejs",
};No UI changes needed if you used the two-button page above.
Run pnpm dev and ngrok http 3000
Cast your ngrok URL in TBA → “Use App”
Purchase Fortune ($0.01) or Golden Fortune ($0.10)
See the paid content return in-app — no context switching
You just shipped a multi-tier, monetized Miniapp that runs inside TBA, using x402 to price-gate API routes and CDP infra to handle payments.
Next ideas:
Add more tiers and content types (e.g., images, signed data, gated downloads)
Switch to NETWORK=base for production and set a mainnet receiver wallet
Personalize pricing per route, per user, or time-boxed sales
Track purchases and receipts for analytics
Source: Explore the official x402 examples repo to extend this project.
Community: Join the Coinbase Developer Platform Discord to share builds and get help.
x402 Examples
Coinbase Developer Docs
HeimLabs
TBA (The Base App): (see product links)
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 🚀

Onchain social commerce has been too click-heavy: break out of the feed, connect a wallet, fight a checkout. Conversion drops.
Miniapps on TBA (The Base App) + the x402 protocol fix this. Miniapps are lightweight web apps that run directly inside a TBA feed. x402 revives HTTP’s dormant 402 Payment Required to turn any API route into a vending machine for paid content—seamlessly funded by onchain payments via Coinbase Developer Platform.
In this tutorial, we’ll build a fun, monetized Onchain Fortune Cookie Miniapp end-to-end:
Bootstrap with the official x402 template
Add a backend API and protect it with an x402 paywall
Ship a mobile-ready React UI for the full flow inside TBA
Upgrade to a premium tier with different pricing
By the end, you’ll have a working Miniapp you can share in a TBA cast and charge for — no janky redirects.
🧰 Clone and run the official x402 TypeScript full-stack template
🔐 Add /api/fortune protected by x402 ($0.01)
💳 Add a premium /api/golden-fortune tier ($0.10)
A mobile-friendly Next.js page with two purchase buttons
🚀 Test in TBA using an ngrok URL that resolves into a Frame + “Use App” button
(Watch the Full Video here)

Key pieces:
Miniapp UI (Next.js/React): Simple mobile layout with buttons that call /api/fortune and /api/golden-fortune. The fetch looks “normal”; x402 handles the payment handshake behind the scenes.
x402 Middleware: Wraps routes in paymentMiddleware, pricing in fiat strings (e.g., "$0.01"), and selects Base or Base Sepolia. It triggers the onchain payment flow when a protected route is hit.
Protected API Routes: Ordinary Next.js route.ts handlers—your business logic only runs after a successful payment.
CDP + OnchainKit: Provides wallet infra, payment facilitation, and a smooth purchase flow.
TBA + Frames: Your public HTTPS URL resolves into a Frame; the “Use App” button launches the Miniapp inline.
We’ll start from the official x402 template to skip boilerplate.
git clone https://github.com/coinbase/x402.git
cd x402/examples/typescript/fullstack/farcaster-miniapp
# Install all deps for the example
pnpm install
# Build the monorepo packages (critical)
cd ../../
pnpm install
pnpm build
# Go back to the example
cd fullstack/farcaster-miniapp# Copy the example env
cp env.example .env.localEdit .env.local:
# where payments land
RESOURCE_WALLET_ADDRESS=0xYourReceivingAddress
# base-sepolia for testing; switch to base for prod
NETWORK=base-sepolia
# CDP portal > OnchainKit
NEXT_PUBLIC_ONCHAINKIT_API_KEY=your_onchainkit_key
NEXT_PUBLIC_ONCHAINKIT_PROJECT_NAME=Onchain Fortune Cookie
# Use emoji placeholders - no image files needed
NEXT_PUBLIC_APP_HERO_IMAGE=https://emojicdn.elk.sh/🥠?style=apple&size=600
NEXT_PUBLIC_APP_ICON=https://emojicdn.elk.sh/🥠?style=apple&size=200
NEXT_PUBLIC_APP_SPLASH_IMAGE=https://emojicdn.elk.sh/🥠?style=apple&size=200
NEXT_PUBLIC_ICON_URL=https://emojicdn.elk.sh/🥠?style=apple&size=200Tip: For production, move to
NETWORK=baseand swapRESOURCE_WALLET_ADDRESSaccordingly.
Create a simple paid content endpoint.
File: app/api/fortune/route.ts
import { NextResponse } from "next/server";
const fortunes = [
"Your next transaction will be blessed with low gas fees.",
"A 100x gem is in your near future. DYOR.",
"The blockchain whispers secrets of prosperity to you.",
"You will successfully avoid a rug pull this week.",
"GM will bring you good luck today.",
"Diamond hands will lead to diamond rings.",
"A wild airdrop appears! It's super effective.",
];
export async function GET() {
// Runs only AFTER a successful x402 payment
const ix = Math.floor(Math.random() * fortunes.length);
return NextResponse.json({ fortune: fortunes[ix], timestamp: new Date().toISOString() });Wrap the route with paymentMiddleware and set the price.
File: middleware.ts
import { facilitator } from "@coinbase/x402";
import { Address } from "viem";
import { paymentMiddleware } from "x402-next";
const payTo = process.env.RESOURCE_WALLET_ADDRESS as Address;
const network = (process.env.NETWORK || "base-sepolia") as "base" | "base-sepolia";
export const middleware = paymentMiddleware(
payTo,
{
"/api/fortune": {
price: "$0.01",
network,
config: { description: "Your onchain fortune cookie!" },
},
},
facilitator,
);
export const config = {
matcher: ["/api/fortune"],
runtime: "nodejs",
};Any request to /api/fortune now triggers a $0.01 payment before the handler executes.
Create a minimal, mobile-first page with two buttons (standard + premium). If your template already includes a full page, you can replace it; otherwise add this as app/page.tsx.
File: app/page.tsx
"use client";
import { useState } from "react";
export default function FortuneCookieApp() {
const [loading, setLoading] = useState<null | "basic" | "golden">(null);
const [result, setResult] = useState<string>("");
async function fetchFortune(path: "/api/fortune" | "/api/golden-fortune", tier: "basic" | "golden") {
try {
setLoading(tier);
setResult("");
const res = await fetch(path, { method: "GET" });
// If payment is needed, the x402 middleware will orchestrate it.
// From the client, this remains a normal fetch that resolves after purchase.
if (!res.ok) {
const text = await res.text();
throw new Error(`Request failed: ${res.status} ${text}`);
}
const data = await res.json();
setResult(data.fortune ?? JSON.stringify(data));
} catch (e: any) {
setResult(e?.message ?? "Something went wrong.");
} finally {
setLoading(null);
}
}
return (
<main className="min-h-screen bg-[#0c0c10] text-white flex flex-col items-center px-4 py-10">
<div className="w-full max-w-md">
<div className="flex flex-col items-center gap-3 mb-8">
<div className="text-6xl">🥠</div>
<h1 className="text-2xl font-semibold text-center">Onchain Fortune Cookie</h1>
<p className="text-sm opacity-80 text-center">
Grab a fortune inside TBA. Pay per request using x402 on Base Sepolia.
</p>
</div>
<div className="grid grid-cols-1 gap-3">
<button
onClick={() => fetchFortune("/api/fortune", "basic")}
disabled={loading !== null}
className="rounded-2xl px-4 py-4 bg-white/10 hover:bg-white/15 transition border border-white/10"
>
{loading === "basic" ? "Purchasing…" : "Buy Fortune - $0.01"}
</button>
<button
onClick={() => fetchFortune("/api/golden-fortune", "golden")}
disabled={loading !== null}
className="rounded-2xl px-4 py-4 bg-yellow-500/20 hover:bg-yellow-500/25 transition border border-yellow-500/30"
>
{loading === "golden" ? "Purchasing… " : "Buy GOLDEN Fortune - $0.10"}
</button>
</div>
<div className="mt-6 min-h-[96px] rounded-2xl border border-white/10 bg-white/5 p-4">
<div className="text-xs uppercase tracking-wide opacity-60 mb-1">Result</div>
<p className="text-base leading-6">{result || "-"}</p>
</div>
<p className="mt-6 text-xs opacity-60 text-center">
Network: {process.env.NEXT_PUBLIC_NETWORK ?? "base-sepolia"} · Powered by x402
</p>
</div>
</main>
);
}Note: The payment UX is handled by x402 when your protected route is hit. Your client code stays simple: call
fetchand display the response.
We’ll expose your dev server to the internet so TBA can frame it.
Run dev server
Expose with ngrok (new terminal)
ngrok http 3000Update your env
Copy the public https://… URL from ngrok to .env.local:
NEXT_PUBLIC_URL=https://your-ngrok-id.ngrok-free.appRestart dev:
# stop then
pnpm devOpen in TBA
In TBA, create a new cast with your NEXT_PUBLIC_URL.
The URL resolves to a Frame with a Use App button.
Tap it to launch your Miniapp inline and complete a purchase.
File: app/api/golden-fortune/route.ts
import { NextResponse } from "next/server";
const goldenFortunes = [
"You've found the legendary Diamond Hand Pepe. Hodl for unimaginable gains.",
"The memecoin you ape into next will not be a rug. This is financial advice.",
"Satoshi's ghost will reveal the next big narrative to you tonight.",
];
export async function GET() {
const ix = Math.floor(Math.random() * goldenFortunes.length);
return NextResponse.json({ fortune: goldenFortunes[ix], isGolden: true, timestamp: new Date().toISOString() });
}File: middleware.ts (updated)
import { facilitator } from "@coinbase/x402";
import { Address } from "viem";
import { paymentMiddleware } from "x402-next";
const payTo = process.env.RESOURCE_WALLET_ADDRESS as Address;
const network = (process.env.NETWORK || "base-sepolia") as "base" | "base-sepolia";
export const middleware = paymentMiddleware(
payTo,
{
"/api/fortune": {
price: "$0.01",
network,
config: { description: "Your onchain fortune cookie!" },
},
"/api/golden-fortune": {
price: "$0.10",
network,
config: { description: "Your GOLDEN onchain fortune cookie!" },
},
},
facilitator,
);
export const config = {
matcher: ["/api/fortune", "/api/golden-fortune"],
runtime: "nodejs",
};No UI changes needed if you used the two-button page above.
Run pnpm dev and ngrok http 3000
Cast your ngrok URL in TBA → “Use App”
Purchase Fortune ($0.01) or Golden Fortune ($0.10)
See the paid content return in-app — no context switching
You just shipped a multi-tier, monetized Miniapp that runs inside TBA, using x402 to price-gate API routes and CDP infra to handle payments.
Next ideas:
Add more tiers and content types (e.g., images, signed data, gated downloads)
Switch to NETWORK=base for production and set a mainnet receiver wallet
Personalize pricing per route, per user, or time-boxed sales
Track purchases and receipts for analytics
Source: Explore the official x402 examples repo to extend this project.
Community: Join the Coinbase Developer Platform Discord to share builds and get help.
x402 Examples
Coinbase Developer Docs
HeimLabs
TBA (The Base App): (see product links)
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
1 comment
Build apps that earn — not just run. 💸 We just built a monetized Miniapp on TBA using x402 + CDP: paywalled API routes, onchain payments, no redirects. @coinbasedev