Tiered Pricing Strategies for ChatGPT Apps: Complete Optimization Guide

Choosing the right pricing strategy for your ChatGPT app can make the difference between $10K MRR and $100K MRR. With 800 million weekly ChatGPT users and the ChatGPT App Store opening in December 2026, the opportunity is massive—but only if you price correctly.

This guide reveals battle-tested tiered pricing strategies used by top SaaS companies, adapted specifically for ChatGPT apps. You'll learn how to select value metrics, define tiers, implement feature gating, run pricing experiments, and optimize conversion rates.

Whether you're launching your first ChatGPT app or optimizing an existing pricing model, these strategies will help you maximize revenue while delivering exceptional customer value.

Value Metric Selection: The Foundation of Pricing

Your value metric determines how customers pay and directly impacts revenue scalability. The best value metric aligns with customer value perception and grows naturally with usage.

Four Value Metric Models

1. Usage-Based Pricing: Charge per API call, message, or tool execution. Best for apps with variable usage patterns (e.g., document processing, data analysis). Example: $0.01 per ChatGPT tool call.

2. Seat-Based Pricing: Charge per user or team member. Ideal for collaboration tools, team workspaces, or multi-user dashboards. Example: $29/user/month.

3. Feature-Based Pricing: Charge based on feature access (basic vs. premium features). Works well when features have clear value differentiation. Example: Basic ($49) vs. Pro ($149) with AI optimization.

4. Hybrid Models: Combine multiple metrics. Most flexible and revenue-optimized. Example: $49/month base + $0.005 per API call + $10 per additional user.

For ChatGPT apps specifically, hybrid models perform best because they capture both baseline value (subscription) and usage spikes (overage charges).

// Example 1: Pricing Tier Manager (TypeScript)
/**
 * PricingTierManager - Comprehensive pricing tier management system
 * Handles tier definitions, value metrics, entitlements, and upgrade logic
 */

interface ValueMetric {
  type: 'usage' | 'seat' | 'feature' | 'hybrid';
  unit: string; // "api_call", "user", "app", etc.
  baseAllowance: number;
  overageRate?: number; // For usage-based or hybrid
  pricePerUnit?: number; // For seat-based
}

interface PricingTier {
  id: string;
  name: string;
  displayName: string;
  price: number; // Monthly price in cents
  billingCycle: 'monthly' | 'annual';
  valueMetrics: ValueMetric[];
  features: {
    id: string;
    name: string;
    enabled: boolean;
    limit?: number;
  }[];
  popular?: boolean;
  annualDiscountPercent?: number;
}

class PricingTierManager {
  private tiers: Map<string, PricingTier> = new Map();

  constructor() {
    this.initializeTiers();
  }

  private initializeTiers(): void {
    // Free Tier (Lead Magnet)
    this.tiers.set('free', {
      id: 'free',
      name: 'free',
      displayName: 'Free',
      price: 0,
      billingCycle: 'monthly',
      valueMetrics: [
        {
          type: 'usage',
          unit: 'tool_calls',
          baseAllowance: 1000,
          overageRate: 0 // No overage allowed
        }
      ],
      features: [
        { id: 'instant_app', name: 'Instant App Wizard', enabled: true },
        { id: 'basic_templates', name: 'Basic Templates', enabled: true, limit: 1 },
        { id: 'custom_domain', name: 'Custom Domain', enabled: false },
        { id: 'ai_optimization', name: 'AI Optimization', enabled: false },
        { id: 'analytics', name: 'Analytics Dashboard', enabled: false },
        { id: 'priority_support', name: 'Priority Support', enabled: false }
      ]
    });

    // Starter Tier (Entry-Level Paid)
    this.tiers.set('starter', {
      id: 'starter',
      name: 'starter',
      displayName: 'Starter',
      price: 4900, // $49/month
      billingCycle: 'monthly',
      valueMetrics: [
        {
          type: 'hybrid',
          unit: 'apps',
          baseAllowance: 3,
          pricePerUnit: 1000 // $10 per additional app
        },
        {
          type: 'usage',
          unit: 'tool_calls',
          baseAllowance: 10000,
          overageRate: 0.5 // $0.005 per call
        }
      ],
      features: [
        { id: 'instant_app', name: 'Instant App Wizard', enabled: true },
        { id: 'basic_templates', name: 'Basic Templates', enabled: true, limit: 3 },
        { id: 'custom_domain', name: 'Custom Domain', enabled: false },
        { id: 'ai_optimization', name: 'AI Optimization', enabled: false },
        { id: 'analytics', name: 'Analytics Dashboard', enabled: true },
        { id: 'priority_support', name: 'Priority Support', enabled: false }
      ],
      annualDiscountPercent: 20 // $470.40/year instead of $588
    });

    // Professional Tier (Primary Revenue Driver)
    this.tiers.set('professional', {
      id: 'professional',
      name: 'professional',
      displayName: 'Professional',
      price: 14900, // $149/month
      billingCycle: 'monthly',
      valueMetrics: [
        {
          type: 'hybrid',
          unit: 'apps',
          baseAllowance: 10,
          pricePerUnit: 1000
        },
        {
          type: 'usage',
          unit: 'tool_calls',
          baseAllowance: 50000,
          overageRate: 0.4 // $0.004 per call (20% discount)
        }
      ],
      features: [
        { id: 'instant_app', name: 'Instant App Wizard', enabled: true },
        { id: 'basic_templates', name: 'All Templates', enabled: true, limit: 10 },
        { id: 'custom_domain', name: 'Custom Domain', enabled: true },
        { id: 'ai_optimization', name: 'AI Optimization', enabled: true },
        { id: 'analytics', name: 'Advanced Analytics', enabled: true },
        { id: 'priority_support', name: 'Priority Support', enabled: true }
      ],
      popular: true,
      annualDiscountPercent: 20
    });

    // Business Tier (High-Volume)
    this.tiers.set('business', {
      id: 'business',
      name: 'business',
      displayName: 'Business',
      price: 29900, // $299/month
      billingCycle: 'monthly',
      valueMetrics: [
        {
          type: 'hybrid',
          unit: 'apps',
          baseAllowance: 50,
          pricePerUnit: 500 // $5 per additional app (50% discount)
        },
        {
          type: 'usage',
          unit: 'tool_calls',
          baseAllowance: 200000,
          overageRate: 0.3 // $0.003 per call (40% discount)
        }
      ],
      features: [
        { id: 'instant_app', name: 'Instant App Wizard', enabled: true },
        { id: 'basic_templates', name: 'Unlimited Templates', enabled: true },
        { id: 'custom_domain', name: 'Custom Domain', enabled: true },
        { id: 'ai_optimization', name: 'AI Optimization', enabled: true },
        { id: 'analytics', name: 'Advanced Analytics', enabled: true },
        { id: 'priority_support', name: 'Dedicated Support', enabled: true },
        { id: 'api_access', name: 'API Access', enabled: true },
        { id: 'white_label', name: 'White Label', enabled: true }
      ],
      annualDiscountPercent: 25
    });
  }

  getTier(tierId: string): PricingTier | undefined {
    return this.tiers.get(tierId);
  }

  getAllTiers(): PricingTier[] {
    return Array.from(this.tiers.values());
  }

  getVisibleTiers(): PricingTier[] {
    // Return tiers in display order (omit free for pricing page)
    return [
      this.tiers.get('starter')!,
      this.tiers.get('professional')!,
      this.tiers.get('business')!
    ];
  }

