Pricing Strategy skill for Codex

Price on the value metric, backed by WTP data. Codex works through pricing in dependency order: segments, value metric, model, packaging, price points, then rollout. It analyzes Van Westendorp survey data with a bundled script that drops inconsistent answers and reports the acceptable range per segment, checks prices against cost floors and customer alternatives, and treats changes for existing customers as a contractual step needing approval and legal review.

  • Product Management
  • $pricing-strategy
  • 160 lines
  • 3 files
  • ~1,900 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- pricing-strategy

Installs to ~/.agents/skills/pricing-strategy/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $pricing-strategy, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Works through SaaS and digital product pricing decisions - choosing a value metric, pricing model (flat, per seat, usage-based, tiered, hybrid), good-better-best packaging, and price points - backed by willingness-to-pay research (Van Westendorp price sensitivity survey with a bundled analysis script, Gabor-Granger, conjoint), competitor and cost floors, and a safe rollout plan for price changes with grandfathering. Use when asked to price a new product, restructure plans or tiers, design a pricing survey, analyze willingness-to-pay data, or plan a price increase.

Use it when

  • You are pricing a new product or plan
  • Tiers have grown confusing and need repackaging
  • You have or plan a willingness-to-pay survey
  • A price increase for existing customers is being considered

Use something else when

  • You are designing the pricing page layout and copy: use pricing-page
  • You need to implement subscriptions and invoices in Stripe: use stripe-billing
  • You need revenue, churn and LTV calculations: use saas-metrics

How to check the result

Script output matches the reported price points, chosen prices sit inside the measured acceptable range and above the cost floor, and each tier names its target segment.

The mistake it prevents

Copying a competitor's price list, which imports their segment and cost structure while ignoring what your own customers value.

~/.agents/skills/pricing-strategy/SKILL.md

--- name: pricing-strategy description: Works through SaaS and digital product pricing decisions - choosing a value metric, pricing model (flat, per seat, usage-based, tiered, hybrid),… license: MIT ---

A pricing decision is done well when the price scales with the value customers receive (the value metric), each tier exists for a named segment, price points rest on willingness-to-pay evidence rather than competitor copying, and existing customers are handled explicitly before anything changes. Pricing affects revenue, contracts and consumer-law obligations: final decisions belong to the business owner, and price-change notices should be reviewed by legal or finance.

Order of decisions

Decide in this order; later choices depend on earlier ones.

1. Segments: who buys, and how their value differs
2. Value metric: the unit price scales with
3. Model: how the metric turns into a bill
4. Packaging: which features and limits go in which tier
5. Price points: the numbers
6. Rollout: new vs existing customers, discounts, communication

Value metric

A good value metric grows as the customer gets more value, is easy for the buyer to predict, and is easy to measure and bill.

CandidateFits whenRisk
Seats / usersValue grows with people collaboratingDiscourages adding users; seat sharing
Active usersAdoption varies a lot by customerBills are harder to predict
Usage (API calls, GB, minutes, messages)Cost and value scale with volumeBill shock; needs spend caps and alerts
Outcomes (transactions processed, revenue share)Value is directly measurableHard to forecast; disputes
Entities (projects, contacts, locations)Value tied to scale of what is managedCustomers game by consolidating
Flat feeSingle segment, simple productLeaves money on the table with large customers

Test each candidate: “If a customer gets twice the value, does this number roughly double?” and “Could the buyer predict next month’s bill?”

Model and packaging

  • Good-better-best (three tiers) is the default for self-serve SaaS. Design each tier for a named segment, put the features that segment cannot do without in its tier, and gate on things large customers need (SSO, audit logs, admin controls, higher limits), not on core value everyone needs.
  • Hybrid (platform fee plus usage) when there is a fixed cost to serve plus variable consumption.
  • Free tier vs free trial: free tier when network effects or bottom-up adoption drive growth and marginal cost is low; time-limited trial when value appears within days and serving free users is expensive.
  • Keep the number of plans small enough to compare at a glance; add an “Enterprise - contact us” tier rather than a fourth self-serve tier.

Pricing page layout and copy belong to $pricing-page; Stripe implementation details to $stripe-billing.

Willingness-to-pay research

MethodUse whenSampleOutput
Qualitative pricing interviewsEarly, defining value and metric10-20Value drivers, budget owners, anchors
Van Westendorp PSMNeed an acceptable range for a defined offer100+ per segment (30 minimum)Range and indicative points
Gabor-GrangerChoosing among specific price points100+ per segmentDemand curve, revenue-maximizing point
Conjoint (choice-based)Trading off features vs price across packages200+; specialist toolingPart-worths, feature value
Live price testSelf-serve traffic is large enoughDepends on conversion rateReal conversion and revenue per visitor

