.do

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/year

Usage-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) = $139

Content 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% convert

Next Steps

Resources