NIC BRIEF — Put annual back on the quiz paywall at $97 charged today, and make the paywall bucketable

Week 1 job · 27 July 2026 · written directive, not a discussion · owner: Nic · reviewer: Aga
🔴 The one-paragraph version
The live quiz ships VITE_QUIZ_VARIANT="paywall_v2_2" as a build-time constant. Variant v2_2's tier list contains no annual plan, and the tier function takes only the variant — never the path — so 100% of traffic, on every path, has been unable to buy annual since 20 June — last annual sale 19 Jun, then 21 days at exactly zero (20 Jun – 9 Jul). Annual is 48.9% of new-sale cash. This brief does two things: (1) adds an annual rung so the paywall can sell the plan that pays, and (2) replaces the hardcoded constant with sticky randomised assignment so we can run a real test instead of shipping blind. Both are small. Do them together.
✅ RULED BY AGA, 27 Jul 2026 — annual goes back at $97, CHARGED TODAY
"Restore annual, charged today, at $97." This supersedes the earlier CMO/CEO $157-with-trial ruling — that reasoning is preserved in the plan document as the fallback and its falsifiers are now our monitoring list.

Ship it as the HYBRID, not a naive swap:

🔴 Still absolute: never put $97 behind a trial. At the measured 36.9% trial conversion that models at −9% vs today. $97 is safe only when charged today. If you find yourself building "$97, $0 today", stop and flag it.

Kill-switch: rolling 14-day revenue per paywall view below $6.04 ⇒ revert to the live tier list and tell Aga.

⚠️ Two corrections from Aga — read before you start
1. QuizFinished is NOT a signal. The 157 → 8 fall is a quiz-migration artifact — the old quiz carried that tag, the new one does not. Struck from the diagnostic. Do not spend time on it.

2. The date is not a proven cause. Stripe shows the last annual sale on 19 June 2026 and then 21 days at exactly zero (20 Jun – 9 Jul) — no other dry gap of even 7 days since February. That part is solid. But the new quiz funnel went live around May, so the June break is a later change, not the launch. Please pull the actual paywall-variant switch date from deploy history — it is the one fact that turns this from correlation into cause, and you are the only person who can get it.

Scope — what this brief covers and what it does not

In scopeOut of scope
New paywall variant v2_5 (Arm A) — additive to v2_2Redesigning the quiz body or the landing screen
Fallback variant v2_6 — $157 behind the trial, built not shippedAny change to the checkout/register page design
Sticky randomised variant assignment + persistenceChanging renewal prices on the existing book
Emitting quiz_variant to GA4 (event param + user property)The 4 dead traffic sources (Aga owns, separate task)
Forwarding UTMs + variant into Stripe subscription_data.metadataAny native-app pricing (RevenueCat rail is untouched)

Step 1 — Build variant v2_5 (Arm A). This is the cash.

Design principle: strictly additive
Do not remove the tiers that currently work. Today's start-to-paid is a healthy 61.8% precisely because the 4-week and 12-week tiers charge immediately. We are adding a rung, not swapping the model. The pre-built v2_1 is not a safe drop-in — it deletes quarterly and trial-gates every tier.

Tier list for v2_5 — THE SHIP

TierPriceMechanicaddToCartValueSelected
4-Week$12.48charged today — unchanged12.48no
12-Week$24.98charged today — unchanged24.98no
12-Month$97CHARGED TODAY · no trial97YES — pre-selected on render
✅ $97 also fixes the inverted value ladder — for free
The page sells in per-day and per-week framing. At the current ramp prices the ladder now runs monthly $6.20/wk → quarterly $3.84/wk → annual $1.87/wk — monotonic, so "BEST VALUE" on the annual rung is true, not decorative.

This is why the ramps stay at their current prices. At $157 the ladder inverted (12-week worked out cheaper per day than annual) and would have forced a reprice of the ramps. At $97 it does not. Change nothing about the 4-week and 12-week cards.

❓ OPEN for Aga — the 1-Week free-trial tier
The ruling covers the annual tier's price and mechanic. It does not say what happens to the existing 1-Week free-trial rung. Do not delete it on your own initiative — build the three tiers above, leave the 1-week rung exactly as it is, and flag it. Aga decides separately.