Stated willingness to pay overestimates real behaviour; treat survey output as a range to test, not a final answer.

Van Westendorp survey

Show the offer description first (what it includes, billing period, currency), then ask in this order:

  1. At what price would this be so cheap that you would question its quality? (too cheap)
  2. At what price would it be a bargain, a great buy for the money? (cheap)
  3. At what price would it start to feel expensive, but you would still consider it? (expensive)
  4. At what price would it be too expensive to consider? (too expensive)

Analyze with the bundled script (drops respondents whose answers are out of order, needs at least 30 valid rows):

python3 scripts/van_westendorp.py responses.csv
python3 scripts/van_westendorp.py responses.csv --segment-col company_size --segment 50-200

CSV columns: too_cheap,cheap,expensive,too_expensive plus optional segment columns. It prints the point of marginal cheapness (PMC), optimal price point (OPP), indifference price point (IPP), point of marginal expensiveness (PME) and the acceptable range PMC to PME. Run it per segment; pooled results hide segments with very different budgets. Curve-crossing conventions differ slightly between tools; report which one you used.

Survey sampling and question bias: $survey-design.

Floors and anchors

  • Cost floor: gross margin target (often 70-80%+ for SaaS) on cost to serve, including infrastructure, third-party API costs and support. Usage-heavy AI features need per-unit cost modelling before any flat price.
  • Alternatives: what the customer pays today (competitor, spreadsheet plus staff time, agency). Competitor prices are anchors, not answers.
  • Value ceiling: quantified value for the customer (hours saved x loaded cost, revenue gained). Price usually captures 10-30% of quantified value.

Price changes for existing customers

Changing what existing customers pay is a high-trust, often contractual action.

- [ ] Inventory affected customers by plan, contract terms and renewal dates
- [ ] Choose: grandfather indefinitely, grandfather for N months, or migrate at renewal
- [ ] Model revenue and churn impact for each option (show assumptions)
- [ ] Check contract terms and notice obligations with legal/finance
- [ ] Draft customer communication: what changes, when, why, what they keep
- [ ] Get explicit approval before any email is sent or billing is changed
- [ ] Monitor churn, downgrades and support tickets for 60-90 days

Never change prices in a billing system or send pricing emails from Codex without the user’s explicit confirmation of the exact change and audience.

Output template

## Pricing recommendation: <product>
Segments: <list with value differences>
Value metric: <unit> - why: <scales with value, predictable, measurable>
Model: <e.g. good-better-best, per active seat, annual discount 15-20%>
| Tier | Target segment | Price | Key limits | Gated features |
Evidence: PSM range <x-y> (n=..., segment ...), interviews (n=...), cost floor <z>
Risks: <cannibalization, bill shock, migration churn>
Tests to run: <live test or follow-up research>
Decision owner: <name>; legal/finance review needed for <items>

Gotchas

  • Copying the market leader’s price inherits their cost structure and segment, not yours.
  • Too many discount levers (coupons, custom deals) destroy price integrity; set a discount policy with approval thresholds.
  • Usage pricing without caps, alerts and a predictable minimum produces bill shock and churn.
  • Annual-only plans raise cash but block small buyers; offer monthly at a premium.
  • Survey respondents who are current customers anchor on what they already pay; segment them from prospects.
  • This skill supports business pricing decisions; it is not financial or legal advice.

Verification

Check that each tier names its target segment, the value metric passes the “twice the value” and “predictable bill” tests, price points sit inside the measured acceptable range and above the cost floor, the script output matches the reported numbers, and any change to existing customers has a written plan and an approval step.

What the zip contains

Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Pricing Strategy"
  short_description: "Price on the value metric, backed by WTP data"
  default_prompt: "$pricing-strategy Help me choose between per-seat and usage-based pricing for our API product"
