Subscription Pricing
Build recurring revenue with subscription-based services
Subscription pricing provides predictable recurring revenue while giving customers unlimited or capped access to your services. This model creates long-term customer relationships and stable cash flow.
When to Use Subscriptions
Best for:
- Services with consistent, ongoing value
- High-frequency usage patterns
- Customer retention focus
- Predictable revenue needs
- Long-term relationships
Not ideal for:
- Sporadic, one-time usage
- Highly variable value delivery
- Price-sensitive trial users
- Services with high marginal costs
Core Patterns
Pattern 1: Simple Monthly Subscription
Fixed monthly fee with usage limits:
import $, { db, on, send } from 'sdk.do'
const contentGenerationService = await $.Service.create({
name: 'AI Content Generator Pro',
type: $.ServiceType.ContentGeneration,
pricing: {
model: 'subscription',
plans: [
{
id: 'starter',
name: 'Starter',
price: 49.0,
interval: 'month',
limits: {
articles: 10,
words: 15000,
images: 20,
},
features: ['basic-templates', 'seo-optimization', 'plagiarism-check', 'email-support'],
},
{
id: 'professional',
name: 'Professional',
price: 149.0,
interval: 'month',
limits: {
articles: 50,
words: 100000,
images: 100,
},
features: ['all-templates', 'seo-optimization', 'plagiarism-check', 'priority-support', 'custom-branding', 'team-collaboration'],
},
{
id: 'enterprise',
name: 'Enterprise',
price: 499.0,
interval: 'month',
limits: {
articles: Infinity,
words: Infinity,
images: Infinity,
},
features: ['unlimited-everything', 'dedicated-account-manager', 'custom-templates', 'api-access', 'white-label', 'sla-guarantee'],
},
],
},
})
// Subscription creation
on.Subscription.create(async (subscriptionRequest) => {
const plan = contentGenerationService.pricing.plans.find((p) => p.id === subscriptionRequest.planId)
if (!plan) {
throw new Error('Invalid plan')
}
// Create subscription
db.Subscriptions.create({
customerId: subscriptionRequest.customerId,
serviceId: contentGenerationService.id,
planId: plan.id,
status: 'active',
currentPeriodStart: new Date(),
currentPeriodEnd: addMonths(new Date(), 1),
price: plan.price,
limits: plan.limits,
features: plan.features,
})
// Create usage tracker
await db.create($.Usage, {
subscriptionId: subscription.id,
periodStart: subscription.currentPeriodStart,
periodEnd: subscription.currentPeriodEnd,
articles: 0,
words: 0,
images: 0,
})
// Charge first payment
await send($.Payment.charge, {
customerId: subscriptionRequest.customerId,
amount: plan.price,
description: `${plan.name} Plan - First month`,
subscriptionId: subscription.id,
})
return subscription
})
// Check limits before service execution
on.ServiceRequest.created(async (request) => {
if (request.serviceId !== contentGenerationService.id) return
// Get customer subscription
const subscription = await db.findOne($.Subscription, {
customerId: request.customerId,
serviceId: contentGenerationService.id,
status: 'active',
})
if (!subscription) {
return await send($.ServiceRequest.reject, {
requestId: request.id,
reason: 'No active subscription',
upgrade: {
message: 'Subscribe to use this service',
plans: contentGenerationService.pricing.plans,
},
})
}
// Get current usage
const usage = await db.findOne($.Usage, {
subscriptionId: subscription.id,
periodStart: subscription.currentPeriodStart,
})
const plan = contentGenerationService.pricing.plans.find((p) => p.id === subscription.planId)
// Check article limit
if (usage.articles >= plan.limits.articles) {
return await send($.ServiceRequest.reject, {
requestId: request.id,
reason: 'Monthly article limit reached',
current: usage.articles,
limit: plan.limits.articles,
upgrade: {
message: 'Upgrade your plan for more articles',
nextPlan: getNextPlan(plan),
},
})
}
// Check word limit
const estimatedWords = estimateWordCount(request.inputs)
if (usage.words + estimatedWords > plan.limits.words) {
return await send($.ServiceRequest.reject, {
requestId: request.id,
reason: 'Monthly word limit exceeded',
current: usage.words,
limit: plan.limits.words,
estimated: estimatedWords,
})
}
// Execute service...
const result = await executeContentGeneration(request)
// Update usage
await db.update(usage, {
articles: { increment: 1 },
words: { increment: result.wordCount },
images: { increment: result.imageCount },
})
// Deliver result (no additional charge for subscribers)
await send($.ServiceResult.deliver, {
requestId: request.id,
outputs: result,
usage: {
articles: usage.articles + 1,
words: usage.words + result.wordCount,
remaining: {
articles: plan.limits.articles - usage.articles - 1,
words: plan.limits.words - usage.words - result.wordCount,
},
},
})
})
function addMonths(date: Date, months: number): Date {
const result = new Date(date)
result.setMonth(result.getMonth() + months)
return result
}Example Plans:
- Starter: $49/month (10 articles, 15k words)
- Professional: $149/month (50 articles, 100k words)
- Enterprise: $499/month (unlimited)
Pattern 2: Annual Subscriptions with Discount
Incentivize long-term commitments:
const analyticsService = await $.Service.create({
name: 'Business Analytics Platform',
type: $.ServiceType.Analytics,
pricing: {
model: 'subscription',
plans: [
{
id: 'monthly',
name: 'Monthly',
price: 99.0,
interval: 'month',
features: ['basic-analytics', 'up-to-10-users'],
},
{
id: 'annual',
name: 'Annual',
price: 950.0, // ~$79/month (20% discount)
interval: 'year',
features: ['basic-analytics', 'up-to-10-users'],
discount: {
percentage: 0.2,
compared: 'monthly',
savings: 238.0, // $99 * 12 - $950
},
},
],
},
})
on.Subscription.create(async (subscriptionRequest) => {
const plan = analyticsService.pricing.plans.find((p) => p.id === subscriptionRequest.planId)
const periodLength = plan.interval === 'year' ? 12 : 1
const periodEnd = addMonths(new Date(), periodLength)
const subscription = await db.create($.Subscription, {
customerId: subscriptionRequest.customerId,
serviceId: analyticsService.id,
planId: plan.id,
status: 'active',
interval: plan.interval,
currentPeriodStart: new Date(),
currentPeriodEnd: periodEnd,
price: plan.price,
})
// Charge upfront
await send($.Payment.charge, {
customerId: subscriptionRequest.customerId,
amount: plan.price,
description: `${plan.name} Plan - ${plan.interval === 'year' ? 'Annual' : 'Monthly'}`,
subscriptionId: subscription.id,
})
// Send welcome email with savings info
if (plan.discount) {
await send($.Email.send, {
to: subscriptionRequest.customer.email,
template: 'subscription-welcome',
data: {
plan: plan.name,
savings: plan.discount.savings,
renewalDate: periodEnd,
},
})
}
return subscription
})Pricing Comparison:
- Monthly: $99/month = $1,188/year
- Annual: $950/year = $79/month (20% savings)
Pattern 3: Feature-Based Tiers
Different features at different price points:
const crmService = await $.Service.create({
name: 'CRM Suite',
type: $.ServiceType.CRM,
pricing: {
model: 'subscription',
plans: [
{
id: 'basic',
name: 'Basic',
price: 29.0,
interval: 'month',
perUser: true, // $29 per user
features: {
contacts: 1000,
emails: 1000,
pipelines: 1,
customFields: 5,
apiCalls: 1000,
support: 'email',
integrations: ['basic'],
},
},
{
id: 'plus',
name: 'Plus',
price: 59.0,
interval: 'month',
perUser: true,
features: {
contacts: 10000,
emails: 10000,
pipelines: 5,
customFields: 25,
apiCalls: 10000,
support: 'priority',
integrations: ['basic', 'advanced'],
automation: true,
reporting: 'advanced',
},
},
{
id: 'premium',
name: 'Premium',
price: 99.0,
interval: 'month',
perUser: true,
features: {
contacts: Infinity,
emails: Infinity,
pipelines: Infinity,
customFields: Infinity,
apiCalls: Infinity,
support: 'dedicated',
integrations: ['all'],
automation: true,
reporting: 'custom',
whiteLabel: true,
customDomain: true,
},
},
],
},
})
// Feature-based access control
async function hasFeatureAccess(subscription: any, feature: string, usage?: number): Promise<boolean> {
const plan = crmService.pricing.plans.find((p) => p.id === subscription.planId)
const features = plan.features
// Check feature existence
if (!(feature in features)) return false
// Check boolean features
if (typeof features[feature] === 'boolean') {
return features[feature]
}
// Check numeric limits
if (typeof features[feature] === 'number' && usage !== undefined) {
return usage < features[feature]
}
// Check array membership
if (Array.isArray(features[feature]) && usage !== undefined) {
return features[feature].includes(usage)
}
return true
}
on.ServiceRequest.created(async (request) => {
const subscription = await db.findOne($.Subscription, {
customerId: request.customerId,
serviceId: crmService.id,
status: 'active',
})
if (!subscription) {
return await send($.ServiceRequest.reject, {
requestId: request.id,
reason: 'No active subscription',
})
}
// Check feature access
const requestedFeature = request.inputs.feature
const hasAccess = await hasFeatureAccess(subscription, requestedFeature, request.inputs.usage)
if (!hasAccess) {
return await send($.ServiceRequest.reject, {
requestId: request.id,
reason: `Feature '${requestedFeature}' not available on your plan`,
upgrade: {
message: `Upgrade to access ${requestedFeature}`,
requiredPlan: findMinimumPlanForFeature(requestedFeature),
},
})
}
// Execute service...
})Per-User Pricing:
- Basic: $29/user/month (1k contacts, basic features)
- Plus: $59/user/month (10k contacts, automation)
- Premium: $99/user/month (unlimited, white-label)
Pattern 4: Usage-Based Tiers with Overage
Combine subscriptions with usage overage:
const emailService = await $.Service.create({
name: 'Email Marketing Platform',
type: $.ServiceType.EmailMarketing,
pricing: {
model: 'subscription-with-overage',
plans: [
{
id: 'starter',
name: 'Starter',
basePrice: 19.0,
interval: 'month',
included: {
emails: 5000,
contacts: 1000,
},
overage: {
emails: 0.001, // $0.001 per email over limit
contacts: 0.05, // $0.05 per contact over limit
},
},
{
id: 'growth',
name: 'Growth',
basePrice: 49.0,
interval: 'month',
included: {
emails: 25000,
contacts: 5000,
},
overage: {
emails: 0.0008, // 20% cheaper overage
contacts: 0.04,
},
},
{
id: 'scale',
name: 'Scale',
basePrice: 199.0,
interval: 'month',
included: {
emails: 150000,
contacts: 25000,
},
overage: {
emails: 0.0005, // 50% cheaper overage
contacts: 0.03,
},
},
],
},
})
// Monthly billing with overage
on.BillingCycle.ended(async (cycle) => {
const subscriptions = await db.query($.Subscription, {
where: {
serviceId: emailService.id,
status: 'active',
},
})
for (const subscription of subscriptions) {
const plan = emailService.pricing.plans.find((p) => p.id === subscription.planId)
// Get usage for the period
const usage = await db.findOne($.Usage, {
subscriptionId: subscription.id,
periodStart: cycle.start,
})
let totalCost = plan.basePrice
const lineItems = [
{
description: `${plan.name} Plan`,
amount: plan.basePrice,
},
]
// Calculate email overage
if (usage.emails > plan.included.emails) {
const overageEmails = usage.emails - plan.included.emails
const emailOverageCost = overageEmails * plan.overage.emails
totalCost += emailOverageCost
lineItems.push({
description: `Email Overage (${overageEmails.toLocaleString()} emails)`,
quantity: overageEmails,
rate: plan.overage.emails,
amount: emailOverageCost,
})
}
// Calculate contact overage
if (usage.contacts > plan.included.contacts) {
const overageContacts = usage.contacts - plan.included.contacts
const contactOverageCost = overageContacts * plan.overage.contacts
totalCost += contactOverageCost
lineItems.push({
description: `Contact Overage (${overageContacts.toLocaleString()} contacts)`,
quantity: overageContacts,
rate: plan.overage.contacts,
amount: contactOverageCost,
})
}
// Create invoice
await send($.Invoice.create, {
customerId: subscription.customerId,
subscriptionId: subscription.id,
period: { start: cycle.start, end: cycle.end },
lineItems,
total: totalCost,
})
// Reset usage for new period
await db.update(usage, {
emails: 0,
contacts: usage.contacts, // Contacts carry over
periodStart: cycle.end,
periodEnd: addMonths(cycle.end, 1),
})
}
})Example Billing:
- Starter at $19 + 2,000 overage emails = $21
- Growth at $49 + 10,000 overage emails = $57
- Scale at $199 (no overage) = $199
Pattern 5: Freemium Model
Free tier with paid upgrades:
const designToolService = await $.Service.create({
name: 'AI Design Studio',
type: $.ServiceType.DesignTool,
pricing: {
model: 'freemium',
free: {
limits: {
projects: 3,
exports: 10,
storage: 100, // MB
templates: 'basic',
},
features: ['basic-editor', 'community-templates', 'watermark'],
},
paid: [
{
id: 'pro',
name: 'Pro',
price: 12.0,
interval: 'month',
limits: {
projects: 100,
exports: 500,
storage: 10000, // 10GB
templates: 'all',
},
features: ['advanced-editor', 'all-templates', 'no-watermark', 'high-res-export', 'brand-kit'],
},
{
id: 'team',
name: 'Team',
price: 25.0,
interval: 'month',
perUser: true,
limits: {
projects: Infinity,
exports: Infinity,
storage: 100000, // 100GB
templates: 'all',
},
features: ['all-pro-features', 'team-collaboration', 'shared-brand-kits', 'version-history', 'priority-support'],
},
],
},
})
// Free tier usage tracking
on.ServiceRequest.created(async (request) => {
if (request.serviceId !== designToolService.id) return
// Check for paid subscription
const subscription = await db.findOne($.Subscription, {
customerId: request.customerId,
serviceId: designToolService.id,
status: 'active',
})
let limits, features
if (subscription) {
// Paid user
const plan = designToolService.pricing.paid.find((p) => p.id === subscription.planId)
limits = plan.limits
features = plan.features
} else {
// Free user
limits = designToolService.pricing.free.limits
features = designToolService.pricing.free.features
}
// Check limits
const usage = await getCurrentUsage(request.customerId)
if (usage.projects >= limits.projects) {
return await send($.ServiceRequest.reject, {
requestId: request.id,
reason: 'Project limit reached',
upgrade: {
message: 'Upgrade to Pro for 100+ projects',
plans: designToolService.pricing.paid,
trial: { duration: 14, unit: 'days' },
},
})
}
// Execute service...
const result = await executeDesignGeneration(request)
// Add watermark for free users
if (!subscription && features.includes('watermark')) {
result.image = await addWatermark(result.image)
}
await send($.ServiceResult.deliver, {
requestId: request.id,
outputs: result,
upgrade: !subscription
? {
message: 'Remove watermarks with Pro',
plan: 'pro',
}
: undefined,
})
})Freemium Tiers:
- Free: 3 projects, 10 exports/month, watermarks
- Pro: $12/month, 100 projects, no watermarks
- Team: $25/user/month, unlimited, collaboration
Advanced Patterns
Prorated Upgrades/Downgrades
Handle mid-cycle plan changes:
on($.Subscription.planChange, async (changeRequest) => {
const subscription = await db.findOne($.Subscription, {
id: changeRequest.subscriptionId,
})
const oldPlan = contentGenerationService.pricing.plans.find((p) => p.id === subscription.planId)
const newPlan = contentGenerationService.pricing.plans.find((p) => p.id === changeRequest.newPlanId)
// Calculate proration
const now = new Date()
const periodLength = subscription.currentPeriodEnd.getTime() - subscription.currentPeriodStart.getTime()
const timeUsed = now.getTime() - subscription.currentPeriodStart.getTime()
const timeRemaining = subscription.currentPeriodEnd.getTime() - now.getTime()
const oldPlanCredit = (oldPlan.price * timeRemaining) / periodLength
const newPlanCharge = (newPlan.price * timeRemaining) / periodLength
const proratedAmount = newPlanCharge - oldPlanCredit
// Upgrade: charge difference
if (proratedAmount > 0) {
await send($.Payment.charge, {
customerId: subscription.customerId,
amount: proratedAmount,
description: `Upgrade to ${newPlan.name} (prorated)`,
breakdown: {
oldPlanCredit: -oldPlanCredit,
newPlanCharge,
total: proratedAmount,
},
})
}
// Downgrade: credit for next billing
else if (proratedAmount < 0) {
await db.create($.Credit, {
customerId: subscription.customerId,
amount: Math.abs(proratedAmount),
reason: `Downgrade to ${newPlan.name}`,
expiresAt: subscription.currentPeriodEnd,
})
}
// Update subscription
await db.update(subscription, {
planId: newPlan.id,
price: newPlan.price,
limits: newPlan.limits,
features: newPlan.features,
})
// Reset usage if downgrading
if (newPlan.price < oldPlan.price) {
const usage = await db.findOne($.Usage, {
subscriptionId: subscription.id,
})
// Check if current usage exceeds new limits
for (const [metric, limit] of Object.entries(newPlan.limits)) {
if (usage[metric] > limit) {
await send($.Customer.notify, {
customerId: subscription.customerId,
type: 'usage-over-limit',
message: `Your ${metric} usage (${usage[metric]}) exceeds the ${newPlan.name} limit (${limit})`,
})
}
}
}
})Trial Periods
Offer free trials before charging:
on.Subscription.create(async (subscriptionRequest) => {
const plan = contentGenerationService.pricing.plans.find((p) => p.id === subscriptionRequest.planId)
const trialDays = 14 // 14-day free trial
const trialEnd = new Date()
trialEnd.setDate(trialEnd.getDate() + trialDays)
const subscription = await db.create($.Subscription, {
customerId: subscriptionRequest.customerId,
serviceId: contentGenerationService.id,
planId: plan.id,
status: 'trialing',
trialStart: new Date(),
trialEnd,
currentPeriodStart: trialEnd, // Billing starts after trial
currentPeriodEnd: addMonths(trialEnd, 1),
price: plan.price,
limits: plan.limits,
features: plan.features,
})
// Send trial started email
await send($.Email.send, {
to: subscriptionRequest.customer.email,
template: 'trial-started',
data: {
plan: plan.name,
trialEnd,
price: plan.price,
},
})
// Schedule trial ending reminders
await send($.Notification.schedule, {
customerId: subscriptionRequest.customerId,
type: 'trial-ending-soon',
sendAt: new Date(trialEnd.getTime() - 3 * 24 * 60 * 60 * 1000), // 3 days before
})
return subscription
})
// Convert trial to paid when trial ends
on($.Subscription.trialEnding, async (subscription) => {
// Charge first payment
try {
await send($.Payment.charge, {
customerId: subscription.customerId,
amount: subscription.price,
description: `${subscription.plan.name} - First payment`,
subscriptionId: subscription.id,
})
// Update subscription status
await db.update(subscription, {
status: 'active',
})
await send($.Email.send, {
to: subscription.customer.email,
template: 'subscription-activated',
data: {
plan: subscription.plan.name,
amount: subscription.price,
nextBillingDate: subscription.currentPeriodEnd,
},
})
} catch (error) {
// Payment failed - cancel subscription
await db.update(subscription, {
status: 'canceled',
canceledAt: new Date(),
cancellationReason: 'trial-payment-failed',
})
await send($.Email.send, {
to: subscription.customer.email,
template: 'trial-payment-failed',
data: {
plan: subscription.plan.name,
error: error.message,
},
})
}
})Subscription Renewals
Automatic recurring billing:
// Renew subscriptions
on.BillingCycle.ended(async (cycle) => {
const subscriptions = await db.query($.Subscription, {
where: {
status: 'active',
currentPeriodEnd: cycle.end,
},
})
for (const subscription of subscriptions) {
try {
// Charge renewal
await send($.Payment.charge, {
customerId: subscription.customerId,
amount: subscription.price,
description: `${subscription.plan.name} - Renewal`,
subscriptionId: subscription.id,
})
// Extend subscription period
const newPeriodStart = subscription.currentPeriodEnd
const newPeriodEnd = addMonths(newPeriodStart, subscription.interval === 'year' ? 12 : 1)
await db.update(subscription, {
currentPeriodStart: newPeriodStart,
currentPeriodEnd: newPeriodEnd,
})
// Reset usage
await db.update($.Usage, {
where: { subscriptionId: subscription.id },
data: {
articles: 0,
words: 0,
images: 0,
periodStart: newPeriodStart,
periodEnd: newPeriodEnd,
},
})
// Send renewal confirmation
await send($.Email.send, {
to: subscription.customer.email,
template: 'subscription-renewed',
data: {
plan: subscription.plan.name,
amount: subscription.price,
nextBillingDate: newPeriodEnd,
},
})
} catch (error) {
// Payment failed
await handleFailedRenewal(subscription, error)
}
}
})
async function handleFailedRenewal(subscription: any, error: Error) {
// Mark as past due
await db.update(subscription, {
status: 'past_due',
})
// Send payment failed email
await send($.Email.send, {
to: subscription.customer.email,
template: 'payment-failed',
data: {
plan: subscription.plan.name,
amount: subscription.price,
error: error.message,
updatePaymentUrl: `https://app.do/billing`,
},
})
// Schedule retries
for (let attempt = 1; attempt <= 3; attempt++) {
await send($.Payment.retry, {
subscriptionId: subscription.id,
attempt,
scheduleAt: new Date(Date.now() + attempt * 24 * 60 * 60 * 1000), // Daily retries
})
}
// Cancel after 3 failed attempts
setTimeout(
async () => {
const current = await db.findOne($.Subscription, { id: subscription.id })
if (current.status === 'past_due') {
await db.update(subscription, {
status: 'canceled',
canceledAt: new Date(),
cancellationReason: 'payment-failed',
})
await send($.Email.send, {
to: subscription.customer.email,
template: 'subscription-canceled',
data: {
reason: 'Payment failed after multiple attempts',
},
})
}
},
7 * 24 * 60 * 60 * 1000
) // 7 days
}Usage Alerts
Notify customers about usage:
on($.ServiceResult.delivered, async (result) => {
const subscription = await db.findOne($.Subscription, {
customerId: result.customerId,
serviceId: result.serviceId,
status: 'active',
})
if (!subscription) return
const usage = await db.findOne($.Usage, {
subscriptionId: subscription.id,
})
const plan = contentGenerationService.pricing.plans.find((p) => p.id === subscription.planId)
// Check each limit
for (const [metric, limit] of Object.entries(plan.limits)) {
if (limit === Infinity) continue
const percentage = usage[metric] / limit
// 80% warning
if (percentage >= 0.8 && percentage < 0.9) {
await send($.Customer.notify, {
customerId: subscription.customerId,
type: 'usage-warning',
message: `You've used 80% of your ${metric} limit`,
data: {
metric,
current: usage[metric],
limit,
percentage: Math.round(percentage * 100),
},
})
}
// 90% critical
else if (percentage >= 0.9 && percentage < 1.0) {
await send($.Customer.notify, {
customerId: subscription.customerId,
type: 'usage-critical',
message: `You've used 90% of your ${metric} limit`,
data: {
metric,
current: usage[metric],
limit,
percentage: Math.round(percentage * 100),
upgrade: {
nextPlan: getNextPlan(plan),
},
},
})
}
// Limit reached
else if (percentage >= 1.0) {
await send($.Customer.notify, {
customerId: subscription.customerId,
type: 'limit-reached',
message: `You've reached your ${metric} limit`,
data: {
metric,
current: usage[metric],
limit,
upgrade: {
required: true,
nextPlan: getNextPlan(plan),
},
},
})
}
}
})Best Practices
1. Transparent Value
Show customers what they're getting:
// Display value on subscription page
async function getSubscriptionValue(planId: string, customerId: string) {
const plan = contentGenerationService.pricing.plans.find((p) => p.id === planId)
// Calculate per-unit value
const perArticlePrice = plan.price / plan.limits.articles
const perWordPrice = plan.price / plan.limits.words
// Compare with pay-per-use
const payPerUseService = await db.findOne($.Service, {
type: $.ServiceType.ContentGeneration,
pricing: { model: 'per-use' },
})
const savings = {
articles: plan.limits.articles * payPerUseService.pricing.rate - plan.price,
percentage: Math.round(
((plan.limits.articles * payPerUseService.pricing.rate - plan.price) / (plan.limits.articles * payPerUseService.pricing.rate)) * 100
),
}
return {
plan,
perArticlePrice,
perWordPrice,
savings,
breakEvenPoint: Math.ceil(plan.price / payPerUseService.pricing.rate),
}
}2. Easy Cancellation
Allow customers to cancel anytime:
on($.Subscription.cancel, async (cancelRequest) => {
const subscription = await db.findOne($.Subscription, {
id: cancelRequest.subscriptionId,
})
// Cancel at end of period (not immediately)
await db.update(subscription, {
cancelAtPeriodEnd: true,
cancellationReason: cancelRequest.reason,
canceledAt: new Date(),
})
// Send confirmation
await send($.Email.send, {
to: subscription.customer.email,
template: 'subscription-cancel-confirmed',
data: {
plan: subscription.plan.name,
accessUntil: subscription.currentPeriodEnd,
reason: cancelRequest.reason,
},
})
// Offer retention discount
if (cancelRequest.reason === 'too-expensive') {
await send($.Offer.create, {
customerId: subscription.customerId,
type: 'retention-discount',
discount: 0.3, // 30% off
validUntil: subscription.currentPeriodEnd,
})
}
})3. Upgrade Incentives
Encourage customers to upgrade:
async function suggestUpgrade(customerId: string) {
const subscription = await db.findOne($.Subscription, {
customerId,
status: 'active',
})
if (!subscription) return null
const usage = await db.findOne($.Usage, {
subscriptionId: subscription.id,
})
const plan = contentGenerationService.pricing.plans.find((p) => p.id === subscription.planId)
// Check if consistently hitting limits
const usageHistory = await db.query($.Usage, {
where: {
subscriptionId: subscription.id,
},
orderBy: { periodStart: 'desc' },
limit: 3,
})
const hitLimitCount = usageHistory.filter((u) => u.articles >= plan.limits.articles * 0.9).length
if (hitLimitCount >= 2) {
const nextPlan = getNextPlan(plan)
return {
reason: 'consistent-high-usage',
message: `You've consistently used 90%+ of your limits. Upgrade to ${nextPlan.name} for ${nextPlan.limits.articles} articles`,
nextPlan,
additionalValue: {
articles: nextPlan.limits.articles - plan.limits.articles,
priceIncrease: nextPlan.price - plan.price,
perArticlePrice: (nextPlan.price - plan.price) / (nextPlan.limits.articles - plan.limits.articles),
},
}
}
return null
}Real-World Examples
SaaS Platform
// Typical SaaS pricing
const pricing = {
plans: [
{ name: 'Starter', price: 29, users: 1, features: 'basic' },
{ name: 'Team', price: 99, users: 5, features: 'advanced' },
{ name: 'Business', price: 299, users: 20, features: 'premium' },
],
}
// Revenue per customer:
// Starter: $348/year
// Team: $1,188/year
// Business: $3,588/yearUsage-Based SaaS
// Base subscription + usage
const pricing = {
base: 49, // $49/month base
included: 10000, // 10k API calls included
overage: 0.001, // $0.001 per call over
}
// Example billing:
// 10k calls = $49
// 50k calls = $49 + (40k × $0.001) = $89
// 100k calls = $49 + (90k × $0.001) = $139Content Platform
// Freemium to premium
const pricing = {
free: { projects: 3, exports: 10 },
pro: { price: 12, projects: 100, exports: 500 },
team: { price: 25, users: 'unlimited', collaboration: true },
}
// Conversion funnel:
// Free → Pro: 5-10% convert
// Pro → Team: 15-20% convertNext Steps
- Tiered Pricing → - Volume-based pricing
- Hybrid Models → - Combine strategies
- Per-Use Pricing → - Usage-based billing
- Monetization Overview → - Compare models
Resources
- Building Services → - Implement subscriptions
- Best Practices → - Optimize retention
- Deployment → - Launch your service