Requirements

  1. Pre-selection must land on annual. Reuse the existing pattern tiers.find(t => !!t.checkoutUrl) — but verify it selects the 12-Month rung, not the first direct-charge tier. If ordering makes that fragile, select explicitly by tier id.
  2. One primary CTA. Today the page has two competing buttons — a mid-page "Claim My Plan →" (charged today) and a sticky "Get My Recovery Plan Free →" ($0 today, then $24.97/mo). The sticky button currently enrols at MONTHLY, the lowest-cash plan on the page. Both buttons must submit the selected tier. This is the single highest-leverage line in the brief.
  3. No "50% off" framing on the annual rung. $157 is presented as the annual price, not as a discounted number. Keep $157 as the stated list and renewal price everywhere else — this preserves the published "$157/yr (~$3.02/wk)" across BWA, TMA.com, email and YouTube, so no estate-wide re-sync is needed.
  4. addToCartValue must carry the real price. The live trial tier sends value 0, and Meta optimises on add_to_cart — so we are actively training the ad platform on a zero-value event. Send the plan's actual price.

Step 2 — Build v2_6 as the FALLBACK build it, do not ship it

The fallback is the earlier CMO/CEO recommendation: annual $157, pre-selected, behind the 7-day trial, ramps unchanged. Build it behind the flag so a revert is a config change, not a sprint.

TierPriceMechanicNotes
4-Week$12.48charged todayunchanged
12-Week$24.98charged todayunchanged
12-Month$157$0 today · 7-day trialpre-selected; keeps the published $157 anchor

When the fallback gets used: the kill-switch fires (14-day RPV < $6.04), or Aga rules again after the first $97 cohorts read out.

Step 3 — Replace the hardcoded variant with sticky bucketing

Today: VITE_QUIZ_VARIANT is a build constant; zt() reads it and Ice only validates it. There is no randomisation, so every variant is 100%/0% and no test can run.

// Required behaviour — assign ONCE, on first quiz load, then never change.
// Sticky across sessions and devices-per-browser. Server-side is fine too.

const ARMS = ['paywall_v2_5', 'paywall_v2_6'];   // 50/50
const KEY  = 'tma_quiz_arm_v1';

function getArm() {
  // 1. explicit override always wins — needed for QA and for Aga to preview
  const forced = new URLSearchParams(location.search).get('arm');
  if (forced && ARMS.includes(forced)) return forced;

  // 2. sticky assignment
  let arm = null;
  try { arm = localStorage.getItem(KEY); } catch (e) {}
  if (!arm) arm = readCookie(KEY);
  if (arm && ARMS.includes(arm)) return arm;

  // 3. first touch — assign and persist to BOTH stores
  arm = ARMS[Math.floor(Math.random() * ARMS.length)];
  try { localStorage.setItem(KEY, arm); } catch (e) {}
  writeCookie(KEY, arm, 180);   // 180 days, root domain
  return arm;
}
Non-negotiables on the bucketing

Step 4 — Instrument it, or the test is unreadable

Bucketing without instrumentation produces a test nobody can read. These three ship with the variants, not after.

4a · GA4 — quiz_variant as event param AND user property
Event param alone will not let you split funnel steps by arm across sessions. Set both, on first quiz load.
gtag('set', 'user_properties', { quiz_variant: arm });
gtag('event', 'quiz_started', { quiz_variant: arm, screen_id: 'S01' });
4b · 🔴 Stripe — forward UTMs + variant into subscription_data.metadata

This fixes the test and the dark attribution in one change. The quiz already appends utm_source · utm_medium · utm_campaign · utm_content · quiz_variant · screen_id + email to the register URL. The break is that the register page never forwards them into Stripe: subscription.metadata and invoice.metadata are empty on all 154 sales in the last 90 days.

stripe.checkout.sessions.create({
  // …existing config…
  subscription_data: {
    metadata: {
      quiz_variant: arm,
      utm_source:   params.utm_source   || '',
      utm_medium:   params.utm_medium   || '',
      utm_campaign: params.utm_campaign || '',
      utm_content:  params.utm_content  || '',
      quiz_session: params.session_id   || ''
    }
  }
});

