| name | monetization-strategies |
| description | This skill should be used when designing revenue models - choosing between freemium, subscriptions, usage-based pricing, ads, enterprise licensing, and hybrid approaches. |
Monetization Strategies
Overview
Effective monetization balances three critical factors: value extraction, user experience, and market expectations. The right revenue model aligns with your product's value delivery pattern, user psychology, and competitive landscape. In 6-day sprints, choosing the optimal monetization strategy early prevents costly pivots and accelerates time-to-revenue.
Core Principle: Charge based on the value metric that grows with customer success, minimizes friction, and creates predictable revenue streams.
When to Use
Use this skill when you need to:
- Design initial pricing strategy for a new product
- Optimize existing monetization that's underperforming
- Evaluate model changes (e.g., free to freemium to paid-only)
- Set pricing tiers and feature distribution
- Implement payment infrastructure rapidly
- A/B test pricing variations to maximize conversion
- Transition between business models (pivot scenarios)
- Calculate unit economics and LTV:CAC ratios
- Design enterprise vs self-serve pricing
- Choose payment providers and integration patterns
Monetization Models
Freemium
Feature-Gated Freemium
- Free tier has limited features
- Premium unlocks advanced capabilities
- Best for: Productivity tools, SaaS platforms
// Feature gate implementation
interface PlanLimits {
plan: 'free' | 'pro' | 'enterprise';
maxProjects: number;
allowsAdvancedAnalytics: boolean;
allowsTeamCollaboration: boolean;
apiCallsPerMonth: number;
}
const PLAN_LIMITS: Record<string, PlanLimits> = {
free: {
plan: 'free',
maxProjects: 3,
allowsAdvancedAnalytics: false,
allowsTeamCollaboration: false,
apiCallsPerMonth: 1000
},
pro: {
plan: 'pro',
maxProjects: 50,
allowsAdvancedAnalytics: true,
allowsTeamCollaboration: true,
apiCallsPerMonth: 50000
},
enterprise: {
plan: 'enterprise',
maxProjects: Infinity,
allowsAdvancedAnalytics: true,
allowsTeamCollaboration: true,
apiCallsPerMonth: Infinity
}
};
function canAccessFeature(user: User, feature: keyof PlanLimits): boolean {
const limits = PLAN_LIMITS[user.plan];
return !!limits[feature];
}
Usage-Gated Freemium
- Free tier has limited usage
- Premium removes constraints
- Best for: API services, storage, compute
# Usage tracking for freemium
class UsageGate:
def __init__(self, user_id, plan):
self.user_id = user_id
self.plan = plan
self.limits = {
'free': {'requests': 1000, 'storage_gb': 5},
'starter': {'requests': 10000, 'storage_gb': 50},
'pro': {'requests': 100000, 'storage_gb': 500},
'unlimited': {'requests': float('inf'), 'storage_gb': float('inf')}
}
def check_usage(self, resource_type):
current_usage = self.get_current_usage(resource_type)
limit = self.limits[self.plan][resource_type]
if current_usage >= limit:
return {
'allowed': False,
'reason': f'{resource_type}_limit_exceeded',
'current': current_usage,
'limit': limit,
'upgrade_required': True
}
return {
'allowed': True,
'remaining': limit - current_usage,
'usage_percent': (current_usage / limit) * 100
}
Subscriptions
Tiered Subscription Pricing
| Tier | Price | Target User | Key Features |
|---|---|---|---|
| Starter | $9/mo | Individual | Basic features, 1 user |
| Pro | $29/mo | Small teams | Advanced features, 5 users |
| Business | $99/mo | Growing companies | All features, 20 users |
| Enterprise | Custom | Large orgs | Unlimited, SLA, custom integration |
Implementation Pattern (Stripe)
// Stripe subscription setup
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
async function createSubscription(customerId, priceId, trialDays = 14) {
try {
const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
trial_period_days: trialDays,
payment_behavior: 'default_incomplete',
payment_settings: {
save_default_payment_method: 'on_subscription',
},
expand: ['latest_invoice.payment_intent'],
});
return {
subscriptionId: subscription.id,
clientSecret: subscription.latest_invoice.payment_intent.client_secret,
status: subscription.status,
trialEnd: subscription.trial_end,
};
} catch (error) {
throw new Error(`Subscription creation failed: ${error.message}`);
}
}
// Webhook handler for subscription events
async function handleStripeWebhook(event) {
switch (event.type) {
case 'customer.subscription.created':
await onSubscriptionCreated(event.data.object);
break;
case 'customer.subscription.updated':
await onSubscriptionUpdated(event.data.object);
break;
case 'customer.subscription.deleted':
await onSubscriptionCanceled(event.data.object);
break;
case 'invoice.payment_succeeded':
await onPaymentSucceeded(event.data.object);
break;
case 'invoice.payment_failed':
await onPaymentFailed(event.data.object);
break;
}
}
Annual vs Monthly Pricing
- Annual: 16-20% discount (2-2.4 months free)
- Improves cash flow and retention
- Reduces churn touchpoints
// Pricing calculator with annual discount
interface PricingTier {
name: string;
monthlyPrice: number;
annualDiscount: number; // percentage
features: string[];
}
function calculateAnnualPrice(tier: PricingTier): {
monthlyTotal: number;
annualPrice: number;
savings: number;
effectiveMonthlyPrice: number;
} {
const monthlyTotal = tier.monthlyPrice * 12;
const annualPrice = monthlyTotal * (1 - tier.annualDiscount / 100);
const savings = monthlyTotal - annualPrice;
const effectiveMonthlyPrice = annualPrice / 12;
return {
monthlyTotal,
annualPrice,
savings,
effectiveMonthlyPrice
};
}
// Example usage
const proPlan: PricingTier = {
name: 'Pro',
monthlyPrice: 29,
annualDiscount: 20,
features: ['Advanced analytics', 'Priority support', 'Custom branding']
};
// Result: Annual = $278.40, Save $69.60, Effective $23.20/mo
Usage-Based / Consumption Pricing
Best for: APIs, cloud services, infrastructure Value metric: Requests, compute hours, storage, seats
# Metered billing implementation
class MeteringService:
def __init__(self, stripe_client):
self.stripe = stripe_client
self.price_per_unit = {
'api_request': 0.001, # $0.001 per request
'storage_gb_hour': 0.02, # $0.02 per GB-hour
'compute_hour': 0.50 # $0.50 per compute hour
}
def record_usage(self, subscription_item_id, quantity, timestamp=None):
"""Record usage for metered billing"""
return self.stripe.subscription_items.create_usage_record(
subscription_item_id,
quantity=quantity,
timestamp=timestamp or int(time.time()),
action='increment' # or 'set' for absolute values
)
def calculate_monthly_bill(self, usage_data):
"""Calculate bill based on usage"""
total = 0
breakdown = {}
for resource_type, quantity in usage_data.items():
if resource_type in self.price_per_unit:
cost = quantity * self.price_per_unit[resource_type]
breakdown[resource_type] = {
'quantity': quantity,
'unit_price': self.price_per_unit[resource_type],
'total': cost
}
total += cost
return {
'total': round(total, 2),
'breakdown': breakdown,
'billing_period': self.get_current_period()
}
def get_usage_alerts(self, user_id, threshold_percent=80):
"""Alert users approaching usage limits"""
usage = self.get_user_usage(user_id)
budget = self.get_user_budget(user_id)
if usage >= budget * (threshold_percent / 100):
return {
'alert': True,
'current_usage': usage,
'budget': budget,
'percent': (usage / budget) * 100,
'message': f'You have used {usage}% of your budget'
}
return {'alert': False}
One-Time Purchases
Best for: Mobile apps, desktop software, digital products
// Swift StoreKit 2 implementation
import StoreKit
class PurchaseManager: ObservableObject {
@Published var purchasedProductIDs = Set<String>()
private let productIDs = [
"com.app.premium_unlock",
"com.app.pro_features",
"com.app.lifetime_access"
]
func loadProducts() async throws -> [Product] {
return try await Product.products(for: productIDs)
}
func purchase(_ product: Product) async throws -> Transaction? {
let result = try await product.purchase()
switch result {
case .success(let verification):
let transaction = try checkVerified(verification)
await transaction.finish()
await updatePurchasedProducts()
return transaction
case .userCancelled, .pending:
return nil
@unknown default:
return nil
}
}
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .unverified:
throw PurchaseError.failedVerification
case .verified(let safe):
return safe
}
}
}
In-App Purchases (IAP)
Consumable: Credits, coins, energy (can be repurchased) Non-Consumable: Permanent unlocks (one-time) Auto-Renewable Subscriptions: Weekly, monthly, annual Non-Renewing Subscriptions: Fixed duration
// Android Google Play Billing implementation
class BillingManager(private val activity: Activity) {
private lateinit var billingClient: BillingClient
fun initializeBilling() {
billingClient = BillingClient.newBuilder(activity)
.setListener { billingResult, purchases ->
if (billingResult.responseCode == BillingResponseCode.OK && purchases != null) {
for (purchase in purchases) {
handlePurchase(purchase)
}
}
}
.enablePendingPurchases()
.build()
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingResponseCode.OK) {
queryProducts()
}
}
override fun onBillingServiceDisconnected() {
// Retry connection
}
})
}
fun queryProducts() {
val productList = listOf(
QueryProductDetailsParams.Product.newBuilder()
.setProductId("premium_monthly")
.setProductType(BillingClient.ProductType.SUBS)
.build()
)
val params = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build()
billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList ->
// Handle product details
}
}
fun launchPurchaseFlow(productDetails: ProductDetails) {
val flowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(
listOf(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.build()
)
)
.build()
billingClient.launchBillingFlow(activity, flowParams)
}
}
Advertising
Display Ads: Banner, interstitial, rewarded video Native Ads: Integrated content Sponsored Content: Premium placements
// Ad monetization with Google AdMob
class AdManager {
constructor() {
this.rewardedAd = null;
this.interstitialAd = null;
}
async loadRewardedAd() {
const { RewardedAd, RewardedAdEventType } = require('react-native-google-mobile-ads');
this.rewardedAd = RewardedAd.createForAdRequest('ca-app-pub-xxxxx');
this.rewardedAd.addAdEventListener(RewardedAdEventType.LOADED, () => {
console.log('Rewarded ad loaded');
});
this.rewardedAd.addAdEventListener(RewardedAdEventType.EARNED_REWARD, reward => {
this.grantReward(reward.amount, reward.type);
});
await this.rewardedAd.load();
}
async showRewardedAd(feature) {
if (this.rewardedAd && this.rewardedAd.loaded) {
await this.rewardedAd.show();
} else {
// Fallback: offer alternative or retry
this.offerAlternativeToAd(feature);
}
}
grantReward(amount, type) {
// Grant user premium feature temporarily
// or give them virtual currency
console.log(`Granted ${amount} of ${type}`);
}
// Ad revenue tracking
calculateARPDAU(dailyRevenue, dailyActiveUsers) {
return dailyRevenue / dailyActiveUsers;
}
}
Ad Revenue Optimization
- ARPDAU (Average Revenue Per Daily Active User): Target $0.02-$0.10
- Fill rate: Aim for 95%+
- eCPM (effective cost per mille): Track by placement
- Ad frequency capping: Max 3-5 per session
Enterprise Licensing
Best for: B2B SaaS, developer tools, infrastructure
// Enterprise license key management
interface EnterpriseLicense {
organizationId: string;
licenseKey: string;
seats: number;
features: string[];
validUntil: Date;
allowedDomains: string[];
onPremiseDeployment: boolean;
slaLevel: 'standard' | 'premium' | 'enterprise';
}
class LicenseValidator {
private licenses: Map<string, EnterpriseLicense> = new Map();
validateLicense(key: string, domain: string): {
valid: boolean;
license?: EnterpriseLicense;
reason?: string;
} {
const license = this.licenses.get(key);
if (!license) {
return { valid: false, reason: 'Invalid license key' };
}
if (new Date() > license.validUntil) {
return { valid: false, reason: 'License expired' };
}
if (!this.isDomainAllowed(domain, license.allowedDomains)) {
return { valid: false, reason: 'Domain not authorized' };
}
const currentSeats = this.getActiveSeats(license.organizationId);
if (currentSeats >= license.seats) {
return { valid: false, reason: 'Seat limit exceeded' };
}
return { valid: true, license };
}
generateLicense(orgId: string, config: Partial<EnterpriseLicense>): string {
const licenseKey = this.generateSecureKey();
const license: EnterpriseLicense = {
organizationId: orgId,
licenseKey,
seats: config.seats || 10,
features: config.features || [],
validUntil: config.validUntil || this.getOneYearFromNow(),
allowedDomains: config.allowedDomains || [],
onPremiseDeployment: config.onPremiseDeployment || false,
slaLevel: config.slaLevel || 'standard',
};
this.licenses.set(licenseKey, license);
return licenseKey;
}
private generateSecureKey(): string {
const crypto = require('crypto');
return crypto.randomBytes(32).toString('hex').toUpperCase();
}
private isDomainAllowed(domain: string, allowedDomains: string[]): boolean {
if (allowedDomains.length === 0) return true;
return allowedDomains.some(allowed =>
domain.endsWith(allowed) || allowed === '*'
);
}
}
Marketplace/Commission Models
Best for: Platforms, two-sided markets
# Marketplace commission calculation
class MarketplaceRevenue:
def __init__(self):
self.commission_rates = {
'standard': 0.15, # 15% commission
'premium': 0.10, # 10% for verified sellers
'enterprise': 0.05 # 5% for high-volume
}
self.payment_processing_fee = 0.029 # 2.9% + $0.30
self.fixed_fee = 0.30
def calculate_split(self, transaction_amount, seller_tier='standard'):
"""Calculate marketplace take and seller payout"""
# Payment processing
processing_cost = (transaction_amount * self.payment_processing_fee) + self.fixed_fee
# Marketplace commission
commission_rate = self.commission_rates[seller_tier]
marketplace_commission = transaction_amount * commission_rate
# Seller receives
seller_payout = transaction_amount - marketplace_commission - processing_cost
return {
'transaction_amount': transaction_amount,
'marketplace_commission': round(marketplace_commission, 2),
'processing_cost': round(processing_cost, 2),
'seller_payout': round(seller_payout, 2),
'marketplace_net': round(marketplace_commission - processing_cost, 2),
'commission_rate': commission_rate
}
def calculate_minimum_transaction(self, seller_tier='standard'):
"""Find minimum viable transaction to cover fees"""
commission_rate = self.commission_rates[seller_tier]
# Solve: amount * commission_rate > (amount * 0.029 + 0.30)
min_amount = self.fixed_fee / (commission_rate - self.payment_processing_fee)
return round(min_amount, 2)
# Example usage
marketplace = MarketplaceRevenue()
split = marketplace.calculate_split(100, 'standard')
# Result: Marketplace gets $15, Seller gets $81.80, Processing costs $3.20
Pricing Psychology
Anchoring
Place highest-priced tier first to make others seem reasonable.
<!-- Pricing page with anchoring -->
<div class="pricing-grid">
<!-- Enterprise shown first as anchor -->
<div class="pricing-card enterprise">
<h3>Enterprise</h3>
<div class="price">$999/mo</div>
<span class="badge">Most Popular</span>
<ul class="features">
<li>Unlimited everything</li>
<li>Dedicated support</li>
<li>Custom integrations</li>
</ul>
</div>
<!-- Pro seems reasonable by comparison -->
<div class="pricing-card pro">
<h3>Pro</h3>
<div class="price">$99/mo</div>
<span class="badge">Best Value</span>
<ul class="features">
<li>Advanced features</li>
<li>Priority support</li>
<li>API access</li>
</ul>
</div>
<!-- Starter looks cheap -->
<div class="pricing-card starter">
<h3>Starter</h3>
<div class="price">$19/mo</div>
<ul class="features">
<li>Basic features</li>
<li>Email support</li>
</ul>
</div>
</div>
Decoy Pricing
Add a middle tier that makes the premium tier look better.
// Good-Better-Best pricing with decoy
const pricingTiers = [
{
name: 'Basic',
price: 10,
features: ['5 projects', 'Basic support'],
valueScore: 5 // $2 per project
},
{
name: 'Pro', // DECOY - poor value
price: 25,
features: ['10 projects', 'Email support'],
valueScore: 4, // $2.50 per project
isDecoy: true
},
{
name: 'Premium', // Looks great compared to Pro
price: 30,
features: ['Unlimited projects', 'Priority support', 'API access'],
valueScore: 10, // Much better value
recommended: true
}
];
Value Metric Selection
Choose metrics that grow with customer success:
| Business Type | Poor Value Metric | Good Value Metric |
|---|---|---|
| Email marketing | Number of templates | Number of contacts |
| Analytics | Number of dashboards | Events tracked/month |
| Storage | Number of folders | GB stored |
| Team collaboration | Features unlocked | Number of seats |
| API service | Number of endpoints | API calls/month |
Price Point Optimization
# A/B test price points
class PriceOptimizer:
def __init__(self):
self.test_results = []
def analyze_price_point(self, price, conversions, visitors):
"""Analyze single price point performance"""
conversion_rate = conversions / visitors
revenue = price * conversions
revenue_per_visitor = revenue / visitors
return {
'price': price,
'conversion_rate': conversion_rate,
'revenue': revenue,
'rpv': revenue_per_visitor, # Key metric
'conversions': conversions
}
def find_optimal_price(self, test_data):
"""Find price with highest revenue per visitor"""
results = []
for test in test_data:
result = self.analyze_price_point(
test['price'],
test['conversions'],
test['visitors']
)
results.append(result)
# Sort by revenue per visitor
optimal = max(results, key=lambda x: x['rpv'])
return {
'optimal_price': optimal['price'],
'expected_rpv': optimal['rpv'],
'all_results': sorted(results, key=lambda x: x['rpv'], reverse=True)
}
# Example: Test $9, $19, $29 price points
optimizer = PriceOptimizer()
test_data = [
{'price': 9, 'conversions': 150, 'visitors': 1000}, # 15% CR, $1.35 RPV
{'price': 19, 'conversions': 100, 'visitors': 1000}, # 10% CR, $1.90 RPV ✓ Best
{'price': 29, 'conversions': 60, 'visitors': 1000}, # 6% CR, $1.74 RPV
]
optimal = optimizer.find_optimal_price(test_data)
# Result: $19 is optimal with $1.90 revenue per visitor
Implementation Patterns
Stripe Integration for Subscriptions
// Complete Stripe subscription flow
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const app = express();
// 1. Create checkout session
app.post('/create-checkout-session', async (req, res) => {
const { priceId, customerId, successUrl, cancelUrl } = req.body;
try {
const session = await stripe.checkout.sessions.create({
customer: customerId,
mode: 'subscription',
payment_method_types: ['card'],
line_items: [{ price: priceId, quantity: 1 }],
success_url: successUrl,
cancel_url: cancelUrl,
subscription_data: {
trial_period_days: 14,
metadata: {
userId: req.user.id,
},
},
allow_promotion_codes: true,
});
res.json({ sessionId: session.id });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 2. Handle webhooks
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
switch (event.type) {
case 'checkout.session.completed':
const session = event.data.object;
await fulfillOrder(session);
break;
case 'customer.subscription.updated':
const subscription = event.data.object;
await updateUserSubscription(subscription);
break;
case 'customer.subscription.deleted':
const canceledSub = event.data.object;
await handleCancellation(canceledSub);
break;
case 'invoice.payment_failed':
const failedInvoice = event.data.object;
await handleFailedPayment(failedInvoice);
break;
}
res.json({ received: true });
});
// 3. Create customer portal for self-service
app.post('/create-portal-session', async (req, res) => {
const { customerId } = req.body;
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: 'https://yourapp.com/account',
});
res.json({ url: session.url });
});
// Helper functions
async function fulfillOrder(session) {
const userId = session.metadata.userId;
const subscriptionId = session.subscription;
// Update database
await db.users.update(userId, {
stripeCustomerId: session.customer,
subscriptionId: subscriptionId,
subscriptionStatus: 'active',
planName: session.metadata.planName,
});
// Send welcome email
await sendEmail(userId, 'subscription_welcome');
}
Usage Tracking and Billing
# Usage tracking system for metered billing
from datetime import datetime, timedelta
from collections import defaultdict
class UsageTracker:
def __init__(self, redis_client, stripe_client):
self.redis = redis_client
self.stripe = stripe_client
def track_event(self, user_id, event_type, quantity=1, metadata=None):
"""Track a usage event"""
timestamp = datetime.utcnow()
# Store in Redis for fast access
key = f"usage:{user_id}:{event_type}:{timestamp.strftime('%Y-%m')}"
self.redis.hincrby(key, timestamp.strftime('%Y-%m-%d'), quantity)
# Store detailed event
event = {
'user_id': user_id,
'event_type': event_type,
'quantity': quantity,
'timestamp': timestamp.isoformat(),
'metadata': metadata or {}
}
self.redis.lpush(f"events:{user_id}", json.dumps(event))
# Report to Stripe for billing
subscription_item_id = self.get_subscription_item_id(user_id, event_type)
if subscription_item_id:
self.report_to_stripe(subscription_item_id, quantity)
# Check if approaching limit
self.check_usage_alerts(user_id, event_type)
def get_current_usage(self, user_id, event_type):
"""Get current month usage"""
timestamp = datetime.utcnow()
key = f"usage:{user_id}:{event_type}:{timestamp.strftime('%Y-%m')}"
daily_usage = self.redis.hgetall(key)
total = sum(int(v) for v in daily_usage.values())
return {
'total': total,
'daily_breakdown': daily_usage,
'period': timestamp.strftime('%Y-%m')
}
def report_to_stripe(self, subscription_item_id, quantity):
"""Report usage to Stripe for metered billing"""
try:
self.stripe.subscription_items.create_usage_record(
subscription_item_id,
quantity=quantity,
timestamp=int(datetime.utcnow().timestamp()),
action='increment'
)
except Exception as e:
# Log error, implement retry logic
print(f"Failed to report usage to Stripe: {e}")
def check_usage_alerts(self, user_id, event_type):
"""Alert users approaching limits"""
usage = self.get_current_usage(user_id, event_type)
limit = self.get_user_limit(user_id, event_type)
if not limit or limit == float('inf'):
return
percent_used = (usage['total'] / limit) * 100
# Alert at 80%, 90%, 100%
if percent_used >= 80 and not self.alert_sent(user_id, 80):
self.send_usage_alert(user_id, event_type, percent_used, limit)
if percent_used >= 100:
self.enforce_limit(user_id, event_type)
License Key Management
// License key generation and validation (Go)
package licensing
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"time"
)
type License struct {
Key string `json:"key"`
Email string `json:"email"`
Product string `json:"product"`
ValidUntil time.Time `json:"valid_until"`
MaxActivations int `json:"max_activations"`
Features []string `json:"features"`
}
type LicenseManager struct {
secretKey string
}
func NewLicenseManager(secret string) *LicenseManager {
return &LicenseManager{secretKey: secret}
}
// Generate creates a new license key
func (lm *LicenseManager) Generate(email, product string, duration time.Duration) string {
expiry := time.Now().Add(duration).Unix()
data := fmt.Sprintf("%s:%s:%d:%s", email, product, expiry, lm.secretKey)
hash := sha256.Sum256([]byte(data))
hashStr := hex.EncodeToString(hash[:])
// Format as XXXX-XXXX-XXXX-XXXX
key := strings.ToUpper(hashStr[:16])
formatted := fmt.Sprintf("%s-%s-%s-%s",
key[0:4], key[4:8], key[8:12], key[12:16])
return formatted
}
// Validate checks if a license key is valid
func (lm *LicenseManager) Validate(key string, email string, product string) (bool, error) {
// Remove hyphens
cleanKey := strings.ReplaceAll(key, "-", "")
// Check format
if len(cleanKey) != 16 {
return false, fmt.Errorf("invalid key format")
}
// Look up license in database
license, err := lm.getLicenseFromDB(cleanKey)
if err != nil {
return false, err
}
// Verify email and product
if license.Email != email || license.Product != product {
return false, fmt.Errorf("license mismatch")
}
// Check expiration
if time.Now().After(license.ValidUntil) {
return false, fmt.Errorf("license expired")
}
// Check activation count
activations := lm.getActivationCount(cleanKey)
if activations >= license.MaxActivations {
return false, fmt.Errorf("activation limit reached")
}
return true, nil
}
// Activate registers a device/machine with the license
func (lm *LicenseManager) Activate(key, deviceID string) error {
// Record activation in database
return lm.recordActivation(key, deviceID)
}
Trial Period Strategies
// Trial management system
interface TrialConfig {
durationDays: number;
requiresCreditCard: boolean;
autoConvertToPaid: boolean;
trialType: 'feature-limited' | 'time-limited' | 'hybrid';
}
class TrialManager {
async startTrial(userId: string, config: TrialConfig): Promise<Trial> {
const startDate = new Date();
const endDate = new Date(startDate);
endDate.setDate(endDate.getDate() + config.durationDays);
const trial: Trial = {
userId,
startDate,
endDate,
status: 'active',
requiresCreditCard: config.requiresCreditCard,
autoConvertToPaid: config.autoConvertToPaid,
daysRemaining: config.durationDays,
};
await this.saveTrial(trial);
await this.scheduleTrialEndNotifications(trial);
return trial;
}
async scheduleTrialEndNotifications(trial: Trial): Promise<void> {
// Schedule emails: 7 days before, 3 days before, 1 day before, on expiry
const notifications = [
{ daysBefore: 7, template: 'trial_ending_soon' },
{ daysBefore: 3, template: 'trial_ending_very_soon' },
{ daysBefore: 1, template: 'trial_ending_tomorrow' },
{ daysBefore: 0, template: 'trial_expired' },
];
for (const notif of notifications) {
const sendDate = new Date(trial.endDate);
sendDate.setDate(sendDate.getDate() - notif.daysBefore);
await this.scheduler.schedule({
userId: trial.userId,
template: notif.template,
sendAt: sendDate,
data: {
daysRemaining: notif.daysBefore,
upgradeUrl: this.generateUpgradeUrl(trial.userId),
},
});
}
}
async checkTrialStatus(userId: string): Promise<TrialStatus> {
const trial = await this.getTrial(userId);
if (!trial) {
return { hasTrialAccess: false, reason: 'no_trial' };
}
const now = new Date();
const daysRemaining = Math.ceil(
(trial.endDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
);
if (daysRemaining <= 0) {
if (trial.autoConvertToPaid && trial.requiresCreditCard) {
await this.convertToSubscription(userId);
return { hasTrialAccess: true, reason: 'converted_to_paid' };
}
return { hasTrialAccess: false, reason: 'trial_expired', daysRemaining: 0 };
}
return {
hasTrialAccess: true,
reason: 'trial_active',
daysRemaining,
showUpgradePrompt: daysRemaining <= 3,
};
}
}
Conversion Optimization
Paywall Placement
// Intelligent paywall timing
class PaywallStrategy {
shouldShowPaywall(user: User): {
show: boolean;
reason: string;
placement: string;
} {
// Don't show if already paid
if (user.subscription.isPaid) {
return { show: false, reason: 'already_subscribed', placement: 'none' };
}
// Show after value demonstration
if (user.metrics.sessionsCount >= 3 && user.metrics.actionsCompleted >= 5) {
return {
show: true,
reason: 'value_demonstrated',
placement: 'post_action',
};
}
// Show on premium feature access
if (user.attemptedPremiumFeature) {
return {
show: true,
reason: 'premium_feature_access',
placement: 'feature_gate',
};
}
// Show when hitting limit
if (user.usage.current >= user.usage.limit * 0.8) {
return {
show: true,
reason: 'approaching_limit',
placement: 'usage_limit',
};
}
return { show: false, reason: 'too_early', placement: 'none' };
}
getPaywallMessage(reason: string, user: User): string {
const messages = {
value_demonstrated: `You're loving ${APP_NAME}! Upgrade to unlock unlimited access.`,
premium_feature_access: 'This feature is available on our Pro plan.',
approaching_limit: `You've used ${user.usage.current} of ${user.usage.limit} ${user.usage.unit}. Upgrade for unlimited.`,
};
return messages[reason] || 'Upgrade to continue enjoying premium features.';
}
}
Upgrade Prompts
// React upgrade prompt component
import React, { useState, useEffect } from 'react';
const UpgradePrompt = ({ trigger, user }) => {
const [showPrompt, setShowPrompt] = useState(false);
useEffect(() => {
// Timing strategies
const strategies = {
immediate: () => true,
delayed: () => setTimeout(() => setShowPrompt(true), 5000),
onExit: () => {
const handleExit = (e) => {
if (e.clientY <= 0) setShowPrompt(true);
};
document.addEventListener('mouseleave', handleExit);
return () => document.removeEventListener('mouseleave', handleExit);
},
afterValue: () => user.completedActions >= 3,
};
strategies[trigger.timing]?.();
}, [trigger, user]);
if (!showPrompt) return null;
return (
<div className="upgrade-prompt-overlay">
<div className="upgrade-prompt-modal">
<h2>{trigger.headline || 'Unlock Premium Features'}</h2>
<p>{trigger.description}</p>
<div className="pricing-comparison">
<div className="current-plan">
<h3>Free</h3>
<ul>
<li>✓ {user.usage.limit} {user.usage.unit}/month</li>
<li>✗ Advanced analytics</li>
<li>✗ Priority support</li>
</ul>
</div>
<div className="upgrade-plan highlighted">
<span className="badge">Popular</span>
<h3>Pro</h3>
<div className="price">$29/mo</div>
<ul>
<li>✓ Unlimited {user.usage.unit}</li>
<li>✓ Advanced analytics</li>
<li>✓ Priority support</li>
<li>✓ Custom integrations</li>
</ul>
<button onClick={() => window.location.href = '/upgrade'}>
Upgrade Now
</button>
</div>
</div>
<button className="dismiss" onClick={() => setShowPrompt(false)}>
Maybe later
</button>
</div>
</div>
);
};
export default UpgradePrompt;
Feature Discovery
// Progressive feature disclosure
class FeatureOnboarding {
constructor(userId) {
this.userId = userId;
this.features = [
{ id: 'basic_feature', tier: 'free', complexity: 1 },
{ id: 'advanced_analytics', tier: 'pro', complexity: 2 },
{ id: 'team_collaboration', tier: 'pro', complexity: 3 },
{ id: 'api_access', tier: 'enterprise', complexity: 4 },
];
}
async getNextFeatureToIntroduce() {
const user = await this.getUser(this.userId);
const completed = user.onboarding.completedSteps;
// Find next feature user hasn't seen
const nextFeature = this.features.find(
(f) =>
!completed.includes(f.id) &&
f.complexity === completed.length + 1
);
if (!nextFeature) return null;
// If premium feature, show upgrade opportunity
if (nextFeature.tier !== 'free' && !user.subscription.isPaid) {
return {
...nextFeature,
locked: true,
upgradeMessage: `This feature is available on our ${nextFeature.tier} plan.`,
};
}
return nextFeature;
}
async recordFeatureUsage(featureId) {
// Track which features users actually use
await this.analytics.track(this.userId, 'feature_used', {
feature_id: featureId,
user_plan: await this.getUserPlan(this.userId),
timestamp: new Date(),
});
}
async identifyUpgradeOpportunities() {
const usage = await this.getFeatureUsage(this.userId);
// If user repeatedly tries premium features, show targeted upgrade
const premiumAttempts = usage.filter(
(u) => u.feature_tier !== 'free' && u.blocked
);
if (premiumAttempts.length >= 3) {
return {
shouldShowUpgrade: true,
message: `You've tried to use premium features ${premiumAttempts.length} times. Upgrade to unlock them!`,
features: [...new Set(premiumAttempts.map((p) => p.feature_id))],
};
}
return { shouldShowUpgrade: false };
}
}
Business Model Comparison
Model Selection Matrix
| Factor | Freemium | Subscription | Usage-Based | One-Time | Enterprise |
|---|---|---|---|---|---|
| Revenue predictability | Medium | High | Medium | Low | High |
| Viral growth potential | High | Medium | Low | Medium | Low |
| Customer LTV | Medium | High | Variable | Low | Very High |
| Sales complexity | Low | Low | Medium | Low | High |
| Support burden | High | Medium | Low | High | Medium |
| Time to first revenue | Slow | Fast | Fast | Immediate | Very Slow |
| Churn risk | High | Medium | Low | N/A | Very Low |
| Best for | B2C SaaS | B2B SaaS | APIs, Infrastructure | Tools, Apps | Enterprise Software |
SaaS vs Marketplace
// Revenue model comparison
interface RevenueModel {
type: 'saas' | 'marketplace';
revenue: number;
costs: number;
margin: number;
}
function compareSaaSvsMarketplace(monthlyUsers: number): {
saas: RevenueModel;
marketplace: RevenueModel;
recommendation: string;
} {
// SaaS model: Subscription per user
const saasPrice = 29;
const saasConversion = 0.05; // 5% free to paid
const saasCOGS = 5; // Per user cost
const saasRevenue = monthlyUsers * saasConversion * saasPrice;
const saasCosts = monthlyUsers * saasConversion * saasCOGS;
const saasMargin = ((saasRevenue - saasCosts) / saasRevenue) * 100;
// Marketplace model: Commission on transactions
const avgTransactionValue = 100;
const transactionsPerUser = 2;
const commissionRate = 0.15; // 15%
const paymentProcessing = 0.029;
const marketplaceGMV = monthlyUsers * transactionsPerUser * avgTransactionValue;
const marketplaceRevenue = marketplaceGMV * commissionRate;
const marketplaceCosts = marketplaceGMV * paymentProcessing;
const marketplaceMargin = ((marketplaceRevenue - marketplaceCosts) / marketplaceRevenue) * 100;
const saasModel = {
type: 'saas' as const,
revenue: saasRevenue,
costs: saasCosts,
margin: saasMargin,
};
const marketplaceModel = {
type: 'marketplace' as const,
revenue: marketplaceRevenue,
costs: marketplaceCosts,
margin: marketplaceMargin,
};
const recommendation =
marketplaceRevenue > saasRevenue
? 'Marketplace model generates more revenue with network effects'
: 'SaaS model provides more predictable, scalable revenue';
return {
saas: saasModel,
marketplace: marketplaceModel,
recommendation,
};
}
B2C vs B2B Pricing
# Pricing strategy by customer type
class PricingStrategy:
def __init__(self, customer_type):
self.customer_type = customer_type
self.strategies = {
'b2c': {
'price_points': [9, 19, 49], # Lower, single digit price sensitivity
'billing_cycle': 'monthly',
'trial_length': 7,
'sales_process': 'self-serve',
'payment_methods': ['card', 'paypal', 'apple_pay'],
'decision_speed': 'fast', # Minutes to hours
'discount_strategy': 'volume',
},
'b2b_smb': {
'price_points': [49, 99, 299], # Mid-market
'billing_cycle': 'annual',
'trial_length': 14,
'sales_process': 'low-touch',
'payment_methods': ['card', 'invoice'],
'decision_speed': 'medium', # Days to weeks
'discount_strategy': 'annual_commitment',
},
'b2b_enterprise': {
'price_points': [999, 2999, 'custom'], # High, often custom
'billing_cycle': 'annual',
'trial_length': 30,
'sales_process': 'high-touch',
'payment_methods': ['invoice', 'purchase_order'],
'decision_speed': 'slow', # Months
'discount_strategy': 'negotiated',
}
}
def get_pricing(self):
return self.strategies[self.customer_type]
def calculate_discount(self, base_price, seats, commitment_months):
"""Calculate appropriate discount based on customer type"""
strategy = self.strategies[self.customer_type]
if strategy['discount_strategy'] == 'volume':
# B2C: Discount for prepayment
if commitment_months >= 12:
return base_price * 0.80 # 20% off annual
return base_price
elif strategy['discount_strategy'] == 'annual_commitment':
# B2B SMB: Discount for annual + seats
discount = 0
if commitment_months >= 12:
discount += 0.15
if seats >= 10:
discount += 0.05
return base_price * (1 - discount)
elif strategy['discount_strategy'] == 'negotiated':
# Enterprise: Complex negotiation
base_discount = 0.10 # Start at 10%
if seats >= 100:
base_discount += 0.10
if commitment_months >= 24:
base_discount += 0.05
return base_price * (1 - min(base_discount, 0.30)) # Cap at 30%
Geographic Pricing
// Purchasing power parity (PPP) pricing
class GeographicPricing {
constructor() {
// PPP multipliers relative to USD
this.pppMultipliers = {
US: 1.0,
GB: 0.95,
CA: 0.85,
AU: 0.90,
DE: 0.90,
FR: 0.88,
IN: 0.25,
BR: 0.40,
MX: 0.45,
PL: 0.55,
AR: 0.35,
ID: 0.30,
};
}
calculateLocalPrice(baseUSDPrice, countryCode) {
const multiplier = this.pppMultipliers[countryCode] || 1.0;
const localPrice = baseUSDPrice * multiplier;
// Round to local pricing conventions
return this.roundToConvention(localPrice, countryCode);
}
roundToConvention(price, countryCode) {
// Different regions have different "charm pricing" conventions
const conventions = {
US: (p) => Math.round(p) - 0.01, // $9.99
GB: (p) => Math.round(p) - 0.01, // £9.99
EU: (p) => Math.round(p), // €10
IN: (p) => Math.round(p / 10) * 10 - 1, // ₹99
BR: (p) => Math.round(p / 10) * 10 - 0.01, // R$99.99
};
const region = this.getRegion(countryCode);
const convention = conventions[region] || ((p) => Math.round(p));
return convention(price);
}
shouldShowLocalPricing(countryCode) {
// Only show local pricing if significantly different
const multiplier = this.pppMultipliers[countryCode] || 1.0;
return multiplier < 0.75; // 25%+ difference
}
getRegion(countryCode) {
const regions = {
US: ['US', 'CA'],
GB: ['GB'],
EU: ['DE', 'FR', 'ES', 'IT', 'PL'],
IN: ['IN'],
BR: ['BR', 'AR', 'MX'],
};
for (const [region, countries] of Object.entries(regions)) {
if (countries.includes(countryCode)) return region;
}
return 'US';
}
}
// Example usage
const pricing = new GeographicPricing();
const basePrice = 29; // USD
console.log('US:', pricing.calculateLocalPrice(basePrice, 'US')); // $28.99
console.log('IN:', pricing.calculateLocalPrice(basePrice, 'IN')); // ₹199 (~$7.25)
console.log('BR:', pricing.calculateLocalPrice(basePrice, 'BR')); // R$49.99 (~$11.60)
Anti-Patterns
Over-Complicating Pricing
Bad: 6+ tiers with confusing feature matrices Good: 3 clear tiers with obvious value differences
// BAD: Too many tiers
const badPricing = [
{ name: 'Free', price: 0 },
{ name: 'Starter', price: 9 },
{ name: 'Basic', price: 19 },
{ name: 'Pro', price: 29 },
{ name: 'Pro Plus', price: 49 },
{ name: 'Business', price: 99 },
{ name: 'Enterprise', price: 299 },
];
// GOOD: Clear progression
const goodPricing = [
{ name: 'Free', price: 0, target: 'Individuals', features: ['Basic'] },
{ name: 'Pro', price: 29, target: 'Power users', features: ['Advanced'] },
{ name: 'Team', price: 99, target: 'Teams', features: ['Collaboration'] },
];
Charging Too Early
Anti-pattern: Paywall before demonstrating value
// BAD: Immediate paywall
app.get('/signup', (req, res) => {
res.redirect('/pricing'); // No value shown
});
// GOOD: Value-first approach
app.get('/signup', async (req, res) => {
await createFreeAccount(req.user);
await grantTrialAccess(req.user, 14);
res.redirect('/onboarding'); // Show value first
});
Ignoring Upgrade Friction
Anti-pattern: Making it hard to upgrade or downgrade
# BAD: Requires contacting sales to upgrade
def upgrade_plan(user_id, new_plan):
return {"message": "Please contact sales@company.com to upgrade"}
# GOOD: Self-service upgrade flow
def upgrade_plan(user_id, new_plan):
user = get_user(user_id)
price_difference = calculate_prorated_upgrade(user, new_plan)
return {
"can_upgrade": True,
"new_plan": new_plan,
"charge_today": price_difference,
"next_billing_date": user.next_billing_date,
"upgrade_url": f"/checkout?plan={new_plan}"
}
Poor Trial Design
Anti-patterns:
- Too short (< 7 days)
- Requires credit card with no warning
- Full feature access = no upgrade urgency
- No communication during trial
// GOOD trial design
const trialConfig = {
duration: 14, // Long enough to see value
requiresCreditCard: false, // Reduce friction
limitations: 'usage', // Create upgrade urgency
communications: [
{ day: 0, type: 'welcome', goal: 'onboard' },
{ day: 3, type: 'tips', goal: 'engagement' },
{ day: 7, type: 'milestone', goal: 'retention' },
{ day: 11, type: 'upgrade_offer', goal: 'conversion' },
{ day: 13, type: 'final_reminder', goal: 'conversion' },
],
};
Not Measuring Conversion
Anti-pattern: No data on pricing performance
# Track conversion metrics
class PricingAnalytics:
def track_conversion_funnel(self, user_id, step):
"""Track each step of conversion funnel"""
funnel_steps = [
'viewed_pricing',
'clicked_plan',
'entered_payment',
'completed_purchase'
]
self.analytics.track(user_id, step, {
'timestamp': datetime.utcnow(),
'funnel_position': funnel_steps.index(step),
'session_id': self.get_session_id()
})
def calculate_conversion_rate(self, from_step, to_step, days=30):
"""Calculate conversion rate between funnel steps"""
users_at_start = self.get_users_at_step(from_step, days)
users_at_end = self.get_users_at_step(to_step, days)
return {
'conversion_rate': len(users_at_end) / len(users_at_start),
'from_step': from_step,
'to_step': to_step,
'period_days': days
}
Examples
Example 1: SaaS Freemium with Stripe
// Complete freemium SaaS implementation
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// Define pricing tiers
const PLANS = {
free: {
name: 'Free',
price: 0,
stripePriceId: null,
limits: { projects: 3, storage: 1024, apiCalls: 1000 },
},
pro: {
name: 'Pro',
price: 29,
stripePriceId: 'price_xxxxx',
limits: { projects: 50, storage: 10240, apiCalls: 50000 },
},
team: {
name: 'Team',
price: 99,
stripePriceId: 'price_yyyyy',
limits: { projects: 200, storage: 102400, apiCalls: 500000 },
},
};
// Middleware to check plan access
function requiresPlan(minPlan) {
return async (req, res, next) => {
const user = await getUser(req.userId);
const planHierarchy = ['free', 'pro', 'team'];
if (planHierarchy.indexOf(user.plan) >= planHierarchy.indexOf(minPlan)) {
next();
} else {
res.status(403).json({
error: 'Upgrade required',
current_plan: user.plan,
required_plan: minPlan,
upgrade_url: '/upgrade',
});
}
};
}
// Create subscription
app.post('/api/subscribe', async (req, res) => {
const { planId } = req.body;
const plan = PLANS[planId];
if (!plan || plan.price === 0) {
return res.status(400).json({ error: 'Invalid plan' });
}
try {
// Create or get Stripe customer
let customerId = req.user.stripeCustomerId;
if (!customerId) {
const customer = await stripe.customers.create({
email: req.user.email,
metadata: { userId: req.user.id },
});
customerId = customer.id;
await updateUser(req.user.id, { stripeCustomerId: customerId });
}
// Create checkout session
const session = await stripe.checkout.sessions.create({
customer: customerId,
mode: 'subscription',
line_items: [{ price: plan.stripePriceId, quantity: 1 }],
success_url: `${process.env.APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.APP_URL}/pricing`,
subscription_data: {
trial_period_days: 14,
metadata: { userId: req.user.id, plan: planId },
},
});
res.json({ checkoutUrl: session.url });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Protected route example
app.post('/api/projects', requiresPlan('pro'), async (req, res) => {
// Only Pro and Team users can access this
const project = await createProject(req.user.id, req.body);
res.json(project);
});
Example 2: Usage-Based API Pricing
# API with usage-based metered billing
from flask import Flask, request, jsonify
import stripe
import redis
from datetime import datetime
app = Flask(__name__)
stripe.api_key = os.getenv('STRIPE_SECRET_KEY')
redis_client = redis.Redis(host='localhost', port=6379, db=0)
# Pricing tiers
PRICING = {
'pay_as_you_go': {
'base': 0,
'per_request': 0.001,
'included_requests': 0
},
'startup': {
'base': 29,
'per_request': 0.0005,
'included_requests': 10000
},
'growth': {
'base': 99,
'per_request': 0.0002,
'included_requests': 100000
}
}
def track_api_usage(user_id, endpoint, response_time):
"""Track API usage for billing"""
timestamp = datetime.utcnow()
month_key = timestamp.strftime('%Y-%m')
# Increment usage counter
redis_client.hincrby(f'usage:{user_id}:{month_key}', 'requests', 1)
redis_client.hincrby(f'usage:{user_id}:{month_key}', 'total_time_ms', response_time)
# Log individual request
redis_client.lpush(f'requests:{user_id}:{month_key}', json.dumps({
'endpoint': endpoint,
'timestamp': timestamp.isoformat(),
'response_time': response_time
}))
# Report to Stripe for metered billing
subscription_item_id = get_subscription_item_id(user_id)
if subscription_item_id:
try:
stripe.subscription_items.create_usage_record(
subscription_item_id,
quantity=1,
timestamp=int(timestamp.timestamp())
)
except Exception as e:
print(f"Failed to report usage: {e}")
@app.route('/api/v1/<path:endpoint>', methods=['GET', 'POST'])
def api_endpoint(endpoint):
# Authenticate
api_key = request.headers.get('X-API-Key')
user = authenticate_api_key(api_key)
if not user:
return jsonify({'error': 'Invalid API key'}), 401
# Check rate limits
usage = get_current_usage(user['id'])
plan = PRICING[user['plan']]
billable_requests = max(0, usage['requests'] - plan['included_requests'])
estimated_cost = plan['base'] + (billable_requests * plan['per_request'])
# Track usage
start_time = datetime.utcnow()
result = process_api_request(endpoint, request)
response_time = (datetime.utcnow() - start_time).total_seconds() * 1000
track_api_usage(user['id'], endpoint, response_time)
# Add usage headers
response = jsonify(result)
response.headers['X-RateLimit-Remaining'] = plan['included_requests'] - usage['requests']
response.headers['X-Usage-Month'] = usage['requests']
response.headers['X-Estimated-Cost'] = f"${estimated_cost:.2f}"
return response
@app.route('/api/usage', methods=['GET'])
def get_usage():
"""Get current month usage and costs"""
api_key = request.headers.get('X-API-Key')
user = authenticate_api_key(api_key)
usage = get_current_usage(user['id'])
plan = PRICING[user['plan']]
billable = max(0, usage['requests'] - plan['included_requests'])
cost = plan['base'] + (billable * plan['per_request'])
return jsonify({
'period': datetime.utcnow().strftime('%Y-%m'),
'plan': user['plan'],
'requests': {
'total': usage['requests'],
'included': plan['included_requests'],
'billable': billable
},
'cost': {
'base': plan['base'],
'usage': billable * plan['per_request'],
'total': cost
}
})
Example 3: Mobile App with IAP
// iOS app with in-app purchases and subscriptions
import StoreKit
import SwiftUI
// Product IDs
enum ProductID: String, CaseIterable {
case proMonthly = "com.app.pro.monthly"
case proYearly = "com.app.pro.yearly"
case premiumUnlock = "com.app.premium.unlock"
case creditsPack = "com.app.credits.100"
}
@MainActor
class StoreManager: ObservableObject {
@Published var products: [Product] = []
@Published var purchasedProductIDs = Set<String>()
@Published var subscriptionStatus: SubscriptionStatus?
private var updateListenerTask: Task<Void, Error>?
init() {
updateListenerTask = listenForTransactions()
Task {
await loadProducts()
await updatePurchasedProducts()
}
}
deinit {
updateListenerTask?.cancel()
}
func listenForTransactions() -> Task<Void, Error> {
return Task.detached {
for await result in Transaction.updates {
do {
let transaction = try self.checkVerified(result)
await self.updatePurchasedProducts()
await transaction.finish()
} catch {
print("Transaction verification failed: \(error)")
}
}
}
}
func loadProducts() async {
do {
let productIDs = ProductID.allCases.map { $0.rawValue }
self.products = try await Product.products(for: productIDs)
} catch {
print("Failed to load products: \(error)")
}
}
func purchase(_ product: Product) async throws -> Transaction? {
let result = try await product.purchase()
switch result {
case .success(let verification):
let transaction = try checkVerified(verification)
await updatePurchasedProducts()
await transaction.finish()
return transaction
case .userCancelled, .pending:
return nil
@unknown default:
return nil
}
}
func updatePurchasedProducts() async {
var purchasedIDs = Set<String>()
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result else { continue }
if transaction.revocationDate == nil {
purchasedIDs.insert(transaction.productID)
}
}
self.purchasedProductIDs = purchasedIDs
// Check subscription status
await updateSubscriptionStatus()
}
func updateSubscriptionStatus() async {
// Check if user has active subscription
let hasProMonthly = purchasedProductIDs.contains(ProductID.proMonthly.rawValue)
let hasProYearly = purchasedProductIDs.contains(ProductID.proYearly.rawValue)
if hasProMonthly || hasProYearly {
subscriptionStatus = .active
} else {
subscriptionStatus = .inactive
}
}
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .unverified:
throw StoreError.failedVerification
case .verified(let safe):
return safe
}
}
func hasAccess(to feature: String) -> Bool {
// Check if user has purchased access
switch feature {
case "premium":
return subscriptionStatus == .active ||
purchasedProductIDs.contains(ProductID.premiumUnlock.rawValue)
default:
return false
}
}
}
// Pricing view
struct PricingView: View {
@StateObject private var store = StoreManager()
@State private var isPurchasing = false
var body: some View {
VStack(spacing: 20) {
Text("Unlock Premium Features")
.font(.title)
.fontWeight(.bold)
ForEach(store.products, id: \.id) { product in
PricingCard(product: product) {
await purchaseProduct(product)
}
}
Button("Restore Purchases") {
Task {
try? await AppStore.sync()
}
}
}
.padding()
}
func purchaseProduct(_ product: Product) async {
isPurchasing = true
do {
if let transaction = try await store.purchase(product) {
// Purchase successful
print("Purchased: \(transaction.productID)")
}
} catch {
print("Purchase failed: \(error)")
}
isPurchasing = false
}
}
enum SubscriptionStatus {
case active
case inactive
case grace
case trial
}
enum StoreError: Error {
case failedVerification
}
Example 4: Dynamic Pricing with A/B Testing
// A/B testing framework for pricing
interface PricingExperiment {
id: string;
name: string;
variants: PricingVariant[];
status: 'draft' | 'running' | 'completed';
startDate: Date;
endDate?: Date;
targetMetric: 'revenue' | 'conversion' | 'ltv';
}
interface PricingVariant {
id: string;
name: string;
price: number;
allocation: number; // Percentage of traffic
conversions: number;
revenue: number;
visitors: number;
}
class PricingExperimentManager {
async assignVariant(userId: string, experimentId: string): Promise<PricingVariant> {
// Check if user already assigned
const existing = await this.getAssignment(userId, experimentId);
if (existing) return existing;
// Get experiment
const experiment = await this.getExperiment(experimentId);
if (experiment.status !== 'running') {
return experiment.variants[0]; // Default to control
}
// Assign based on allocation percentages
const rand = this.hash(userId + experimentId) % 100;
let cumulative = 0;
for (const variant of experiment.variants) {
cumulative += variant.allocation;
if (rand < cumulative) {
await this.saveAssignment(userId, experimentId, variant.id);
return variant;
}
}
return experiment.variants[0];
}
async trackConversion(
userId: string,
experimentId: string,
revenue: number
): Promise<void> {
const assignment = await this.getAssignment(userId, experimentId);
if (!assignment) return;
await this.incrementMetrics(experimentId, assignment.variantId, {
conversions: 1,
revenue,
});
}
async analyzeExperiment(experimentId: string): Promise<ExperimentResults> {
const experiment = await this.getExperiment(experimentId);
const control = experiment.variants[0];
const results: VariantResult[] = [];
for (const variant of experiment.variants) {
const conversionRate = variant.conversions / variant.visitors;
const avgRevenue = variant.revenue / variant.visitors;
// Statistical significance test
const significance = this.calculateSignificance(
variant,
control,
experiment.targetMetric
);
results.push({
variantId: variant.id,
name: variant.name,
conversionRate,
avgRevenue,
totalRevenue: variant.revenue,
visitors: variant.visitors,
improvement: ((avgRevenue - control.revenue / control.visitors) /
(control.revenue / control.visitors)) * 100,
isSignificant: significance.pValue < 0.05,
confidence: significance.confidence,
});
}
return {
experimentId,
results: results.sort((a, b) => b.avgRevenue - a.avgRevenue),
winner: results.find((r) => r.isSignificant && r.improvement > 0),
recommendation: this.generateRecommendation(results),
};
}
private calculateSignificance(
variant: PricingVariant,
control: PricingVariant,
metric: string
): { pValue: number; confidence: number } {
// Simplified z-test for proportions
const p1 = variant.conversions / variant.visitors;
const p2 = control.conversions / control.visitors;
const n1 = variant.visitors;
const n2 = control.visitors;
const pooled = (variant.conversions + control.conversions) / (n1 + n2);
const se = Math.sqrt(pooled * (1 - pooled) * (1 / n1 + 1 / n2));
const z = (p1 - p2) / se;
// Approximate p-value
const pValue = 2 * (1 - this.normalCDF(Math.abs(z)));
const confidence = (1 - pValue) * 100;
return { pValue, confidence };
}
private normalCDF(z: number): number {
// Approximation of cumulative distribution function
const t = 1 / (1 + 0.2316419 * Math.abs(z));
const d = 0.3989423 * Math.exp((-z * z) / 2);
const probability =
d *
t *
(0.3193815 +
t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274))));
return z > 0 ? 1 - probability : probability;
}
private hash(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
return Math.abs(hash);
}
}
// Usage example
const experimentManager = new PricingExperimentManager();
// Create experiment
const experiment: PricingExperiment = {
id: 'pricing_test_001',
name: 'Pro Plan Price Point Test',
variants: [
{ id: 'control', name: 'Control $29', price: 29, allocation: 34, conversions: 0, revenue: 0, visitors: 0 },
{ id: 'variant_a', name: 'Test $19', price: 19, allocation: 33, conversions: 0, revenue: 0, visitors: 0 },
{ id: 'variant_b', name: 'Test $39', price: 39, allocation: 33, conversions: 0, revenue: 0, visitors: 0 },
],
status: 'running',
startDate: new Date(),
targetMetric: 'revenue',
};
// In pricing page
app.get('/pricing', async (req, res) => {
const variant = await experimentManager.assignVariant(req.user.id, 'pricing_test_001');
res.render('pricing', {
price: variant.price,
experimentId: 'pricing_test_001',
});
});
// Track conversion
app.post('/subscribe', async (req, res) => {
// Process subscription
const subscription = await createSubscription(req.user.id, req.body.planId);
// Track experiment conversion
await experimentManager.trackConversion(
req.user.id,
'pricing_test_001',
subscription.amount
);
res.json({ success: true });
});
Key Takeaways for 6-Day Sprints:
- Day 1: Choose model based on value delivery pattern
- Day 2: Implement basic payment flow (Stripe Checkout is fastest)
- Day 3: Add feature gates and usage tracking
- Day 4: Create pricing page with 3 clear tiers
- Day 5: Implement trial and upgrade flows
- Day 6: Add analytics tracking and test end-to-end
Critical Decisions:
- Freemium vs paid-only: Freemium if viral growth matters
- Monthly vs annual: Offer both, 20% discount on annual
- Trial length: 14 days is optimal for most B2B SaaS
- Payment provider: Stripe for speed, flexibility, and developer experience
Revenue Model Priority: Subscription > Usage-based > One-time > Ads (for predictability and LTV)