Pricing Optimization skill

Pricing Optimization is an agent skill for AI coding assistants (Claude Code, OpenClaw, Cursor, Codex). SaaS pricing strategy and billing implementation — value metrics, tiering, discount governance, willingness-to-pay research (Van Westendorp/Gabor-Granger), localization, price increases, and production Stripe Billing (Checkout, usage meters, schedules, dunning, tax). Use when setting/testing prices, designing tiers, or implementing subscription/usage billing. Install with: npx skills-ws install pricing-optimization.

conversionv1.0.1Updated
copied ✓
openclawclaude-codecursorcodex
0 installsSecurity scan: cleanSource code

Pricing Optimization

Workflow

1. Value Metric Selection

The value metric is what you charge for. Get this wrong and everything else fails.

Good value metric criteria:

  • Scales with value delivered to customer
  • Easy for customer to understand
  • Predictable for customer to budget
  • Grows as customer succeeds
Metric typeExamplesBest fitWatch out for
Per seat$X/user/monthCollaboration tools where every user gets valueCustomers share logins; AI agents replace seats (seat counts can shrink)
Per usage / metered$X/API call, $X/GB, $X/1k tokensInfra, APIs, AI products where cost tracks consumptionUnpredictable bills hurt buyer trust → add caps, alerts, or prepaid credits
Hybrid (platform fee + usage)$X/mo base + overageUsage products needing revenue floor & expansionTwo dials to explain; keep the base meaningful, not a tax
Per feature / tierTier-gated accessHorizontal SaaS with distinct segmentsFeature gates feel arbitrary if not value-aligned
Per outcome$X/lead, $X/transaction, % of GMVPerformance tools that can attribute resultsAttribution disputes; revenue swings with customer's business
Committed spendAnnual $ commitment drawn down by usageEnterprise usage products, procurement-friendlyRequires forecasting; overage/rollover policy must be explicit
Flat rate$X/monthSimple, single-persona productsLeaves expansion revenue on the table

Decision framework (guidelines, not laws — validate against your buyer):

  • Value scales ~linearly with active users, and seats aren't easily shared → per seat (but stress-test against AI/automation eroding seat counts).
  • Cost-to-serve and value both track consumption → usage / metered; pair with a platform fee (hybrid) when you need a predictable revenue floor and land-and-expand.
  • Features cleanly separate segments by their jobs-to-be-done → tier-based.
  • You can credibly attribute a business outcome → outcome-based.
  • Selling to procurement-led enterprises → committed spend with usage drawdown.
  • There is rarely one "right" metric: many durable companies run hybrid (e.g. seats + usage, or platform fee + transaction %). Prefer the metric the buyer already uses to measure success internally.

2. Van Westendorp Price Sensitivity

Survey questions (ask all 4):

  1. At what price would this be so cheap you'd question the quality?
  2. At what price is this a bargain — great buy for the money?
  3. At what price is this getting expensive — you'd think twice?
  4. At what price is this too expensive — you'd never consider it?

Analysis: Plot cumulative distributions of all 4 questions. Intersections give:

IntersectionMeaning
"Too cheap" ∩ "Getting expensive"Point of marginal cheapness
"Bargain" ∩ "Too expensive"Point of marginal expensiveness
"Too cheap" ∩ "Too expensive"Optimal price point
"Bargain" ∩ "Getting expensive"Indifference price point

Acceptable price range: Between marginal cheapness and marginal expensiveness.

Sampling & rigor:

  • Minimum ~150–200 responses per segment for stable curves. Report results per segment (SMB vs mid-market vs enterprise behave very differently); a blended curve hides everything.
  • Recruit qualified buyers, not a generic panel. Screen for category awareness and purchase intent, or the stated prices are fiction.
  • VW measures price perception, not demand or willingness-to-pay. It tells you a plausible range, not a revenue-maximizing point. Treat it as a starting hypothesis to A/B test, with confidence intervals — not a precise number.
  • Stated-preference bias: people under-report what they'd pay and over-report price sensitivity. Anchor against actual conversion data once you have it.

B2B caveat — separate buyer from user: the person who feels the price (economic buyer / procurement) is usually not the daily user. Survey both, and weight the economic buyer's range for the headline number.