scripts/van_westendorp.py124 lines, 4.9 KB
#!/usr/bin/env python3
"""Van Westendorp Price Sensitivity Meter from survey responses.

Usage:
  python3 van_westendorp.py responses.csv [--segment-col plan --segment Pro]

CSV columns (case-insensitive): too_cheap, cheap, expensive, too_expensive
  - one row per respondent, numeric prices in one currency and billing period
  - optional extra columns (e.g. segment) are ignored unless --segment-col is set

Curve conventions (as in common PSM tooling):
  too cheap      share answering too_cheap >= p           (falls with price)
  not cheap      1 - share answering cheap >= p           (rises)
  not expensive  1 - share answering expensive <= p       (falls)
  too expensive  share answering too_expensive <= p       (rises)
  PMC = too cheap x not cheap        OPP = too cheap x too expensive
  IPP = cheap x expensive            PME = too expensive x not expensive

Respondents whose answers are not ordered too_cheap <= cheap <= expensive <=
too_expensive are dropped and counted. Exits non-zero on bad input or n < 30.
Standard library only.
"""
import argparse
import csv
import sys

COLS = ["too_cheap", "cheap", "expensive", "too_expensive"]


def load(path, seg_col, seg_val):
    rows, dropped = [], 0
    with open(path, newline="", encoding="utf-8-sig") as fh:
        reader = csv.DictReader(fh)
        if not reader.fieldnames:
            sys.exit("error: empty CSV")
        reader.fieldnames = [f.strip().lower() for f in reader.fieldnames]
        missing = [c for c in COLS if c not in reader.fieldnames]
        if missing:
            sys.exit(f"error: missing columns: {', '.join(missing)}")
        if seg_col and seg_col.lower() not in reader.fieldnames:
            sys.exit(f"error: segment column '{seg_col}' not found")
        for row in reader:
            if seg_col and (row.get(seg_col.lower()) or "").strip() != seg_val:
                continue
            try:
                vals = [float(str(row[c]).replace(",", "").strip().lstrip("$")) for c in COLS]
            except ValueError:
                dropped += 1
                continue
            if not (vals[0] <= vals[1] <= vals[2] <= vals[3]) or vals[0] >= vals[3]:
                dropped += 1
                continue
            rows.append(vals)
    return rows, dropped


def curves(rows, p):
    n = len(rows)
    too_cheap = sum(r[0] >= p for r in rows) / n
    cheap = sum(r[1] >= p for r in rows) / n
    expensive = sum(r[2] <= p for r in rows) / n
    too_exp = sum(r[3] <= p for r in rows) / n
    return {
        "too_cheap": too_cheap,
        "cheap": cheap,
        "not_cheap": 1 - cheap,
        "expensive": expensive,
        "not_expensive": 1 - expensive,
        "too_expensive": too_exp,
    }


def crossing(rows, grid, a, b):
    """First price where curve a (falling) meets or drops below curve b (rising)."""
    prev_p, prev_d = None, None
    for p in grid:
        c = curves(rows, p)
        d = c[a] - c[b]
        if d <= 0:
            if prev_d is None or prev_d == d:
                return p
            return prev_p + (p - prev_p) * prev_d / (prev_d - d)
        prev_p, prev_d = p, d
    return None


def main():
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("csv_path")
    ap.add_argument("--segment-col")
    ap.add_argument("--segment")
    ap.add_argument("--steps", type=int, default=2000, help="price grid resolution")
    args = ap.parse_args()
    if bool(args.segment_col) != bool(args.segment):
        sys.exit("error: use --segment-col and --segment together")

    rows, dropped = load(args.csv_path, args.segment_col, args.segment)
    print(f"respondents used: {len(rows)}  dropped (inconsistent or non-numeric): {dropped}")
    if len(rows) < 30:
        sys.exit("error: fewer than 30 valid respondents; results would be unstable")

    lo = min(r[0] for r in rows)
    hi = max(r[3] for r in rows)
    grid = [lo + (hi - lo) * i / args.steps for i in range(args.steps + 1)]
    points = {
        "PMC  point of marginal cheapness": crossing(rows, grid, "too_cheap", "not_cheap"),
        "OPP  optimal price point": crossing(rows, grid, "too_cheap", "too_expensive"),
        "IPP  indifference price point": crossing(rows, grid, "cheap", "expensive"),
        "PME  point of marginal expensiveness": crossing(rows, grid, "not_expensive", "too_expensive"),
    }
    for label, value in points.items():
        print(f"{label:<40} {'n/a' if value is None else f'{value:,.2f}'}")
    pmc = points["PMC  point of marginal cheapness"]
    pme = points["PME  point of marginal expensiveness"]
    if pmc is not None and pme is not None:
        print(f"\nacceptable price range: {pmc:,.2f} - {pme:,.2f}")
        if pmc >= pme:
            print("warning: PMC >= PME; the sample does not agree on a range")
    if dropped / max(1, len(rows) + dropped) > 0.2:
        print("warning: over 20% of answers were inconsistent; check question order and wording")


if __name__ == "__main__":
    main()

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc