SEO & Growth No. 430

Conversion Optimization skill for Claude Code

Research-led A/B tests sized before they start. Claude runs CRO as a research process, not a list of button tweaks. It quantifies funnel leaks by device, pairs each with qualitative evidence from replays and surveys, and writes falsifiable hypotheses ranked with ICE. A bundled stdlib calculator sizes tests, finds the detectable effect for your traffic, and flags sample ratio mismatch before anyone reads the results.

  • nameconversion-optimization
  • size178 lines · ≈2,050 tokens
  • files2
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Runs a conversion rate optimization program from funnel analysis to shipped experiments, covering drop-off diagnosis, qualitative research (session replays, surveys, user tests), evidence-backed hypotheses, ICE prioritization, A/B test design with sample size, run length, guardrails, and SRM checks, and a test log. Use when signup, checkout, or trial conversion is low, when planning A/B tests in PostHog, Optimizely, VWO, GrowthBook, or Statsig, when asked how long a test must run, or when deciding which page changes to try first.

Try asking

  • Our checkout converts at 1.8% on mobile vs 3.9% on desktop. Find out why and plan tests
  • How long do we need to run a test on a 4% signup rate to detect a 10% lift?
  • Prioritize these 12 test ideas from the team and design the first experiment

Use it when

  • Signup, checkout, or trial conversion is below expectations
  • Planning or sizing an A/B test
  • Turning a backlog of test ideas into a prioritized roadmap
  • A test result looks suspicious or the traffic split is uneven

Skip it when

How to check the result

Each test has a precomputed sample size and run length from ab_calc.py, passes the SRM check at readout, and is logged with its primary metric, guardrails, and decision.

The common mistake it prevents

Stopping a test as soon as the dashboard shows significance, which turns random noise into a shipped 'winner' that never holds up.

~/.claude/skills/conversion-optimization/SKILL.md

--- name: conversion-optimization description: Runs a conversion rate optimization program from funnel analysis to shipped experiments, covering drop-off diagnosis, qualitative research… license: MIT ---

CRO done well finds where and why qualified visitors drop, tests the changes most likely to move the business metric, and only declares winners from tests that were sized in advance, ran full weeks, passed a sample-ratio check, and did not hurt guardrails. Most of the value comes from research and prioritization, not from button colors.

Ground rules

  • Diagnose before prescribing. No hypothesis without a quantitative signal (where) and a qualitative one (why).
  • Optimize for the metric that pays: completed purchases, activated accounts, retained revenue — not clicks on the CTA you changed.
  • Size every test before launch. If the page lacks traffic to detect a realistic effect, do not A/B test it; ship research-backed fixes and monitor, or test higher in the funnel.
  • Fix bugs and broken flows directly; they are not experiments.
  • Never stop a test the moment it “reaches significance” (peeking inflates false positives) unless the tool uses sequential testing designed for it.
  • Respect consent and privacy: session replay and heatmap tools must mask inputs and load only under the consent the user’s region requires.

Workflow

- [ ] 1. Define the conversion and baseline
- [ ] 2. Map the funnel and find the biggest leaks
- [ ] 3. Research why (qualitative)
- [ ] 4. Write hypotheses
- [ ] 5. Prioritize with ICE
- [ ] 6. Design and size the test
- [ ] 7. QA and launch
- [ ] 8. Analyze, decide, log

1. Conversion and baseline

Name the primary conversion event, its unit (visitor, session, account), and the baseline rate over the last 4–8 full weeks, split by device and top traffic sources. Confirm the event fires once per conversion and matches backend records within ~5%; if not, fix tracking first (analytics-tracking-plan).

2. Funnel and leaks

Build the step funnel in the analytics tool (GA4 funnel exploration, PostHog/Mixpanel/ Amplitude funnels), per device:

StepUsersStep conversionDrop-offMobile vs desktop
Landing view40,000——
Pricing view9,20023%77%
Signup start2,30025%75%
Signup complete1,15050%50%mobile 31% / desktop 62%

