A/B experiments
Price experiments are configured entirely in the monetize.software dashboard (paywall editor → A/B Experiments) and require zero integration code. A share of your visitors sees variant prices, the rest see the control ones, and the dashboard reports view → purchase conversion per variant with statistical significance.
Requires @monetize.software/sdk / sdk-react 3.1+ (sdk-extension 3.2+). Older SDK versions simply ignore the experiment payload — their users always see the control prices, so upgrading is safe to roll out gradually.
How it works
- The bootstrap payload carries the running experiment with all variants (
bootstrap.experiment). There is at most one running experiment per paywall. - The SDK deterministically buckets the device: a hash of the stable visitor id (
pw-visitor-id) and the experiment id, proportional to the traffic weights. The first assignment is persisted in storage and stays sticky for this device — reopening the paywall, reloading the page, or editing weights later never flips an already-bucketed visitor. - For the assigned variant the SDK substitutes the variant prices into the bootstrap before anything renders: the modal’s price grid,
getPrices()/getCachedPrices(), offers and CTA references all see the variant’s prices and ids. Control (and any unrecognized experiment kind) renders the paywall exactly as configured. - Checkout charges whatever price id the UI shows — variant prices are real prices on the backend, so Stripe, Paddle, Chargebee, Overpay and Polar all work unchanged.
- Every built-in analytics event is enriched with
experiment_id+variant, andcreateCheckoutautomatically attaches the visitor id and assignment — that’s what powers the conversion report in the dashboard.
Experiments never break the paywall. Any failure — storage unavailable, malformed config, unknown experiment kind — silently falls back to the control experience.
What you need to do
Nothing, for the standard PaywallUI integration. Upgrade the SDK, start an experiment in the dashboard, watch the results.
Two caveats for advanced setups:
Don’t hardcode price ids. paywall.checkout('123') or <PaywallButton priceId="123"> with a hardcoded control price id bypasses the experiment — that user gets charged the control price regardless of their variant. Render your own pricing UI from getPrices() / getCachedPrices() and pass the ids they return; those are already the variant’s ids for bucketed users.
Keep built-in analytics enabled (analytics: true, the default) while an experiment is running. The dashboard measures exposure from the SDK’s paywall_viewed events — with analytics: false your variants get traffic but the report stays empty.
Reading the assignment
If you want to mirror the variant into your own analytics or keep host-rendered pricing consistent:
const assignment = paywall.billing.getExperimentAssignment();
// → { experimentId: 'e7a1…', variant: 'b' } | null (no running experiment / bootstrap not loaded yet)
if (assignment) {
amplitude.track('experiment_exposure', {
experiment_id: assignment.experimentId,
variant: assignment.variant
});
}The assignment is available after the first bootstrap() resolves (e.g. inside the ready event, or after paywall.preload()). The raw config is also on the bootstrap itself:
interface PaywallExperiment {
id: string;
kind: 'prices'; // open set — future kinds are ignored by older SDKs
variants: Array<{
key: string; // 'control', 'b', …
weight: number; // relative traffic weight
prices?: Array<PaywallPrice & { replaces: string | null }>;
}>;
assigned_variant?: string; // stamped client-side after bucketing
}
const bootstrap = await paywall.billing.bootstrap();
bootstrap.experiment?.assigned_variant; // 'control' | 'b' | undefinedBehavior details
| Situation | Behavior |
|---|---|
| Experiment stopped in the dashboard | Everyone returns to control on the next bootstrap revalidate (stale-while-revalidate window, at most ~1 hour of client cache). |
| A new experiment starts | New experiment id → devices are re-bucketed from scratch. Assignments never carry over between experiments. |
| Same user on another device / browser / incognito | Bucketing is per-device (local visitor id), so they may land in a different variant. This is the standard trade-off of anonymous-traffic experiments. |
| Offers (discount countdowns) | Percentage discounts apply on top of the variant’s prices, same as control. |
| Local (geo) prices | Fully supported: each variant carries its own local prices (configured in the dashboard’s Local Pricing editor, pre-filled proportionally to the USD change). A visitor sees their local currency in whichever arm they land in, and checkout charges the same localized amount. |
| Chrome extension | Works identically — the assignment lives in the shared offscreen storage, so popup, content scripts and background all agree on the variant. |
Next steps
- BillingClient — bootstrap and caching — where the experiment payload lives and how it revalidates.
- Events — the analytics channel that feeds the experiment report.
- Storage adapters — where the sticky assignment is persisted (
pw-{paywallId}-exp-v1).