Stronger alternatives when stakes are high:

  • Gabor–Granger — ask purchase likelihood at specific discrete prices; yields a demand curve and a revenue-maximizing price (better for what to charge, where VW only gives a range).
  • Conjoint / MaxDiff — trades off features × price to reveal willingness-to-pay per feature and optimal packaging, not just one price. Best when designing tiers.
  • Live price tests / paywall experiments — the only ground truth. Randomize price by cohort and measure conversion + retention + expansion, not just first-order conversion.

3. Tier Design

3-tier standard (recommended starting point):

ElementStarterProfessionalEnterprise
Price anchorLow (attract)Medium (convert)High (capture)
TargetIndividual / small teamGrowing teamLarge organization
Value metric limitLowMediumUnlimited or custom
SupportSelf-serveEmail + chatDedicated CSM
FeaturesCore onlyCore + advancedAll + custom

Pricing rules:

  • Professional should be 2-3x Starter price
  • Enterprise should be 3-5x Professional (or custom)
  • Professional tier should be the obvious "best value" (anchor effect)
  • Include one "decoy" feature in Professional that makes it clearly better than Starter
  • Enterprise commonly uses "talk to sales" (custom pricing, security review, MSA negotiation). But this is not a law: product-led-growth companies increasingly offer self-serve annual contracts, in-product procurement/SSO upgrades, and usage commitments with a sales-assist motion. Use "talk to sales" when deals genuinely need negotiation; otherwise an "Enterprise, from $X" self-serve path reduces friction and shortens cycles.

4. Discount Strategy

Guardrails:

Discount typeMaxApproval
Annual prepay20%Self-serve
Multi-year deal30%Manager approval
Competitive switch15%Manager approval
Volume (10+ seats)15%Auto-calculated
Strategic / Logo40%VP approval + documented justification

Rules:

  • Never discount more than 40% (devalues product permanently)
  • Always trade something: discount for annual commitment, case study, referral
  • Track discount rate by rep (flag reps averaging > 20%)
  • Sunset discounts: "This rate is locked for 12 months, then standard pricing"
  • Document every discount reason in CRM

Implementing discounts in Stripe — coupons vs promotion codes:

  • A Coupon defines the discount (percent/amount, duration: once / repeating / forever). Don't expose raw coupon IDs to customers.
  • A Promotion code is a customer-facing code that wraps a coupon, with its own limits (max redemptions, expiry, first-time-customer only, minimum amount). Surface these via allow_promotion_codes: true in Checkout (see §8).
  • Apply a coupon programmatically with discounts: [{ coupon }] (Checkout/Subscriptions); avoid the legacy top-level coupon field.

Discount governance & compliance: "was/now" and percentage-off claims are regulated in the EU (Omnibus Directive: the reference price must be the lowest in the prior 30 days) and the UK/other markets. Keep promo terms, expiry, and renewal price honest and auditable. This is general guidance, not legal advice — verify promotional displays with counsel.

5. Price Localization

Purchasing Power Parity (PPP) adjustments:

TierCountriesAdjustment
Full priceUS, UK, Canada, Australia, Germany, France100%
Tier 2Spain, Italy, Portugal, Czech Republic, Poland70-80%
Tier 3Brazil, Mexico, Turkey, South Africa50-60%
Tier 4India, Indonesia, Philippines, Nigeria30-40%