  calculateMonthlyPrice(tierId: string, annualBilling: boolean = false): number {
    const tier = this.getTier(tierId);
    if (!tier) return 0;

    if (annualBilling && tier.annualDiscountPercent) {
      return tier.price * (1 - tier.annualDiscountPercent / 100);
    }

    return tier.price;
  }

  calculateAnnualSavings(tierId: string): number {
    const tier = this.getTier(tierId);
    if (!tier || !tier.annualDiscountPercent) return 0;

    const monthlyTotal = tier.price * 12;
    const annualTotal = this.calculateMonthlyPrice(tierId, true) * 12;
    return monthlyTotal - annualTotal;
  }
}

export { PricingTierManager, PricingTier, ValueMetric };

Learn more about ChatGPT App Builder pricing models and monetization strategies for ChatGPT apps.

Tier Definition: Good-Better-Best Psychology

The "Good-Better-Best" pricing model leverages anchoring bias and choice architecture to drive customers toward your target tier (usually the middle option).

Key Principles

  1. Three-Tier Minimum: Offer at least 3 paid tiers. Four tiers (Free + 3 paid) is optimal for ChatGPT apps.
  2. Anchor High: The highest tier should be 2-3x the middle tier price to make the middle tier feel reasonable.
  3. Mark the Winner: Label your target tier as "Most Popular" or "Recommended" to guide decision-making.
  4. Clear Value Ladder: Each tier should offer 30-50% more value than the previous tier.
// Example 2: Entitlement Service (TypeScript)
/**
 * EntitlementService - Manages feature access and usage limits
 * Enforces tier-based permissions and tracks usage against quotas
 */

interface Subscription {
  userId: string;
  tierId: string;
  status: 'active' | 'canceled' | 'past_due' | 'trialing';
  currentPeriodStart: Date;
  currentPeriodEnd: Date;
  cancelAtPeriodEnd: boolean;
}

interface UsageRecord {
  userId: string;
  metric: string; // "tool_calls", "apps_created", etc.
  count: number;
  periodStart: Date;
  periodEnd: Date;
}

class EntitlementService {
  constructor(
    private pricingManager: PricingTierManager,
    private db: Database
  ) {}

  async hasFeatureAccess(userId: string, featureId: string): Promise<boolean> {
    const subscription = await this.getActiveSubscription(userId);
    if (!subscription) {
      // Default to free tier
      const freeTier = this.pricingManager.getTier('free');
      return freeTier?.features.find(f => f.id === featureId)?.enabled ?? false;
    }

    const tier = this.pricingManager.getTier(subscription.tierId);
    if (!tier) return false;

    const feature = tier.features.find(f => f.id === featureId);
    return feature?.enabled ?? false;
  }

  async canCreateApp(userId: string): Promise<{
    allowed: boolean;
    reason?: string;
    currentUsage?: number;
    limit?: number;
  }> {
    const subscription = await this.getActiveSubscription(userId);
    const tierId = subscription?.tierId ?? 'free';
    const tier = this.pricingManager.getTier(tierId);

    if (!tier) {
      return { allowed: false, reason: 'Invalid subscription tier' };
    }

    // Find apps metric
    const appsMetric = tier.valueMetrics.find(m => m.unit === 'apps');
    if (!appsMetric) {
      return { allowed: true }; // No limit
    }

    // Check current usage
    const usage = await this.getCurrentUsage(userId, 'apps_created');
    const limit = appsMetric.baseAllowance;

    if (usage >= limit) {
      return {
        allowed: false,
        reason: `App limit reached (${limit} apps). Upgrade to create more.`,
        currentUsage: usage,
        limit: limit
      };
    }

    return {
      allowed: true,
      currentUsage: usage,
      limit: limit
    };
  }

  async canExecuteToolCall(userId: string): Promise<{
    allowed: boolean;
    reason?: string;
    willIncurOverage?: boolean;
    overageCost?: number;
  }> {
    const subscription = await this.getActiveSubscription(userId);
    const tierId = subscription?.tierId ?? 'free';
    const tier = this.pricingManager.getTier(tierId);

    if (!tier) {
      return { allowed: false, reason: 'Invalid subscription tier' };
    }

    // Find tool_calls metric
    const callsMetric = tier.valueMetrics.find(m => m.unit === 'tool_calls');
    if (!callsMetric) {
      return { allowed: true }; // No limit
    }

    const usage = await this.getCurrentUsage(userId, 'tool_calls');
    const limit = callsMetric.baseAllowance;

    // Check if overage is allowed
    if (usage >= limit) {
      if (callsMetric.overageRate && callsMetric.overageRate > 0) {
        // Overage allowed
        return {
          allowed: true,
          willIncurOverage: true,
          overageCost: callsMetric.overageRate // In cents
        };
      } else {
        // Hard limit (free tier)
        return {
          allowed: false,
          reason: `Monthly limit reached (${limit} tool calls). Upgrade to continue.`
        };
      }
    }

    return { allowed: true, willIncurOverage: false };
  }

  async recordUsage(userId: string, metric: string, count: number = 1): Promise<void> {
    const subscription = await this.getActiveSubscription(userId);
    const periodStart = subscription?.currentPeriodStart ?? new Date();
    const periodEnd = subscription?.currentPeriodEnd ?? new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);

    await this.db.query(`
      INSERT INTO usage_records (user_id, metric, count, period_start, period_end, recorded_at)
      VALUES ($1, $2, $3, $4, $5, NOW())
      ON CONFLICT (user_id, metric, period_start, period_end)
      DO UPDATE SET count = usage_records.count + $3, updated_at = NOW()
    `, [userId, metric, count, periodStart, periodEnd]);
  }

  async getCurrentUsage(userId: string, metric: string): Promise<number> {
    const subscription = await this.getActiveSubscription(userId);
    const periodStart = subscription?.currentPeriodStart ?? new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);

    const result = await this.db.query(`
      SELECT COALESCE(SUM(count), 0) as total
      FROM usage_records
      WHERE user_id = $1 AND metric = $2 AND period_start >= $3
    `, [userId, metric, periodStart]);

    return parseInt(result.rows[0]?.total ?? '0', 10);
  }

  async getActiveSubscription(userId: string): Promise<Subscription | null> {
    const result = await this.db.query(`
      SELECT * FROM subscriptions
      WHERE user_id = $1 AND status IN ('active', 'trialing')
      ORDER BY current_period_end DESC
      LIMIT 1
    `, [userId]);

    return result.rows[0] ?? null;
  }

  async getUpgradeRecommendation(userId: string): Promise<{
    shouldUpgrade: boolean;
    reason?: string;
    recommendedTier?: string;
  }> {
    const subscription = await this.getActiveSubscription(userId);
    const currentTierId = subscription?.tierId ?? 'free';

    // Check usage patterns
    const toolCalls = await this.getCurrentUsage(userId, 'tool_calls');
    const appsCreated = await this.getCurrentUsage(userId, 'apps_created');

    const currentTier = this.pricingManager.getTier(currentTierId);
    if (!currentTier) {
      return { shouldUpgrade: false };
    }

    // Check if user is hitting limits frequently
    const toolCallsMetric = currentTier.valueMetrics.find(m => m.unit === 'tool_calls');
    const appsMetric = currentTier.valueMetrics.find(m => m.unit === 'apps');

    if (toolCallsMetric && toolCalls >= toolCallsMetric.baseAllowance * 0.8) {
      // User has used 80% of tool calls
      const nextTier = this.getNextTier(currentTierId);
      return {
        shouldUpgrade: true,
        reason: `You've used ${toolCalls} of ${toolCallsMetric.baseAllowance} tool calls this month.`,
        recommendedTier: nextTier?.id
      };
    }

    if (appsMetric && appsCreated >= appsMetric.baseAllowance * 0.8) {
      const nextTier = this.getNextTier(currentTierId);
      return {
        shouldUpgrade: true,
        reason: `You've created ${appsCreated} of ${appsMetric.baseAllowance} apps.`,
        recommendedTier: nextTier?.id
      };
    }

    return { shouldUpgrade: false };
  }

  private getNextTier(currentTierId: string): PricingTier | null {
    const tierOrder = ['free', 'starter', 'professional', 'business'];
    const currentIndex = tierOrder.indexOf(currentTierId);
    if (currentIndex === -1 || currentIndex === tierOrder.length - 1) {
      return null;
    }
    return this.pricingManager.getTier(tierOrder[currentIndex + 1]) ?? null;
  }
}

export { EntitlementService, Subscription, UsageRecord };

Explore ChatGPT app monetization techniques and SaaS pricing best practices to refine your tier structure.

Feature Gating: Driving Upgrade Conversions

Feature gating is the art of restricting premium features to paid tiers while maintaining a great free user experience. Done right, it drives upgrades without frustrating users.

Feature Gating Best Practices

1. Gate Value, Not Usability: Free users should be able to complete core workflows. Gate advanced features (analytics, AI optimization, custom domains) rather than basic functionality.

2. Contextual Upgrade Prompts: Show upgrade prompts when users attempt to use gated features, not randomly. Include clear value propositions.

3. Progressive Disclosure: Let users see (but not use) premium features. This builds desire without creating frustration.

// Example 3: Feature Gate Middleware (TypeScript)
/**
 * FeatureGateMiddleware - Express middleware for API feature gating
 * Enforces tier-based access control with contextual upgrade messaging
 */

import { Request, Response, NextFunction } from 'express';
import { EntitlementService } from './entitlement-service';

interface FeatureGateOptions {
  featureId: string;
  featureName: string;
  minimumTier: 'free' | 'starter' | 'professional' | 'business';
  upgradeMessage?: string;
  allowTrial?: boolean;
}

class FeatureGateMiddleware {
  constructor(private entitlementService: EntitlementService) {}

  /**
   * Middleware factory for feature gating
   */
  gate(options: FeatureGateOptions) {
    return async (req: Request, res: Response, next: NextFunction) => {
      const userId = req.user?.uid;
      if (!userId) {
        return res.status(401).json({
          error: 'Unauthorized',
          message: 'Authentication required'
        });
      }

      const hasAccess = await this.entitlementService.hasFeatureAccess(
        userId,
        options.featureId
      );

      if (!hasAccess) {
        const subscription = await this.entitlementService.getActiveSubscription(userId);
        const currentTier = subscription?.tierId ?? 'free';

        return res.status(403).json({
          error: 'Feature not available',
          message: options.upgradeMessage ?? `${options.featureName} requires ${options.minimumTier} tier or higher.`,
          currentTier,
          minimumTier: options.minimumTier,
          upgradeUrl: `/pricing?upgrade=${options.minimumTier}`,
          feature: options.featureName
        });
      }

      next();
    };
  }

  /**
   * Usage-based gating (tool calls, API requests)
   */
  usageGate(metric: string) {
    return async (req: Request, res: Response, next: NextFunction) => {
      const userId = req.user?.uid;
      if (!userId) {
        return res.status(401).json({
          error: 'Unauthorized',
          message: 'Authentication required'
        });
      }

      let allowed = false;
      let errorMessage = '';
      let willIncurOverage = false;
      let overageCost = 0;

      if (metric === 'tool_calls') {
        const check = await this.entitlementService.canExecuteToolCall(userId);
        allowed = check.allowed;
        errorMessage = check.reason ?? '';
        willIncurOverage = check.willIncurOverage ?? false;
        overageCost = check.overageCost ?? 0;
      }

      if (!allowed) {
        return res.status(429).json({
          error: 'Usage limit exceeded',
          message: errorMessage,
          upgradeUrl: '/pricing',
          metric
        });
      }

      // Attach usage info to request
      req.usageInfo = { willIncurOverage, overageCost };

      next();
    };
  }

  /**
   * Soft gate - allows access but shows upgrade prompt
   */
  softGate(options: FeatureGateOptions) {
    return async (req: Request, res: Response, next: NextFunction) => {
      const userId = req.user?.uid;
      if (!userId) {
        return next();
      }

      const hasAccess = await this.entitlementService.hasFeatureAccess(
        userId,
        options.featureId
      );

      // Attach gate info to request
      req.featureGateInfo = {
        hasAccess,
        featureName: options.featureName,
        minimumTier: options.minimumTier,
        upgradeMessage: options.upgradeMessage
      };

      next();
    };
  }

  /**
   * Resource limit gate (apps, users, projects)
   */
  resourceLimitGate(resourceType: 'apps' | 'users' | 'projects') {
    return async (req: Request, res: Response, next: NextFunction) => {
      const userId = req.user?.uid;
      if (!userId) {
        return res.status(401).json({
          error: 'Unauthorized',
          message: 'Authentication required'
        });
      }

      let check: { allowed: boolean; reason?: string; currentUsage?: number; limit?: number };

      if (resourceType === 'apps') {
        check = await this.entitlementService.canCreateApp(userId);
      } else {
        // Implement other resource checks
        check = { allowed: true };
      }

      if (!check.allowed) {
        return res.status(403).json({
          error: 'Resource limit exceeded',
          message: check.reason,
          currentUsage: check.currentUsage,
          limit: check.limit,
          upgradeUrl: '/pricing',
          resourceType
        });
      }

      next();
    };
  }
}

// Usage examples:
/*
app.post('/api/apps',
  authenticate,
  featureGate.resourceLimitGate('apps'),
  createAppHandler
);

app.post('/api/chatgpt/tool-call',
  authenticate,
  featureGate.usageGate('tool_calls'),
  executeToolCallHandler
);

app.get('/api/analytics/advanced',
  authenticate,
  featureGate.gate({
    featureId: 'advanced_analytics',
    featureName: 'Advanced Analytics',
    minimumTier: 'professional',
    upgradeMessage: 'Unlock advanced analytics with real-time insights, custom reports, and AI-powered recommendations.'
  }),
  getAdvancedAnalyticsHandler
);
*/

export { FeatureGateMiddleware, FeatureGateOptions };

Review ChatGPT app feature gating patterns and upgrade conversion optimization for implementation strategies.

Pricing Experimentation: Data-Driven Optimization

Pricing is never "set it and forget it." The best SaaS companies continuously experiment with pricing to maximize revenue per customer.

Experiment Types

1. Price Point Testing: Test different price levels for the same tier (e.g., $49 vs. $59 vs. $69).

2. Tier Structure Testing: Test 3-tier vs. 4-tier models, or different feature bundles.

3. Discount Testing: Test annual discounts (15% vs. 20% vs. 25%), promotional discounts, volume discounts.

4. Value Metric Testing: Test usage-based vs. seat-based vs. hybrid models.

// Example 4: Plan Comparer Component (React)
/**
 * PlanComparerComponent - Interactive pricing table with upgrade prompts
 * Displays tier comparison, feature matrix, and conversion-optimized CTAs
 */

import React, { useState, useEffect } from 'react';
import { PricingTier, PricingTierManager } from './pricing-tier-manager';

interface PlanComparerProps {
  currentTierId?: string;
  highlightTier?: string;
  showAnnualToggle?: boolean;
  experimentVariant?: 'control' | 'variant_a' | 'variant_b';
}

const PlanComparerComponent: React.FC<PlanComparerProps> = ({
  currentTierId,
  highlightTier = 'professional',
  showAnnualToggle = true,
  experimentVariant = 'control'
}) => {
  const [pricingManager] = useState(() => new PricingTierManager());
  const [annualBilling, setAnnualBilling] = useState(false);
  const [tiers, setTiers] = useState<PricingTier[]>([]);

  useEffect(() => {
    setTiers(pricingManager.getVisibleTiers());
  }, [pricingManager]);

  const formatPrice = (priceInCents: number): string => {
    return `$${(priceInCents / 100).toFixed(0)}`;
  };

  const getCTAText = (tierId: string): string => {
    if (currentTierId === tierId) {
      return 'Current Plan';
    }

    if (currentTierId) {
      const tierOrder = ['free', 'starter', 'professional', 'business'];
      const currentIndex = tierOrder.indexOf(currentTierId);
      const targetIndex = tierOrder.indexOf(tierId);

      if (targetIndex > currentIndex) {
        return 'Upgrade Now';
      } else {
        return 'Downgrade';
      }
    }

    return 'Start Free Trial';
  };

  const isCurrentPlan = (tierId: string): boolean => {
    return currentTierId === tierId;
  };

  const shouldHighlight = (tierId: string): boolean => {
    return tierId === highlightTier;
  };

  const getAnnualSavings = (tier: PricingTier): number => {
    return pricingManager.calculateAnnualSavings(tier.id);
  };

  return (
    <div className="plan-comparer">
      {showAnnualToggle && (
        <div className="billing-toggle">
          <label>
            <input
              type="checkbox"
              checked={annualBilling}
              onChange={(e) => setAnnualBilling(e.target.checked)}
            />
            Annual Billing (Save up to 25%)
          </label>
        </div>
      )}

      <div className="tiers-grid">
        {tiers.map((tier) => {
          const monthlyPrice = pricingManager.calculateMonthlyPrice(tier.id, annualBilling);
          const annualSavings = annualBilling ? getAnnualSavings(tier) : 0;

          return (
            <div
              key={tier.id}
              className={`tier-card ${shouldHighlight(tier.id) ? 'highlighted' : ''} ${isCurrentPlan(tier.id) ? 'current' : ''}`}
            >
              {tier.popular && <div className="badge">Most Popular</div>}
              {isCurrentPlan(tier.id) && <div className="badge current">Current Plan</div>}

              <h3 className="tier-name">{tier.displayName}</h3>

              <div className="pricing">
                <span className="price">{formatPrice(monthlyPrice)}</span>
                <span className="period">/month</span>
              </div>

              {annualBilling && annualSavings > 0 && (
                <div className="savings">
                  Save {formatPrice(annualSavings)}/year
                </div>
              )}

              <button
                className={`cta-button ${shouldHighlight(tier.id) ? 'primary' : 'secondary'}`}
                disabled={isCurrentPlan(tier.id)}
              >
                {getCTAText(tier.id)}
              </button>

              <div className="features-list">
                <h4>Features</h4>
                <ul>
                  {tier.features.map((feature) => (
                    <li key={feature.id} className={feature.enabled ? 'enabled' : 'disabled'}>
                      <span className="icon">{feature.enabled ? '✓' : '✗'}</span>
                      <span className="feature-name">
                        {feature.name}
                        {feature.limit && ` (${feature.limit})`}
                      </span>
                    </li>
                  ))}
                </ul>
              </div>

              <div className="value-metrics">
                <h4>Usage Limits</h4>
                <ul>
                  {tier.valueMetrics.map((metric, idx) => (
                    <li key={idx}>
                      <strong>{metric.baseAllowance.toLocaleString()}</strong> {metric.unit}
                      {metric.overageRate && metric.overageRate > 0 && (
                        <span className="overage">
                          {' '}+ ${(metric.overageRate / 100).toFixed(3)} per additional {metric.unit}
                        </span>
                      )}
                    </li>
                  ))}
                </ul>
              </div>
            </div>
          );
        })}
      </div>

      <style jsx>{`
        .plan-comparer {
          max-width: 1200px;
          margin: 0 auto;
          padding: 2rem;
        }

        .billing-toggle {
          text-align: center;
          margin-bottom: 2rem;
        }

        .billing-toggle label {
          font-size: 1.1rem;
          cursor: pointer;
        }

        .tiers-grid {
          display: grid;
          grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
          gap: 2rem;
        }

        .tier-card {
          background: rgba(255, 255, 255, 0.02);
          border: 1px solid rgba(212, 175, 55, 0.2);
          border-radius: 12px;
          padding: 2rem;
          position: relative;
          transition: transform 0.2s, border-color 0.2s;
        }

        .tier-card:hover {
          transform: translateY(-4px);
          border-color: rgba(212, 175, 55, 0.5);
        }

        .tier-card.highlighted {
          border-color: #D4AF37;
          border-width: 2px;
          transform: scale(1.05);
        }

        .tier-card.current {
          opacity: 0.7;
        }

        .badge {
          position: absolute;
          top: -12px;
          left: 50%;
          transform: translateX(-50%);
          background: #D4AF37;
          color: #0A0E27;
          padding: 0.25rem 1rem;
          border-radius: 12px;
          font-size: 0.875rem;
          font-weight: 600;
        }

        .badge.current {
          background: #4A90E2;
        }

        .tier-name {
          font-size: 1.5rem;
          margin-bottom: 1rem;
          color: #D4AF37;
        }

        .pricing {
          margin-bottom: 1rem;
        }

        .price {
          font-size: 3rem;
          font-weight: 700;
          color: #FFFFFF;
        }

        .period {
          font-size: 1rem;
          color: #A0A0A0;
        }

        .savings {
          color: #4CAF50;
          font-size: 0.875rem;
          margin-bottom: 1rem;
        }

        .cta-button {
          width: 100%;
          padding: 1rem;
          border: none;
          border-radius: 8px;
          font-size: 1rem;
          font-weight: 600;
          cursor: pointer;
          transition: background 0.2s;
          margin-bottom: 2rem;
        }

        .cta-button.primary {
          background: linear-gradient(135deg, #D4AF37, #F4D03F);
          color: #0A0E27;
        }

        .cta-button.primary:hover {
          background: linear-gradient(135deg, #F4D03F, #D4AF37);
        }

        .cta-button.secondary {
          background: rgba(212, 175, 55, 0.1);
          color: #D4AF37;
          border: 1px solid #D4AF37;
        }

        .cta-button.secondary:hover {
          background: rgba(212, 175, 55, 0.2);
        }

        .cta-button:disabled {
          opacity: 0.5;
          cursor: not-allowed;
        }

        .features-list, .value-metrics {
          margin-top: 1.5rem;
        }

        .features-list h4, .value-metrics h4 {
          font-size: 1rem;
          margin-bottom: 0.75rem;
          color: #E8E9ED;
        }

        .features-list ul, .value-metrics ul {
          list-style: none;
          padding: 0;
        }

        .features-list li {
          padding: 0.5rem 0;
          display: flex;
          align-items: center;
        }

        .features-list li.enabled {
          color: #E8E9ED;
        }

        .features-list li.disabled {
          color: #606060;
        }

        .icon {
          margin-right: 0.5rem;
          font-weight: bold;
        }

        .value-metrics li {
          padding: 0.5rem 0;
          color: #E8E9ED;
        }

        .overage {
          font-size: 0.875rem;
          color: #A0A0A0;
        }
      `}</style>
    </div>
  );
};