Leak size = users lost × value per conversion. Big device gaps, steps below peer pages, and error-event spikes are the leads. Check page speed for the leaking step (field LCP and INP at p75) — slow mobile steps are a common hidden cause; hand off to core-web-vitals.

3. Qualitative research

MethodAnswersVolume needed
Session replays of drop-offsWhere people hesitate, rage-click, hit errors20–40 sessions per leaking step
On-page exit or intent survey (1 question)“What’s stopping you from signing up today?”50–200 responses
Post-conversion surveyWhat almost stopped buyers; what convinced them50+
5 moderated user testsComprehension, trust, usability failures5 per segment finds most major issues
Sales/support transcriptsObjections in customer wordsAll recent
Form analyticsField-level abandonment and errorsPer form

Code findings into themes with counts. A theme seen in both data and research becomes a hypothesis.

4. Hypotheses

Format every hypothesis so it can be wrong:

Because we saw <evidence: data + research>, we believe that <change> for <segment>
will cause <metric> to <direction>. We'll know when <primary metric> moves by ≥ <MDE>
without <guardrail> worsening.

Prefer changes that address the motivation, clarity, trust, or friction problem the research revealed: clearer value proposition, answering an objection near the CTA, removing form fields, showing price earlier, adding guest checkout, fixing mobile layout.

5. Prioritize with ICE

Score 1–10, average or multiply, sort descending:

FactorHigh score means
ImpactLarge leak × strong evidence × close to revenue
ConfidenceSupported by both quant and qual data, or proven in similar tests
EaseDays not weeks to build, no cross-team dependencies, enough traffic to test

Break ties toward bolder changes: on low-to-mid traffic, small tweaks cannot reach significance. If a page needs > 8 weeks to detect the expected effect, it scores low on Ease.

6. Design and size the test

Use the bundled calculator (two-sided z-test for proportions):

python3 scripts/ab_calc.py size --baseline 0.042 --mde 0.10 --daily 3000
python3 scripts/ab_calc.py mde --baseline 0.042 --per-variant 20000

Design rules:

  • MDE: the smallest lift worth acting on, usually 5–20% relative. Smaller MDE → quadratically more traffic.
  • Defaults: alpha 0.05 two-sided, power 0.8, 50/50 split. With multiple variants the script applies Bonferroni; prefer A/B over A/B/C/D unless traffic is plentiful.
  • Run length: whole weeks (minimum 1, ideally 2+) to cover weekday cycles; stop at the precomputed sample size, not at first significance. Cap at ~4–6 weeks; cookie churn and seasonality degrade longer tests.
  • Randomization unit: user (logged-in ID or stable device ID), not pageview, so a person always sees one variant.
  • Primary metric: one, decided in advance. Guardrails: revenue per visitor, refund rate, error rate, page performance, downstream activation.
  • Targeting: only the users who reach the changed experience (trigger-based exposure logging) — diluting with unexposed users hides effects.
  • Flicker: client-side testing tools can flash the original; use server-side or edge assignment or the tool’s anti-flicker snippet with a short timeout.

7. QA and launch

  • Both variants render on target devices and browsers; screenshots attached.
  • Exposure event fires once per user with the variant name; conversion events attribute to it.
  • Internal traffic and bots excluded.
  • No other test overlaps the same users and metric (or tests are mutually exclusive).
  • Rollback is one switch (feature flag), documented.

8. Analyze, decide, log

  1. SRM check first: python3 scripts/ab_calc.py srm <control_n> <variant_n>. If it exits 1 (p < 0.001), the assignment or logging is broken; discard results until fixed.
  2. Read the primary metric with its confidence interval, not just the p-value. Then guardrails. Segment cuts (device, new vs. returning) are exploratory; do not declare segment winners unless pre-registered.
  3. Decide: ship (primary up, guardrails flat), iterate (directional, learnings clear), or kill. Inconclusive is a valid result — do not run “until it wins”.
  4. Hand deep statistical questions (CUPED, sequential testing, Bayesian readouts) to experiment-analysis.
  5. Log every test, including losers, in the template below.

