pricing-page

Scaffold a complete pricing system — tier definitions, feature gating, Dodo Payments integration, and a polished frontend component. Use when the user wants to add pricing, create a pricing page, set up payment tiers, integrate Dodo Payments, or implement feature gating. Triggers on requests like "pricing page", "add pricing", "subscription tiers", "Dodo Payments", "payment plans", "feature gating", "freemium model", "monetize my app", "charge users", or any mention of pricing, billing, or subscription management.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "pricing-page" with this command: npx skills add tushaarmehtaa/tushar-skills/tushaarmehtaa-tushar-skills-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.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

dodo-webhook

No summary provided by upstream source.

Repository SourceNeeds Review
General

deploy-check

No summary provided by upstream source.

Repository SourceNeeds Review
General

ship-credits

No summary provided by upstream source.

Repository SourceNeeds Review
General

aeo-ready

No summary provided by upstream source.

Repository SourceNeeds Review