Stripe propagates this to invoice.subscription_details.metadataverified live, the field exists and is currently {}. Once set, every future sale is attributable to arm and source without any new pipeline.

Note: backfilling history recovers only ~20–30%; 70–80% of past attribution is permanently dark. That is accepted — this fix is about everything from here on.

4c · Log the silent bail-out
There is a branch if (i !== "control" && !P?.checkoutUrl) return; that exits without firing any event. Its victims are invisible by construction — they are not in the 356 counted buy-click abandoners. Emit an event before returning so we can size it. Until this logs, "the dead button costs $X" is unmeasurable, not small.

Step 5 — Ship the defect fixes in the same deploy

#DefectFix
1GET app.themovementathlete.com/api/quiz/config returns 404 — quiz runs on fallback configRestore the endpoint. Plausible (unproven) root cause of the dead downsell button.
2The downsell button ("show me another option") has an onclick but does nothing across 6 screensWire it, or remove it. A dead price-objection escape hatch is worse than none.
3A "Report Bug" widget ships on the production paywall, overlapping the primary CTA on mobileRemove from production build.
4Copy says "96 progression levels across 9 foundations"Change to "100+ progression levels across the 9 Fundamentals" — source of truth is 123; customer-facing copy uses the drift-proof "100+".
5Funnelytics tracker returns 403Fix or remove — a broken tracker on every page load is latency for nothing.
6🔴 STRIPE — the billing period on both ramps is misstated. Live Stripe: monthly3 ($24.97) is interval: month, interval_count: 1 → bills every month. quarterly2 ($49.97) is interval_count: 3 → bills every 3 months. No TMA price in the 82 active prices uses a week interval — there is no 4-week or 12-week product. The paywall says "4 Weeks / then $24.97 per 4 wks" (implies 13 charges a year) and "12 Weeks / then $49.97 per 12 wks".Relabel to the real cadence: 4 Weeks → 1 Month, 12 Weeks → 3 Months, "/ 4 wks" → "every month", "/ 12 wks" → "every 3 months". No arithmetic changes — $287 already = 12 monthly cycles and $175 already = 4 quarterly cycles, both computed on the true cadence. Labels only. CMA209 §5.32 requires the price stated per actual billing period.
7🔴 STRIPE — the hero's mechanic does not exist. The paywall promises "$97 today … renews $157". There is no $60-off / 38.2%-off coupon in the Stripe account (coupon set is percent-off 30/50/70/99 plus tma_v3_50off_once), and every annual sub created since 1 Jun 2026 used the $157 price with no coupon at all.Mint a duration: once mechanism that charges $97 on the first annual term and $157 thereafter, or the "renews $157" line is unbuildable as written. Do not implement $97 as a trial — models −9% at the measured 36.9% trial conversion. $97 is safe only when charged today.
8The ramps' 50%-off is real and is already minted: coupon tma_v3_50off_once — "TMA 50% first cycle", percent_off: 50, duration: once, valid, 9 redemptions.No action — recorded so nobody re-mints it. This is what backs the new on-card line "Half price for your first 12 weeks / 4 weeks".

Step 6 — Wire the paywall page shipped 29 Jul (design is done; these are the hooks)

🔴 BUILD FROM THIS ONE: tma-paywall-spec.netlify.app — the UI contract. It is the same page with every variant stripped out: one arm only (hero 12-Month full-width + two ramps, first-year cost on every tier), no model tabs, no alternative arms, no A/B appendix. What is on that page is what ships. The only control on it is the 8-path preview strip — copy changes per path; layout and pricing do not.

The full 4-model comparison page still exists if you want to see the arms that were not chosen: docs/company/PRICING/paywall-4model-v2-2026-07-13/site/index.html (live: tma-revenue-plan-2026.netlify.app/pricing/…/site/). It opens on the m7 SHIP arm. Reference only — build from the contract.

🔴 Both are generated from the same source, so they can never disagree: the contract is emitted from the comparison page by build_nic_spec.py. If one changes, the other is regenerated and redeployed in the same pass. Neither is hand-edited.

Either way the design is complete. What it cannot do by itself:

#What is stubbedWhat you must wire
6.1Every CTA scrolls instead of paying. exitGo(), stBtn and cta2Btn all call scrollIntoView. The line is marked // PRODUCTION: in the source.Hand off to real checkout for the SELECTED tier. The selected tier is on .pkhero.sel / .pkramp.sel via data-m7 (12mo / 12wk / 4wk). 🔴 The sticky CTA must carry the selected tier through — today's live build enrols every converting trial at $24.97 regardless of what was tapped.
6.2tma_ckt_out is never set. The exit modal's only mobile-viable trigger reads this sessionStorage flag on return from checkout.Set sessionStorage.setItem('tma_ckt_out','1') at the moment the CTA hands off to Stripe, and set window.__tmaPurchased = true on success. Without both, the exit modal cannot fire on mobile at allmouseout needs a cursor.
6.3Exit-modal analytics push to window.dataLayer via a local track() shim.Point track() at the real analytics layer. Events already emitted: paywall_exit_modal_shown (with reason = back / checkout_return / pointer), paywall_exit_modal_cta, paywall_exit_modal_dismiss. Carry quiz_variant, session_id, path, device and the UTM set on each.
6.4The back-button trap was infinite in the previous build — popstate re-pushed unconditionally, so back never left the page. Fixed in the mockup: it intercepts once then releases.Carry the fixed version across. Do not restore the re-push. Named dark pattern; UK DMCC direct enforcement live since Apr 2025 with fines to 10% of global turnover.
6.5🔄 CHANGED 29 Jul (Aga) — the countdown is BACK on m7, and it must be REAL. The strip reads "⚡ EXCLUSIVE QUIZ PRICE · closes in 47:59:59 · then $157/yr". The mockup already runs a genuine per-visitor 48h deadline (localStorage, 7-day cooldown, never silent-resets) and expires to a real state: body.exp swaps the strip and shows .expmsg.m7only.Your half: the deadline must be enforced server-side. At T+48h from quiz completion the $97 coupon must actually die and the page must serve $157. The client timer and the server expiry must share one deadline (persist it with the quiz session). A timer whose expiry changes nothing is a misleading-urgency claim (CMA, Emma Sleep; DMCC fines to 10% of global turnover) — if the server half can't ship in the same deploy, ship WITHOUT the countdown and tell Aga.
6.6Hero card had no unselected state — tapping a ramp left two teal cards with the annual still reading "Selected". Fixed via .pkhero.sel.Carry across. On a payment surface a shopper who cannot tell what they are about to be charged does not tap.
6.7addToCartValue: 0 on the live build.Send the selected tier's real price. A zero-value add_to_cart poisons every downstream optimisation signal.
6.8NEW 29 Jul — the exit modal is now TWO steps. Step 1 is the guarantee reframe (unchanged). "Not now" opens step 2: the free-app route"Try it on us — 3 workouts, free." with real App Store / Google Play links.Carry both steps. exitStep2() / exitBack() / exitStore(store) and exitReset() are in the source. The modal must always open on step 1 (exitReset() runs on every open). New events: paywall_exit_free_shown, paywall_exit_free_back, paywall_exit_free_store_click{store}.
6.9Order summary above the card fields (#m7ord, driven by the M7_ORD map). Plan name · charged today · renewal terms.Keep it, and keep it in lockstep with the selected tier and the sticky bar. Before this existed a buyer could tap 4 Weeks, scroll three screens of proof, and arrive at the card fields with nothing on screen stating what they were about to be charged. Every real checkout restates the line item above the card.
6.10Tier selection now fires analyticsselM7() emits paywall_plan_selected{plan, from, seq, ms_since_view}.Wire it through. Previously every tier tap was invisible, which is why 604 buy-clicks → 154 paid has never been explainable. 12mo→4wk→12mo→buy is a price-anxious buyer you converted; 12mo→4wk→exit is a clean price objection.
6.11🔴 The status-bar demo hook. Tapping the phone status bar toggles the exit modal.Now gated behind ?demo=1 so it cannot reach production. Preview at …/site/?demo=1. Do not ship it ungated — you build this page verbatim, and an ungated version is a live defect.

6.12 — 🔴 The free-app exit: why it is on the EXIT and must never move to the plan row

The old bundle's button reads "I don't want to pay anything — show me another option" and leads to the free app ("No credit card required. Ever."). That is a zero-price option, not a cheaper rung — and its second step was never built (alert("Prototype: would send free-workout email.")).

Two CRO seats ruled independently: it does not go beside a paid tier. Shampanier, Mazar & Ariely (2007), Marketing Science 26(6): holding the price difference constant, moving the cheap option to $0 flipped shares from ~40/40 to ~90/10. Next to a pre-selected $97 that is a share-flip risk, not a marginal one — and this page exists to move mix back to annual.

The rule for the build: the free route appears only after the paid offer has been declined, on step 2 of the exit modal. No store link, and no free-tier mention, anywhere in the plan row. If a future change puts one there, this ruling has to be re-argued first.

6.13 — 🔴 READ THIS BEFORE YOU TRUST ANY EXIT-MODAL NUMBER

We have already shipped this exact kind of control once, wired 8 events to it, and can read none of them.

So the new events above will not be readable either until the pipeline is fixed. The missing piece is GTM mapping + server-side CAPI, not the button. Compounded by the attribution blackout — zero of 278 subscriptions in 90 days carry any attribution — even a click count could not be tied to revenue.

🔴 Do not report "the downsell isn't being used" from an empty dashboard. Until track() is pointed at a real, GTM-mapped layer, an empty number means the pipe is broken, not that the control is unused. Map the custom events in GTM as part of this deploy, or the whole of Step 4 stays unreadable.

6.15 — The PASSWORD FIELD on the checkout (new, 29 Jul — Aga directive)

The buyer creates their password ON the checkout, exactly like the current quiz register flow. The paybox now carries, above the wallets: Email (prefilled from the quiz, locked) + "Create a password — this logs you into your app".

6.16 — THE PROCESS PATH, end to end (who gets what, buyer vs non-buyer)

BUYERNON-BUYER (abandons the paywall)
AccountAlready exists (created at P52). Checkout sets their chosen password (6.15).Already exists (created at P52), password = the generated one.
CredentialsThey chose them at checkout — nothing to send beyond the receipt/welcome.Generated credentials already emailed at capture (SendGrid transactional — existing lane). The exit modal also shows them on-screen (masked, tap-to-show).
Next touch🔴 NEW — the buyer welcome email (06_EMAIL_BUYER_WELCOME.html), fires on successful payment. Subject: “Your plan knows your push from your pull”. Tags: %FIRSTNAME% %PLANNAME% %AMOUNTCHARGED% %RENEWALDATE% %RENEWALAMOUNT%.

🔴 IT CARRIES NO CREDENTIALS. Buyers set their own password at checkout (§6.15), so it says “log in with the email you used and the password you chose” and explicitly states no password email is coming. Do not add one — sending a buyer hunting for an email that doesn’t exist is the failure mode this wording kills.

🔴 THE FORK IS MUTUALLY EXCLUSIVE. Buyer → welcome email. Non-buyer → free-workouts email. Never both to the same person.

🔴 NEW — the free-workouts email (04_EMAIL_FREE_WORKOUTS.html). Subject: "Your login is in this email" · one email, no split. Send transactionally to paywall abandoners ~T+1h from last paywall view. Copy is Aga-owned; the trigger and send are yours.

🔴 SCOPE — QUIZ USERS ONLY (Aga, 29 Jul). This email fires only for people who came through the QUIZ and abandoned its paywall. Never app signups, never other lead lanes. A welcome email already goes out for this audience — 750 TMA | Welcome Sequence After Quiz fires at stage-1 email input — so check timing so this lands as its own touch, not a same-hour pile-up. Suppress anyone who PAID. Env note: we're on SiteGround — send from whichever transactional lane already delivers the password emails.

🔴 SUBJECT GATE. The subject promises the login inside works. Do not send until B6 is verified — the quiz-generated password actually logs into the app. If it doesn't, the subject and the credentials card change first.

Measured bypurchase + Stripe metadata (Step 4).paywall_exit_free_store_click · store-link ?ct=paywall_exit / email ?ct=paywall_abandon_email · app activation (list 204 → 988 TMA | Welcome Sequence AFTER APPSIGNUP takes over).

🔴 One rule across both paths: the free option never appears next to a paid tier. It exists only on exit step 2 and in the abandonment email.

6.14 — Bug fixed 29 Jul: "Back to my plan" looped between the two exit panels

Found by Aga on the live page. exitBack() originally called exitReset() only — which returns the user to step 1 of the same modal. So Not now → Back → Not now cycled between the two panels forever and never returned anyone to the checkout. Fixed: exitBack() now closes the modal and scrolls to the card fields of the visible model. Carry the fix; do not reintroduce the reset-only version. Same class of defect as the infinite back-button trap in 6.4 — a control that looks like an exit but isn't one.

6.17 — Conversion-science layer added 29 Jul (build these as specced)

The mockup now carries a researched behavioural layer. It is presentation only — no pricing or mechanics changed — but several parts need real data or real form attributes from you.

WhatWhat you must wire
“What happens next” billing timeline
Blinkist pattern: +23% trial starts, +4% paid, −55% complaints
3 steps with REAL computed dates, step 1 marked complete. Dates must come from the actual charge/renewal schedule, not hardcoded. Per tier — annual: today $97 → reminder email → renews $157. Trial: today $0 → email 3 days before → $24.97/mo starts. The reminder-email step is a promise — make sure that email actually sends.
Quiz-answer echo blockRender 2–3 of the buyer’s own quiz answers above the price block from quizState (the mockup fakes it from the path chips). Their words, never ours.
Absolute expiry datetime beside the countdownPrint the real expiry moment (#obExpAt), server-authoritative — same deadline that kills the $97. Countdown timers are the CMA’s flagship DMCC target (8 investigations opened, 100+ firms written to); a checkable absolute date is both compliant and more persuasive.
Equal prominence on billed amountsCharged-today and renewal figures must sit in the same size/contrast class as the per-day/per-week figure — not muted grey beneath it. Apple 3.1.2 rejects paywalls that promote a normalised price more conspicuously than the billed amount (mass rejections Jan 2026; Cal AI reportedly pulled Apr 2026). Nothing decision-critical below 11px.
Encapsulated card blockKeep the distinct container + border around the card fields, and do not reuse that styling anywhere else — Baymard’s finding is that the effect disappears when the treatment is shared. Reassurance stays above the fields (19% abandon over card-detail distrust, and the anxiety spikes at the card interface).
🔴 Form mechanics (Baymard, hard numbers)autocomplete="cc-number|cc-exp|cc-csc" · inputmode="numeric" · card-type auto-detect + auto-spacing · labels ABOVE fields, never placeholder-only · one field per datum · inline help at the point of doubt. 60% of top-50 mobile sites fail 2 of 5 keyboard optimisations. The sticky CTA must yield when the keyboard takes half the viewport.

A/B candidates for LATER — do NOT run them now. With ~13 paywall views/day nothing here is powerable (a 30% annual-share test needs ~3.3 years), and zero quiz attribution reaches Stripe. Park these until the pipeline is fixed: (1) 2 visible plans + “view all plans” link — all four published RevenueCat paywall redesigns won by shortening (+31% to +72% install-to-trial); (2) badge on the middle tier vs on the default — MadMuscles deliberately decouples them.

Acceptance criteria — how we know it worked

  1. Load the quiz twice in a fresh browser → same arm both times; clear storage → arm may change.
  2. Across ~40 fresh sessions the split is roughly 50/50 (not 100/0).
  3. ?arm=paywall_v2_6 forces Arm B and does not write to localStorage or cookie.
  4. On both arms the paywall shows an annual rung and it is pre-selected on load.
  5. Both the mid-page and the sticky CTA carry the selected tier through to checkout — confirm by buying the 12-Month rung and checking the Stripe amount.
  6. A test purchase produces a Stripe subscription whose metadata contains quiz_variant and all four UTM keys. Not empty.
  7. GA4 real-time shows quiz_variant on quiz_started and as a user property.
  8. add_to_cart carries the tier's real price — never 0.
  9. Buy the annual rung on a test card: Stripe charges $97, and the subscription's next invoice is $157. If it charges $157, the duration: once mechanism (defect 7) was never minted.
  10. No tier anywhere on the page says "4 Weeks" or "12 Weeks" — the labels read 1 Month and 3 Months, matching the real Stripe intervals (defect 6).
  11. Press the phone back button on the paywall: the exit modal appears ONCE, and pressing back again leaves the page. If back is swallowed a second time, the re-push was reintroduced — that is a hard fail.
  12. On a phone: tap the CTA → land on Stripe → press back without paying → the exit modal appears. If it never appears, tma_ckt_out is not being set (6.2).
  13. Tapping a ramp card visibly deselects the annual — exactly one card reads as selected at any time, and the sticky bar matches it.

Guardrail — how the test gets stopped

This is a guardrail, not a significance rule

At 13 paywall views/day, detecting a +40% revenue-per-visitor difference honestly takes ~143 days. We are not waiting for that, and we are not pretending a 3-week readout is significant.

Both arms are modelled above today, so the test is not "does this work" — it is "which of these two is better". The stop condition is catastrophe only:

If either arm's rolling 14-day revenue per paywall view falls below $6.04 (today's measured baseline), kill that arm and send 100% to the other. Review weekly; expect no verdict on which arm wins before November.

Order of work

  1. THE DIAGNOSTIC FIRST — the config 404 + the early-July lead fall as one investigation, timeboxed 2–4 hrs. Nothing below starts until it reports.
  2. Step 1 (v2_5) — this is the cash. If everything else slips, this must not.
  3. Step 4b (Stripe metadata) — smallest change with the largest permanent payoff; it ends the attribution blackout.
  4. Step 3 (bucketing) + Step 2 (v2_6) — together, or the test cannot run.
  5. Defect 7 (mint the $97→$157 duration: once mechanism) — do this with Step 1. Without it the annual rung cannot charge $97 and the whole ship arm is blocked.
  6. Step 6.1 + 6.2 — the checkout handoff and tma_ckt_out. 6.1 is the same work as the sticky-CTA fix, so it costs nothing extra once you are in there.
  7. Defect 6 (relabel 4 Weeks → 1 Month, 12 Weeks → 3 Months) — labels only, no arithmetic. Cheap, and it removes a factual misstatement about what a customer gets charged.
  8. Step 4a, 4c, Step 5, Step 6.3–6.7 — same deploy if they fit, next deploy if not.
If you can only do one thing this week
Add the annual rung and make the sticky CTA carry the selected tier. Every converting trial currently enrols at $24.97 instead of $157. On the July trial run-rate that alone is a modelled +$1,300–2,600/month, on identical traffic and identical conversion.

Why this outranks everything else in the brief — verified live from Stripe, 29 Jul 2026: annual share did not simply fall, monthly doubled. Feb–Apr: annual 36.0% / quarterly 30.2% / monthly 33.7% (n=258). 10–27 Jul: annual 16.1% / quarterly 19.5% / monthly 64.4% (n=87). The live variant paywall_v2_2 has no annual tier on it at all — this is a missing product, not a presentation problem. And on the Q1-2026 cohort at 4–7 months (270 subs, 1,527 invoices): annual realises $79.04 per subscriber with 69.7% still active; monthly realises $27.99 with 15.6% still active. Every point of mix moved from monthly to annual is worth ~$51 per head already, and the gap is still widening.

Prepared 27 July 2026. Evidence: live bundle read (index-DPkL72Wi.js), tools/finance/quiz_buys_90d.py, tools/quiz-analytics/pull_quiz_deep.py, tools/finance/pricing_model_rerun.py. Baseline 90 days: 1,174 paywall views → 604 buy-clicks → 154 paid → $7,091.97 · ASP $46.05 · RPV $6.04. Modelled figures are tagged MODELLED in the source documents and are not measured outcomes.
Mockups of both arms: docs/company/PRICING/paywall-4model-v2-2026-07-13/site/index.html (tabs Arm A and Arm B, plus What is LIVE today).
Build target (variants stripped): tma-paywall-spec.netlify.app — generated from that same file by build_nic_spec.py; the two are updated together, never separately.