Nic — to-do list

12 live · 2 parked · 60 completed

This page is generated from the tracked queue (marketing/seo/SEO_HUMAN_QUEUE.json) — it is never typed by hand, so it cannot drift from the real list. Project documents explain the work; this says what is next. Each item names the doc it came from.

Live — 12 item(s)

NIC-106open
🔴 REPOINT 'QLG | ADV+ OFF | MC | Control' TO THE QUIZ ROOT — 5 min in Ads Manager. All 4 of its live ads still land on https://quiz.themovementathlete.com/2026/, the old page that does NOT show the 97 dollar offer. Change the destination on all four to https://quiz.themovementathlete.com/ (the ROOT). Nothing else: leave url_tags, budgets, geo and the objective exactly as they are, and do NOT pause the campaign. EXACT LIST — adset 'General / T2 / FM 30-54 US/Canada' (GBP 9.62/day, geo US+CA): ad 120253084939360728 and ad 120253084939340728. Adset 'General / T2 / FM 30-54 AUS, FR, UK SING, NZ' (GBP 9.62/day, geo NZ GB IL AU FR): ad 120245527336080728 and ad 120245527336070728. WHY THIS AND NOT A PAUSE — measured live 11 Aug on the Meta API, 30 days, unified attribution: MC Control produced 129 leads at GBP 2.43 each, the biggest lead source on Meta (56% of the two QLG lines' 230 leads; 57 in the last 7 days). Pausing would cut Meta lead volume by more than half in the window we are banking leads for Black Friday. Repointing keeps all 129 leads/month and sends them to the page that shows the 97 AND enrols them into 993 SALES SEQUENCE, whose 10 Sep gate the whole paid plan waits on. Zero cash change either way — the account's daily budget is GBP 45.48 before and after. This is also exactly what Aga's 10 Aug ruling asked for ('RE-AIM into the ONE geo-locked lead line, quiz root'); NIC-97 was built as a clone, which the spec allowed, so MC Control itself was never touched. TWO OPTIONAL EXTRAS while you are in there: (a) drop IL from the second adset's geo — Israel is the only country in this campaign not on the buyer list (US UK CA AU DE SE NL FR); (b) pause the third adset 'General / T2 / FM 22-65 North America' — it holds a GBP 7.00/day budget but all SIX of its ads are paused, so it cannot deliver and spent GBP 0 in the last 7 days. Hygiene only, frees nothing. VALIDATION (2 min): after saving, open each of the 4 ads' preview and confirm the click goes to the quiz root and lands on the page showing the 97 offer, then confirm the campaign is still ACTIVE and its daily budget is unchanged at GBP 9.62 + 9.62.
impact 5/5 · 5 min
UNBLOCKED + APPROVED by Aga, 11 Aug 2026: 'yes nic needs to know about this — one Meta campaign is still sending people to the old quiz page instead of the new one that sells the $97.' That resolves the 7-Aug-vs-10-Aug contradiction in favour of the 10 Aug RE-AIM ruling. AGA-160 (the ruling row) is therefore answered and closes. Nic reads this from the quiz-funnel-versions page, not the attributio
from: quiz-funnel-versions §19 row 1a + the 11 Aug build-status block · consequence of NIC-97 shipping as a clone
NIC-111open
🍎 ATTACH A BUILD TO iOS 1.70.72 AND SUBMIT — this ONE act ships everything else, and nothing else is left to write. The version has sat in 'Prepare for Submission' since 30 Jul with no build, so the submit button is greyed out. Waiting on it: (a) the corrected keyword field — the LIVE one is 'ab,butt,beachbody,tracker,gym,exercise,diet,planner,trainer,buttock,calisthenics,ladder,routine,diet' (diet twice, 'beachbody' is a competitor brand, and only one term describes what we sell), replaced by 'bodyweight,workout,plan,progression,home,beginner,strength,mobility,pull,push,handstand,planche,core'; (b) the support-URL fix — the live one 404s and a dead support URL is a known App Review REJECTION reason; (c) the NEW title+subtitle written 12 Aug on Aga's GO: 'Movement Athlete: Calisthenics' + 'Bodyweight Skills & Mobility' (the live title carries ZERO keywords). It also unblocks any non-branded Apple Search Ads test, because Search-Match targets off listing metadata — so the ASA experiment cannot start until this ships. ASK: attach the current build and tell us the submit date. If it is stuck, say what on. Aga has already approved every string; there is no copy left to write.
impact 5/5 · 15 min
from: app-store-fleet 12 Aug · marketing/aso/APP_STORE_DEEP_DIVE_2026-08-12.md · cockpit 🍎 iOS tab
NIC-102open
🍎 SHIP THE STAGED iOS KEYWORD FIELD — the LIVE one wastes ~86 of its 100 characters, and the good one is ALREADY WRITTEN and sitting unsubmitted. Read live from App Store Connect 11 Aug 2026. LIVE (v1.70.71, READY_FOR_SALE): 'ab,butt,beachbody,tracker,gym,exercise,diet,planner,trainer,buttock,calisthenics,ladder,routine,diet' — 'diet' appears TWICE (Apple ignores the duplicate), 'calisthenics' is ALREADY indexed free from the subtitle 'Calisthenics & Gymnastics' so it is 13 wasted characters, 'butt'+'buttock' are both off-brand, 'beachbody' is a competitor brand, and only ONE term describes what we sell. ALREADY STAGED on v1.70.72 (PREPARE_FOR_SUBMISSION): 'bodyweight,workout,plan,progression,home,beginner,strength,mobility,pull,push,handstand,planche,core' — no duplicates, nothing already-indexed, dramatically better. ✅ SO THE ASK IS SIMPLY: confirm v1.70.72 is actually going out, and tell us roughly when. If it is stuck, say what on. This matters because 87.3% of iOS installs come from App Store SEARCH and 74.3% install WITHOUT ever opening the product page — the keyword field and subtitle ARE the conversion surface, and this costs GBP 0. Optional further gain, our proposal, 92/100 chars, skill-intent over generics: 'bodyweight,pullup,pushup,handstand,muscleup,planche,lever,dips,mobility,strength,progression'.
impact 4/5 · 10 min
from: app-funnel deep dive 11 Aug 2026 (cockpit /app-funnel/)
NIC-110open
💷 NIC — CUT 'Quiz Funnel | Competitors Branded | US' FROM £10/DAY TO £5/DAY, AND DO IT AFTER NIC-108 LANDS. It bids on rivals' brand names (mostly 'betterme'). It is the biggest line in the Google account — £314.77 over 30 days, 32%% of all Google spend, running at its £10/day cap — and it returns 24 conversions at £1.58/click. THE PROBLEM IS WHAT THAT COSTS PER OUTPUT: £13.12 per conversion, against PMax Search at £1.19, Branded at £1.74, and Meta's cheapest lead line at £2.43. It is 3rd of 5 Google lines by cost-per-conversion — better than Non-Branded (£40.07) and Competitive Non-Branded (no conversions at all), but 7.5x worse than the two that work. And 86%% of this account's conversions are not sales — 30d the account booked 105 leads + 4 add_to_cart + 4 Android installs against just 18 purchases (14 Purchase | Nic - Built on July 2026 + 4 Lifetime $297 Promo) — so those 24 are leads, and the 1-10 Aug Stripe audit found all 3 verified Google sales came from Branded, none from this line. 🔴 WHY CUT AND NOT PAUSE: competitor-brand search is genuinely in-market intent ('betterme alternative' is a person shopping), which a Meta cold lead-form fill is not, so the two lead types are not interchangeable at any price — and killing a line before knowing what its output feeds is the mistake the MC Control round already cost us. A halving keeps the intent flowing, halves the bleed and stays readable. 🔴 WHY AFTER NIC-108, NOT BEFORE: the freed £5/day has exactly one good home — PMax, the cheapest converter in the account and the only line actually at its budget cap (£150.32/30d against £5.00/day). Branded cannot absorb it: it spent £40.00 in 30 days against a £6/day cap, so it is volume-bound, not budget-bound, and money moved there will not spend. But PMax leaks 40%% of its budget into placements that return zero until NIC-108 fixes it — so moving money in first would just enlarge the leak. THE ORDER: (1) NIC-108 placements first; (2) then set this campaign's daily budget to £5.00 and raise PMax's to £10.00; (3) change nothing else — leave the keywords, the bidding, the geo and the objective alone. ALSO HOLD UNTIL AFTER 13 AUG: AGA-157's read on Competitive Non-Branded lands 13 Aug and two simultaneous Google changes make the account-level read unusable. VALIDATION (5 min): after saving, confirm the two daily budgets read £5.00 and £10.00 and the account's enabled total is unchanged at £36.00/day. THE READ, 10 SEP (the existing gate): if this line still shows zero Stripe-verified sales AND its cost-per-conversion is still above £10, pause it then — that is the kill condition, pre-registered now so the decision is not re-argued. If you think there is a reason to keep it at £10/day that these numbers cannot see, say so and it stands — you built it and you may hold information this analysis does not.
impact 4/5 · 10 min
from: AGA-104 item (a), re-measured live 12 Aug 2026 on Google Ads API v25 · reassigned to Nic by Aga, 12 Aug: 'this needs to go on the to-do list for Nick'
NIC-105open
🔴 REMOVE THE THIRD-PARTY GTM CONTAINER FROM THE CHECKOUT — GTM-PPGJXHH is hardcoded in app/apps/templates/base.html and is NOT ours. Aga checked all 7 GTM accounts / 12 containers on 11 Aug: no match. It loads on every legacy /register/ checkout — the page that holds a live Stripe key — alongside our own GTM-TKGDZR9, and we cannot see what it fires. DELETE two blocks in that one file: the <script> at lines ~12-18 (head) and the <noscript> iframe at ~52-55 (body). Leave GTM-TKGDZR9 untouched. While in there, also delete the dead Universal Analytics tag UA-104067894-1 (line ~20) — UA was sunset by Google in Jul 2023 and sends nothing. Nothing else in the file changes. AFTER DEPLOY: load /register/yearly3/ and confirm only GTM-TKGDZR9 loads.
impact 4/5 · 15 min
APPROVED FOR NIC by Aga, 11 Aug 2026. Routed to the quiz-funnel-versions page per her surface ruling the same day (not retargeting → not the playbook).
from: Browser-measured 11 Aug 2026 + Aga's GTM account list (all 12 containers, screenshot) + Django source trace
NIC-108open
🔴 NIC — STOP PMax BUYING PLACEMENTS THAT HAVE NEVER CONVERTED. 'Quiz Funnel | PMax | US | Sales OBJ' is the only LIVE campaign breaching the settled 'Category Search ONLY — no Demand Gen, no YouTube' ruling, and the breach is measurable: over the last 30 days its non-Search arms took £60.85 across 280 clicks and returned ZERO conversions — CONTENT/Display £28.37 (39,067 impressions, 210 clicks, 0 conv), SEARCH_PARTNERS £28.17 (48 clicks, 0 conv), YOUTUBE £3.28 (17 clicks, 0 conv), DISCOVER £1.03 (5 clicks, 0 conv). That is 40%% of the campaign's £150.32 spend buying nothing. 🔴 DO NOT PAUSE THE CAMPAIGN — its SEARCH arm took £93.07 and returned 78 conversions at £0.53/click, the lowest cost-per-conversion in the account. THE ASK: keep the Search delivery, stop the rest. PMax gives no clean channel toggle, so pick whichever you judge best and tell us which: (1) Account-level Excluded Placements + Content Suitability to cut Display/YouTube inventory; (2) campaign-level negative keywords + turn OFF Final URL expansion; or (3) the clean structural answer — retire PMax and move its £5/day into a standard Search campaign, which is what the ruling actually asks for. TWO CAVEATS TO CARRY, not decorations: (a) 86%% of this account's 'conversions' are email addresses, not sales — 30d the account booked 59 Leads-GTM + 46 generate_lead + 4 add_to_cart + 4 Android installs against only 14 Purchase + 4 Lifetime-promo — so PMax's 78 are leads; (b) PMax self-attributes generously (78 conversions from 174 clicks is a 45%% rate), so treat its Search figure as directional, not proven. VALIDATION (5 min): after the change, re-open the campaign's 'Where ads showed'/placement report 3 days later and confirm Display/YouTube impressions have gone to ~0 while Search impressions and conversions hold.
impact 4/5 · 15 min
from: AGA-104 re-measurement 12 Aug 2026 · Google Ads API v25 campaign x ad_network_type breakdown · quiz-funnel-versions §PA
NIC-22assigned-dev
✅ REASSIGNED 31 Jul — the WP Engine security deletions moved to the EXTERNAL DEV (now Step 0 of the migration brief), not Nic. Nic's only remaining involvement in the hosting migration is dropping DNS TTL to 300s ~48h before cutover. Original detail: `docs/company/HOSTING_MIGRATION_2026-08/NIC_BRIEF_DELETE_EXPOSED_FILES.md`.
impact 4/5 · 25 min
Status corrected 2 Aug (was 'open' with a REASSIGNED title — id-collision leftover from TRUST_AUDIT F1): the WP Engine security deletions belong to the external dev. The quiz-bundle app-test defect this id once pointed at lives at NIC-30.
from: docs/company/HOSTING_MIGRATION_2026-08/NIC_BRIEF_DELETE_EXPOSED_FILES.md
NIC-99open
🟡 BUILD THE $97 WARM RETARGETING TEST — sequenced AFTER the 45-min sitting (keyword surgery + NIC-97 + MOF pause) and the 17-Aug CPL report. Spec: ONE Meta campaign, objective OUTCOME_SALES, ~$5/day (~$150/mo from the freed budget), WARM audiences only (site visitors 30d · quiz-starters · quiz-checkout abandoners; EXCLUDE payers via the ever-paid set), creative = the $97 offer straight (no promo framing), destination = quiz root, keep url_tags. Judged ONLY on stamped Stripe sales (checkout_version=quiz_checkout + fbclid), 4-week read. Brief = tma-remarketing-playbook.netlify.app — use its corrected economics (£67.48 full-life per purchase during promos; the £2.41 is withdrawn), so expectations are honest: evergreen-$97 retargeting is UNTESTED, this is a test not a scale-up. Strategic kicker: the Nov BF retargeting push is Nic's build anyway — this warms the audiences, pixel events and creative learnings for it.
impact 4/5 · 60 min
from: quiz-funnel-versions ad-spend box (Aga's 11 Aug amendment: 'I don't think retargeting should be off') + tma-remarketing-playbook.netlify.app
NIC-100assigned-dev
🤖 ANDROID: 32% OF USERS NEVER SUCCESSFULLY SIGN IN — REAL, MEASURED, AND STILL UNEXPLAINED AFTER A FULL ON-DEVICE QA PASS (12 Aug). 🔴 DO NOT HUNT A BROKEN SIGN-IN — there isn't one. A tester ran the whole flow on a real Android phone: signing up, signing in, the quiz→app journey, and every failure path. The door WORKS (quiz email → password → in, 3 taps / 25 seconds). Password reset WORKS (verified in our mail provider's logs). The crash theory was already refuted (Play vitals 0.00% crash, 0.10% ANR) and so was the cheap-geo theory (buyer-countries-only, Android 45.6% vs iOS 111%). What remains is the measurement: buyer-geo login iOS 239/242 = 98.8% vs Android 85/125 = 68.0%, and social_login_attempted fires 103x on iOS and ZERO on Android. 🔎 SO THE OPEN QUESTION IS NARROWER AND SHARPER: not 'what is broken' but 'why do a third of Android users abandon a flow that demonstrably works'. The leading candidates are all friction, not failure — no autofill (the emailed 20-character password must be hand-copied every single time), no show-password toggle, no forgot-password link at the moment sign-in fails, and an email-verification wall the app imposes on every quiz-created lead. Those are specced as TMA-A1..A6 in docs/company/ANALYTICS/ANDROID_AUTH_TICKETS_2026-08-12.md. 🔴 The next step is user-level data, NOT another QA script.
impact 4/5 · 60 min
🔬 12 Aug 2026 — MANUALLY TESTED ON A REAL ANDROID PHONE (tester Leigh; script at /android-signin-test/; 32 screenshots). RESULT: NO BLOCKING DEFECT EXISTS. The quiz→app door WORKS — the emailed password signs you in in 3 taps / 25 seconds, which also confirms NIC-94 live for the first time. Two alarms raised during the pass were both KILLED before reaching any dev: (1) 'forgot password always fail
from: app-funnel deep dive 11 Aug 2026 (cockpit /app-funnel/)
NIC-109open
🍎 NIC — WHICH REPORT DID THE $2,277 COME FROM? Your Slack note reads '$404 spend across June and July → $2,277 revenue → 5.6 ROAS'. THE SPEND RECONCILES: Apple's own billing for 1 Jun–31 Jul is $377.32 (and $408.24 for 12 Jun–12 Aug), so $404 is real. THE REVENUE DOES NOT: AppsFlyer's aggregate partners report for the same window credits Apple Search Ads with $1,098.82 from 6 buyers — June $99.98 (1 buyer), July $998.84 (5 buyers). That is roughly HALF your figure, so one of the two systems is being read on a different basis and we should not publish either number until we know which. THE MOST LIKELY CAUSE, so you know what to look for: Apple's own dashboard attributes on Apple's window, which counts VIEW-through installs — over 12 Jun–12 Aug Apple recorded 165 tap-installs but 305 view-installs, and a view-inclusive basis roughly doubling credited revenue is exactly the shape of this gap. THE ASK: tell us which report and which exact date range the $2,277 came from (Apple Search Ads dashboard? AppsFlyer dashboard? RevenueCat? App Store Connect?) and whether its attribution setting includes view-through. If it is Apple's own view-through figure, both numbers are 'right' and we simply label which basis we quote from now on. 🔴 SEPARATELY, AND THIS IS THE PART THAT CHANGES A DECISION — Aga asked whether the ROAS is branded-only and the measurement says her instinct is right and stronger than she put it: 100%% of the attributed revenue in your window came from OUR OWN brand campaigns (Branded EU $763.89, Branded US-SR $334.93), while the 61%% of the spend that went to competitor and non-branded terms ($230.45 across Competitive-Branded, Non-Branded, Competitive Non-Branded, Non-Direct Competitive Demand Gen and Competitors_MC) returned 15 installs, ZERO buyers and $0.00. So the honest split is: branded 7.5x, non-brand 0.0x, blended 2.9x — not 5.6x across the board. Nothing to change in the account, those five are already paused; this is so the 5.6x is not quoted as if it were repeatable on new keywords.
impact 3/5 · 5 min
from: Aga's Slack question 12 Aug 2026: 'but this is ONLY on branded terms right?' · reconciled against Apple Search Ads API v5 + AppsFlyer aggregate partners report
NIC-18assigned-dev
CONFIRM THE PHP STATE — the WP Engine activity log shows 'PHP update from version 8.4.0 to …' at 04:50 on 30 Jul under developer.mrski, and the portal separately reports '5 PHP outdated · 5 environments need updates'. We CANNOT read PHP version from outside (WP Engine doesn't expose it in headers or REST), so the current state is genuinely unknown to us. Two questions: (1) which environment did that update apply to, and to what version — if anything moved DOWN from 8.4.0 that matters; (2) is BWA still on 7.4.33? If NIC-14 is already done, say so and we close it.
impact 3/5 · 10 min
from: WP Engine dashboard 30 Jul
NIC-14assigned-dev
BWA is on PHP 7.4.33 — end of life, no security updates, and WordPress will stop supporting it (min recommended 8.3). WP Engine portal change + regression test. Flagged by the WP dashboard itself.
impact 3/5 · 30 min
from: 28 Jul BWA dashboard

Parked — 2 item(s)

NIC-101parked
🔴 MAKE THE IN-APP CANCEL DOWNSELL PLAN-AWARE — annual leavers are shown a WORSE deal than our own standing offer. Measured 11 Aug: 84 of the 115 in the cancel queue are ANNUAL, and every canceller sees $9.99/mo. That is $119.88/yr, and MEASURED over 516 people who ever took it (493 cancelled): median life 2.0 months, $34/taker — against annual-at-50% = $78.50. It defers churn by two months at 40% of the price. (1) BRANCH THE CANCEL DOWNSELL ON PLAN: annual → the 50% annual save; monthly/quarterly → keep $9.99/mo (price nine-ninetynine). 🔴🔴 MECHANISM — CORRECTED 11 Aug, READ THIS: the save at cancel-time MUST be a COUPON APPLIED TO THEIR EXISTING SUBSCRIPTION (a 50%-off duration=once coupon such as tma_v3_50off_once, so their next renewal bills $78.50 and auto-renew stays on) — NEVER a checkout link. At the cancel moment they STILL HOLD AN ACTIVE SUBSCRIPTION (88 of 115 have access, median 106 days), so a fresh checkout opens a SECOND subscription and DOUBLE-BILLS them. This is the exact defect the 10 Jul R1 rebuild already caught with /50promo/. The checkouts-v3/50off/12month/ link is correct ONLY for the truly-lapsed (access ended), i.e. the 976 Cancelled Win-Back arc — not for this screen. (2) ANSWER WHAT WE CANNOT SEE: does subscription_cancelled fire at renew-off (access still paid) or at access-end? (3) 🔴 IF PAUSE IS EVER BUILT it needs ENTITLEMENT GATING: Stripe's pause_collection leaves status=active, so an app gating on status alone grants FREE ACCESS — and portal subscription_pause reads enabled=false and is deprecated, so the real path is native POST /subscriptions/{id}/pause. Note the 10 Jul finding that our web cancel button is CUSTOM, not the Stripe portal, so portal settings do not govern this screen.
impact 5/5 · 1.5h
✅ PARKED on Aga's ruling, 11 Aug 2026: 'this is backlog.' The finding itself stands and is not withdrawn — the in-app cancel screen offers annual members a worse deal than our own standing $78.50 offer (CANCELLATION_AND_WINBACK_STRATEGY_2026-08-11 §10) — but it is not being worked now and must not app
from: docs/company/ANALYTICS/CANCELLATION_AND_WINBACK_STRATEGY_2026-08-11.md §2 + §5 — all figures measured live 11 Aug
NIC-107parked
BACKLOG (Aga: park it, not to be raised again) — checkouts-v3 header renders '4.9 average rating'. House rule is 4.7/4.8; 4.9 is retired. Aga has seen it and ruled it PARKED on 11 Aug 2026. Pick up only if the header is being edited for another reason.
impact 1/5 · 5 min
✅ PARKED on Aga's ruling: 'park it, not to be raised again.' Kept out of Nic's surfaces.
from: Aga 11 Aug 2026 — backlog only, do not raise

Completed & historical — 60 item(s)

Kept in full, on this page, by Aga's instruction.

NIC-103done
✅ WITHDRAWN 11 Aug 2026 — Aga had already sent the IAP 'Confirmation' dialog deletion to the devs herself before this row was minted. Not a live task. Kept only as the evidence record for why the dialog goes.
impact 5/5 · 45 min
✅ WITHDRAWN, not completed by the fleet. Aga sent the deletion request to the devs directly on 11 Aug 2026, in parallel with this row being filed. NOT VERIFIED against a shipped build — this row makes no claim that the dialog is gone from the app; only that the ask is with the devs and does not need r
from: docs/company/ANALYTICS/APP_REVENUE_TRUTH_2026-08-06.md + AGA-110 resolution + App Store Connect financeReports pulled live 11 Aug 2026
NIC-104cancelled
🔴 THE V3 CHECKOUT PAGES PUBLISH '4.9 average rating' — the retired, banned figure, on the pages that take the money. Measured live 11 Aug 2026 in a real browser (JS rendered): checkouts-v3/trial/1month/, /12month/ and /lifetime497/ all render '★ 4.9 average rating' in the header band. Published figure is 4.8 (4.7 also acceptable); 4.9 is RETIRED on every surface (Aga, 4 + 6 Aug). Same string family already fixed on /50promo/ (NIC-95) and the quiz bundle (NIC-46) — the v3 CHECKOUT templates were missed by both sweeps, i.e. the sweeps landed on the marketing pages and not on the pages that take the money. Rendered by the React app (MovementAthlete-React-ProdTemp), which is not on the fleet's machine, so this half needs Nic. ✅ THE OTHER HALF OF THIS ROW IS DONE: the 24 legacy /register/ links on /start-training/ were swapped to checkouts-v3/trial/ by the fleet on 11 Aug and live-verified (0 legacy links remain; clicking the monthly button lands on trial/1month).
impact 4/5 · 20 min
✅ BACKLOGGED ON AGA'S RULING, 11 Aug 2026: 'backlog this, I never want to hear about this again.' Third time it reached her. NOT FIXED — the defect is real and still live: checkouts-v3/trial/1month/, /12month/ and /lifetime497/ each render the banned '4.9 average rating' in the header band (browser-me
from: live browser measurement 11 Aug 2026 — headless Chromium over the checkouts-v3 and legacy /register/ carts
NIC-95done
🔴 /50promo/ — 4 defects on the page every evergreen sales email points at. (1) The page publishes '4.9 From 100k+ members' — 4.9 is RETIRED and banned estate-wide; it must read 4.8. (2) Its countdown says 'SALE ENDS IN 2 DAYS' while the email arc pointing at it runs an 11-day window — a reader on day 2 sees '9 DAYS LEFT' in the email and '2 days' on the page. (3) The raw HTML carries two lifetime prices, $498 and $297, plus '7 DAYS FREE TRIAL' cards — the arc is a charge-immediately offer with no trial language by rule, so whichever renders needs settling. (4) Its checkout buttons route to legacy /register/yearly3/promo/ etc with hardcoded _gl= tokens — these are absent from CHECKOUTS_V3_URL_MAP_AND_USE_CASES_2026-08-04.md and hardcoded _gl= is banned by the checkout rule. This is the money path for the whole arc.
impact 5/5 · 45 min
✅ CLOSED BY AGA'S RULING 10 Aug 2026: 'leave this page ALONE, no more fixes.' What was done before the stop: the banned 4.9-star rating on /50promo/ (page 252080) was corrected to 4.8 live — both spots (block attribute ratingSuffix and rendered ep_sr_after_text), backup tma-pages-252080-20260810T10190
from: 993 fix session 10 Aug 2026 — live fetch of themovementathlete.com/50promo/
NIC-96done
🔴 750 WELCOME — 9 OF 24 LIVE MESSAGES SEND A COMPLETELY DIFFERENT EMAIL IN THE PLAINTEXT PART. Found 10 Aug while fixing AGA-140. Three whole slots — campaign 4152 (msgs 13700/13701/13702), 4151 (13697/13698/13699) and 4147 (13685/13686/13687), all three subject variants each — carry HTML that is the current welcome copy while their text/plaintext part is still the OLD campaign they were duplicated from (e.g. 4147's plaintext is the 2024 'Backwards Logic Everyone Follows' email about progressive overload, nothing to do with the HTML). Any recipient whose client renders the text part, and every plaintext-based spam/deliverability check, sees the wrong email. Cause: the B/C variant fill wrote HTML into duplicated campaigns and never regenerated the plaintext. Fix is mechanical — regenerate text from html per message (the 993 uploader already does exactly this via ac_plaintext_regen.html_to_text) — but it changes what live members receive on an arc with 19,861 entered, so it needs a decision before it is applied.
impact 4/5 · 30 min
✅ DONE 10 Aug 2026 on Aga's ruling ('they should all receive THE NEWEST updated version') — executed by the fleet, no Nic action needed. tools/email-sequences/fix_750_plaintext.py regenerated the text part from each message's own html (same html_to_text as the 993 SALES SEQUENCE uploader) for all 24 l
from: AGA-140 fix session 10 Aug 2026 — live AC prose comparison across all 24 wired 750 messages
NIC-97done
🟢 AGA RULED (10 Aug): BUILD THE ONE GEO-LOCKED LEAD LINE — ~$150-200/mo, buyer countries only, pointed at the QUIZ ROOT. Spec: (1) ONE Meta lead campaign (re-aim 'QLG | ADV+ OFF | MC | Control' or clone 'QLG | ADV+ ON | US | Variant 2' — your implementation call), objective OUTCOME_LEADS. (2) GEO: US UK CA AU DE SE NL FR ONLY — hard-exclude IN/PK/PE/ZA/BD (the app-install lesson: 6,700 installs from never-buying geos = 0 buyers; MC Control's current mix ~20% US + CA 24% UK 22% AU 15% FR 9% already passes, minus the tail). (3) DESTINATION: https://quiz.themovementathlete.com/ (the ROOT — its leads then see the $97 AND enter 750 TMA | Welcome Sequence After Quiz → 993 SALES SEQUENCE; the /2026/ Control page gives up the paywall payout for no reason). Keep url_tags. (4) BUDGET ~$5-7/day from the paused MOF budget — inside the envelope. (5) While in there per the same ruling set: PAUSE 'MOF - Lead Form' (99 leads/10d at $0.73 that never reach the quiz, 0 purchases in 77d). Success metric: stamped quiz sales + 993 SALES SEQUENCE conversions on this line's leads, read 10 Sep — judged at the REAL quiz-lead CPL (~$6.57 GA4-lens), never the $1.35 platform figure.
impact 5/5 · 25 min
✅ VERIFIED LIVE 11 Aug 2026 ~21:40 UTC against the Meta Marketing API v21.0 (act_710789699289834 'TMA NEW', GBP) — not against Nic's report. (1) The line is BUILT: new campaign 'QLG | ADV+ ON | MC | Variant 2 | Quiz Root' (id 120253591188430728), effective_status ACTIVE, objective OUTCOME_LEADS, daily
from: quiz-funnel-versions §LB MC-Control decision box · AGA'S GO 10 Aug evening: 'i need this'
NIC-98cancelled
🔴 FIX /start-training/ — RE-MEASURED LIVE 11 Aug 13:10 MDT, and this row's numbers have moved: the DEAD $297 lifetime price is GONE (zero occurrences of '297' on the page) and 'checkouts-v3' now appears 3x where it was 0. STILL OPEN: (1) the RETIRED 4.9★ rating is live in the visible copy — 'From 100k+ members' — and 4.9 is banned on every surface (published figure is 4.7/4.8); note this is ONE string, not the 12 this row previously claimed, because that count matched inside '$24.97' and SVG path coordinates, and the page carries no 4.8 at all. (2) 9 hrefs (not 17) still point at the legacy /register/ estate: monthly3 x3, yearly3 x3, quarterly2 x3. (3) 2 hrefs (not 15) still carry a hardcoded _gl= token. (4) /50promo/ lifetime $498 vs the map's $497, and checkouts-v3/trial/checkout-1month/ still 404s (NIC-59). This page takes organic, email, YouTube AND the BWA /30days/ traffic now 301'd into it (FLEET-58, shipped 11 Aug). Verify with a cache-busted fetch, not from the editor.
impact 5/5 · 45 min
✅ BACKLOGGED ON AGA'S RULING, 11 Aug 2026: 'leave this alone, backlog this.' CANCELLED not parked — a parked row still renders on her page, which is how NIC-104 kept coming back. The remaining defect is REAL and unchanged: /start-training/ carries the retired 4.9★ in visible copy ('From 100k+ members'
from: growth-fleet review of the remarketing playbook, 11 Aug 2026 — tma-funnel-cro-lead + tma-data-verification-officer, both verified live
NIC-94done
📧 THE RESULTS EMAIL TELLS EVERY LEAD THEIR PASSWORD IS "[Chosen Password]" — quiz_email.html line 24 is literal text, not a merge field, so the reader sees the words '[Chosen Password]' where their password should be. It is the only login instruction in the email (line 22 correctly renders Username: their email). Fires on every assessment completion, subject '<Name>, your results are ready!'. TWO THINGS: (a) confirm the live 30 Jul template still has that literal line, (b) decide what should be there — these accounts often have NO password the person knows (quiz signup falls back to settings.SECRET_KEY when the quiz posts none), so the correct fix is probably a set-password link, not a password. Also paste the abandon template ('Your login is in this email') — it is the 5th transactional template and was not in the 30 Jul set.
impact 4/5 · 15 min
✅ CLOSED ON AGA'S WORD, 11 Aug 2026: 'that was fixed'. BASIS STATED HONESTLY — this is a Django-rendered transactional template (quiz_email.html, rendered server-side per docs/company/ANALYTICS QUIZ_FUNNEL_EMAIL_MASTER), so it cannot be read from a live URL the way a web page can; the fleet did not re
from: Django template read 7 Aug 2026: tma-backend/app/apps/quiz/templates/quiz_email.html L24
NIC-93moved-to-aga
🔴 STOP THE DOUBLE-WELCOME: the verify email enrols quiz leads (and BUYERS) into 992 TMA | Welcome Sequence AFTER APPSIGNUP AB TEST Aug4th. CONFIRMED LIVE 4 Aug, still live 7 Aug, and until today NO row owned it. The mechanism: clicking 'One Last Step: Verify Your Account' runs activteUserAndToList() -> activated=True -> list 204 -> tag free_signed_up_mobile_&_web (tag 167, 11,859 subs, 184/wk) -> 992's sole entry trigger fires. The tag cannot tell a quiz finisher from a buyer from a genuine app signup, so HALF of 992's first entrants were the wrong audience: derek.deatley@ was a quiz lead in TWO full welcome arcs (~19 overlapping emails); jacnast@ was a PAYING CUSTOMER receiving a lead-acquisition arc. THE FIX, either level: (a) ROOT (preferred, ~30 min) — activteUserAndToList() only applies the tag when the account did NOT originate from the quiz (the QuizUserCreated tag 241 is already on every quiz lead, so the check exists) and never for an already-paying customer; (b) INTERIM (Aga, builder, 5 min) — add 'tag QuizUserCreated does-not-exist AND not on list 133 CURRENT CUSTOMERS' as a segment condition on 992's entry. NOTE: the journey strategy's J1 (merge the T+0 emails, kill verify) removes the verify email entirely, which closes the biggest path into this — if J1 ships first, this row shrinks to the tag-routing check for non-verify activations.
impact 4/5 · 30 min
✅ MOVED TO AGA on her ruling, 11 Aug 2026: 'we are dealing with this, this is Aga's task.' The double-welcome (the verify email enrolling quiz leads AND buyers into a second welcome sequence) is hers; off Nic's list and off his surfaces. The diagnosis stays on the row for whoever picks it up.
from: QUIZ_FUNNEL_EMAIL_MASTER §Failure-5 (confirmed live 4 Aug: half of 992's first entrants were the wrong audience) + the 7 Aug journey sweep which proved NO fix row existed despite the abandonment master claiming one did
NIC-90done
🔴 ONE ANSWER NEEDED BEFORE WE ARM THE ABANDON LANE — is the ≈T+1h "Your login is in this email" Django→SendGrid send LIVE in production right now, and what exactly triggers it? Answer in two lines: (a) live yes/no, (b) the trigger condition and delay as the code actually has it today. WHY IT MATTERS AND WHY NOW: 756 Part 1 - Abandoned Cart Reminder is about to be loaded with two recovery emails and armed. Its supply is quiz_abandon_sync.py, which found 43 abandoners worth $4,171 in the last 30 days. If your T+1h send is live, the same person gets a Django→SendGrid email at ~1h AND our E1 on the next daily sync — two recovery emails from two systems that cannot see each other. QUIZ_FUNNEL_EMAIL_MASTER §5 records it as live and says it 'makes it five in an hour for a decliner', but that came from a SendGrid log reading, not from the code, and we hold no SendGrid credential to re-check it. If it IS live we will delay our E1 past it or drop the overlap; if it is NOT, we send E1 immediately on entry. Do not change anything — this is a read-and-report.
impact 4/5 · 10 min
✅ ANSWERED BY NIC 7 Aug 2026 (his own code, authoritative for this): (a) YES the abandon email is live — it had been held off by a safety switch on their side, that is now FIXED and a new build was deploying to Azure Prod as he wrote, so abandons can actually go. (b) TRIGGER: it fires ~1 HOUR AFTER so
from: QUIZ_FUNNEL_EMAIL_MASTER_2026-08-04.html §5 + AGA-57 abandon lane
NIC-91done
FIX THE TWO ONE-LINERS THAT UNBLOCK EVERY FUNNEL COMPARISON — (a) each old quiz build (2026-v2-2, /2026/, 2026-v2-3) forwards its baked-in VITE_QUIZ_VARIANT into the /register/ handoff AND the /register/ Django flow writes it into subscription metadata (the legacy rail writes NOTHING today — 54 of 54 legacy subs since 1 Jul empty, re-pulled 7 Aug PM); (b) v1 first-touch UTM rehydration — corrected: 5 of 8 buyers DO carry utm_source (google paid x1, tma x3, bwa x1), the gap is the 3 bare-landing buyers + only 1 external paid source. Until (a) ships, NO revenue/AOV/retention comparison between quiz builds is possible. Fully re-verified 7 Aug PM: live bundles + fresh Stripe pull; detailed instructions on the Nic brief #quiz-variant-stamp.
impact 5/5 · 60 min
✅ SHIPPED by Nic 7 Aug 2026 ~19:20 UTC. Two of the three writes VERIFIED LIVE the same day in served code, 19:24-19:28 UTC: (a) /register/monthly_2026/ now serves a hidden quiz_variant input plus the full utm_source/medium/campaign/content/term + gclid + fbclid + pe set into process_stripe, with JS ba
from: quiz-funnel-versions deep analysis 7 Aug 2026 (CEO doc §1 STOP box + §18 to-do #3)
NIC-92done
CHECKOUTS-V3: RESOLVE THE 5 UNBILLABLE ACTIVE SUBS — the phantom mint itself is FIXED (verified 7 Aug PM: zero phantom mints since 5 Aug; the only 2 mints since are real trialing checkouts — Nic's defer-mint works). Remaining: 5 subs ACTIVE with no payment method, no paid invoice, no discount (sub_1TxtC5…, sub_1TyF1X…, sub_1TyF2N…, sub_1TyF6d…, sub_1TyIEB… — all $24.97/mo, minted 27-28 Jul pre-fix, re-verified individually still active 7 Aug 15:35 UTC; they are 5 of 5 of the window's active v3 subs). Bill them or end them — a billing decision, ~20 min, no code work left. Was: 'stop the phantom mint + resolve' at ~2h.
impact 4/5 · 2.0h
✅ VERIFIED LIVE 7 Aug 2026 19:26 UTC against the Stripe API, each subscription fetched individually by id. All five unbillable actives are now status=canceled, canceled_at 2026-08-07 18:58 UTC, reason cancellation_requested: sub_1TxtC5I6s6pBAbnmWhvmZm6g, sub_1TyF1XI6s6pBAbnmRUud2JzC, sub_1TyF2NI6s6pBA
from: checkouts-v3 diagnosis, quiz-funnel-versions doc 7 Aug 2026
NIC-50done
🔴 THE LAST LEG OF EMAIL ATTRIBUTION — persist the pe= recipient into Stripe metadata. SPLIT OUT OF NIC-19 ON AGA'S RULING (4 Aug) so it stops competing with the debris sweep: it was change 5 on a card whose remaining items are a 404, a past_due backlog and a TLS header, and it kept sliding behind them. IT IS NOT DEBRIS — it is the one thing standing between us and knowing which email made a sale. THE CHAIN, verified 3 Aug: (leg 1) our email links carry utm_* — the AC estate hit 100%, 1,101 of 1,101 links, 0 of 21 automations dark; (leg 2) BOTH checkout estates ALREADY flatten utm_*/gclid/fbclid into Customer/Subscription/PI metadata at create-intent — do NOT rebuild this, the audit confirmed it exists; (leg 3) THIS — the recipient id. Without leg 3 we can say 'email made this sale' but never WHICH CONTACT, WHICH AUTOMATION, WHICH EMAIL. WHAT TO DO: the email links already carry a pe= parameter identifying the recipient; capture it at create-payment-intent alongside the utm_* you already flatten, and write it into the same Stripe metadata blob. Same handlers as the rest of NIC-19 (create-payment-intent / update-payment-intent in checkouts_v3_stripe.py, and the quiz equivalent in quiz_checkout_stripe.py). ~30 min. SCOPE: both estates. DONE WHEN: one click from a real AC email through to a purchase lands a Stripe intent whose metadata carries BOTH utm_source AND the pe= recipient id — read it in the Stripe dashboard, not in code. DEADLINE THAT IS REAL: the Email/Lifecycle fleet's charter gate is 'email revenue attributable by 29 Aug 2026'; this is the only remaining blocker on it, and the Evolution Law cannot name a losing email slot on conversions (rather than opens) until it lands.
impact 5/5 · 30 min
✅ CLOSED 4 Aug 2026 — THE EMAIL-ATTRIBUTION WIRING GATE. pe= is now forwarded into the payment intent on BOTH estates. VERIFIED LIVE: v3 /static/checkouts_v3/attribution.js reads pe from the live URL and treats it as LAST-touch by design (its own comment: 'pe (email recipient / AC contact) is last-tou
from: split out of NIC-19 change 5 by Aga's ruling 4 Aug 2026 · CHECKOUT_ATTRIBUTION_CODE_AUDIT_GROUND_TRUTH_2026-08-03.md §10
NIC-51superseded
EXPOSE stampPaywallView SO THE RECOVERY LANE HAS A SUPPLY — without it everything already built for quiz-checkout recovery is a one-shot. MEASURED 4 Aug against live Stripe: since page-load minting stopped on 3 Aug, new unpaid quiz records in the same clock window each day went 6 -> 7 -> 2 -> 0. The Stripe feed the recovery lane reads is now DEAD, leaving a finite backlog of ~37 people whose $97 windows have mostly expired and no ongoing supply. stampPaywallView already exists server-side (your own 3 Aug audit names it as the quiz abandon mechanism, Redis/cache + quiz_checkout_abandon.py) — we simply cannot reach it. DIRECTIVE: expose it read-only, either a small authenticated endpoint or a scheduled export, carrying per person: email, plan, path/persona, and the paywall-view timestamp. No new capture, no schema work — surface what you already store. ACCEPTANCE: we can pull the last 72 hours of paywall views with those four fields and reconcile the count against your own store. VALUE: the difference between ~37 people once and ~9-12/day continuously, which at the 3-5% cart-recovery benchmark is roughly $900-1,500/mo recurring versus a few hundred dollars once. ALSO PLEASE ANSWER IN ONE LINE: does quiz_checkout_abandon.py currently SEND anything, or does it only stamp and queue? If it already sends, our ActiveCampaign lane must not arm at all or we double-email people who just declined to pay.
impact 5/5 · 45 min
✅ SUPERSEDED 4 Aug by NIC-52 before it ever reached Nic's brief. I asked for a new read-only endpoint exposing stampPaywallView. Aga pushed back — why not use what we already work with — and she was right. Reading app/apps/users/helpers.py in the Django source: ActiveCampaignAPIHelper ALREADY EXISTS a
from: CHECKOUT_EMAIL_COVERAGE_AND_GAPS_2026-08-04.md §1.3a + §6a
NIC-52superseded
TAG THE PAYWALL VIEW INTO ACTIVECAMPAIGN — one call to a method you already have, ~15 min. Wherever stampPaywallView runs today, also call ActiveCampaignAPIHelper.add_tag_to_quiz_contact(...) with a tag like reached-checkout plus the timestamp as its value. That helper ALREADY EXISTS in app/apps/users/helpers.py alongside add_contact_to_quiz_started, and the contact is already in ActiveCampaign because the quiz put them there — so this creates nothing new anywhere. Make the call async/fire-and-forget so it can never slow the checkout page. WHY THIS AND NOT STRIPE: since page-load minting stopped on 3 Aug, Stripe no longer records anyone who views the paywall and leaves — verified 4 Aug, no unpaid quiz record in over 15 hours against 6-17/day the week before. That was the right fix and must NOT be undone; writing page-views back into Stripe would re-create the phantom records NIC-41 removed and re-break the conversion denominator. ActiveCampaign is where the person already lives and where the email actually sends from, and Aga's team already holds AC credentials so no new access or endpoint is needed. ACCEPTANCE: view the quiz paywall as a test contact, do not pay, and the contact carries the tag in ActiveCampaign within a minute. PLEASE ALSO CONFIRM IN ONE LINE: is ACTIVE_CAMPAIGN_INTEGRATION_ENABLED true in production? The helper no-ops silently when it is false. VALUE: without this the recovery sequence fires once at ~37 stale records and goes silent forever; with it, ~9-12 people a day, continuously.
impact 5/5 · 15 min
✅ SUPERSEDED 4 Aug by NIC-54, which merges every quiz stage-tag into ONE task in one sitting. Splitting them made Nic open the same files three times for the same one-line pattern. Also, investigation moved the target: proven live that 200 of 200 contacts on AC list 209 'Stage 1: Input The Email' are
from: CHECKOUT_EMAIL_COVERAGE_AND_GAPS_2026-08-04.md §1.3a; app/apps/users/helpers.py ActiveCampaignAPIHelper
NIC-53superseded
THE NEW QUIZ STOPPED WRITING TWO OF ITS FOUR STAGE SIGNALS ON 31 JUL — we are blind in the middle of the funnel. Verified 4 Aug by pulling the newest contacts added to each ActiveCampaign stage list: list 209 Stage 1 Input The Email has five additions dated 4 Aug (live) and list 201 Stage 2 Registered in the APP likewise (live) — but list 202 Stage 3 Started The Quiz and list 200 Stage 4 Finished the Quiz both stop dead at 31 Jul, the day the new quiz shipped, and were already sparse before that. CONSEQUENCE: we can see who types an email and who registers in the app, and nothing in between. We cannot tell who started the quiz, who finished it, or who saw their results — so we cannot size drop-off, cannot detect a broken results screen (finished with no paywall view is the signature of a bug), and cannot measure any funnel change you or we ship. DIRECTIVE: restore both writes in the new quiz — the same ActiveCampaignAPIHelper path the old quiz used, on questions-answered and on results-rendered, and please keep them as two distinct events rather than one, since answering the questions and actually seeing the result are different moments and only the second one means the person got what they came for. ACCEPTANCE: take the quiz as a test contact and both lists gain that contact within a minute, with results-rendered firing only once the results screen is actually shown.
impact 5/5 · 30 min
✅ SUPERSEDED 4 Aug by NIC-54, which merges every quiz stage-tag into ONE task in one sitting. Splitting them made Nic open the same files three times for the same one-line pattern. Also, investigation moved the target: proven live that 200 of 200 contacts on AC list 209 'Stage 1: Input The Email' are
from: QUIZ_FUNNEL_DROPOFF_SIGNALS_AND_SEQUENCES_2026-08-04.md §HEADLINE
NIC-54done
✅ SHIPPED & VERIFIED LIVE 7 Aug 2026 — THE QUIZ NOW WRITES WHAT IT KNOWS INTO ACTIVECAMPAIGN. 15 'NEW quiz-*' tags (stage + path) carry STATE and 10 custom fields carry DATA, written at lead time. Verified by live census, not by claim: real leads created 7 Aug carry both — contact 378032 (06:43) holds persona SKILL_SEEKER, obstacle CONFUSION, future_stakes 'Never unlocking the skills I know are possible', age 18_24, gender MALE, life_context FLEXIBLE, background GYM, recency CURRENT, weakest Abs. 🔴 THE ACCEPTANCE TEST PASSES: AC tag total is still 5,845 — no value leaked into a tag name, so the 5,623-junk-tag pattern did NOT recur. Re-run any time: python3 tools/retention/quiz_lead_signal_check.py
impact 5/5 · 75 min
✅ VERIFIED LIVE 7 Aug 2026 by census of the ActiveCampaign API, re-runnable as: python3 tools/retention/quiz_lead_signal_check.py --sample 2 (exit 0 = HEALTHY). All 15 tags exist and are being applied to real leads: email-captured 8 · questions-done 8 · gave-email 8 · results-seen 4 · reached-checkout
from: QUIZ_FUNNEL_FINAL_SOLUTION_2026-08-04.html §4a
NIC-55superseded
🔴 STOP TAGGING INTERNAL LINKS WITH utm_source — they are erasing which channel actually won each lead. Every quiz CTA on themovementathlete.com (nav, hero, blog, magnets) carries ?utm_source=tma&utm_medium=site|blog&utm_campaign=<page>. GA4 STARTS A NEW SESSION when campaign parameters change, so the moment any visitor clicks one, their real origin — ChatGPT, organic, social, referral — is replaced by 'tma / site'. Evidence: 1,211 AI-engine sessions over 180 days produced ZERO attributed leads (≈12 expected); the 'tma' lane holds 23 leads/90d and is growing because this tagging only landed 23–31 Jul. 🔴 Cross-domain measurement is ALREADY configured (1 self-referral in 90d across ~108k sessions), so these UTMs buy nothing GA4 wasn't doing — they only cost us the source. FIX: drop utm_source/utm_medium/utm_campaign from INTERNAL links and carry 'which page sent them' in a custom event parameter instead (does not restart the session). ⚠️ Does NOT touch external content — emails, social, YouTube keep their UTMs, that rule is correct and unchanged.
impact 5/5 · 45 min
✅ SUPERSEDED 5 Aug by AGA-73, which found this first (3 Aug) and owns it. I raised it independently from the outreach angle without seeing AGA-73 — the title guard caught the duplicate. What my pass ADDS is folded into AGA-73: the other session measured the relabel on SESSIONS (0.33%, called hygiene);
from: marketing/seo/OUTREACH_MASTER_2026-07.md §7a
NIC-58cancelled
🔴 TMA (app.themovementathlete.com) — THREE OF THE FOUR /checkouts-v3/30trial/ URLs ARE 404. Verified live 4 Aug: 30trial/1month/ = 200 ($9.99/30d), but 30trial/3month/, 30trial/12month/ and 30trial/lifetime497/ all return 404. Either build the three missing plans or confirm 1month is the only intended plan so the map can record it as by-design. Repro: curl -o /dev/null -w '%{http_code}' https://app.themovementathlete.com/payments/checkouts-v3/30trial/12month/
impact 2/5 · 15 min
✅ CANCELLED by Aga 7 Aug 2026 — and it should never have reached Nic, because the product call was ALREADY MADE and recorded. CHECKOUTS_V3_URL_MAP_AND_USE_CASES_2026-08-04.md §5 is titled '30trial/ — $9.99/30-day intro · 🔴 MONTHLY ONLY' and carries Aga's own 4 Aug quote ('this is our 30 day trial, for
from: docs/company/ANALYTICS/CHECKOUTS_V3_URL_MAP_AND_USE_CASES_2026-08-04.md §5 D1
NIC-59done
🔴 TMA (app.themovementathlete.com) — THE 50%-OFF PAGE'S EXPIRY ESCAPE HATCH IS A DEAD LINK. When the offer window closes the /checkouts-v3/50off/ pages show 'your window has closed' plus a button 'Start a 7-day free trial on the monthly plan →'. That button points at ../../trial/checkout-1month/ which resolves to /payments/checkouts-v3/trial/checkout-1month/ = 404. Should be ../../trial/1month/ (200). A one-token fix: an expired-offer visitor who still wants to buy currently hits a dead page instead of the default checkout. Repro: curl -o /dev/null -w '%{http_code}' https://app.themovementathlete.com/payments/checkouts-v3/trial/checkout-1month/
impact 3/5 · 10 min
✅ VERIFIED LIVE 7 Aug 2026 19:25 UTC by re-running the same four-page crawl the card's done-when specified. The dead 'checkout-' prefix is gone from all four 50off pages: 50off/1month -> ../../trial/1month/, 3month -> trial/3month/, 12month -> trial/12month/, lifetime497 -> trial/{1,3,12}month/. All f
from: docs/company/ANALYTICS/CHECKOUTS_V3_URL_MAP_AND_USE_CASES_2026-08-04.md §5 D2
NIC-73cancelled
🔴 TMA (quiz.themovementathlete.com) — THE LIVE QUIZ PUBLISHES 4.7★ AND THE PUBLISHED FIGURE IS 4.8★. Rides the next quiz deploy, ~15 min of edits. ⚠️ THIS IS OUR MISTAKE, NOT NIC'S — read this before raising it with him. The 4 Aug rebuild correctly removed the 4.9★ claims per NIC-46, but that row told him to 'ship 4.7 unattributed (31 Jul ruling)' and Aga SUPERSEDED that ruling to 4.8★ the same day he executed it. He built exactly what the task said; the task was stale by hours. THE RULING: 4.8★ is a BLENDED cross-platform figure (App Store + Google Play + Facebook + internal member feedback) and is the number we publish everywhere. 4.7 is the Google-Play-only reading and is ASO-internal ONLY — it must never be published, and 4.9 stays retired. VERIFIED LIVE 4 Aug in bundle index-DFwXN-qe.js — SEVEN user-facing surfaces to change: (1) '4.7 rated' (landing-v3 star row) (2) '4.7 · 100,000+ plans created' (trust-text) (3) '4.7 · 100k+ users' (p52-v4 star row) (4) '4.7 average rating · 100,000+' (5) '4.7★ rating' (stat block) (6) '★★★★★ 4.7' + '100,000+ athletes' (7) 'The programme people dont cancel · 4.7 ★ · 47 countries'. Change 4.7 -> 4.8 in all seven; leave every member count alone (100,000+ / 100k+ are correct and now consistent, and the 53,000+ contradiction is already gone). Do NOT attribute 4.8 to a single storefront anywhere in the copy.
impact 4/5 · 15 min
✅ DROPPED BY AGA, 4 Aug 2026 — her call, asked and answered. The live quiz keeps 4.7 on its seven surfaces; it is not worth a deploy slot or Nic's attention. Do NOT re-raise this, do not re-file it under another id, and do not fold it into a future quiz deploy as a 'while we are in there' extra. The 4
from: CHECKOUT_ATTRIBUTION_CODE_AUDIT_GROUND_TRUTH_2026-08-03.md section 0.3 · live bundle read 4 Aug 2026 · supersedes the 31 Jul star ruling carried by the now-closed NIC-46
NIC-79done
🔴 TMA DNS — FIX THE SPF RECORD ON themovementathlete.com (3 records, ~15 min work + 20 min validation). PROVEN LIVE, not theory: a real Authentication-Results header from 4 Aug 2026 reads 'spf=permerror'. TWO faults, and the second is the one that actually matters. (1) include:spf.acemsb4.com is NXDOMAIN — RFC 7208 5.2 makes that a PERMERROR, and because SPF evaluates LEFT TO RIGHT and stops at the first match, any sender not matched earlier falls into it and never reaches ~all. (2) THE REAL HOLE: there is NO include:_spf.google.com. The 'mx' mechanism authorises Google's INBOUND aspmx IPs (172.253/173.194/142.251) but Workspace SENDS from 74.125.0.0/16 + 209.85.128.0/17 — zero overlap — so every hello@/hi@/nic@ email walks the whole record into the dead include. Under DMARC p=reject only the Workspace DKIM key (google._domainkey, live) stops us rejecting our own mail: no redundancy, and forwarded mail that breaks DKIM has nothing left. NOT AT RISK and NOT to be touched: ActiveCampaign and SendGrid both use their own envelope subdomains (em-152341 / em9638) with valid SPF and aligned DKIM — both verified dmarc=pass — so the acemsb4 include is vestigial and deleting it changes nothing for them. STEPS ARE IN THE BRIEF at tma-nic-attribution-brief.netlify.app under 'Fix the TMA SPF record'. Measured lookup budget after the fix: 7 of 10.
impact 5/5 · 35 min
✅ VERIFIED LIVE 5 Aug 2026 22:4x UTC by me, dig +short @ns1.siteground.net themovementathlete.com TXT (authoritative NS, not a public resolver). Root SPF now reads: v=spf1 a mx ip4:35.214.157.236 include:spf.tapfiliate.com include:themovementathlete.com.spf.auto.dnssmarthost.net include:_spf.google.co
from: SPF/DMARC deep-dive 5 Aug — full SPF-tree walk + per-sender check_host() simulation + real Authentication-Results headers pulled from hello@ via Gmail API
NIC-41done
MOVE STRIPE INTENT CREATION OFF PAGE LOAD — static/quiz_checkout/stripe-client.js. Today maybePrefetch() runs on DOMContentLoaded and calls ensureIntent() whenever the email field is prefilled, and it is ALWAYS prefilled from the quiz — so Stripe creates a customer + subscription + open $97 invoice for every PAGE VIEW, before the visitor touches anything. Three consequences, all live: (1) checkout conversion is unmeasurable — requires_payment_method and 'payment method attached' are meaningless because every non-buyer looks identical to an instant bounce; (2) the recovery lane counts bounces as abandoners, so the abandoned-cart audience is inflated with people who never intended to buy; (3) Stripe fills with phantom subscriptions (48 records for 41 distinct people in the first 4 days). DIRECTIVE: move the ensureIntent() call from DOMContentLoaded to the FIRST interaction with a card field (focus or first keystroke on card number). Keep the existing password-blur trigger. Do not create an intent on load under any condition. TEST (budget 15m of the 45m): load the checkout with a prefilled email and do NOT touch the card form -> confirm NO new Stripe customer/subscription is created; then type one character into the card number field -> confirm the intent IS created and payment still completes end to end. DONE WHEN: a page load with zero card interaction leaves no trace in Stripe, and a real purchase still works.
impact 5/5 · 45 min
✅ SHIPPED BY NIC 3 Aug 2026 and VERIFIED LIVE the same day by reading the SERVED file https://app.themovementathlete.com/static/quiz_checkout/stripe-client.js (HTTP 200, 17,446 B) — not the dev message. maybePrefetch: 0 occurrences (removed). DOMContentLoaded: 0 occurrences (no load-time mint of any k
from: quiz-checkout diagnosis 3 Aug (fleet + skeptic gate)
NIC-42done
NAME ONE CANONICAL DENOMINATOR FOR LEADS — the same metric currently has THREE live values on the same day: GA4 ordered funnel 65/7d, GA4 generate_lead 103/7d, ActiveCampaign quiz list 140/7d (a 2.15x spread). The RATE is worse: kpi_15k.json reports quiz lead_rate 26.6% (65 over 244 quiz-landing visitors) while data_funnel.json reports 1.1% (103 over 8,957 property sessions) — a 24x spread, same estate, same day. Two of the four sourcing errors in the 3 Aug quiz analysis trace directly back to this. Until one pair is canonical, every conversion or lead-rate claim anyone makes is arguable and unfalsifiable. DIRECTIVE: reconcile the three counts (they differ for real reasons — event scope, hostname filter, AC list membership and dedup) and publish ONE canonical numerator+denominator pair as the definition of 'quiz lead' and 'quiz lead rate', written into the scoreboard config rather than any doc. Every other counter stays visible but is explicitly labelled as a different measure, never mixed. Precedent to follow: the existing app-signups ruling that RC customers_new and GA4 onboarding are different counters, both real, never mixed. TEST (budget 30m of the 90m): after the change, kpi_15k.json and data_funnel.json report the SAME lead count and the SAME lead rate for the same window, and a third party can reproduce both from the stated query without asking which source to use. DONE WHEN: one definition is in config, the two scoreboard files agree, and the other counters carry an explicit label saying what they measure instead.
impact 4/5 · 1.5h
✅ DONE BY THE FLEET 5 Aug 2026 — taken off Nic entirely (Aga: 'can you please execute on that'). The row read as 'nobody knows what a lead is'; that was never the fault. A written definition already existed in kpi_15k.json. The real defect was that ONE word covered THREE doors — quiz capture / free ap
from: quiz-checkout diagnosis 3 Aug (skeptic gate finding)
NIC-37done
🔊 MAKE THE DEAD BUY BUTTON SPEAK (~20m — same file as NIC-30, do them in one sitting) — extracted from the retired on-page-port task (Aga, 3 Aug). In the quiz checkout handler's fallback block (the SAME block carrying VITE_REGISTER_PAY_URL and addToCartValue — the one NIC-30 opens to remove app-test): the branch that silently no-ops when a tapped plan doesn't match any plan object must (1) emit an analytics event checkout_button_dead with the plan id + screen, and (2) show the visitor a visible error ('Something went wrong — refresh and try again') instead of nothing. Today that person vanishes from every number we have. Test: force a mismatched plan id in a test build → the event fires in GA4 DebugView and the message renders. Done when a forced mismatch leaves a trace instead of silence.
impact 3/5 · 20 min
✅ VERIFIED LIVE 4 Aug 2026 in bundle index-DFwXN-qe.js. The fallback paywall path now BOTH emits an event and speaks to the buyer: lt("checkout_button_dead", {plan, quiz_variant, paywall_variant}) plus visible copy 'This plan isn't available right now. Please choose another plan.' Shipped in the same
from: revenue-plan task 19 · extracted from retired NIC-35, Aga ruling 3 Aug
NIC-43done
🔍 TMA (themovementathlete.com) — NOINDEX THE NON-PRODUCTION HOSTS AND THE APP'S PAY ROUTES. Found 3 Aug via GSC page-by-host on the TMA domain property. TWO PARTS; part B matters more. (A) NON-PRODUCTION IS INDEXED: app-test. (7 impr / 2 clicks, incl. /login?next=/register), staging. (7 impr) and beta. (17 impr / 1 click) all return HTTP 200 with NO robots meta; staging. and beta. don't even serve a robots.txt (the SPA catches the route and returns HTML). Volume is tiny — hygiene, not an emergency — but they should never be indexable. (B) 🔴 PRODUCTION app. IS EXPOSING PAY ROUTES: /register/monthly2/promo/ (104 impr, 3 clicks) and /register/lifetimespecial/ (22 impr, 1 click) are live 'Register and pay' pages with NO robots meta — a promo price and the lifetime special are publicly findable in Google outside any campaign. Also indexed: a /login URL carrying a full GA linker blob (_gl / _ga / _ga_ZBQN3P8WKW, 71 impr); tracking params should never be crawlable. FIX: send `X-Robots-Tag: noindex, nofollow` on ALL of app-test./staging./beta., and on app.'s /login and /register/* routes. The non-production hosts ideally also get HTTP basic auth, which both de-indexes and stops anyone reaching them. 🔴 ORDER MATTERS — DO NOT add `Disallow: /` to robots.txt first: blocking the crawl stops Google ever SEEING the noindex, which leaves the URLs indexed as bare links. noindex first, let it recrawl and drop out, only then consider disallowing. NO EFFECT ON CAMPAIGNS: paid/email/social traffic reaches these pages by direct link, which noindex does not touch. ACCEPTANCE: curl -I each host and route, expect X-Robots-Tag: noindex; the hosts then fall out of the TMA GSC property within ~2 weeks.
impact 3/5 · 45 min
✅ VERIFIED LIVE 7 Aug 2026 19:24 UTC by curl -I. All three remaining nginx hosts now return 'X-Robots-Tag: noindex, nofollow': quiz-test (200), monitor (302 -> /login), grafana-dev (302 -> /login). The Django app-route half was already confirmed 5 Aug and re-verified today on /login, /register/monthly
from: marketing/seo/TMA_SEO_GEO_ANALYSIS_AND_PLAN_2026-07-20.md 12.6-N - GSC TMA property page-by-host 28d, pulled 3 Aug 2026
NIC-38moved-to-aga
991 NEW Re-Engagement - For people who stopped opening Before Sunset: R1 canary hard-bounced 6.9% (33/478) — 3.5× the 2% hard-stop line. Pre-clean the ~9,128-contact backlog (drop known-dead/bounce-history addresses) BEFORE enter_991_reengagement.py continues the 2k/day feed, or domain reputation pays for the hygiene we'd get free from the suppression step.
impact 4/5 · 30 min
✅ Moved to Aga 3 Aug on her ruling — the email lane is her estate. Now tracked as AGA-63; verified present in the queue under that id. Removed from Nic's open list and shown on his brief under 'Closed or moved' so it stays visible rather than vanishing.
from: email-selflearn WEEKLY_2026-08-03
NIC-39moved-to-aga
Activate the 3 pre-registered email A/B splits in ActiveCampaign (750 TMA | Welcome Sequence After Quiz E1 subject test + nurture + re-engage) — 3rd consecutive week the self-learn loop has ZERO live experiments because splits need manual AC setup. Setup specs: EXPERIMENT_LEDGER.json W29 records + WEEKLY_2026-08-03.md §5.
impact 4/5 · 45 min
✅ Moved to Aga 3 Aug on her ruling — the email lane is her estate. Now tracked as AGA-64; verified present in the queue under that id. Removed from Nic's open list and shown on his brief under 'Closed or moved' so it stays visible rather than vanishing.
from: email-selflearn WEEKLY_2026-08-03
NIC-40moved-to-aga
Extend tools/email-analytics/collect_metrics.py to pull ActiveCampaign spam-COMPLAINT counts — the safety sweep's #1 hard-stop (pause at 0.10% complaints) has been unmonitorable (DARK) since the loop was built, on the platform carrying ~100% of send volume. Flagged 27 Jul, still open.
impact 4/5 · 45 min
✅ Moved to Aga 3 Aug on her ruling — the email lane is her estate. Now tracked as AGA-65; verified present in the queue under that id. Removed from Nic's open list and shown on his brief under 'Closed or moved' so it stays visible rather than vanishing.
from: email-selflearn WEEKLY_2026-08-03
NIC-44moved-to-aga
MAKE THE 48-HOUR $97 WINDOW ACTUALLY BINDING — today it can be re-minted, and the recovery sequence's urgency depends on it. Probed live 3 Aug against the served page: the per-visitor deadline is CORRECTLY fixed once set (same email returns the identical timestamp on repeat requests — good). Two holes remain. (1) Any address the server has no cache entry for gets a FRESH 48h window on first sight — verified with a never-seen address, exactly 48.0h. So the offer is re-obtainable with a new email, and an existing person whose cache entry has lapsed gets a new window: wajackson1@gmail.com abandoned on 30 Jul and today holds a live window running to 4 Aug 08:53. (2) TMA_QC_OFFER_EXPIRED came back FALSE on every address tested including a 90-hour-old abandoner, so we have never observed the expired state and CANNOT confirm the $97 is actually killed at charge time. Your own source comment in the page says it outright: 'Nic: the same deadline must kill the $97 server-side (coupon dies -> $157). A timer that changes nothing is banned.' DIRECTIVE: (a) once an email has had a window, do not mint it a second one — a lapsed cache entry must resolve to EXPIRED, not to a fresh 48h; (b) enforce it at charge time, not just in the UI. ACCEPTANCE: take an address whose window has passed, load the checkout -> TMA_QC_OFFER_EXPIRED is true and the page shows $157; attempt the purchase -> Stripe charges $157, not $97. WHY IT MATTERS TO US: emails E3 and E4 tell people their window is closing and then closed. If the page hands them a fresh 48h on arrival, we have lied to them in writing and taught them the deadline is theatre.
impact 4/5 · 45 min
✅ MOVED TO AGA 3 Aug on her ruling — now AGA-68. Not Nic's to decide: it is a pricing/CRO call before it is an engineering task. Binding = honest urgency; soft = nobody is ever turned away but the emails must drop every deadline claim. Her ruling changes E3/E4 copy, not the code. If she rules binding,
from: quiz-checkout abandonment wiring, live-probed 3 Aug
NIC-45folded
CARRY THE QUIZ'S OWN ANSWERS INTO THE CHECKOUT — four extra params, and the recovery emails stop being generic. NOT blocking, do it after NIC-41/44. Verified live 3 Aug: the checkout page reads exactly TWO url params (path, persona) and Stripe subscription metadata carries email, persona, path, plan_slug, plan_key, plan_id, offer, offer_expired, coupon, quiz_variant, screen_id, checkout_version, purchase_path — plus utm_source/medium/campaign on 33 of 48. What it does NOT carry, and the quiz already knows: OBSTACLE, FUTURE_STAKES (their own words for what they are afraid of if nothing changes), AGE, GENDER. Those four are the strongest copy assets we have and they die at the checkout boundary. DIRECTIVE: append obstacle, future_stakes, age, gender to the quiz-to-checkout hand-off URL exactly as path/persona already travel, read them the same way, and include them in the reached-checkout signal from NIC-41 (and in intent metadata when a card is touched). No Django schema work, no new tables — same pattern as the params already flowing. ACCEPTANCE: complete the quiz, land on the checkout, and confirm all four appear in the reached-checkout record with the values the quiz collected. WHY IT MATTERS TO US: today every recovery email can say WHAT they wanted (persona). With these it can say what was standing in their way and what they told us they were afraid of — that is the difference between a template and a letter.
impact 3/5 · 30 min
✅ FOLDED INTO NIC-54 on 6 Aug 2026 (Aga's call: bundle everything Nic sends us about a lead into one task). The NEED is real and confirmed live; the old DIRECTIVE was wrong on destination. It said 'append obstacle, future_stakes, age, gender to the quiz-to-checkout hand-off URL' so the values would ri
from: quiz-checkout abandonment wiring, live-probed 3 Aug
NIC-46done
quiz.themovementathlete.com LIVE bundle: two 4.9-star strings ('4.9 · 100,000+ plans created' + '★★★★★ 4.9 · 100k+ users') violate the settled 4.7★ ruling, and one 'Join 53,000+ people' contradicts the six '100,000+' strings — fix in the 2026-quiz-checkout-v1 source and redeploy. Found 3 Aug by quiz-fact-checker while verifying the magnet-thank-you rebuild.
impact 3/5 · 20 min
✅ CLOSED 4 Aug 2026 — both stated defects fixed, VERIFIED LIVE in bundle index-DFwXN-qe.js: no 4.9 rating strings remain, '53,000' returns 0 occurrences, and member counts are consistent (100,000+ x14, 100k+ x3). ** BUT IT SHIPPED 4.7, AND THAT IS NOW WRONG. ** This row told Nic 'ship 4.7 unattributed
from: magnet-thank-you redesign session 2026-08-03
NIC-47done
🔴 NIC-24 RELOCATED: YOUTUBE'S 3,802 SESSIONS FIRE **ZERO** CONVERSION EVENTS OF ANY KIND — AND THE CONVERSIONS ARE PILING UP UNDER (direct). CROSS-DOMAIN MEASUREMENT IS THE PRIME SUSPECT. Queried live GA4 3 Aug. By sessionSource over 28 days: youtube = ~3,802 sessions and NOT ONE form_start, form_submit, add_to_cart, purchase or any other lead-ish event — literally zero, not merely few. google = 10,364 sessions with 683 add_to_cart but also no form_start/form_submit. (direct) = 12,116 sessions carrying 248 form_start, 222 form_submit, 233 purchase, 197 add_to_cart — i.e. almost ALL the measured conversion behaviour on the property sits under (direct). Two further smells in the same data: (direct) shows 12,263 sessions against 11,456 conversions, and the single largest hostName bucket is '(not set)' at 11,289 sessions, ahead of themovementathlete.com itself. LEADING HYPOTHESIS (not yet proven): cross-domain measurement in GTM-TKGDZR9 is incomplete across themovementathlete.com → quiz. → checkout. → app., so the converting session is orphaned into a NEW (direct) session and the original channel is lost at the moment of conversion. This is the opposite of what I first proposed — attribution is NOT being lost on the inbound link (YouTube arrives correctly tagged, yt_description 1,847 + yt_magnet 1,712 both youtube/organic); it is being lost at the CONVERSION hop. WHAT TO CHECK, IN ORDER: (1) GTM cross-domain 'configure your domains' list — must include all five hosts plus bodyweighttrainingarena.com; (2) that the _gl linker actually appears on links between them; (3) referral exclusions for the same hosts; (4) why hostName is '(not set)' on 11,289 sessions. Ties to NIC-42 (one definition of a lead) — a lead cannot be defined consistently while the converting session is orphaned.
impact 5/5 · 60 min
✅ ANSWERED IN-SESSION 3 Aug — no Nic time needed. The 3,802 'YouTube sessions/28d' are NOT visitors. They are link-validation crawlers triggered by OUR OWN bulk description edits. Verified 5 independent ways against live GA4 (property 177471782): (1) DEVICE — YouTube sessions are 0.21% Apple vs 37% si
from: GA4 Data API, property 177471782, 28d, queried live 3 Aug — sessionSource x eventName, sessionSource/Medium, and hostName breakdowns
NIC-48done
📺 YOUTUBE (TMA main channel): IS THE TRAFFIC HUMAN? — 3,802 sessions/28d at 1.13 pages/session and 1.2% engaged, with ZERO form_start / generate_lead / add_to_cart events. Attribution is NOT broken (yt_description 1,847 + yt_magnet 1,712 land correctly tagged) — the traffic simply does not stick. Two candidates, untested: YouTube in-app webview (no cookie persistence, instant bounce) vs bot traffic. Segment YouTube sessions by device/browser/webview and compare engaged-session rate against site baseline. This BLOCKS the whole YouTube conversion subfleet: no CTA experiment can be trusted until it is answered, because a test on bounces returns a real-looking number that is noise.
impact 5/5 · 45 min
✅ MERGED INTO NIC-47 same day — duplicate. Two sessions raised the same investigation with different hypotheses: NIC-47 suspected cross-domain measurement; NIC-48 suspected traffic validity (webview vs bots). The GA4 ground truth doc (GA4_PROPERTY_GROUND_TRUTH_2026-08-03.md) settles the framing: cross
from: GA4_PROPERTY_GROUND_TRUTH_2026-08-03.md §5 · YT conversion subfleet rule zero
NIC-49cancelled
GATE SELFTEST — delete me
impact 1/5 · 1 min
✅ Self-test row for the CHECK-BEFORE-YOU-ASK gate in queue_edit.py — verified the gate refuses an Aga-facing add without --checked, and that the nic side is unaffected. No real work.
from: selftest
NIC-34done
PURCHASE SIGNAL — SERVER-SIDE CAPI + PIXEL-LOAD PROOF (from revenue-plan tasks 11+29-check) — browser half shipped (/static/checkouts_v3/analytics.js fires purchase + GA4). Remaining: (1) server-side Meta CAPI purchase on payment_intent.succeeded webhook with event_id dedup against the browser event; (2) 5-min proof the Meta pixel actually loads on checkouts-v3 pages (Pixel Helper → GTM-TKGDZR9); (3) Estate B (quiz checkout) parity — same purchase event contract. SCOPE: both estates (Aga 2 Aug).
impact 4/5 · 4.0h
✅ CLOSED 6 Aug 2026. Nic shipped the quiz-side browser fbq Purchase with eventID = PaymentIntent id, deduping against the server CAPI webhook at /payments/analytics/stripe-purchase/ that already existed. PROOF: two Meta Pixel Helper screenshots sent to Aga on Slack, captured on app-test under Stripe t
from: TMA-REVENUE-STORY-AND-PLAN-2026-07.html#plan task 11 · folded 2 Aug 2026
NIC-33cancelled
🔴 REBUILD ABANDONED-CART CAPTURE ON THE PAYMENT-INTENT FLOW (from revenue-plan task 10) — the old Stripe checkout-abandoned webhook is obsolete: checkouts-v3 and the quiz checkout create PaymentIntents/Subscriptions directly, no Checkout Session ever exists for it to listen to (it fired 0 contacts in 46 days). VERIFIED 2 Aug: the quiz estate already creates a Customer WITH email at handoff (live specimen: quiz_checkout sub 'incomplete', email present, persona/path/plan metadata) — so typed-email abandoners already exist as reachable Stripe customers on that estate; v3 gets the same via NIC-19 change 2 (stamp-abandon-email → buyer_email + Customer.modify). Build: nightly (or webhook payment_intent.created/canceled) export of incomplete/abandoned records WITH email → AC abandonment lane. Purchase exclusion evaluated at SEND time.
impact 5/5 · 2.0h
✅ RETIRED BY AGA 7 Aug 2026 ('this has to be removed'). Verified against the artefacts the same day, not against intent: (a) the quiz-side capture half is DONE — NIC-54 shipped, five NEW quiz-* stage tags carry real contacts per live census (quiz_lead_signal_check.py exit 0, 5 contacts on NEW quiz-rea
from: TMA-REVENUE-STORY-AND-PLAN-2026-07.html#plan task 10 · folded into the brief 2 Aug 2026
NIC-35superseded
QUIZ PAYWALL → ON-PAGE CHECKOUT (port the v3 contract) + MAKE THE DEAD BUY-BUTTON SPEAK (from revenue-plan tasks 28+19) — 604 buy-clicks produced 248 subscription starts; the redirect to app.themovementathlete.com is where the rest die. Mount Stripe Elements under the tiers exactly as checkouts-v3 does (attribution.js + create/update-payment-intent contract — after NIC-19/NIC-16 land, porting inherits their fixes). Inside the same build: the handler branch where the Buy button silently no-ops for unmatched plans must emit an analytics event + visible error instead of nothing. VERIFIED 2 Aug: /api/quiz/config now returns 200 (task-8 gate cleared — it was 404 in the 27 Jul read).
impact 5/5 · 7.0h
✅ RETIRED AS SUPERSEDED — Aga ruling 3 Aug 2026 ('yes do that now'). The task was written 29 Jul against the OLD flow (paywall → /register/), where 604 buy-clicks → 248 starts. The 31 Jul quiz-checkout ship replaced that flow. VERIFIED live 3 Aug: the quiz bundle contains zero Stripe code (no js.strip
from: TMA-REVENUE-STORY-AND-PLAN-2026-07.html#plan tasks 28+19 · folded 2 Aug 2026
NIC-36moved-to-aga
MAKE A FREE TEST ACCOUNT so the free-member experience can be audited (from revenue-plan task 33) — 10 min: create a throwaway free account, send credentials to Aga/Claude. Unblocks the upgrade-path audit (what a free member sees at the paywall, which upgrade CTAs exist, whether the .99 step-down is reachable).
impact 3/5 · 10 min
✅ Moved to Aga 3 Aug on her ruling. Now tracked as AGA-66. Removed from Nic's open list; shown on his brief under 'Closed or moved' with the NIC pill stripped so nic_brief_sync_check does not flag it STALE.
from: TMA-REVENUE-STORY-AND-PLAN-2026-07.html#plan task 33 · folded 2 Aug 2026
NIC-30done
🔴 TEST DOMAIN IN THE LIVE QUIZ BUNDLE — moved here from NIC-22 after an audit-confirmed ID COLLISION (TRUST_AUDIT_2026-07-31 F1: two sessions allocated NIC-22 for different tasks). THE DEFECT, still live (auditor re-verified during the audit): the deployed bundle index-CEOCAHYQ.js carries 7 hits for "app-test" incl. VITE_REGISTER_PAY_URL:"https://app-test.themovementathlete.com/register/yearly_2026/" (the FALLBACK register route — a real buyer who hits it pays on a test domain) + the same fallback block still sends addToCartValue: 0 on monthly_trial. Cards: attribution brief #quiz-bundle-defects · analytics brief task-36. ACCEPTANCE (a command, never intent): curl the deployed bundle, grep — zero "app-test" hits and no addToCartValue: 0.
impact 4/5 · 20 min
✅ VERIFIED LIVE 4 Aug 2026 by curling the SERVED bundle. Hash moved index-CEOCAHYQ.js -> index-DFwXN-qe.js (1,339,617 B) so the rebuild is real, not a claim. 'app-test' 0 occurrences (was 7). VITE_REGISTER_PAY_URL now https://app.themovementathlete.com/register/yearly3/ (prod). Bonus verified in the s
from: docs/company/GOVERNANCE/TRUST_AUDIT_2026-07-31.md (F1) + https://tma-nic-attribution-brief.netlify.app#quiz-bundle-defects
NIC-21cancelled
🔴 A BUYER MUST NEVER RECEIVE AN ABANDONMENT EMAIL — confirm paying customers are excluded from the cart-abandonment lane; someone who pays gets the BUYER WELCOME, never "you left something behind". Raised by Aga 31 Jul on the back of the new quiz checkout going live the same day. Why now: the checkout shipped and the abandonment capture is being rebuilt on the v3 payment-intent flow in the SAME week — that is exactly how a paying customer gets mailed "you didn't finish" on day one. Steps: (1) name the live lane (756/757 Abandoned Cart Reminder were dormant — the Stripe checkout-abandoned webhook had fired 0 contacts in 46 days; if nothing is live, say so and this closes in one line); (2) confirm the purchase exclusion is evaluated AT SEND TIME, not only at entry — entry-only is the trap, buy 20 min after enrolling and the mail still goes; (3) test end to end with a real/coupon-zeroed purchase from the new checkout; (4) check the reverse — a genuine abandoner still gets the mail. DO NOT arm the abandonment lane until this is proven.
impact 4/5 · 30 min
✅ Removed from Nic's task list by Aga, 2 Aug 2026 (mid-session ruling: 'remove this from tasks list'). Task withdrawn — abandonment-lane purchase-exclusion check no longer tracked as Nic work.
from: docs/company/ANALYTICS/TMA-REVENUE-STORY-AND-PLAN-2026-07.html#plan (task 34)
NIC-23folded
🔴 EMAIL IS DARK BECAUSE STRIPE NEVER RECORDS THE EMAIL SOURCE AT PURCHASE — the backend half of the email attribution fix. Half is DONE (Aga, 31 Jul): AC links carried no UTMs at all (1,036 of 1,080 across 19 of 20 live automations; AC's click redirect drops the referrer so every click booked as (direct) and the GA4 email lane read 0 leads/28d) — 854 links across 237 emails rewritten live, tagging 4.1%→83.2%. THE HALF THAT IS NIC'S: even with a perfectly tagged click, nothing writes the email source into Stripe at the moment of purchase, so an email-driven sale stays unattributable. Detail = Analytics Master Brief tasks 27 (persist email utm_* + recipient into Stripe at checkout) + 17 (last-funnel-step export). This is the EMAIL fleet's 90-day WIRING GATE, due 29 Aug. ACCEPTANCE: a test purchase from a tagged email lands in Stripe with utm_source + recipient in metadata, and the revenue pull splits by source.
impact 5/5 · 4.0h
from: docs/company/GOVERNANCE/FLEET_CHARTERS.json (P2-email goal_90d) + https://tma-nic-attribution-brief.netlify.app
NIC-24superseded
🔴 YOUTUBE: 3,695 SESSIONS/28d, ZERO ATTRIBUTED LEADS — DIAGNOSE BEFORE BUILDING. Sessions attribute perfectly (yt_status.json), descriptions 100% UTM-tagged, YPP accepted; what is zero is generate_lead CREDITED TO YOUTUBE, in every window ever measured. TWO SUSPECTS needing OPPOSITE work: (A) attribution loss — the links viewers actually click (older descriptions, pinned comments, end-screen cards) drop UTMs at the quiz. subdomain hop, so YouTube leads exist but land in the 44.7% dark bucket (same class of bug fixed for content pages 29 Jul); (B) the traffic genuinely does not convert, and it is a CTA/content problem no wiring date fixes. THE 10-MINUTE CHECK THAT SEPARATES THEM, RUN FIRST: in GA4, do generate_lead events fired by youtube-referred sessions exist under a DIFFERENT source label (dark/direct) in the same window? Yes→A, No→B. This is the YOUTUBE fleet's 90-day gate, due 22 Aug; fuller card = Analytics Master Brief task 35. ACCEPTANCE: the verdict written down with GA4 evidence, and either a tagged YouTube click produces a lead carrying utm_source=youtube, or the fleet goal is formally re-set as a CTA experiment.
impact 5/5 · 60 min
✅ SUPERSEDED BY NIC-47 (Aga's ruling, 3 Aug) — and its acceptance test was actually RUN and ANSWERED the same day. NIC-24 specified one decisive 10-minute check: 'in GA4, do generate_lead events fired by youtube-referred sessions exist under a DIFFERENT source label (dark/direct) in the same window? Y
from: docs/company/GOVERNANCE/FLEET_CHARTERS.json (P4-youtube goal_90d) + https://tma-nic-attribution-brief.netlify.app
NIC-16done
🔴 CARRY THE UTMs INTO THE CHECKOUT — the last broken link between a lead and a sale. VERIFIED 30 Jul by reading the live checkout end-to-end: the shim is served (/static/checkouts_v3/attribution.js 200), stripe-client.js posts attribution:getAttribution() to create-payment-intent, and the server DOES write it into Stripe metadata — all three legs work. But across the last 30 checkouts-v3 payment intents the attribution object is ALWAYS {first_seen, landing, referrer:null, page, ua_webview} with NO utm_source / utm_campaign / gclid. Cause: the shim can only read UTMs off the checkout page's own URL, and visitors arrive there with a bare URL because the quiz/paywall link doesn't forward the query string. PROOF it isn't the shim: the older /v2_checkout/ estate, where paid ads land directly on checkout, stamps utm_source=google + utm_campaign + gclid + landing_path perfectly. FIX: forward the incoming query string onto the /payments/checkouts-v3/ link (same pattern as Fix A). ACCEPTANCE: one test purchase with ?utm_source=nictest through the quiz -> the Stripe payment intent's attribution contains utm_source:'nictest'. Claude verifies in a minute.
impact 5/5 · 45 min
✅ CLOSED 4 Aug 2026. Nic ran the acceptance test FIRST as the audit demanded (verify-before-build): tagged checkout links do write into Stripe, proving legs 1+2 end-to-end in production. He then shipped the one genuine residual the audit named (section 3): first-touch rehydrate on the quiz->checkout h
from: 30 Jul live checkout review + Stripe metadata read; brief https://tma-nic-attribution-brief.netlify.app
NIC-19done
🔴 MAKE THE BUYER VISIBLE TO THE MACHINES — five small changes in checkouts-v3 (create-payment-intent / update-payment-intent). ⚠️ ADDED BY A PARALLEL SESSION 30 Jul eve, captured here so the queue stays the superset — full detail lives on the brief. 2 of the 5 original asks were already built by Nic ✅: the metadata layer works (plan_slug · offer · coupon · conversion_value_usd · first-touch attribution all arriving as designed) and buyer identity at pay-click works end-to-end, verified on a live completed trial. Overlaps NIC-16 (carry the UTMs into the checkout) — treat as one visit, not two.
impact 5/5 · 2.0h
✅ VERIFIED DONE against the live artifact 7 Aug 2026 PM — all five changes shipped and the last residual is fixed. Changes 1 (defer-mint, email-blur — and its effect measured: zero phantom v3 mints since 5 Aug), 4 (/lead route + phantom sweep 40/40 + success-page gate) and 5 (pe= recipient into the in
from: https://tma-nic-attribution-brief.netlify.app (parallel session, 30 Jul eve)
NIC-17moved-to-aga
🔴 WP ENGINE SECURITY DEBT — 69 outdated plugins across 3 environments + 11 outdated themes (portal, 30 Jul). Outdated plugins are the #1 WordPress attack vector and this is NOT hypothetical: BWA was already compromised once (crypto spam posts, rogue admin, an old owner's recovery email still on admin accounts — found and cleaned 22 Jul). ASK IS TRIAGE, NOT 69 UPDATES: which are (a) security releases, (b) on LIVE environments rather than dev/staging, (c) touching Divi/Yoast/Redirection/WP Rocket (the ones that break the site if they go wrong). Update the security ones, schedule the rest. ⚠️ Note a live WP Engine platform incident today (404s on custom pages + wp-admin login problems) — rule it out before diagnosing any 404/login issue as ours.
impact 4/5 · 45 min
✅ ASSIGNED 31 Jul — Aga passed this to the WP external dev (the same person taking NIC-14 PHP 7.4→8.2). Off Nic's plate. Jira-ready ticket: marketing/seo/tickets/TICKET_wp_security_and_site_level.md
from: WP Engine dashboard 30 Jul + brief https://tma-nic-attribution-brief.netlify.app
NIC-20done
🔴 VERIFY THE GENERATED PASSWORD ACTUALLY LOGS IN — 15-minute gate on the post-purchase welcome email. ⚠️ ADDED BY A PARALLEL SESSION 30 Jul eve. The new welcome email shows the buyer their username + a generated password in a credentials box. Before ANY real send: buy on a test card, take the password from the email, confirm it logs into the app, and confirm the %EMAIL%/%PASSWORD% SendGrid substitutions are mapped. Rendered email: tma-paywall-spec.netlify.app/email-welcome-noquiz. 🔴 Nothing sends until this passes — a welcome email handing every new buyer a password that doesn't work would be the worst possible first impression.
impact 5/5 · 15 min
✅ Aga confirmed done, 2 Aug 2026 ('this is done'): generated password logs into the app; welcome-email credential gate passed. Closed on Aga's confirmation of the test.
from: https://tma-nic-attribution-brief.netlify.app (parallel session, 30 Jul eve)
NIC-15done
🍎 IN-APP REVIEW PROMPT — ⚠️ CORRECTED 30 Jul by Aga: it ALREADY EXISTS and fires after 3 completed workouts. So this is a TUNING question, not a build. The open number: what share of installs actually reach workout 3? We have 297 App Store ratings vs Thenx's 12,273 (41x) and store ranking is ratings-weighted, so if a prompt is live and we're still at 297, something upstream is throttling it — most likely the gate sits behind an event most users never reach. Apple allows 3 prompts/365 days, so there is headroom to trigger earlier or add a second trigger. NO CHANGE REQUESTED YET — just the reach-workout-3 number, then we re-rank.
impact 4/5 · 15 min
✅ DONE 31 Jul — Aga confirmed it is handled: the Jira ticket is raised and with the RN team (AGA-39). Off both Nic's and Aga's plate. The one figure still owed back from the RN team: what share of installs ever reach workout 3 — the gate the existing review prompt sits behind. That answer, when it com
from: master §13.4 A6 (corrected) + Aga 30 Jul
NIC-07done
✅ P10 front-lever cluster — DONE 12 Aug 2026, but the row's premise was STALE: the 3 source pages were ALREADY HARD-DELETED and 404ing, not live.
impact 3/5 · 30 min
⛔ blocked on: Aga approving the front-lever keyword map — Nic must NOT start; surfacing it as a 'do today' item contradicts the brief.
✅ DONE 12 Aug 2026. 🔴 THE ROW WAS 5 DAYS STALE AND WRONG IN THE DIRECTION THAT MATTERS. It said all 4 URLs were still 200 and framed this as a live-page merge. Measured today: /improve-front-lever/, /how-to-do-a-front-lever/ and /front-lever-variation/ all return 404 (confirmed with Cache-Control head
from: marketing/seo/TMA_SEO_GEO_ANALYSIS_AND_PLAN_2026-07-20.md §12.6 P10 (T2.4)
NIC-01done
S1 REDIRECT PACK — the #1 blocker: quiz-root 302 + $is_args$args · trailing-slash redirects (TMA + BWA) forward the query string · doorway quiz CTA on [/calisthenics-assessment/](https://themovementathlete.com/calisthenics-assessment/) forwards location.search · GA4 'Configure your domains' present on EVERY stream. Acceptance: tools/analytics/check_link_integrity.sh → 0 dropping, exit 0
impact 5/5 · 45 min
✅ ✅ CLOSED 29 Jul — Nic shipped Fix A (quiz-root redirect on the EC2 box now forwards the query string). VERIFIED live: utm_source, utm_content AND _gl all survive the hop to /2026-v2-2/. Guard moved 18 preserved/4 dropping → 19/1. The remaining 3 items were re-scoped and are NOT Nic's: (a) front-leve
from: docs/company/ANALYTICS/NIC_BRIEF_ATTRIBUTION_REDIRECTS_2026-07-23.md
NIC-02done
llms.txt files ONLY (needs real file upload — TESTED: a .txt path never reaches PHP on either host, so no snippet/plugin can serve it; our attempt 404'd and auto-rolled back). Create on TMA (404 today); on BWA DELETE the stale AIOSEO static file and upload the replacement (its first entry dumps raw Divi shortcodes at any LLM). Both bodies written out in NIC_SCHEMA_INSTALL_PACK §4a/§4b.
impact 4/5 · 10 min
✅ DONE by Claude 27 Jul — both sites are WP ENGINE. A route-based snippet 404s (nginx serves .txt statically), but a one-shot snippet CAN write a physical file to ABSPATH, which nginx then serves. TMA /llms.txt created (was 404); BWA's stale AIOSEO file OVERWRITTEN (0 'All in One SEO', 0 Divi shortcod
from: marketing/seo/TMA_SEO_GEO_ANALYSIS_AND_PLAN_2026-07-20.md §12.6 P8 (T2.6)
NIC-04moved-to-aga
BWA speed #3 — enable next-gen image formats (WebP/AVIF): ShortPixel/Imagify bulk-convert + WP Rocket WebP compatibility (30-50% off every image byte)
impact 3/5 · 15 min
✅ MOVED 27 Jul to AGA-09 — dashboard clicks, no server access required.
from: marketing/seo/NIC_BRIEF_bwa_site_speed_and_schema_2026-07-23.md #3
NIC-05moved-to-aga
BWA schema #6 — homepage Organization schema: Yoast Site representation = The Movement Athlete (url + sameAs) + visible 'A The Movement Athlete publication' footer line linking to themovementathlete.com/about
impact 3/5 · 15 min
✅ MOVED 27 Jul to AGA-06 — dashboard clicks, no server access required.
from: marketing/seo/NIC_BRIEF_bwa_site_speed_and_schema_2026-07-23.md #6
NIC-03cancelled
BWA speed #2 — remove the two hardcoded Google-CDN jQuery copies (3.1.0 + 3.6.0) + dead lpcontent.net leadbars/Pinterest embeds, then enable WP Rocket Delay-JS with safe exclusions
impact 3/5 · 30 min
✅ CANCELLED 27 Jul — the 'duplicate jQuery' finding was FALSE: both Google-CDN copies sit inside the Simple Custom CSS & JS plugin's default instructional COMMENT and never load; the LeadPages leadbar is commented out too. Comment-stripped DOM = 1 real jQuery (WP core). Nothing to do.
from: marketing/seo/NIC_BRIEF_bwa_site_speed_and_schema_2026-07-23.md #2
NIC-06moved-to-aga
GA4 key-event config — unmark session_start as key event; mark generate_lead (+ landed_on_pricing_page) so organic→lead is a reportable conversion in standard reports
impact 3/5 · 30 min
✅ MOVED 27 Jul to AGA-08 — dashboard clicks, no server access required.
from: marketing/seo/TMA_SEO_GEO_ANALYSIS_AND_PLAN_2026-07-20.md §11 task 0.2
NIC-08done
BWA a11y #7 — restore pinch-zoom: Divi child-theme viewport override dropping user-scalable=0 + maximum-scale (settings change did NOT take; Divi prints it in parent header.php)
impact 2/5 · 10 min
✅ DONE by Claude 27 Jul — BWA has NO child theme, but Code Snippets is REST-writable: shipped snippet id 7 'BWA — restore pinch-zoom (WCAG 1.4.4)' (wp_head, rewrites the viewport tag at parse time). Verified live: override renders, homepage + 2 articles all 200.
from: marketing/seo/NIC_BRIEF_bwa_site_speed_and_schema_2026-07-23.md #7
NIC-09moved-to-aga
Repair or trash post [/g/](https://themovementathlete.com/g/) (ID 210957) — 500s on every render AND breaks wp/v2/posts page-5 listing (trashing is fine, thin testimonial, no traffic)
impact 2/5 · 10 min
✅ RE-SCOPED 29 Jul — NOT blocked on WP-CLI. Isolated: ONLY the `content` field 500s; id/status/slug/date/title/Yoast-meta all read fine over REST. The wp-admin Posts LIST renders title/author/date only, so that screen loads and the post can be trashed without server access; Claude can also set status=
from: marketing/seo/tma-phase0/NIC_TICKET_g-post-500_2026-07-24.md
NIC-10folded
Chrome _gl follow-up (P15) — verify the nav/footer quiz CTAs get GA4 cross-domain linker decoration (_gl) on click on both sites; fix the tag config if not
impact 2/5 · 15 min
✅ FOLDED 27 Jul into NIC-01 as 'Fix E' (GA4 Configure-your-domains on every stream) — same visit, same acceptance test.
from: marketing/seo/TMA_SEO_GEO_ANALYSIS_AND_PLAN_2026-07-20.md §12.6 P15
NIC-13cancelled
Un-freeze the modified date on TMA (decision ③) — theme/plugin pins stale dates; enable true lastmod so freshness signals (and P8's sitemap lastmod) are honest
impact 2/5 · 15 min
✅ CANCELLED 27 Jul — dateModified is NOT frozen: verified current on every edited page (home 07-24T03:26, best-cal-program 07-24T18:30, BWA flagship 07-24T15:59) and sitemap lastmod matches. The real issue was stale VISIBLE bylines in copy = content lane (STALE_BYLINE lint + light-pass queue).
from: marketing/seo/TMA_SEO_GEO_ANALYSIS_AND_PLAN_2026-07-20.md §7 decision ③ + §12.6 P8
NIC-11moved-to-aga
IndexNow/Bing key setup (decision ③) — mint + host the IndexNow key on both domains so every ship gets pinged to Bing (~87% of ChatGPT citations come via Bing's index)
impact 2/5 · 20 min
✅ MOVED 27 Jul to AGA-07 — dashboard clicks, no server access required.
from: marketing/seo/TMA_SEO_GEO_ANALYSIS_AND_PLAN_2026-07-20.md changelog 24 Jul (P5 row, 'IndexNow ping SKIPPED') + §7 decision ③
NIC-12done
P11 index-hygiene 301s — canonical /blog/ (301 /blogs/ + /blog-new/), trash the test pages, 301 the 14 dead promos (Claude delivers the backlink-checked list; Aga flags any promo with recent traffic first)
impact 2/5 · 30 min
⛔ blocked on: Claude's backlink-checked list + Aga flagging any promo still getting traffic. Nic must NOT start.
✅ VERIFIED DONE against the live artifact 7 Aug 2026 PM — the work no longer exists for Nic. All 20 'still needs action' URLs from NIC83_BLOG_HYGIENE_EVIDENCE_2026-08-06.md re-crawled live: 19 return one-hop 301s to 200 targets (incl. /blogs/ → /blog/, /home-page-test/ → /, /70-off-new-aug24/ → /start
from: marketing/seo/TMA_SEO_GEO_ANALYSIS_AND_PLAN_2026-07-20.md §12.6 P11 (T2.5)

Reference — the original attribution brief

Preserved verbatim. Nothing was removed when this became the to-do list.

Open the full attribution brief

The Movement Athlete · Attribution & tracking

Closed. The attribution brief is empty — every card verified against the live system, not against the message.

Updated 7 Aug 2026 · CLOSED — 0 open for Nic, 0 P0 · The five cards that were open this morning were all confirmed shipped on 7 Aug 2026, 19:24–19:28 UTC: the Meta Marketing API for the ad repoint, individually re-fetched Stripe subscription ids for the unbillable actives, a re-run of the four-page 50%-off crawl, curl -I on every host and app route, and the served /register/ HTML plus the live quiz bundle for the attribution stamps · 1 item still being watched by us, not by Nic (the server-side PaymentIntent stamp, which needs one post-deploy sale to prove) · 1 decision with Aga

Nic — that is the whole board cleared, and we checked every one of them ourselves before writing it here. The Meta Variant 2 ads are on the quiz root (read off the Marketing API across 200 ads — zero active creatives still reference /2026-v2-2/); all five unbillable v3 subscriptions read canceled when re-fetched by id; the checkout- prefix is gone from all four 50%-off pages and the old broken path still 404s, which is how we know it was removed rather than redirected around; and quiz-test, monitor and grafana-dev all serve X-Robots-Tag: noindex, nofollow. ✅ UPDATE 10 Aug 2026 — the one open question on this board is now answered. On 7 Aug two of the three attribution writes were confirmed in live code and the third — the checkout_version stamp on the PaymentIntent — could not be measured, because the last sale of that day landed about eight hours before you deployed. Eighteen sales later, the checker has run: your quiz-rail write is CONFIRMED, 6 of 6. Every completed sale through the quiz checkout now carries checkout_version, quiz_variant and the full UTM set (including gclid) on the PaymentIntent itself — and that stamp is what let us tell Aga, on 10 Aug, exactly which sales paid advertising created. The legacy /register/ PaymentIntents are still completely bare (0 of 12), so the "legacy money cannot be split by funnel" problem is unchanged. Nothing on this is being handed back to you. Two things stay as they are on purpose: /2026/ is the Control arm, and /2026-v2-2/ stays live as a rollback with no ads feeding it. Click any row for the detail; nothing here needs another document.

✅ Shipped 3–7 Aug — thank you, and here is the proof we read20

Every row below was verified against the system your server is actually running — not against your message. That is the standard, and you have now passed it twelve times. 🔴 The two headlines: pe= closed the last leg of email-revenue attribution (that KPI was reported DARK on the fleet scoreboard with a wiring deadline of 29 Aug — you cleared it 25 days early), and the SPF fix took our own hello@ mail off a permerror under p=reject, where only a DKIM key was standing between us and rejecting our own email. Two consequences to know before anyone reads a dashboard: phantom trial counts collapse and conversion rates jump — both BY DESIGN (clean baselines start 4 Aug, honest read ~11 Aug), and one instruction we gave you was stale — see the star-rating row.

Open for you0

Nothing. This board is empty — for the first time since it was created.
Every card that was open this morning is now in Shipped above, each with the check we ran against the live system written on it. One thing is still being watched, and it is ours, not yours: the checkout_version stamp on the PaymentIntent is server-side, so it can only be proven by a sale created after your deploy — and there had not been one yet when we checked. We built a checker for it. If it ever comes back negative you will hear from us with the evidence attached; silence means it landed.

📦 Folded — merged into a bigger task1

Nothing to do here — this is the record of a task that was merged, not dropped. Aga's call, 6 Aug: everything you send us about a lead should be one task in one sitting, because it is the same file and the same helper each time. The need is real and confirmed live; the old instruction was wrong about where to put it — see the card.

Parked — listed so nothing is invisible1

Assigned to you and not actionable yet. Do not start them until the prerequisite clears and we tell you.

Closed or moved21

Context — the time split, what changed since 27 Jul, and why you're getting this page

The split

~1.5–2h · Nic
Task 1 — three changes now, one sitting: phantoms · debris+bugs · the email-source field-add. Abandoner capture and provisioning closed 3 Aug — already built.
10m test, then ~45m · Nic
Task 2 — run the ?utm_source=nictest acceptance FIRST; the hand-off already forwards the query string, so the fix is first-touch rehydrate for bare landings
2 min · Aga
Delete the /calisthenics-assessment/ Redirection rule (AGA-54) — the JS fix is already live, this completes it
What changed on 2 Aug: a deep verification pass re-checked every claim on this page against the live systems — read-only Stripe scans (both estates), the GA4 Data API, cache-busted fetches of the shipped quiz bundle, and live probes of /lead, RETURN_URL, the success page and /api/quiz/config. Aga's rulings the same day: the abandonment-email check removed (its substance lives in task 4's acceptance tests) · the password gate closed as done · the WP Engine estate moved to Aga (AGA-53) · the email-source join folded into task 1 · every checkout fix scoped to both new estates. Done for you meanwhile: the YouTube diagnosis (task 7) and the assessment-doorway UTM regression (found, fixed with a guarded snippet, verified). The open revenue-plan checkout tasks (8 · 10 · 11 · 19 · 28 · 29 · 30 · 33) were verified one by one and are folded into this page — task 8 turned out already fixed, the rest are tasks 3–8 here. This page is the single home for Nic's work; the source of truth behind it is the tracked queue, and the two are gate-checked against each other.

3 Aug — a code audit of the LIVE repos: everything above was probed from the outside or read from an Apr-2026 clone. On 3 Aug the actual repos (MovementAthlete-Django, MovementAthlete-React-ProdTemp) were audited, and four asks turned out to be already built by you: the typed-email abandoner stamp (task 1, change 2), buyer provisioning (provision_buyer_from_intent, change 3), the UTM write into Stripe metadata on both estates (change 5's premise), and the entire server-side Meta CAPI webhook (task 5). Three diagnoses were corrected — RETURN_URL is proxy-TLS not a source typo, the success page defect is UI/analytics only because provisioning already refuses bad intents, and addToCartValue:0 is on 1wk_trial not monthly_trial. One new constraint appears twice: removing a page-load mint must not remove the capture riding on it (stampPaywallView on quiz, stamp-abandon-email on v3). Also on 3 Aug: Aga deleted the doorway Redirection rule — the UTM regression is verified closed (guard 19 preserved · 0 dropping) — and took the abandonment-lane rebuild herself (AGA-57) and the magnet/YouTube wiring (AGA-58). Later the same day the on-page checkout port was retired as superseded — the 31 Jul ship replaced its premise; the 14-Aug read decides any successor — and its one live fragment became task 4, the dead-button trace (same file as task 3).

Why you're getting this page — what we're about to build on your work

We're switching on personalised conversion email on top of the v3 data you built — trial-conversion (day-3 value, day-6 "your trial ends tomorrow" with the buyer's actual plan and price) · cart-abandon recovery · payment-recovery notes that name the real plan and coupon. Those machines read Stripe. Tasks 1–2 make what they read true and complete — that's why the detail below is worth your time. Everything on this page was live-verified twice, the second time by an independent adversarial pass instructed to refute it.

Aga's own list lives elsewhere now — her click-by-click steps are generated from the same tracked queue as this page, so the two can no longer disagree: Aga & Nic — the step-by-step list. This page is the home for Nic's tasks; anything assigned to Nic appears here. 🔒 Gate (Aga, 6 Aug 2026): nothing enters this brief without Aga's approval and full live verification — everything currently on it carries both.

The Meta Variant 2 ads — repointed to the quiz root, CLOSED 7 Aug 2026

✅ DONE verified 7 Aug 2026, 19:24–19:28 UTC via the Meta Marketing API, 200 ads scanned 2 ad destination URLs — both now on the root AGA-56

✅ Closed — both ads are on the quiz root, read off the Meta API

Nothing left to do on this card. On 7 Aug 2026, 19:24–19:28 UTC we pulled 200 ads from the Marketing API and resolved every destination on the active creatives. Both Variant 2 ads — 120245799998740728 and 120245799998720728 — now point at https://quiz.themovementathlete.com/, the ROOT, which is what makes them survive the next switch. No active creative references /2026-v2-2/ any more. The four ads still on /2026/ are the Control arm and are deliberate; we are recording that here so a future sweep does not re-open them as a defect.

Everything below this line is the original card, kept so the diagnosis and the reasoning stay on the record.

Recorded for everyone else

Your architecture note is now in our canonical docs, because it explains something that had confused us: Django renders the body itself (apps/mail.pyrender_to_string over activation_email.txt, password_reset_email.txt, quiz_email.html, weekly_progress_mail2.html) and ships it over SMTP — SendGrid only delivers and owns none of the copy. That is why Email Activity shows sends while Dynamic/Marketing templates look empty. We will stop looking for the copy in SendGrid.

✅ 6 Aug 2026 (PM): Aga reviewed this card and SENT THE TASK TO YOU — AGA-56 (her decision-and-dispatch half) is closed, and she ruled REPOINT. 7 Aug: most of it is already done. You shipped the root repoint and moved all Google Ads onto the $97 build — both verified live. One thing is left: the Meta “Variant 2” ads. See the next card.

📝 Your documentation of the three versions — recorded here so we all read the same thing

You sent this on 7 Aug 2026. It is captured verbatim below (left) with what the wire says the same day (right), so nothing depends on memory. Everything matches except one line, flagged in red.

What you documentedVerified live 7 Aug 2026
Root force-redirect changed — was quiz…//2026-v2-2/, now quiz…//2026-quiz-checkout-v1/curl -I: root returns 302 → /2026-quiz-checkout-v1/
1 · Current primary funnel. Landing /2026-quiz-checkout-v1/ · no React paywall, redirects to checkout · checkout app…/payments/quiz-checkout/ · success …/quiz-checkout/success✅ all three 200. Stripe confirms the rail: subscriptions carry checkout_version=quiz_checkout, quiz_variant=quiz_checkout_2026_v1, offer=quiz97
2 · /2026-v2-2/ — “still live (200), not redirected” · in-quiz paywall · pays via /register/monthly_2026/, /register/monthly_2026_promo/promo/, /register/quarterly_2026_promo/promo/ · success /payments/success_2026✅ 200, no redirect — your status line is right. (Your covering note also said “I simply redirect /2026-v2-2/ → v1”; that one is not live, and the status line is the accurate half.) ✅ Stripe agrees on the rail: the only prices selling there are monthly3, monthly2, quarterly2
3 · /2026/ Control · in-quiz paywall · pays via /register/yearly_2026/ · success /payments/success_2026 · served by QLG | ADV+ OFF | MC | Control, kept for its low CPLs✅ 200 · Stripe agrees: the only price selling on that rail is yearly3 · ✅ campaign ACTIVE, both its ads point at /2026/. Your URL map and Stripe's price map agree independently — that is the two-source check on this whole table.
“All Google Ads goes here now” (to v1)✅ Confirmed in GA4: all four Quiz Funnel campaigns land on v1 from wk31. v2-2's Google traffic went to ~0 on 31 Jul.
“Meta Ads QLG | ADV+ ON | US | Variant 2 goes here” (to v1) · and “due to the force redirect, Variant 2 no longer serving /2026-v2-2/🔴 Not yet. Read off the live creatives via the Marketing API on 7 Aug, the campaign is ACTIVE and both of its ads still have https://quiz.themovementathlete.com/2026-v2-2/ as their destination URL (ads 120245799998740728, 120245799998720728). The root redirect cannot catch them — they land one level below the root. GA4 confirms the consequence: 7–14 Meta sessions/day on v2-2 every day since the switch, flat, no decay, and $37.43 spent 1–6 Aug against a funnel whose rail has taken $0 in August.

So: the redirect did its job everywhere it could reach. Ad deep-links are the one place it structurally cannot — which is exactly what this card has been about since 6 Aug.

🥇 Why this outranks everything else on this page

To be precise about what IS working: the $97 page is live, the quiz ROOT is repointed to it (you shipped that), Google Ads is fully moved across, and it is SELLING — 8 paid subscriptions on the quiz-checkout rail since 31 Jul: 7 × $97.00 annual + 1 × $12.48 monthly, $691.48 gross (re-pulled 7 Aug PM; GA4 independently corroborated the first week). Everyone who arrives through the root — YouTube, PMax, direct, homepage — sees the $97 annual. The gap is only the paid deep-links: the ACTIVE Meta ads’ destination URLs (read from the ad creatives via the API, re-verified 7 Aug) are literally quiz.themovementathlete.com/2026/ and …/2026-v2-2/ — one level BELOW the root, so the root redirect never fires for them, and those paths still 200 with their own old bundles. Measured on human landings in the 7 days after the switch: 232 of 540 (43%) still reach a build with no $97 annual — 16% deliberately (the Control arm Aga is keeping) and 27% unintended (v2-2 + v2-3). The ad money sees the old offer; everyone else sees the new one.

📊 Full version-by-version analysis — traffic, sources, funnels, AOV, retention and lead quality across all three funnels, with every command to reproduce it: tma-growth-cockpit-2026.netlify.app/quiz-funnel-versions/ ↗

What each doorway is, and who feeds it — bundle identities verified live 6 Aug

PathWhat it is (from its served bundle)Who lands there (GA4, 14d landing sessions)
/2026/The ORIGINAL 2026 quiz build (bundle index-BqqEpqKj.js). Pays via 6 /register/ URLs · 0 checkouts-v3 refs · no $97 annual.317 — 80% from ONE ACTIVE Meta campaign: “QLG | ADV+ OFF | MC | Control” (ig 149 + fb 95 + threads 10).
/2026-v2-2/The v2-2 quiz build (index-DPkL72Wi.js). Same 6 /register/ URLs · 0 v3 · no $97 annual.691 — direct 219 · google organic 160 · “QLG | ADV+ ON | US | Variant 2” (ACTIVE) fb 87 + ig 22 · homepage 41 · PMax 32 · BWTA 18.
/2026-v2-3/The v2-3 build (index-DBgHnaAZ.js). Older /register/ set · no $97 annual.46 — incl. an ActiveCampaign email: 967 TMA | Welcome Sequence After LEAD MAGNET, E7 still links it (that fix is Aga-side).
/2026-quiz-checkout-v1/The $97 build (index-DFwXN-qe.js) — 12-Month $97 today / $157 struck, pays via the checkouts-v3 rail.3,157 — YouTube organic 1,197 · PMax · Competitors Branded · homepage · the quiz root.

The session counts above are the 6 Aug 14-day snapshot, kept for the record. The current post-switch picture is in the analysis doc linked above.

Where that leaves the old A/B: the Control arm (/2026/) stays live on purpose — Aga is keeping it, and it is the only funnel still selling the trial + 50%-off annual. The Variant 2 arm (/2026-v2-2/) is the one that is running on inertia: still taking paid traffic on the pre-$97 offer months after the test stopped being read. Repointing its two ads ends that, and leaves the page itself intact as the rollback option.

What is left — one campaign, two ads

AGA-56 is RULED and CLOSED (Aga, 6 Aug): repoint. Backport is off the table. Three of the four pieces are shipped; here is the whole board:

PieceStatus 7 AugWho
Root force-redirect → v1DONE — 302 verified liveYou — shipped
All Google Ads onto v1DONE — all four Quiz Funnel campaigns land on v1, GA4-verifiedYou — shipped
/2026/ Control ads stay putBY DESIGN — Aga is keeping this arm (cheap multi-country leads, and it is the only live seller of the trial+50% annual). Not a defect; leave it.No action
“QLG | ADV+ ON | US | Variant 2” ad destinations🔴 OPEN — both ads still read /2026-v2-2/~5 min. In Ads Manager set both ads’ website URL to https://quiz.themovementathlete.com/the ROOT, not the v1 path, so the ads follow every future switch automatically and never need touching again. Keep the existing url_tags as they are. Aga's ruling 7 Aug: this one is yours to decide and do — whether to repoint to the root, and when.

Done when: both ads in 120245799636960728 read the root as their destination, and GA4 Meta sessions on /2026-v2-2/ fall from ~7–14/day to ~0 within 48h. Leave the /2026-v2-2/ page itself serving — Aga wants the rollback option kept open, and once the ads move, its only remaining traffic is organic/direct residue. A 301 on it is a later decision, not this task.

Capture what the quiz already knows — SHIPPED 7 Aug 2026

DONE · verified live 7 Aug by ActiveCampaign census activecampaign_sync.py + the quiz bundle NIC-54

✅ Shipped — and here is exactly what we checked, so you know the standard

Nic — nothing left to do on this card. We did not take it on trust. We ran a live census of the ActiveCampaign API on 7 Aug and read real contacts, not config: all 15 NEW quiz-* tags exist and are being applied (email-captured 8 · questions-done 8 · gave-email 8 · results-seen 4 · reached-checkout 5 · card-started 1 · all-fundamentals-mastered 0, which is correct — nobody is 9/9 yet), and all 10 fields exist and are populating.

One real lead, so you can see it end to end — contact 378032, created 06:43 on 7 Aug: %TMA_QUIZ_PERSONA% = SKILL_SEEKER, %TMA_QUIZ_OBSTACLE% = CONFUSION, %TMA_QUIZ_AGE% = 18_24, %TMA_QUIZ_GENDER% = MALE, %TMA_QUIZ_LIFE_CONTEXT% = FLEXIBLE, %TMA_QUIZ_BACKGROUND% = GYM, %TMA_QUIZ_TRAINING_RECENCY% = CURRENT, %TMA_QUIZ_WEAKEST_FUNDAMENTAL% = Abs, and %TMA_QUIZ_FUTURE_STAKES% = “Never unlocking the skills I know are possible”. That last one is a sentence a human typed about their own life, and until today it died at the end of the quiz.

🔴 And the test that actually mattered: the AC tag total is still 5,845 — exactly the baseline we handed you. You put the values in fields and the state in tags, so no value leaked into a tag name and the 5,623-junk-tag pattern did not recur. A test that only asked “did the code run” would have passed through that bug; this one did not have to.

Re-runnable by anyone, any time: python3 tools/retention/quiz_lead_signal_check.py --sample 2 (read-only; exit 0 = healthy, and it fails loudly if the tag total ever climbs).

One thing we spotted and then closed ourselves — logged so you never inherit it: the paywall-view count briefly ran ahead of the results-screen count, which normally means a screen is failing to stamp. It was not. It was one contact (378018, 00:57) that carries the account tags and reached-checkout but none of the lead-write tags and none of the ten fields — it went through before the lead write was live, since the first complete one lands at 02:47. Every one of the eight leads since is clean and complete. Deploy boundary, not a defect, nothing for you to do.

And a correction to our own spec, not to your code: we told you quiz-email-captured fires "before any answer is submitted." That was wrong about this funnel — the email screen is #32, after the questions (1–31), so email-typed and answers-submitted are genuinely the same moment. The two tags landing in the same second, on 8 of 8 leads, is correct behaviour. We have fixed the spec our end.

The specification below is left in place as the record of what was built — and the tag/field law in it stands permanently for anything that writes to ActiveCampaign next.

🔴 THE LAW THIS SHIPPED UNDER — kept because it binds everything written to ActiveCampaign from here on (historical: it reversed an earlier instruction, and you followed the corrected one)

Do NOT use ActiveCampaignAPIHelper.add_tag_to_quiz_contact() for these tags. The quiz-funnel email master told you to ("same one-line pattern, five places"). That instruction is withdrawn. Here is what it actually does — app/apps/users/helpers.py:90:

self.add_tag_to_contact_by_id(contact_id, f'{tag_name}: {tag_value}')
# the VALUE is concatenated into the tag NAME

That is the machine that built our tag mess. Measured live 6 Aug: ActiveCampaign holds 5,845 tags, and 5,623 of them (96.4%) are one pattern — "Gender: Female; Age: 10-19; Fitness Goal: …". Three pieces of information became 5,623 tags. Each one describes a single person (we sampled: 1.1 contacts per tag, max 2), so none of them can ever be used as a segment.

Passing a timestamp as tag_value would mint one unique tag per contact per stage — five new tags for every lead, forever. It also speaks the retired v1 API. ✅ The correct pattern is already in the file you are editing: activecampaign_sync._apply_tags() applies fixed tag names by id via POST /api/3/contactTags. Copy that.

The rule, so this never happens again

Goes in a…CarriesWhy
TAGstate — did this happen, yes/noFixed set. Segmentable. Never grows.
CUSTOM FIELDdata — the value itselfOne field, many values. Queryable and updatable.

If a tag's name would differ between two contacts, it is a field.

Why this matters — and why we are not putting it in Stripe

The quiz asks people four revealing things: what is standing in their way, what they fear if nothing changes — in their own words, their age, and their gender. It also works out whether they have mastered all nine fundamentals. Today every one of those dies at the end of the quiz. So a recovery email can say "you wanted to build muscle" and nothing more.

🔴 The earlier plan said to route this through Stripe. That was wrong, and here is why

The original task (NIC-45) told you to append these to the quiz→checkout hand-off URL so they would ride into Stripe metadata. Two reasons that fails:

1. Stripe only ever sees people who reach the checkout. Everyone who finishes the quiz and stops — the large majority, and exactly the audience a recovery email exists for — would leave nothing behind. We would be storing the data only for the people who least need chasing.

2. It welds the destination into the front end. The quiz is a React build that needs a rebuild and an EC2 deploy to change. Every future change of destination would mean another one.

What we are doing instead: the fields ride the quiz_lead message the quiz already sends, and Django decides where they go. That is one function, on a server we deploy freely.

✅ And this is what makes the Customer.io move nearly free

Today: that function writes to ActiveCampaign. At migration: add a second write to Customer.io in the same function, run both for a fortnight, then drop the AC line. You never open the quiz again. No rebuild, no redeploy, no re-testing the funnel. (For context: Customer.io cannot send at all today — its sending domain has no MX, SPF or DKIM and has never delivered a message. Nothing in this task depends on that being fixed.)

✅ Already done for you — the entire ActiveCampaign side

Created via the API on 6 Aug 2026 and re-confirmed present the same day. Do not create any of these; the ids are stable.

idTag — how far they gotFires when
63770NEW quiz-email-capturedthe email is typed — before any answer is submitted
63771NEW quiz-questions-doneanswers submitted
63772NEW quiz-results-seenthe results screen actually renders
63773NEW quiz-reached-checkoutthe paywall renders — where stampPaywallView already runs
63774NEW quiz-card-startedfirst touch on the card field
63775NEW quiz-all-fundamentals-masteredall 9 fundamentals assessed at level 4

🔴 The field list changed on 6 Aug — twice. This is the final set: 10.

Aga reviewed it and spotted that the goal itself was missing — the one thing that says what a person actually wants. That review turned up several more the quiz already collects and nobody stores.

Then she cut it back, and she was right to: "we might not need all of them — just whatever we will use for marketing." Five were dropped and deleted from ActiveCampaign — Dream Skill, Session Minutes, Frequency, Equipment, Injury Area. They configure the product; they do not change what an email says. The test every survivor passed: would this change a line of copy or a segment? If you see a reference to those five anywhere, it is stale.

idField — who they areMerge tagValue
61TMA Quiz Persona — THE GOAL%TMA_QUIZ_PERSONA%PAIN_FREE · MUSCLE_BUILDER · FAT_BURNER · SKILL_SEEKER · AGE_STRONG · STRENGTH_BUILDER · JUST_START · ALL_MASTERED — the field already exists; it is just never written at LEAD time, only after checkout
64TMA Quiz Obstacle%TMA_QUIZ_OBSTACLE%INJURY · PLATEAU · CONFUSION · CONSISTENCY · TIME_OBSTACLE
65TMA Quiz Future Stakes%TMA_QUIZ_FUTURE_STAKES%free text — their own words
66TMA Quiz Age%TMA_QUIZ_AGE%age band
67TMA Quiz Gender%TMA_QUIZ_GENDER%gender
68TMA Quiz All Fundamentals Mastered%TMA_QUIZ_ALL_FUNDAMENTALS_MASTERED%yes / no
69TMA Quiz Life Context%TMA_QUIZ_LIFE_CONTEXT%screen S5 "What best describes your day-to-day?" — desk work · physically demanding job · juggling family · unpredictable · flexible
70TMA Quiz Background%TMA_QUIZ_BACKGROUND%GYM · BODYWEIGHT · ATHLETIC · IRREGULAR · BEGINNER · INJURED
71TMA Quiz Training Recency%TMA_QUIZ_TRAINING_RECENCY%CURRENT · RECENT · LAPSED · MEDIUM_BREAK · LONG_BREAK · NEVER
77TMA Quiz Weakest Fundamental%TMA_QUIZ_WEAKEST_FUNDAMENTAL%weakest of the 9, from the assessment

🔑 LIFE_CONTEXT is an array — send [0], do not stringify it.

🔴 Naming matters here: it is "all fundamentals mastered" — never "already-elite" or "advanced". It is a measured outcome (every one of the nine fundamentals at level 4), not a description of the person.

🔴 Naming ruling (Aga, 6 Aug): every new-quiz tag is prefixed NEW quiz- — and that changes one thing in your code

All 15 tags carry the prefix so nobody ever mistakes them for the 5,623 legacy tags in the AC tag manager. Consequence: the names your Django code references (PATH_TAGS values and the "quiz-gave-email" literal) NO LONGER EXIST in AC. A name-based lookup would fail even after you fix the limit bug. Use the ids above — or if you keep name lookup, update the strings in the dict to the NEW quiz-* names in the same edit.

Plus 9 tags your CURRENT code already looks for — which did not exist

NEW quiz-gave-email (63776) and the eight path-* tags (63777–63784). Your live code has been trying to apply these for months and silently finding nothing, because they were never created. They exist now.

🔴 Why nothing has been tagged — one thing proven, two things to check

First, the part that is certain — and it is not a code bug

The nine tags your code applies did not exist in ActiveCampaign at all. We pulled the full tag list from the AC API on 6 Aug: NEW quiz-gave-email and all eight path-* tags were absent. _apply_tags() only ever looks a tag up — it never creates one — so every quiz lead hit if not tag_id: continue and silently did nothing. That alone accounts for zero tags, regardless of anything else. We created all nine on 6 Aug, so that cause is now removed.

This IS the current quiz path, not the legacy one — the live bundle (index-DFwXN-qe.js, served at /2026-quiz-checkout-v1/) sends source: "quiz_v3_prodtemp", and activecampaign_sync.py:66 branches on that exact string. POST /api/quiz/quiz_lead answers 400 "email required" in prod right now, so the handler is live.

⚠️ And now the honest caveat on the two below

We read these in our copy of the backend, which is dated 4 April 2026. The file is definitely the one serving the live quiz (proven above), but we cannot see your current tree — so if you have already fixed either of these since April, ignore it and tell us. We would rather flag something you have fixed than stay quiet about something you have not.

Check 1 · the tag lookup asks for 100 of 5,845, then gives up in silence

# app/apps/quiz/activecampaign_sync.py, in _apply_tags()
resp = requests.get(f"{AC_BASE_URL}/api/3/tags", headers=HEADERS,
                    params={"limit": 100}, timeout=10)   # 100 of 5,845
...
tag_id = tag_map.get(tag_name)
if not tag_id:
    continue          # no log, no error, no tag

NEW quiz-gave-email sorts at index 5,705 alphabetically — it can never appear in the first 100. Even now the tags exist, this code still cannot find them. Fix: look up by name (params={"search": tag_name}) or hardcode the stable ids above. 🔴 And log the miss — the silent continue is what hid this for months.

Check 2 · obstacle arrives and is thrown away

def sync_quiz_lead(name, email, primary_path, obstacle, source="") -> bool:

obstacle appears in the signature and nowhere else in the file. The quiz sends it correctly; Django accepts it and drops it. It had nowhere to go because AC had no field for it — field 64 now exists.

Step by step

Step 1 · Write the ten answers into AC ~30m

app/apps/quiz/activecampaign_sync.py — add alongside _apply_tags:

# AC custom field ids — created 6 Aug 2026, stable.
QUIZ_FIELD_IDS = {
    "persona": 61,                      # THE GOAL — PRIMARY_PATH
    "obstacle": 64, "future_stakes": 65, "age": 66,
    "gender": 67, "all_fundamentals_mastered": 68,
    "life_context": 69, "background": 70,
    "training_recency": 71, "weakest_fundamental": 77,
}

def _apply_field_values(contact_id, values):
    # Write quiz answers onto the contact. Never raises.
    for key, field_id in QUIZ_FIELD_IDS.items():
        value = values.get(key)
        if value in (None, ""):
            continue                      # never blank out a real value
        try:
            requests.post(
                f"{AC_BASE_URL}/api/3/fieldValues", headers=HEADERS,
                json={"fieldValue": {"contact": int(contact_id),
                                     "field": field_id, "value": str(value)}},
                timeout=10)
        except Exception as exc:
            logger.warning("AC field %s failed for %s: %s", key, contact_id, exc)

Widen sync_quiz_leadkeep every new argument optional so no existing caller breaks — and call it after _apply_tags. Then pass the values through at the call site, app/apps/quiz/views.py:292.

Step 2 · Send the missing values from the quiz ~20m

PM(e, t) builds the lead payload and already sends {name, email, phone, primary_path, obstacle, quiz_variant, source} — so obstacle is already there. Add these:

persona:                   (t.PRIMARY_PATH ?? "").trim(),   // THE GOAL
future_stakes:             (t.FUTURE_STAKES ?? "").trim(),
age:                       (t.AGE ?? "").trim(),
gender:                    (t.GENDER ?? "").trim(),
all_fundamentals_mastered: ctx.allMastered === true,
life_context:              (t.LIFE_CONTEXT ?? [])[0] ?? "",   // ARRAY
background:                (t.BACKGROUND ?? "").trim(),
training_recency:          (t.TRAINING_RECENCY ?? "").trim(),
weakest_fundamental:       ctx.weakest?.name ?? "",

🔴 persona is the one that was missing, and it is the most important field on the page — it is what they actually want. The field has existed all along and is only ever written after checkout, so for every lead who does not buy we currently do not record their goal at all.

Do not recalculate the last oneallMastered = fundamentals.every(m => progress === 4) is already computed on the results context. And note age/gender already leave the quiz on rR(e), the app's plan-provisioning call — same values, different pipe. Reuse them.

Step 3 · Fire the five stage tags ~45m

One call per stage, fixed tag name by id, async so none of them can slow a page. Reuse stampPaywallView's moment for NEW quiz-reached-checkout rather than adding a new hook.

🔴 Timestamps go in a field or nowhere — never in the tag name. If we want per-stage timestamps later, that is five more custom fields, not five tag-name patterns.

Step 4 · How we will both know it worked ~15m

Run one full quiz with a fresh address, then check that contact: it should carry NEW quiz-email-captured, NEW quiz-questions-done, NEW quiz-results-seen, NEW quiz-gave-email and its path-* tag — and its fields should show the obstacle and the rest populated, not blank.

🔴 Then the real test, and it is a number: ActiveCampaign's tag total was 5,845 when we handed you this. Re-count it. If it went up, a value leaked into a tag name — stop and re-read section 1. A test that only asks "did the code run" would have passed happily through every bug on this page.

Full written spec, with the provenance of every claim: docs/company/ANALYTICS/LEAD_SIGNAL_CONTRACT_2026-08-06.md. One caveat stated there and worth repeating: our copy of the backend is dated 4 April 2026, so function names and the two bugs are near-certainly current but line numbers may have moved. Everything on the ActiveCampaign side and everything in the quiz bundle was read from the live systems on 6 Aug.
2

Make the buyer visible to the machines — one two-line residual left

changes 1–5 ALL SHIPPED ✅ · sweep executed · only RETURN_URL open SCOPE: quiz checkout + ALL checkouts-v3 (Aga, 2 Aug) ~20m left nginx + Django settings NIC-19

✅ 6 Aug — all five changes are shipped. This card is one residual, ~20 minutes.

Everything that was open on this card is closed, and we verified each one ourselves rather than taking the message for it:

  • Change 1 — defer-mint (3 Aug). Curled /static/checkouts_v3/stripe-client.js: no load-time mint, stamp-abandon-email ×3, cancel-incomplete ×2. You chose mint-on-email-blur, one of the two answers that keeps abandon recovery alive.
  • Changes 2 + 3 — closed by the 3 Aug code audit. You had already built both, months ago.
  • Change 4 — the debris sweep, executed 5 Aug. Dry run first, then --execute: 40 matched · 40 cancelled · 0 errors. We re-read all 17 ids this page listed — all 17 canceled, stamped 22:11:21–31Z. The one we flagged to eyeball had no email and no payment method, so no real buyer was caught.
  • Change 4 — /lead is live. POST returns 400 {"status":"error","detail":"email required"}, GET returns 405 Allow: POST, OPTIONS. It 404'd on both verbs on 2 Aug.
  • Change 4 — the success-page gate is live. We probed quiz success with pi_INVALID_PROBE_123: it now 302s away to a checkout page — no "You're in", and no fbq/Purchase anywhere in the served HTML.
  • Change 5 — pe= shipped 4 Aug (NIC-50). The email-revenue gate was due 29 Aug. You cleared it 25 days early.

What is left: the RETURN_URL scheme only — see the rewritten change 4 below, which now carries the diagnosis rather than the symptom.

🔁 Re-verified 5 Aug 2026 — read-only, against live Stripe

Census re-run after your sweep: trialing 9 · past_due 16 — and every single one of those 25 carries a customer email. The anonymous, no-email, no-card class that this whole card existed to kill is at zero. 🔴 One thing worth Aga's eye, not yours: 16 past_due subscriptions with real addresses on them are not debris — that is a dunning population, i.e. real members whose payment is failing. That is a retention lane, and it is now visible precisely because the phantoms stopped hiding it.

📌 Scope ruling — Aga, 2 Aug

Every fix in this card applies to BOTH new checkout estates: the new quiz checkout (quiz.themovementathlete.com → /2026-quiz-checkout-v1/app.themovementathlete.com/payments/quiz-checkout/) and ALL new Stripe checkouts-v3 surfaces (/payments/checkouts-v3/*). Where the quiz estate differs, the card says so inline. This card also absorbed the old task 6 — the email-source join is now change 5 below, so everything lands in one sitting in the same handlers.

🔬 3 Aug — a code audit read the LIVE repos; this card is corrected against it

The audit ran against MovementAthlete-Django (quiz-checkout + checkouts-v3) and MovementAthlete-React-ProdTemp — the actual live code, not the Apr-2026 clone this page previously leaned on. Result: changes 2 and 3 below are CLOSED — you had already built both; change 4's two diagnoses are corrected (RETURN_URL · success page); and change 5 shrank from "build the write" to "capture the recipient + verify arrival" — the UTM write into Stripe metadata already exists on both estates. What's genuinely open: change 1 (phantoms, with a new coupling constraint) and the corrected 4 + 5.

✅ First, what we VERIFIED WORKING — more than we first thought. No action on any of this.

  • The metadata layer works. plan_slug · offer · coupon · conversion_value_usd · first-touch attribution — all arriving exactly as designed.
  • Buyer identity at pay-click works END TO END — we checked the live completed trial. Its customer carries email + name + django_email + django_user_id + consent_at, and the intent metadata carries buyer_email + user_id — including on the SetupIntent (trial) path. An earlier read of this said email/user-link were missing; that was wrong — they land for everyone who clicks pay. Credit where due.
  • Consequence: Stripe's trial-ending + dunning emails DO reach real trial buyers. The "invisible buyer" problem only exists for visitors who never complete checkout — which is the phantom-subs problem below, not an identity-capture problem.
  • The 3 Aug code audit adds three more "already built":stamp-abandon-email writes buyer_email onto the intent AND calls Customer.modify(email) — old change 2, closed. ② Completed buyers are provisioned by checkouts_v3_provision.provision_buyer_from_intent (Django user, django_user_id on the Customer, AC lead + plan tag) — both success pages call it — old change 3, closed. ③ Both estates already flatten utm_* / gclid / fbclid into Customer/Subscription/PI metadata at create-intent — see the corrected change 5.

The one structural problem

The subscription is born before the buyer exists. startCheckout() runs on page load — before any field is touched — and the server creates a Customer + Subscription right then.

Estate nuance (3 Aug audit): on v3 the mint is unconditional — startCheckout() on load, every visitor. On the quiz estate it is conditional but effectively always-on for the funnel that matters: maybePrefetch()ensureIntent() fires on DOMContentLoaded whenever the email is prefilled — which is every arrival from the quiz handoff. Same phantom effect, different trigger — the quiz half of the fix is NIC-41 (task 7); change 1 here is the v3 half.

So every visitor who opens a checkout page and leaves mints an anonymous "trialing" subscription. The census of all 94 v3 subscriptions ever created (30 Jul evening, fully paginated — an earlier version of this page said 78/4 past_due; that scan truncated at the newest 100 subs and the adversarial re-check caught it. States churn daily — the 2 Aug re-run is in the banner above: 18 trialing all-phantom, 17 past_due, and the quiz estate adds its own pattern: 17 incomplete subs, all 17 with an email, plus 25 already expired — 42 reachable typed-email abandoners — that lane is now with Aga, AGA-57):

What Stripe showsCountWhat it really is
trialing · card + email ✅1The only real trial
trialing · no card, no email16Abandoned page-views posing as trials — never self-clean
incomplete_expired39Abandoned paid-path visits (these DO self-expire ~23h)
canceled16Tests + expired abandons
past_due17Phantom "trials" that ended — failing invoices addressed to nobody, accruing at ~8–9/week since 17 Jul
active5Your coupon test buys ($24.97 price, $0.25 paid via 99OFF2)

Bonus churn (v3): applying a coupon calls recreate() → a second customer + subscription per visitor, without cancelling the first. The quiz estate has no coupon recreate — but its plan switch remints client-side without cancelling the prior incomplete sub. Same debris class, both estates.

🔓 What this enables — honest today-vs-after (two rows already work thanks to your pay-click stamps)

MachineTodayThe day this ships
Trial-conversion email🟡 Possible for REAL trials (your pay-click stamps made it so) — but our jobs must filter 16 phantoms per 1 real to find themClean feed: "trialing" = a real buyer. Day-3 value + day-6 "your trial ends tomorrow" naming their actual plan + price, no filtering gymnastics
Stripe's own trial-ending + dunning emails✅ Work for real buyers alreadyStop ALSO firing failed-invoice machinery at ~8–9 new phantom past_dues a week
Cart-abandon recovery🟡 Corrected 3 Aug: v3's stamp-abandon-email already persists typed emails (buyer_email + Customer.modify) — the machines just aren't reading them yet. Quiz persists them separately in Redis (stamp-paywall-view) — that lane is Aga's, AGA-57Every typed-email abandoner is recoverable — the highest-intent free traffic we have — and change 1's coupling keeps it that way
Payment-recovery (dunning) on v3🟡 Reaches real buyers; wades through phantom invoices addressed to nobodyEvery open invoice in the scan is a real member with a real address
Metrics❌ 78 "subscriptions", 1 real trial — every trial count, churn rate and abandonment read on v3 is noiseDashboards tell the truth without hand-filtering

The five changes — where · what · how you know it worked

The snippets below are the shape of each change, written against Stripe's Python API — adapt to your named handlers (checkouts_v3_stripe.py · views_quiz_checkout.py / quiz_checkout_stripe.py), never paste as drop-in. Revised 30 Jul evening: two of the original five changes (identity on the customer, user link) were already built by you — moved to the green card. Revised 2 Aug: the email-source join (old task 6) folded in as change 5. Revised 3 Aug — the code audit read the live repos: changes 2 and 3 closed as already-built; 4 and 5 corrected in place. What remains below is genuinely open.

1 · ✅ SHIPPED 3 Aug — phantom minting stopped on v3. Nothing to do; kept for the record. DONE · verified live

Done. You deferred create-intent to valid email blur (or a prefilled warm email), kept stamp-abandon-email fed, and made coupon recreate() cancel the prior incomplete sub. Verified by us in the served JS the same day. The original instruction follows only so the reasoning is not lost.

Preferred Don't create the subscription at page load. Create it at pay-click (or email-blur) — the client hooks already exist; create-payment-intent just moves later in the flow.

Stopgap you can ship in 5 minutes today

# at trial-subscription creation — phantoms then die at trial end instead of going past_due
stripe.Subscription.create( ...,
    trial_period_days=7,
    trial_settings={"end_behavior": {"missing_payment_method": "cancel"}} )

Plus a nightly sweep for the ones already sitting there

# BOTH states — trialing phantoms AND the past_due junk they age into.
# auto_paging_iter matters: a limit-100 scan misses older debris (we made that mistake ourselves).
for status in ("trialing", "past_due"):
    for sub in stripe.Subscription.list(status=status, limit=100).auto_paging_iter():
        if (sub["metadata"].get("checkout_version") == "v3"
                and not sub.get("default_payment_method")
                and not stripe.Customer.retrieve(sub["customer"]).get("email")
                and time.time() - sub["created"] > 86400):
            stripe.Subscription.cancel(sub["id"])

Also: when coupon-apply calls recreate(), cancel the previous sub/intent before creating the new one — right now every coupon user mints a duplicate set. (Quiz estate: same rule for the client-side plan-switch remint — cancel the prior incomplete sub.)
⚠️ Coupling constraint — from the 3 Aug audit: stamp-abandon-email needs an intentId to stamp. If you defer create-intent to pay-click, typed-email abandoners lose their Stripe stamp — so either mint at email-blur instead of page-load, or persist the email server-side the way the quiz does (Redis-style stamp). Deleting the early mint without one of these blinds abandon recovery.
Test: open a trial page, close the tab → 24h later no trialing sub from that visit exists — AND a visitor who typed an email before leaving is still captured somewhere real.

2 · ✅ CLOSED — typed-email abandoners are already captured. Nothing to do.

What we asked for, and what the code actually says This change asked you to confirm and extend stamp-abandon-email. The 3 Aug audit read the handler: it already writes buyer_email onto the PaymentIntent/SetupIntent AND already calls Customer.modify(email) — which was the "extend" half. Both halves exist. We saw no stamped intent in the live scan simply because almost nobody had typed an email and left yet, not because the write was missing. Our error, corrected — no work here.

The one thing to carry forward This mechanism is v3-only. The quiz estate does not write a Stripe buyer_email for abandoners — it stamps stamp-paywall-view (email, plan, path, pending password) into Redis/cache and queues via quiz_checkout_abandon.py. Two different systems — do not port one onto the other. The live consequence is the coupling warning in change 1: if v3's create-intent moves later, stamp-abandon-email has no intent to stamp.

3 · ✅ CLOSED — provisioning is already built. Question answered without you.

We asked where completed v3 buyers get provisioned. The audit found it: checkouts_v3_provision.provision_buyer_from_intent — it creates the Django user, writes django_user_id onto the Stripe Customer, and creates the ActiveCampaign lead + plan tag. Both success pages call it (quiz-checkout and checkouts-v3 share this one path). The djstripe customer.subscriber worry was misplaced — you provision directly and never depend on that signal chain. Line closed for good, as promised.

4 · ✅ Sweep + ALL FOUR bugs are DONE — RETURN_URL verified https:// at the 7 Aug PM re-probe

  • ✅ The past_due phantoms — cancelled. All 17 ids this card listed are canceled; your run reported 40 matched / 40 cancelled / 0 errors across both states. Nothing further.
  • /lead — live. Verified on both verbs. The exit-intent addresses land.
  • ✅ The success page — gated. A junk intent id no longer renders a success state or fires analytics.
  • RETURN_URL — fixed and verified. Re-probed 7 Aug PM: all three checkouts-v3 pages now render window.RETURN_URL = "https://…/checkouts-v3/success"; the only http:// remaining on any page is a New Relic internal constant. The diagnosis below is kept for history; no action remains. (We did not re-test HSTS on app. — optional hardening, your call.)

Severity, honestly Low. The server already redirects the bare http:// success URL up to https://, so buyers do end on TLS. But the hop is in the clear and there is no Strict-Transport-Security header on app. to prevent it, so it is a real defect rather than a cosmetic one. Fix it when convenient, not before the noindex hosts.

The diagnosis — so this is not a hunt We went looking rather than handing you "verify if still broken". In the repo copy we hold, both halves of the TLS signal are missing:

# nginx/sites-enabled/django_project — BOTH proxy blocks (~L42-44 and ~L64-66)
# set Host, X-Real-IP and X-Forwarded-For ... and never X-Forwarded-Proto.
proxy_set_header Host              $host;
proxy_set_header X-Real-IP         $remote_addr;
proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;   # <-- add this, in BOTH blocks

# Django settings — SECURE_PROXY_SSL_HEADER appears NOWHERE in app/.
# Without it Django ignores the header even once nginx sends it.
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

With both in place request.build_absolute_uri reports https and RETURN_URL corrects itself — no change to the checkout code at all. ⚠️ One honesty caveat on that diagnosis: our copy of the backend is a 2 April 2026 clone, so treat the two file locations as a strong hypothesis rather than confirmed-current. The symptom is confirmed live today; if your tree already sets one of the two, the other is the one that is missing. Test: curl any v3 checkout page and grep RETURN_URL — it should read https://.

5 · ✅ SHIPPED 4 Aug — the email-revenue join is closed. Nothing to do; kept for the record. DONE · NIC-50

⚠️ Our premise was wrong — corrected 3 Aug. This change said "nothing writes the email source into the Stripe purchase record." That is false. The audit found that both estates already flatten utm_*, gclid and fbclid into Customer / Subscription / PaymentIntent metadata at create-intent. The write exists. What's missing is narrower — see below.

Where the join actually breaks Aga fixed her half on 31 Jul: AC links now carry UTMs (live audit 2 Aug: 908 of 1,101 links tagged, 82.5% — was 4.1%). The Stripe write exists. So when a purchase shows empty UTMs, the cause is upstream — the visitor arrived at the checkout URL without them (bare landing, a redirect that stripped the query string, or v3 first-touch storage never populated). That is task 2's territory, not a missing write here.

What is genuinely yours (~30m)

  1. Add the recipient email to the flatten list. The utm_* keys already travel; the pe= param (the AC recipient) does not. Same code path, one more field — on both estates.
  2. Confirm the offer-ladder surfaces hold it. /start-training · /30trial · /50promo are where tagged email links land before the checkout — if they drop the query string on the way through, the metadata arrives empty no matter how good the write is. One probe each with ?utm_source=actest&pe=x@y.com.
  3. The read-back (revenue split by utm_campaign/utm_content) is our job, not yoursrevenue_per_source_pull.py gets extended the day the recipient field exists.

Test: one purchase driven from a tagged email link arrives in Stripe with utm_source and recipient in metadata. Email is the one channel where we own the audience (≈6,000 engaged-reachable contacts, zero media cost) and it was the Email fleet's 90-day gate, due 29 Aug — cleared 25 days early. (Doc pointer, unchanged: this is Analytics Brief tasks 27/31; the last-funnel-step export is task 11 and stays separate.)

How we'll both know the whole task worked

DoExpect
One test trial (test card)Regression check — customer shows email + name + card + django_user_id, intent metadata has buyer_email (all already working today; must stay true after the refactor)
Open a checkout page, close the tabNo trialing subscription survives 24h
Type an email into a v3 checkout, then leave without payingStill capturedbuyer_email on the intent + email on the Customer. This works today; the change-1 refactor must not break it
POST the exit-intent formThe address lands somewhere real (not 404)
Land on the success page with a junk intent idNo success state rendered, no GA4/pixel fire (provisioning already refuses — that half needs nothing)
Buy from a tagged email linkStripe metadata carries utm_source and the recipient
Tell us it's liveWe re-run the sub census (read-only, one minute) → zero new anonymous trialing subs, and we flip the email machines on
2

Carry the UTMs into the checkout — the last broken link in the chain

✅ DONE 4 Aug · was P0, verified live SCOPE: BOTH estates (Aga, 2 Aug) ~45m quiz/paywall → checkout link NIC-16

🔴 Do the 10-minute test BEFORE you write anything here

This may not be a build at all. The 3 Aug code audit of the live repos found that handoffQuizCheckout2026.ts already copies utm_*, gclid and fbclid off location.search, and that both Django estates already flatten them into Customer/Subscription/PI metadata at create-intent. So "0 of 30 intents carry utm_source" most likely means the checkout URL arrived empty — an upstream gap — not that a write is missing.

The test: one purchase with ?utm_source=nictest&utm_campaign=verify on the quiz URL, then read the Stripe intent metadata. Present ⇒ the quiz half is already closed and the only real work is first-touch rehydrate for bare landings (visitors who arrive with no query string at all). Absent ⇒ debug qsAttr / create-intent. Never rebuild the handoff blind. This same test is also the fastest proof of legs 1 and 2 of the email-revenue chain in task 1 change 5.

Next, the good news: your checkout is doing its job. We read /payments/checkouts-v3/trial/12month/ live and traced the whole path. The attribution shim is served (/static/checkouts_v3/attribution.js, 200), stripe-client.js posts attribution: getAttribution() into create-payment-intent, and your server writes it into Stripe metadata. All three legs work. We also confirmed first-touch persistence is working — one intent created on 28 Jul still carried a first_seen of 21 Jul.

The problem is what is inside it. Across the last 30 checkouts-v3 payment intents, the attribution object looks like this in 29 of 30 (the thirtieth carries no attribution at all):

{"first_seen":"2026-07-28T22:13:01.394Z",
 "landing":"/payments/checkouts-v3/normal/12month/",
 "referrer":null,
 "page":"/payments/checkouts-v3/normal/12month/",
 "ua_webview":false}

No utm_source. No utm_campaign. No gclid. Referrer null. The shim can only read UTMs from the checkout page's own URL — and visitors are arriving there with a bare URL, because whatever links them from the quiz/paywall to the checkout does not carry the query string across.

The contrast proves it is not the shim's fault. On the older /v2_checkout/ estate, where paid ads land people directly on the checkout, the same metadata comes through fully populated — utm_source=google, utm_campaign=tma_lifetime_retarg_demandgen, a real gclid, and a landing_path. Same mechanism, different journey: those visitors arrive with UTMs on the URL.

🔁 Re-verified 2 Aug — and the quiz estate has the same hole, with proof

Re-run against live Stripe (read-only): of the last 30 checkouts-v3 intents, 29 carry an attribution object and 0 — zero — carry any utm_source. Unchanged since 30 Jul.

New evidence from the quiz estate (Aga pulled the specimen, 2 Aug): a real $97 purchase attempt (checkout_version: quiz_checkout) carries beautiful metadata — persona, path, plan_id, quiz_variant, screen_id, offer — and not a single utm_* key. Also: quiz-estate PaymentIntents carry no metadata at all (identity lives only on the Customer/Subscription), which is why they are invisible to every intent-level scan. So the quiz side needs two things: forward the query string into the checkout, and stamp the attribution into subscription_data.metadata and the intent — this is the revenue-plan task 29, folded in here because it is the same fix. The v3 attribution.js pattern is the port.

🔬 3 Aug — the code audit changes what you should DO here (read before starting)

Two of this card's instructions turned out to be already-built. From the live repos:

  • "Append the query string on the quiz→checkout handoff" is ALREADY DONE. handoffQuizCheckout2026.ts copies utm_*, gclid and fbclid straight off location.search. Do not rewrite the handoff.
  • "The checkout only reads path/persona" is FALSE. Django also reads email, plan and the UTMs, and the Stripe _metadata write includes them when present. The write is there.

So the real gap is upstream of both: the handoff copies what is on the URL, and does not rehydrate from the stored first touch (Amplitude / sessionStorage tma_quiz_first_touch) when the current quiz URL is bare. Bare landings — someone arriving from an ad that dropped its query string, or a second-session return — are what produce "0 of 30 intents carry utm_source".

What to do — in this order

  1. Run the acceptance test FIRST (10 min). One purchase with ?utm_source=nictest&utm_campaign=verify on the quiz URL → we read the Stripe metadata. This is a diagnostic, not a formality — it tells us which half is broken before you write anything.
  2. If utm_source: "nictest" arrives: the quiz forward-path is proven working and closes here. The remaining loss is bare landings only → move to step 4.
  3. If it does not arrive: the break is in qsAttr / create-intent, not in the handoff. Debug there. Do not rebuild the handoff blind — it already does the copy.
  4. First-touch rehydrate (the genuine new work). When the current URL carries no UTMs, read the stored first touch (tma_quiz_first_touch) and use it in the handoff. Same fix conceptually for v3 bare landings, where first-touch storage exists but arrives empty.

How we will both know it worked

Two probes, not one. (a) A purchase with ?utm_source=nictest on the URL → utm_source: "nictest" in the Stripe metadata. (b) A purchase from a bare quiz URL, in a session that first arrived tagged → the metadata still carries the original source. (b) is the one that proves the rehydrate. Each takes us a minute to confirm read-only.

Why this is worth the time: right now we can tell Aga which channel produced a lead, but never which lead produced a sale — because the source does not survive to the payment. This closes the loop from article/video → lead → cash, which is the number the whole growth plan is steered by. The 3 Aug correction makes it cheaper, not less important: most of the plumbing is already yours — what is left is proving where the source evaporates and covering the bare-landing case.
3

A test domain is baked into the live quiz checkout bundle

✅ DONE 4 Aug · was re-opened 3 Aug, then shipped and verified ~20m frontend build NIC-30 was NIC-22

🔴 Re-verified 3 Aug, AFTER your two checkout ships — still open, and untouched by them

Your ships were Django handlers; this one is the React bundle, so it was never going to move with them. We re-ran this card's own acceptance test today: quiz.themovementathlete.com/2026-quiz-checkout-v1/ still serves index-CEOCAHYQ.js — the identical bundle hash this defect was filed against, so no rebuild has been deployed. Curled it (1,338,941 bytes): 7 hits for app-test, all VITE_REGISTER_PAY_URL. addToCartValue:0 still present — and addToCartValue:25 is present alongside it, which confirms the audit's correction that the zero is on 1wk_trial, not on monthly_trial. Please pair this with task 4: one rebuild+redeploy closes both.

🔁 Re-verified 2 Aug — still live, cache-busted

A fresh cache-busted fetch of the shipped bundle (index-CEOCAHYQ.js — same bundle, no redeploy since) still returns 7 hits for app-test and addToCartValue:0 is still present (1 hit). Both defects stand exactly as written below. Reminder of the stakes: the fallback register route on app-test.… has been observed serving a Stripe TEST key — if that route ever fires, a real buyer "pays" into a test account.

🔬 3 Aug code audit — two refinements before you start

  • Check the repo before you edit it. The audit confirms the defect in the deployed dist-2026-quiz-checkout-v1, but .env.production in the repo may already point at production. The dist can disagree with source — in which case the whole fix is rebuild + redeploy to EC2, with no source edit at all. Check source first; it may save you the edit entirely.
  • addToCartValue: 0 is on 1wk_trial, not monthly_trialcorrected 3 Aug; the line below was wrong. monthly_trial correctly sends 25. Fix the 1wk_trial entry.

⚠ Reopened 31 Jul — this was marked done while the defect is still live

No resolution was recorded, and a cache-busted fetch of the current bundle (index-CEOCAHYQ.js) still returns 7 hits for app-test, including VITE_REGISTER_PAY_URL. Not a criticism — a process note: this one closes on a grep of the deployed bundle, not on intent. The acceptance test below takes ten seconds.

First, the good news: the checkout is correct on the path that matters. Verified in the shipped bundle 31 Jul — quiz.themovementathlete.com redirects to /2026-quiz-checkout-v1/, annual charges $97 today, ramps are $24.98 / $12.48, addToCartValue is populated (97 / 25 / 12), tma_ckt_out is set on hand-off, the exit modal fires, and UTMs including gclid/fbclid are forwarded. Two things turned up on the fallback route.

The two defects
  1. The production bundle ships a test domain. VITE_REGISTER_PAY_URL: "https://app-test.themovementathlete.com/register/yearly_2026/". This is the fallback register route — the primary (app.themovementathlete.com/payments/quiz-checkout/) is right — but if the fallback ever fires, a real buyer is sent to a test domain to pay. Point it at production, or remove the fallback.
  2. The same fallback block still sends addToCartValue: 0 — on 1wk_trial (corrected 3 Aug: this card previously said monthly_trial; that entry is fine at 25, and annual_trial sends 157). Harmless while /50off/ is the live path; a Meta-optimisation bug the moment the fallback is used — a 0 teaches Meta to find people worth nothing.
Done when

The deployed bundle greps clean: zero hits for app-test, and no plan object left carrying addToCartValue: 0 (check the 1wk_trial entry specifically). Companion: task 4 (the dead-button trace) lives in this same fallback block — do both in one sitting.

4

Make the dead Buy button speak — same file as task 3, one sitting

extracted from the retired on-page-port task · revenue-plan task 19 ~20m quiz checkout fallback block NIC-37

What it is, in plain terms: in the quiz checkout handler there is a branch that runs when the tapped plan doesn't match any plan object — and it does nothing. No event, no message. The visitor taps Buy, nothing happens, and they vanish from every number we have. We cannot even say how often it fires.

🔬 3 Aug audit — which surfaces this actually affects

The dead branch is R10Paywall, non-control variants: no checkoutUrl ⇒ silent return before add_to_cart ever fires. The live quiz_checkout_2026_v1 variant does NOT hit this handler — it auto-hands-off instead. So this is not costing us sales on the main funnel today. Still worth the 20 minutes, because it protects the other variants, the exit flow and the control — the surfaces we test against — and a silent no-op is the one failure mode no dashboard can ever show us.

How
  1. It's the same fallback block task 3 opens (the one carrying VITE_REGISTER_PAY_URL and the addToCartValue map) — do both in one sitting.
  2. In the no-match branch: fire an analytics event checkout_button_dead with the plan id and screen, and render a visible error — "Something went wrong — refresh and try again." Anything but silence.
  3. Test: force a mismatched plan id in a test build → the event appears in GA4 DebugView and the message renders.
Done when

A forced plan-mismatch click leaves a trace (event + visible message) instead of silence — and from that day we know whether this branch fires once a year or fifty times a week.

Abandoned-cart capture rebuild — moved to Aga (3 Aug ruling)

MOVED · now AGA-57 · Aga is handling it in a dedicated session

Aga, 3 Aug: "remove this task for Nic, I am handling this now." The lane build (Stripe abandoners → the abandonment emails, purchase exclusion evaluated at send time) is hers, tracked as AGA-57. The verified facts travel with it: 42 typed-email, reached-payment abandoners already sit in Stripe on the quiz estate (17 incomplete, all with emails, + 25 expired; ~14/day accruing), and none of the 20 scheduled jobs mails them today.

Your only related piece stays in task 1, change 2: the buyer_email stamp + Customer.modify on the v3 estate — that is what makes v3 abandoners visible to whatever lane Aga builds. Nothing else here is yours.

Give the quiz estate the purchase signal v3 already has — SHIPPED

folded from revenue-plan task 11 · rescoped 3 Aug by the code audit DONE · closed on Slack proof quiz success page NIC-34

✅ 6 Aug — CLOSED. You shipped it and you proved it.

Quiz success now fires browser fbq Purchase with eventID = the PaymentIntent id, so it dedupes against the server CAPI webhook that was already running. Closed on the two Meta Pixel Helper screenshots you sent Aga on Slack, captured on app-test under Stripe test mode.

Being precise about what that evidence is, because you should know the standard rather than guess at it: those screenshots prove the code fires and carries the right eventID. They are an app-test capture, not a production one. We are not asking you for a production capture — it arrives free with the first real quiz purchase in Meta Events Manager, and chasing one now would mean running a live card through the funnel for no additional information.

Why we could not verify the quiz half ourselves — and it is a compliment: quiz success correctly 302s without a valid intent (your own success-gate ship), and the fbq lives inline on the template rather than in a static file, so there is nothing for us to curl. /static/quiz_checkout/analytics.js 404s and stripe-client.js carries zero fbq. The v3 half we did verify live: /static/checkouts_v3/analytics.js, 3,261 bytes, fbq ×2 · eventID ×1 · Purchase ×3.

Background — the server-side CAPI webhook already existed

This task asked you to build the server event. You already built it. The 3 Aug audit found it live: /payments/analytics/stripe-purchase/ (analytics_stripe_purchase) fires Meta CAPI on payment_intent.succeeded with event_id = the PaymentIntent id — exactly the dedup contract we were about to ask for. Combined with /static/checkouts_v3/analytics.js on the browser side, checkouts-v3 is complete. Do not build this again. The estimate drops from half a day to about an hour.

What the task was — all three now done, kept for the record:

  1. Quiz success is dataLayer-only. It does not fire an fbq Purchase, and it does not carry an eventID. Give it the same contract v3 has: browser fbq Purchase with eventID = the PaymentIntent id, so it dedups against the webhook that is already running. Value + currency from the intent, not the page.
  2. Confirm the webhook covers the quiz estate's intents too — same handler, but worth one look, since the quiz path creates its intents through quiz_checkout_stripe.py.
  3. Pixel proof (5 min, ops not code): open a checkouts-v3 page with Meta Pixel Helper against GTM-TKGDZR9 and confirm the pixel actually loads — assumed, never proven. If Aga is quicker to this than you, she can do it — it needs no repo access.
Done when

A test buy on the quiz estate shows in Meta Events Manager as ONE deduplicated purchase (server + browser), matching what v3 already does today.

On-page checkout port — retired as superseded (Aga, 3 Aug)

RETIRED · the 31 Jul ship replaced its premise · 14-Aug read decides any successor was revenue-plan tasks 28 + 19 ~6–8h quiz paywall · Stripe Elements

Why this was retired — the full story, verified live 3 Aug

Where the task came from: the revenue plan of 29 Jul — two days before the new quiz checkout shipped. Back then, tapping Buy on the quiz paywall sent people into the old /register/ signup flow, and the measurement showed 604 Buy-taps producing only 248 subscription starts — the hop was where the rest died. The fix proposed: mount the card form directly under the price tiers, no hop at all.

What the 31 Jul ship changed: the new flow already collapses most of that. Verified against the live bundle 3 Aug: the quiz's pricing screens contain zero Stripe code (no card fields in the bundle) — a Buy tap hands off once to app.themovementathlete.com/payments/quiz-checkout/, the combined price-plus-card page (a real buyer's Stripe metadata carries screen_id: QuizCheckoutHandoff). So today it is: pricing screen → one hop → payment page. Vastly tighter than the flow the 604→248 number was measured on — which makes that number obsolete as this task's justification.

The ruling: whether today's single hop leaks anything is unknowable until the 14-day read of the new checkout (~14 Aug — already the plan's kill-switch read). If Buy-taps → payment-page arrivals → paid reads healthy, nothing is ever built here. If the hand-off shows a real leak, a successor task gets scoped with fresh numbers as its reason. The one fragment that stayed alive — the dead-button instrumentation — is now task 4, folded next to task 3 because both live in the same fallback block. The original spec is kept below purely for the record.

✓ The deploy gate in front of this is CLEAR — verified 2 Aug

The revenue plan held this build behind "why does /api/quiz/config 404?". Live check 2 Aug: it returns 200 now. Nothing blocks this task except tasks 1–2 (do those first — the port then inherits handlers that are already fixed).

Why it is worth 6–8 hours: 604 buy-clicks produced 248 subscription starts. The redirect from the quiz paywall to app.themovementathlete.com is where the other 356 die. You already proved the on-page pattern on checkouts-v3 — this is a port, not an invention.

  1. Mount Stripe Elements under the tiers on the quiz paywall, on the v3 contract: attribution.js + create-payment-intent/update-payment-intent — the buyer never leaves the page.
  2. Inside the same build, give the dead button a voice (old task 19): the handler branch that silently no-ops when a plan doesn't match must emit an analytics event (checkout_button_dead with the plan id) and show the visitor an error — today that person vanishes from every number we have.
  3. Keep the fallback register route OUT of the bundle (task 3 removes app-test; don't reintroduce a fallback that can point anywhere but production).
Done when

A test buyer completes payment without leaving the paywall, the attribution lands per task 2's acceptance test, and a forced plan-mismatch click leaves a trace instead of silence.

YouTube / magnet lead counting — moved to Aga (3 Aug ruling). Instructions kept here for her.

MOVED · now AGA-58 · Aga does the Leadpages + GTM clicks herself ~45–60m

✓ Nic: nothing in this card is yours — Aga took the wiring on 3 Aug (AGA-58)

Kept on this page because the click-by-click below is what Aga works from. The diagnosis was run 2 Aug (GA4 Data API, property 177471782, 28d); the verdict and evidence follow.

The verdict — neither suspect A nor suspect B as written

C — a landing-mix + measurement gap. YouTube's traffic overwhelmingly never goes near the quiz: 96% of its 3,810 sessions land on main-site lead-magnet pages, and generate_lead only exists on the quiz subdomain — so even a converting magnet visitor is structurally invisible. The numbers:

Measured (28d, GA4 API)Value
YouTube sessions (youtube/organic + youtube.com/referral)3,695 + 115 — the brief's claim verified exactly
Where they land/calisthenics-assessment/ 1,991 · /free-28-days-calisthenics-challenge/ 938 · handstand PDF 271 · muscle-up 107 · … — all themovementathlete.com magnet pages. Only ~44 sessions ever reach the quiz host
Where generate_lead firesquiz.themovementathlete.com: 353 of 353 — it does not exist anywhere on the main site
Form events on the magnet pagesZero. 2,515 page_views on /calisthenics-assessment/ produced no form_start, no form_submit — the embedded AC forms are invisible to GA4's enhanced measurement
Leads credited to YouTube1

So suspect B ("the traffic doesn't convert") is unprovable either way from GA4 — conversion on the pages YouTube actually lands on is simply not instrumented. And suspect A (quiz-hop UTM loss) affects at most the ~44 quiz-host sessions. The honest reading: we cannot count YouTube's leads until the magnet pages report form submissions.

✓ Plus a fresh suspect-A regression — found 2 Aug, FIXED and verified closed 3 Aug

Sometime after 31 Jul, /calisthenics-assessment/ — YouTube's #1 landing page — acquired a Redirection-plugin 301 to the quiz root. WP Engine's edge cache serves the cached bare-URL 301 to every utm-only visitor, stripping utm_source/_gl in flight (the link-integrity guard regressed 19/0 → 18/1 on exactly this URL; mechanism proven with a control parameter — non-utm params reach the page, utm-only requests get the cached 301).

Closed, nothing for you: the tag-preserving client-side redirect is live on the page (Code Snippet 9, now with a no-JS fallback), Aga deleted the plugin rule on 3 Aug (AGA-54, done), and the link-integrity guard re-ran clean: 19 preserved · 0 dropping, exit 0. Same hop, same destination, attribution intact — verified end-to-end.

The one thing left for you (~45–60m) — final version, corrected twice against the live pages

How the magnet forms ACTUALLY work (read this first — it kills two wrong approaches)

The email forms are Leadpages pop-ups ("Leadboxes") rendered inside a cross-origin iframe (embed.lpcontent.net). Two consequences, both verified:

  • GTM on our pages cannot see the submit — the built-in Form Submission trigger and Element Visibility tricks are dead ends here (the earlier version of this card suggested them; ignore that).
  • The UTM capture into ActiveCampaign already works — page URL → iframe URL → hidden fields → AC contact (proven by tools/analytics/check_magnet_utm_capture.mjs, 8/10 magnets live-verified). You are NOT building attribution; you are only telling GA4 that a lead happened.

The one reliable hook: an after-submit redirect to a thank-you page — configured inside Leadpages. So yes: this task needs the Leadpages login (app.leadpages.com).

✅ Already built for you — the shared thank-you page is LIVE

themovementathlete.com/resource-thank-you/ (created 3 Aug, verified 200) — "check your inbox" copy + an assessment CTA. ALL ten pop-ups point at this ONE page; each appends its own ?magnet=<slug> so we can still tell which magnet converted. You do not create any pages.

✅ Step A is DONE — Aga swapped all ten pop-ups on 6 Aug 2026

Every magnet pop-up now redirects to /resource-thank-you/. The table below is kept as the record of what was set, not as work to do. 🔴 Leadpages SAVED is not PUBLISHED — if any magnet later stops redirecting, that is the first thing to check.

Step A — Leadpages (~2 min per pop-up × 10)
  1. Log in at app.leadpages.com → left menu Conversion Tools → Pop-ups (older UI calls them Leadboxes).
  2. Open each of the ten magnet pop-ups → click the form / button widget → find the after-submit / thank-you setting → choose "Go to URL" (instead of the inline thank-you message) → paste that magnet's URL from this table → Update/Publish:
Pop-up (magnet)Paste as the after-submit URL
28-day challengehttps://themovementathlete.com/resource-thank-you/?magnet=challenge
Handstand PDF…/resource-thank-you/?magnet=handstand
Muscle-up plan…/resource-thank-you/?magnet=muscle-up
Planche guide…/resource-thank-you/?magnet=planche
Back-lever…/resource-thank-you/?magnet=back-lever
Front-lever…/resource-thank-you/?magnet=front-lever
Ring dips…/resource-thank-you/?magnet=ring-dips
Shoulder mobility…/resource-thank-you/?magnet=shoulder-mobility
Press to handstand…/resource-thank-you/?magnet=press-to-handstand
Get started…/resource-thank-you/?magnet=get-started

One check per box while you're in there: if a pop-up currently shows a direct download link after submit (instead of "check your email"), the redirect would hide it — tell us which one and we add that file's link to the thank-you page. The delivery email from AC keeps working regardless.

Step B — Google Tag Manager — ✅ ALREADY DONE, SKIP IT

✅ The fleet built and published this on 6 Aug — nothing for you here

Container GTM-TKGDZR9, published as version 116 (verified by reading the live version back). Variable URL - magnet (query) (URL / Query / key magnet) · trigger TMA - Lead Magnet Thank-You Pageview (Page Path contains /resource-thank-you/) · tag GA4 - generate_lead (magnet)G-ZBQN3P8WKW with lead_type=magnet and magnet_slug. Do not rebuild it — a duplicate tag double-counts every lead.

One thing that was silently broken and is now fixed: the thank-you page is served by a WordPress code snippet that bypasses the theme header, so the site-wide GTM container was not loading on that page at all (0 references, against 3 on any normal page). The container tag was injected into the snippet on 6 Aug and verified live. Worth knowing generally: any snippet-served page is invisible to everything installed in the theme.

Step C — the end-to-end test (ten seconds)
  1. Open /free-28-days-calisthenics-challenge/?utm_source=nictest, submit a test email in the pop-up.
  2. You should land on /resource-thank-you/?magnet=challenge.
  3. GA4 (analytics.google.com) → Realtime: a generate_lead event appears; in DebugView its session source reads nictest.
If the Leadpages login cannot be found: say so and stop — the fallback needs no Leadpages and no GTM at all: the leads already arrive in ActiveCampaign carrying their utm_source (verified), so the fleet can count magnet leads by source from AC directly. GA4 stays magnet-blind in that world, but the YouTube question still gets answered.
Done when

Magnet submissions appear as generate_lead with real sources — from that day the YouTube lane (and every magnet lane) counts. YouTube fleet gate: due 22 Aug.

Make a free test account — 10 minutes, unblocks the free-member audit

MOVED · now AGA-66 · Aga is handling this

Three questions about what a free member sees cannot be answered from outside the login wall (which upgrade CTAs exist, what the paywall shows a logged-in free user, whether the $9.99 step-down is reachable). Create a throwaway free account and send the credentials to Aga — the audit runs the same day.

Decide with Aga: does /register migrate to checkouts-v3 or retire?

DECISION · revenue-plan task 32 · not build work yet

We run two checkout estates on two different analytics contracts, and nobody has decided which survives. Every task on this page doubles in maintenance until this is ruled. Bring a recommendation to Aga (15 minutes of thought, no code); she rules; the loser gets a retirement date. Parked here so it is not invisible — do not start migrating anything until the ruling exists.

Front-lever cluster consolidation — off your list (7 Aug): the evidence downgraded it, the fleet executes

✅ MOVED TO FLEET · ranked lastNIC-07

Why it left your brief: the keyword map this card waited on landed 6 Aug (marketing/seo/NIC82_FRONT_LEVER_KEYWORD_MAP_2026-08-06.md, Aga-approved) and it refuted the premise. The bare skill name is a zero-click SERP (61.8–71.8% zero-click across the clusters; planche = 135,725 impressions → 97 clicks), so consolidation is worth ≈+1.1 leads/28d [MODELLED] across all three clusters — a 20-minute hygiene task, not a project: 3 × 301 (/improve-front-lever/ · /how-to-do-a-front-lever/ · /front-lever-variation//front-lever-progression/) + 6 internal-link repoints, with an explicit DO-NOT on the tempting fourth merge. The fleet applies these itself via the Redirection REST API — the same mechanism that shipped the NIC-12 batch — ranked last behind everything on this page. Stale figures corrected while we were here: 8 live pages, not nine; the 9,184/0.25% was a windowless 28-day under-read (336-day truth: 192,114 impressions / 451 clicks / 0.235%). Live spot-check 7 Aug: all four URLs still 200, so the work is real — it is just not yours.

Index hygiene — DONE: executed by the fleet 6 Aug, every URL live-verified 7 Aug

✅ DONE · 20 of 20 verified liveNIC-12

We checked before asking — and the work no longer exists. The backlink-checked evidence list (marketing/seo/NIC83_BLOG_HYGIENE_EVIDENCE_2026-08-06.md, all 867 published documents on both sites) named 20 URLs still needing action. The fleet executed the batch under AGA-123 on 6 Aug via the Redirection REST API, and on 7 Aug we re-crawled all 20: 19 return a one-hop 301 to a 200 target — including /blogs//blog/, /home-page-test//, and the missed 25th promo /70-off-new-aug24//start-training/ — and 1 is drafted (404, deliberate). Method note that survives: every retirement was draft + 301, never a delete, because external backlinks are unverifiable from here (no backlink tool in the estate; GSC Links is UI-only) and a 301 preserves unknown equity either way. Residuals, neither yours: (1) /calisthenics-gear/ + /freemonth/ still link the retired /50-off-black-friday/ internally — works via the 301, but the edit is Aga-gated (affiliate ruling); (2) cosmetic draft residue on BWA page 210021 under a live redirect.

Verify the generated password actually logs into the app — 15-minute gate on the welcome email

✓ CLOSED 2 Aug — Aga confirmed the test passed

✓ Done — closed on Aga's confirmation, 2 Aug 2026

The generated password logs into the app; the credentials-box welcome email is cleared to send. Kept below for the record.

The new post-purchase welcome email shows the buyer their username (email) + generated password in a credentials box. Before the first real send we need one test: buy on a test card, take the password from the email, and confirm it logs into the app. Also confirm the %EMAIL%/%PASSWORD% SendGrid substitutions are mapped. The email is rendered for you at tma-paywall-spec.netlify.app/email-welcome-noquiz. Nothing sends until this passes.

A buyer must never receive an abandonment email — removed from your list by Aga, 2 Aug

REMOVED · Aga ruling 2 Aug 2026

Aga took this off the task list on 2 Aug. The substance survives as an acceptance test inside task 4 (the abandonment-lane rebuild): the purchase exclusion is evaluated at send time, a test buyer never receives the abandonment mail, and a genuine abandoner still does. No standalone work item remains.

WP Engine estate — moved under Aga (2 Aug ruling)

MOVED · now AGA-53 · external WordPress dev executes

Aga, 2 Aug: "not for Nic — move it under Aga." The whole workstream (69 outdated plugins + 11 themes triage, the two Lighthouse-48 mobile scores, the bandwidth watch, the deleted themovementdev install) is now AGA-53 on Aga's queue — she coordinates the external WordPress dev directly. The PHP upgrade was already with a separate dev (target 8.2 — Divi 4.27.7 is only tested to 8.2). Nothing here is yours.

Email-source join — folded into task 1 (Aga, 2 Aug)

FOLDED · now change 5 of the buyer-visibility pack

Aga's 2 Aug ruling: this is the same handler work as task 1, so it now lives there as change 5 — write the email source into Stripe at purchase, with the full step-by-step inline (nothing external to open). One correction made in the fold: the old card pointed at Analytics Brief "task 17" for the last-funnel-step export — that export is task 11; task 17 is the canonical-identity keystone. The wrong pointer is gone.

/api/quiz/config 404 — already fixed · verified 200, 2 Aug

no action — the paywall-deploy gate is CLEAR

The revenue plan (task 8) held every quiz-paywall deploy behind this: "the live quiz calls /api/quiz/config, gets a 404, and runs on fallback config — nothing else starts until this reports." Live check 2 Aug: it returns 200 now. Whoever fixed it during the 31 Jul–1 Aug quiz work: thank you. The gate in front of task 6 (the on-page checkout) is open, and the revenue doc's task 8 is stale.

/calisthenics-assessment/ was silently stripping UTMs — found & fixed 2 Aug, no action for you

Code Snippet 9 live · Aga's 2-min rule deletion queued (AGA-54)

What happened: a Redirection-plugin 301 (added after 31 Jul) sent the assessment doorway — YouTube's #1 landing page, 1,991 sessions/28d — to the quiz root with a literal target. WP Engine's edge cache then served that cached bare 301 to every utm-only visitor, so their utm_source/_gl vanished mid-hop and the link-integrity guard regressed 19/0 → 18/1.

The fix, already live: a guarded Code Snippet (id 9, deployed with health-check + auto-rollback, verified 2 Aug) client-redirects the page to quiz.themovementathlete.com/ + location.search — the browser carries the query string, the edge can't strip it. Aga deletes the plugin rule (2 minutes, AGA-54) and the guard returns to 0 dropping. Same funnel behaviour, attribution intact. Filed here so you know why the doorway behaves differently this week.

Redirect pack — done, and it worked

Fix A shipped · verified live 29 Jul

Fix A (quiz root forwards the query string) is live — utm_* and _gl both survive. Guard moved 18/4 → 19/1 → 19/0, exit 0.

The other three were not yours, which is why they are off this list:

  • Fix B /free-front-lever-training-guide/ — we proved WP Engine's edge strips utm_* and _gl before PHP ever runs (control parameters survived the same redirect that dropped the real ones). No WordPress-level fix exists for any 301 on WP Engine. The rule now is simply: link final destinations, never a WP alias. Probe retired, with the reasoning written into the guard so nobody re-opens it.
  • Fix C trailing slashes — WordPress core redirect_canonical(), not nginx. Aga ruled: do not patch core to satisfy a probe.
  • Fix E GA4 domains — already correct.

The /g/ post that 500s — off your list

re-scoped 29 Jul

Only the content field errors; id, status, slug, title and meta all read fine over the API. So the wp-admin list renders and it can be trashed without WP-CLI — no server access needed. The body is unrecoverable via the API (zero revisions); a DB pull is the only route if the story is worth saving. Awaiting Aga's call.

BWA redirect loop — fixed, no action

fixed 29 Jul · verified

A live article was unreachable behind a 51-hop loop on 3,821 hits — two redirect plugins pointing at each other. Fixed at source via the API: the offending rule disabled, the page's slug corrected (it had genuinely been misspelled for years), and a replacement 301 added so the old URL still resolves. …-strength/ now serves 200; …-strenght/ 301s to it in one hop.

llms.txt · pinch-zoom · IndexNow · Organization schema — all shipped

27–29 Jul · both sites verified

Also live since this page was last updated: FAQ structured data on 9 pages (the estate had none), IndexNow active on both sites, and BWA's Organization schema corrected — it had been telling every AI engine our organisation was "Calisthenics Academy".

Stop creating a Stripe record on page load — SHIPPED 3 Aug 2026

DONE · verified live 3 Aug against the served file quiz checkout · stripe-client.js NIC-41

✅ Shipped — and here is exactly what we checked, so you know the standard

Nic — nothing left to do on this card. We did not take the ship on trust: we curled https://app.themovementathlete.com/static/quiz_checkout/stripe-client.js (HTTP 200, 17,446 bytes) and read it. maybePrefetch: 0 occurrences. DOMContentLoaded: 0 occurrences. stampPaywallView: 3 occurrences. cancel-incomplete: present, called as cancelIncomplete(oldSub) on plan switch. Your own comment at line 497 reads "Mint only when the buyer engages: password blur, tap-to-load card form, or pay."

You got the hard part right. The trap on this task was deleting the mint and the signal together — that would have fixed the number and silently killed the recovery lane in the same commit. You kept stampPaywallView, and you cancelled the prior incomplete sub on plan switch, so the orphan-remint half is closed too. Tests logged: QC-INTENT · card 4242 success in Chrome and Safari · login with the checkout password · API plan amounts + force_expired · Google Pay and PayPal Express.

🔴 One consequence everyone must know before reading a dashboard

A measurement discontinuity landed on 3 Aug 2026. Do not compare across it. Phantom trial and subscription counts will collapse, and checkout conversion rate will jumpboth by design, neither is churn or a demand change. The pre-3-Aug conversion rate was fiction and is not a baseline. Clean data starts 4 Aug; first honest read ~11 Aug, reliable ~17 Aug. The quiz abandoner audience also shrinks by design and becomes higher-intent — judge that lane on recovered purchases, never on list size.

The original task is kept below for the record.

What it was, in plain terms: in static/quiz_checkout/stripe-client.js, maybePrefetch() runs on DOMContentLoaded and calls ensureIntent() whenever the email field is pre-filled — and it is always pre-filled, because it comes across from the quiz. So Stripe creates a customer, a subscription and an open $97 invoice for every page view, before the visitor has touched anything.

Why it matters
  1. Checkout conversion is unmeasurable. requires_payment_method and "no payment method attached" are the mechanical default for every non-buyer — someone who bounced in two seconds is indistinguishable from someone who read the whole page and decided no. We spent a day reaching a wrong conclusion because of this.
  2. The recovery lane is counting bounces as abandoners, so the abandoned-cart audience is inflated with people who never intended to buy.
  3. Stripe fills with phantom subscriptions — 48 records for 41 distinct people in the first four days.
How
  1. Move the ensureIntent() call out of maybePrefetch() / DOMContentLoaded.
  2. Fire it on the first interaction with a card field — focus, or first keystroke on the card number. Keep the existing password-blur trigger as it is.
  3. Do not create an intent on load under any condition.
  4. 🔴 KEEP stampPaywallView — do not delete it along with the mint. Corrected 3 Aug by the code audit: the signal you need already exists, so this is now a "leave it alone" instruction, not a build. stamp-paywall-view already records email · plan · path · pending password into Redis/cache and queues through quiz_checkout_abandon.py. When you move ensureIntent(), stampPaywallView must still fire on load — it is what keeps the recovery lane fed once the Stripe phantom is gone. No new endpoint needed. Why this half is not optional: checked live on 3 Aug across all 44 abandoner subscriptions — every invoice reads attempted=false, attempt_count=0, and not one carries a payment_method or a last_payment_error. Nobody has ever typed a card into this checkout. So the phantom intent is currently the only record anywhere that a person reached the checkout and left. The moment "no trace in Stripe" is true and stampPaywallView has been removed with it, the recovery lane loses its entire audience feed, silently — we would fix the number and kill the lane in the same commit.
  5. Also avoid orphan remints. The quiz plan-switch currently remints client-side without cancelling the prior incomplete subscription. Whatever you do to the load-time mint, make the switch cancel the one it replaces — otherwise the debris just moves rather than stopping.
  6. Test (budget 15m of the 60m): load the checkout with a pre-filled email and do not touch the card form → confirm no new Stripe customer or subscription appears, and that the stamp-paywall-view record did arrive with plan/path intact. Then type one character into the card number field → confirm the intent is created and a real purchase still completes end to end. Finally switch plans twice → confirm only one incomplete subscription exists, not three.

The same constraint bites on checkouts-v3 — read before task 1, change 1

v3 has the identical trap in mirror image. There, typed-email abandoners are captured by stamp-abandon-email, which needs an intentId to stamp onto. So if v3's create-intent is deferred to pay-click, that capture has nothing to write to and abandon recovery goes blind on that estate. Two acceptable answers: mint at email-blur rather than page load (early enough to stamp, late enough to mean something), or give v3 a Redis-style stamp like the quiz has. Either works — silently doing neither is the failure mode.

Done when

A page load with zero card interaction leaves no trace in Stripe but does leave a stamp-paywall-view record, and a real purchase still works. From that day, checkout conversion becomes a real number and the recovery lane keeps a real audience.

2

The 48-hour $97 window — moved to Aga, do not build

MOVED · now AGA-68 probed live 3 Aug on the served page nothing for you here

Nic — nothing to do here. This came off your list on 3 Aug. Whether the deadline should truly bind is a pricing decision (binding buys honest urgency, but the person who returns on day 5 ready to pay $97 would meet $157 and may buy nothing). Aga rules first. The findings below are kept so whoever picks it up does not have to re-probe.

What is already right: the per-visitor deadline is fixed once set — the same address returns the identical timestamp on repeated requests, so the clock does not silently restart on every visit. That part works.

The two holes
  1. An address the server has never seen gets a fresh 48 hours. Verified with a never-seen address: exactly 48.0h. So the offer is re-obtainable with a new email — and an existing person whose cache entry has lapsed gets a brand-new window. Live example: wajackson1@gmail.com abandoned on 30 Jul and today holds a window running to 4 Aug 08:53.
  2. We have never observed the expired state. TMA_QC_OFFER_EXPIRED came back false on every address tested, including a 90-hour-old abandoner — so we cannot confirm the $97 is actually killed at charge time, only in the UI. Your own comment in the page source says it plainly: "the same deadline must kill the $97 server-side (coupon dies → $157). A timer that changes nothing is banned."
How
  1. Once an email has had a window, never mint it a second one. A lapsed cache entry must resolve to EXPIRED, not to a fresh 48h.
  2. Enforce it at charge time, not just in the interface.
Done when

Take an address whose window has passed and load the checkout → TMA_QC_OFFER_EXPIRED is true and the page shows $157. Attempt the purchase → Stripe charges $157, not $97.

Why it matters to us: emails 3 and 4 of the recovery sequence tell people their window is closing, and then that it closed. If the page hands them a fresh 48 hours on arrival, we have lied to them in writing — and taught them that every deadline we ever set is theatre.

6

Wrong star rating and a contradicted member count, both in the live quiz bundle

found 3 Aug by the fact-checker ~20m — batch it with task 3 2026-quiz-checkout-v1 source → rebuild NIC-46

Batch this with task 3. It is the same bundle and the same redeploy, so doing them together costs almost nothing extra — and doing them apart costs a second deploy.

What is wrong
  1. Two "4.9" star strings4.9 · 100,000+ plans created and ★★★★★ 4.9 · 100k+ users. The settled ruling (31 Jul) is to ship 4.7 unattributed. Measured live 3 Aug: iTunes API 4.53 across 297 ratings; the Google Play listing reads "Rated 4.7 stars". 4.9 overstates both.
  2. One "Join 53,000+ people" sitting on the same funnel as six "100,000+" strings. Whichever is right, they cannot both be — a visitor who notices stops believing either.
  3. Also re-export Step-1-Take-Assessment-1.png — the image bakes in "It only takes 5 minutes" against Aga's 3-minute ruling, so it cannot be fixed in copy.
Done when

A grep of the redeployed bundle returns zero "4.9" star strings and zero "53,000+", and the assessment image no longer says 5 minutes. 🔴 Do not ship 4.8 anywhere — that claim is governed separately by AGA-84 and needs Aga's written confirmation.

3

Quiz answers into the recovery emails — FOLDED into NIC-54, 6 Aug

FOLDED into NIC-54 — one task, one sitting no separate work NIC-45

✅ 6 Aug — merged into NIC-54, and three checks made it smaller

Aga's call: everything you send us about a lead is one task — the five stage tags and these attributes hit the same helper (add_tag_to_quiz_contact) and the same /api/quiz/quiz_lead handler. Splitting them would have you opening the same files twice.

We read your live bundle before writing the task, and each finding shrinks it:

  • obstacle is ALREADY sent. PM(e,t) posts {name, email, phone, primary_path, obstacle, quiz_variant, source} today. It was dying because ActiveCampaign had no field to receive it — not a quiz fault.
  • all_mastered is ALREADY computedallMastered = fundamentals.every(m => progress === 4) on the results context, and ALL_MASTERED is also an 8th PRIMARY_PATH value. One more field on the payload, not a calculation to build.
  • age and gender already leave the quiz — but on rR(e), the app's plan-provisioning call, not the lead call.

✅ We already did our half so you are not blocked — the five receiving AC custom fields were created via the API on 6 Aug: [64] TMA Quiz Obstacle · [65] TMA Quiz Future Stakes · [66] TMA Quiz Age · [67] TMA Quiz Gender · [68] TMA Quiz All Mastered.

🔴 The original instruction below is WITHDRAWN — do not build it

It said: append the four to the quiz→checkout hand-off URL so they ride into Stripe metadata. Two reasons that is the wrong place:

1. Stripe metadata only exists if they reach checkout. Everyone who finishes the quiz and stops leaves nothing — and that is precisely the audience a recovery email is for.

2. It hardcodes the destination into the front end. Put the fields on the existing quiz_lead payload instead and Django becomes the fan-out — AC today, AC + Customer.io during the migration, CIO after. You touch the quiz once, ever. (Customer.io cannot send at all today — mail.themovementathlete.com has no MX/SPF/DKIM, verified 6 Aug — so a browser-to-CIO signal would land nowhere.)

For the record — the original card

What travels today (verified live 3 Aug, and re-confirmed against the hand-off code): the quiz → checkout hand-off sends email, path, persona and plan (plus the utm_*/gclid/fbclid it copies off the URL). Stripe subscription metadata carries email, persona, path, plan_slug, plan_key, plan_id, offer, offer_expired, coupon, quiz_variant, screen_id, checkout_version, purchase_path, plus utm_source/medium/campaign on 33 of 48.

What does not travel, and the quiz already knows: OBSTACLE · FUTURE_STAKES (their own words for what they fear if nothing changes) · AGE · GENDER. All four die at the checkout boundary.

How
  1. Append the four to the quiz → checkout hand-off URL exactly as path and persona already travel, and read them the same way.
  2. Include them in the stamp-paywall-view record — and in intent metadata once a card is touched.
  3. No Django schema work and no new tables. Same pattern as the params already flowing.
Done when

Complete the quiz, land on the checkout, and all four appear in the stamp-paywall-view record carrying the values the quiz collected.

Why it matters to us: today a recovery email can say what they wanted. With these it can say what was standing in their way, and what they told us they were afraid of. That is the difference between a template and a letter.

8

One definition of a lead — there are currently three

from the 3 Aug skeptic gate ~1.5h incl. 30m validation scoreboard config NIC-42

What it is, in plain terms: the same metric has three live values on the same day, and the rate built from it has two values 24× apart depending on which denominator you pick.

SourceLeads / 7dDenominator
GA4 ordered funnel65244 quiz-landing visitors → 26.6%
GA4 generate_lead1038,957 property sessions → 1.1%
ActiveCampaign quiz list140

They differ for real reasons — event scope, hostname filter, AC list membership and dedup. The problem is not that they differ, it is that nothing declares which one is "the" number. Two of the four sourcing errors in the 3 Aug quiz analysis trace directly back to this.

How
  1. Reconcile the three counts and establish why each differs.
  2. Publish one canonical numerator + denominator pair as the definition of "quiz lead" and "quiz lead rate" — written into the scoreboard config, not into a doc where it can drift.
  3. Keep every other counter visible, but label each explicitly as a different measure. Never mix them. Follow the precedent already set for app sign-ups: RevenueCat customers_new and GA4 onboarding are different counters, both real, never mixed.
  4. Test (budget 30m of the 1.5h): kpi_15k.json and data_funnel.json report the same lead count and the same lead rate for the same window, and a third party can reproduce both from the stated query without asking which source to use.
Done when

One definition lives in config, the two scoreboard files agree, and the other counters carry an explicit label saying what they measure instead.

Pre-clean the re-engagement backlog before the feed continues

MOVED · now AGA-63 · Aga is handling this

What it is: the R1 canary on 991 NEW Re-Engagement - For people who stopped opening hard-bounced 6.9% (33 of 478) — 3.5× the 2% hard-stop line.

How
  1. Pre-clean the ~9,128-contact backlog: drop known-dead and bounce-history addresses.
  2. Do this before enter_991_reengagement.py continues the 2k/day feed.
Done when

The backlog is cleaned and the feed resumes under the 2% line. Skipping this means domain reputation pays for hygiene we would otherwise get free from the suppression step.

Activate the 3 pre-registered A/B splits in ActiveCampaign

MOVED · now AGA-64 · Aga is handling this

What it is: three A/B splits are pre-registered and waiting — the 750 TMA | Welcome Sequence After Quiz E1 subject test, plus nurture and re-engage. They need manual setup in ActiveCampaign before they can run.

How
  1. Build the three splits in AC. Setup specs are already written — EXPERIMENT_LEDGER.json W29 records and WEEKLY_2026-08-03.md §5.
Done when

All three are live. This is the third consecutive week the self-learning loop has run with zero live experiments, so it is currently learning nothing.

Make spam complaints visible — the #1 hard-stop is currently blind

MOVED · now AGA-65 · Aga is handling this

What it is: the weekly safety sweep pauses all sending at 0.10% spam complaints. That is its number-one hard stop — and complaint counts have been DARK since the loop was built, on the platform carrying roughly 100% of send volume. We cannot currently trigger the most important safety rule we have.

How
  1. Extend tools/email-analytics/collect_metrics.py to pull ActiveCampaign spam-complaint counts.
  2. Feed them into the safety sweep so the 0.10% hard-stop can actually fire.
Done when

Complaint rate appears in the weekly metrics pull and the hard-stop is armed. Flagged 27 Jul, still open.

The attribution stamps — SHIPPED 7 Aug 2026; all 3 now CONFIRMED on the quiz rail (10 Aug)

✅ SHIPPED page + source writes CONFIRMED in served code, 7 Aug 2026, 19:24–19:28 UTC ✅ PaymentIntent stamp CONFIRMED 10 Aug — 6 of 6 quiz sales legacy /register/ PIs still bare — FLEET-45 NIC-91 + Task 30

✅ Shipped — and as of 10 Aug all three are confirmed on the quiz rail

Nothing left for you on this card. Verified 7 Aug 2026, 19:24–19:28 UTC against served code, not against the message:

(1) The page — CONFIRMED. /register/monthly_2026/ now serves <input type="hidden" name="quiz_variant" id="quiz-variant-input"> into process_stripe, alongside the full utm_* set plus gclid, fbclid and pe. The page JS backfills the variant from ?quiz_variant= or ?utm_content=, so it survives a link that only carries the UTM.

(3) The source — CONFIRMED. The live bundle index-yC00xO94.js at /2026-quiz-checkout-v1/ defines first_touch_utm_source, _medium, _campaign, _content, _term, plus first_touch_fbclid, first_touch_fbc and first_touch_fbp, gated by a once-only __tma_quiz_first_touch_applied__ flag. That flag is the part that matters — it is what makes it first-touch rather than a re-read of whatever URL the visitor is on now.

(2) The checkout system — NOT YET MEASURABLECONFIRMED 10 Aug, see the green box below. The original 7 Aug reasoning is kept as written: The checkout_version copy onto the PaymentIntent happens in Django, which we cannot read from outside. It can only be proven by a sale created after your deploy. There was none: the last PaymentIntent of the day was 11:31 UTC and you shipped around 19:20 UTC. Reading those empty PIs as a failure would be a false negative — they were minted eight hours before the code existed.

So we built the check instead of leaving it to memory. python3 tools/analytics/attribution_stamp_check.py filters to your deploy time and reports one of three states: confirmed, still-no-sale, or genuinely missing. On 7 Aug it returned still-no-sale.

✅ ANSWERED 10 Aug 2026 — run again on 18 real sales

Verdict: PARTIAL — 6 stamped, 12 not. And the split is not random, it is one rail versus the other.

✅ The quiz rail: 6 of 6 stamped. Your write works. Every completed sale through /payments/quiz-checkout/ since your deploy carries, on the PaymentIntent, checkout_version=quiz_checkout, quiz_variant=quiz_checkout_2026_v1 and the full utm_* set — including gclid where the visitor arrived from Google. That is the write we could not prove on 7 Aug, and it is now proven on real money. It is also doing real work: it is the only reason we could answer Aga’s question of 10 Aug about which sales paid ads actually created.

⚠️ The legacy /register/ rail: 0 of 12 stamped. Those PaymentIntents ($9.99, $19.97, $24.97, $49.97, $132.13, $157…) carry no checkout_version, no quiz_variant and no UTMs at all. So the "we cannot split the legacy money by funnel" problem below is still true.

One genuinely new thing, and it is yours: on 10 Aug a subscription appeared carrying checkout_version=register — the first ever. So the page write from (1) is now firing in production, not just present in the served HTML. It still carries no UTMs and no quiz-build name, so it does not yet close the attribution gap. No action requested — recorded so the next person does not re-open a question that now has an answer.

Everything below this line is the original card, kept so the diagnosis and the reasoning stay on the record.

🔴 The problem, in one line

Three quiz funnels are live at the same time and the money cannot be split between them. Re-verified 7 Aug PM from the four live bundles + a fresh Stripe pull: every build links stampless /register/ checkout URLs, and all 54 subscriptions on legacy prices since 1 Jul carry EMPTY metadata — 54 of 54. Nothing in Stripe names the page a legacy sale came from. So when we ask "which build converts better, and at what AOV" the honest answer today is we cannot know — not "we estimate", cannot. (One correction to our own earlier evidence line: the bundles' /register/ link sets are no longer identical — v1's rebuilt bundle now leans yearly_2026 ×8, v2-2 and /2026/ lean yearly3 ×7, and 2026-v2-3 links the older monthly2/monthly3/quarterly2/yearly3 set. The conclusion is unchanged: none of them carry a variant.)

And here is what re-verification found that makes this cheaper than we thought: every build already knows its own name. Read off the served bundles today: VITE_QUIZ_VARIANT is baked in at build time — paywall_v2_2 · control (the /2026/ build) · paywall_v2_3 · quiz_checkout_2026_v1 — and the shared event helper already attaches it to every GA4/Meta event. The name exists client-side in all four builds. It just never reaches Stripe on the legacy rail.

Do this

(a) Get the build name onto the legacy sale — two halves, both needed. The client half: where the 3 old builds compose their /register/… links (shared code — one change, rebuilt into three bundles), append the value the bundle already holds, e.g. ?quiz_variant=paywall_v2_2. The server half — and without this the client line does nothing: the /register/ Django flow currently writes zero metadata (54 of 54 legacy subs empty), so it must read that param and write it into the subscription's Stripe metadata at create, the same key v1 uses (quiz_variant). Same field, same shape, so one query reads all four.

(b) v1 — first-touch rehydration, precisely scoped. Correcting our own earlier line here: a fresh Stripe pull shows 5 of 8 v1 buyers DO carry a utm_source on the subscription (google ×1 with gclid — the only external paid source · tma ×3 internal · bwa ×1 our blog); 3 of 8 are bare. The handoff already forwards the live query string — what is missing is the buyer who lands bare (returns later, direct, internal nav). Fix: persist first-touch utm_*/gclid/fbclid on first landing (localStorage; the Amplitude SDK in the bundle already computes first_touch_*, so the values exist) and have the checkout handoff append them whenever the current query string has none. Keep writing them where you write them today — on the subscription; on this rail the PI carries no UTMs and nothing needs to move.

(c) The checkout system — folded in from the estate-stamp card, same sitting

The quiz rail's subscriptions are already stamped and your client already sends checkout_version:'quiz_checkout' in the create body — but the PaymentIntent drops it (8/8 succeeded quiz PIs since 1 Aug bare, each join-proven quiz-rail; the PI is what your server CAPI keys on) and the client purchase events carry no estate stamp (0 hits in all four bundles). Two writes: copy the field you already receive onto the PI at create-intent, and send checkout_version on the purchase-side events alongside the quiz_variant they already carry. 🔴 This half must land before the ~11 Aug honest read. Full evidence, numbers and acceptance checks: the estate-stamp card below — kept intact because the analytics brief links it.

Done when — one validation pass covers all three

A test sale from each of the four builds carries its build name in subscription metadata (quiz_variant) · the newest quiz-rail PI carries metadata.checkout_version=quiz_checkout and the served bundle greps >0 for checkout_version · a bare-landing test purchase on v1 (land with UTMs → navigate → return with none → buy) still carries its first-touch utm_source. One query then separates the funnels, the estates and the sources. Tell us the field names you used and we will point every report at them the same day.

What we tried first, so you are not repeating it

We grepped all four live bundles twice (7 Aug AM + PM — the PM pass found the link sets have diverged but are equally stampless), read Stripe metadata (54/54 legacy subs empty, re-pulled 7 Aug 15:33 UTC), and ran a $157 price-fingerprint test to infer the split — it failed: sales are decoupled from page traffic (Facebook says 5, Stripe says 19). Attribution needs a write in the checkout handoff + /register/ code, which is in repos we cannot write. Evidence: tma-growth-cockpit-2026.netlify.app/quiz-funnel-versions/ §1 + §18.

The T+1h abandon email — ANSWERED 7 Aug 2026

ANSWERED · acted on the same day Django→SendGrid NIC-90

The question — two lines closes it

(a) Is the ≈T+1h "Your login is in this email" Django→SendGrid send live in production right now? (b) What is the trigger condition and delay, as the code has it today?

🔴 Do not change anything. This is a read-and-report.

Why it matters, and why now

756 Part 1 - Abandoned Cart Reminder is about to be loaded with two recovery emails and armed. Its supply is our abandon sync, which found 43 abandoners worth $4,171 in 30 days. If your T+1h send is live, the same person gets your email at ~1h AND our E1 on the next sync — two recovery emails from two systems that cannot see each other, to someone who just declined a price.

Your answer changes ours, both ways: if it is live we delay E1 past it or drop the overlap; if it is not, we send E1 immediately on entry. Either is fine — guessing is not.

Why we cannot answer this ourselves — we tried three ways

(1) Our Django clone is 5b54ac7e, dated 2 Apr 2026, and predates the machinery entirely — grepping paywall_view/abandon across app/**/*.py returns zero hits, so it cannot show a send built after April. (2) No SendGrid credential exists anywhere in our estate — we searched .env.local and the whole repo for the SG. key pattern. (3) The AC API cannot see it, because it is a Django→SendGrid send outside ActiveCampaign altogether. The fact lives only in the production repo or the SendGrid console. One caveat on our own record: our email master says this send is live, but that came from a log reading, not from the code — which is exactly why we are asking rather than asserting.

checkouts-v3 phantom mint — CLOSED 7 Aug 2026: the mint is fixed AND all 5 stranded subs are cancelled

✅ DONE · both halves all 5 ids re-fetched individually from Stripe, 7 Aug 2026, 19:24–19:28 UTC NIC-92

✅ Closed — both halves; all five subscription ids re-fetched from Stripe

Nothing left to do on this card. On 7 Aug 2026, 19:24–19:28 UTC we fetched each of the five subscriptions individually by id — not a list query, so there is no chance of a filter hiding one. All five return status=canceled, canceled_at 2026-08-07 18:58 UTC, cancellation_details.reason=cancellation_requested. The code half was already closed on 5 Aug and remains so. There are no active unbillable v3 subscriptions left from that window.

Everything below this line is the original card, kept so the diagnosis and the reasoning stay on the record.

✅ Finding 1 is RESOLVED — your defer-mint stopped the phantom mint, and we can see it in the daily counts

The 2.2×/visitor figure came from a window (1 Jul–6 Aug) that is mostly before your NIC-19 defer-mint. Re-pulled 7 Aug 15:35 UTC, v3-stamped mints per day: Jul mean 3.0/day with spikes of 21 and 29 · Aug 1st=1 · 2nd=3 · 3rd=9 · 4th=8 (all eight incomplete_expired ghosts) · 5th=0 · 6th=2 · 7th=0. The two 6 Aug mints are both trialing — real checkout starts. Zero phantom mints since 5 Aug. A visitor who lands and does nothing now leaves no Stripe record — the "done when" on the code half is met, and no further code work is being asked for on this card.

🔴 Finding 2 still stands and it is money — five ACTIVE subscriptions we cannot charge

Re-verified individually 7 Aug 15:35 UTC — all five still active right now, each with no payment method, no paid invoice above $0, no discount, no trial end, all on ad-hoc $24.97/mo prices, minted 27–28 Jul (pre-fix): sub_1TxtC5I6s6pBAbnmWhvmZm6g · sub_1TyF1XI6s6pBAbnmRUud2JzC · sub_1TyF2NI6s6pBAbnmjEUjUbjn · sub_1TyF6dI6s6pBAbnm7jiTRl8z · sub_1TyIEBI6s6pBAbnmehkuB2MI. Sharper than first written: these five are ALL of the currently-active v3 subs from the window — 5 of 5 active are unbillable (3 more sit trialing, a different and billable state). They need a billing call — bill them, or end them, but they cannot stay as they are.

The shape of the real funnel, for context

LayerCountWhat it tells us
GA4 visitors53the true denominator
Stripe subscriptions minted1172.2× the visitors — the defect
Cards entered4real purchase intent
Payments1the actual sale
Cancellations58all cancellation_requested (API-initiated); zero payment failures — so this is not churn, it is cleanup of records that should never have existed

Every conversion rate computed off the minted number is wrong by 2.2×, in the flattering direction. (This table describes the mostly pre-fix window — kept so nobody re-reads the era's numbers as current. The v3 stamp first appears 17 Jul, so the window effectively starts there.)

Done when

The 5 unbillable actives are resolved one way or the other — that is the whole remaining task. The other half ("a visitor who does nothing leaves no Stripe record") is verified met since 5 Aug; we re-run the daily-mint count after any v3 deploy. Per-layer diagnosis: tma-growth-cockpit-2026.netlify.app/quiz-funnel-versions/ §13 Step 3.

Noindex the non-production hosts — CLOSED 7 Aug 2026: all three hosts serve the header

✅ DONE 3 hosts + 4 app routes curled 7 Aug 2026, 19:24–19:28 UTC NIC-43

✅ Closed — all three hosts serve the header, and we re-checked the app routes too

Nothing left to do on this card. curl -I on 7 Aug 2026, 19:24–19:28 UTC:

quiz-test. → 200 with X-Robots-Tag: noindex, nofollow · monitor. → 302 to /login with the header · grafana-dev. → 302 to /login with the header. The security item on grafana-dev is closed along with the SEO one.

We also re-checked the Django half rather than assuming it still held: /login, /register/monthly_2026/, /payments/quiz-checkout/ and /payments/checkouts-v3/trial/1month/ all return 200 with the header, and app-test. inherits it. staging. and beta. are Cloudflare and remain Aga’s under AGA-67 — they were never on your list.

Everything below this line is the original card, kept so the diagnosis and the reasoning stay on the record.

✅ 6 Aug — the half that mattered is shipped, and we verified all four routes

curl -I against production returns X-Robots-Tag: noindex, nofollow on:

app.themovementathlete.com/login                      X-Robots-Tag: noindex, nofollow
app.themovementathlete.com/register/*                 X-Robots-Tag: noindex, nofollow
app.themovementathlete.com/payments/quiz-checkout/    X-Robots-Tag: noindex, nofollow
app.themovementathlete.com/payments/checkouts-v3/*    X-Robots-Tag: noindex, nofollow

That was the pricing-exposure half, and it is closed. app-test. inherits the same Django headers, so that host is covered too. Three hosts remain, and they are all server-level rather than Django — which is why they did not come along for the ride:

HostChecked live 5 AugStill needs
quiz-test.200, no headernginx add_header
monitor.302, no headernginx add_header — its Disallow: / does not de-index
grafana-dev.302 → /login, no header🔴 nginx header and a look at why it is publicly reachable
staging. · beta.200, no headerNot yours — Cloudflare, Aga (AGA-67)

Context, because it changes the fix: the /register/ promo URLs are intentional unlisted-URL offers — public by design, reached only by direct link. Aga is keeping them. So the job is not to redirect or remove anything. It is to make them reachable by link and invisible in search, which is exactly what noindex does.

Verified live 3 Aug. app.'s robots.txt disallows only /survey, /skills/, /workouts/, /achievements//register/ and /login are NOT blocked, so Google can crawl them and will see a noindex on the next crawl. That is the good case; no robots.txt surgery needed.

HostLiverobots.txtServed by
app.200real, 4 rulesnginx
app-test.302 → 200same as app.nginx
quiz-test.200none (SPA returns HTML)nginx
grafana-dev.200 → /loginsame as app.nginx
monitor.200 → /loginDisallow: /nginx
staging.200nonecloudflare → Aga (AGA-67)
beta.200nonecloudflare → Aga (AGA-67)
How — your half (the nginx hosts)
  1. Send X-Robots-Tag: noindex, nofollow on app./register/* (blanket). This also covers the live main routes /register/monthly_2026/ and /register/yearly3/that is intended: register and checkout pages should never rank.
  2. Same header on app./login — an indexed /login URL is carrying a full GA linker blob (_gl / _ga / _ga_ZBQN3P8WKW, 71 impressions). Tracking parameters should never be crawlable.
  3. Same header on app-test., quiz-test. and grafana-dev.. 🔴 grafana-dev. is a monitoring dashboard, publicly reachable, and was missing from the original scope — treat that as a security item, not an SEO one. quiz-test. was also missing.
  4. monitor. already serves Disallow: /, but a Disallow does not de-index — it needs the header too.
  5. Basic auth on the non-production hosts is the stronger fix — it de-indexes and stops anyone reaching them. Flag it if it would break a tester or CI workflow before applying.
🔴 The one ordering rule

noindex FIRST. Never add a robots.txt Disallow first. Blocking the crawl stops Google ever seeing the noindex, which freezes the URLs in the index as bare links — the opposite of the goal. Let them be crawled, see the noindex, and drop out. Only then consider disallowing.

No effect on the offers. Anyone with a direct link still reaches these pages and can still buy. noindex only removes them from search results.

Done when

curl -I on each host and route returns X-Robots-Tag: noindex, and the hosts plus the two promo URLs fall out of the TMA Search Console property within about two weeks.

Fix the TMA SPF record — SHIPPED & verified live 5 Aug

DONE 5 Aug 2026 · verified at the authoritative NS SCOPE: themovementathlete.com ONLY ~15m + 20m validation DNS zone edit — no code NIC-79

✅ 6 Aug — CLOSED, and we checked it the right way

Verified with dig +short @ns1.siteground.net themovementathlete.com TXTthe authoritative nameserver, not a public resolver. That distinction earned its keep here: a public-resolver answer showed no SPF at all while the zone was serving one. The record now reads:

v=spf1 a mx ip4:35.214.157.236 include:spf.tapfiliate.com
       include:themovementathlete.com.spf.auto.dnssmarthost.net
       include:_spf.google.com ~all

The dead include:spf.acemsb4.com is gone and include:_spf.google.com is present — the two things that mattered. You kept mx where we suggested dropping it, and that is fine — we are not asking you to touch it again. It costs one DNS lookup and the record still sits inside the RFC 7208 limit of ten. Your SiteGround screenshot is on Slack. The original diagnosis is kept below for the record only — no action in it.

📋 For the record — the fault this fixed

Pulled from a real Authentication-Results header on a message in the hello@ mailbox, dated 4 Aug 2026:

spf=permerror (google.com: permanent error in processing)

DMARC is currently passing only because the Google Workspace DKIM key (google._domainkey) is signing and aligned. There is no second line of defence. If that key is ever rotated badly — or the mail is forwarded, which breaks DKIM — p=reject means the message is rejected, not junked. Silent and unrecoverable.

The two faults

1. include:spf.acemsb4.com does not exist. It is NXDOMAIN. Under RFC 7208 §5.2, an include: whose target has no SPF record returns permerror — and because SPF mechanisms evaluate left to right and stop at the first match, anything not matched earlier falls into it and never reaches ~all.

2. The one that actually matters: there is no include:_spf.google.com. The record leans on mx to cover Google, and that does not work:

# what "mx" authorises — Google's INBOUND servers
aspmx.l.google.com      172.253.145.26
alt1.aspmx.l.google.com 173.194.42.27
alt2.aspmx.l.google.com 142.251.96.27

# where Workspace actually SENDS from (_spf.google.com)
ip4:74.125.0.0/16   ip4:209.85.128.0/17

# overlap: none. So every hello@ / hi@ / nic@ send walks the whole
# record and lands in the dead include above.

✅ Do NOT touch ActiveCampaign or SendGrid — both are correct

Both send with their own envelope subdomains, each carrying a valid SPF record and aligned DKIM. Verified dmarc=pass in real headers. The acemsb4 include is vestigial — AC never consults it, so deleting it changes nothing for them.
• ActiveCampaign → em-152341.themovementathlete.com, DKIM acdkim1
• SendGrid → em9638.themovementathlete.com, DKIM s1

Step 1 — replace the root SPF record

TMA's zone is authoritative at ns1/ns2.siteground.net, so this is the SiteGround DNS zone editor. If the Cloudflare migration has already moved the zone by the time you do this, do it there — the values are identical.

Edit the TXT record on themovementathlete.com whose value starts v=spf1. Leave every other TXT record alone (the google-site-verification and _globalsign ones must stay).

FROM:

v=spf1 a mx ip4:35.214.157.236 include:spf.tapfiliate.com include:themovementathlete.com.spf.auto.dnssmarthost.net include:spf.acemsb4.com ~all

TO — copy this exactly:

v=spf1 a ip4:35.214.157.236 include:_spf.google.com include:spf.tapfiliate.com include:themovementathlete.com.spf.auto.dnssmarthost.net ~all

Three changes: the dead include:spf.acemsb4.com is removed, include:_spf.google.com is added, and mx is dropped (it only authorised Google's inbound relays, which never send our mail — dropping it buys back a DNS lookup). Keep ~all, do not change it to -all this pass.

Measured lookup budget after this change: 7 of the 10 RFC 7208 allows.

Step 2 — fix the malformed record on the app subdomain

The TXT record on app.themovementathlete.com is missing a space before ~all, which makes it an include: of a domain that does not exist — a second permerror — and leaves the record with no terminal all at all:

# current — note "themovementathlete.com~all" is being read as one hostname
v=spf1 +a +mx +ip4:35.214.157.236 +a:spf.tapfiliate.com +a:tapfiliate.com +mx:spf.tapfiliate.com +include:spf.tapfiliate.com +include:themovementathlete.com~all

Replace the whole value with:

v=spf1 include:_spf.google.com include:spf.tapfiliate.com ~all

The +a: / +mx: mechanisms pointed at Tapfiliate were burning DNS lookups to authorise hosts that never send as app.. This takes it from 9 lookups saying nothing to 3 saying something true.

Deliberately NOT in this task

DMARC is currently p=reject; sp=none. The sp=none leaves every subdomain with no policy and should eventually be tightened — but not in the same change as the SPF fix. If anything regresses we need to know which edit caused it. We will raise sp= as a separate task after a fortnight of clean reports. Do not change the DMARC record.

Step 3 — verify (do not skip; this is the part that proves it)

Wait for the TTL to expire, then confirm the record is actually being served and that the chain resolves clean:

# 1. the new record is live
dig +short TXT themovementathlete.com @1.1.1.1 | grep spf1

# 2. the dead include is gone and Google is present
dig +short TXT themovementathlete.com @1.1.1.1 | grep -c acemsb4      # must be 0
dig +short TXT themovementathlete.com @1.1.1.1 | grep -c _spf.google  # must be 1

# 3. the app subdomain
dig +short TXT app.themovementathlete.com @1.1.1.1

Then the real test — send one email from hello@themovementathlete.com to a Gmail address you control. Open it, Show original, and confirm the header now reads:

spf=pass ... smtp.mailfrom=themovementathlete.com
dmarc=pass (p=REJECT ...)

Send us that header block and the task closes. Until an spf=pass is seen in a real message, this is not done — a correct-looking record is not the same as a passing one.

Timeline — implementation + validation

~15 min to make the two record edits, +20 min to wait out the TTL, validate the values against the live zone, run the checks and send the test message. ≈35 min total. If the SiteGround panel disagrees with anything written above, trust the panel and tell us — these values were derived by resolving the live DNS from outside, not from inside your control panel.

Stamp checkout_version on the quiz rail’s payment intent and client events — folded into the attribution-stamps card (Aga, 7 Aug)

FOLDED · write (c) of the attribution stamps — one sitting, one validation pass Django create-intent + quiz bundle events — no release Task 30 on the analytics brief

✅ What is already working — re-measured 7 Aug from 1 Aug only, on Aga’s instruction

The new checkouts released Friday 31 Jul, so the window is 1 Aug onward — 75 subscriptions, pulled exhaustively (paginated, not a top-100). Your subscription-level stamp works on both estates: 50 carry checkout_version=quiz_checkout (with quiz_variant=quiz_checkout_2026_v1) and 23 carry checkout_version=v3. The remaining 2 are not checkout sales at all — see the resolved note below. So at the subscription level, “which checkout produced this money” is answerable in Stripe today. (Denominator honesty, from the 7 Aug PM re-pull: 53 of the 75 are incomplete_expired page-load ghosts — the real-activity split is 22: quiz_checkout 9 · v3 11 · dj-stripe 2. The by-estate conclusion is identical on either basis.) This card is about the two layers that still miss the stamp — not a claim that nothing works.

🔴 Gap 1 — the PaymentIntent never gets the stamp (the subscription does)

Re-verified 7 Aug, 1 Aug onward, exhaustively (102 PIs): 8 “Subscription creation” PIs succeeded — 7 at $97, 1 at $12.48 — and not one carries checkout_version (metadata empty or just password_set_at_checkout). And we closed the loop this time: we walked each of the 8 through invoice → subscription, and every linked subscription IS stamped quiz_checkout — so all 8 are proven quiz-rail sales whose PI alone is blind. checkouts-v3 stamps the PI at create — its live stripe-client.js sends {checkout_version:'v3', plan, attribution} in the create body, and 12 v3-stamped PIs sit in the same window (precision so nobody misreads them: all 12 are canceled $0 abandoned mints, zero collected — they prove the v3 write works, not that v3 sold 12). And the second verification pass found the good news: your quiz client already sends the field too. The live /static/quiz_checkout/stripe-client.js create body reads {plan_key, email, attribution: qsAttr(), checkout_version: 'quiz_checkout'} — the value arrives at the server on every mint and lands on the subscription but not the PI. So Gap 1 is a pure server-side copy of a field you already receive.

Why the PI matters even though the sub is stamped: the PI is the object your server CAPI keys on (event_id = PI id at /payments/analytics/stripe-purchase/), the object refunds and disputes reference, and the object any PI-level export reads. Today every one of those needs a join through invoice→subscription to learn the estate; one metadata write at create-intent removes the join.

🔴 Gap 2 — the client events: 0 hits in all four live quiz bundles

Re-pulled 7 Aug: checkout_version appears 0 times in the served bundles of /2026-quiz-checkout-v1/ (index-yC00xO94.js — note it was rebuilt again since 6 Aug’s index-DFwXN-qe.js), /2026-v2-3/, /2026/ and /2026-v2-2/. The funnel events do carry quiz_variant (screen_view/page_view/fbq — good), but the purchase-side events carry no estate stamp, so GA4 and Meta can only split a sale by URL heuristics — which break every time a path changes, which is exactly what the last week was.

The consequence with a date on it: the ~11 Aug honest read of the $97 paywall needs per-estate RPV (the <$6.04 kill-switch). Without the stamp that number is a hand-built join instead of one GA4 filter.

✅ RESOLVED — the “unexplained subs” question is withdrawn, no action

An earlier draft of this card asked you to name the path behind 4 metadata-less subs. Aga corrected the window — the new checkouts released Fri 31 Jul, so the 2× Quarterly V2 (31 Jul) are pre-release and don’t count. We traced the remaining 2 ourselves (Monthly V3, 2 + 6 Aug): both minted by Django/dj-stripe provisioning, not a checkout — a $0 subscription_update invoice at creation, no payment intent, customer metadata djstripe_subscriber + tma_user_uuid. Nothing mints sales outside the two stamped estates. Question closed.

What to do — two writes, one sitting

  1. Server: at the quiz rail’s create-intent (the handler behind /payments/quiz-checkout/), copy the checkout_version the request body already delivers into the PaymentIntent metadata at create — the same write the sub already gets, one object earlier. Genuinely one line: the client half is verified already shipping.
  2. Client: in the quiz checkout’s purchase-side events (dataLayer/gtag/fbq), send checkout_version:'quiz_checkout' alongside the quiz_variant the screen events already carry — same shape v3’s analytics.js emits, so both estates report identically.

How we will both know it worked

# 1. next real quiz sale — the PI itself carries the estate
stripe payment_intents list --limit 5   # newest quiz PI: metadata.checkout_version = quiz_checkout

# 2. the served bundle carries the stamp
curl -s https://quiz.themovementathlete.com/2026-quiz-checkout-v1/assets/index-*.js | grep -c checkout_version   # must be > 0

We re-run both checks the day you say it shipped — same as every card on this page.