Gotchas

  • Novelty effects: a redesign often spikes in week 1 and decays. Look at the trend over the full run.
  • Testing on pages where most traffic is returning customers measures habit disruption, not persuasion.
  • Measuring “click on CTA” as primary rewards clickbait CTAs that lose at checkout.
  • Multiple simultaneous changes in one variant are fine for big-swing tests, but you will not know which change worked; follow up with isolating tests if it matters.
  • Before/after comparisons are not tests; seasonality, campaigns, and releases confound them.
  • Heatmaps aggregate across devices and scroll depths; segment them or they mislead.

Output: test log entry

### T-014 Pricing page: show annual price per month + objection FAQ near CTA
Hypothesis: Because 38% of exit-survey answers cite "unclear what I'd pay" and replays show
pricing → FAQ scroll loops, showing monthly-equivalent price and a 4-question FAQ beside the
CTA will raise trial starts from pricing viewers by ≥ 10%.
ICE: I8 C7 E8 = 7.7 · Primary: trial_started / pricing viewers · Guardrails: paid conversion at 30d, RPV
Design: 50/50 user-level, baseline 4.2%, MDE 10% rel, n = 39,000/variant, 21 days
SRM: p = 0.41 (pass)
Result: +11.8% (95% CI +3.1% to +20.5%), guardrails flat → Ship
Learning: Price clarity outweighs discount framing for SMB visitors.

Bundled files

Loaded only when a step in the skill needs them. Review them here before installing.

scripts/ab_calc.py 147 lines · 5.4 KB
#!/usr/bin/env python3
"""A/B test planning and sanity checks (stdlib only).

Sample size for a conversion-rate test (two-sided, per variant):
    python3 ab_calc.py size --baseline 0.042 --mde 0.10 [--alpha 0.05] [--power 0.8]
                            [--variants 2] [--daily 3000]
    --mde is RELATIVE lift (0.10 = +10%). Use --abs-mde for absolute points (0.004).
    --daily = eligible visitors per day across all variants; prints run length.

Minimum detectable effect for a fixed traffic budget:
    python3 ab_calc.py mde --baseline 0.042 --per-variant 20000

Sample ratio mismatch check (chi-square goodness of fit vs. intended split):
    python3 ab_calc.py srm 50312 49120 [--weights 50 50]
    Exits 1 when p < 0.001 (treat results as untrustworthy until explained).
"""
from __future__ import annotations

import argparse
import math
import sys
from statistics import NormalDist

Z = NormalDist()


def n_per_variant(p1: float, p2: float, alpha: float, power: float, comparisons: int) -> int:
    a = alpha / comparisons  # Bonferroni for multiple treatment arms vs. control
    za = Z.inv_cdf(1 - a / 2)
    zb = Z.inv_cdf(power)
    pbar = (p1 + p2) / 2
    num = (za * math.sqrt(2 * pbar * (1 - pbar)) + zb * math.sqrt(p1 * (1 - p1) + p2 * (1 - p2))) ** 2
    return math.ceil(num / (p2 - p1) ** 2)


def gammaincc(s: float, x: float) -> float:
    """Regularized upper incomplete gamma Q(s, x)."""
    if x <= 0:
        return 1.0
    if x < s + 1:  # series for P, return 1 - P
        term = total = 1.0 / s
        k = s
        for _ in range(500):
            k += 1
            term *= x / k
            total += term
            if abs(term) < abs(total) * 1e-15:
                break
        return 1.0 - total * math.exp(-x + s * math.log(x) - math.lgamma(s))
    # continued fraction (Lentz) for Q
    b = x + 1 - s
    c = 1 / 1e-300
    d = 1 / b
    h = d
    for i in range(1, 500):
        an = -i * (i - s)
        b += 2
        d = an * d + b
        d = 1e-300 if abs(d) < 1e-300 else d
        c = b + an / c
        c = 1e-300 if abs(c) < 1e-300 else c
        d = 1 / d
        delta = d * c
        h *= delta
        if abs(delta - 1) < 1e-15:
            break
    return math.exp(-x + s * math.log(x) - math.lgamma(s)) * h


