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
- Three-Tier Minimum: Offer at least 3 paid tiers. Four tiers (Free + 3 paid) is optimal for ChatGPT apps.
- Anchor High: The highest tier should be 2-3x the middle tier price to make the middle tier feel reasonable.
- Mark the Winner: Label your target tier as "Most Popular" or "Recommended" to guide decision-making.
- 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
- Price Intelligently: SaaS Pricing Strategy
- OpenView Partners: SaaS Pricing Models
- Stripe: Value-Based Pricing Guide
Last updated: January 2026