export default PlanComparerComponent;
// Example 5: Upgrade Prompt System (TypeScript)
/**
 * UpgradePromptSystem - Contextual upgrade prompts with A/B testing
 * Displays targeted upgrade messages based on user behavior and tier limits
 */

interface UpgradePrompt {
  id: string;
  triggerId: string; // "apps_limit_reached", "tool_calls_80_percent", etc.
  title: string;
  message: string;
  ctaText: string;
  ctaUrl: string;
  variant?: 'control' | 'variant_a' | 'variant_b';
  urgency?: 'low' | 'medium' | 'high';
}

class UpgradePromptSystem {
  private prompts: Map<string, UpgradePrompt[]> = new Map();

  constructor() {
    this.initializePrompts();
  }

  private initializePrompts(): void {
    // Apps limit prompts
    this.prompts.set('apps_limit_reached', [
      {
        id: 'apps_limit_control',
        triggerId: 'apps_limit_reached',
        title: 'App Limit Reached',
        message: 'You\'ve reached your plan\'s app limit. Upgrade to create more apps.',
        ctaText: 'View Plans',
        ctaUrl: '/pricing',
        variant: 'control',
        urgency: 'high'
      },
      {
        id: 'apps_limit_variant_a',
        triggerId: 'apps_limit_reached',
        title: 'Unlock Unlimited Apps',
        message: 'You\'re building amazing ChatGPT apps! Upgrade to Professional and create up to 10 apps with advanced features.',
        ctaText: 'Upgrade to Pro - $149/mo',
        ctaUrl: '/pricing?plan=professional',
        variant: 'variant_a',
        urgency: 'medium'
      }
    ]);

    // Tool calls limit prompts
    this.prompts.set('tool_calls_80_percent', [
      {
        id: 'tool_calls_80_control',
        triggerId: 'tool_calls_80_percent',
        title: 'Usage Warning',
        message: 'You\'ve used 80% of your monthly tool call limit. Consider upgrading to avoid interruptions.',
        ctaText: 'Upgrade Now',
        ctaUrl: '/pricing',
        variant: 'control',
        urgency: 'medium'
      },
      {
        id: 'tool_calls_80_variant_a',
        triggerId: 'tool_calls_80_percent',
        title: 'Running Low on Tool Calls',
        message: 'You\'ve used 80% of your 10,000 tool calls this month. Upgrade to Professional for 50,000 calls/month + overage protection.',
        ctaText: 'Get 5x More Calls - $149/mo',
        ctaUrl: '/pricing?plan=professional&source=usage_warning',
        variant: 'variant_a',
        urgency: 'high'
      }
    ]);

    // Feature gate prompts
    this.prompts.set('custom_domain_gated', [
      {
        id: 'custom_domain_control',
        triggerId: 'custom_domain_gated',
        title: 'Custom Domain Unavailable',
        message: 'Custom domains are available on Professional and Business plans.',
        ctaText: 'Upgrade to Pro',
        ctaUrl: '/pricing?plan=professional',
        variant: 'control',
        urgency: 'low'
      },
      {
        id: 'custom_domain_variant_a',
        triggerId: 'custom_domain_gated',
        title: 'Brand Your ChatGPT App',
        message: 'Use your own domain (e.g., app.yourbrand.com) to build trust and SEO authority. Available on Pro plans.',
        ctaText: 'Add Custom Domain - Upgrade to Pro',
        ctaUrl: '/pricing?plan=professional&feature=custom_domain',
        variant: 'variant_a',
        urgency: 'medium'
      }
    ]);

    // Analytics gate prompts
    this.prompts.set('analytics_gated', [
      {
        id: 'analytics_control',
        triggerId: 'analytics_gated',
        title: 'Analytics Unavailable',
        message: 'Advanced analytics require a paid plan.',
        ctaText: 'View Plans',
        ctaUrl: '/pricing',
        variant: 'control',
        urgency: 'low'
      },
      {
        id: 'analytics_variant_a',
        triggerId: 'analytics_gated',
        title: 'Unlock Data-Driven Insights',
        message: 'See exactly how users interact with your ChatGPT app. Get real-time analytics, conversion tracking, and AI-powered recommendations on Pro plans.',
        ctaText: 'Start Free Trial - See Your Data',
        ctaUrl: '/pricing?plan=professional&feature=analytics&trial=true',
        variant: 'variant_a',
        urgency: 'medium'
      }
    ]);
  }

  getPrompt(triggerId: string, variant: 'control' | 'variant_a' | 'variant_b' = 'control'): UpgradePrompt | null {
    const prompts = this.prompts.get(triggerId);
    if (!prompts || prompts.length === 0) return null;

    const matchingPrompt = prompts.find(p => p.variant === variant);
    return matchingPrompt ?? prompts[0];
  }

  getAllTriggersForUser(userId: string, usageData: {
    appsCreated: number;
    appsLimit: number;
    toolCalls: number;
    toolCallsLimit: number;
    currentTier: string;
  }): string[] {
    const triggers: string[] = [];

    // Check apps limit
    if (usageData.appsCreated >= usageData.appsLimit) {
      triggers.push('apps_limit_reached');
    } else if (usageData.appsCreated >= usageData.appsLimit * 0.8) {
      triggers.push('apps_80_percent');
    }

    // Check tool calls limit
    if (usageData.toolCalls >= usageData.toolCallsLimit * 0.8) {
      triggers.push('tool_calls_80_percent');
    }

    if (usageData.toolCalls >= usageData.toolCallsLimit) {
      triggers.push('tool_calls_limit_reached');
    }

    return triggers;
  }

  async trackPromptImpression(promptId: string, userId: string): Promise<void> {
    // Track that this prompt was shown to this user
    // Used for A/B testing analysis
    console.log(`Prompt impression: ${promptId} for user ${userId}`);
  }

  async trackPromptClick(promptId: string, userId: string): Promise<void> {
    // Track that user clicked on upgrade prompt
    // Used for conversion rate analysis
    console.log(`Prompt click: ${promptId} for user ${userId}`);
  }

  async trackUpgradeConversion(promptId: string, userId: string, newTierId: string): Promise<void> {
    // Track that user upgraded after seeing prompt
    // Used to measure prompt effectiveness
    console.log(`Upgrade conversion: ${promptId} -> ${newTierId} for user ${userId}`);
  }
}

export { UpgradePromptSystem, UpgradePrompt };

See A/B testing for ChatGPT apps and pricing optimization strategies for detailed methodologies.

Implementation Patterns: Technical Architecture

Here's the complete technical architecture for implementing tiered pricing in your ChatGPT app, including database schema, API design, and usage tracking.

-- Example 6: Database Schema (SQL)
-- Complete database schema for tiered pricing with subscriptions, usage tracking, and billing

-- Users table
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email VARCHAR(255) UNIQUE NOT NULL,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

-- Subscriptions table
CREATE TABLE subscriptions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  tier_id VARCHAR(50) NOT NULL, -- 'free', 'starter', 'professional', 'business'
  status VARCHAR(50) NOT NULL, -- 'active', 'canceled', 'past_due', 'trialing'
  stripe_subscription_id VARCHAR(255) UNIQUE,
  stripe_customer_id VARCHAR(255),
  current_period_start TIMESTAMP NOT NULL,
  current_period_end TIMESTAMP NOT NULL,
  cancel_at_period_end BOOLEAN DEFAULT FALSE,
  trial_end TIMESTAMP,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW(),
  UNIQUE(user_id, status) -- Only one active subscription per user
);

CREATE INDEX idx_subscriptions_user_id ON subscriptions(user_id);
CREATE INDEX idx_subscriptions_status ON subscriptions(status);
CREATE INDEX idx_subscriptions_stripe_customer ON subscriptions(stripe_customer_id);

-- Usage records table
CREATE TABLE usage_records (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  subscription_id UUID REFERENCES subscriptions(id) ON DELETE SET NULL,
  metric VARCHAR(50) NOT NULL, -- 'tool_calls', 'apps_created', 'api_requests', etc.
  count INTEGER NOT NULL DEFAULT 0,
  period_start TIMESTAMP NOT NULL,
  period_end TIMESTAMP NOT NULL,
  recorded_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW(),
  UNIQUE(user_id, metric, period_start, period_end)
);

CREATE INDEX idx_usage_user_metric ON usage_records(user_id, metric);
CREATE INDEX idx_usage_period ON usage_records(period_start, period_end);

-- Overage charges table
CREATE TABLE overage_charges (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  subscription_id UUID NOT NULL REFERENCES subscriptions(id) ON DELETE CASCADE,
  metric VARCHAR(50) NOT NULL,
  units_consumed INTEGER NOT NULL,
  rate_per_unit INTEGER NOT NULL, -- In cents
  total_charge INTEGER NOT NULL, -- In cents
  period_start TIMESTAMP NOT NULL,
  period_end TIMESTAMP NOT NULL,
  stripe_invoice_id VARCHAR(255),
  paid BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_overage_user ON overage_charges(user_id);
CREATE INDEX idx_overage_subscription ON overage_charges(subscription_id);
CREATE INDEX idx_overage_period ON overage_charges(period_start, period_end);

-- Billing events table (audit log)
CREATE TABLE billing_events (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  subscription_id UUID REFERENCES subscriptions(id) ON DELETE SET NULL,
  event_type VARCHAR(100) NOT NULL, -- 'subscription.created', 'subscription.upgraded', 'payment.succeeded', etc.
  event_data JSONB,
  stripe_event_id VARCHAR(255),
  created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_billing_events_user ON billing_events(user_id);
CREATE INDEX idx_billing_events_type ON billing_events(event_type);
CREATE INDEX idx_billing_events_created ON billing_events(created_at DESC);

-- Upgrade prompts tracking (for A/B testing)
CREATE TABLE upgrade_prompt_events (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  prompt_id VARCHAR(100) NOT NULL,
  trigger_id VARCHAR(100) NOT NULL,
  variant VARCHAR(50) NOT NULL,
  event_type VARCHAR(50) NOT NULL, -- 'impression', 'click', 'conversion'
  converted_to_tier VARCHAR(50), -- If event_type = 'conversion'
  created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_prompt_events_user ON upgrade_prompt_events(user_id);
CREATE INDEX idx_prompt_events_prompt ON upgrade_prompt_events(prompt_id);
CREATE INDEX idx_prompt_events_type ON upgrade_prompt_events(event_type);
CREATE INDEX idx_prompt_events_created ON upgrade_prompt_events(created_at DESC);

-- Pricing experiments table
CREATE TABLE pricing_experiments (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  experiment_name VARCHAR(255) NOT NULL,
  variant_name VARCHAR(100) NOT NULL,
  tier_id VARCHAR(50) NOT NULL,
  price_override INTEGER, -- In cents, overrides default tier price
  feature_overrides JSONB, -- JSON object with feature ID -> enabled/disabled
  active BOOLEAN DEFAULT TRUE,
  start_date TIMESTAMP NOT NULL,
  end_date TIMESTAMP,
  created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_experiments_active ON pricing_experiments(active);
CREATE INDEX idx_experiments_dates ON pricing_experiments(start_date, end_date);

-- User experiment assignments
CREATE TABLE user_experiment_assignments (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  experiment_id UUID NOT NULL REFERENCES pricing_experiments(id) ON DELETE CASCADE,
  variant_name VARCHAR(100) NOT NULL,
  assigned_at TIMESTAMP DEFAULT NOW(),
  UNIQUE(user_id, experiment_id)
);

CREATE INDEX idx_user_experiments ON user_experiment_assignments(user_id, experiment_id);

-- Views for analytics

-- Current period usage by user
CREATE VIEW current_period_usage AS
SELECT
  u.id as user_id,
  u.email,
  s.tier_id,
  ur.metric,
  COALESCE(SUM(ur.count), 0) as total_usage,
  s.current_period_start,
  s.current_period_end
FROM users u
LEFT JOIN subscriptions s ON u.id = s.user_id AND s.status = 'active'
LEFT JOIN usage_records ur ON u.id = ur.user_id
  AND ur.period_start >= s.current_period_start
  AND ur.period_end <= s.current_period_end
GROUP BY u.id, u.email, s.tier_id, ur.metric, s.current_period_start, s.current_period_end;

-- Upgrade prompt conversion rates
CREATE VIEW upgrade_prompt_conversion_rates AS
SELECT
  prompt_id,
  variant,
  COUNT(DISTINCT CASE WHEN event_type = 'impression' THEN user_id END) as impressions,
  COUNT(DISTINCT CASE WHEN event_type = 'click' THEN user_id END) as clicks,
  COUNT(DISTINCT CASE WHEN event_type = 'conversion' THEN user_id END) as conversions,
  ROUND(
    100.0 * COUNT(DISTINCT CASE WHEN event_type = 'click' THEN user_id END) /
    NULLIF(COUNT(DISTINCT CASE WHEN event_type = 'impression' THEN user_id END), 0),
    2
  ) as click_through_rate,
  ROUND(
    100.0 * COUNT(DISTINCT CASE WHEN event_type = 'conversion' THEN user_id END) /
    NULLIF(COUNT(DISTINCT CASE WHEN event_type = 'impression' THEN user_id END), 0),
    2
  ) as conversion_rate
FROM upgrade_prompt_events
GROUP BY prompt_id, variant;

-- Monthly recurring revenue by tier
CREATE VIEW mrr_by_tier AS
SELECT
  tier_id,
  COUNT(*) as active_subscriptions,
  SUM(
    CASE
      WHEN tier_id = 'starter' THEN 4900
      WHEN tier_id = 'professional' THEN 14900
      WHEN tier_id = 'business' THEN 29900
      ELSE 0
    END
  ) / 100.0 as monthly_recurring_revenue
FROM subscriptions
WHERE status = 'active'
GROUP BY tier_id;
// Example 7: Usage Limit Enforcer (TypeScript)
/**
 * UsageLimitEnforcer - Real-time usage tracking and limit enforcement
 * Tracks usage, enforces quotas, calculates overages, and triggers upgrade prompts
 */

interface UsageLimitConfig {
  metric: string;
  limit: number;
  overageAllowed: boolean;
  overageRate?: number; // In cents per unit
  hardLimit?: number; // Absolute maximum (even with overage)
  resetPeriod: 'daily' | 'monthly' | 'billing_cycle';
}

interface UsageCheckResult {
  allowed: boolean;
  currentUsage: number;
  limit: number;
  remaining: number;
  willIncurOverage: boolean;
  overageCost?: number;
  resetDate?: Date;
  upgradeRecommended?: boolean;
}

class UsageLimitEnforcer {
  constructor(
    private db: Database,
    private pricingManager: PricingTierManager,
    private entitlementService: EntitlementService
  ) {}

  async checkUsageLimit(
    userId: string,
    metric: string
  ): Promise<UsageCheckResult> {
    // Get user's current subscription
    const subscription = await this.entitlementService.getActiveSubscription(userId);
    const tierId = subscription?.tierId ?? 'free';
    const tier = this.pricingManager.getTier(tierId);

    if (!tier) {
      throw new Error('Invalid tier');
    }

    // Find the relevant value metric
    const valueMetric = tier.valueMetrics.find(m => m.unit === metric);
    if (!valueMetric) {
      // No limit for this metric
      return {
        allowed: true,
        currentUsage: 0,
        limit: Infinity,
        remaining: Infinity,
        willIncurOverage: false,
        upgradeRecommended: false
      };
    }

    // Get current usage
    const currentUsage = await this.entitlementService.getCurrentUsage(userId, metric);
    const limit = valueMetric.baseAllowance;
    const remaining = Math.max(0, limit - currentUsage);

    // Check if usage exceeds limit
    if (currentUsage >= limit) {
      // Check if overage is allowed
      if (valueMetric.overageRate && valueMetric.overageRate > 0) {
        return {
          allowed: true,
          currentUsage,
          limit,
          remaining: 0,
          willIncurOverage: true,
          overageCost: valueMetric.overageRate,
          resetDate: subscription?.currentPeriodEnd,
          upgradeRecommended: currentUsage >= limit * 1.5 // Recommend upgrade if >50% over
        };
      } else {
        // Hard limit reached
        return {
          allowed: false,
          currentUsage,
          limit,
          remaining: 0,
          willIncurOverage: false,
          resetDate: subscription?.currentPeriodEnd,
          upgradeRecommended: true
        };
      }
    }

    // Usage within limit
    return {
      allowed: true,
      currentUsage,
      limit,
      remaining,
      willIncurOverage: false,
      upgradeRecommended: remaining < limit * 0.2 // Recommend if <20% remaining
    };
  }

  async recordUsageAndEnforce(
    userId: string,
    metric: string,
    count: number = 1
  ): Promise<{
    success: boolean;
    error?: string;
    usageInfo: UsageCheckResult;
  }> {
    // Check if usage is allowed
    const checkResult = await this.checkUsageLimit(userId, metric);

    if (!checkResult.allowed) {
      return {
        success: false,
        error: `${metric} limit exceeded. Upgrade to continue.`,
        usageInfo: checkResult
      };
    }

    // Record usage
    await this.entitlementService.recordUsage(userId, metric, count);

    // If overage occurred, record overage charge
    if (checkResult.willIncurOverage && checkResult.overageCost) {
      await this.recordOverageCharge(
        userId,
        metric,
        count,
        checkResult.overageCost
      );
    }

    return {
      success: true,
      usageInfo: {
        ...checkResult,
        currentUsage: checkResult.currentUsage + count,
        remaining: Math.max(0, checkResult.remaining - count)
      }
    };
  }

  private async recordOverageCharge(
    userId: string,
    metric: string,
    units: number,
    ratePerUnit: number
  ): Promise<void> {
    const subscription = await this.entitlementService.getActiveSubscription(userId);
    if (!subscription) return;

    const totalCharge = units * ratePerUnit;

    await this.db.query(`
      INSERT INTO overage_charges (
        user_id, subscription_id, metric, units_consumed, rate_per_unit, total_charge,
        period_start, period_end
      )
      VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
    `, [
      userId,
      subscription.id,
      metric,
      units,
      ratePerUnit,
      totalCharge,
      subscription.currentPeriodStart,
      subscription.currentPeriodEnd
    ]);
  }

  async getOverageChargesForPeriod(userId: string): Promise<{
    charges: Array<{ metric: string; units: number; cost: number }>;
    totalCost: number;
  }> {
    const subscription = await this.entitlementService.getActiveSubscription(userId);
    if (!subscription) {
      return { charges: [], totalCost: 0 };
    }

    const result = await this.db.query(`
      SELECT metric, SUM(units_consumed) as units, SUM(total_charge) as cost
      FROM overage_charges
      WHERE user_id = $1
        AND period_start >= $2
        AND period_end <= $3
        AND paid = FALSE
      GROUP BY metric
    `, [userId, subscription.currentPeriodStart, subscription.currentPeriodEnd]);

    const charges = result.rows.map(row => ({
      metric: row.metric,
      units: parseInt(row.units, 10),
      cost: parseInt(row.cost, 10)
    }));

    const totalCost = charges.reduce((sum, charge) => sum + charge.cost, 0);

    return { charges, totalCost };
  }
}

export { UsageLimitEnforcer, UsageLimitConfig, UsageCheckResult };

Check out usage-based billing implementation and ChatGPT app billing architecture.

// Example 8: Conversion Tracker (TypeScript)
/**
 * ConversionTracker - Tracks user journey from trial to paid conversion
 * Measures conversion funnels, attribution, and upgrade triggers
 */

interface ConversionEvent {
  userId: string;
  eventType: 'signup' | 'trial_start' | 'feature_used' | 'upgrade_prompt_shown' | 'upgrade_prompt_clicked' | 'upgrade_completed';
  eventData?: Record<string, any>;
  timestamp: Date;
  source?: string; // Attribution source
  campaign?: string; // Marketing campaign
}

interface ConversionFunnel {
  signups: number;
  trialStarts: number;
  featureUsage: number;
  upgradePromptShown: number;
  upgradePromptClicked: number;
  upgradeCompleted: number;
  conversionRate: number;
}

class ConversionTracker {
  constructor(private db: Database) {}

  async trackEvent(event: ConversionEvent): Promise<void> {
    await this.db.query(`
      INSERT INTO conversion_events (
        user_id, event_type, event_data, source, campaign, created_at
      )
      VALUES ($1, $2, $3, $4, $5, $6)
    `, [
      event.userId,
      event.eventType,
      JSON.stringify(event.eventData ?? {}),
      event.source,
      event.campaign,
      event.timestamp
    ]);
  }

  async getFunnelMetrics(
    startDate: Date,
    endDate: Date,
    source?: string
  ): Promise<ConversionFunnel> {
    const whereClause = source
      ? `WHERE created_at BETWEEN $1 AND $2 AND source = $3`
      : `WHERE created_at BETWEEN $1 AND $2`;

    const params = source ? [startDate, endDate, source] : [startDate, endDate];

    const result = await this.db.query(`
      SELECT
        COUNT(DISTINCT CASE WHEN event_type = 'signup' THEN user_id END) as signups,
        COUNT(DISTINCT CASE WHEN event_type = 'trial_start' THEN user_id END) as trial_starts,
        COUNT(DISTINCT CASE WHEN event_type = 'feature_used' THEN user_id END) as feature_usage,
        COUNT(DISTINCT CASE WHEN event_type = 'upgrade_prompt_shown' THEN user_id END) as upgrade_prompt_shown,
        COUNT(DISTINCT CASE WHEN event_type = 'upgrade_prompt_clicked' THEN user_id END) as upgrade_prompt_clicked,
        COUNT(DISTINCT CASE WHEN event_type = 'upgrade_completed' THEN user_id END) as upgrade_completed
      FROM conversion_events
      ${whereClause}
    `, params);

    const row = result.rows[0];
    const signups = parseInt(row.signups, 10);
    const upgradeCompleted = parseInt(row.upgrade_completed, 10);

    return {
      signups,
      trialStarts: parseInt(row.trial_starts, 10),
      featureUsage: parseInt(row.feature_usage, 10),
      upgradePromptShown: parseInt(row.upgrade_prompt_shown, 10),
      upgradePromptClicked: parseInt(row.upgrade_prompt_clicked, 10),
      upgradeCompleted,
      conversionRate: signups > 0 ? (upgradeCompleted / signups) * 100 : 0
    };
  }

  async getTimeToConversion(userId: string): Promise<number | null> {
    const result = await this.db.query(`
      SELECT
        MIN(CASE WHEN event_type = 'signup' THEN created_at END) as signup_at,
        MIN(CASE WHEN event_type = 'upgrade_completed' THEN created_at END) as upgrade_at
      FROM conversion_events
      WHERE user_id = $1
    `, [userId]);

    const row = result.rows[0];
    if (!row.signup_at || !row.upgrade_at) return null;

    const signupTime = new Date(row.signup_at).getTime();
    const upgradeTime = new Date(row.upgrade_at).getTime();

    return (upgradeTime - signupTime) / (1000 * 60 * 60); // Hours
  }

  async getConversionAttribution(): Promise<Array<{
    source: string;
    signups: number;
    conversions: number;
    conversionRate: number;
    avgTimeToConversion: number;
  }>> {
    const result = await this.db.query(`
      WITH source_metrics AS (
        SELECT
          source,
          COUNT(DISTINCT CASE WHEN event_type = 'signup' THEN user_id END) as signups,
          COUNT(DISTINCT CASE WHEN event_type = 'upgrade_completed' THEN user_id END) as conversions
        FROM conversion_events
        WHERE source IS NOT NULL
        GROUP BY source
      ),
      time_to_conversion AS (
        SELECT
          source,
          AVG(
            EXTRACT(EPOCH FROM (upgrade_at - signup_at)) / 3600
          ) as avg_hours
        FROM (
          SELECT
            source,
            user_id,
            MIN(CASE WHEN event_type = 'signup' THEN created_at END) as signup_at,
            MIN(CASE WHEN event_type = 'upgrade_completed' THEN created_at END) as upgrade_at
          FROM conversion_events
          WHERE source IS NOT NULL
          GROUP BY source, user_id
          HAVING MIN(CASE WHEN event_type = 'signup' THEN created_at END) IS NOT NULL
            AND MIN(CASE WHEN event_type = 'upgrade_completed' THEN created_at END) IS NOT NULL
        ) sub
        GROUP BY source
      )
      SELECT
        sm.source,
        sm.signups,
        sm.conversions,
        ROUND((sm.conversions::NUMERIC / NULLIF(sm.signups, 0)) * 100, 2) as conversion_rate,
        ROUND(COALESCE(ttc.avg_hours, 0), 2) as avg_time_to_conversion
      FROM source_metrics sm
      LEFT JOIN time_to_conversion ttc ON sm.source = ttc.source
      ORDER BY sm.conversions DESC
    `);

    return result.rows.map(row => ({
      source: row.source,
      signups: parseInt(row.signups, 10),
      conversions: parseInt(row.conversions, 10),
      conversionRate: parseFloat(row.conversion_rate),
      avgTimeToConversion: parseFloat(row.avg_time_to_conversion)
    }));
  }
}

export { ConversionTracker, ConversionEvent, ConversionFunnel };
// Example 9: Price Calculator (TypeScript)
/**
 * PriceCalculator - Calculates total cost including base price, overages, and discounts
 * Used for checkout, invoicing, and price preview
 */

interface PriceBreakdown {
  basePriceCents: number;
  overageChargesCents: number;
  subtotalCents: number;
  discountCents: number;
  taxCents: number;
  totalCents: number;
  currency: string;
  billingCycle: 'monthly' | 'annual';
  breakdown: Array<{
    description: string;
    amountCents: number;
  }>;
}

class PriceCalculator {
  constructor(
    private pricingManager: PricingTierManager,
    private usageLimitEnforcer: UsageLimitEnforcer
  ) {}

  async calculatePrice(
    userId: string,
    tierId: string,
    annualBilling: boolean = false,
    promoCode?: string
  ): Promise<PriceBreakdown> {
    const tier = this.pricingManager.getTier(tierId);
    if (!tier) {
      throw new Error('Invalid tier ID');
    }

    // Base price
    const basePriceCents = this.pricingManager.calculateMonthlyPrice(tierId, annualBilling);

    // Overage charges (if upgrading mid-period)
    const overages = await this.usageLimitEnforcer.getOverageChargesForPeriod(userId);
    const overageChargesCents = overages.totalCost;

    // Subtotal
    const subtotalCents = basePriceCents + overageChargesCents;

    // Discount (promo code or annual discount)
    let discountCents = 0;
    if (annualBilling && tier.annualDiscountPercent) {
      const regularPrice = tier.price;
      discountCents = Math.round(regularPrice * (tier.annualDiscountPercent / 100));
    }

    if (promoCode) {
      // Apply promo code discount (implement promo code logic)
      discountCents += await this.getPromoCodeDiscount(promoCode, subtotalCents);
    }

    // Tax (implement tax calculation based on location)
    const taxCents = await this.calculateTax(userId, subtotalCents - discountCents);

    // Total
    const totalCents = subtotalCents - discountCents + taxCents;

    // Breakdown
    const breakdown: Array<{ description: string; amountCents: number }> = [
      {
        description: `${tier.displayName} Plan (${annualBilling ? 'Annual' : 'Monthly'})`,
        amountCents: basePriceCents
      }
    ];

    if (overageChargesCents > 0) {
      breakdown.push({
        description: 'Usage Overages',
        amountCents: overageChargesCents
      });
    }

    if (discountCents > 0) {
      breakdown.push({
        description: annualBilling ? 'Annual Billing Discount' : 'Promo Code Discount',
        amountCents: -discountCents
      });
    }

    if (taxCents > 0) {
      breakdown.push({
        description: 'Tax',
        amountCents: taxCents
      });
    }

    return {
      basePriceCents,
      overageChargesCents,
      subtotalCents,
      discountCents,
      taxCents,
      totalCents,
      currency: 'USD',
      billingCycle: annualBilling ? 'annual' : 'monthly',
      breakdown
    };
  }

  formatPrice(cents: number): string {
    return `$${(cents / 100).toFixed(2)}`;
  }

  private async getPromoCodeDiscount(promoCode: string, subtotalCents: number): Promise<number> {
    // Implement promo code validation and discount calculation
    // This is a placeholder
    return 0;
  }

  private async calculateTax(userId: string, amountCents: number): Promise<number> {
    // Implement tax calculation based on user location (Stripe Tax, TaxJar, etc.)
    // This is a placeholder
    return 0;
  }
}

export { PriceCalculator, PriceBreakdown };

Read more about Stripe billing integration for ChatGPT apps and revenue optimization strategies.

Conclusion: Pricing as a Growth Lever

Tiered pricing isn't just about charging for your ChatGPT app—it's a strategic growth lever that compounds over time. By selecting the right value metrics, defining psychologically optimized tiers, implementing smart feature gates, and continuously experimenting, you can 2-3x your revenue without acquiring a single new customer.

The ChatGPT App Store opportunity is massive (800 million weekly users), but only if you price correctly. Use the strategies and code examples in this guide to build a pricing system that scales with your customers' success.

Ready to optimize your ChatGPT app pricing? Start building with MakeAIHQ's no-code ChatGPT app builder and implement these pricing strategies in minutes, not months. Or explore our Professional plan ($149/month) to unlock advanced analytics and AI-powered pricing optimization.

Related Resources

  • ChatGPT App Monetization Strategies: Revenue Models Guide
  • Usage-Based Billing for ChatGPT Apps: Implementation Guide
  • SaaS Pricing Psychology: Conversion Optimization
  • Stripe Webhooks for Subscription Management
  • Feature Gating Best Practices for ChatGPT Apps
  • A/B Testing Pricing Pages: Statistical Significance
  • ChatGPT App Builder Pricing

External Resources


Last updated: January 2026