def cmd_size(a) -> int:
    p1 = a.baseline
    p2 = p1 + a.abs_mde if a.abs_mde else p1 * (1 + a.mde)
    if not 0 < p1 < 1 or not 0 < p2 < 1:
        print("ERROR: rates must be between 0 and 1")
        return 2
    comps = max(1, a.variants - 1)
    n = n_per_variant(p1, p2, a.alpha, a.power, comps)
    total = n * a.variants
    print(f"baseline {p1:.4%} -> target {p2:.4%} (relative {p2 / p1 - 1:+.1%}, absolute {p2 - p1:+.4f})")
    print(f"alpha {a.alpha} two-sided{' (Bonferroni over %d comparisons)' % comps if comps > 1 else ''}, power {a.power}")
    print(f"sample size: {n:,} per variant, {total:,} total")
    if a.daily:
        days = math.ceil(total / a.daily)
        weeks = math.ceil(days / 7)
        print(f"run length at {a.daily:,}/day: {days} days -> run {max(weeks, 1) * 7} days (whole weeks, minimum 7)")
        if days > 56:
            print("WARNING: > 8 weeks. Test a bolder change, a higher-traffic page, or a higher-rate metric.")
    return 0


def cmd_mde(a) -> int:
    p1, n = a.baseline, a.per_variant
    lo, hi = 1e-6, 1 - p1 - 1e-9
    for _ in range(100):  # bisection on absolute lift
        mid = (lo + hi) / 2
        if n_per_variant(p1, p1 + mid, a.alpha, a.power, 1) > n:
            lo = mid
        else:
            hi = mid
    print(f"with {n:,} per variant at baseline {p1:.4%}: MDE = +{hi:.4f} absolute ({hi / p1:+.1%} relative)")
    return 0


def cmd_srm(a) -> int:
    counts = a.counts
    weights = a.weights or [1] * len(counts)
    if len(weights) != len(counts):
        print("ERROR: --weights must match number of counts")
        return 2
    total, wsum = sum(counts), sum(weights)
    chi2 = sum((c - total * w / wsum) ** 2 / (total * w / wsum) for c, w in zip(counts, weights))
    df = len(counts) - 1
    p = gammaincc(df / 2, chi2 / 2)
    shares = ", ".join(f"{c / total:.2%}" for c in counts)
    print(f"observed shares: {shares}; chi2 = {chi2:.2f}, df = {df}, p = {p:.6f}")
    if p < 0.001:
        print("SRM DETECTED: assignment or logging is broken. Do not read the results until fixed.")
        return 1
    print("No sample ratio mismatch at p < 0.001.")
    return 0


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    sub = ap.add_subparsers(dest="cmd", required=True)
    s = sub.add_parser("size")
    s.add_argument("--baseline", type=float, required=True)
    s.add_argument("--mde", type=float, default=0.10)
    s.add_argument("--abs-mde", type=float)
    s.add_argument("--alpha", type=float, default=0.05)
    s.add_argument("--power", type=float, default=0.8)
    s.add_argument("--variants", type=int, default=2)
    s.add_argument("--daily", type=int)
    m = sub.add_parser("mde")
    m.add_argument("--baseline", type=float, required=True)
    m.add_argument("--per-variant", type=int, required=True)
    m.add_argument("--alpha", type=float, default=0.05)
    m.add_argument("--power", type=float, default=0.8)
    r = sub.add_parser("srm")
    r.add_argument("counts", type=int, nargs="+")
    r.add_argument("--weights", type=float, nargs="+")
    a = ap.parse_args()
    return {"size": cmd_size, "mde": cmd_mde, "srm": cmd_srm}[a.cmd](a)


if __name__ == "__main__":
    sys.exit(main())

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc