🔒 Gate (Aga, 6 Aug 2026): nothing enters this brief without Aga's approval AND full live verification. Every task currently here carries both.
At a glance — 16 open (10 Nic-side · 6 store-release) — 80/20 re-cut 6 Aug
Analytics and the launch blockers around it, in order. P0 = blocks revenue / a promise already made on a page / seeing a sale at all. P1 = points the money right. P2 = improves what works. REL = needs an App/Play-store release (external RN team). Click any # to jump to that task’s full steps.
Reorganised 24 Jul: open items first (by priority), done items banked & struck through at the bottom. The through-line: the funnel is still partly blind — web trial→paid, gclid→Stripe and BWA/source attribution are DARK, so no big bet (scale paid, pour into BWA, CRO the paywall) can yet be picked on evidence. The top open items light that up.
Attribution / UTM-redirect fixes (nginx Fix 1, quiz-root 301→302, _gl preservation) are a SEPARATE brief → tma-nic-attribution-brief — not duplicated here.
If only four things get done, they are these — in this order. Together ~2.5h of console/build time, ranked by measured $-per-minute against the cash gap. Re-cut 7 Aug PM on live verification.
- №1 · R6 — kill
Competitive Non-Branded | US(id 24012014523). Re-verified 7 Aug PM: still ENABLED, £206.74/30d, 0 conversions, spent every one of the last 14 days. All 66 keywords are equipment-PURCHASE queries — wrong intent class; keyword surgery cannot save it. ~$260/mo [MEASURED] back for ~5 min. console - №2 · Task 4 — Meta. First UN-PAUSE the 3 OUTCOME_SALES retargeting campaigns (7 of the account’s 8 pixel purchases on 9% of spend — ~5 min), then REBUILD prospecting as OUTCOME_SALES optimised to
InitiateCheckout— objectives are immutable, so it is a build, not a toggle (sheet on the card). CAPI gate closed: 201 server Purchase events/28d. console + build, ~90 min - №3 · Task 8 — Google, in the SAFE order: kill the 4 armed double-count Purchase actions + demote add_to_cart/begin_checkout (zero delivery risk, ~15 min). 🔴 HOLD the lead-category demotion until purchase values arrive — 7 of 12 purchases carry £0 value (no tROAS; PMax would stall). The £123k fake value sits on a PAUSED campaign; the live poison is the LEAD category at 76% of bid signal. console
- №4 · Task 5 — AppsFlyer, now a 20-MIN AUDIT: the RC→AppsFlyer wiring already exists and fires (
af_purchase61/$4,037 per 30d — the old “21 vs 44” was an Amplitude proxy error, struck). Real residual: 2,384 paid customers → 0 conversions/180d = Task 17 (identity) or traffic quality. dashboard - Task 30 — stamp
checkout_version: → moved to Nic's attribution brief 7 Aug PM, folded by Aga into one sitting with NIC-91 (“the attribution stamps”). One home per task — the full re-verified evidence lives there.
The old rider is GONE from this board (7 Aug PM): Task R (RETURN_URL) verified FIXED live — all three v3 pages render https:// — and Task N (noindex hosts) moved to Nic’s attribution brief (NIC-43). One home per task.
Banked by the 6 Aug verify: Task 1 ✅ (8 rc_* events landing, rc_initial_purchase 44/30d — residual $amplitudeUserId folds into Task 17) · Task 34 ✅ (the pipeline's token already carries instagram_basic + instagram_manage_insights and pulled live IG data today — 43,441 followers read back) · the Claude Stripe cohort row ✅ — web trial→paid = 30.0% (24/80 real-email trials, 8 May–23 Jul cohort, 22 phantoms excluded) vs app 62%. The rest of the 80%: 11 is UNBLOCKED and TAKEN BY THE FLEET 7 Aug (Aga-approved; zero Nic) — Intercom turns out to hold the whole funnel state per contact with email; the census is running and segment D went to the Abandonment Fleet · 10 is CLOSED 7 Aug, not merely downgraded — UTM survival verified passing live, the source split already proven, and the one true residual (GA4 user_id) is Task 17’s work · 16 stays blocked on 5+8 (all UACs verified PAUSED) · 26 + the six REL rows are external. The step-by-step blocks below keep their old grouping — the order is this box and the table.
| Priority | # | Task | Owner | Where | Release? | Status |
|---|---|---|---|---|---|---|
| ⚡ THE 20% — DO THESE FOUR, IN THIS ORDER (this is where the money is) | ||||||
| ⚡ №1 | R6 ↓ | Kill the junk campaign — Quiz Funnel | Competitive Non-Branded | US (id 24012014523) RE-VERIFIED LIVE 7 Aug PM via the Ads API: still ENABLED, £206.74/30d · 25 clicks · 0 conversions · spent every one of the last 14 days incl. £7.91 today. All 66 keywords are equipment-PURCHASE queries (bars, machines, gear) — wrong intent class, so the 59 never-served keywords cannot save it. ~$260/mo [MEASURED] for ~5 min. Detail below. | Nic | Google Ads console (690-916-9207) | 🟢 no | ☐ open — ~5 min, best $/min on the board |
| ⚡ №2 | 4 ↓ | Switch Meta off the Lead objective → Purchase RE-VERIFIED 7 Aug PM: still all OUTCOME_LEADS — and it is a REBUILD (objectives immutable). Cheapest revenue first: un-pause the 3 OUTCOME_SALES retargeting campaigns (7 of 8 purchases on 9%% of spend), then build the InitiateCheckout campaign — full sheet on the card | Nic | Meta Ads Manager | 🟢 no | ☐ open — 20 min |
| ⚡ №3 | 8 ↓ | Google Ads — demote the proxy conversions, keep Purchase primary RE-VERIFIED 7 Aug PM: fake value sits on a PAUSED campaign — live poison is the LEAD category (76%% of bid signal) + 5 primary Purchase actions armed to double-count + 7/12 purchases £0 value (no tROAS). Safe order on the card | Nic | Google Ads console (690-916-9207) | 🟢 no | ☐ open — 30 min |
| ⚡ №4 | 5 ↓ | T5 — AppsFlyer: 20-min config audit RE-VERIFIED 7 Aug PM — the wiring ALREADY EXISTS and fires (7 rc_* columns live; af_purchase 61/$4,037 per 30d); the “21 vs 44 under-count” was an Amplitude proxy error, struck. Real finding: 2,384 paid-media customers → 0 conversions/180d (= Task 17 identity or traffic quality). Just audit the mapping + “attributed users only” toggle — steps on the card. | Nic | RC dashboard + AppsFlyer | 🟢 no | ☐ open — 20 min audit |
| 🕰 THE 80% — waits, blocked, or external. None of these before the four above. | ||||||
| P2 | 16 ↓ | Rebuild UAC → optimise to the trial event all UACs verified PAUSED 6 Aug | Claude → Nic | Ads console | 🟢 no | ☐ blocked on 5 + 8 |
| P2 | 26 ↓ | Email deep-link infra — OneLink template + ESP wiring | Nic | AppsFlyer + CIO/AC | 🟢 no | ☐ open — later |
| REL | 17 ↓ | T4 — canonical user identity (keystone) | external RN team | App code | 🔴 yes | ☐ open |
| REL | 18 ↓ | T7 — native product + lifecycle events | external RN team | App code | 🔴 yes | ☐ open |
| REL | 22 ↓ | RevenueCat Paywalls SDK — remote paywall & price control | external RN team | App code | 🔴 yes | ☐ open |
| REL | 23 ↓ | RN‑5 — iOS ATE/ATT + AppsFlyer always-init | external RN team | App code | 🔴 yes | ☐ open |
| REL | 24 ↓ | RN‑6 — install-UTM stitch + ➕ email deep-link add-on | external RN team | App code | 🔴 yes | ☐ open |
| REL | 25 ↓ | RN‑7 — screen-level tracking | external RN team | App code | 🔴 yes | ☐ open |
| P2 | R23 ↓ | Work the /register page as CRO — BLOCKED on Aga’s decision 32 moved off the revenue plan 7 Aug — still open, full text below | Nic | /register/<plan>/ — Estate B | 🟢 no | ☐ open — BLOCKED · Aga decides first |
| P3 · LAST | R16 ↓ | The landing screen — the biggest single loss (ranked LAST, deliberately) moved off the revenue plan 7 Aug — still open, full text below | Nic | Quiz landing screen | 🟢 no | ☐ open — re-ranked LAST |
The tasks, in priority order
STEP-BY-STEP DETAIL
the ORDER lives in the ⚡ 80/20 box + the table above — blocks below keep their old grouping▶5 T5 — Fix the AppsFlyer conversion events ★ highest leverage broken🟢 no releaseNic · 3–6h
rc_* event columns live; af_purchase 61/$4,037 per 30d). 🔴 The “21 vs 44” was an Amplitude proxy error — AppsFlyer’s own report says 61; reading AppsFlyer health out of Amplitude violates platform-own-truth. The real break: RevenueCat’s own attribution shows 2,384 paid-media customers → 0 conversions in 180d (FB 1,334, Google 838) while ASA converts 21% — either identity fragmentation (= Task 17) or traffic quality (= a budget call), and neither is fixed by wiring. Nic’s 20-min audit + the 5-min dashboard read + the new acceptance test: docs/company/ANALYTICS/NIC_EXECUTION_PACK_2026-08-07.md. Android paid 90d: 4,236 installs → 2 purchases vs organic 862 → 27 — confirmed and worse than the card said.af_purchase under-counts, 21 vs 44 real rc_initial_purchase.⚙️ What it enables: per-channel Cost-per-Trial and CAC — the number the $3,000/mo cap decision runs on — and true purchase postbacks for Meta and Google to optimise toward.
🔓 What it unblocks: Task 16 (UAC rebuild is gated on this), Task 4's optimisation event arriving reliably, the revenue plan's “no scale of paid before a real conversion event” hard stop, and the cockpit's DARK CPT tile.
af_purchase currently fires ~2× against 194 real subscribers$appsflyerId IS present. It appears on every recently-active customer profile (6/6 with July activity, each also carrying $idfa and af_install_time); it's missing only on dormant profiles last seen months ago — i.e. the current app build already writes it. So this is a dashboard toggle, not a release. No RN-team involvement needed.- RevenueCat → Integrations → AppsFlyer → connect (dev key + app ids). The
$appsflyerIdattribute is already there, so purchases will post server-side with the correct media source. - Enable the events:
af_start_trial,af_purchase,af_subscribe— each carrying revenue + currency. - One nuance: dormant users (last active before ~June) have no
$appsflyerId. That's fine — purchases come from active users, so postbacks will attribute correctly going forward.
af_purchase count reconciles to RevenueCat purchases within ±5% over a 7-day window, and purchases appear against the correct media source (not just Organic).
▶1
Finish the RevenueCat → Amplitude connection — ✅ VERIFIED DONE 6 Aug
✅ done🟢 no releasewas: Nic · 5 min
rc_* events landing with canonical names and real volume — rc_initial_purchase 44, rc_cancellation 49, rc_renewal 18, rc_trial_started 9. The trial denominator is live. The only residual is $amplitudeUserId identity, which is Task 17's keystone anyway — it folds there, and this row is banked. (Was, 24 Jul: 7 rc_, rc_trial_started 3/30d.)proj4a978c73 → Integrations → Amplituderc_* events are now in Amplitude with the exact canonical names (incl. rc_renewal and rc_trial_started, so the trial denominator is live). Remaining: enable the last few lifecycle toggles + reconcile ±5% vs RC — then Lead→Trial and Trial→Paid read end to end.- Open the Amplitude integration you already configured.
- In the event list, enable every remaining lifecycle event:
rc_trial_started,rc_trial_converted,rc_trial_canceled,rc_renewal,rc_uncancellation,rc_non_subscription_purchase,rc_subscription_paused,rc_expiration,rc_billing_issue,rc_product_change,rc_transfer,rc_web_purchase_redeemed,rc_experiment_enrollment. - Keep the
rc_prefix and the exact names above (they're the locked taxonomy — don't use RevenueCat's verbose defaults likerc_renewal_event). - Identity gap found 10 Jul: no customer profile carries
$amplitudeUserId. RevenueCat's events will therefore attach to RC's own ids, not to the Amplitude user ids the rest of our product events use — so anrc_initial_purchasecan't be joined to that same person's onboarding or workouts. Until Task 17 (T4) ships, set the RevenueCat subscriber attribute$amplitudeUserIdso the two systems agree on who the user is. - Once
af_purchaseis fixed (Task 5), accept Amplitude's merge suggestion forpurchase+rc_initial_purchase— but leaveaf_purchaseout of the merged view until then, or it poisons the metric.
rc_renewal appears in Amplitude within 24h and its volume reconciles to within ±5% of RevenueCat (194 subs, ~6 renewals/day). Claude verifies via the API and reports the counts.P1 · detail blocks
order per the table above▶8 GA4 → Google Ads conversion import ("Task 3" in the 16-Jul message) Lead + Purchase tags shipped 20–21 Jul — verify in UI🟢 no releaseNic · 30 min
docs/company/ANALYTICS/NIC_EXECUTION_PACK_2026-08-07.md.generate_lead — and add_to_cart is still a PRIMARY action carrying £123,634 of fake conversion value (562 all-conv), begin_checkout another £108,126. The remaining fix is a 30-min demotion: set add_to_cart / begin_checkout / the Lead actions to SECONDARY (observation), leave Purchase as the one primary. 30d spend context: £929.75 — Competitors-Branded £265, Non-Branded £220, Competitive Non-Branded £199 (24 clicks, 0 conv), PMax £147, Branded £27.177471782) + Google Ads console (account 690-916-9207)add_to_cart is still PRIMARY carrying £123,634 of fake conversion value and begin_checkout another £108k, so the £930/mo Google budget optimises toward people who click, not people who pay.⚙️ What it enables: ROAS bidding on real money; per-campaign verdicts that mean something — e.g. Competitive Non-Branded's £199/30d → 0 conversions becomes a decidable kill instead of an artefact.
🔓 What it unblocks: Task 16; every keep/kill/scale decision on the Google account; the gclid→Stripe ROI read the plan requires before any cap raise.
generate_lead (verified live on the quiz). Full analysis + proof: docs/company/ANALYTICS/TRACKING-SYSTEM/. ✅ 22 Jul: workspace confirmed published (Nic). Remaining: after checkout testing (Task 19) completes, run one ads-side test to confirm the conversions appear in the Ads UI on a fresh payment. Consequence: Ads is not inheriting GA4's 1.52× double-fire — but the GTM route has its own checklist: ① the tag must send a transaction_id/Order ID (Ads dedupes repeat order-ids; without it a success-page refresh re-fires just like GA4) ② keep ONE primary conversion action — if the GA4 import is ever enabled too, set it to SECONDARY/observation or Ads double-counts at its own level ③ the tag must fire on BOTH Lead→Paid paths (trial-start success AND direct-purchase success, per the green box) ④ conversion VALUE → ROAS — dataLayer push of value + currency + transaction_id on the success pages (Nic: GTM work + success-page rewiring, likely bundled with the checkout-pages testing sprint and with the meter/Task 9 as one job). Original context: Google had never received a purchase signal. That's the whole reason its campaigns bought 1,230 installs and zero attributable subscribers — the algorithm optimised toward the only thing it could see: cheap installs.- (Steps 1–4 = the original GA4-import route — SUPERSEDED by Nic's independent GTM tag, 20 Jul. Keep for reference; if ever enabled alongside the GTM tag, the imported action must be SECONDARY.) GA4 → Admin → Product links → Google Ads links → link account
690-916-9207. - GA4 → Admin → Events → mark
purchaseas a key event. - Google Ads → Goals → Conversions → Import → Google Analytics 4 → select
purchase. - Set it as the campaign's conversion goal. (Do this after Task 9 fixes the purchase event, or you'll import a broken signal.)
▶4 Switch Meta campaigns OFF the Lead objective → Purchase blocking all cold spend🟢 no releaseNic · 20 min
OUTCOME_LEADS. 🔴 Meta objectives cannot be changed after creation — this is a ~60–90 min REBUILD, not a 20-min toggle. 🔴 The card’s suggested events are EMPTY: StartTrial=3, Subscribe=2, fb_mobile_purchase=17 per 28d — unusable; and cold Purchase needs ~£13.8k/mo to exit learning. Optimise the new OUTCOME_SALES campaign to InitiateCheckout (92/30d, fits the envelope at ~£1.2k/mo). The CAPI gate is now CLOSED: pixel 2128008637263218 received 294 Purchase events (201 SERVER) in 28d — the flip is unblocked (dedupe itself still [UNVERIFIED]). Cheap win first: the 3 PAUSED OUTCOME_SALES retargeting campaigns produced 7 of the account’s 8 purchases on 9% of spend — un-pause them. Full build sheet (budgets, adset ids, 4 creatives to duplicate, exclusions incl. the missing customer-exclusion) is ready: docs/company/ANALYTICS/NIC_EXECUTION_PACK_2026-08-07.md. Spend context: Meta is ~£752/mo run-rate now, not £2.7k.OUTCOME_LEADS — QLG | ADV+ ON | US | Variant 2, MOF - Lead Form, QLG | ADV+ OFF | MC | Control — and their ad sets optimise to LEAD / LEAD_GENERATION. Nothing has moved since this task was written. Gate: flip only after the CAPI purchase verify (one real purchase seen with matching event_id).OUTCOME_LEADS, optimising to LEAD — an algorithm trained on 5,171 Leads vs 172 Purchases, which is why 2,878 installs produced one purchaser. Every pound of Meta spend (~£2.7k/mo) is still buying form-fillers today.⚙️ What it enables: Meta's budget hunts BUYERS instead of form-fillers — the single biggest lever on paid traffic quality, at zero extra spend.
🔓 What it unblocks: the “No Meta spend — warm OR cold” hard stop; the plan's lever ① (paid working inside the cap, modelled +$3,720/mo at full effect); honest creative verdicts — today creative is blamed for what the objective caused.
- Every active campaign → objective OUTCOME_SALES (not Leads, not App Installs).
- Ad set → optimisation event = Purchase — i.e. the
fb_mobile_purchaseapp event mapped in Task 2 (only once Tasks 2 & 3 are verified — never optimise to an event that isn't arriving). Not Subscribe — see the note below. - Exclude existing subscribers and active app users from all cold audiences.
- Meta app-install campaigns stay permanently off. They are not being restructured; they are retired.
Subscribe, so Meta's "Subscribe" now includes renewals (~6/day) and is not a clean new-subscriber count. Optimise the campaign toward fb_mobile_purchase (actual purchases) or StartTrial — never Subscribe — or Meta chases an inflated signal. True new-sub count = reconcile vs Stripe/RC (the capture-all-then-reconcile approach Task 3 locks in). The mapping itself is correct — it's the only one Meta offers; this is only about the objective / optimisation event.fb_mobile_purchase (Purchase) — never Subscribe.▶11 Export each user's “last funnel step” to Customer.io / ActiveCampaign ★ best ROI on this page free revenue🟢 no releaseFLEET · shipped 7 Aug
%TMA_FUNNEL_STAGE% + _AT) now exist and stamp nightly from Intercom (com.tma.funnelstage 02:30) — the quiz-tag dependency this row cited was never the substrate, Intercom was, and no Nic time was needed. Stall pools live today: survey_unfinished 4,356 · no_workout 6,775 · no_trial ~1,800; segment D (checkout-abandon) carved out to /abandon-fleet by Aga. This row starts the day the stages stamp; do not double-assign it here.⚙️ What it enables: four addressable segments emailed at £0 — est. $1,000–2,600/mo recoverable; the free version of the warm-pool retargeting the plan ranks as our best paid audience (~570 paywall-seen + ~450 buy-clicked per 90d).
🔓 What it unblocks: the non-converter email lanes; payer-seeded lookalikes later. ⚠ Starts the day the five stage tags flow — that substrate is with Nic on his own brief; do not double-work it from here.
The four segments to stamp on each user:
| Segment | Definition | What they get |
|---|---|---|
| A · survey unfinished | signed up, AppOnboarding_started, no AppOnboarding_completed | “Finish setup — 2 minutes” |
| B · no workout | onboarding complete, no workout_started | “Your 3 free workouts are waiting” |
| C · workout, no trial | workout_started, no trial_started | “Here's what week 2 looks like” → free trial |
| D · paywall, no trial | premium_offer_viewed, no trial_started | 72h later: $9.99 / 30-day step-down |
- Compute each user's furthest funnel step (from the backend's own event/state data — no need to query GA4).
- Write it to a Customer.io / ActiveCampaign contact attribute, e.g.
funnel_stage=survey_unfinished | no_workout | no_trial | paywall_seen, pluslast_seen_at. - Refresh nightly.
▶10
Make the quiz funnel segmentable by traffic source in GA4 — CLOSED 7 Aug
✅ closed🟢 no app releaseNic · 1–2h
quiz.themovementathlete.com/?utm_source=nictest&utm_medium=cpc&utm_campaign=probe 302s to /2026-quiz-checkout-v1/ with all three parameters intact. (2) sessionSource populated — proven 6 Aug by the filtered pull below. (3) GA4 user_id at email capture — the only genuinely open piece, and it is Task 17’s work (canonical user identity, external RN team): the served quiz bundle’s only user_id references are Amplitude SDK internals, and it ships no GA4 measurement ID at all. A row whose headline names a dead ask is a row nobody can read — that is why this one looked open when it was not. Folded into Task 17.The 6 Aug downgrade, kept for the record — 🔎 DOWNGRADED 6 Aug: GA4 already splits the quiz host by source — verified with a filtered pull (never a top-N list): 6,840 quiz-host sessions/28d incl. google/cpc 356 + fb/ig paid 601, and
generate_lead splits too (372 total, 166 paid-sourced). The paid-vs-organic question is answerable today. Residual is only: UTMs surviving the landing→quiz hop, and user_id at email capture for the web↔app join.generate_lead incl. 166 paid-sourced).⚙️ What it enables: the residual only: UTMs surviving the landing→quiz hop, and
user_id at email capture so web and app join later.🔓 What it unblocks: clean per-source CRO verdicts on the quiz (paid ≥47% start-rate target); feeds the Task 17 identity join.
- Ensure UTMs survive the landing → quiz navigation (don't strip query params on redirect).
- Confirm
sessionSource/firstUserSourceare populated on the quiz page-view and screen events. - Set GA4
user_idonce the email is captured, so web and app join later.
▶R6 Kill the junk traffic — the last live dead placement moved from the revenue plan · 7 Aug🟢 no releaseNic · ~15 min · console
24012014523 is ENABLED and spent every one of the last 14 days: £75.68/7d · £177.88/14d · £206.74/30d, 0 conversions in every window, £7.91 today. It is 22% of 30-day account spend. Why keyword-level doctrine does not save it: all 66 keywords are equipment-purchase intent (calisthenics bars for sale, street workout equipment…) — buyers of hardware, not app subscriptions; the 3 keywords someone already paused carried 100% of last-7d spend and it is still spending, and the 59 never-served keywords are the same wrong intent class, so “untested ≠ failed” does not apply. Do: pause the ENTIRE campaign in the console (~5 min). Done when: status PAUSED and next-day spend £0 — we re-run the same API read the day you say it is done. Adjacent, for the next pass: Quiz Funnel | Non-Branded | US spent £228.73/30d for 6 conversions — watch it, not kill it yet.Quiz Funnel | Competitive Non-Branded | US is still ENABLED and burned £199.28 in 30 days for 24 clicks and 0 conversions.690-916-9207)why this matters + step-by-step →
🔄 CORRECTED 29 Jul 2026 — live-verified against the Google Ads API and Meta Graph API. The earlier version of this task said these placements were “10.1% of all quiz traffic”, that Competitive Non-Branded costs “$14.64 a click”, and that it “has converted nobody in 90 days”. All three were wrong. The two halves of this task turned out to be opposite problems and are now split.
Why it matters — Meta Audience Network: it already stopped, 17 days ago. AN was a three-week burst, not a standing condition. Weekly spend (GBP, account level):
| week | AN spend | AN clicks | AN leads | account total | AN % |
| 15 Jun | £11.36 | 35 | 20 | £527.89 | 2.2% |
| 22 Jun | £21.90 | 302 | 4 | £720.71 | 3.0% |
| 29 Jun | £136.90 | 2,553 | 0 | £1,749.56 | 7.8% |
| 06 Jul | £35.96 | 504 | 0 | £671.28 | 5.4% |
| 13 Jul | £0.01 | 0 | 0 | £59.03 | 0.0% |
| 20 Jul | £0.07 | 0 | 0 | £130.94 | 0.1% |
| 27 Jul | £0.04 | 0 | 0 | £52.42 | 0.1% |
90-day AN total is £213.81 of £6,428.79 = 3.3%, not 10.1% — and it is currently 0.1% of spend. Nobody switched it off; Advantage+ reallocated on its own, which means it can come back at any time without anyone touching anything. The original reasoning was still right: AN produced 3,505 clicks for 28 leads while barely registering in GA4 (129 visitors) — most “clicks” never landed, which is the accidental-tap signature. So the fix is still worth making, just as a 5-minute permanent lock, not an emergency.
Why it matters — Google “Competitive Non-Branded”: live, and the opposite story. Status ENABLED right now. Over the last 7 days it is the single biggest Google line item: £101.72 of £287.04 total Google spend (35%) for 9 clicks.
| week | spend | clicks | £/click | conversions |
| 20 Jul | £73.56 | 5 | £14.71 | 0 |
| 27 Jul | £32.77 | 5 | £6.55 | 0 |
| TOTAL | £106.33 | 10 | £10.63 | 0 |
First impression: 10 July 2026. The campaign is 19 days old, not 90. So “converted nobody in 90 days” was false, and “$14.64 a click” was one week’s figure (w/c 20 Jul), not an average.
🔴 Read this before repeating the argument. “It has converted nobody” is not evidence of anything. On 10 clicks, at our ~3% land→pay rate, you would expect 0.3 sales. Zero is exactly what a perfectly healthy campaign would also show. Kill it on unit economics instead: at £10.63 a click against a $5.02/lead benchmark and a $24/sub floor, it cannot reach a viable CAC at any conversion rate. That argument survives scrutiny; the zero does not.
How to do it
- Do this first — Google Ads → pause
Quiz Funnel | Competitive Non-Branded | US | Sales OBJ. This is the only live bleed of the two. - Meta Ads Manager → ad sets → Manual placements → untick Audience Network. Not urgent — this is a lock so the late-June spike cannot repeat.
- ⚠️ Hold the Display/PMax exclusion. A live GA4 pull contradicts the “zero leads” claim for
googleads.g.doubleclick.net— it shows the highest event density of any source. Resolve before cutting. - Record both dates separately — see the correction to task 20 below.
⚠️ Correction to task 20. Task 20 says “wait 14 days after task 6”. The Meta half of the denominator already changed on ~13 July and nobody recorded it. That clock has partly run. Re-baseline from the Google pause date, and treat any Meta-driven change in the rates as having already happened.
✅ Done when: Competitive Non-Branded shows Paused, Audience Network unticked on every live ad set, and both dates recorded separately.
▶R16 The landing screen — the biggest single loss (ranked LAST, deliberately) moved from the revenue plan · 7 Aug🟢 no releaseNic · re-ranked LAST
index-yC00xO94.js) contains nine landing variants (landing_v3_1…9) but variant selection is compile-time (VITE_QUIZ_VARIANT baked in), so every one is dead code and no landing test can currently run at all — a readable A/B needs a per-variant build + router/CDN split that does not exist. ~$270/mo [MODELLED] vs genuine build work; stays last. Promotion trigger, so this is not parked forever: re-rank R16 the week BOTH are true — (a) the attribution stamps have shipped (results become attributable per build), and (b) v1 clears the 21 Aug gate (≥12 sales — at today’s ~8/mo a landing A/B cannot reach a read in any reasonable window; the honest conversion CI is 1.1–4.7%). Until then every hour here buys less than the four cards above.why this matters + step-by-step →
Why it matters. 🔴 Re-ranked 27 Jul. It was parked at the bottom on the reasoning “stable means hard, and we cannot A/B test”. Half of that no longer holds — bucketing lands with task 1. It is still right to fix the known missing thing (annual) before the unknown creative problem, but this is the largest number in the business and everything downstream multiplies by it. It goes straight after the paywall read is clean — not last.
How to do it
- Wait for task 6 (junk out) and task 20 (re-baseline) so you are testing against a stable denominator.
- Bucketing from task 1 must be live, or the test is unreadable.
- Then treat it as real creative work: a message hypothesis, not a tweak.
✅ Done when: A tested change with a before/after against the 49.8% baseline.
▶R23
Work the /register page as CRO — BLOCKED on Aga’s decision 32
moved from the revenue plan · 7 Aug🟢 no releaseNic · BLOCKED · Aga decides first
#ck32 and mirrored at the Nic brief’s #register-decision — Aga’s call, ~10 min, and it decides whether this task exists at all). Both /register/ URLs still 200. 🔴 Ignore any “604 buy-clicks → 248 starts” figure in this card — that measurement was made on the OLD /register/ flow and Aga retired it 3 Aug with the on-page-checkout task; the CRO upside is currently unquantifiable until the ruling + a fresh read./register/ plan URLs return 200 live (yearly3, yearly_2026, monthly_2026, quarterly_2026_promo, monthly_tripwire_2026) — the page is genuinely still serving buyers. Do not CRO a page that may be retired. Decision 32 stays on Aga’s plan./register/<plan>/ — Estate B/register/ plan URLs return 200 live today (yearly3, yearly_2026, monthly_2026, quarterly_2026_promo, monthly_tripwire_2026) — so the page is genuinely still serving buyers. Do not CRO a page that may be retired: rule 32 first./register/<plan>/, NOT checkouts-v3]why this matters + step-by-step →
🔄 RE-SCOPED 29 Jul 2026. This task used to say “the checkout / register page”, which was ambiguous — and now dangerously so, because there are two checkout estates. It means Estate B only: the six app.themovementathlete.com/register/<plan>/ pages the quiz paywall redirects to. It does not mean the new checkouts-v3 pack, which Nic has just rebuilt.
Why it matters. The last surface before money on the quiz path, never given a conversion pass. 604 buy-clicks produced 248 subscription starts — everything lost between those two numbers is lost here or on the hop to get here.
How to do it
- Wait until the paywall read is clean (tasks 1, 6, 20).
- Check the migration decision first — if
/registeris being retired in favour of checkouts-v3, this task disappears rather than gets done. Do not CRO a page that is about to be deleted. - If it stays: run it through the checkout design loop (
/checkout-fleet). - Deploy only on a double SHIP verdict.
✅ Done when: a measured change with a before/after — or a written decision that the page is being retired instead.
▶✓Stripe cohort pull — web trial→paid rate — ANSWERED 6 Aug: web trial→paid = 30.0%✅ done · number litClaude · Stripe API
The number, so nobody re-asks: web trial→paid = 30.0% — 24 paid of 80 real-email trials, phantom mints excluded (measured 6 Aug from Stripe). App trial→paid = 62% (RevenueCat, low volume). The web funnel converts trials at roughly half the app’s rate — that gap is the CRO target, and every paid-scale model should use 30%, not the app number.
P2 · detail blocks
blocked or low leverage▶16Rebuild Google UAC to optimise to the trial event (later)blocked🟢 no releaseClaude spec → Nic
⚙️ What it enables: a keep/kill decision on evidence: UK/US only, optimise to trial-start, 200-install baseline, scale only if install→trial ≥5%.
🔓 What it unblocks: nothing today — this row EXISTS to stay blocked until Tasks 5 + 8 verify. Restarting it earlier re-buys the same blind installs.
- Do not restart UAC until Tasks 5 and 8 are verified.
- Rebuild UK/US only, optimisation event = trial start.
- Collect a 200-install baseline; scale only if install→trial ≥5%.
▶26 Email deep-link infra — OneLink template + ESP wiring (the add-on's no-release half) 🟢 no releaseNic · ~½ day
docs/company/ANALYTICS/NIC_EXECUTION_PACK_2026-08-07.md.⚙️ What it enables: emails that open the app on the right screen with attribution intact, sendable from CIO/AC without their link-wrapping breaking it.
🔓 What it unblocks: Task 24's app-side work (they only function together); email→app conversion measurement. ⚠ Verified 6 Aug: our AppsFlyer plan has NO raw-data reports, so the measurement depth is capped until the plan question is settled — price that in before spending the ½ day.
- Confirm OneLink is on the AppsFlyer plan (most paid tiers include it; if not, add the standalone "Deep Linking Suite"). 5-minute dashboard check.
- Build the OneLink template — the smart link carrying
deep_link_value+ a per-link web fallback URL. - Complete AppsFlyer's ESP integration for Customer.io (and ActiveCampaign) so their link-wrapping doesn't break the deep link. Register the OneLink + ESP click domains — and hand those exact domains to the RN team for Task 24.
- Hand off: the route map (
deep_link_value→ screen) ↗ to the RN team; the OneLink URL pattern to the email team.
🔴 NEEDS A STORE RELEASE
6 open — external RN team, not NicThese six are the only tasks on this page that need an App/Play-store release. Full detail in the RN Team Brief ↗.
▶17 T4 — One canonical user identity across all SDKs the keystone 🔴 store releaseexternal RN team · 6–10h
$amplitudeUserId.⚙️ What it enables: one person, one journey, across web→app→payment; CAC and payback BY CHANNEL — the two cockpit metrics that stay dark until this ships.
🔓 What it unblocks: cohort LTV by source; every “which channel actually pays back” decision. The keystone REL item — the other five multiply its value.
- Pick the canonical id: the backend user id (the one the web quiz will also set as GA4
user_id). - On login and on signup, set it everywhere:
- RevenueCat:
Purchases.logIn(userId) - Amplitude:
amplitude.setUserId(userId) - AppsFlyer:
setCustomerUserId(userId)
- RevenueCat:
- Set the RevenueCat subscriber attributes
$amplitudeUserIdand$appsflyerId(fromAppsFlyer.getAppsFlyerUID()) so the server-side connectors can attach correctly. - Handle logout / account switching so ids never leak between users.
▶18 T7 — Native product & lifecycle events (retention) 🔴 store releaseexternal RN team · 8–16h
premium_offer_viewed 1,273 · trial_started 102, 90d to 20 Jul) and since 7 Aug the three coarse stall-gates are labelled per person in AC — but nothing carries a clean timestamp against a canonical id, so no cohort curve can be drawn.⚙️ What it enables: the TIMING layer — when they stalled, whether they came back, and what re-engagement did. The stall LOCATION is already answered per person by
%TMA_FUNNEL_STAGE% (shipped 7 Aug, no release).🔓 What it unblocks: in-app conversion optimisation and the retention diagnosis the churn number keeps asking for.
com.tma.funnelstage, 02:30) stamps %TMA_FUNNEL_STAGE% on every 90d-active app signup already in AC — survey_unfinished 4,356 · no_workout 6,775 · no_trial ~1,800 (checkout 432 = /abandon-fleet; payers excluded) — so “which gate did this person stop at, and how big is each stall pool” is answerable and emailable today (funnel_stage_export.py --dry-run). What it does NOT give is why this task exists: the stamp is a current-state label whose _AT field is the day the job ran, not the day they stalled — so still no D1/D7/D30 curve, no activation timing, no push/review denominator, no canonical-id stitching. What remains in scope: the timestamped event stream (steps 1–3 below). Masters: AUDIENCE_INTEGRITY_MASTER_2026-08-07.md + AUDIENCE_DATA_DICTIONARY_2026-08-07.md.177471782 (filter platform ∈ (Android, iOS)), giving D7/D28 engagement-retention cohorts by platform, with history. Baseline (pre-promo = the truth): D7 ~20% / D28 ~6%, stable a year (iOS 24%/7% vs Android 16%/5%) (Feb–Apr 2026 baseline, n=1,378, as_of 24 Jul — re-pull before quoting; source: SESSION_MASTER_SUMMARY_2026-07-20_21.md + retention_status.json, NOT the deep-analysis doc, whose 13%/6% is the promo-polluted read). (The May 50%-off promo retained healthier; the 4–12 Jul lifetime promo retained at ~0% and polluted every recent blended number — always report baseline, promo-excluded.) This task is still needed: it upgrades that from messy, un-stitched engagement cohorts to clean, canonical-id Amplitude cohorts — and the paid/subscription retention that decides break-even still comes from RevenueCat — and Task 1 was banked ✅ 6 Aug (rc_initial_purchase 44/30d landing), so that rail is live; the residual id-stitch is Task 17. GA4's native events are also double-tagged + carry an iOS screen_view firehose (part of what proper events here fix). Full verified read: docs/company/ANALYTICS/NATIVE-APP/NATIVE_APP_DEEP_ANALYSIS_2026-07-20.md.onboarding_completed → first_workout_started → workout_completed, notification_permission_*, and D1/D7 return. (2) Value-perception / subscription churn — 20.5% leave on cost/value, 65% all-time / 71% in 2026 would “pay nothing” (re-derived 20 Jul on 3,984 answered — supersedes the old 64%), but only 6.9% say "lack of usage" while engagement D28 is 6% → most "too expensive" is a usage failure laundered as a price answer. This is the fast lever (email/billing saves — R1, dunning, $9.99 win-back) and reconciles to RevenueCat. Net for Nic: the retention events here should light up the activation funnel + D1 return (problem 1), not just the D30 curve — that's the half that fixes the pipe feeding the churn queue.- Initialise and identify the native Amplitude SDK on both platforms, using the Task 17 id.
- Fire the already-named events from the Event Catalog at the right moments (workout started/completed, auth, key product actions). You're not designing events — 53 are already named and dormant.
- Also add push + review measurement (new — folded into this task in the RN brief):
push_permission_*,push_received/push_opened,app_openedwith source,in_app_review_prompted. These make re-engagement measurable and give D1/D7/D30 a denominator. - Defer the onboarding-screen and paywall-screen events until the signup redesign ships — those screens are being rebuilt.
▶22 RevenueCat Paywalls SDK — remote paywall & price control the one that ends the release cycle 🔴 store releaseexternal RN team · 8–16h
⚙️ What it enables: remote paywall and price control — test offers and price points in the app WITHOUT a release.
🔓 What it unblocks: in-app pricing experiments; makes the annual-mix push testable on the native side.
- Upgrade the RevenueCat SDK to a Paywalls-capable version; add
RevenueCatUI. - Render the paywall from the remote Offering + Paywall config (never hardcoded product ids) so Experiments can assign variants without a build.
- Keep firing the existing checkout events (
subscription_checkout_screen_opened,payment_initiated,subscription_checkout_confirmed) so the funnel stays continuous. - Confirm in the RN brief's confirm-pass: does the app already render the current Offering? current RC SDK version? (both size the work).
▶23 RN‑5 — iOS ATE/ATT + AppsFlyer always-init 🔴 store releaseexternal RN team · ~3h · ships R1 with 22
app/_layout.tsx, services/AppsFlyerService.ts, services/FacebookService.ts · external RN team⚙️ What it enables: iOS install and purchase attribution (within ATT consent rates).
🔓 What it unblocks: judging ASA and any iOS paid spend on real numbers — ASA is already our best-attributed paid line (Branded-EU $1,375/30d, verified 6 Aug).
timeToWaitForATTUserAuthorization, and let ATE follow ATT. Small, attribution-critical — ships in Release 1 with Task 22.▶24 RN‑6 — install-UTM stitch + ➕ email deep-link add-on 🔴 store releaseexternal RN team · ~4h + ~2.5h add-on
AppsFlyerService + auth hooks + DeepLinkService · iOS Associated Domains · Android App-Links · external RN team⚙️ What it enables: install-UTM stitching (web session → app install joined) and, with Task 26, email deep-links that land on the right screen.
🔓 What it unblocks: closing the web→app attribution gap that currently swallows every install driven by content or email.
media_source/UTM and stitch to user_id + email on register, so "which paid install → which email" is answerable. (2) ➕ Email deep-link add-on: make an email link open the app to the right screen (direct) and carry a new user through install to that screen (deferred) — instead of dumping everyone at the store.- Stitch (RN‑6 core): persist the full AppsFlyer conversion payload for all sources; on register/login set
setCustomerUserId+ RC/AF attributes + POST{user_id, email, appsflyer_id, media_source, utm_*}to the TMA endpoint. - ➕ Deep-link add-on: wire the OneLink deep-link listener → the existing
DeepLinkService(today it onlyconsole.logs); route ondeep_link_valueto the mapped screen, handling deferred (first-launch) as well as direct. - ➕ Register domains: add the OneLink subdomain AND the ESP (Customer.io/AC) click domain to iOS Associated Domains + Android App-Links (the ESP wraps links, which breaks a plain Universal Link unless both are registered — this is why it needs a binary).
- ➕ Route map: implement TMA's
deep_link_value→ screen table (the route map ↗ — workout · resume_assessment · upgrade · update_payment · today); unknown → home.
▶25 RN‑7 — screen-level tracking 🔴 store releaseexternal RN team · ~3h
AppsFlyerService / AnalyticsService · external RN team⚙️ What it enables: screen-level funnels in Amplitude — drop-off located to a screen, not guessed.
🔓 What it unblocks: the app-side equivalent of the quiz drop-off analysis; feeds Task 18's lifecycle picture.
Nic — paid, wiring, web/checkout, backend. That's almost everything on this page.
Aga — email (ActiveCampaign) and Intercom. Task 20 (Day-5 trial reminder) ✅ done 10 Jul; remaining: the Intercom sends once Task 12's code change lands.
External RN team — only what needs an App Store / Play Store release: Tasks 17, 18, 22, 23, 24, 25, plus the code half of Task 12. Two releases: Task 22 (+ RN‑5 / Task 23) ship first & standalone (close Leak #2 + Meta App-Install attribution sooner); 17/18/24/25/12 ride the redesign. Task 24 (RN‑6) now carries the email deep-link add-on. See the RN Team Brief ↗.
Claude — all dashboards, verification against live APIs, the weekly reconciliation. Zero hours from anyone else.
| Priority | # | Task | Owner | Where | Release? | Status |
|---|---|---|---|---|---|---|
| ✅ BANKED — no action, answers recorded (the 6 Aug verify + the 7 Aug closes) | ||||||
| FLEET | 11 ↓ | %TMA_FUNNEL_STAGE%), integrity gate armed. Master: AUDIENCE_INTEGRITY_MASTER_2026-08-07.md. Nothing for Nic. | FLEET | Intercom → AC nightly | 🟢 no | ✅ fleet-shipped 7 Aug |
| MOVED | → | Nic | Nic brief | 🟢 no | ➡️ moved · not open here | |
| P1 | 10 ↓ | user_id, which is Task 17’s job, not a row of its own | Nic | GA4 / web tagging | 🟢 no | ✅ closed 7 Aug — folded into 17 |
| P0 | 1 ↓ | Nic | RevenueCat dashboard | 🟢 no | ✅ verified done 6 Aug | |
| P1 | Claude ↓ | Claude | Stripe API | 🟢 no | ✅ done 6 Aug — number lit | |
Banked today, each on a live read: Task 34 ✅ already done — the pipeline’s Meta token carries
instagram_basic + instagram_manage_insights and pulled live IG data (43,441 followers) · Task 1 ✅ — 8 rc_* events landing with volume (rc_initial_purchase 44/30d); $amplitudeUserId folds into Task 17 · the Claude Stripe cohort row ✅ — web trial→paid = 30.0% (24/80 real-email trials, 8 May–23 Jul cohort, 22 no-email phantoms excluded) vs app 62%. The dark number this page kept citing is lit.
Two diagnoses corrected on the evidence: Task 5 —
af_purchase now UNDER-counts (21 vs 44/30d) and every paid Android row in AppsFlyer carries $0.00 revenue while Organic soaks $4,978; the old “fires ~2×” line is retired. Task 8 — the Purchase tag WORKS (9 purchases with value, 30d); the live poison is add_to_cart still PRIMARY with £123,634 of fake value. The fix shrank to a 30-min demotion.
Also verified still-broken the same hour: Meta’s 3 active campaigns all
OUTCOME_LEADS (Task 4) · RETURN_URL http:// on all three v3 pages (R) · all three hosts still headerless (N) · checkout_version 0 hits in the served quiz bundle (Task 30).
⚠ One discrepancy logged, ours to re-check, not an ask: a 6 Aug PM probe of
POST /payments/analytics/lead (with and without trailing slash) returned 404, where the earlier 6 Aug verify recorded 400 email required. Either the path probed differs or the route moved — [UNVERIFIED which]; re-run before anyone treats /lead as regressed.
📋 Housekeeping per Aga, same session: the context blocks that used to sit above the task table (the 3 Aug defer-mint note, the attribution-pack closure, the abandon-email explainer, the NIC-22 governance note, the MRR-30 filter, the Lead→Paid reframe, the hard stops) moved here from the to-do tab — they are law and context, not work. The to-do tab now opens on the ⚡ 80/20 box and the table.
RETURN_URL http:// scheme (we have done
the diagnosis — it is two config lines, both located) and noindex on three non-Django hosts
(quiz-test. · monitor. · grafana-dev.), plus NIC-45 phase two
when you get to it. Both re-verified still-broken today, so neither is a stale ask.
Everything else on this tab is a different workstream — AppsFlyer, the Google Ads import, the Meta objective,
the RN-team release items. pe=, /lead, the phantom sweep, the success-page gate, the quiz
Purchase proof, the Django noindex and the SPF record are all closed and must not reappear as asks anywhere.
If another surface still lists one of them, that surface is stale and this page is right.stampPaywallView so
abandon recovery keeps a real audience. checkouts-v3 now mints on valid email blur, so
stamp-abandon-email still has an intentId; coupon recreate() cancels the prior incomplete sub.
Both verified the same day by reading the files the server actually serves
(maybePrefetch 0× · DOMContentLoaded 0× · stampPaywallView 3× ·
stamp-abandon-email 3×) — not from the dev message. Closes NIC-41 and NIC-19 change 1.
pe=) · NIC-19 change 4
(/lead live, 40 phantom subs cancelled, success-page gate) · NIC-34 (quiz
fbq Purchase + eventID) · NIC-79 (SPF).
What is genuinely still open: the RETURN_URL http:// scheme (two config lines,
both located) · noindex on three non-Django hosts (quiz-test. ·
monitor. · grafana-dev.) · NIC-45 (phase 2).
NIC-42 moved to Aga — a lead is three doors (quiz start · free app signup · lead magnet),
never summed.
Ground truth for all of it: docs/company/ANALYTICS/CHECKOUT_ATTRIBUTION_CODE_AUDIT_GROUND_TRUTH_2026-08-03.md
— it overrides this brief and the attribution brief wherever they disagree.
utm_*/gclid/fbclid into Stripe metadata ·
✅ the pe= recipient field shipped 4 Aug on both estates (NIC-50 / NIC-19 change 5)
— 25 days early. 🔴 Do not list pe= as open anywhere.
One residual, and it is ours not Nic's: the client leg is verified in the served files, but
pe landing on real Stripe metadata needs one genuine emailed-through sale to observe.
Our check, not a task for him: python3 tools/finance/pe_attribution_check.py — and read the
count, not the vibe: 0 blobs with pe is WAIT, never FAIL.
VITE_REGISTER_PAY_URL: "https://app-test.themovementathlete.com/…" on the fallback register
route) was flipped to done with no resolution recorded. At the time, a cache-busted fetch of the then-current bundle (index-CEOCAHYQ.js) still returned 7 hits for app-test, and the queue-vs-brief drift-checker caught the disagreement. ✅ FIXED — Nic shipped the rebuild 4 Aug, and we re-verified it on 6 Aug across ALL FOUR live quiz builds (index-DFwXN-qe.js · index-D7C0zE_6.js · index-DPkL72Wi.js · index-DBgHnaAZ.js): app-test returns 0 hits in every one. See Task 36, now closed.
The rule this sets, and it is not about blame: a defect closes on a re-verified artifact, never on intent. Every task on this board should be closeable by someone else repeating one command — which is why the acceptance tests here are written as greps, curls and test purchases rather than as “confirm it works”. If a task cannot be closed that way, its acceptance test is the thing that needs fixing first.
checkouts-v3 pages live), so the front door now exists. The blocker moved: before that door can be trusted with ad money → Task 29 (prove the Meta pixel actually fires on v3) + Task 30 (the quiz's own checkout track must emit the same three fields, so its sales are attributable).
2 · Fix the purchase signal — Tasks 9 → 5 → 2/3 (+1, 7, 8) so ASA + Meta warm scale with eyes open.
3 · Non-converter emails — Task 11 export + the Intercom pushes + the AC sequence. Free money.
Tasks 6, 10, 12, 13, 15, 16, 17, 18, 23, 24, 25, 26 WAIT — pick them up only when they unblock a payer or the payers are done. (The deep-link add-on — 24 / 26 — is enabling infra: it rides the redesign release and doesn’t touch MRR in 30 days.)
We don't have a normal trial→paid funnel. A lead becomes a paying customer at the paywall, two ways, and every "money" task must count both:
- Carded free trial — card up front, auto-charged unless cancelled → trial-start already IS the purchase (~50% don't cancel = retention, not the acquisition rate).
- Direct discounted purchase — a big share take a ~50%-off offer and pay immediately, no trial — these never fire
trial_started.
What it changes: rc_trial_started (Task 1) is only one path's denominator — the true gate metric is Lead → Paid by source. The money event (Task 9 / meter) must fire on both the carded trial-start AND the direct discounted purchase, tagged by path/source/plan/value/transaction_id; Task 3 imports that one event deduped — import only trial_started and the algorithm goes blind to the direct-purchase buyers. Order: behind Task 19. Full spec: event catalog §1 · MESSAGE-TO-NIC-FUNNEL-CORRECTION-2026-07-16.md.
1. ✅ Cleared (10 Jul) — the Day-5 reminder automation (Task 20) is live. The on-page promise (Day-5 reminder email, plus SMS where a number was given) is now kept, so trials can run. This was the former hard stop; it no longer blocks launch.
2. No cold ad spend before Task 9. Every paid dollar in the plan lands on the web checkout, whose purchase event is currently double-counted and misses roughly one sale in five.
4. No SCALE of paid before gclid→Stripe + a real conversion event. /grow leaky-bucket verdict (24 Jul): paid fails the gate today — Google optimises a £0.75 ghost event, web conversion is dark, and churn ≈ new. Fix the conversion event + wire gclid→Stripe (Task 8 + gclid row) before a penny more in scale.
3. No Meta spend — warm OR cold — before Tasks 5 + 2 + 3. The CAPI purchase (trial-start) event must fire server-side, dedupe by event_id, and reconcile to ≥1 real RevenueCat/Stripe record first. Today Meta optimises to Lead (trained on 5,171 Leads vs 172 Purchases), so it buys form-fillers. Meta warm can start the day this verifies; cold waits for the warm read.
dig on the authoritative NS, a GTM live-version read — never against a message)eventID dedup — NIC-34) · Task 27 (pe= recipient — NIC-50) · Task 29 (Meta pixel on v3) · Task 31 (attribution into Stripe metadata) · Task 36 (app-test in the quiz bundle) · the gclid row.
Two of those were never Nic's to do, and we should not have been holding them against him: Task 29 was answerable from the GTM container in one read, and the
gclid row was already shipped per the 3 Aug code audit.
🔁 Closed earlier this window and listed here so nothing reappears as an ask:
/lead is a real route (POST → HTTP 400 email required, GET → 405 Allow: POST, OPTIONS — it does not 404) · the phantom-subscription sweep ran with --execute: 40 matched, 40 cancelled, 0 errors · the success-page gate ships UI and analytics only on succeeded/processing · X-Robots-Tag: noindex, nofollow is live on the whole Django estate · the SPF record is fixed and authoritative.
📎 Closure evidence from Nic on Slack: two Meta Pixel Helper screenshots (quiz Purchase) and one SiteGround DNS screenshot.
Live updates
checkouts-v3 — 24 pages live (verified 29 Jul 2026 against the live pages + the three JS modules, not a doc)app.themovementathlete.com/payments/checkouts-v3/…: 5 offer sets × 4 plans + success page —
trial · 50off · normal · 30trial · upgrade (± coupon), each in 1month / 3month / 12month / lifetime497.
Every page carries GTM-TKGDZR9 + GA4 G-ZBQN3P8WKW, attribution.js, analytics.js, on-page Stripe Elements + wallets, and a structured
window.TMA_PLAN (real first_price_usd vs renew_price_usd, so the money value in GA4/Meta is finally correct — e.g. 50%-off annual = 78.5 first / 157 renew; $9.99 30-day = 9.99 first / 24.97 renew).
/create-payment-intent receives {checkout_version:'v3', plan, attribution}; purchase carries a real value + transaction_id and is deduped; add_to_cart fires on intent, never page load. Good work — credit it.There are TWO checkout tracks — and that is deliberate (Aga, 29 Jul).
checkouts-v3 serves email, ads, promo and upgrade links.
The quiz has its own separate checkout deployment — the 4-model quiz paywall v2 (docs/company/PRICING/paywall-4model-v2-2026-07-13/site/, reviewable at
tma-revenue-plan-2026 ↗). Two different things — do not merge them.
🔴 What IS open is parity: the live quiz bundle still ships six hardcoded /register/<plan>/ URLs that emit
no attribution, no plan value, no checkout_version, and the 4-model paywall page is currently a design (no Stripe/gtag/fbq wired). So a quiz sale can't yet be told apart from a v3 sale.
Net effect on this list: Task 19 ✅ closed for the v3 estate · Tasks 27 + gclid half-closed (captured client-side; Stripe-metadata write still unproven) · Task 9 NOT closed (v3 has no Checkout Session, so checkout.session.completed can never fire, and the quiz track is separate) · new Tasks 29 (pixel proof) / 30 (quiz-track parity) / 31 (Stripe metadata) below.rc_* event types now flow (the "rc_* = zero" note is stale) — but rc_trial_started = 3 in 30d vs ~128 web trial-starts in GA4. First step is NOT re-wiring: audit RevenueCat → Integrations → Amplitude (ALL event types, BOTH platforms, sent-events log), then the ±5% reconcile vs the RC dashboard. RC→Meta CAPI + identity join still open. Web Stripe→GA4+Meta CAPI ✅ shipped 21 Jul — don't redo.NEW small task — GTM flag: container GTM-TKGDZR9 shows "2 issues", unreviewed since the 23-Jul cross-domain work → open, fix or report (~5 min).
Social attribution tasks live separately: Nic social tasks (repo:
docs/company/ANALYTICS/NIC_SOCIAL_TASKS_2026-07-24.md) (quiz-root redirect pack · IG/FB token 4 scopes · Leadpages hidden fields · cart-abandon · quiz colour) — they feed the social scoreboard ↗.Three pieces landed, all measurement — the gate the paid plan waits on. Full analysis + screenshot proof: docs/company/ANALYTICS/TRACKING-SYSTEM/.
- ① Google Ads LEAD tracking (NEW). A new independent GTM tag fires the Google Ads Lead conversion off
generate_lead(Conv ID18226881983) — verified live on the quiz. Leads can finally register in Google Ads the way Purchases do (advances Task 8). - ② Google Ads PURCHASE tag upgraded. Now sends conversion value + currency (ROAS) and a
transaction_idfor firing dedupe ("Part of Task 19"), plus a Custom-JS layer that captures both the new 2026 checkout and the legacy checkout (advances 8 / 9 / 3). - ③ Server-side Stripe → GA4 + Meta CAPI webhook (NEW, LIVE).
/payments/analytics/stripe-purchase/fires the purchase server-side even when the browser success page never loads — isolated from billing, so it can't break access. 200 OK on a real USD 9.99 payment. The resilient answer to Task 9's ~19% capture loss + 1.52× double-fire.
✅ Confirmed 22 Jul (Nic): the GTM workspace is published — tags live in the container. ⏳ Remaining, sequenced per Nic: finish checkout testing (Task 19) first, then run one ads-side test confirming Lead + Purchase (with value) appear in the Google Ads UI on a fresh real payment — that closes the web measurement gate. Still open (separate, app-side): RC→AppsFlyer S2S (Task 5) + gclid→Stripe ROI join.
DONE today: Task 7 — Apple Search Ads cost now flows into AppsFlyer (ASA, our best-converting paid source, is finally judgeable on cost-per-tap → CPA). Task 2 — RevenueCat + Adapty in-app purchase/trial events now flow to Meta and Google Ads.
Architecture correction (Nic — the brief's old framing was imprecise): AppsFlyer is the hub, not a direct RC→Meta pipe. App SDKs (RevenueCat rc_* + Adapty adapty_* + native af_*) fire into AppsFlyer, which forwards fb_mobile_purchase / StartTrial / Subscribe to Meta + Google. AppsFlyer→RevenueCat only sends Install (attribution direction) — so the old "RC→AppsFlyer postback" wording (Task 5) is superseded: the events already flow. AppsFlyer covers the mobile app only; the web quiz→Stripe checkout is a separate stack (Task 9).
- ① Purchase-event mapping — CORRECTED 13 Jul (Nic): this is NOT a dedup task. Meta Ads Manager only supports its standard events (Subscribe / StartTrial /
fb_mobile_purchase— the dropdown), and is stricter than Events Manager, so you can't give each source its own unique Meta event. That's why multiple AppsFlyer sources (af_purchase,rc_initial_purchase,adapty_trial_converted) all map to the onefb_mobile_purchase. Nic is keeping all sources on purpose — if one tool detects a sale another misses, muting a source would lose the conversion; over-capture is safer than a silently missed sale. My earlier "mute to one source of truth / chase a custom event" advice was wrong — disregard it. The only real consequence: Meta's raw purchase COUNT may overlap, so the true number is always the Stripe/RC reconciliation, never Meta's tally. - ② Task 1 — RC→Amplitude — ✅ mostly done (live-checked 13 Jul). Amplitude now shows 7
rc_*events incl.rc_trial_started,rc_renewal,rc_trial_converted— the trial denominator is live (the 10 Jul "2 of 15" reading was stale). Only left: confirm volumes reconcile ±5% vs RC (~194 subs, ~6 renewals/day) +$amplitudeUserIdidentity stitch. - ③ Task 13 — geo exclusions (ET/TZ/NG/BD) — forward guardrail, low priority. Verified live 16 Jul: no active campaign targets these geos (~$4/30d Meta spillover only). Apply when Google UAC turns back on.
- ④ Task 9 — web quiz purchase event, server-side + deduped. The biggest remaining gap for the web-quiz scaling strategy (1.52× dup, 81% capture). Backend ~3–6h. Blocks cold web spend + Task 8.
- ⑤ Task 8 — GA4→Google Ads import. Only AFTER Task 9 (else Google imports a broken signal). Lights up Google Search.
- ⑥ Task 4 — switch Meta Lead→Purchase (after ① confirms clean counts AND there's enough purchase volume; if thin, stay on trial-start / CPTS).
- ⑦ then, in any order: Task 14 (Meta warm ~75K CRM audiences) · Task 10 (GA4 funnel by source) · Task 11 (last-funnel-step export → free retargeting) · Task 21 (failed-card dunning — ✅ diagnosed + personal rail built 23 Jul; config optimal) · Task 6 (name the mystery pixel event).
Open question for Nic: both Adapty AND RevenueCat fire trial/purchase events — running two subscription SDKs doubles the count and the cost. Which is source of truth? · iOS ceiling: the RC integration has Advanced Privacy ON, so non-consenting iOS 14.5+ users won't post back — a structural attribution cap on iOS, not a bug. · Blocked/monitor: Task 16 (UAC rebuild — after 5+8) · Task 15 (Google API access — awaiting Google) · Task 19 (v3 checkout 8-page wiring — the live quiz checkout already converts at 50%-off, so this is the standalone campaign checkouts, not a quiz blocker).
✅ Banked — done, no action
13 tasks closed. Struck through so they read as finished; expand any one for the original spec and what landed.
| Priority | # | Task | Owner | Where | Release? | Status |
|---|---|---|---|---|---|---|
| P1 | 28 ↓ | Aga | App Store Connect API | 🟢 no | ➔ moved to Aga 6 Aug — AGA-116 | |
| P0 | 35 ↓ | Nic | GA4 property 177471782 | 🟢 no | ✅ closed 3 Aug — NIC-47, 19d early | |
| P0 | 29 ↓ | Prove the Meta pixel loads on checkouts-v3 NEW 29 Jul | Nic | Pixel Helper → GTM-TKGDZR9 | 🟢 no | ✅ closed 6 Aug — pixel does load (GTM read) |
| P0 | 9 ↓ | Web quiz purchase event — server-side + deduped | Nic | Stripe webhook → GA4 + CAPI | 🟢 no | ✅ shipped — NIC-34, eventID dedup |
| P1 | NEW | gclid → Stripe metadata join (real Google ROI) | Nic | web checkout → Stripe metadata | 🟢 no | ✅ already built — folded into 31 |
| P1 | 31 ↓ | Prove attribution lands in Stripe metadata NEW 29 Jul | Nic | create-payment-intent handler → Stripe metadata | 🟢 no | ✅ client leg proven — residual is ours |
| P1 | 27 ↓ | Email → revenue — capture email UTMs at checkout | Nic | Web / Stripe metadata + finance pull | 🟢 no | ✅ shipped 4 Aug — NIC-50, 25d early |
| P0 | 36 ↓ | Test domain live in the production quiz bundle (app-test) — closed twice while live NEW 31 Jul | NIC | Vite env · quiz checkout build | ||
| P0 | 19 ↓ | Checkouts — redesign + instrument ALL surfaces | Nic | Web / server + Django/Azure | 🟢 no | ✅ shipped 29 Jul |
| P2 | 15 ↓ | Google Ads API — Basic access | Nic (monitor) | Google Ads | 🟢 no | ✅ done |
| P0 | 20 ↓ | AC Day-5 trial reminder automation | Aga | ActiveCampaign | ✅ done | |
| P0 | 2 ↓ | RC → Meta CAPI (via AppsFlyer) | Nic | RC/Adapty → AppsFlyer → Meta | 🟢 no | ✅ done 16 Jul |
| P0 | 3 ↓ | Purchase-event mapping — capture-all (NOT dedup) | Nic | Meta Ads Manager (standard events only) | 🟢 no | ✅ by design 13 Jul |
| P1 | 7 ↓ | Apple Search Ads cost data | Nic | AppsFlyer / ASA console | 🟢 no | ✅ done 13 Jul |
| P2 | 13 ↓ | Geo exclusions (ET/TZ/NG/BD) — forward guardrail | Nic | Ads consoles | 🟢 no | ✅ met on active |
| P1 | 14 ↓ | Meta Custom Audiences (warm pool) | Aga | Ads Manager + CIO | 🟢 no | ✅ done 16 Jul |
| P2 | 6 ↓ | Meta pixel event = "TMA Page View" | Nic | Events Manager | 🟢 no | ✅ identified 16 Jul |
| P2 | 12 ↓ | Intercom push — anon-install registration | RN team → Aga | Intercom / app | 🟡 check | ✅ answered |
| P1 | 21 ↓ | Involuntary churn recovery — failed-card dunning | Aga (send) · Claude | Stripe config + payment-recovery personal rail | 🟢 no | ✅ diagnosed + rail built 23 Jul |
▶19
Checkouts — redesign + instrument ALL surfaces (quiz · full-price · 50%-off · $9.99; spans Django) — ✅ SHIPPED 29 Jul for the new checkouts-v3 estate
✅ done (v3 estate)quiz paywall not migrated → Task 30🟢 no releaseNic
checkouts-v3 pages live & verified — GTM+GA4, attribution.js, on-page Stripe Elements + wallets, TMA_PLAN with real first-vs-renew price, {checkout_version,plan,attribution} into the payment intent, ATC on intent not page-load. Residual split out: pixel proof → 29 · quiz estate → 30 · Stripe metadata → 31app.themovementathlete.com/payments/checkouts-v3/<set>/<plan>/ across five offer sets (trial, 50off, normal, 30trial, upgrade ± coupon) × four plans (1month, 3month, 12month, lifetime497), plus /success and the /30trial/ vanity redirect. Sampled pages returned HTTP 200 and every one carries the same contract:
- Instrumentation: GTM
GTM-TKGDZR9+ GA4G-ZBQN3P8WKW,/static/checkouts_v3/analytics.js,/static/checkouts_v3/attribution.js,stripe-client.js?v=20260724layerC. - Correct money value at last —
window.TMA_PLANdistinguishesfirst_price_usdfromrenew_price_usd(50%-off annual 78.5 → 157; 30trial monthly 9.99 → 24.97; lifetime 497 one_time), sobegin_checkout/purchaseno longer report a made-up number. - The three-field capture the brief asked for is met —
/create-payment-intentreceives{checkout_version:'v3', plan, attribution};/update-payment-intentattaches the buyer before payment (so an abandoning buyer's email is captured). - Mode branching works as specified — PaymentIntent for one-time / paid-today, SetupIntent (
seti_) for $0 trials, express-checkout element for wallets. add_to_cartfires on intent, never page load — the source comment names why ("page-load ATC poisons A/B denominators"). This is exactly right and matters for Task 8.purchasecarriesvalue+currency+transaction_id(the Stripe intent id, passed through the return URL) and is deduped so a success-page refresh cannot double-fire.
/register/ estate — that migration is now its own decision, Task 30. The Meta-pixel risk this shipped with is Task 29, and the Stripe-metadata proof is Task 31.checkout.themovementathlete.com is cancelled → 302-redirects to /start-training/ (verified live 13 Jul), so it is no longer where ad dollars land. Claude has analysed only the Netlify pieces; the Django checkouts are not in this repo and are Nic's to size.marketing/campaigns/checkouts-v3-mobile/NIC_BRIEF.md + zip TMA-Checkouts-v3-8pages-STRIPE-WIRING-for-Nic-2026-07-10.zip/v2_checkout/ pattern already wired for the $297 July-4 page. Same DOM contract, same deploy shape. Without this there is nothing for paid traffic to buy./create-payment-intentnow receives{ checkout_version: 'v3', plan: window.TMA_PLAN, attribution }. Branch onmode:one_time→ PaymentIntent (identical to v2).subscription+trial_days: 7→ create the subscription with trial, return the SetupIntent clientSecret (seti_…); the client detectsseti_and callsconfirmSetup.subscription+ no trial → subscription with a first-cycle-only discount (couponduration: 'once'or a phased schedule — your call); return the first invoice’s PaymentIntent clientSecret.
/update-payment-intentalso receivesphone: string|nullandsms_opt_in: boolean. Phone is optional (Aga, 9 Jul). Storesms_opt_inwith the number — it is TCPA consent for the trial-reminder text only, not marketing SMS.- Wallet sheets on subscription pages: pass recurring labels (
applePay.recurringPaymentRequest/ GPay equivalent) so the Apple Pay sheet itself shows “7-day free trial, then $157/year”. Compliance and conversion. /lead(exit-intent capture) is back on every page;sourceis per-page (checkouts-v3-{set}-{slug}-exit).- Log the consent checkbox server-side (timestamp + plan). California requires records for ≥3 years.
- Don’t touch the honest 24h timer on the 50%-off set — real expiry, never resets.
▶21
Involuntary churn recovery — failed-card dunning — free money, no release
P1🟢 no release✅ DIAGNOSED + rail BUILT 23 JulAga (send) · Claude (built)
charge.failed config is already OPTIMAL (Smart Retries 4×/1mo = Stripe's max, all recovery emails ON, Card Updater on, Retain off) — no config toggle. The clean matured June cohort refuted the "recovers 20–40%" assumption below: 22 renewal invoices failed ≥1 attempt, $1,666 at risk → recovered only 6.6% by value (18.2% by count; $1,132 / 68% uncollectible). The recovery lever is now the built Jesse-voiced payment-recovery personal rail (tools/retention/payment_recovery_lane.py, R1-style, manual-send from hello@, ~$160–320/mo MODELLED) = lever #2; voluntary cancels / R1 (~$2,130/mo) = lever #1. App-side rc_billing_issue recovery stays Apple/Google's own dunning. The steps below remain the reference spec only.rc_billing_issue when a renewal fails; today nothing acts on it. Our own cockpit says churn is the bigger number — this recovers a chunk of it with zero ad spend and zero app release. It has been sitting unclaimed. → Diagnosed + built (23 Jul — see the green box above): the web charge.failed config is optimal and the recovery lever (the built payment-recovery personal rail) is LIVE; this maps to item #1 of §⑳ THE ONE execution list ↗. (Same lever, two sides — don't scope it twice.)- RevenueCat → confirm a billing grace period is enabled (and Apple/Google billing-retry is on) so a failed renewal enters a grace / billing-issue state instead of cancelling instantly — this is what buys the window to recover them.
- Trigger a short dunning flow on
rc_billing_issue: an email (AC) and/or an Intercom push — “your payment didn’t go through — update your card to keep your progress”. Keep it 2–3 touches over the grace window, not a long sequence. - Deep-link straight to the update-payment screen (the App Store / Play billing management for app subs, or the web billing portal for web subs) — never to a generic home screen.
- Exit the moment payment recovers (
rc_renewal/ uncancellation), and stop after the grace window closes.
rc_billing_issue event triggers a dunning message within the grace window, and Claude can report the recovery rate (billing-issue → recovered vs churned) each week. Requires Task 1’s rc_billing_issue toggle to be on.▶15Google Ads API — Basic access — ✅ GRANTED (20 Jul)done🟢 no releaseNic · monitor
- ✅ Landed 20 Jul — the cockpit auto-pulls Google spend daily via the Ads API (MTD/yesterday/7d in the SPEND line; the trust receipt shows it ✅ API-LIVE). Campaign-level conversions/ROAS become useful once Task 8's value capture ships.
api_status: OK with conversions included.
▶20
ActiveCampaign Day-5 trial reminder automation — ✅ done (10 Jul 2026)
✅ complete🟢 no releaseAga (email)
- Trigger on trial start (Stripe subscription created with
trial_period_days: 7→ webhook → AC contact tag / event). - Wait until Day 5, then send: what they signed up for, the exact charge date and amount, how to cancel, and one line of value (their level / first session).
- If
sms_opt_in = trueand a phone number exists, send the matching SMS. Send to nobody else — it is reminder-only consent. - Exit the automation if the trial is cancelled before Day 5.
▶2
Turn on RevenueCat → Meta CAPI (purchase signal)
✅ done 16 Jul🟢 no releaseNic · 30 min
fb_mobile_purchase/StartTrial/Subscribe — no plain "Purchase" here). Now dedup (Task 3)- In RevenueCat → Integrations → Meta, enter the pixel ID and access token for ad account
act_710789699289834. - Map to Meta's available app-event names. RevenueCat's Meta (mobile) integration only offers the App Events set —
StartTrial,Subscribe,fb_mobile_purchase,MobileAppInstall— so there is no plain "Purchase", and you map one Meta event per RC event. Set: Trial Started → StartTrial · Trial Converted → Subscribe · Initial Purchase →fb_mobile_purchase· Renewal → Subscribe · Non-Subscription Purchase →fb_mobile_purchase. ✅ set this way 16 Jul (Aga) — the earlier "Purchase + Subscribe" note was web-pixel guidance and isn't offered here. - Include value + currency on every event (without them Meta can't optimise on revenue).
- Set the integration's user identity to the canonical id (Task 17) so events attach to real people, not anonymous devices.
- In Events Manager, confirm the events arrive with source = Server and that match quality is "Good" or better (needs hashed email /
external_id).
fb_mobile_purchase arriving from the server source with healthy match quality. ✅ mapping configured 16 Jul; verify match quality in Events Manager.
▶3
Confirm CAPI de-duplication (event_id)
unverified🟢 no releaseNic · 20 min
event_id)event_id, Meta counts each sale twice — our cost-per-purchase looks half what it really is, and we scale a number that isn't real.- Make the web checkout's browser-side Purchase event send an
event_id— use the Stripe checkout session id. - Make the server-side event (CAPI, from Task 2 or Task 9) send the same
event_idfor the same sale. - In Events Manager → your Purchase event → check the deduplication panel shows matched pairs, not two separate events.
▶7
Get Apple Search Ads cost data flowing
missing number🟢 no releaseNic · 30 min
- In AppsFlyer, open the Apple Search Ads integration and enable cost (this is the quick route).
- Alternatively/additionally, create an API certificate in Apple Search Ads → Account Settings → API, and connect it.
- Confirm the cost column populates for ASA campaigns for the last 30 days.
▶13Geo exclusions (forward guardrail)🟢 no release✅ met on active campaignsNic · 15 min
Verified live 16 Jul (Meta API): no active campaign targets ET/TZ/NG/BD — the 3 active Lead campaigns target US/GB/AU/FR/NZ/IL/CA only. About $4 over 30 days trickles in via Advantage+/broad delivery (0.1% of spend) — incidental, not targeting. So on Meta this is effectively met; the rule below is a forward guardrail for when Google UAC turns back on.
- Exclude ET, TZ, NG, BD from every paid campaign, permanently.
- Treat AP / SA / LATAM as no-paid zones until regional pricing is a deliberate product decision (Aga's call, not a campaign setting).
▶14Build Meta Custom Audiences for warm retargeting✅ done 16 Jul🟢 no releaseAga
- Website Custom Audience: quiz visitors who didn't purchase (exclude purchasers).
- Customer list audience: the engaged email list, uploaded from CIO/AC.
- Exclude active subscribers from both.
▶6
"TMA Page View" pixel event 598921317256444 — identified
✅ resolved · keep🟢 no releaseNic
598921317256444 — identified598921317256444 = "TMA Page View" — a View Content custom conversion (rule: standard PageView where URL contains "themovementathlete"; value £0; pixel 2128008637263218). Verified: NO active ad set optimises toward or reports on it — the 4 live ad sets optimise to Lead / lead-gen. So the earlier "if a campaign optimises toward it…" concern doesn't apply: it's a passive counter, costing nothing.- ✅ Named: TMA Page View (fires on
PageViewon themovementathlete URLs). - ✅ Verified no active ad set uses it as optimisation / conversion event.
- Do NOT delete it. Deleting is irreversible (a custom conversion's rule/category can't be edited) and throws away ~27.5K of history + a usable retargeting asset — a page-view custom conversion can seed a Website Custom Audience for warm retargeting (feeds Task 14). It affects nothing unless a campaign selects it as a goal.
- Only standing rule: never pick it as a campaign's optimisation event (nobody has).
▶12
Push notifications for the never-signed-up pool — via Intercom
answered 10 Jul🟡 anon pool needs RN changeRN team → then Aga
Can we just do this in Intercom? — Yes, if three things are true
- The Intercom mobile SDK is in the app. Our Intercom Sprint 1 build spec already filters audiences on
enabled_push_messaging = true, which implies the SDK is there — but confirm it against the live build. - Push credentials are uploaded to Intercom (one-time, no app release):
- iOS — Apple Developer → Certificates, Identifiers & Profiles → Keys → create a key with Apple Push Notifications service (APNs) enabled, download the
.p8. Upload to Intercom with the Key ID, Team ID and Bundle ID. - Android — Firebase console → Project settings → Service accounts → generate a private key (JSON). Upload to Intercom with the package name.
- iOS — Apple Developer → Certificates, Identifiers & Profiles → Keys → create a key with Apple Push Notifications service (APNs) enabled, download the
- The app registers the device token with Intercom — once notification permission is granted it must call
Intercom.sendTokenToIntercom(token). (With Expo, the@intercom/intercom-react-nativeconfig plugin wires the native side automatically.)
The never-signed-up install pool is NOT in Intercom: of ~41,000 contacts with a mobile device, every single one has an email (0 with a device and no email). So the app only registers people with Intercom after signup. Reaching anonymous installs needs the RN-team change (
Intercom.loginUnidentifiedUser() + token on first launch) — 🔴 store release, bundle it with the redesign build.~40,900 signed-up app users ARE push-reachable in Intercom right now. That means the whole retargeting ladder for people who signed up but didn’t convert — finished onboarding but never worked out, worked out but never trialed, saw the paywall and left — can be reached by Intercom push today, with no app release and without waiting on Task 11’s Customer.io export. Intercom push is the faster path for those segments; Task 11’s export is still worth it for email + finer segmentation.
How to send it (once the check passes)
- Intercom → Outbound → New message → Push notification.
- Audience: the same filter as above. Exclude anyone who has started a trial or subscribed.
- Copy: match it to where they stopped. For people who never opened the app twice, the hook is the free value they left behind: “Your 3 free workouts are waiting.”
- Deep link straight into the workout screen — not the app home screen.
- Send once. No follow-up sequence to this pool: if they do not open, they are written off.
▶28
Confirm whether the app "proceeds" figure is gross or net (5-minute check) — ➔ MOVED TO AGA 6 Aug 2026 (AGA-116)
the $15K number depends on it🟢 no releaseAga or Nic · App Store Connect
ASC_KEY_ID/ASC_ISSUER_ID/ASC_KEY_PATH are in .env.local and the key authenticates live (/v1/apps returns Movement Athlete, id 1357148593). Apple's financial report carries both columns on every line — what the customer paid, and Extended Partner Share, what Apple actually pays us — so one API call settles gross-vs-net permanently, and --since 2024-01 also closes legacy task 18. Tool built 6 Aug: tools/finance/asc_finance_reports.py. The one input Apple exposes through no endpoint is the 8-digit vendor number, printed only in the UI — that is the whole of what is now on Aga's list as AGA-116 (2 min). Nothing here for Nic.tools/finance/data_app_proceeds.json and the RevenueCat revenue chartRevenueCat gross × a calibration ratio taken from the last closed month. For June that ratio came out at iOS 1.02× and Play 0.75×. A store commission (15% small-business, or 30%) should put iOS near 0.85× — so 1.02× is not possible if both figures mean what we think they mean. Either RevenueCat's revenue measure is already net of Apple's cut, or the number entered in data_app_proceeds.json as "Total Estimated Proceeds" is actually gross customer revenue. Whichever it is, the app portion of the daily $15K figure could be ~15% high (~$350–500/mo on today's numbers).- Open App Store Connect → Payments and Financial Reports for June 2026. Note the figure labelled Total Estimated Proceeds (what Apple actually paid us) — and separately the gross/customer-paid total if shown.
- Compare with RevenueCat's June App Store revenue: $3,085 (USD, from the Charts API). Our file currently holds £2,355.05 for iOS June (≈ $3,146 @1.336).
- If Apple's proceeds really are ≈$3,146 → RevenueCat's
revenueis effectively already net, and the ratio is right; just confirm so we can stop flagging it.
If Apple's proceeds are lower (e.g. ≈$2,600–2,700 = 85% of gross) → the file holds gross, and the correct June iOS net must be entered. The scoreboard recalibrates itself automatically on the next run. - Same check for Google Play (its 0.75× ratio looks plausible, so this is lower priority).
metrics/overview returns only gross revenue, and the Charts API silently ignores revenue_type=proceeds — it accepts any parameter and drops it). There are also no App Store Connect / Play Console API credentials in .env.local, only Apple Ads. So this one number is a human dashboard read. Adding ASC API keys would also close it permanently — worth doing if it recurs.data_app_proceeds.json holds true NET proceeds for June, and the ⚠ reconciliation warning disappears from the 🎯 $15K cockpit tab on the next 06:00 run.
▶35
YouTube lead lane — 3,695 sessions/28d, ZERO attributed leads. Diagnose which of two failures it is — ✅ CLOSED 6 Aug 2026 (answered by NIC-47)
P0🟢 no releaseNIC · ~1–2 h + 15 min validation
NIC-47, closed with that resolution; NIC-24 superseded, NIC-48 merged as a duplicate). The device split alone is decisive: 0.21% Apple on the YouTube source vs 37% site-wide — 5 iOS sessions out of 3,809, which no real audience produces. Both halves of this card's “done when” are satisfied: the diagnosis is written with its GA4 evidence, and the P4 charter goal was formally re-set (AGA-81, closed). The one live consequence: no honest YouTube traffic number exists before 9 Aug 2026 — every day of the retrofit is crawler-contaminated, and the drop we will see on 9 Aug is the crawlers stopping, not the channel dying. Do not build tracking against this lane in the meantime. Ground truth: GA4_PROPERTY_GROUND_TRUTH_2026-08-03.md §5.yt_status.json), descriptions are 100% UTM-tagged, YPP accepted — and the generate_lead count attributed to YouTube is 0 in every window, ever. This is the P4 fleet's gate: due 22 Aug. It is deliberately framed as a DIAGNOSIS, not a fix — the two candidate causes need different work.generate_lead × session source · the quiz-subdomain redirect · older video descriptions / pinned comments / cardsquiz. subdomain hop, so YouTube leads land in the 44.7% dark bucket. Hypothesis B — genuine non-conversion: 3,695 sessions really do produce ~0 leads, and the problem is the CTA/content, not the wiring. The 10-minute discriminating check, 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? If yes → A: fix the hop (same pattern as the 29 Jul quiz-root fix that lit the content lanes) and leads appear with no new content. If no → B: no wiring date fixes it — the goal converts to a CTA experiment and the fleet's target is re-set.Done when: the diagnosis is written (A or B, with the GA4 evidence), and either the hop preserves UTMs end-to-end (test click → lead shows utm_source=youtube) or the P4 charter goal is formally converted to a CTA experiment.
▶29
Prove the Meta pixel actually loads on the checkouts-v3 pages (5-minute check) — ✅ CLOSED 6 Aug 2026
✅ closed 6 Aug — answered from GTM, not a Nic task🟢 no release
checkouts-v3 pages (5-minute check)FB Pixel - Page view is live, unpaused, and fires on trigger Page View — type domReady with NO filters, i.e. every page the container loads on. Its HTML is the standard base snippet: fbq('init','2128008637263218') + fbq('track','PageView'). And GTM-TKGDZR9 is present 2× in the served v3 page HTML. So window.fbq is defined by the time analytics.js runs, and its guarded fbq("track","Purchase",…,{eventID}) fires rather than silently no-opping. Repeat it: python3 tools/analytics/gtm_read.py tags then read the firing trigger. Honest limit: this is the container configuration, not a Pixel Helper capture. It is unambiguous (unfiltered domReady + container on the page), but the conclusive artefact is a real v3 sale showing deduped in Events Manager. Nothing to build either way. ⚠ gtm_read.py whatfires did NOT list this tag — that command resolves URL-based triggers only, so an unfiltered all-pages trigger is invisible to it. Do not read its silence as absence.Where it stands —
fbq appears 0× in v3 page HTML (only inside analytics.js, behind a typeof guard) → if GTM doesn't inject the pixel, every v3 Purchase silently no-ops. The old /register/ estate inits it inline — the two are not equivalent.GTM-TKGDZR9analytics.js the Meta call is guarded: if (typeof window.fbq === "function") { fbq("track","Purchase",…, {eventID: transaction_id}) }. That guard is correct engineering — but it means that if fbq is undefined, the Purchase silently no-ops and nothing anywhere reports an error. And fbq appears ZERO times in the v3 page HTML (verified 29 Jul on four sampled pages) — it exists only inside analytics.js, so the pixel must be injected by GTM. The old /register/ estate, by contrast, inits the pixel inline (fbq('init','2128008637263218') + PageView + InitiateCheckout, 8 occurrences). So the two estates are not equivalent, and the newer one is the unproven one. If GTM isn't injecting it, every v3 sale is invisible to Meta — and Meta will keep optimising on the events it can still see.- Open
/payments/checkouts-v3/trial/12month/with Meta Pixel Helper. Confirm pixel 2128008637263218 fires PageView. - Enter card details to trigger intent → confirm InitiateCheckout; complete a live test purchase → confirm Purchase with a value and an eventID.
- If the pixel is absent: add it to the GTM container for the
/payments/checkouts-v3/path (or inline it, as/register/does — simplest and matches the estate that already works). - While in the container: clear the "2 issues" GTM flag noted 24 Jul (unreviewed since the 23-Jul cross-domain work).
▶9
Web quiz purchase event — server-side + deduplicated — ✅ CLOSED 6 Aug 2026
✅ shipped — NIC-34proof is app-test, not prod🟢 no release
purchase event — server-side + deduplicatedfbq Purchase with eventID = the PaymentIntent id, deduping against the server-side CAPI webhook at /payments/analytics/stripe-purchase/ that has been live since 21 Jul. That is the dedup key the 3 Aug code audit said this estate had to use — v3 has no hosted Checkout Session, so there is no checkout.session.completed and no session id. Proof: two Meta Pixel Helper screenshots sent to Aga on Slack. Honest limit, and it is a real one: that capture is app-test under Stripe test mode, not production. We could not verify the quiz half from outside, because quiz success correctly 302s away without a valid intent (his own success-gate ship working) and the fbq is inline on the success template rather than in a static file — /static/quiz_checkout/analytics.js 404s and stripe-client.js carries fbq 0×. The v3 half IS verified live by us: /static/checkouts_v3/analytics.js serves 3,261 B with fbq 2×, eventID 1×, Purchase 3×. Prod confirmation lands on the first real quiz purchase in Events Manager — that is a read, not a task for Nic.Where it stands — Live-verified 24 Jul (GA4 vs Stripe): purchase dedup 7.7× overall / 1.9× on checkout (target ~1×); purchase tag NOT firing on quiz.themovementathlete.com; app-test env polluting prod (57 phantom/30d). Exclude test env + fire the tag on the quiz domain. 29 Jul reframe: v3 has no Checkout Session, so the
checkout.session.completed key is obsolete (use the PaymentIntent id); and the quiz still isn't on v3 — decide Task 30 before re-measuring.checkout.themovementathlete.com) — Stripe webhook · Nic (20 Jul): bundling this with the GA4→Ads import (Task 8) as one closely-related job, incl. conversion-value capture for ROAS- Fire
purchaseserver-side from the Stripe webhookcheckout.session.completed— not from the browser success page. - Use the Stripe checkout session id as the dedup key (
event_idfor Meta CAPI,transaction_idfor GA4). - Send it to: GA4 (Measurement Protocol), Meta CAPI, and — once Task 8 is done — Google Ads via the GA4 import.
- Remove or guard the browser-side duplicate so a page refresh can't re-fire it.
- Include value + currency + plan (annual / quarterly / monthly).
/payments/analytics/stripe-purchase/, on payment_intent.succeeded) now fires the purchase to GA4 + Meta CAPI (200 OK on a real USD 9.99 payment), and the GTM tag carries a transaction_id for dedupe. → Claude to re-verify events ≈ users ≈ Stripe on 24h of fresh data.checkouts-v3 ship — this task is NOT closed, and step 1 above is now impossible as written. Two things changed:
① The dedup key no longer exists on the new estate. v3 uses on-page Stripe Elements, not hosted Checkout Sessions — so there is no checkout.session.completed and no session id. It fires payment_intent.succeeded instead, and the dedup key is the PaymentIntent id (v3 already passes it through the return URL as transaction_id). The webhook isn't broken; that step is obsolete. Nic must confirm whether the old /register/ estate still uses hosted Checkout Sessions — if it does, the two estates need two different dedup keys, which is exactly why Task 30 exists.
② The quiz — the surface this task is named after — was not touched. v3's purchase is still browser-side, fired from /success, deduped only by sessionStorage (per browser, per session: it stops a refresh double-firing, but not a returning browser, and it cannot dedupe against the server-side event). And the quiz paywall doesn't route through v3 at all. So the 24-Jul finding stands: the purchase tag is not firing on quiz.themovementathlete.com. Re-verify against fresh data after Task 30 is decided — measuring the dedup rate across two estates with two contracts will just produce a number nobody can act on.purchase event arrives, its transaction_id equals the Stripe checkout session id, and a page refresh produces no second event.
▶31
Prove the captured attribution actually lands in Stripe metadata (closes Task 27 + the gclid row) — ✅ CLOSED 6 Aug 2026
✅ client leg provenresidual is ours, not Nic's🟢 no release
metadata (closes Task 27 + the gclid row)/static/checkouts_v3/attribution.js serves 1,730 B carrying utm_source, gclid, fbclid and ttclid, and the recipient field pe 12×. Per the 3 Aug code audit, UTM/gclid/fbclid flattening into Stripe metadata at create-intent is already shipped on BOTH estates — this was never a build. What is left is an observation, not an ask: pe landing on real Stripe metadata needs one genuine emailed-through sale to see. Our check: python3 tools/finance/pe_attribution_check.py — and read the count, not the vibe: 0 blobs carrying pe means WAIT, never FAIL. ⚠ Empty UTMs in Stripe are an upstream gap, not a missing write — run the ?utm_source=nictest acceptance test before anyone proposes rebuilding this.Where it stands — The last inch of Task 27 + gclid. Client sends it; unproven whether the server writes it. Closes both when a tagged test click shows
utm_campaign + gclid on the Stripe record.POST /payments/checkouts-v3/create-payment-intent → Stripe PaymentIntent / Subscription metadataattribution.js captures first-touch utm_source / utm_medium / utm_campaign / utm_content / utm_term plus gclid, fbclid, ttclid, persists them in localStorage (key tma_attribution_v1, with first_seen, landing, referrer, and an in-app-webview flag), and getPayload() is posted into create-payment-intent. That is precisely what Tasks 27 and the gclid row asked the browser to do. What is still unproven is the last inch: whether the server writes that payload into Stripe metadata — because only a value on the Stripe record makes revenue joinable back to a campaign or an email. From outside we can see the request being sent; we cannot see what the handler does with it.- Make one test purchase through a v3 page with
?utm_source=test&utm_campaign=metatest&gclid=TESTGCLID123on the URL. - Open that PaymentIntent (and the resulting Subscription) in Stripe → confirm
metadatacarriesutm_campaign,utm_content,gclid,checkout_version, and the buyer email. - If it doesn't: write the payload into
metadataon both the PaymentIntent and the Subscription (the subscription is what the finance pull reads at renewal). - Then extend
tools/finance/revenue_per_source_pull.pyto aggregate revenue byutm_campaign+utm_content— that is the moment "which email / which ad drove revenue" becomes answerable.
metadata contains the campaign and gclid, and the revenue pull can split real money by source. At that point Task 27's web half and the gclid→Stripe row both close.
▶27
Email → revenue attribution — capture email UTMs at checkout — ✅ CLOSED 6 Aug 2026
✅ shipped 4 Aug — NIC-50, 25 days early🟢 no release
pe= recipient field ships on BOTH estates — quiz checkout and checkouts-v3 — which was the last broken link in email→revenue attribution. The fleet scoreboard reported that KPI DARK with a wiring deadline of 29 Aug; Nic cleared it on 4 Aug, 25 days early. All three legs are now wired: the AC estate is 100% UTM-tagged (1,101/1,101 links), both checkouts write utm_*/gclid/fbclid into Stripe metadata, and pe= carries the recipient. Do not list pe= as open anywhere — if another surface still shows it as an ask, that surface is stale and this page is right.Where it stands — "Which email drove revenue" (web half); fleet already auto-tags. 29 Jul: browser capture DONE on v3 — remaining work is the Stripe write + read-back = Task 31. ⚠ only applies to links landing on
checkouts-v3; quiz-root links reach /register/, which captures nothing.metadata + tools/finance/revenue_per_source_pull.py — no app releaseutm_source/medium/campaign/content/term) and blocks anything untagged — that half is done. But a UTM only reaches a GA4 session; to answer "which email drove the subscription," the web checkout must persist those UTMs (+ the recipient email) into the Stripe purchase record, and the revenue pull must read them back. This is the cheap, high-ROI web half of email→revenue (most email CTAs point to web checkouts). The native App-Store→RevenueCat half is the deep-link / RN‑6 work (Task 24) — separate, flagged, not here.- Persist the UTMs + recipient email into the Stripe sale (core). On every web landing a tagged email link can reach (
/start-training,/30trial,/50promo, the checkout surfaces, ThriveCart lifetime): read the 5utm_*params, hold them across the flow, and write them into the Stripe Checkout Session / PaymentIntent / Subscriptionmetadata— plus the recipient email (from the existingpe=param). That puts email + campaign + content on the paid record = the join key. - Read it back. Extend
revenue_per_source_pull.py(today it splits revenue by payment rail only) — or activate the dormant email revenue node (Agent 15 / the n8n workflow) — to aggregate revenue byutm_campaign+utm_content, and surface a "revenue per email / per sequence" column in the/email-analyticsdashboard (engagement-only today). - GA4 session tie (complementary). Confirm GA4 buckets email sessions as "Email" with the right campaign; tie session→purchase via
user_id(the same identity work as Task 17 — cross-reference, don't double-scope).
checkouts-v3 ship — do not rebuild it. attribution.js already reads all five utm_* params plus gclid/fbclid/ttclid, holds them first-touch in localStorage across the whole flow, and posts them into /create-payment-intent alongside checkout_version and the plan. Buyer email is attached separately via /update-payment-intent before payment. What remains is only the server write + the read-back — split out as Task 31, which is where this task now finishes. Caveat: this only holds for links pointing at checkouts-v3. Email/CTA links that follow the quiz-root rule land on the quiz → /register/, which captures none of it — see Task 30.metadata (the gclid pipe for ads)? If yes → just add the utm_* keys. If no → build the passthrough. Also confirm ThriveCart (lifetime) forwards utm_*. Full spec + acceptance tests: docs/company/ANALYTICS/EMAIL-UTM-REVENUE-WIRING-BRIEF-2026-07-14.md.metadata carries utm_campaign + utm_content + recipient email; the revenue pull outputs revenue per sequence + per email; and the email dashboard shows a populated "revenue per email" figure (not just opens/clicks).
▶—
gclid → Stripe metadata join (real Google ROI) — ✅ CLOSED 6 Aug 2026
✅ already built — folded into 31🟢 no release
gclid is captured in /static/checkouts_v3/attribution.js alongside fbclid and ttclid, held first-touch, and posted into /create-payment-intent, which flattens it into Stripe metadata. The 3 Aug code audit lists gclid flattening at create-intent under ALREADY SHIPPED on both estates. Real Google ROI is therefore a join we run (Stripe metadata → Ads), not a build we ask for. Folded into Task 31's read.Where it stands — Real Google CAC/ROAS. Today Google Ads optimises a £0.75 “ghost” conversion event (not a subscriber) with no gclid→Stripe join — so paid cannot be scaled on evidence. Pairs with Task 8 + Task 27. 29 Jul: browser half DONE —
attribution.js captures gclid/fbclid/ttclid first-touch and posts it to create-payment-intent. Server write = Task 31.
▶36
A test domain is live in the production quiz bundle — app-test.themovementathlete.com — ✅ CLOSED 6 Aug 2026
✅ shipped 4 Aug — all 4 bundles clean🟢 no release
app-test.themovementathlete.comindex-CEOCAHYQ.js; that bundle is gone. Cache-busted fetches today: /2026-quiz-checkout-v1/assets/index-DFwXN-qe.js (1,339,617 B), /2026-v2-1/assets/index-D7C0zE_6.js (1,047,283 B), /2026-v2-2/assets/index-DPkL72Wi.js (1,309,793 B) and /2026-v2-3/assets/index-DBgHnaAZ.js (1,047,283 B) — app-test returns 0 hits in every one. Shipped by Nic 4 Aug with the React rebuild (NIC-30 / 37 / 46). The lesson this card was reopened to teach still stands and is worth keeping: it was once closed with no resolution recorded while the defect was live, and the queue-vs-brief drift checker caught it. A defect closes on a re-verified artefact, never on intent. ⚠ Checking one bundle is not checking the estate — the quiz root 302s to /2026-quiz-checkout-v1/, and a wrong asset path returns the 3,530 B SPA shell with HTTP 200, which greps clean and looks like a pass. Always assert the byte size.index-CEOCAHYQ.js — the identical bundle hash this was filed against, so no
rebuild has been deployed.app-test, all
VITE_REGISTER_PAY_URL; addToCartValue:0 still present, with :25 alongside it —
confirming the code audit's correction that the zero sits on 1wk_trial, not monthly_trial.
🔴 Batch with NIC-37 (dead Buy button) and NIC-46 (two “4.9★” strings — 🔴 the published figure is 4.8★, Aga 4 Aug 2026, blended cross-platform; the old “4.7” instruction on this row was superseded the same day Nic executed it and shipped 4.7 to 7 live surfaces — never quote 4.7 or 4.9)
— one rebuild+redeploy closes all three. It was previously marked done twice while the defect was live, which is
why this closes on a grep returning zero and never on intent. Tracked as NIC-30 (moved off NIC-22 after an id collision — TRUST_AUDIT F1); full card on the
attribution brief.VITE_REGISTER_PAY_URLaddToCartValue populated. The two defects are both on the
fallback register route. (1) The production bundle ships
VITE_REGISTER_PAY_URL: "https://app-test.themovementathlete.com/register/yearly_2026/" —
if that fallback ever fires, a real buyer is sent to a test domain to pay. (2) The same fallback block still
sends addToCartValue: 0 on monthly_trial, which teaches Meta to optimise toward people
worth nothing.
The recommendation, stated plainly: fix it — it is twenty minutes and it removes a path where a paying customer lands on a test domain. But the more valuable thing here is not the fix. This task has been closed twice without the artifact changing, which is why the acceptance test below is written as a command anyone can run in ten seconds rather than as “confirm it works”. A defect closes on a re-verified artifact, never on intent. If a task on this board cannot be closed that way, its acceptance test is the thing to fix first.
Done when: curl the deployed bundle and grep it — zero hits for
app-test, and no plan object left carrying addToCartValue: 0.
▶34
Instagram token — add instagram_basic so the IG scorecard stops being DARK — ✅ ALREADY DONE, verified 6 Aug 2026
✅ done🟢 no releaseAGA · ~5 min
instagram_basic so the IG scorecard stops being DARKFB_TOKEN, whose live scope list already includes instagram_basic + instagram_manage_insights (+ manage_comments; system-user token, no expiry), and a live Graph pull read back the IG business account (themovementathlete, 43,441 followers, 2,728 media). The IG card can render on the next refresh — no action left for Aga. (Original ask, kept for history: the social scoreboard's Meta token lacks the instagram_basic scope, so the IG platform card has been DARK since 24 Jul while IG drives 41 leads/28d (GA4). This is the P3 Social fleet's 90-day WIRING gate: due 8 Aug — FLEET_CHARTERS.json.)instagram_basic (+ re-issue the long-lived token) → paste into the pipeline envDone when: the social scoreboard's IG card renders live follower/post data on the next 06:30 refresh and social_status.json drops the DARK flag.
1 · Where every system stands — and exactly what to do
One row per system. The last column is the only thing you need to act on — each links to the task below with the full steps.
| System | Status | Where it stands today | What needs to be done | Release? |
|---|---|---|---|---|
| RevenueCat | healthy | Live & accurate. 194 active subs, 3 trials. Project proj4a978c73. It is the source of truth everything else reads from. |
Nothing to fix. It's the source for Tasks 1, 2 and 5. | 🟢 no |
| Amplitude | connected · partial | ✅ RevenueCat→Amplitude connected. 8 of the 15 lifecycle rc_* events are live (incl. rc_renewal, rc_trial_started, rc_trial_converted) — re-verified daily by the 06:00 T2 auto-check. Remaining: enable the last few lifecycle toggles + reconcile ±5% vs RC (240 subs, ~6 renewals/day renewal rate as of 10 Jul). |
Task 1 — enable the remaining 13 event toggles in RevenueCat → Integrations → Amplitude. ≈5 min. | 🟢 no |
| AppsFlyer | events broken | SDK installed, but af_purchase has fired ~2× against 194 real subscribers. Because of this, every paid app channel reports near-zero conversions — Google, Meta and Apple Search Ads alike. |
Task 5 ★ — turn on the RevenueCat→AppsFlyer server-side integration. Highest-leverage job on this page: it repairs all three ad platforms at once. Release question already checked — none needed. | 🟢 no |
| Meta Ads | optimising wrong | The pixel has been trained on 5,171 Lead events vs 172 Purchases, so Meta buys form-fillers, not buyers. It has no server-side purchase signal. Ad account act_710789699289834. |
Task 2 turn on RevenueCat→Meta CAPI · Task 3 confirm event_id dedup · Task 4 switch campaigns off the Lead objective → Purchase · Task 6 name the unknown pixel event. |
🟢 no |
| Google Ads | signal shipped · verify | Was blind — had never once received a purchase signal. Now (Nic, 20–21 Jul): Purchase conversions register with value + currency + transaction_id, and Leads register too — both via independent GTM tags (Conv ID 18226881983). Account 690-916-9207; API Basic access live. ⏳ confirm they appear in the Ads UI on a fresh payment. |
Verify Lead + Purchase land in the console (set primary action + ROAS). Then Task 13 geo exclusions, Task 16 UAC rebuild (later, blocked). | 🟢 no |
| GA4 | partial | Property 177471782. App events landed 8 Jul — good. Web purchase historically double-fired (~1.8×) at ~47% capture; Nic 21 Jul: a browser-independent server-side Stripe webhook now fires purchase to GA4 + Meta CAPI (covers ad-blocked / closed-tab sales) with transaction_id dedupe. ⏳ re-verify events ≈ users ≈ Stripe on fresh data. |
Verify Task 9 on 24h of fresh data (events ≈ users ≈ Stripe ±5%). Task 10 — segment the quiz funnel by traffic source. | 🟢 no web, not app |
| Apple Search Ads | no cost data | Our best-converting paid source — 27 installs produced 2 purchasers (7.4%, ~2.6× organic). But no spend data flows, so cost-per-tap is unknown and the channel can't be judged or scaled. | Task 7 — enable cost in AppsFlyer's Apple Search Ads integration (or connect the ASA API cert). The single most important missing number in the paid plan. | 🟢 no |
| Web quiz + checkout | server-side shipped · verify | ⚠ verified 13 Jul: checkout.themovementathlete.com is cancelled — 302-redirects to /start-training/ (Netlify-served). Paid dollars land on the post-quiz paywall + the other checkout surfaces (full-price · 50%-off · $9.99), several on legacy Django. Nic 21 Jul: a GTM Custom-JS now normalises BOTH the new 2026 checkout and the legacy checkout data layers, and the server-side Stripe webhook backstops the browser — the Task 9 fix now applies wherever the live web checkout runs. |
Task 9 — verify the server-side deduped purchase event on fresh data · Task 10 source segmentation. | 🟢 no |
| Native app (iOS + Android) |
identity missing | Live; most product events are named in the catalog but not wired. There is no single user id shared across RevenueCat, Amplitude and AppsFlyer, so server-side events can't attach to real people. | Task 17 T4 canonical user identity (the keystone) · Task 18 T7 product + lifecycle events for retention. | 🔴 yes external RN team |
| Backend / CRM | not exporting | ~500 users/week sign up, finish onboarding and never touch their 3 free workouts. ~180/week see the paywall and don't start a trial. We can't email them because no contact carries where they stopped. | Task 11 ★ — export each user's “last funnel step” to Customer.io/AC. Best ROI on this page: ~$1,000–2,600/month recoverable at zero ad cost. · Task 12 push via Intercom (5-min check first). | 🟢 no |
Not one ad platform can currently see a purchase. RevenueCat knows every sale; Amplitude, AppsFlyer, Meta and Google mostly don't. Tasks 1, 2, 3, 5 and 9 are the entire critical path — they connect the sale to the systems that need to learn from it. Everything else on this page is either housekeeping or depends on those five.
Nic — you own everything in this document, including the web quiz + checkout tasks (9 and 10), which sat outside the original app-only brief. The one exception: anything that requires shipping a new build to the App Store or Play Store goes to the external RN team — that's Task 17 (canonical user identity) and Task 18 (native product events), plus Task 5 and Task 12 only if their 2-minute checks come back negative.
Everything else — dashboards, ad consoles, GA4, the backend, and the web checkout — is console or server-side work with no release cycle. Those two RN-team tasks can ride the planned signup/onboarding redesign build rather than needing a release of their own.
2 · The app-release answer
🟢 no release 25 of 31 tasks — Nic
Done in a dashboard, an ad console, GA4, the backend, or the web checkout. No App Store / Play review, no waiting on a build. Every task gating the paid plan is in this column, and all of it is yours — it can be done this week. The email deep-link add-on's no-release half (Task 26 — confirm the OneLink plan, build the OneLink template, wire Customer.io/AC) sits here too.
🔴 store release 6 tasks (+1 to check) — external RN team
- Task 17 · T4 canonical user identity — app code, both platforms, 6–10h
- Task 18 · T7 native product events — app code, both platforms, 8–16h (now also carries push/review measurement)
- Task 22 · RevenueCat Paywalls SDK — remote paywall & price testing, 8–16h · ends the paywall release cycle
- Task 23 · RN‑5 iOS ATE/ATT + AppsFlyer always-init — ~3h · ships R1 with 22 (Meta App-Install attribution)
- Task 24 · RN‑6 install-UTM stitch + email deep-link add-on — ~4h + ~2.5h add-on · email links open the app to the right screen (direct + deferred)
- Task 25 · RN‑7 screen-level tracking — ~3h · app opens → page path
Task 5 · T5— resolved 10 Jul:$appsflyerIdis present → dashboard toggle, no release- Task 19 · push tokens — backend only if the push SDK is already in the app (check)
Both can ride the planned signup/onboarding redesign build — no separate release needed. Everything else on this page is yours, Nic.
Nothing paid scales until this wiring is done — until then we spend blind and the circuit-breaker can't compute. Live order (13 Jul — see the green box at the top): Tasks 1, 2, 7 ✅ done and Task 3 is settled by design (capture-all, Nic — not a dedup task). Top actionable now: Task 9 (web purchase event, blocks cold web spend) → Task 8 (Google, after 9) → Task 4 (switch Meta Lead→Purchase) → Task 13 (geo excl). The paid-optimisation event = the trial-start (card-on-file) — the buying decision — so the interim KPI everywhere is Cost-per-Trial-Start reconciled to RevenueCat (CPTS), not cost-per-purchase (trial events are 5–10× more frequent → a kill/scale call is valid weeks sooner). Paid plan: Paid Roadmap ↗.
Every dark number on the cockpit resolves to one of these, in sequence — each depends on the one before:
- Task 1 · rc_trial_started → Amplitude — the denominator. Nothing about trials is measurable until this fires. Lights up: Sign-up→Trial, the trial funnel steps. console, no release
- Task 5 · RC→AppsFlyer postback — tags trials/purchases to the paid source. Lights up: per-channel conversion. console (after a 2-min check)
- Task 9 · web purchase event (server-side + dedup) — lights up: Paywall→Purchase % and Revenue per visitor. web, no app release
- Task 8 · GA4→Google Ads import + Task 2 · Meta CAPI trial event — lights up: Cost per Trial by channel (Google, Meta). console
- Task 17 · canonical user identity (T4) — joins web+app to one person. Lights up: CAC and payback by channel. needs an app release — external RN team
Task 1 is the single highest-leverage item — it's currently 5th in the table below; treat it as the first wiring task. CAC/payback (Task 17) needs a release, so it lands last.
9 · Suggested order — re-cut under the MRR-30 filter (10 Jul)
| When | Tasks | Why this order |
|---|---|---|
| NOW — PAYER 1 | 19 (checkout Stripe wiring) | The single remaining trial-launch blocker (20 ✅ done). Every day unwired = 500 signups/wk hitting a dead front door. |
| This week — PAYER 2 | 9 (web purchase event) · 5 (AppsFlyer events) · 2 + 3 (Meta CAPI + dedup) · 1 (Amplitude toggles, minutes) | These make a purchase visible to every ad platform — ASA + Meta warm can then scale with eyes open. Nothing else matters until they're done. |
| This week — PAYER 3 | 11 (funnel-stage export → CIO/AC) · 21 (failed-card dunning) (Aga in parallel: R1 send + Intercom pushes + AC non-converter sequence) | Free money — ~$1–2.6K/mo retargeting revenue + recovered involuntary churn, no ad spend. |
| Then (payer-2 enablers) | 7 (ASA cost) · 8 (Google import) · 4 (Meta objective) · 14 (audiences) | Point the platforms at the now-correct signal before scaling spend. |
| WAITS — fails MRR-30 | 6 · 10 · 13 · 15 · 16 | Hygiene/read-outs — do them only when they unblock a payer or the payers are done. Nothing bad happens while they wait. |
| EARLY RELEASE (standalone) | 22 (RC Paywalls SDK) · 23 (RN‑5 ATE/ATT) | Aga's call (11 Jul): ship on their own, ahead of the redesign — 22 closes Leak #2 (paywall→card), 23 restores Meta App-Install attribution. Never block a payer. |
| WAITS — redesign release | 17 (T4 identity) · 18 (T7 events) · 24 (RN‑6 stitch + email deep-link add-on) · 25 (RN‑7 screens) · 12 (push) | External RN team, one build with the signup/onboarding redesign — never blocks a payer. |
| Do alongside — no release | 26 (deep-link OneLink infra — Nic) · 27 (email UTM→revenue capture — Nic) | 26: confirm OneLink plan · OneLink template · wire CIO/AC · hand RN the domains + route map (feeds Task 24). 27: persist email utm_* + recipient into Stripe metadata at checkout → "revenue per email". Both small, no release. |
8 · Claude owns — zero hours from you
All dashboards and the cockpit · the weekly cost-per-purchase reconciliation (CAPI purchases ↔ Stripe/RevenueCat) · ad frequency and creative hook-rate reporting · source-segmented funnel views · the UTM + creative naming convention (META_INJ_STATIC_RW_2026W30) · verification of every task above against the live APIs, with the actual counts.
How each task is signed off: tell Claude a task is done → Claude queries the live API (Amplitude / AppsFlyer / RevenueCat / GA4 / Meta) and reports the real numbers. Pass → next task. Fail → you get told exactly what's wrong (wrong project, missing id, dedup issue). No ambiguity, no “looks fine to me”.
7 · For the external RN team — app code + a store release
These six require a build shipped to the App Store / Play Store, across two releases: Task 22 (RC Paywalls SDK) + Task 23 (RN‑5 ATE/ATT) ship first, standalone, ahead of the redesign (close Leak #2 + restore Meta App-Install attribution); Tasks 17, 18, 24, 25 ride the signup/onboarding redesign build. Task 24 (RN‑6) carries the email deep-link add-on. Full execution detail, hours and code snippets are in the RN Team Brief ↗ — the source of truth for the release items.
The problem: TMA's marketing emails can only link to the App/Play Store — a dead end for people who already have the app, and a lost journey for those who don't. The fix: a smart link (OneLink, already owned) that opens the app to the right screen, and carries a brand-new user through install to that screen (deferred). The same build finally lets us attribute which email drives revenue on native (the web half of that is Task 27).
📄 The route map (deep_link_value → screen — the artifact both halves need) lives here: Email Deep-Link Route Map ↗.
🔴 needs release app-code half → Task 24 / RN‑6 (RN team)
- Wire the OneLink deep-link listener → the app's
DeepLinkService/router (direct + deferred) - Register the OneLink + ESP click domains in iOS Associated Domains / Android App-Links
- Implement the
deep_link_value→ screen route map - ~2.5h, folded into RN‑6 — rides the redesign release
🟢 no release setup half → Task 26 (Nic)
- Confirm OneLink is on the AppsFlyer plan (or add the Deep Linking Suite)
- Build the OneLink template + per-link web fallback
- Complete AppsFlyer's ESP integration for Customer.io (+ ActiveCampaign)
- Hand the RN team the exact OneLink + ESP domains + route map; give the email team the link pattern
10 · Open checks
$appsflyerId is presentChecked live against the RevenueCat API on 10 Jul: the attribute exists on every recently-active customer. Task 5 is a dashboard toggle — nothing goes to the RN team.
What this is about: roughly 760 people a week install the app from Google ads and never sign up, so we have no email for them. A push notification is the only free way to reach them, and it needs a push token — the per-device id the app gets when a user allows notifications, which the backend must store. The question is just whether the app already asks for permission and saves that token. If yes, Task 12 is backend-only; if there is no push SDK at all, it rides the RN team's redesign build. Worth exactly one push, then write-off — we do not pay to retarget them.
We use Intercom, so this may already be possible. The 5-minute test: Intercom → Contacts → filter Enabled push messaging is true + Email is unknown + First seen in the last 60 days. Hundreds of contacts → we can push today, no release. Zero → the app only registers people with Intercom after signup, and reaching anonymous installs becomes a code change for the external RN team. Full setup and send steps are in Task 12.
Event naming is locked: RevenueCat events use the rc_ prefix · AppsFlyer uses af_ · the web quiz's purchase / add_to_cart / generate_lead are reserved for GA4 · the app's own client events use subscription_*. Amplitude event names are permanent — no renaming without a migration plan. Full taxonomy and the live checklist are linked from the Growth Cockpit (📋 Analytics Checklist tab).
TMA Analytics Master Brief v2.7 · 14 Jul 2026 · This document stays live until every task is signed off.
Working notes by area
The framing that used to sit above each block of tasks, kept verbatim so nothing is lost.
3 · Do these first — this week, no release, unblocks the money
In order. Tasks 1–8 are the critical path: they make the ad platforms able to see a purchase, which is the entire reason paid spend is currently unjudgeable.
4 · Web quiz + checkout — where the ad money actually lands
These two were outside the original app-only brief, but they are yours and they are the highest-risk gap: the paid strategy points every cold ad dollar at the web quiz. Task 9 blocks all cold spend.
★ Beyond analytics — the launch blockers (Tasks 19–21)
These sit outside the analytics stack but gate the same revenue, so they belong on one list. None needs an app-store release.