skills-ws
Version:
87 agent skills for AI coding assistants — marketing, growth, web3, dev, design & operations. Built for OpenClaw, Claude Code, Cursor, and Codex.
455 lines (361 loc) • 25.3 kB
Markdown
---
name: pricing-optimization
description: "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."
---
# 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 type | Examples | Best fit | Watch out for |
|-------------|----------|----------|---------------|
| Per seat | $X/user/month | Collaboration tools where every user gets value | Customers share logins; AI agents replace seats (seat counts can shrink) |
| Per usage / metered | $X/API call, $X/GB, $X/1k tokens | Infra, APIs, AI products where cost tracks consumption | Unpredictable bills hurt buyer trust → add caps, alerts, or prepaid credits |
| Hybrid (platform fee + usage) | $X/mo base + overage | Usage products needing revenue floor & expansion | Two dials to explain; keep the base meaningful, not a tax |
| Per feature / tier | Tier-gated access | Horizontal SaaS with distinct segments | Feature gates feel arbitrary if not value-aligned |
| Per outcome | $X/lead, $X/transaction, % of GMV | Performance tools that can attribute results | Attribution disputes; revenue swings with customer's business |
| Committed spend | Annual $ commitment drawn down by usage | Enterprise usage products, procurement-friendly | Requires forecasting; overage/rollover policy must be explicit |
| Flat rate | $X/month | Simple, single-persona products | Leaves 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:
| Intersection | Meaning |
|-------------|---------|
| "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):**
| Element | Starter | Professional | Enterprise |
|---------|---------|-------------|------------|
| Price anchor | Low (attract) | Medium (convert) | High (capture) |
| Target | Individual / small team | Growing team | Large organization |
| Value metric limit | Low | Medium | Unlimited or custom |
| Support | Self-serve | Email + chat | Dedicated CSM |
| Features | Core only | Core + advanced | All + 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 type | Max | Approval |
|---------------|-----|----------|
| Annual prepay | 20% | Self-serve |
| Multi-year deal | 30% | Manager approval |
| Competitive switch | 15% | Manager approval |
| Volume (10+ seats) | 15% | Auto-calculated |
| Strategic / Logo | 40% | 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:**
| Tier | Countries | Adjustment |
|------|-----------|------------|
| Full price | US, UK, Canada, Australia, Germany, France | 100% |
| Tier 2 | Spain, Italy, Portugal, Czech Republic, Poland | 70-80% |
| Tier 3 | Brazil, Mexico, Turkey, South Africa | 50-60% |
| Tier 4 | India, Indonesia, Philippines, Nigeria | 30-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:**
| When | Action |
|------|--------|
| 90 days before | Internal alignment: sales, CS, support briefed |
| 60 days before | Email announcement to all customers (clear, empathetic) |
| 30 days before | Reminder email + lock-in offer (annual at current price) |
| Day of | Price change live + support team ready for questions |
| 30 days after | Review 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
```typescript
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
```typescript
// 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
| Pattern | Implementation | Best for |
|---------|---------------|----------|
| Free trial → paid | `subscription_data: { trial_period_days: 14 }` | Products needing time to show value |
| Freemium | No Stripe until upgrade; gate features in code | Wide-funnel products |
| Metered / usage-based | A **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 credits | Sell credits, draw down via meter events / Billing Credits | Bursty AI usage; predictable customer spend |
### Freemium Feature Gates
```typescript
// 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):**
```typescript
// 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:**
```typescript
// 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
```typescript
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:**
```typescript
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:
```typescript
// 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)
```typescript
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
| Item | Details |
|------|---------|
| 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 CLI | `stripe 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:
```typescript
// 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}` });
```
2. **Meter-event idempotency** — the meter event `identifier` (see §9) dedupes usage within a rolling 24h window; this is separate from the header above.
3. **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