Implementation:

  • Use IP geolocation for the initial display, then let users self-select country/currency (geolocation is approximate and trips up travelers/VPNs).
  • Allow currency switching that adjusts the actual price, not just the symbol.
  • Present the local price as the price for that market; you don't need to surface an internal "discount %." But it must not be deceptive — keep terms, renewal price, and tax treatment honest in every locale.
  • Localized tiers should still ladder consistently (don't make a Tier-4 plan cheaper in absolute terms than the same plan one tier up).

Legal / compliance guardrails (get a professional review before launch):

  • Tax (VAT/GST/sales tax): EU/UK B2C prices must generally be shown VAT-inclusive; US sales tax is typically added at checkout. This affects what number you display per region. Let Stripe Tax (or equivalent) compute and collect — see §8.
  • Consumer transparency (EU Omnibus, UK CMA, etc.): the displayed price, recurring amount, renewal date, and cancellation terms must be clear and accurate. "Was/now" discount claims are regulated (reference-price rules).
  • Arbitrage & fairness: geo-priced plans invite VPN abuse. Decide your stance (tolerate small leakage vs. verify billing country via payment method / tax ID) and apply it consistently — discriminatory enforcement creates legal and reputational risk.
  • Existing-customer fairness: don't silently raise an individual's localized price; honor §7's notice timeline.
  • Sanctions / export: screen restricted jurisdictions; don't sell where you're not permitted.

Pricing, tax, and consumer-protection rules vary by jurisdiction and change. The above is general guidance, not legal or tax advice — verify your specific tiers, displays, and renewal notices with qualified counsel/tax advisors.

6. Annual vs Monthly

Best practices:

  • Default to annual on pricing page (show monthly price as comparison)
  • Annual discount: 15-20% (2 months free is standard messaging)
  • Show monthly price per-month even for annual ("$49/mo billed annually")
  • Offer monthly-to-annual upgrade path with prorated credit
  • Track annual vs monthly mix (target: 60%+ annual for predictable revenue)

7. Price Increase Playbook

Communication timeline:

WhenAction
90 days beforeInternal alignment: sales, CS, support briefed
60 days beforeEmail announcement to all customers (clear, empathetic)
30 days beforeReminder email + lock-in offer (annual at current price)
Day ofPrice change live + support team ready for questions
30 days afterReview churn impact, adjust if needed

Email template:

Subject: Changes to your [Product] plan

Hi [Name],

On [date], we're updating our pricing. Your plan will change
from $X/mo to $Y/mo.

Why: [Honest reason — new features, increased costs, market alignment].

What you can do:
- Lock in current pricing by switching to annual before [date]
- Upgrade to [plan] to get [specific new value] at the new rate
- Questions? Reply to this email — we're here to help.

[Name], [Title]

Expected impact: Well-communicated 10-20% increase typically sees < 2% incremental churn. Poorly communicated or >30% increase can see 5-10%+ churn.

8. Stripe Integration Quickstart

Checkout Session Creation

import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

async function createCheckout(priceId: string, userId: string) {
  return stripe.checkout.sessions.create({
    mode: 'subscription',
    // Omit payment_method_types to let Stripe auto-manage enabled methods
    // (cards, wallets, local methods) from the Dashboard.
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${process.env.APP_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.APP_URL}/pricing`,
    automatic_tax: { enabled: true },          // requires Stripe Tax + origin address
    tax_id_collection: { enabled: true },      // collect VAT/GST IDs for B2B reverse-charge
    customer_update: { name: 'auto', address: 'auto' }, // needed so Tax can use the address
    allow_promotion_codes: true,               // promotion codes (see §4) — not raw coupon IDs
    metadata: { userId },
    subscription_data: { metadata: { userId } },
  });
}

Tax: enable Stripe Tax (automatic_tax) so the right VAT/GST/sales tax is calculated and collected per the customer's location and your registrations — this is what makes the localized, consumer-law-compliant prices in §5 correct. Display EU/UK B2C prices tax-inclusive where required.

Webhook Handler

// app/api/stripe/webhook/route.ts
import { headers } from 'next/headers';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: Request) {
  const body = await req.text();
  const sig = (await headers()).get('stripe-signature')!;
  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
  } catch {
    return new Response('Invalid signature', { status: 400 });
  }

  // Idempotency: persist event.id and no-op if already processed.
  // Stripe retries deliveries; the same event can arrive more than once.
  if (await alreadyProcessed(event.id)) return new Response('OK', { status: 200 });

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session;
      // Create subscription record, link to userId from metadata
      break;
    }
    case 'invoice.paid': {
      // Extend subscription period, grant/refresh entitlements, send receipt
      break;
    }
    case 'invoice.payment_failed': {
      // Dunning: Stripe Smart Retries will retry automatically.
      // Read invoice.next_payment_attempt; email the customer a fix-card link.
      // Revoke access only after the retry schedule is exhausted (see subscription status).
      break;
    }
    case 'customer.subscription.trial_will_end': {
      // Fires ~3 days before trial end — nudge the user to add/confirm payment.
      break;
    }
    case 'customer.subscription.updated': {
      // Plan changes + status transitions. Watch status:
      // 'past_due' / 'unpaid' → in dunning; 'active' → recovered; 'canceled' → revoke.
      break;
    }
    case 'customer.subscription.deleted': {
      // Mark subscription canceled, revoke access at period end
      break;
    }
  }

  await markProcessed(event.id);  // commit idempotency record after handling
  return new Response('OK', { status: 200 });
}

Critical: Never parse the body as JSON before passing to constructEvent — it needs the raw string for signature verification.

Production essentials this implies:

  • Idempotency store: record every handled event.id (a unique-indexed table or KV row) and short-circuit duplicates. Stripe guarantees at-least-once, not exactly-once, delivery.
  • Entitlements: grant access on invoice.paid / checkout.session.completed and revoke on cancel/unpaid — drive feature access off subscription status, not just plan. Stripe also offers a managed Entitlements API that emits an active-entitlements summary you can cache.
  • Dunning / recovery: enable Smart Retries and the Customer Portal so customers can update a failed card (invoice.payment_failed → email a portal link). Most involuntary churn is recoverable here.
  • Audit trail: log raw event payloads + your handling outcome for reconciliation and dispute defense.

9. Subscription Patterns

PatternImplementationBest for
Free trial → paidsubscription_data: { trial_period_days: 14 }Products needing time to show value
FreemiumNo Stripe until upgrade; gate features in codeWide-funnel products
Metered / usage-basedA Billing Meter + a price with recurring.usage_type: 'metered' and recurring.meter: <meter_id>; report usage via meter events (see below)API/AI products, infrastructure
Prepaid creditsSell credits, draw down via meter events / Billing CreditsBursty AI usage; predictable customer spend

Freemium Feature Gates

// lib/subscription.ts
type Plan = 'free' | 'pro' | 'enterprise';
const FEATURE_ACCESS: Record<string, Plan[]> = {
  'basic-projects': ['free', 'pro', 'enterprise'],
  'export-csv': ['pro', 'enterprise'],
  'api-access': ['pro', 'enterprise'],
  'custom-domain': ['enterprise'],
  'team-members': ['pro', 'enterprise'],
};

export function hasAccess(feature: string, plan: Plan): boolean {
  return FEATURE_ACCESS[feature]?.includes(plan) ?? false; // unlisted = denied (fail closed: a typo cannot expose a paid feature)
}

Usage-Based Billing (Billing Meters — current API)

As of 2024, Stripe's usage-based billing is built on Billing Meters + meter events. The old subscriptionItems.createUsageRecord / standalone usage_type: 'metered' price flow is legacy; do not use it for new integrations. Note: since acquiring Metronome (January 2026), Stripe positions Metronome as the recommended path for new usage-based integrations; Billing Meters remains fully supported and is the simpler fit for standard SaaS usage pricing.

One-time setup (per metered dimension):

// 1. Create a meter — defines the event name and how Stripe aggregates usage.
const meter = await stripe.billing.meters.create({
  display_name: 'API calls',
  event_name: 'api_calls',                 // events reference this name
  default_aggregation: { formula: 'sum' }, // or 'count'
  customer_mapping: {                       // how an event maps to a customer
    type: 'by_id',
    event_payload_key: 'stripe_customer_id',
  },
  value_settings: { event_payload_key: 'value' }, // which payload key holds the number
});

// 2. Create a metered price tied to the meter, then sell it via Checkout (§8).
const price = await stripe.prices.create({
  currency: 'usd',
  unit_amount: 1,                                  // 1 cent per unit, or use tiers
  recurring: { interval: 'month', usage_type: 'metered', meter: meter.id },
  product_data: { name: 'API usage' },
});

Report usage (real-time or batched) — this replaces usage records:

// Send a meter event whenever usage occurs. Stripe aggregates by customer + period.
await stripe.billing.meterEvents.create({
  event_name: 'api_calls',
  payload: {
    stripe_customer_id: customerId,   // matches customer_mapping above
    value: String(apiCallCount),      // matches value_settings.event_payload_key
  },
  identifier: `api_${requestId}`,     // idempotency: unique within a rolling 24h
  // timestamp defaults to now; must be within ~35 days past / 5 min future
});
  • Idempotency is built in: a unique identifier makes retries safe — Stripe rejects duplicates within a rolling 24-hour window (duplicate_meter_event). Use a stable per-event id (e.g. request/job id).
  • Aggregation is asynchronous. Reported usage is summarized into the invoice's metered line items; it is not reflected instantly. Read current totals via the meter's event summaries rather than assuming real-time balances.
  • High throughput / low latency: use the v2 meter event stream (POST /v2/billing/meter_event_stream against meter-events.stripe.com) with a short-lived meter-event session token (valid ~15 min) to batch many events per call. The single-event call above is fine for typical volumes.
  • Related building blocks: Billing Credits (grant prepaid/free credits drawn down by metered usage) and usage alerts/thresholds (notify or act when a customer crosses a usage level). For complex, high-scale metering (token/GPU-second billing à la OpenAI/Anthropic/Databricks), Metronome is the heavyweight option: Stripe completed its acquisition of Metronome in January 2026, and Stripe's docs now recommend Metronome as the primary platform for new usage-based integrations, while classic Billing Meters remains fully supported as a low-level building block. Choose plain Billing Meters for standard usage products; reach for Metronome when you need hierarchical accounts, complex rating/credits, or real-time spend orchestration.

APIs and the recommended approach evolve. Verify current meter syntax and limits at https://docs.stripe.com/billing/subscriptions/usage-based and the API version notes at https://docs.stripe.com/changelog.

10. Pricing Page Implementation

Plan Comparison Component Pattern

const PLANS = [
  { name: 'Free', price: '$0', priceId: null, features: ['5 projects', 'Community support'] },
  { name: 'Pro', price: '$29/mo', priceId: 'price_pro_monthly', features: ['Unlimited projects', 'Priority support', 'API access'], popular: true },
  { name: 'Enterprise', price: 'Custom', priceId: null, cta: 'Contact Sales', features: ['Everything in Pro', 'SSO', 'SLA', 'Dedicated CSM'] },
] as const;

Upgrade/Downgrade Flows

Upgrade — apply now and charge the prorated difference:

await stripe.subscriptions.update(subscriptionId, {
  items: [{ id: subscriptionItemId, price: newPriceId }],
  proration_behavior: 'always_invoice', // raise an invoice for the difference immediately
});

Downgrade — defer to the end of the current period. A plain subscriptions.update with proration_behavior: 'none' changes the item immediately (just without proration) — it does not schedule the change for renewal. To actually take effect at period end, use a subscription schedule with two phases:

// 1. Promote the subscription to a schedule (no-op billing-wise).
const schedule = await stripe.subscriptionSchedules.create({
  from_subscription: subscriptionId,
});

// 2. Phase 1 = current plan until period end; Phase 2 = new (lower) plan after.
const sub = await stripe.subscriptions.retrieve(subscriptionId);
const item = sub.items.data[0];

await stripe.subscriptionSchedules.update(schedule.id, {
  end_behavior: 'release', // return to a normal subscription once phases complete
  phases: [
    {
      items: [{ price: item.price.id, quantity: item.quantity }],
      start_date: item.current_period_start,
      end_date: item.current_period_end,   // run the current plan to period end
    },
    {
      items: [{ price: newPriceId, quantity: 1 }], // downgraded plan kicks in here
    },
  ],
});
// The customer keeps full access until current_period_end, then drops to the new plan.

Alternatively, if you use the Customer Portal for self-serve plan changes, enable "schedule downgrades at period end" (the schedule_at_period_end portal setting) and Stripe creates the schedule for you. Either way, gate in-app entitlements off the subscription's effective plan/status, not the requested one.

Customer Portal (self-serve management)

const portalSession = await stripe.billingPortal.sessions.create({
  customer: stripeCustomerId,
  return_url: `${process.env.APP_URL}/dashboard/billing`,
});
// Redirect user to portalSession.url

11. Testing Payments

ItemDetails
Test card (success)4242 4242 4242 4242 any future exp, any CVC
Test card (decline)4000 0000 0000 0002
Test card (3D Secure)4000 0025 0000 3155
Webhook CLIstripe listen --forward-to localhost:3000/api/stripe/webhook

Idempotency — there are three distinct layers, don't conflate them:

  1. API write idempotency — pass an idempotencyKey so a retried create call doesn't double-charge. Use Checkout/PaymentIntents, not the legacy Charges API:
// Idempotent one-time payment (PaymentIntent — current API; Charges is legacy)
await stripe.paymentIntents.create(
  { amount: 2000, currency: 'usd', automatic_payment_methods: { enabled: true } },
  { idempotencyKey: `pi_${orderId}` },
);

// Or, for a Checkout Session:
await stripe.checkout.sessions.create({ /* ... */ }, { idempotencyKey: `co_${orderId}` });
  1. Meter-event idempotency — the meter event identifier (see §9) dedupes usage within a rolling 24h window; this is separate from the header above.

  2. Webhook idempotency — store each handled event.id and skip duplicates (see §8). Inbound delivery is at-least-once.

Testing checklist:

  • Successful checkout → subscription created in DB
  • Card decline → user sees error, no DB record created
  • Webhook replay (stripe trigger checkout.session.completed) → idempotent
  • Subscription cancel → access revoked, status updated
  • Plan upgrade → prorated charge correct
  • Plan downgrade → takes effect at period end