pricing-page
Scaffold a complete pricing system — tier definitions, feature gating logic, Dodo Payments checkout, and a frontend pricing component. Reads the project first, wires into the existing stack.
Phase 1: Understand the Project
Before writing anything, read the codebase:
1.1 Stack Detection
- Framework: Next.js / other?
- Database: What ORM/client? What does the users table look like?
- Auth: How is the current user identified in API routes?
- Existing payments: Any Dodo SDK or payment routes already?
1.2 Ask the User
I'll scaffold pricing for your [framework] app.
Quick decisions:
1. How many tiers? (e.g., Free + Pro, or Free + Pro + Enterprise)
2. What's the pricing model? (flat rate / credits / per-seat / usage-based)
3. Monthly billing, annual, or both?
4. What features are gated behind paid? (or let me suggest based on the codebase)
I'll handle Dodo Payments integration.
Phase 2: Tier Definitions
Create a single source of truth for tiers. Adapt based on the user's answers:
// config/pricing.ts
export type Plan = 'free' | 'pro' | 'enterprise';
export const PLANS = {
free: {
name: 'Free',
price: 0,
description: 'Get started',
features: ['[Feature 1]', '[Feature 2]'],
limits: {
// Fill based on codebase — e.g., creditsPerMonth: 50
},
cta: 'Get Started',
ctaHref: '/signup',
},
pro: {
name: 'Pro',
priceMonthly: 0, // fill from user
priceAnnual: 0, // fill from user
description: 'For serious users',
features: ['Everything in Free', '[Pro Feature 1]', '[Pro Feature 2]'],
limits: {
// creditsPerMonth: 500, etc.
},
cta: 'Upgrade to Pro',
highlighted: true,
badge: 'Most Popular',
},
} as const;
For a credits-based app, add credit pack definitions alongside plan definitions.
Phase 3: Feature Gating
Create a utility that checks access before any gated feature runs. This is the enforcement layer — everything else is display:
// lib/feature-gate.ts
type Plan = 'free' | 'pro' | 'enterprise';
// Define which plans can access which features
// Populate based on what actually exists in the codebase
const FEATURE_ACCESS: Record<string, Plan[]> = {
'api-access': ['pro', 'enterprise'],
'export-data': ['pro', 'enterprise'],
'custom-domain': ['enterprise'],
'priority-support': ['enterprise'],
};
export function canAccess(userPlan: Plan, feature: string): boolean {
return FEATURE_ACCESS[feature]?.includes(userPlan) ?? false;
}
In API routes, check before any expensive work:
export async function POST(req: Request) {
const user = await getAuthUser(req);
if (!canAccess(user.plan, 'api-access')) {
return Response.json(
{ error: 'This feature requires Pro.', upgradeUrl: '/pricing' },
{ status: 403 }
);
}
// ... rest of handler
}
In UI, show the locked state rather than hiding the feature. Users need to know the feature exists:
function ExportButton({ userPlan }: { userPlan: Plan }) {
if (!canAccess(userPlan, 'export-data')) {
return (
<button
onClick={() => router.push('/pricing')}
className="opacity-60"
title="Upgrade to Pro to export"
>
🔒 Export — Pro only
</button>
);
}
return <button onClick={handleExport}>Export</button>;
}
Phase 4: Dodo Payments Integration
Environment Variables
DODO_API_KEY= # From Dodo dashboard
DODO_WEBHOOK_SECRET= # whsec_... format — see /dodo-webhook skill
DODO_PRODUCT_ID= # Product ID for Pro plan
APP_URL= # Frontend URL for checkout redirect
Install: npm install @dodopayments/sdk
Checkout Creation Endpoint
// app/api/payments/create-checkout/route.ts
import DodoPayments from '@dodopayments/sdk';
const dodo = new DodoPayments({ bearerToken: process.env.DODO_API_KEY });
export async function POST(req: Request) {
const user = await getAuthUser(req);
const { planId } = await req.json();
const checkout = await dodo.payments.create({
payment_link: true,
customer: { email: user.email },
product_cart: [{ product_id: process.env.DODO_PRODUCT_ID!, quantity: 1 }],
metadata: {
userId: user.id, // REQUIRED — webhook uses this
planId, // which plan they're buying
},
return_url: `${process.env.APP_URL}/checkout/success?plan=${planId}`,
});
return Response.json({ checkout_url: checkout.payment_link });
}
The metadata is how your webhook finds the user. If userId isn't in metadata, the webhook can't update the right account. Use the /dodo-webhook skill to wire the webhook handler.
Customer Portal
Link users to Dodo's hosted billing portal for plan management, cancellation, and invoice history:
// app/api/billing/portal/route.ts
export async function GET(req: Request) {
const user = await getAuthUser(req);
const portal = await dodo.customerPortal.create({
customer_id: user.dodoCustomerId,
return_url: `${process.env.APP_URL}/settings/billing`,
});
return Response.json({ portal_url: portal.url });
}
Add a "Manage billing" link in user settings that hits this endpoint.
Checkout Success Page
Create /checkout/success — this is the page Dodo redirects to after payment. The webhook may arrive a few seconds after the redirect, so poll for the updated plan:
// app/checkout/success/page.tsx
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
export default function CheckoutSuccess() {
const [plan, setPlan] = useState<string | null>(null);
const router = useRouter();
useEffect(() => {
// Poll until plan updates (webhook may lag by a few seconds)
let attempts = 0;
const interval = setInterval(async () => {
const res = await fetch('/api/auth/me');
const user = await res.json();
if (user.plan !== 'free' || attempts > 10) {
setPlan(user.plan);
clearInterval(interval);
}
attempts++;
}, 1000);
return () => clearInterval(interval);
}, []);
if (!plan) return <p>Confirming your upgrade...</p>;
return (
<div>
<h1>You're on {plan}.</h1>
<p>Your account has been upgraded.</p>
<a href="/dashboard">Go to dashboard →</a>
</div>
);
}
Phase 5: Pricing UI Component
Generate a responsive pricing component. The design must emphasize one tier — users who see three equal-weight tiers often leave without deciding:
// components/pricing-cards.tsx
'use client';
import { PLANS } from '@/config/pricing';
interface PricingCardsProps {
currentPlan?: string;
onUpgrade?: (planId: string) => void;
}
export function PricingCards({ currentPlan, onUpgrade }: PricingCardsProps) {
const handleUpgrade = async (planId: string) => {
const res = await fetch('/api/payments/create-checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ planId }),
});
const { checkout_url } = await res.json();
window.location.href = checkout_url;
};
return (
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{Object.entries(PLANS).map(([planId, plan]) => (
<div
key={planId}
className={`rounded-xl border p-6 ${
'highlighted' in plan && plan.highlighted
? 'border-black shadow-xl'
: 'border-gray-200'
}`}
>
{'badge' in plan && plan.badge && (
<span className="text-xs font-bold uppercase tracking-widest text-black">
{plan.badge}
</span>
)}
<h3 className="mt-2 text-xl font-bold">{plan.name}</h3>
<p className="mt-1 text-3xl font-bold">
{'price' in plan ? (plan.price === 0 ? 'Free' : `$${plan.price}/mo`) : `$${plan.priceMonthly}/mo`}
</p>
<ul className="mt-4 space-y-2">
{plan.features.map((f) => (
<li key={f} className="flex items-center gap-2 text-sm text-gray-700">
<span className="text-green-500">✓</span> {f}
</li>
))}
</ul>
<div className="mt-6">
{currentPlan === planId ? (
<div className="py-2 text-center text-sm text-gray-400">Current plan</div>
) : (
<button
onClick={() => handleUpgrade(planId)}
className={`w-full rounded-lg py-2 text-sm font-medium ${
'highlighted' in plan && plan.highlighted
? 'bg-black text-white'
: 'border border-gray-300 text-gray-700 hover:border-black'
}`}
>
{plan.cta}
</button>
)}
</div>
</div>
))}
</div>
);
}
Phase 6: Verify
Flow 1: Feature Gating
[ ] Free user hits gated endpoint → 403 with upgradeUrl
[ ] Pro user hits same endpoint → proceeds normally
[ ] Gated UI shows locked state, links to /pricing
Flow 2: Checkout
[ ] "Upgrade" button creates checkout session
[ ] Redirects to Dodo-hosted checkout page
[ ] userId is in checkout metadata
[ ] After payment, redirects to /checkout/success
Flow 3: Webhook (handled by /dodo-webhook skill)
[ ] Webhook verified and processed
[ ] User plan updated in database
[ ] Success page reflects new plan after polling
Flow 4: Billing Portal
[ ] "Manage billing" link accessible in settings
[ ] Opens Dodo customer portal
[ ] Returns to app after portal actions
Flow 5: Edge Cases
[ ] Users without dodoCustomerId don't crash portal link
[ ] Checkout success polling stops after plan updates
[ ] Env vars in .env.example, not hardcoded
See references/guide.md for pricing psychology, A/B test ideas, and advanced feature gating patterns.
More from tushaarmehtaa/tushar-skills
ship-credits
Scaffold a complete credits/token metering system for any app — database schema, backend middleware, payment webhooks, frontend state, and UI components. Goes from zero to "users can buy and spend credits" in one session.
17deploy-check
Pre-flight check before pushing to production. Catches TypeScript errors, accidentally staged secrets, pending migrations, and hygiene gaps before they hit live users.
6ship-email
Scaffold transactional and campaign email infrastructure end-to-end — provider setup, templates, user segmentation, and admin send UI. Use when the user wants to add email to their app — welcome emails, notifications, re-engagement, or bulk campaigns. Triggers on requests like "add email", "set up Resend", "email campaigns", "transactional email", "send emails to users", "welcome email", "notification emails", or any mention of email sending in an app context.
6make-skill
Turn any workflow into a properly structured Claude Code skill — YAML frontmatter, phase-based instructions, real code blocks, and a verify checklist. Use when the user wants to package a repeated workflow, create a new skill, turn a process into a slash command, or publish to the skills directory. Triggers on requests like "make a skill", "create a skill", "turn this into a skill", "new skill for...", "package this as a skill", "build a skill", "I want to publish a skill", "help me write a skill", or any request to create a reusable Claude Code skill.
6mvp-spec
Turn a rough product idea into a structured MVP spec — problem statement, personas, core loop, feature split, data model, API routes, page list, and tech stack recommendation. Write this before touching any code. Triggers on requests like "spec this out", "MVP spec", "plan this product", "what should I build first", "scope this idea", "PRD", "product spec", "write a spec for...", "help me plan this", "what do I build in v1", "product requirements", or any request to structure a product idea before writing code.
6og-image
Set up dynamic Open Graph image generation and all required meta tags so links look professional when shared on Twitter/X, LinkedIn, Slack, or anywhere that renders link previews. Triggers on requests like "OG image", "open graph", "social preview", "link preview", "Twitter card", "meta tags for sharing", "my links look broken when I share them", or any mention of how links appear when shared on social media.
6