The customer journey β€” master

10 documents Β· rewritten 11 Aug 2026 Β· 2 decisions need you

Every email job, and what it has actually done

Measured live 10 Aug 2026 — launchctl, each lane's own state file, the ActiveCampaign API and the SendGrid log. Not from the docs.

JobRunsSends or drafts?What it has actually done
Scheduled and running · all last exit 0
com.tma.r1daily
R1 reply assistant
05:35 dailydrafts + sends A-tier queue 110 ($1,557 MRR) · 57 A-tier sent · 7 replies · 1 draft waiting · 2 saves ($34.96) · ran 05:40
com.tma.paymentrecovery
dunning
05:50 dailydrafts 41 people in state · ran 05:50
com.tma.paymentautosend dailysends fires dunning drafts older than 14h unless held for Aga
com.tma.quizabandon
quiz-abandon SYNC
every 30 minneither writes AC fields + tags only. 91 abandoners in a 30-day window, 3 actionable · ran 08:45. πŸ”΄ It detects; it never writes an email.
Manual — no schedule
quiz_abandon_manual_notes.py --draft
the personalised hello@ notes
you run itdrafts only 40 drafts created · 39 sent Fri 7 Aug · 1 STILL WAITING in Gmail. πŸ”΄ No send path exists in the tool — its own docstring: “drafts only, Aga sends; no --live. Skips are all correct: already-drafted, 1-in-5 holdout, a payer, a test address. It only drafts at ≥24h, which is what puts the welcome intro before the ask. 7-day window.
failed_purchase_lane.py not scheduleddrafts only 9 in state · last ran 05:55
cart_abandon_reconcile.py
πŸ”΄ the OTHER lane — v3 / normal checkouts
neverneither πŸ”΄ no state file exists — it has never run. Not scheduled, and not in the job register at all.
ActiveCampaign arcs
756 Part 1 - Abandoned Cart Reminder
757 Part 2 - Abandoned Cart Reminder
paused πŸ”΄ 0 ever entered. Neither has sent a single email. Enrolment is refused — the arming sentinel is absent.
750 TMA | Welcome Sequence After Quiz on quiz finishsends ACTIVE · 19,860 entered
Nic's Django → SendGrid
“Your login is in this email” ~1h after checkoutsends 41 sends in 3 days — the only thing reaching an abandoner today

πŸ”΄ Two corrections to what I told you. (1) I said the quiz lane “does nothing with them” — wrong: it drafts, and that is exactly what produced Friday's 39. What it does not do is draft automatically; the sync runs every 30 minutes, the drafting does not. (2) I said restoring --hours 1 revealed the 91 — it did not. Dry-run at --hours 1 and --hours 24 both return 91 abandoners · $8,827 · 3 actionable, identically. The pool comes from the 30-day --days window, not the hours gate.

⚠️ And a denominator bug in my own code. abandoners: 91 combines two sources — Stripe (71 checkouts − 13 paid = 58) plus 33 from the AC tag feed that Stripe cannot see. But checkout_conversion_pct: 18.3% is 13/71, the Stripe-only denominator. On the same population it is 12.5%. And $8,827 is 91 × $97 flat — the 58 Stripe records carry their real plan, the 33 tag-only ones carry none. Treat it as a rough pool, never as revenue.

🟑 The quiz welcome was paused for ~82 minutes — it is back on

750 TMA | Welcome Sequence After Quiz was paused today at 16:41 and un-paused at 18:03. Both timestamps read live from the ActiveCampaign API (automations/750 now returns status: 1, mdate 2026-08-07T18:03:43). Nothing replaced it while it was off — it was the only automation modified today, and every other welcome arc serves a different audience (967 … After LEAD MAGNET, 988/992 … AFTER APPSIGNUP, 986 Welcome Lifetime Members).

Cost of the window: arrivals run ~21–25/day (18 part-day, then 25 · 25 · 21 · 11), so roughly 1–2 quiz leads got the two transactional emails and then silence. Small, and now closed.

⚠️ One thing still unconfirmed, stated plainly: ACTIVE is a status, not an arrival. The newest entry is still 16:15zero since the un-pause. At ~1/hour that is exactly what you would expect this soon and is not evidence of a fault, but it is not proof it works either. The fleet re-checks; if no entry lands within ~2h of 18:03, the trigger may have been changed too and AGA-137 reopens. Nothing for you to do meanwhile.

Don’t implement the system. Implement three things.

The architecture below is the destination, not the next step. Everything here is ~10 minutes of your time plus one change from Nic — and none of it needs the new arc built.

  1. Aga · 1 minSend the draft sitting in hello@ right now. One gate-cleared personalised note is waiting (checked live: Gmail + SendGrid + ActiveCampaign + Stripe all clear, last touch 69.7h ago). πŸ”΄ “Arming” it is not possible — the tool has no send path at all (“drafts only, Aga sends; no --live exists”). Drafting happens; the send is your click. If you want that changed, it is a build, not a switch — say so and I’ll spec it properly.
  2. NicThe T+0 swap you’ve already written. One template swap: point send_post_quiz_email_for() at the results-and-login email. The old “{Name}, your results are ready!” stops because it is the thing being replaced — on a trigger that is already correct. Then the ~1h checkout trigger comes out. Blocked on two answers: your [N], and whether that email can hand over a one-tap login link.
  3. Aga · 5 minOpen 750 TMA | Welcome Sequence After Quiz in the AC builder and check the “If customer → END” goal exists. Goals are invisible to the API and two of our own sources disagree. If it is missing, people who have paid are still being sold to — the highest risk-per-minute on the whole board.
  4. Aga · 5 minPull campaign 3715 from that welcome’s E1. Your own 4 Aug ruling. Re-checked live in the AC API just now, not from a doc: campaign 3715 “The systematic approach to unbreakable strength” still reports automation: 750 and last sent 6 Aug 19:50 — so it is confirmed still attached and still sending. Lifetime 5,622 sends · 66 unsubscribes (1.17%) · 58 link clicks. πŸ”΄ It unsubscribes more people than it gets clicks. (The 2.86% figure elsewhere in our estate is a trailing-28-day window, not this lifetime one — I have not re-derived it, so treat only the numbers above as checked.)

Then stop and measure for two weeks. Build the branched arc below only if those three don’t move the number. πŸ”΄ This is not a compromise — it is what our own EVOLUTION LAW already requires: never rewrite a live sequence, change one slot at a time. The full rebuild needs your written override either way (AGA-134).

Why it looked complex: ActiveCampaign’s API cannot create or edit automations, triggers or goals — only the builder can. (It can read some structure: a campaign object does report its automation id, which is how step 3 above was verified. Goals are the part that stays invisible.) So every branch in the design below is your clicks, not mine. That is the real constraint, and it is why the plan starts with three small changes instead of an arc.

The rule that drives everything: the free app is the product, so day 0–3 is about getting them to workout 1 β€” and the sales pitch lands the morning after their first workout, when they've just felt it work.

Activated users convert 5–10Γ—. Today this moment β€” the highest-intent moment in the funnel β€” has no email at all.

Day 0, before and after

Today a new lead gets up to six emails in the first 18 hours, from three systems that can't see each other. The new journey sends two.

Today β€” up to 6 emails, no ask

  • T+0"…your results are ready!" (prints [Chosen Password])
  • T+0"One Last Step: Verify Your Account" (gates nothing; double-enrols into a second welcome)
  • T+0750 welcome E1 (1.14% unsub β€” a failing slot)
  • T+5 MIN750 welcome E2
  • T+1H"Your login is in this email" (free workouts β€” undercuts the $97 they just declined)
  • NEXT AMpersonal note (checkout-reachers)

New β€” 2 emails, each with one job

  • T+0The arrival email β€” your results, your login, your first workout (one email replaces three)
  • T+2HWelcome E1 β€” personalised to their goal and how far they got (nothing else sends on day 0)

Four people, four inboxes

Same journey, different behaviour. Colour = which machine sends it.

transactional (arrival) welcome & activation πŸ’° the pitch abandonment recovery the 50%-off event buyer β€” exits everything
M

Maya β€” finishes the quiz, sees her results, never opens the app

the most common path Β· goal: FAT_BURNER Β· obstacle: consistency
DAY 0
Arrival"Your results are ready β€” your plan, your login, your first workout"
Welcome E1 Β· +2hMirrors HER results"Your fat-loss plan starts at your actual level β€” here's what the quiz found."
DAY 1
Activation nudge"You said staying consistent is the hard part"One job: get her to workout 1. Zero price talk.
DAY 2
Welcome E2Jesse's origin story
DAY 4
Welcome E3The mechanism β€” personalised to her obstacleMicro-progressions = consistency is built in, not willed.
DAY 6
Welcome E4Proof β€” a real member matched to herA woman, her age band, same goal. Verbatim quote.
DAY 7
πŸ’° The askThe soft pitch β€” $97 for the yearShe never activated, so the ask leads with the plan, not the app.
DAY 9–26
The 50%-off eventStill not a buyer β†’ the 16-email event begins ($78.50 for the year)3 warm-up days β†’ cart opens β†’ 10 days β†’ cart closes β†’ survey. Then the long-term nurture pool.
B

Ben β€” does his first workout on day 1

the path we build for Β· goal: MUSCLE_BUILDER Β· obstacle: plateau
DAY 0
Arrival"Your results are ready β€” your plan, your login, your first workout"
Welcome E1 Β· +2hMirrors his results
DAY 1
Ben does workout 1 in the app. The nightly sync sees it.
DAY 2 AM
πŸ’° The pitch β€” jumps the queue"You just felt what it does. Here's the year β€” $97."The highest-intent moment in the funnel. Personalised: "you said hitting a plateau is what holds you back β€” that's what month one is built around."
DAY 3
πŸ’° ObjectionThe doubt, answered + the 60-day guarantee"Can bodyweight really build muscle? Leverage is load."
DAY 5
πŸ’° The smaller doorQuarterly β€” $49.97, standard price
DAY 9–26
The 50%-off eventStill not a buyer β†’ the event (his last, best price)
C

Carla β€” opens the $97 checkout, doesn't pay

recovery owns her first Β· goal: SKILL_SEEKER Β· her own words captured
DAY 0
Arrival"Your results are ready…" β€” login + first workout includedThe free access she'd have got "as a consolation" an hour after declining now arrived BEFORE the price did.
DAY 1
Recovery E1 Β· 24h"Your plan is built. It's still yours."Mirrors her plan, full $97, no deadline claim. Welcome WAITS β€” recovery owns days 0–3.
DAY 2
Recovery E2Her objection + the 60-day guaranteeEchoes what she told the quiz she fears β€” never quotes it back.
DAY 3–4
Recovery E4The smaller doors β€” quarterly $49.97, or start free
Welcome joinsCheckout-aware copy, re-checked she's still not a customer
DAY 9–26
The 50%-off eventStill not a buyer β†’ the event
1 IN 5
Holdout: one Carla in five gets NO recovery emails β€” the control arm that makes "recovered revenue" a real number instead of a story.
D

Dan β€” buys on day 2

any path, any moment
DAY 0
Arrival"Your results are ready…"
Welcome E1 Β· +2hMirrors his results
DAY 2
He buys πŸŽ‰Exits EVERYTHING, everywhere, at once β†’ buyer onboarding takes overNo more welcome, no pitch, no recovery, no event β€” ever. This is the exit that must be verified in the builder (the 5-minute check on your list).

The machine behind it

One automation, branched on what they did β€” then one 5-email spine, three exits. This replaces today's 10-email / 23-day welcome.

1 PERSON FINISHES THE QUIZ …and types their EMAIL 2 T+0 · THAT SECOND — two emails go out (Django→SendGrid) EMAIL A · VERIFY YOUR ACCOUNT “One Last Step: Verify Your Account” unchanged EMAIL B · YOUR RESULTS ARE READY “…your plan, your login, your first workout” their login · [N] free workouts · no card EVERYONE who finishes (this was Nic’s T+1h login email — moved to T+0) GONE “{Name}, your results are ready!” printed [Chosen Password] literally the T+1h login send email B does its job at T+0 then ActiveCampaign — ONE machine (it REPLACES 750 TMA | Welcome Sequence After Quiz — see step 8) 3 2 SAFETY GATES already a customer? already in the app-signup welcome? out → 975 Onboarding | After The Purchase customers are NEVER marketed to — buying from ANY step below exits here WAIT ~2h the quiz data lands 4 HOW FAR DID THEY GET? ONLY GAVE THEIR EMAIL E1: “see the results you built” ↓ straight into the spine SAW THEIR RESULTS E1 mirrors their results ↓ straight into the spine OPENED THE CHECKOUT …and did NOT buy ($97 declined) ↓ RECOVERY takes them first 5 RECOVERY — and the 24h wait is ALREADY BUILT IN the lane only drafts at ≥24h, so they meet us FIRST, then get asked. No builder change. T+1h · the personalised hello@ note (the engine) their goal · their obstacle · their checkout link · it ASKS for the sale 🏹 DRAFTS only — there is NO send path in the tool. 39 sent Fri 7 Aug · 1 draft waiting NOW 756 / 757 Part 1 & 2 - Abandoned Cart Reminder replaced by the note — paused, 0 ever entered, never sent one email buys → 975 (up top) · still no buy by day 3–4 ↓ joins the spine arriving mid-conversation — E1 skipped, they already know who we are day 3–4, unconverted 6 THE SPINE — the welcome that sells 5 emails · ~7 days · this IS the welcome — nobody also enters 750 E1 who you are — stage-aware (their branch above personalises it) E2 Jesse’s origin story E3 the mechanism — aimed at THEIR obstacle E4 proof — the matched member quote E5 the ask — $97, soft ⚡ workout 1 done → the pitch jumps the queue, next morning buys at ANY point → 975 · exits everything ↑ 7 unconverted THE 50%-OFF EVENT · day ~9–26 16 emails · cart opens & closes · $78.50/yr still no LONG-TERM POOL nurture → re-engagement 8 “DO THEY ALSO ENTER OUR NORMAL WELCOME (750)?” TODAY: YES — measured 10 Aug: 52 of 52 checkout abandoners are inside 750 TMA | Welcome Sequence After Quiz RIGHT NOW — recovery and welcome talk over each other. IN THIS FLOW: NO. The spine IS the welcome — one machine, one conversation. 750 is replaced, never edited: the new automation ships beside it and 750 drains (Evolution-Law path — your GO on AGA-134). πŸ”΄ “WAIT 3 DAYS” IS RETIRED — Aga: “why would we wait 3 days on our hottest leads?” Right. Nothing waits. The order already works: T+0 access email · T+2h welcome E1 introduces us · ~24h+ the note ASKS. First-timers meet us before they are sold to, and the lane’s own ≥24h rule enforces it. ZERO builder changes needed. ✅ buyer-exit (“If customer → END”) CONFIRMED by Aga in the builder, 10 Aug — on ALL sequences. Buyers exit; that assumption is now checked.
One automation replaces today's tangle: two safety gates (customers and duplicates never enter), a two-hour wait for the quiz data to land, a three-way branch on how far they got, one five-email spine, and three exits. The gold moment β€” the pitch after workout 1 β€” can fire from anywhere in the spine.
Where each piece is specced: the journey β€” POST_QUIZ_JOURNEY_STRATEGY_2026-08-07.md (your six J-decisions on AGA-133) Β· the welcome machine drawn above β€” WELCOME_PERSONALISATION_ENGINE_EXPANSION_2026-08-07.md Β§4 Β· recovery β€” ABANDONMENT_STRATEGY_2026-08-07.md Β· the event β€” evergreen-sales-sequence-2026-08/DEPLOY_SPEC.md (16 emails built, waiting on entry).
Already ruled: the $97 window is binding (copy can't claim it until enforced) Β· recovery holdout armed 1-in-5 Β· the 39 notes sent. Still yours: the J-decisions Β· the 5-minute buyer-exit check in the builder Β· the win-back price.
Timing detail per lane lives in the docs; this page is the picture, regenerated whenever they change.

What changed & where we are

Session log β€” the rulings, the retractions, the open list

Everything decided and measured 7–10 Aug Β· the six claims of mine that were wrong and why Β· who owns what next

SESSION_HANDOFF_2026-08-10_JOURNEY_PAGE_AND_THE_RETRACTIONS.md

SESSION HANDOFF β€” the journey page, Aga's rulings, and six retractions

7 Aug eve β†’ 10 Aug 2026 Β· read this before touching the post-quiz journey, the abandonment lanes, 750 TMA | Welcome Sequence After Quiz, or the customer-journey page.

The one-line state: the post-quiz journey is now DECIDED and drawn; the abandonment machine is BUILT and drafting; nothing is blocked on analysis. Five things wait on Aga, and one of them is a one-minute click.


0 Β· WHERE EVERYTHING LIVES

The page Aga reads https://tma-customer-journey-2026.netlify.app β€” generated, never hand-edited
Builder tools/email-sequences/build_journey_page.py + journey_visual_partial.html
Deploy python3 tools/finance/estate_sync.py (entry customer-journey, mode stage)
Journey strategy master docs/company/ANALYTICS/POST_QUIZ_JOURNEY_STRATEGY_2026-08-07.md (Β§5.5b–d are this session)
Abandonment estate ABANDONMENT_STRATEGY_2026-08-07.md + operations manual Β· personalisation engine Β· fallback ladder

πŸ”΄ Never hand-edit the page. Edit the docs or the partial, run the builder, deploy. It regenerates on every sync.


1 Β· AGA'S RULINGS THIS SESSION β€” all binding

ruling detail
The four-email flow "That is it." 1 verification Β· 2 the access email (plan, login, free workouts) Β· 3 the abandonment email if they hit checkout and don't buy Β· 4 welcome E1 at ~2h. Use these four lines whenever the journey needs explaining β€” it is her framing and it is clearer than ours
BUYER-EXIT EXISTS She checked the AC builder: "If customer β†’ END" is present on all sequences. Goals are API-invisible, so her eyes were the only possible instrument β€” never mint a fleet row to re-verify this. Closes the June exit_on_conversion=0 contradiction
EVOLVE, DON'T REBUILD She killed the new-spine design: "we have a spine, why create a new one? we can personalise the current emails." Correct β€” 5 of 6 deltas retrofit into 750 TMA \| Welcome Sequence After Quiz. The new spine is SHELVED unless measured evolution stalls
"WAIT 3 DAYS" IS RETIRED "why would we wait 3 days on our hottest leads?" Right. Nothing waits β€” see Β§3
The T+0 SendGrid spec Verify stays Β· the login email is RENAMED "Your results are ready β€” your plan, your login, your first workout" and RETRIGGERED to email-capture for everyone Β· the [Chosen Password] results email is RETIRED. [N] (free-workout count) is undecided and blocks the copy
Two defects deprioritised "not important right now" β†’ FLEET-49

2 Β· πŸ”΄ SIX RETRACTIONS β€” READ THESE OR YOU WILL REPEAT THEM

Every one was mine, caught by Aga or by measurement. The estate's memory carries all six; the docs carry them inline so the page cannot mislead her the way I did.

# what I claimed what is true
1 "%QUIZPASSWORD% is verified, it already works β€” nothing needs building" NOT a live merge tag. AC has 54 custom fields, none a password. I matched a BUILD-PACK spec for an AC email to Nic's live Django send by content similarity. MANIFEST.json already recorded TX3 as "Aga's spec β€” sender postdates our clone", and this file's own notes already flagged that its build gate B6 was never verified. β‡’ ask Nic for a one-tap login LINK
2 "the abandon machine does nothing with them" It DRAFTS. --draft is its only mode. That produced the 39 notes. Only the detection is scheduled; the drafting is not. Reading --help settles this in one call
3 "restoring --hours 1 surfaced 91 abandoners / \$8,827" Identical at --hours 24. The pool is the 30-day --days window. The restore stands on its merits but revealed nothing
4 "the one email that actually asks them to buy has never been sent, not once" False, and said AFTER I had already retracted it once. The note asks twice at \$97 with their own link; 39 sent 7 Aug. I was thinking of 756 Part 1 - Abandoned Cart Reminder / 757 Part 2 - Abandoned Cart Reminder, which the note replaces
5 "the accelerator wording is a one-line fix" 32 occurrences across 25 files, incl. the buyer-welcome email. And tma-paywall-spec is not in ESTATE_SITES, so the corrected page cannot deploy through the sanctioned path
6 "abandoners: 91 / conversion 18.3%" My own denominator bug. 91 = Stripe (71βˆ’13=58) + 33 AC-tag-only, while 18.3% is the Stripe-only 13/71. Consistently 12.5%. $8,827 is flat 91Γ—\$97 though only 58 carry a real plan β‡’ a rough pool, never revenue

πŸ”‘ The pattern in 1, 2 and 4: I treat a dead artefact as if it were the whole channel. Before saying "we never do X", name which artefact would prove it β€” usually a sent log, a --help, or one API call I have already made.


3 Β· THE DECIDED FLOW (what to build toward)

T+0    β‘  verification        β‘‘ the access email β€” plan Β· login Β· [N] free workouts Β· no card
                                (was Nic's T+1h login email; RETRIGGERED to email-capture, EVERYONE)
                                RETIRED: "{Name}, your results are ready!" (printed [Chosen Password])
T+2h   β‘’ welcome E1 β€” they MEET us          ← first-timers don't know us; this must come first
~24h+  β‘£ the abandonment note β€” now the ASK  ← the lane's own β‰₯24h rule enforces this order

βœ… No builder change is needed for the ordering. The lane already refuses to draft before 24h ("the Django send owns the first hour", 7 Aug). "Wait 3 days" was an unexamined phrase inherited from the strategy doc's "recovery owns days 0–3" β€” it is retired.

The welcome: evolve 750 TMA | Welcome Sequence After Quiz slot by slot. Live scoreboard, 10 Aug (13 slots clear the β‰₯200-send bar):

slot sends unsub clicks
2985 "The systematic approach to unbreakable strength" 1,513 1.39% 1.78% worst eligible β†’ J5 starts here
3715 β€” SAME SUBJECT, bigger volume 5,666 1.18% 1.02% πŸ”΄ 67 unsubs vs 58 clicks Β· Aga's W-D5 pull
3716 "Our secret system (finally revealed)" 4,990 0.32% 0.28% πŸ”΄ 16 vs 14
2981 Β· 3045 Β· 3097 16–19k 0.20–0.34% ~1.0–1.1% healthy β€” a rebuild would have thrown these away

⚠️ 2985 and 3715 share a subject line at very different volumes β€” never treat them as one slot. The estate's older "769 sends / 2.86% unsub" figure matches neither live campaign.


4 Β· MEASURED FACTS WORTH INHERITING

  • 52 of 52 contacts tagged NEW quiz-reached-checkout are also inside 750 TMA | Welcome Sequence After Quiz β€” recovery and welcome talk over each other today.
  • 750 TMA | Welcome Sequence After Quiz carries 29 attached campaigns, not the "10" the estate kept saying β€” including "Gareth Case Study | 50% OFF | 48h LEFT" (~16k sends: a hard discount deadline inside a welcome) and two zero-send "New Campaign" strays.
  • There is NO send path for the hello@ note. Its docstring: "drafts only, Aga sends; no --live exists." 40 drafts created Β· 39 sent Β· 1 waiting in Gmail now (gate-cleared).
  • cart_abandon_reconcile.py (v3 / normal checkouts) has NEVER RUN β€” no state file, not scheduled, absent from the job register. v3 has β‰ˆ0 identifiable abandoners anyway ($24.97/90d), so: wire the capture job, defer the arc behind β‰₯20 abandoners/30d.
  • 756/757 Abandoned Cart Reminder: 0 ever entered. Enrolment refused (sentinel absent).
  • Cross-system handshakes: quizβ†’AC βœ… Β· workout-1 via com.tma.funnelstage βœ… (fields 78/79) Β· purchaseβ†’AC βœ… Β· "they reached checkout" β†’ the welcome ❌ Β· "has anyone emailed them today" is one-way β€” only our note asks; AC and Django never ask anybody.

5 Β· WHAT SHIPPED (code + page)

tools/retention/ac_automation_state_check.py NEW. Alarms when an AC automation silently changes state. Read-only; exit 2 = UNDETERMINED so a truncated fetch can't read as "no changes". Wired as check #14 of the 06:30 com.tma.cockpitwatchdog β€” no new launchd job
tools/finance/cockpit_watchdog.py the new check + its EXPLAIN entry (plain-English alert). πŸ”΄ HEALS values must be STRINGS keying into HEAL_CMDS β€” a tuple there crashes the whole 06:30 job; that bug was caught pre-ship
tools/finance/estate_sync.py "watch": [paths] on stage entries, folded into the change hash. Before this, editing a builder deployed nothing while the tool reported success
build_journey_page.py assert_no_undefined_css_vars() β€” refuses to write when a var is undeclared or declared in one theme block and not its siblings. πŸ”΄ An undefined CSS var in an SVG fill renders BLACK (this is why Aga's 50%-off box was a black rectangle)
quiz_abandon_sync.py --hours 24 β†’ 1, with the retired rationale and the condition that would justify restoring 24
The page jobs panel Β· do-list Β· 8-step diagram Β· two-lane recovery Β· decisions as a click-to-open list Β· W-D1–D4 SUPERSEDED Β· BUYER-EXIT settled Β· J5-NEXT added

Queue: AGA-137 closed on arrival evidence (750 paused 16:41 β†’ un-paused 18:03; the proof was a new entry at 18:22:50, not the status flag). FLEET-49 opened for the deferred wording sweep.


6 Β· OPEN β€” AND WHOSE

# owner item
1 Aga Β· 1 min Send the draft in hello@ (gate-cleared, to a MUSCLE_BUILDER who abandoned ~3 days ago)
2 Aga Arm-or-build: sending is her click today. Auto-send does not exist and must not be bolted on β€” spec it properly if she wants it
3 Aga Β· 5 min W-D5 β€” pull campaign 3715 from the welcome's E1 (the twin of the worst slot)
4 Aga [N] β€” the free-workout count. Blocks the Nic message
5 Aga J5-NEXT go on slot 2985 Β· D3 win-back price (recommendation: SAVE30 \$109.90) Β· J1–J6
6 Nic The T+0 retrigger + the one question: can that email carry a one-tap login link?
7 fleet J5 rewrite of 2985 Β· FLEET-49 wording sweep Β· wire the v3 capture job

πŸ”΄ Uncommitted: this session's code is on disk on branch proto-deploy-1777257452, not committed β€” Aga was told and hasn't asked for a commit.


7 Β· THE TRAPS THIS SESSION PAID FOR

  1. filters[automation]= on /contactAutomations is SILENTLY IGNORED β€” returns every automation. The tell: total 611,152 exceeded the automation's own entered 19,805. Use automations/<id>/contactAutomations. Cross-check a filtered total against a count you already trust.
  2. ACTIVE is a status, not an arrival. A closure needs the artefact working, not the flag.
  3. open(P,"w").write(open(P).read()...) truncates before it reads β€” a canary of mine "passed" a guard by testing an empty file. Read first, then write.
  4. Two dicts sharing a key + .index() = the wrong insertion point. Anchor on the enclosing structure.
  5. A "MUST NOT GO BACK" note I wrote myself is a decision with a premise, not a law. Write the premise beside the rule.
  6. Goals and branches are API-invisible; a campaign's parent automation is NOT. So "AC structure is builder-only" over-claims β€” say "cannot create or edit structure, and cannot see goals or actors."
  7. A naming lookup is also a liveness probe β€” ac_registry.py printing inactive where the markdown registry said ACTIVE is the only reason the 82-minute welcome outage was ever noticed.

8 Β· MEMORY (already written β€” start here, not with this file)

aga_the_four_email_post_quiz_flow_2026-08-10 Β· aga_rulings_2026-08-10_buyer_exit_and_evolve_750 Β· reference_the_hello_abandon_lane_detects_but_does_not_send (name is a misnomer, corrected inside) Β· feedback_active_is_a_status_not_an_arrival Β· reference_ac_automation_state_watchdog Β· ac_api_traps_filter_silently_ignored_2026-08-07 Β· reference_transactional_email_architecture_and_abandon_trigger Β· feedback_never_hand_run_netlify_deploy_use_estate_sync

Personalisation

Personalisation, explained

What we know about each person Β· how an email becomes personal Β· the safety rules β€” in plain words

PERSONALISATION_EXPLAINED_2026-08-07.md

Personalisation, explained β€” what we know, where it goes, how an email becomes personal

The plain-language home of the personalisation engine. Ordered by Aga, 7 Aug 2026: "a subpage that talks about personalization… step by step, everything that needs to be known, so the engines are gonna live here… explained as it would be to a layman's person." One engine powers every journey β€” quiz, abandonment, win-back, and whatever comes next. The technical contract lives in ABANDONMENT_PERSONALISATION_ENGINE_2026-08-07.md; this page is the version a human reads first.


1 Β· What we actually know about each person

When someone finishes the quiz, they don't just give us an email address. They tell us, in their own words, who they are. Since 7 August we save all of it β€” before that, it was thrown away the moment the quiz ended.

What we know Example Who we have it for
Their goal β€” what they actually want "build muscle" Β· "get out of pain" Β· "learn skills" everyone who gives an email
What's standing in their way "I can never stay consistent" Β· "I always hit a plateau" everyone
Their own words about what they fear "Never unlocking the skills I know are possible" β€” typed by a real person those who saw their results
Their age band and gender 35–44, male those who saw their results
Their training background years of gym Β· complete beginner Β· coming back from injury those who saw their results
Their weakest movement (from the assessment) "Squats" Β· "Abs" those who saw their results
How far they got gave email β†’ saw results β†’ opened the checkout β†’ started typing a card everyone, as it happens
Did they actually train? did their first workout in the app, or never opened it everyone, updated nightly

Two honest limits, so nobody over-promises:

  • The richer answers only exist for people who reached their results screen. Someone who typed an email and left told us their goal and their obstacle β€” nothing else. The emails must work either way (see Β§4).
  • This started on 7 August. Anyone from before that date has only their goal and plan saved. The data grows richer every day from here.

2 Β· Where it all lives

Think of it as one card per person inside ActiveCampaign (our email system). The quiz writes the answers onto the card the moment they happen. The app reports every night who trained. The checkout reports who opened it. Everything about one person, one place.

Two things worth knowing about the plumbing, in passing:

  • The "your results are ready" and login emails come from a different system (the website itself, via SendGrid). That system knows nothing about the card β€” which is one reason today's first-day emails feel disconnected from each other.
  • The app's "did they train?" signal updates once a night. So "send the pitch after their first workout" means the next morning β€” which is honestly the better moment anyway.

3 Β· How an email becomes personal β€” step by step

Here is the whole trick, and it's simpler than it sounds:

  1. A small program reads the person's card and writes finished sentences onto it β€” one per slot. Not codes, not labels: real sentences. For a man in his forties with gym experience whose obstacle is plateaus, it writes: "And you told the quiz that hitting a plateau is what's been holding you back."
  2. The email contains slots. Where a generic email says "training is hard to stick with", ours has a slot that says: put this person's obstacle sentence here.
  3. ActiveCampaign fills the slots at send time β€” the same way it already fills in a first name. Each person gets the sentence that was written for them.
  4. A matching real member is chosen as proof. The engine picks a verbatim quote from a real member who matches their goal, age band and gender β€” a 45-year-old man chasing muscle sees a 45-year-old man's words, never a random testimonial.

So a real email reads like this (this is actual output, not a mock-up):

Subject: about the plateau you mentioned The quiz built you a muscle plan that starts at your actual strength level. And you told the quiz that hitting a plateau is what's been holding you back. For men in their forties, with years of gym training behind them, progressive calisthenics is actually perfect. And the quiz says that's exactly where you are. …followed by a real member, his age, his words.

Why we do it this way and not "smart templates": ActiveCampaign cannot do if/else inside an email. We tested this properly β€” zero of 539 real emails in our account use conditional blocks, because they don't exist in AC. Any "conditional" markup just shows every version to every reader. So the intelligence lives in our program, and the email stays dumb and reliable.

4 Β· The safety rules β€” in plain words

These are laws, enforced in the code, not preferences:

  1. If we don't know something, the sentence degrades gracefully β€” it never guesses. No age? The "people like you" line talks about their goal instead of their age. No obstacle? That paragraph simply isn't there. Guessing wrong ("in your 40s" to a 25-year-old) is worse than generic β€” it turns "Jesse knows me" into "this is a template that missed".
  2. An empty slot must never leave a hole. A missing field renders as nothing in AC β€” so every slot is always written, with a sensible default when the data is missing, and slots are whole paragraphs, never half-sentences.
  3. A buyer never gets a sales email. Every lane checks "have they paid?" at the moment of sending β€” and if the check itself fails, we treat them as paid and skip. Losing one send is fine; selling to a customer is not.
  4. One conversation at a time. If the recovery emails own someone this week, the welcome waits. Nobody gets two competing offers at once.
  5. No claims the code doesn't keep. The $97 window is not enforced by the checkout yet, so no email may say "expires in 48 hours" until it truly does.
  6. Real quotes only, word for word, from the tagged testimonial database β€” and the member's own real details carry the proof even when we know little about the reader.
  7. Everything is logged. Every run writes a per-person record: what we knew, which sentence each slot got, which quote was chosen, which link was used and that it worked. Anyone can audit any email after the fact.

5 Β· What's live today vs being built

Piece State
The quiz saves everything (the card) βœ… live since 7 Aug β€” verified on real people
"Did they train" signal, nightly βœ… live on 12,000+ contacts
The engine (sentences, matching, checkout links) βœ… live β€” it built and sent the 39 personal abandonment notes on 7 Aug
Replies watched, personal answers drafted within 12h βœ… live
The honest measurement (holdout β€” 1 in 5 abandoners deliberately not emailed, so "recovered" means something) βœ… armed 7 Aug
Writing the sentences onto cards for the welcome journey πŸ”¨ being built (the welcome writer + its 7 new fields)
The personalised welcome + the pitch-after-workout-1 πŸ”¨ specced, waiting on your J-decisions
The 16-email 50%-off event βœ… written and checked Β· waiting on where it enters
Personalising the win-back ⏳ after abandonment proves the pattern β€” same engine, its own copy

6 Β· Who does what

  • The engine renders sentences and picks proof. It never invents copy.
  • The email fleet writes every sentence variant, through the full review gates (voice, formatting, facts, deliverability). The engine only stores what the fleet wrote.
  • Aga approves copy, arms anything member-facing, and owns the decisions.
  • ActiveCampaign just delivers β€” the same way it always has.

The one idea to keep: we stopped trying to make the email system clever. The cleverness happens on our side, per person, before the send β€” and every journey (quiz, abandonment, win-back) draws from the same card, the same engine, the same rules.

Quiz journey

The post-quiz journey β€” strategy of record

One journey, five stages, gated on behaviour Β· the six J-decisions Β· the phased build

POST_QUIZ_JOURNEY_STRATEGY_2026-08-07.md

THE POST-QUIZ JOURNEY β€” the strategy of record

β­πŸ”΄ THE master strategy for everything a person receives after finishing the quiz β€” ordered by Aga, 7 Aug 2026: "I really need a strategy on the quiz journey sequence… a document outlining the proper right way. Emails that are currently going out are still not personalized, and then obviously there is the abandonment on top." Scope: from the second the quiz ends until they buy, opt down, or hand off to re-engagement β€” both the free-app journey and the money journey, and how the abandonment estate overlays it. Sister master: ABANDONMENT_STRATEGY_2026-08-07.md owns Stage 4 (recovery) in full; this doc owns the journey it overlays. Data law: LEAD_SIGNAL_CONTRACT_2026-08-06.md Β§8. Engine: ABANDONMENT_PERSONALISATION_ENGINE_2026-08-07.md. Every number tagged [LIVE] / [MODELLED] / [UNVERIFIED]. Nothing in Β§7 executes without Aga's approval of the J-decisions below.

🟒 STATUS UPDATE β€” 10 Aug 2026 (live-verified, gate-passed): three of this doc's findings are now RESOLVED. (1) The welcome arc no longer sells β€” Aga ruled "no welcome emails should have 50% off" and it was executed the same day: 0 offer references across all 24 live messages of 750 TMA | Welcome Sequence After Quiz (measured via the API, tma-ac-upload-integrity PASS). (2) The welcome CTA question is RATIFIED LAW: activation-first, per WELCOME_CTA_DOCTRINE_2026-08-10.md (owner seat tma-welcome-sequence-owner) β€” and W7 (day 7) now carries the arc's ONE soft ask: the 7-day free trial (checkouts-v3/trial/12month/, all 3 split variants, fleet-written and QA-gated). This replaces Β§5.1's "move or change price" options for the old E5/E7/E8 asks β€” they are gone, not moved. (3) Stage 2's data question is answered: the workout-1 signal lives in Intercom (workouts_finished_number, minutes-fresh β€” verified live 10 Aug; ~48 people/day hit workout #1; census: 2,805 trained-never-paid). The Stage-2 morning-after email is designed as an event overlay (nightly 0β†’1 tag β†’ mini-automation β†’ send at ~8am; W7 skip-condition for the already-pitched) and awaits Aga's build GO. Note: the Stage-2 ask shipped as the free trial, not the $97 β€” per the ratified doctrine and Aga's no-offer ruling; J4's two-price design now applies to Stages 3–4 only.


0 Β· THE VERDICT β€” and the six decisions

TMA treats a new quiz lead as an email list to drip on, when they are actually a free user we have 72 hours to activate. The journey a lead meets today was assembled system by system β€” four transactional sends, a welcome drip, a planned sales arc, an abandonment overlay β€” and no layer knows what the person has actually done. The result, measured: five to six emails in the first 18 hours with no single ask, a welcome arc personalised by first name only while ten fields of quiz data sit unused, the product's own free trial (3 workouts, no card) treated as a consolation prize instead of the conversion mechanism, and the highest-intent moment in the entire funnel β€” the minutes after someone completes their first workout β€” carrying no email at all.

The fix is one journey with five stages, gated on behaviour rather than time, built almost entirely from assets that already exist. Behaviour-triggered onboarding beats time-drips by ~22%, and activated users convert at 5–10Γ— β€” the strategy below is the industry-standard shape applied to TMA's actual assets.

The six decisions (J1–J6). Everything else in Β§7 is fleet work:

# Decision Recommendation
J1 πŸ”΄ SIMPLIFIED BY AGA, 7 Aug eve β€” the shape is decided; one input is a question for Nic. Don't build a new arrival email. Take Nic's existing "Your login is in this email" β€” it already carries the free first 3 workouts, both app-store links and the quiz root β€” and fire it on results-completion, to everyone. Then AC handles the rest of the journey. GO on the trigger change. ⚠️ One question, not a build: can that email state a working credential? Ask for a one-tap login LINK. πŸ”΄ A plaintext password is NOT available β€” see Β§5.5b, the %QUIZPASSWORD% claim was retracted on measurement.
J2 Adopt behaviour-gating on fields 78/79 (no_workout / no_trial, live on 12,266 contacts) Yes β€” with the nightly-refresh caveat in Β§3c. The branch exists today; no Nic needed for segmentation
J3 Where does the evergreen 50% arc enter β€” and what happens to 760 School of Strength (NEW)? The arc enters at the JOURNEY EXIT (Stage 2 completed, unconverted, ~day 8–10) β€” not from 750's terminal. 760 is superseded as the bridge (it sells the same offer on a second clock β€” the exact 00g collision). AGA-113's repoint should NOT be executed as written
J4 The Stage-2 pitch price: the quiz's $97 or email's $78.50? $97 while their quiz window is alive, $78.50 in the evergreen arc after it. Two prices, two moments, never simultaneous β€” this also finally reconciles AGA-114
J5 Scope of the 750 fix: rebuild or surgical? Surgical, EVOLUTION-LAW-compliant: fix the E1/E2 five-minute gap + retire the unsub-outlier slot + add the engine's personalised fields to ONE slot at a time. No wholesale rewrite of a live arc
J6 Should activation (Stage 1) exist as its own arc, or fold into 750? Own micro-arc (3 emails max), replacing 750's front β€” 750's remaining content emails become the Stage-1/2 fallback body. Detail in Β§5.1

1 Β· THE CURRENT STATE, MEASURED β€” what a quiz lead actually receives

Sources: the SendGrid census (sendgrid_transactional_check.py), the AC live reads behind REVIEW.html (build_review_page.py), the abandonment master Β§1a, and the estate sweep of 7 Aug. πŸ”΄ One law inherited from building that review: the AC API cannot tell you an automation's order β€” ordernum ties and parent is empty. Order is read from SEND VOLUME (the first email always carries the highest), and any step under ~55% of the top is badged position uncertain.

1a Β· The first 18 hours (both funnels' shared trunk)

Clock Email System Measured Verdict
T+0 "{Name}, your results are ready!" Django→SendGrid 51% open · prints literal [Chosen Password] keep, becomes the arrival email (J1)
T+0 "One Last Step: Verify Your Account" Djangoβ†’SendGrid gates nothing (is_active set at creation) Β· clicking enrols into 992 TMA \| Welcome Sequence AFTER APPSIGNUP AB TEST Aug4th β€” a second welcome arc πŸ”΄ kill (J1)
T+0 750 TMA \| Welcome Sequence After Quiz E1 β€” campaign 2981 "you did the part almost everyone skips" (19,142 sends = the true first) AC E1-slot unsub outlier measured at 1.14% on the inserted 3715 restructure (J5)
T+5min 750 E2 AC five-minute gap β€” a drip pretending to be a burst fix the wait (J5)
β‰ˆT+1h "Your login is in this email" β€” free 3 workouts + login Djangoβ†’SendGrid 10.5/day Β· 29.3% open β€” the de-facto abandon email merges into the arrival email (J1); its content is the product's front door, not a recovery concession
T+24h+ the abandonment overlay (1:1 note; the AC arc once armed) Gmail / AC 39 sent 7 Aug Β· holdout armed 1-in-5 unchanged β€” Stage 4

1b Β· The drip after day one

750's 10 live emails run on time, not behaviour. Personalisation census across the whole journey (750's 10 + the evergreen's 19 + 760's 13 = 42 emails): %FIRSTNAME% 42/42 Β· any quiz field 0/42 [LIVE census 7 Aug] β€” Aga's complaint is exactly correct and not an exaggeration. πŸ”΄ And the evergreen arc's %FIRSTNAME% has NO fallback in 19/19 files (750 and 760 use %FIRSTNAME|there%) β€” a no-name contact opens with ", If you're…". Fix in build_evergreen_sales_arc.py, never the output files.

πŸ”΄ [RESOLVED 10 Aug 2026 β€” the arc no longer sells; kept as the record of what was.] πŸ”΄ 750 is not a pure welcome β€” it already SELLS. Three of its ten emails carry /50promo (E5 "Marcus waited 11 years" β€” the real first ask Β· E7 soft mention Β· E8 "last one"). So the journey's CURRENT state has the 50%-off ask landing from ~day 10 inside the welcome β€” before any sales arc β€” which collides with both the $97 quiz window AND any future evergreen event. Β§5.1 resolves this. (Historical trap, fixed 6 Aug: E4/campaign 3107's "50% OFF | 48h LEFT" existed ONLY in the plaintext alternative β€” the phantom-offer class.)

Its terminal enter-block (ordernum 19) points at 751 Nurture Sequence 1 today [LIVE 6 Aug API read]; the chain runs 751 β†’ 758 β†’ 759 where 758 holds 14,619 in flight with zero offer on any surface (registry's 4,163 is stale β€” two independent Aug reads agree on ~14.6k) and 759 reads 0 entered despite a live inbound block [UNVERIFIED why β€” builder-only]. The planned repoint to 760 School of Strength (NEW) (AGA-113, open) would insert a second sales clock β€” see J3. πŸ”΄ Two 4-Aug rulings on 750 remain unexecuted: pull campaign 3715 from E1 (still sending β€” 5,540β†’5,622) and move 750 to a day-3–4 start (gated on the [Chosen Password] fix).

1c Β· The sales layer that exists but has never fired

The evergreen 50% arc: 16 emails (D-3β†’D13, PLF shape, $78.50 annual as "six months free"), built 6 Aug from the proven Memorial Day library, all 19 files lint-PASS, not uploaded, entry undecided (DEPLOY_SPEC.md Β§2 offers two mutually-exclusive entries and defers to Aga). Its promo opt-out is a mailto: because no preferences page exists β€” all five candidate URLs 404 [LIVE 6 Aug].

1d Β· What this adds up to

  • Volume without an ask: 5–6 emails in 18h, none of which asks for the sale (the abandonment master's headline β€” it holds for the whole journey).
  • Data without personalisation: persona + obstacle on every lead, eight more fields on results-seen, ten AC fields live β€” used by zero live sends.
  • A free trial without a journey: the product gives 3 workouts free with no card, and no email is triggered by whether the person ever does one.
  • A sales engine without an entrance: 16 finished sales emails wait on one builder decision β€” and no queue row authorises their deployment at all (fixed this session: J3 carries it).
  • πŸ”΄ The buyer exit is UNVERIFIED β€” the highest-stakes unknown in the journey. Two sources contradict: the Aug reads cite 750's If customer β€” END goal as "what stops buyers being sold to"; the June architecture audit measured exit_on_conversion=0 on every automation. Goals are builder-only to inspect β€” a five-minute check for Aga (Β§7 Phase B). Independent evidence the exit LEAKS regardless: a paying customer (jacnast@) was found inside 992's lead welcome.
  • πŸ”΄ The verify-email double-welcome is live and was UNOWNED. Clicking "Verify Your Account" runs activteUserAndToList() β†’ tag free_signed_up_mobile_&_web β†’ enrols into 992 TMA | Welcome Sequence AFTER APPSIGNUP AB TEST Aug4th. Half of 992's first entrants were the wrong audience (a quiz lead in TWO full welcome arcs; a paying customer in a lead arc). A fix row now exists (minted 7 Aug PM β€” J1 kills the email; the row covers the tag routing).

2 Β· THE STRATEGY β€” one journey, five stages, gated on behaviour

Design principle: email follows what the person DID, not how long ago they quizzed. The quiz's promise is "your plan is ready" β€” every touch either helps them use it or asks them to own it.

quiz ends
   β”‚
   β–Ό
STAGE 0 Β· ARRIVAL (T+0, transactional β€” ONE email)
   "Your results are ready β€” your plan, your login, your first workout."
   (results-ready + login/free-workouts merged Β· verify killed Β· nothing else sends day 0)
   β”‚
   β–Ό
STAGE 1 Β· ACTIVATION (day 0–3 Β· goal = WORKOUT 1, not money)
   branch nightly on field 78:
   β”œβ”€ did workout 1 (`no_trial`)  ──────────────► advance to Stage 2 NOW
   └─ never opened it (`no_workout`) β†’ ≀3 nudges, zero price talk
        └─ still nothing by day 4 β†’ Stage 2 anyway (soft fallback form)
   β”‚
   β–Ό
STAGE 2 Β· CONVERSION (behaviour-triggered Β· the pitch)
   "You just felt what it does. Here's the year." β€” $97 while their quiz window lives (J4)
   persona/obstacle-personalised via the engine's pre-rendered fields
   2–3 beats: the ask β†’ objection+guarantee β†’ the smaller door (quarterly $49.97)
   β”‚  buys β†’ STAGE B (buyer) Β· 975 onboarding β€” exits everything, everywhere
   β–Ό unconverted by ~day 8–10
STAGE 3 Β· THE SALES EVENT (the evergreen 50% arc, 16 emails, D-3β†’D13)
   the scheduled second-chance engine Β· $78.50 Β· enters HERE (J3), never in parallel
   β”‚ unconverted
   β–Ό
STAGE 5 Β· THE LONG POOL
   751/758 nurture (with a designed exit, not a dead end) β†’ 991 re-engagement β†’ sunset

STAGE 4 Β· RECOVERY (event-driven OVERLAY, any time)
   reached checkout + didn't pay β†’ the abandonment estate (its own master)
   entering recovery PAUSES whatever stage arc is running (00g: one conversation)

The one clocked table (extends the abandonment master's Β§5.2b β€” that table remains the timing authority for Stage 4; this is the journey view):

Moment Touch Stage Trigger type
T+0 the arrival email (ONE) 0 event: quiz completed
day 1–3 ≀3 activation nudges 1 state: no_workout, nightly
on workout 1 the pitch 2 πŸ”΄ behaviour β€” the moment that matters
day 4 (fallback) the soft pitch 2 time, only if never activated
+1–2 days objection + guarantee 2 time within stage
+2 days the smaller door (quarterly) 2 time within stage
~day 8–10 evergreen arc D-3 begins 3 state: Stage 2 exhausted, not a buyer
day 8–26 the 16-email sales event 3 its own internal clock
any time checkout view + no pay β†’ recovery 4 event, pauses 1–3
after 3 nurture β†’ 991 5 state

Frequency stays legal by construction: one arc owns the contact at any moment; the Core-tier 1/day ceiling and the 00g one-offer-conversation rule are properties of the design rather than checks bolted on. abandon_collision_gate.py remains the pre-send instrument for the Gmail and Django layers AC cannot see.


3 Β· PERSONALISATION β€” the plan for "the emails are still not personalized"

3a Β· What powers what

Stage Slot Field Tier gate
0 subject + plan line %TMA_QUIZ_PERSONA% (the goal) LEAD β€” every lead has it
1 the nudge's reason-to-open %TMA_QUIZ_OBSTACLE% ("you said consistency is the hard part β€” that's what session one fixes") LEAD
2 the pitch's opening sentence engine-rendered goal_line + obstacle_block (pre-rendered into fields 72–76 by the AC adapter, FLEET-44) Tier A; Tier-B fallback per the ladder
2 proof engine-matched member quote (theme Γ— age Γ— gender) any (member's own demographics carry it)
3 the arc stays broadcast-shaped persona opener optional per slot β€” single-slot tests only LEAD
4 fully personalised the abandonment estate's engine β€” already live per its ladder

The three laws travel everywhere (from Β§8 + the engine): stage-gated completeness Β· an unset AC field renders as an empty string, so every slot is written with a default Β· AC cannot branch β€” personalisation is pre-rendered in code, one merge tag per slot, never data-ac-condition.

3b Β· What this looks like in a real email

Tier-A pitch opener, rendered per person by the existing engine:

subject: "about the plateau you mentioned" Β· "The quiz built you a muscle plan that starts at your actual strength level. And you told the quiz that hitting a plateau is what's been holding you back β€” that's what the first month is built around."

3c Β· πŸ”΄ The one honest constraint β€” the behaviour signal is NIGHTLY

Fields 78/79 refresh from Intercom once a night (02:30). So "the pitch lands after workout 1" means the morning after workout 1, not the hour after. That is still ~100Γ— better than day-0 pitching, and next-morning is arguably the better moment anyway (they wake up feeling it). If we ever want same-hour triggering, that is an Intercom-webhook build for Nic β€” deliberately NOT in this strategy's build plan; the nightly grain ships now.


4 Β· GOVERNANCE β€” priority, exits, and the rules that keep it honest

  1. Priority ladder when two things could touch one person: Stage 4 recovery > Stage 3 sales event > Stage 1–2 arcs > Stage 5 nurture. Entering a higher stage pauses the lower (756-entry already removes 750/751 β€” the same mechanic generalises).
  2. A buyer exits everything, everywhere, at once β€” goal-stops on every arc + the fails-safe paid-check on every non-AC send. No exceptions, no new lane without it.
  3. No deadline claims until D2 enforcement ships (Aga ruled the window binding, the code does not enforce it yet) β€” Stage 2 sells on the plan, the guarantee and the price, not a countdown.
  4. One offer per email Β· ladder descends only post-refusal ($97 β†’ quarterly β†’ the evergreen's $78.50 event β†’ win-back's reason-gated SAVE30). The free app is not on the ladder β€” it is the product, given at Stage 0 by design (Aga's reframe, 7 Aug).
  5. Locked facts as everywhere: 4.8β˜… never 4.9 Β· under $2/week Β· the exact accelerator line Β· 60-day guarantee Β· no Liquid, AC merge tags only Β· no fabricated imagery.
  6. The journey's exits are designed, not accidental: Stage 3 unconverted β†’ nurture with the D13 survey's six utm_content reasons segmenting the hand-off; nurture β†’ 991 re-engagement; nobody ever "completes into silence" again.

5 Β· WHAT CHANGES FOR EACH EXISTING ASSET

5.1 Β· 750 TMA | Welcome Sequence After Quiz β€” restructured, not rewritten (J5, J6)

Its front (E1/E2 burst) is replaced by Stage 0+1; its content/story emails become the body that runs alongside Stage 1–2 at a legal cadence; the unsub-outlier slot (1.14%, campaign 3715 β€” already ruled out by Aga on 4 Aug, never executed) is the EVOLUTION LAW's worst-eligible-slot and gets the first single-slot fix; its terminal enter-block points at the Stage 3 entry, not 751 directly.

πŸ”΄ [RESOLVED 10 Aug 2026 β€” the three asks were REMOVED under Aga's no-offer-in-welcome ruling; W7 now carries the doctrine's soft trial ask instead. See the status banner at top.] πŸ”΄ And its three /50promo asks (E5/E7/E8) must move or change price. As-is they offer 50% off from ~day 10 β€” undercutting the $97 Stage-2 pitch (why pay $97 on day 2 if day 10 brings $78.50?) and pre-empting the evergreen event (the same offer with no event mechanics). Under J4's two-price rule they either become the Stage-2 $97 ask (single-slot swaps) or they retire and the 50% ask lives only in the Stage-3 event. This is the price-integrity fix that makes J4 real, and it was invisible until the census β€” the strategy's clean staging cannot coexist with a permanent 50%-off ask buried in the welcome.

5.2 Β· 760 School of Strength (NEW) β€” superseded as the bridge (J3)

It sells the same offer as the evergreen arc on a second clock β€” the exact collision 00g exists to prevent. AGA-113's one-field repoint should not be executed as written. Aga's options: retire 760, or keep it exclusively for the LEGACY pool (contacts who predate this journey and will never enter it). The doc recommends retire β€” one sales engine, one clock. (If Aga keeps it for the legacy pool: its E1 "Marcus could deadlift 435 lbs" collides with 750 E5's different Marcus ~10 days apart β€” the single-slot rename is mandatory before both run to anyone.)

5.3 Β· The evergreen arc β€” gets its entrance (J3) and three finishing tasks

Enter at the journey exit (~day 8–10 unconverted). Before arming: the preferences page (the opt-out is a mailto: today and every candidate URL 404s β€” one page + one AC automation + tag promo-opt-out in every promo exclusion set) Β· the 760 decision Β· Aga's shells/upload per its DEPLOY_SPEC.

5.4 Β· The abandonment estate β€” unchanged, better fed

Stage 4 as designed; the arrival email raises checkout-reacher quality; recovery E1's "mirror their plan" now lands on people who may have done a workout β€” the copywriter seat gets that context via field 78 in the enrichment.

5.5 Β· SendGrid transactional β€” from 4 sends to 2

Arrival (merged) + password reset. The verify email dies (J1); [Chosen Password] dies with the merge. Every remaining transactional send is measured by sendgrid_transactional_check.py.

5.5b Β· πŸ”΄ J1 AS AGA SIMPLIFIED IT β€” one trigger change, and a collision dissolves

Her instruction, 7 Aug eve: "Nic is already sending the 'Your login' email to a checkout abandoner β€” why not just send this email with password generated to EVERYONE who finished the results, and that's it. The rest we can take care of from the ActiveCampaign side."

Her shape is right and it is better than the merge I proposed β€” take an email that already exists and works, and point it at everyone. Two of my three objections do fall away. The third does not, and I got it wrong twice before getting it right.

What I worried about Where it actually stands
"the merged email needs building" βœ… Correct β€” nothing to write. It already contains the free first 3 workouts, both app-store links, the quiz root and success-stories. Its one gap: no "see your results" link.
"results-ready needs its password fixed" βœ… Moot. The literal [Chosen Password] line (quiz_email.html:24) should simply be deleted β€” it has never rendered a credential.
"Nic must plumb the credential through" πŸ”΄ STANDS. My "already plumbed" claim is RETRACTED. See the box below.

πŸ”΄ RETRACTION β€” read this before quoting anything about %QUIZPASSWORD%. At 21:10 I wrote that this was verified and the credential was already plumbed. It is not. Measured against the live ActiveCampaign API twenty minutes later: 54 custom fields exist and not one is a password. %QUIZPASSWORD% resolves to nothing.

How the error was made, because the mechanism is reusable: the file I read (…/TMA_QUIZ_CHECKOUT_BUILD_PACK_2026-07-29/04_EMAIL_FREE_WORKOUTS.html) is a BUILD PACK β€” a spec for an AC email that was to be built, in AC merge syntax. Nic's live "Your login is in this email" is a Django/SendGrid send. I bridged the two on content similarity β€” both carry free workouts and a login β€” and credited the live email with the spec's variable. Two estates, one plausible-looking bridge. This is the estate's own a label is not the thing rule.

What IS verified, from code (apps/quiz/serializers.py:71): python password = self.validated_data.get('password', settings.SECRET_KEY) Three outcomes, no fourth β€” the user typed one β†’ real password Β· the key is blank β†’ line 81's if created and password: is falsy, set_password never fires, the account has an unusable password Β· the key is absent β†’ the password becomes settings.SECRET_KEY. β‡’ There is no password generation anywhere in the funnel, so "with password generated" describes something that does not exist yet. (The SECRET_KEY branch is worth flagging to Nic on its own merits, separately from this journey work.)

⚠️ And the tag lies: add_tag_to_contact(email, 'QuizPasswordProvided') fires at line 67, unconditionally, before the password is ever read. Never read it as evidence a password exists.

πŸ”‘ The estate already knew, and said so in writing. marketing/email-sequences/sendgrid-transactional-snapshot/MANIFEST.json records TX3's source as "tma-paywall-spec.netlify.app/email (Aga's spec) β€” sender postdates our clone" β€” i.e. an earlier session had already established that this artefact is the spec, not the live sender. That same snapshot's annotation flags the deeper problem: the spec's own build note says "B6 (quiz password logs into the app) must be verified before the first send" β€” and no B6 pass is recorded anywhere in the estate. So even where the variable appears, whether the password actually logs anyone in was never verified. β‡’ I did not just misread a file; I contradicted a finding already written down. Check-before-you-ask applies to our own records, not only to Aga's.

Whether the live email can state a working credential is therefore [NOT KNOWABLE] from here β€” the sender postdates our 2 Apr Django clone, and SendGrid delivers only, so it holds no bodies. β‡’ Ask Nic for a one-tap login LINK, which is the right shape regardless: it is the industry-standard mechanism, it needs no B6-style verification of a password nobody chose, and it sidesteps mailing a plaintext credential entirely.

And the part worth noticing: it dissolves the collision that has been shaping our whole arc. Results-completion happens before the checkout, so everyone who reaches the checkout has already received this email. The ~1h post-checkout trigger therefore becomes redundant, and should be removed β€” one email, one trigger, no duplicate.

β‡’ The T+1h Django send that our abandonment lane was holding 24 hours to avoid stops existing at T+1h. quiz_abandon_sync.py's --hours 24 floor was there only for that collision, so once this ships E1 can return to ≀60min β€” the timing the arc was designed for, and the timing Β§5.2b flags as the one that makes E3's window beat coherent again. D1's whole problem also disappears: the free app now arrives before the price rather than an hour after it was declined.

The ask to Nic β€” one trigger change and one question:

  1. Fire "Your login is in this email" on quiz/results completion for everyone, instead of ~1 hour after an abandoned checkout β€” and drop the [Chosen Password] line from the results email.
  2. Can that email hand the person a one-tap login link? If not, what is the cheapest way for a brand-new quiz finisher to get into the app without typing a password they never chose?

Question 2 is the whole open item. Everything else about J1 is settled: the trigger, the audience, and the collision that dissolves. Until it is answered, no email may promise a password or state login credentials β€” the results email has been shipping a literal [Chosen Password] to every quiz finisher, and repeating that failure in a bigger audience is the one way this change makes things worse rather than better.

5.5c Β· πŸ”΄ THE DECIDED T+0 STATE β€” Aga's ruling, 7 Aug eve, and the message to Nic

Aga ruled the SendGrid layer. After the change a quiz finisher gets TWO emails at T+0, not five β€” and the T+1h abandon send goes away with them, because email 2 has already done its job:

# Email After
1 "One Last Step: Verify Your Account" βœ… STAYS (90 sends/2d)
2 the login email, renamed "Your results are ready β€” your plan, your login, your first workout" βœ… RETRIGGERED to email-capture, carries login + password + [N] free workouts
3 "{Name}, your results are ready!" πŸ”΄ RETIRED β€” #2 replaces it (this is the one printing the literal [Chosen Password])

The trigger she picked is EARLIER than the one I had drafted. Not results-completion β€” the moment the person gives their email, which is the same post_save on Quiz that fires #3 today. Measured consequence, from the tag census: that reaches 16, not the 10 who see results β€” it deliberately includes the ~6-of-16 who finish the questions and never view their results, who today end up with an account and no way into it. That is the right call and it is a bigger expansion than mine.

πŸ”‘ It is ONE template swap, not "move a trigger + stop an email"

signals.py:14 β†’ send_post_quiz_email_for() β†’ mail.py:47 renders quiz_email.html. Point that one call at the new template and both halves happen at once β€” the old email stops because it is the thing being replaced, on a trigger that is already correct. Then the ~1h checkout trigger is removed separately. Simpler than the two-step the brief describes.

πŸ”΄ …and the swap exposes the credential gap rather than solving it

send_post_quiz_email_for(email, first_name) receives no password. The new template wants one. So the swap forces the very plumbing that was in question β€” and this is now the only thing between this plan and shipping. πŸ”΄ The failure mode scales with the audience: the spec has never sent (SendGrid, 2 days: 22 Γ— "Your login is in this email", 0 Γ— "Try the app on us"), so %QUIZPASSWORD% has never been observed rendering, and the spec's own build gate β€” "B6: quiz password logs into the app must be verified before the first send" β€” has no recorded pass. If it resolves to nothing, every quiz finisher receives a raw %QUIZPASSWORD% string instead of 22 checkout-reachers receiving it. β‡’ Ask for a one-tap login LINK, which needs no password, no B6, and cannot render as a broken token.

⚠️ Two defects to fold into the message before it goes

  1. The number 3 appears FOUR times, not three. The brief names "We want to give you the first 3 workouts", "Free to download Β· your first 3 workouts included Β· no card", and the P.S. "The 3 free sessions don't expire" β€” but there is a fourth in the app-store link block, also "your first 3 workouts included". Change three of four and a stale number ships next to the corrected ones.
  2. πŸ”΄ The accelerator wording on the live spec page is wrong and is a claims defect. It reads "Lead Sports Accelerator β€” led by Adidas and ex-Red Bull executives." Canon is "the LEAD Sports Tech Accelerator β€” run by the Adidas family and ex-Red Bull executives." Adidas does not lead it. This ships to everyone once the audience expands. βœ… Good news alongside it: the 4.9β˜… defect is already fixed β€” the page now reads 4.8β˜…, so that one needs no instruction.

Still open and only Aga's: [N]. The copy cannot ship with a placeholder, and the number of free workouts is a product decision, not a copy one.

5.5d Β· πŸ”΄ AGA'S CATCH, 10 Aug β€” the new design deleted the ONLY abandonment email we send

She asked: "so just for the record, in this new one we don't send an abandonment email at all? So far the ideal was that if someone started the checkout we immediately send them the email to recover. What is the industry standard?"

She is right and Β§5.5c was wrong. Verified live 10 Aug:

756 Part 1 - Abandoned Cart Reminder PAUSED Β· entered = 0 β€” has never sent an email
757 Part 2 - Abandoned Cart Reminder PAUSED Β· entered = 0 β€” has never sent an email
Nic's "Your login is in this email" 41 sends / 3 days β€” the only thing reaching an abandoner

So the red box saying "recovery owns days 0–3 (the abandonment arc)" describes an arc that has never fired, while Β§5.5c struck out the T+1h send β€” the one email that actually reaches these people. Net effect as drawn: a checkout abandoner would receive nothing. Corrected in the diagram: the T+1h slot is KEPT, and the checkout branch now says on its face that 756/757 have never sent.

πŸ”΄ …and she meant the hello@ 1:1 lane, not the AC arc. It is not sending either.

Clarified by Aga: "I am talking about the email jobs that are personalised and going out straight from our Gmail, at hello@." That is quiz_abandon_manual_notes.py β€” the lane behind the 39 notes. Verified live 10 Aug:

com.tma.quizabandon (launchd, every 30 min) runs quiz_abandon_sync.py --sync β€” detect + tag ONLY, sends nothing
--enrol into 756/757 REFUSED β€” the QUIZ_ABANDON_ENROL_ARMED sentinel is absent
the personalised hello@ notes manual one-shot. quiz_abandon_sent_log.csv last written 7 Aug 12:40 β€” 39 notes, nothing since

β‡’ The machine finds abandoners every half hour and then does nothing with them.

πŸ”΄ And the window was mine, not the machine's. I moved --hours from 1 β†’ 24 on 7 Aug to dodge the Django T+1h collision. That reasoning was right then and is retired now β€” the 1h slot is being repurposed, not duplicated, so there is nothing to collide with. Restored to --hours 1, which also lands on the 30–60 min benchmark. The obsolete rationale is preserved in the docstring so nobody re-derives it, with the condition that would justify bringing 24 back.

With 1h restored, the lane immediately sees 91 abandoners Β· $8,827 at stake Β· checkout conversion 18.3%. ⚠️ That $8,827 is GROSS AT STAKE, not recoverable revenue β€” good-performer recovery runs 10–14%, and RevenueCat 2026 finds ~20% of lapsed users reactivate with zero intervention, which is exactly why the 1-in-5 holdout is armed. Treat the honest prize as a fraction of it, measured against the holdout, never as the headline.

πŸ”΄ THE DECISION THAT IS AGA'S: does this lane auto-send, or stay manual?

The 39 notes went on her explicit GO. Arming continuous auto-send of personalised 1:1 notes from hello@ to members is a send decision, and it is hers β€” it is one command, and I will not run it unasked. The two shapes:

Stay manual she GOes a batch when she wants one. Safe; the cost is that 91 people are sitting un-emailed right now, and the pool ages
Arm it every abandoner gets a personalised note ~1h after abandoning, automatically, with the 1-in-5 holdout measuring whether it actually works

Recommendation: arm it, once the recovery copy asks for the sale. The lane's own collision gate (abandon_collision_gate.py) already reads all three sending systems plus live Stripe buyer-safety before each send, so the guardrail for auto-send exists. What is missing is not safety β€” it is that the current note still inherits the free-access angle rather than the ask (Β§5.5d).

The industry standard, and where we already sit

First recovery email at 30–60 minutes is the 2026 benchmark; under ~15 minutes reads as too reactive β€” the person may still be on the page or retrying a card. Then email 2 at ~18h, email 3 just over a day. πŸ”‘ Nic's existing ~1h trigger is already AT the benchmark β€” the timing was never the problem.

πŸ”΄ And checkout abandonment is the higher-intent event, distinct from cart abandonment: the person had begun paying and hit an obstacle, so it responds to structural fixes and a direct ask, where cart abandonment responds to information and price framing.

⚠️ The TMA-specific caveat that weakens "send instantly": our trigger is "landed on the checkout with an email already on it" β€” and the quiz PREFILLS that email, so the person never typed it. That makes our signal closer to a page view than to a real checkout start, and a slice of the audience is simply seeing the price for the first time. Which argues for 60 minutes, not 5.

β‡’ The fix: two emails, two jobs. Keep both.

when job
Email 2 "Your results are ready…" email capture (T+0) onboarding β€” login, [N] free workouts, get them to workout 1
A NEW recovery email ~1h after checkout, no purchase ASK FOR THE SALE β€” the thing we have never done

That is the actual prize here, and it has been hiding in plain sight: today an abandoner receives a free-access email and is never asked to buy, while the arc that would ask has never run. The timing was right all along; the content was doing the wrong job. So the instruction to Nic is "repoint email 2 to email-capture and leave the 1h slot in place" β€” not "remove the 1h trigger."

βœ… The collision worry also resolves cleanly: the two now fire at different moments (T+0 capture vs T+1h post-checkout) with different content and different asks, so there is nothing to double-send. Personalisation for the recovery slot is already built β€” the Tier A engine behind the 39 notes.

5.6 Β· Nurture/re-engagement β€” receives designed exits

751/758 keep their role for the long pool but stop being the default destination for a 3-day-old hot lead; 758's zero-offer problem and the 973 migration stay owned by the email fleet's existing rows β€” this doc only fixes who arrives there and when.


6 Β· MEASUREMENT β€” activation first, money second, honesty always

Metric Why it leads Instrument
Activation rate (quiz β†’ workout 1 ≀72h) activated users convert 5–10Γ—; it is the journey's true north star fields 78/79 census (funnel_stage_export), daily
Pitch conversion (Stage 2, split by activated/not) proves the behaviour-gating thesis Stripe money by cohort
Evergreen arc performance per-slot clicks/conversions, never opens its own UTMs (utm_campaign=evergreen-sales, per-slot utm_content)
Recovery lift touched vs holdout β€” inherited law quiz_abandon_recovery_read.py (holdout armed 1-in-5, D7)
List health unsub ≀0.30% per slot, complaint 0.10% hard-stop per-send AC metrics

The holdout philosophy generalises: any claim that a journey change "lifted conversion" needs either a control cohort or the honesty tag [UPPER BOUND]. Pre/post comparisons across the switch are inadmissible without noting the funnel discontinuity (promo calendar law).


7 Β· BUILD PLAN β€” phased, owned, nothing big-bang

Phase A β€” no approvals beyond this doc (fleet, this week): 1. The Stage-2 pitch + Stage-1 nudges written through /email-fleet gates against the engine's fields (extends FLEET-42/FLEET-44 β€” same renderer, new slots). 2. The preferences page + promo-opt-out automation spec (unblocks the evergreen arc's opt-out). 3. The activation census baseline: what % of the last 90 days' quiz leads ever reached no_trial? (One run of the funnel-stage census, filtered to quiz cohort β€” the number J2's success will be judged against.)

Phase B β€” Aga (~1h, after ruling J1–J6): 4. πŸ”΄ FIRST, the five-minute builder check: does 750's If customer β€” END goal actually exist and fire? The estate's two sources contradict (Aug citations vs the June audit's exit_on_conversion=0), goals are API-invisible, and everything in this journey assumes buyers exit. If it is missing, that is the most urgent fix in the whole document. 5. Approve J1 for Nic's brief (the merged arrival email + verify kill, ~1h of his time). 6. The evergreen shells + upload per DEPLOY_SPEC, with its entry per J3. 7. The 760 ruling (J3) β€” retire or legacy-only. AGA-113 updated accordingly, not executed as written. 8. 750's single-slot fixes approved one at a time (J5) β€” starting with the two she ALREADY ruled on 4 Aug (pull 3715; fix the E1β†’E2 five-minute gap).

Phase C — Nic (~2h total, sequenced behind his current queue): 9. The merged arrival email (one Django template replaces two, kills verify) — J1, which also closes the verify→992 double-welcome at its root (the tag routing row covers the interim). 10. (Already deferred by Aga: the 48h enforcement, AGA-132.) 11. Optional, later: the Intercom workout webhook for same-hour triggering (§3c) — only if the nightly grain proves too slow in the data.

Explicitly sequenced LAST: nothing in Phase A blocks on B or C. The pitch can ship against the nightly signal and current arrival emails β€” each later phase upgrades the journey without rebuilding it.


8 Β· SOURCES

Estate: the abandonment master + engine + ladder + ops manual (7 Aug) Β· LEAD_SIGNAL_CONTRACT Β§8 Β· REVIEW.html + build_review_page.py (the 3-layer render + the AC-order trap) Β· the evergreen DEPLOY_SPEC.md/CHANGE_LOG.md Β· EMAIL_REVENUE_STRATEGY_2026-08-05.md (the two-engine model this journey implements at lead level) Β· BROADCAST_CADENCE_AND_COLLISION_DOCTRINE Β· the SendGrid census Β· the AC field censuses (7 Aug). External (2026): behaviour-triggered onboarding +22% vs time-drips; activation-first trial doctrine (activated users convert 5–10Γ—); 3–4 segmentation tracks max; opt-out-style trials ~31% vs freemium ~2.6% β€” Mailgenius onboarding guide Β· Digitalapplied SaaS onboarding playbook Β· Custom.one quiz funnel guide Β· Sequenzy trial-email benchmarks Β· involve.me assessment-quiz churn study.

Welcome sequences

The welcome sequences β€” what runs, why, and how it works

All three doors mapped Β· the workout-1 moment Β· why only one arc ends Β· the rules that bind every welcome email

WELCOME_SEQUENCES_MAP_2026-08-12.md

THE WELCOME SEQUENCES β€” what runs, why, and how it works

The map of every welcome email TMA sends, and the reasoning underneath it. Written 12 Aug 2026 so that anyone β€” Aga, Nic, a future session β€” can see the whole estate at once instead of reconstructing it from four documents. The law it implements: WELCOME_CTA_DOCTRINE_2026-08-10.md (ratified by Aga, amended Β§7). Every number here was measured live, not taken from a doc.


1 Β· THE ONE IDEA: three doors, one system

Three different kinds of person arrive at TMA, and each gets their own welcome arc. Until 11 Aug they pulled in three different directions. They now all point at the same thing.

The idea in one sentence: nobody can train well until they have been measured, so every door leads to the measurement, and the ask for money comes only after they have felt the product.

Who they are How they arrived Their arc What their emails ask for
Quiz finisher Took the web quiz, didn't buy 750 TMA \| Welcome Sequence After Quiz Open the app and do workout 1 β€” they are already measured, so the plan is waiting
Lead-magnet lead Downloaded a free skill guide (planche, handstand, front lever…) 967 TMA \| Welcome Sequence After LEAD MAGNET Take the free quiz β€” a guide lists a skill's steps but cannot tell you which step you are ready for
App signup Installed the app, hit the price screen, didn't pay 992 TMA \| Welcome Sequence AFTER APPSIGNUP AB TEST Aug4th Take the free quiz β€” they own the app but have never been measured
(everyone, once they train) Completed their first workout the workout-1 lane (being built) The 7-day trial β€” the one moment the money ask belongs

There is a fourth arc, 988 TMA | Welcome Sequence AFTER APPSIGNUP, which is the app-signup arc's predecessor. It is retired as an entry β€” nobody new joins β€” but ~162 people are still finishing it, so on 12 Aug its door was repaired too, and it now says the same thing as its successor.


2 Β· WHY MEASUREMENT IS THE DOOR (the thing most people get wrong)

πŸ”΄ A free app signup has never been measured. Aga's own correction, 11 Aug: they answer a few setup questions, land on a home screen showing an example workout built for somebody who has already done the fundamentals, and the app asks them to upgrade to get the assessment.

So in-app measurement is behind the paywall. The web quiz is the free measurement door.

That single fact decides most of the estate:

  • It is why the app-signup arc points at the quiz and not at the app.
  • It is why, before 11 Aug, that arc was doing real damage: it told people "you need to be measured" and then sent them to pay to be measured β€” re-presenting the exact paywall they had just walked away from. One slot was unsubscribing at 8.77% against a 0.5% ceiling.
  • It is why the lead-magnet arc points at the quiz too: a PDF about the planche cannot know whether this reader's shoulders are ready for step 4 or step 11.

3 Β· WHAT EACH ARC ACTUALLY SAYS

750 TMA | Welcome Sequence After Quiz β€” 8 slots

They are measured; the plan exists. Every email's job is to get them into the app and through workout 1. W1 the plan wasn't built for you Β· W2 what your quiz said Β· W3–W4 have you trained yet Β· W5 Jesse's origin story Β· W6 Gareth's proof Β· W7 the arc's one soft trial ask Β· W8 the final story. No offer, ever (Aga, 10 Aug) β€” the discount belongs to 993 SALES SEQUENCE.

967 TMA | Welcome Sequence After LEAD MAGNET β€” 8 slots

πŸ”‘ Who they really are was measured, not assumed: sampling the arc's own contacts shows they downloaded skill guides β€” planche 6, handstand 3 of 30 sampled. So the argument is: the guide gave you the steps; the quiz tells you which step you're standing on. Because the arc serves many different guides, the copy may never name one skill β€” naming the planche to a handstand downloader is a lie the reader will notice.

992 TMA | Welcome Sequence AFTER APPSIGNUP AB TEST Aug4th β€” 8 slots

The copy was already right; only its door was wrong. "The app knows your name and whatever you tapped during setup. It has never seen you move… you'll have seen the example plan when you set it up. That was a demonstration of the shape, built for nobody in particular." Every button now goes to the free quiz; E8 keeps the arc's single soft trial ask.


4 Β· THE WORKOUT-1 MOMENT β€” the piece being built now

The highest-intent moment in the whole funnel is the morning after someone completes their first workout, and today it carries no email at all. About 48 people a day reach it.

Why it matters: someone who trained yesterday has felt the product. The ask stops being "buy something" and becomes "keep doing what you did yesterday" β€” an appeal to consistency rather than to belief.

How it works, end to end:

  1. They train (evening). The app tells Intercom within minutes β€” verified live, not nightly.
  2. 03:15 β€” a nightly job (com.tma.workout1) reads every 90-day-active contact's workout count out of Intercom, compares it against last night's snapshot, and finds everyone who went 0 β†’ 1.
  3. Payers are excluded four ways β€” Intercom's plan field, live Stripe subscriptions on both accounts, the ever-paid set (lifetime is a one-time charge a subscription check cannot see), and the segment gate before any send.
  4. Those people get two labels in ActiveCampaign: workout1-yesterday (the trigger) and workout1-done (permanent).
  5. 08:00 β€” an automation triggered by that label sends one email: session 1 is a nervous-system event, not a muscle one; soreness tracks unfamiliarity, not effectiveness; two free sessions left; and the 7-day trial with its honest mechanics.

πŸ”‘ The label is the bridge β€” ActiveCampaign never has to see the app. That is what makes this a six-click build instead of an integration project.

Three safety rules built into the job: - The first run can only seed, never tag. With no previous snapshot every existing member looks like a first-timer, and the entire back catalogue would receive "you did your first workout yesterday". That is missing data, not an overridable setting. - It refuses outright above 400 people in a day β€” that means a stale snapshot or a data backfill, not 400 first workouts. - The snapshot only advances after a successful pass, so a crash re-runs the same day rather than losing the cohort.


5 Β· WHAT HAPPENS WHEN SOMEONE TRAINS β€” and why only ONE arc ends

This is the subtle part, and the first version of the plan got it wrong.

Arc On workout1-done Why
750 ENDS Its whole job is getting them to workout 1 β€” done. And two slots turn false: W3 "how did your first workout feel?" and W4 "have you pressed start yet?" would ask someone who trained yesterday whether they have started. W7's trial ask would also repeat the morning-after email's ask days later.
992 and 967 KEEP RUNNING πŸ”΄ Doing a workout is not the same as being measured. Someone can complete the app's generic example workout without ever taking the quiz, so the "get measured free" argument is still true after they train β€” arguably truer, since they have now felt a workout built for nobody. Neither arc contains a "have you started yet?" nudge.

The rule this gives us, for any future behaviour trigger: before ending an arc on a behaviour, ask whether the arc's argument survives that behaviour β€” not merely whether the behaviour happened. An activation arc dies at activation. A measurement arc does not.


6 Β· THE RULES THAT BIND EVERY WELCOME EMAIL

  1. πŸ”΄ No offer. Ever. No discount, no percentage, no deadline, no sale. The offer lives only in 993 SALES SEQUENCE, which receives the reader afterwards β€” so the discount lands as an escalation on someone warm, not as an unexplained coupon on someone cold. (Aga, 10 Aug.)
  2. The trial is asked for exactly twice: the morning after workout 1, and once at the end of an arc for people who ignored every free invitation. Never twice in a row.
  3. Trial language only against a trial checkout. The 50off/ family charges immediately β€” trial copy against it is a shipped lie. Every URL comes from the v3 checkout map.
  4. Content links use the quiz root, never a frozen variant path.
  5. A subject or preview may never promise what the body cannot deliver.
  6. Every claim must be true of the artifact β€” if the copy says free, the page must be free.

7 Β· WHAT IS DONE, AND WHAT IS LEFT

Done (10–12 Aug), all live-verified: - 967 β€” 30 stale quiz links repointed; both ghost emails (old quiz-audience copy telling a non-quiz reader "remember your movement diagnosis") replaced; a hidden offer link removed; 13 of 15 emails moved off a grey canvas; 15 of 15 given a first-name fallback that no longer renders as an empty string; 30 untagged links given attribution; E1 rewritten. - 992 β€” every checkout link (2–3 per email) replaced with the free quiz, 45 quiz links now present, per-slot buttons, and an honesty fix where the copy had claimed all calibration sits behind the trial. - 988 β€” the retired twin, still sending to 162 people, repaired the same way (42 β†’ 0 paywall links) plus 8 sentences that described card mechanics and became false under a free button. - The workout-1 job β€” built, seeded with 15,410 contacts, registered, health-checked.

Left: - One builder visit (AGA-161) β€” create the automation, end 750 on workout1-done, and add the promo-opt-out exclusion to 993. Everything else is fleet work. - πŸ”΄ The gap after activation. They exit 750, receive one morning-after email, and then nothing β€” 993 SALES SEQUENCE exists but nothing routes an activated person into it. That handoff is the next real build. It blocks nothing today (one email at the highest-intent moment beats the current silence), but it is why the numbers may look thin before it exists. - Fleet backlog: preview lines that contradict their own emails (FLEET-59, FLEET-64), a non-canonical footer on five variants (FLEET-61), campaign-tag inconsistency (FLEET-65).


8 Β· HOW ANY OF THIS GETS CHANGED

Never by hand in the builder, and never by editing a live email directly. The route is:

Read the doctrine β†’ the fleet writes the copy β†’ email_lint.py β†’ email-qa β†’ a script with byte-exact targets that refuses to guess β†’ backup every message β†’ write plaintext from the final HTML in the same call β†’ read back β†’ tma-ac-upload-integrity (blocking) β†’ verify independently.

Two laws that came out of doing it wrong: a read-back must assert what the plan promised, not merely that the bytes came back, and two rules editing the same region must not be able to match each other's output. Both were learned from real defects, on live emails, this week.

Welcome rebuild

The welcome sequence, personalised β€” the build spec

The abandonment engine extrapolated to the welcome journey Β· stage-branched entry Β· the enrol-writer Β· your W-decisions

WELCOME_PERSONALISATION_ENGINE_EXPANSION_2026-08-07.md

πŸš€ THE PERSONALISATION ENGINE LEAVES THE ABANDONMENT LANE β€” welcome sequence first

β­πŸ”΄ CANONICAL design + build plan for extrapolating the abandonment personalisation engine to the whole email estate, starting with the welcome sequence. Written 7 Aug 2026 on Aga's directive: "take this personalisation engine and extrapolate it for the email sequence, starting with the welcome sequence … very strong fallback … works for people we have data for and for the old people that we don't."

The engine being extrapolated: tools/retention/quiz_abandon_manual_notes.py (code = canon) documented in ABANDONMENT_PERSONALISATION_ENGINE_2026-08-07.md. Data law: LEAD_SIGNAL_CONTRACT_2026-08-06.md Β§8 + AUDIENCE_DATA_DICTIONARY_2026-08-07.md. Missing-data law (abandonment estate): ABANDONMENT_DATA_FALLBACK_LADDER_2026-08-07.md β€” Β§5 of THIS doc is the welcome/LEAD-tier ladder that file says "has not been written yet." Every number below is tagged [LIVE date + instrument] / [MODELLED] / [UNVERIFIED]. Nothing in Β§7 executes on a live automation without Aga's approval (Β§0 decisions). Shareable: https://claude.ai/code/artifact/9bd17095-a7ac-4cca-be28-c5a5340d690a


0 Β· THE VERDICT β€” one paragraph, then Aga's decisions

The engine extrapolates cleanly because it was built for this. Its renderer (assemble()) is surface-agnostic, its adapters are swappable, and its own doc (Β§5) already defines the three extension moves: add a SLOT, add a SURFACE, add an AUDIENCE. The welcome sequence is a new AUDIENCE (quiz leads, not decliners) on a new SURFACE (an ActiveCampaign automation, not Gmail 1:1). What is genuinely missing is four things: (1) the AC field-delivery adapter (designed, unbuilt β€” FLEET-44), (2) a welcome copy pack with a LEAD-tier + zero-data fallback ladder (Β§5 β€” the ladder the abandonment estate names as unwritten), (3) an enrol-writer job that renders each person's sentences into AC custom fields before the automation reads them, and (4) five wiring points in /email-fleet so personalisation is enforced by gates, not stated in briefings (Β§6 β€” today the law is carried by 29 seats and enforced by zero pipeline checks). And the welcome sequence is the right first target on the merits: 750 TMA | Welcome Sequence After Quiz is Tier A (17,076 sends/28d), OVER-HARDSTOP on unsub (0.60% arc) [LIVE 7 Aug scoreboard], runs one generic track to all 7 paths, its first ask is a 50%-off at email 6 (contradicting its own SPEC-WELCOME doctrine), and two of Aga's own 4 Aug rulings against it are still unexecuted (Β§2).

The decisions only Aga can make (everything else in Β§7 is fleet work)

# Decision Recommendation
W-D1 Approve this design + the EVOLUTION LAW path: the personalised welcome deploys as a NEW automation with an atomic entry-trigger swap; 750 TMA | Welcome Sequence After Quiz drains in place, then is paused, never deleted. Her written GO on queue row AGA-134 is the law's override mechanism. GO. The failure evidence (arc OVER-HARDSTOP; E1 slot 3715 unsub 1.14–2.86%, zβ‰ˆ7.6; one generic track) is multi-dated (13 Jul audit, 15 Jul CMO analysis, 4 Aug ruling, 7 Aug scoreboard) β€” but the clean path is her explicit override row.
W-D2 The stage-branched start (Β§4) refines her 4 Aug "start 750 at day 3–4" ruling: non-checkout leads get E1 at ~T+2h; only NEW quiz-reached-checkout leads wait until the offer window closes (day 3–4) β€” because only they are in the $97 conversation the abandonment estate owns. Approve the refinement. It preserves the intent of the ruling (never talk over the offer window) without silencing the 90% of leads who never saw a price.
W-D3 The welcome ask: SPEC-WELCOME doctrine = free-trial only (/start-training/); live 750 TMA | Welcome Sequence After Quiz asks 50%-off at E6; AGA-114 ($97 vs $78.50) is open. Which rung does the new arc's E5 carry? Follow SPEC-WELCOME: soft free-trial ask at E5 for non-checkout cohorts; the $97 conversation stays the abandonment estate's. But this is an offer-ladder ruling β€” genuinely hers.
W-D4 The instrument: 50/50 concurrent split (old vs new arc β€” AGA-64's manual AC setup, still open) or atomic swap with an honest, confounded pre/post? The split, if she'll click AGA-64's setup: Tier A volume reads arc-level unsub + CTR in ~6–8 weeks at ~140–200 entrants/wk [MODELLED from WEEKLY_2026-08-03 + day-one tag census]. Otherwise swap + labelled pre/post.
W-D5 Pull campaign 3715 from E1 of 750 TMA | Welcome Sequence After Quiz NOW (queue row AGA-135) β€” her own 4 Aug ruling, verified unexecuted 7 Aug (no queue row, no ledger entry, slot still sending β€” 769 sends in the 28d window at 2.86% unsub). A 5-minute builder change, independent of the rebuild. Do it this week. It is a guardrail revert, not a rewrite; the fleet cannot do it (AC v3 API cannot edit automation structure).

1 Β· WHAT IS BEING EXTRAPOLATED β€” and the split that makes it portable

The engine (ABANDONMENT_PERSONALISATION_ENGINE_2026-08-07.md, code canon tools/retention/quiz_abandon_manual_notes.py) is three separable layers:

Layer What it is Reusable for welcome?
The renderer β€” assemble() + tier detection + pick_quote() + jly_block() + choose_checkout() + the manifest writer Picks ONE rendered sentence per slot from what we actually hold, degrades by rung, never fabricates, records every choice YES β€” the machinery. Tier detection (any(prof.get(k) for k in (...))), the quote scorer, the grammar maps (second/third person), URL verification, manifest audit β€” all audience-neutral
The copy pack β€” SUBJECT/GOAL/OBSTACLE/JLY dicts + BODY The abandonment sentences ("still sitting here", $97, 48h) NO β€” by design. Engine Β§5: "the renderer is reusable, but the copy is not." A welcome lead never declined a price; recovery copy is nonsense to them. Welcome gets its own pack (Β§4–5)
The adapter β€” build_body() + put_draft() (Gmail) Delivery NO β€” welcome needs the AC adapter (engine Β§4, designed not built, FLEET-44): pre-render slots into custom fields, one PUT per contact, four laws

The build therefore starts with a refactor, not a rewrite: lift the renderer machinery into tools/retention/personalisation_core.py; quiz_abandon_manual_notes.py imports it and keeps behaving identically (its --example/--verify/manifest contract unchanged β€” the abandonment lane must not notice); the welcome pack becomes the second consumer. The engine doc's "code is canon" pointer gains one line naming the core module.

The one constraint that shapes everything (engine Β§3, verified 0/539 live bodies): ActiveCampaign cannot branch inside an email body. An unset field renders as an EMPTY STRING, not a skipped sentence. So all welcome personalisation is pre-rendered into custom fields by our code; each email carries one bare merge tag per slot. AC can branch at the automation level (if/else on tags, goals β€” the "If customer - END" goal on 750 TMA | Welcome Sequence After Quiz is the live proof), which is where stage-routing lives (Β§4).


2 Β· THE WELCOME ESTATE AS IT ACTUALLY IS β€” measured, with what's broken

The lead welcome for quiz leads is 750 TMA | Welcome Sequence After Quiz (registry: ACTIVE, 19,535 entered as of 27 Jul; 19,798 by the 7 Aug live snapshot). Its sibling 967 TMA | Welcome Sequence After LEAD MAGNET (Tier B) serves lead-magnet leads. Buyer-side (975 Onboarding | After The Purchase, 992 TMA | Welcome Sequence AFTER APPSIGNUP AB TEST Aug4th) is out of scope here.

The facts that justify a REPLACEMENT rather than a slot change:

  • Arc health OVER-HARDSTOP [LIVE 7 Aug β€” data/email-analytics/scoreboard/LATEST.md]: 17,076 sends/28d Β· CTR 0.66% Β· unsub 0.60% vs the 0.50% Β§D hard-stop. Worst slot: campaign 3715 ("The systematic approach to unbreakable strength") β€” 769 sends, 2.86% unsub, flagged STRUCTURAL.
  • One generic track to all 7 paths β€” 13 Jul audit: "no evidence of PATH branching in the spine"; only %FIRSTNAME% merge tags in 3 of 10 slots. The 28-directory pathΓ—tier arc library built 25 Apr was never deployed.
  • The ask contradicts the spec β€” SPEC-WELCOME: free-trial CTA only, /start-training/; live: zero /start-training/ links, first ask = 50%-off at email 6 of 10 [EMAIL_OFFER_LAYER_FORENSICS_2026-08-05.md Β§2.1].
  • E1 open ~18–20% vs a 45–65% norm β€” 15 Jul CMO analysis: deliverability + trigger timing are lever #1; copy is lever #2.
  • Aga's two 4 Aug rulings β€” pull 3715 from E1; start the arc day 3–4 β€” are UNEXECUTED [verified 7 Aug: no queue row, no FLEET_LOG entry, slot still sending]. β†’ W-D5, W-D2.
  • Collision at T+0: entry fires on the list-209 subscribe, into the same hour as the two Django sends ("results are ready" + "verify") β€” 3–4 emails in the first hour, 5 for a checkout-opener (Nic's T+1h login email). E1-position campaign 3715 unsubs 3.6Γ— its sibling at identical engagement (zβ‰ˆ7.6) β€” "a placement problem, not a copy problem" [QUIZ_FUNNEL_EMAIL_MASTER_2026-08-04.html Failure 4].
  • [UNVERIFIED β€” builder-only]: true slot order (the API cannot express it; three blocks share ordernum 1) and the second entry trigger. Both must be read from the AC builder during Phase 5 β€” never asserted from the API.

Volume for design + power maths: entrants β‰ˆ 140–200/wk [WEEKLY_2026-08-03: 141/wk; day-one tag census 7 Aug: 8 by midday]. New-lead flow carries LEAD-tier data at minimum from 7 Aug (Β§3).


3 Β· THE DATA CONTRACT, PER STAGE β€” what exists for whom (the whole fallback question)

Verified live 7 Aug (python3 tools/retention/quiz_lead_signal_check.py --sample 2 β€” HEALTHY, tag total at baseline, real contacts carrying both tiers):

Stage (tags) Who they are Data that EXISTS Welcome treatment (Β§4)
Gave email (NEW quiz-email-captured + NEW quiz-questions-done β€” πŸ”΄ SAME SECOND, email screen is #32; "gave email but didn't answer" CANNOT exist) Finished the questions, never rendered results LEAD tier: %TMA_QUIZ_PERSONA% (61) + %TMA_QUIZ_OBSTACLE% (64). The 8 RESULTS fields are BLANK Path L β€” completion nudge E1 ("your results are built and unclaimed"), LEAD-tier slots
Results seen (NEW quiz-results-seen) Saw their results, never opened checkout All 10 fields incl. %TMA_QUIZ_FUTURE_STAKES% (their own words), age, gender, background, recency, weakest fundamental. Never saw a price Path R β€” full personalisation; price copy is nonsense to them
Reached checkout (NEW quiz-reached-checkout) Saw the $97 price, declined All 10 + Stripe metadata The Abandonment Fleet's person (Aga's 7 Aug carve-out) for the offer window; joins the welcome spine at ~day 3–4 (W-D2)
Card started (NEW quiz-card-started) ~0 volume (1 tagged ever) β€” No lane (per the abandonment strategy Β§3)
Purchased (πŸ”΄ NO tag exists) Buyer Stripe / 975 Onboarding | After The Purchase Exit goal β€” never enters, exits instantly if buys mid-arc
ZERO-DATA β€” the ~83k legacy list, any pre-7-Aug contact, any contact whose field write failed πŸ”΄ No quiz signals exist and none can be backfilled (LEAD_SIGNAL_CONTRACT Β§8b) Nothing (maybe %TMA_FUNNEL_STAGE% if app-signup, 90d-active) The floor rung of every slot (Β§5). They don't enter the welcome (it's new-lead-triggered) β€” but the floor is what makes this pattern safe to extend to nurture/win-back/broadcasts, and is the safety net if the enrol-writer ever misses someone

Three laws travel with every placement (contract Β§8a): stage-gating (RESULTS-tier tags only to the results-seen cohort), the empty-string render (never a merge tag as the subject of a sentence), TAG = state / FIELD = data (segment on the 15 NEW quiz-* tags only; the 5,623 legacy tags are never a segment).


4 Β· THE TARGET ARCHITECTURE

4a Β· One automation, stage-branched at the automation level

New automation (fleet-built shells, Aga arms the trigger β€” see Phase 5): TMA | Welcome After Quiz β€” PERSONALISED (2026-08). Entry: the same list-209 subscribe that feeds 750 TMA | Welcome Sequence After Quiz today.

ENTER (list 209 subscribe)
  β”œβ”€ exclusions: is-customer β†’ exit to 975 Onboarding | After The Purchase
  β”‚              in 992 TMA | Welcome Sequence AFTER APPSIGNUP AB TEST Aug4th β†’ do not enter
  β”œβ”€ WAIT ~2h        ← lets NIC-54 fields + the enrol-writer land; clears the T+0 Django pair
  β”œβ”€ IF tag NEW quiz-reached-checkout
  β”‚     β†’ WAIT until day 3–4 (the offer window + abandonment estate own this person)
  β”‚     β†’ re-check is-customer β†’ join spine with checkout-aware slot copy
  β”œβ”€ ELIF tag NEW quiz-results-seen  β†’ Path R: E1 mirrors their results
  └─ ELSE (gave-email only)          β†’ Path L: E1 = "see the results you built"
SPINE (SPEC-WELCOME doctrine, 5 emails / ~7 days β€” not 10/23):
  E1 identity + stage-aware hook Β· E2 origin story Β· E3 mechanism (obstacle-personalised)
  Β· E4 proof (engine-matched member quote) Β· E5 soft ask (W-D3)
EXIT GOALS: buys β†’ exit to 975 Onboarding | After The Purchase (already-proven mechanism)
            trial starts β†’ exit (SPEC-WELCOME rule) Β· end β†’ enter 751 Nurture Sequence 1

The branching is automation-level if/else on tags β€” the thing AC provably does. The personalisation inside each email is pre-rendered fields β€” the thing AC provably renders. Nothing in the design asks AC to do anything unverified.

4b Β· The slots β€” new fields, never shared with the abandonment arc

~7 welcome-owned custom fields, minted at Phase 0 (titles β†’ perstags): TMA_WELCOME_HOOK_LINE Β· TMA_WELCOME_GOAL_LINE Β· TMA_WELCOME_OBSTACLE_LINE Β· TMA_WELCOME_JLY_LINE Β· TMA_WELCOME_PROOF_INTRO Β· TMA_WELCOME_PROOF_QUOTE Β· TMA_WELCOME_NEXT_STEP (the stage CTA line).

πŸ”΄ Why not reuse the abandonment adapter's planned fields (72–74): the same person can be in both estates in the same week (reached-checkout β†’ abandonment arc β†’ welcome spine at day 3–4). Shared fields + the SLOTS-FREEZE-AT-ENROL law = one arc silently rewriting the other's sentences mid-flight. Separate field sets per audience is dumber and safer. The freeze law then applies per audience: the welcome writer refuses to touch a contact already in the welcome arc (readable via the contactAutomations endpoint β€” the same read that exposed the 992 cross-enrol defect); re-render is allowed only at arc boundaries (welcome β†’ nurture).

πŸ”΄ Field capacity is a PROOF, not an assumption (engine Β§4 / FLEET-44 step one): all 54 existing fields are type: text, ZERO textarea in the account, our sentences run 100–200 chars. Write ONE full sentence to ONE test contact, read it back, confirm no truncation. If it truncates β†’ mint as textarea. Only then mint the seven.

4c Β· The enrol-writer β€” tools/retention/welcome_personalise.py

The welcome twin of the abandonment lane, built on the proven funnel_stage_export.py pattern (PUT fieldValues at 11,500 contacts Β· ~4–5 req/s Β· retry 429/5xx Β· abort >200 errors Β· clear-stale Β· read-back verify):

  1. Every 15 min (launchd, registered in AUTOMATION_JOB_REGISTER.md + cockpit watchdog): pull contacts new on list 209 / the NEW quiz-* tags since last run.
  2. Read the Β§8 profile by stable field id β€” never by name.
  3. assemble_welcome(persona, prof, stage) β†’ one rendered sentence per slot, by the Β§5 ladder.
  4. ONE PUT per contact, every slot always written β€” a slot with no data gets its floor sentence, never a blank (adapter law 1).
  5. Manifest row per person (welcome_personalise_manifest.json): stage, tier, fields present, rung chosen per slot, quote id, decision β€” the same auditability contract as the abandonment manifest.
  6. Freeze check before any rewrite (4b). Read back a live sample per run (adapter law 3).

The timing race and its three nets: the automation's ~2h entry wait vs the 15-min job cadence means every entrant has fields before E1 renders (net 1). If the job ever dies, slots render EMPTY β€” so every slot is placed as a standalone paragraph, never mid-sentence (net 2: a missing paragraph degrades the email; a broken sentence discredits it), and the E1 send is the render-with-everything-empty test made permanent (adapter law 4). Net 3 is a hypothesis to test in Phase 0, not to assume: AC's pipe-default merge syntax (%FIRSTNAME|there% works on live 750 TMA | Welcome Sequence After Quiz slots today) may accept a full-sentence default on a custom field tag (%TMA_WELCOME_GOAL_LINE|<floor sentence>%). If a real test send proves it renders (punctuation and all), the floor lives in the email itself and an unwritten contact is indistinguishable from a floor-rung contact. Proven on the wire or not used.

4d Β· What the welcome copy pack may NOT do

Locked facts and laws travel unchanged: 4.8β˜… never 4.9 Β· "LEAD Sports Tech Accelerator β€” run by the Adidas family and ex-Red Bull executives" exactly Β· testimonials verbatim from marketing/TESTIMONIALS-DATABASE/ only (provenance-gated) Β· no invented urgency, no deadline claims the code doesn't enforce Β· enum values never render raw Β· stakes words are echoed, never quoted back (surveillance vs memory) Β· subjects never carry age/gender Β· %FIRSTNAME% never leads a subject (βˆ’4.3pp, n=10,843). All slot copy is written by Agent 07 inside /email-fleet β€” the engine stores it; it does not author it (the EMAIL RULE).


5 Β· THE WELCOME FALLBACK LADDER β€” the LEAD-tier/zero-data ladder, written

The abandonment ladder governs its own estate. This Β§5 is the ladder ABANDONMENT_DATA_FALLBACK_LADDER_2026-08-07.md Β§0a names as "a different, shallower ladder that has not been written yet." Same one law above it: a missing field DEGRADES the sentence gracefully. It never fabricates it. Every degradation is recorded in the manifest. Rung copy below describes the JOB of the sentence β€” the sentences themselves are Agent 07's, through the fleet.

Tier detection per person at render time, from which fields actually return values (never from a date): FULL = any RESULTS-tier field present Β· LEAD = persona and/or obstacle only Β· FLOOR = nothing.

Slot Rung 1 β€” FULL (results-seen) Rung 2 β€” LEAD (every lead) Rung 3 β€” FLOOR (zero data / write failed) NEVER
HOOK_LINE (E1 opening + subject source) Stage-aware + results echo: names their weakest fundamental / their goal from the results they saw Persona: "your {goal} plan" framing "your results are ready and unclaimed" (Path L) / neutral "built from your answers" (Path R) age/gender in a subject; quoting stakes verbatim; "you left something in your cart" (they didn't)
GOAL_LINE Persona line, results-flavoured Persona line (same as abandonment rung 1 shape) Neutral "built from your answers, at your specific level" render the enum (FAT_BURNER)
OBSTACLE_LINE Obstacle-specific ("you told the quiz that X…") Same β€” obstacle is LEAD tier Universal sentence (AC surface: the floor is load-bearing β€” "omit" is impossible) guess an obstacle
JLY_LINE gender + age + background, third-person grammar throughout persona-anchored "for people who…" "for people who want strength that lasts…" claim a demographic we don't hold; mixed second/third person
PROOF_INTRO + PROOF_QUOTE quote matched theme Γ— age band Γ— gender theme (persona+obstacle) only strongest default quote fabricate/edit a quote β€” the member's own real demographics appear at EVERY rung
NEXT_STEP (CTA line) Stage CTA: results-seen β†’ the app/trial step (W-D3); checkout-joiner β†’ checkout-aware, links per the v3 URL map / quiz-root rule Path L β†’ "see your results"; else persona-neutral trial step Generic quiz-root CTA a URL not verified HTTP 200 this run; promising a persona page from a generic link

Two rules the AC surface adds on top of the abandonment ladder: every floor is a real sentence (the empty-string law makes "omit" impossible), and every slot is a standalone paragraph (Β§4c net 2). A slot with no floor rung is not finished (engine Β§5).


6 Β· PERMANENCE β€” where the engine lives in the email fleet, enforced not stated

Today the personalisation law is stated in 29 seat briefings and enforced by zero pipeline artifacts. The wiring that fixes it (each item is a named gap found by the 7 Aug architecture sweep):

# Wiring point The gap today The change
1 Agent 02b step in /email-fleet 02b (Personalization Brief) exists but has NO runbook step, no _briefs/02b-*.json in OUTPUT CONVENTION or DONE WHEN β€” the only personalisation artifact reaching the writer is made by an agent never invoked Add 02b to Step 3 (parallel with 02+06); output _briefs/02b-personalisation-brief.json (audience stage Β· tier Β· slot plan Β· ladder refs); add to OUTPUT CONVENTION + DONE WHEN
2 email_lint.py merge-tag family 16 checks, ZERO on merge tags β€” no blank-render test, no stage-gate check, no fake-conditional detection Add checks 18–21: (18) every %TMA_*% tag exists in contract Β§8 or the rendered-slot registry; (19) blank-render: strip all tags β†’ no sentence may break, no styled block may sit empty; (20) RESULTS-tier tag requires a declared results-seen audience (header comment the fleet writes); (21) data-ac-condition/%IF% = instant FAIL
3 email-variable-auditor wired in The estate's most rigorous personalisation checker is invoked by nobody β€” auto-triggers on request only Blocking evaluator in Step 7 for any email carrying %TMA_*% beyond FIRSTNAME β€” at EVERY tier (08b runs FULL-tier only; FAST currently ships unaudited personalisation)
4 Agent 18 pre-upload check 6 The deployment chokepoint has no merge-tag check Check 6: for every slot tag in the upload β€” field exists (live read), enrol-writer job registered and stamping, defaults verified on a sample
5 Doctrine refresh SPEC-WELCOME.md predates everything (12 Jul): no lead-signal block, offer discipline contradicted by live; 00g-LIFECYCLE-DIRECTOR, 07-MASTER-WRITER, 18-DEPLOYMENT carry no personalisation law at all SPEC-WELCOME rewritten against this doc; _SPECIALIST_COMMON_CORE.md gains "THE PERSONALISATION ENGINE β€” how a specialist requests slots"; 07 + 18 get the block
6 Jobs + registry β€” welcome_personalise.py in launchd + AUTOMATION_JOB_REGISTER.md + cockpit βš™οΈ Daily Jobs; manifest named in the register; this doc + ladder rows added to DOCUMENT_MAP and the abandonment strategy Β§0a register (the engine now has two consumer estates)

Ownership: tma-email-architect owns the engine's estate map (which audiences consume it, which fields belong to whom); email-variable-auditor is the correctness gate; tma-segment-integrity-officer + segment_guard.py remain the blocking audience gate for the enrol-writer's bulk writes (Step 1.4c already covers bulk AC writes β€” no new gate needed, it must simply be run).

The expansion path after welcome (each = new copy pack + stated tier, zero new machinery): 967 TMA | Welcome Sequence After LEAD MAGNET (zero-quiz-data audience β€” proves the floor), the nurture estate (751 Nurture Sequence 1 and siblings, LEAD-tier ladder), 976 Cancelled Win-Back (LEAD-tier + exit-survey reason), broadcasts (floor + engagement tier).


7 Β· BUILD PLAN

Phase What Owner Est
0 β€” Proofs Truncation proof (one sentence β†’ one test contact β†’ read back) β€” shared with FLEET-44 step one Β· pipe-default full-sentence test on a real send to ourselves Β· then mint the 7 welcome fields, record ids in contract Β§8 addendum fleet ~1h
1 β€” Core Extract personalisation_core.py (renderer machinery out of quiz_abandon_manual_notes.py, its behaviour byte-identical β€” --example both tiers before/after) Β· assemble_welcome() + the Β§5 ladder in code Β· welcome slot copy written by Agent 07 through /email-fleet (CRO β†’ humanizer β†’ Aga per engine Β§5) fleet ~half day
2 β€” Adapter + writer The AC field adapter (engine Β§4's four laws β€” build once, both FLEET-44 and welcome consume it) Β· welcome_personalise.py + manifest + freeze check + launchd + register fleet ~half day
3 β€” The emails 5-email arc through /email-fleet (evolution_class: REPLACEMENT, W-D1's row id as aga_override) Β· full gate stack Β· rendered review page for Aga (build_review_page.py pattern) β€” verified in the environment she opens fleet ~1 day
4 β€” Permanence Β§6 wiring points 1–6 Β· fix the two estate defects found by this analysis (below) fleet ~half day
5 β€” Deploy segment_guard.py PASS pasted to Aga Β· Aga/Nic build the automation shells + branches in the AC builder from the fleet's click-by-click spec (entry triggers are UI-only β€” unreadable AND unwritable via API) Β· read true slot order + second entry trigger FROM THE BUILDER while in there Β· atomic trigger swap (or split per W-D4) Β· 750 TMA | Welcome Sequence After Quiz drains, then paused, never deleted Aga + fleet ~1h Aga
Measure KPI = clicks + conversions (never opens) · arc unsub vs the 0.50% hard-stop · slot-tagged UTMs so email_revenue_report.py (FLEET-27) attributes revenue · lead→trial-start stays honestly DARK until attribution lands — say so in every read fleet ongoing

Estate corrections executed with this analysis (defects found during research): AUDIENCE_DATA_DICTIONARY_2026-08-07.md Β§2b still recommended AC "Conditional Content" β€” the instruction corrected as impossible everywhere else (missed by the 30-file sweep); fixed. DOCUMENT_MAP.md cited the pre-purge funnel-stage pool (4,356/6,775/2,809) as stamped %TMA_FUNNEL_STAGE% coverage β€” actual stamped: 3,276 / 5,872 / 1,909 (11,057 labelled, AUDIENCE_INTEGRITY_MASTER_2026-08-07.md); fixed.

Open questions this analysis leaves honestly open: the Django T+0 pair ("results are ready" with the literal [Chosen Password] + the verify email) stays Nic's lane β€” the master doc's sequencing note holds (fix the results email before moving the welcome start); whether 967 TMA | Welcome Sequence After LEAD MAGNET or the nurture estate goes second; CIO migration timing (the quiz-funnel build direction is Customer.io β€” this engine's pure-renderer split is exactly what makes that a new adapter, not a rewrite).

Abandonment

The abandonment master

Every email an abandoner gets Β· the honest prize Β· your decisions Β· the build plan

ABANDONMENT_STRATEGY_2026-08-07.md

ABANDONMENT & WIN-BACK MASTER β€” every email, every dataset, and the plan

⭐ Strategy of record for the Abandonment Recovery Fleet (/abandon-fleet) β€” and, since the 7 Aug evening rewrite, THE master document for how TMA talks to the person who almost bought, on BOTH funnels, plus the win-back estate. Rewritten on Aga's directive the same day the three things that obsoleted the morning version landed: the SendGrid measurements (a read-only key now exists), Nic's NIC-90 answer (his abandon email's real trigger), and the full v3 + win-back sweep (3 exploration agents, every claim file-quoted). Fleet: tma-quiz-checkout-abandon-owner (chairs) Β· tma-abandonment-data-analyst Β· tma-abandonment-copywriter. Data law: LEAD_SIGNAL_CONTRACT_2026-08-06.md Β§8. πŸ“– OPERATIONS (how it runs, hour by hour, rules + enforcement points): ABANDONMENT_OPERATIONS_MANUAL_2026-08-07.md Β· πŸͺœ missing-data law: ABANDONMENT_DATA_FALLBACK_LADDER_2026-08-07.md β€” this file owns the WHY and the decisions; those two own the HOW. Every number is tagged [LIVE date] / [MODELLED] / [UNVERIFIED] with its re-run command. Nothing in Β§7 executes without Aga's approval of this document.


0a Β· πŸ“š THE DOCUMENT REGISTER β€” every sub-doc of this estate, from here (Aga's rule, 7 Aug)

Nothing about abandonment floats outside this register. Adding a doc, tool or seat to this estate = adding its row here in the same session.

Layer Artifact What it owns
THE JOURNEY (parent) POST_QUIZ_JOURNEY_STRATEGY_2026-08-07.md The five-stage post-quiz journey this estate overlays as Stage 4 β€” arrival, activation, the behaviour-triggered pitch, the evergreen sales event, the long pool
WHY (this file) ABANDONMENT_STRATEGY_2026-08-07.md The full email map, prize sizing, the D1–D5 decisions, target-state arc, build plan, estate contradictions
HOW β€” operations ABANDONMENT_OPERATIONS_MANUAL_2026-08-07.md The hour-by-hour abandoner timeline Β· the 12 rules with their enforcement points in code Β· roles Β· the 5 runbook commands Β· hardening
HOW β€” missing data ABANDONMENT_DATA_FALLBACK_LADDER_2026-08-07.md Tier A/B cohort boundary (7 Aug 02:47) Β· per-slot degradation rungs Β· checkout routing priority incl. Aga's default-checkout ruling Β· degrade-never-fabricate law
DATA β€” what the quiz captures LEAD_SIGNAL_CONTRACT_2026-08-06.md (Β§8 only for copy/segments) The 10 stable AC field ids + 15 tags Β· stage-gating Β· empty-string law Β· tag=state/field=data
DATA β€” audiences AUDIENCE_DATA_DICTIONARY_2026-08-07.md The measured audiences + denominators used in Β§3
TRIGGER truth QUIZ_FUNNEL_EMAIL_MASTER_2026-08-04.html Β§1 green banner + QUIZ_ABANDON_AC_BUILD_SPEC_2026-08.md Nic's Django send ("Your login is in this email"), the transactional architecture, the AC-arc build spec
THE LANE (code = copy canon) tools/retention/quiz_abandon_manual_notes.py The verified per-person assembly: template v8, slot lines, checkout router, manifest writer
THE BRIDGE tools/retention/quiz_abandon_sync.py (every 30 min, com.tma.quizabandon) Stripe→AC fields + tag; gated enrol (disarmed)
AUDIT TRAIL tools/retention/quiz_abandon_verify_manifest.json + quiz_abandon_manual_notes.csv Every run's per-person decisions Β· one-note-ever state
QA GATE docs/company/GOVERNANCE/QUIZ_ABANDON_QA_2026-08.md + tools/retention/quiz_abandon_qa.py Must exit 0 before the AC arc is ever armed
THE FLEET .claude/agents/tma-quiz-checkout-abandon-owner.md Β· tma-abandonment-copywriter.md Β· tma-abandonment-data-analyst.md Β· runbook .claude/commands/abandon-fleet.md Who chairs, who proves the audience, who writes; the loop order (data β†’ strategy β†’ copy β†’ gates β†’ ship β†’ measure)
JOBS + DAILY TRACKING docs/company/GOVERNANCE/AUTOMATION_JOB_REGISTER.md Β§2.6 Β· cockpit βš™οΈ Daily Jobs band Schedules, stamps, the ran-today band Aga reads
HOW β€” the engine, in plain words PERSONALISATION_EXPLAINED_2026-08-07.md The layman's version: the card, the slots, the seven safety rules β€” read before the technical contract
HOW β€” the engine ⭐ ABANDONMENT_PERSONALISATION_ENGINE_2026-08-07.md The slot inventory Β· assemble()'s contract Β· the pure-renderer/adapter split Β· the AC field-delivery design (free ids 57/72–76, one-call PUT, the four laws) Β· AC cannot branch Β· how to add a slot, a surface or an audience Β· the operator traps
THE ENGINE'S SECOND ESTATE πŸš€ (7 Aug) WELCOME_PERSONALISATION_ENGINE_EXPANSION_2026-08-07.md The engine extrapolated beyond abandonment, welcome sequence first (Aga's directive): the renderer-core extraction, the welcome copy pack + the LEAD-tier/zero-data fallback ladder (Β§5 β€” the ladder the FALLBACK LADDER's Β§0a scope note names as unwritten), the enrol-writer, separate slot fields per audience (freeze-law collision avoidance with THIS estate's FLEET-44 adapter), and the email-fleet permanence wiring
SHIPPED-STATE RECORD SESSION_HANDOFF_2026-08-07_ABANDONMENT_MACHINE_SHIPPED.md The 7 Aug as-sent record (39 notes). Historical; its living content is rehomed into the four docs above
INSTRUMENT β€” did it work ⭐ tools/retention/quiz_abandon_recovery_read.py Touched vs holdout recovery, the lift, and the honest verdict when there is no holdout
INSTRUMENT β€” is it safe to send ⭐ tools/retention/abandon_collision_gate.py The only check in the estate reading all three sending systems + Stripe + AC unsub
REPLY CHANNEL ⭐ tools/retention/reply_fulfil.py (quiz-abandon rule) + check_replies.py Reply β†’ intent classified β†’ draft carrying their own persona checkout; STOP β†’ suppressed; SLA 12h
PROOF SOURCE marketing/TESTIMONIALS-DATABASE/ (query_testimonials.py) The tagged, verbatim member quotes the copy may use

0 Β· THE VERDICT β€” one paragraph, then the five decisions

TMA already sends an abandoner five to six emails in the first eighteen hours β€” from three systems that cannot see each other β€” and the one thing none of them does is ask for the sale. The paid recovery arc (756 Part 1 - Abandoned Cart Reminder / 757 Part 2 - Abandoned Cart Reminder) is the only piece of the machine that has never fired. Meanwhile the piece that fires most reliably β€” Nic's Django email β€” gives the product away free one hour after the person declined $97. The quiz funnel now stamps everything we need to personalise recovery properly (goal, obstacle, their own words for what they fear); the v3 funnel has effectively no abandoners to recover; and the win-back estate has a live mis-tagging risk and two contradictory canonical prices. The fix is not more emails. It is one recovery conversation per abandoner, sale-first, free-fallback last β€” built from parts that almost all already exist.

The five decisions only Aga can make (everything else in Β§7 is fleet work):

# Decision Recommendation Where argued
D1 Nic's T+1h free email β€” stop it, keep it, or move it? MOVE to day 4. Don't stop it; don't leave it at T+1h. Β§5.1
D2 The $97 48-hour window β€” binding or soft? (= AGA-109) Binding, shape C (real deadline, soft landing) β€” and until Nic ships enforcement, E1/E2 launch now, E3/E4 wait Β§5.2
D3 The win-back price β€” SAVE30 $109.90/yr (21 Jul ruling) or $9.99-ongoing (OFFER_LADDER Β§1b)? Two canonical docs contradict. SAVE30 reason-gated (the newer, evidence-based ruling) β€” but this is genuinely yours Β§5.5
D4 ~~Send the 39 waiting hello@ notes? (= AGA-125)~~ βœ… DECIDED + DONE 7 Aug β€” Aga: "send them"; 39/39 sent, 0 failures, final live buyer-check at send. quiz_abandon_sent_log.csv recovery read ~11 Aug Β§5.3
D5 Approve the build order in Β§7 Phase 1 is all fleet-executable this week Β§7

1 Β· EVERY EMAIL AN ABANDONER RECEIVES TODAY β€” the clocked map

1a Β· Quiz funnel ($97 checkout) β€” the first 18 hours, measured

A person finishes the quiz, sees their results, opens the $97 checkout, and does not pay. This is what actually reaches their inbox [LIVE 4–7 Aug 2026 β€” SendGrid census, tools/retention/sendgrid_transactional_check.py]:

Clock Email System Measured State
T+0 "{Name}, your results are ready!" Djangoβ†’SendGrid 66 sends/4d Β· 51% open Β· prints the literal text [Chosen Password] LIVE πŸ”΄ defect
T+0 "One Last Step: Verify Your Account" Djangoβ†’SendGrid 144 sends/4d Β· opens unmeasurable (plain-text = no pixel) Β· gates nothing Β· clicking it enrols them in a SECOND welcome arc (992 TMA \| Welcome Sequence AFTER APPSIGNUP AB TEST Aug4th) LIVE πŸ”΄ defect
T+0 750 TMA \| Welcome Sequence After Quiz E1 ActiveCampaign unsub 1.14% β€” 3.6Γ— its own E2, zβ‰ˆ7.6; over the EVOLUTION LAW's 1.0% failure line LIVE πŸ”΄ placement failure
T+5min 750 E2 ActiveCampaign the only wait between E1 and E2 is 5 minutes LIVE
β‰ˆT+1h "Your login is in this email" β€” free first 3 workouts + login Djangoβ†’SendGrid 10.5 sends/day on the two complete log days Β· 29.3% open Β· one per person (32 distinct of 32) Β· raw totals move β€” see Β§3a's retention-floor note Β· clicks unmeasurable (AGA-130) LIVE β€” the de-facto abandon email
T+24h+ hello@ 1:1 note β€” dynamic subject by goal/obstacle ("your muscle plan is still sitting here" Β· "about the plateau you mentioned") Gmail (manual lane, drafts-first) 39 SENT 7 Aug on Aga's GO (v8 personalised copy, persona checkouts, ids in quiz_abandon_sent_log.csv) Β· replies watched by check_replies.py from 05:35 daily LIVE
β€” 756/757 β€” the actual paid recovery arc ActiveCampaign 0 entrants, ever. Both PAUSED by Aga 5 Aug (756's email lacks an unsubscribe link + renders raw markdown; 757's four emails are EMPTY, subjects r/r/e/e) NEVER FIRED

Read that table again: five to six touches in ~18 hours, zero of which ask for the $97 sale, from three sending systems (Django/SendGrid Β· ActiveCampaign Β· Gmail) that cannot see each other. An AC collision check is incomplete by construction β€” Nic's send appears in no AC automation, trigger or calendar. [Nic, NIC-90, 7 Aug]

Nic's email, precisely (his own answer, 7 Aug β€” NIC-90 resolution): fires ~1h after someone opens the quiz checkout page with an email on it and does not buy. No password, no card needed. NOT on quiz start; NOT for an email typed earlier that never reached checkout. Held by a safety switch until 7 Aug β€” now fixed and sending. Body: free first 3 workouts + login credentials (repo copy: docs/company/PRICING/TMA_QUIZ_CHECKOUT_BUILD_PACK_2026-07-29/04_EMAIL_FREE_WORKOUTS.html β€” which carries 4.9β˜…, a banned figure, and the wrong accelerator spelling; whether the live Django template matches the repo copy is [UNVERIFIED β€” SendGrid shows events, not bodies]).

1b Β· v3 checkouts (trial/ Β· 50off/ Β· normal/ Β· upgrade/ Β· 30trial/)

A v3 abandoner receives NOTHING, at any stage. β€” CHECKOUT_EMAIL_COVERAGE_AND_GAPS_2026-08-04.md Β§2.1, verbatim. There is no v3 abandonment sequence of any kind, and until this rewrite the strategy of record did not mention v3 at all.

What v3 does have: stamp-abandon-email + email-blur mint shipped 3 Aug (NIC-19 change 1, verified against the served JS), so a typed email now creates an identifiable Stripe record. And tools/retention/cart_abandon_reconcile.py — the hourly Stripe→AC bridge, hard-coded to v3 (tag 63598, field 56) — is written but has never run: no launchd job, no state file, absent from AUTOMATION_JOB_REGISTER.md. §3 has the audience numbers; they change the conclusion.

1c Β· Win-back (cancelled members) β€” for completeness of the map

Lane State Measured
976 Cancelled Win-Back (7 emails) Shell ACTIVE; steps 1–5 mailed 281β†’221 Β· the offer step (6, day 38) has NEVER fired β€” 0 sends Β· and it holds a /30trial/ link banned for cancelled payers (double-bill risk) [LIVE β€” EMAIL_REVENUE_STRATEGY Β§8]
R1 cancel-save (hello@) LIVE β€” Segment A sent 53 sent β†’ 1 save ($24.97 MRR) Β· 4 replies [LIVE 7 Aug r1_status.json]
Dunning (payment_recovery_lane.py) LIVE (the one auto-send lane) 37 invoices β†’ 5 recovered
Failed-purchase ARMED drafts-only 22 in window Β· $2,673 Β· 0 recovered
T-30 watcher LIVE 0 sent today
wb_tag.py πŸ”΄ LIVE and tagging wb-eligible daily (24 rows since 22 Jul) while the FLEET-10 pagination bug is still in check_saves.py/r1_daily.py β€” a paying member on an 11th Stripe record can be mis-tagged as expired risk accruing NOW

2 Β· THE DATA WE NOW HOLD β€” four sources, what each answers, and its traps

Source What it answers Instrument (re-run any time) πŸ”΄ Traps (each already caught once)
SendGrid (read-only key, live 7 Aug) Whether the Django transactional sends deliver and get opened β€” the measurement we had ZERO of before today python3 tools/retention/sendgrid_transactional_check.py Click tracking OFF account-wide β†’ every clicks_count is 0 by construction (decision AGA-130) Β· Email Activity retains only ~3–4 days β€” the earliest day in the log is a floor, NOT a start date Β· /stats is aggregate-only, no per-subject split Β· a plain-text email (the verify send) has no pixel β†’ 0 opens β‰  nobody opens
AC lead signals (NIC-54, live 7 Aug) WHO the abandoner is: 15 NEW quiz-* stage tags + 10 fields β€” the goal, the obstacle, their own typed words for what they fear (%TMA_QUIZ_FUTURE_STAKES%), age, gender, background, recency, weakest fundamental python3 tools/retention/quiz_lead_signal_check.py --sample 2 STAGE-GATED: persona+obstacle on every lead; the other 8 ONLY on results-seen Β· an unset AC field renders as an empty string, not a skipped sentence Β· tag total 5,845 is the leak tripwire Β· no history β€” signals start 7 Aug and compound forward
Stripe Who reached payment machinery, what they abandoned, and the ONLY truth for the KPI (recovered money) python3 tools/retention/quiz_abandon_sync.py (dry) Since defer-mint, Stripe records exist only after email-blur β†’ Stripe UNDERCOUNTS checkout viewers ~7Γ— (Β§3) Β· buyer-check must fail safe (error = treat as paid)
GA4 + Intercom census Scale and funnel shape; the wider audiences beyond the checkout tools/retention/funnel_stage_census_2026-08-07.json (Task 11 lane) GA4 top-N truncation law Β· Intercom =0 matches nothing when the attribute is absent Β· subscription_status set on only 181 β€” payer truth is plan + Stripe

Companion, written the same day by a parallel session: docs/company/ANALYTICS/AUDIENCE_DATA_DICTIONARY_2026-08-07.md β€” "everything we know about a contact when writing email", combining the quiz signals with the app funnel stage (the Intercom census in Β§3c). Read it alongside Β§8 of the contract; it answers "which journey is my reader on?" for a writer who has both datasets available.

What the quiz now tells us about a real abandoner β€” contact 378032, created 06:43 on 7 Aug [LIVE census]: goal SKILL_SEEKER Β· obstacle CONFUSION Β· age 18_24 Β· background GYM Β· weakest Abs Β· and in their own words: "Never unlocking the skills I know are possible." That sentence is the strongest copy asset this company owns, and until this morning it died at the end of the quiz. The rules for using it: echo, never quote; RESULTS-tier fields only to the results-seen cohort; every merge placement passes the empty-string test (Β§8 of the contract).


3 Β· THE TWO FUNNELS, MEASURED β€” and the honest prize

3a Β· Quiz funnel ($97) β€” works, leaks at a known rate

[LIVE 7 Aug β€” quiz_abandon_status.json 09:31]: 30-day window β€” 63 checkouts Β· 8 paid (12.7%) Β· 44 distinct abandoners Β· $4,268 attempted. Personas: FAT_BURNER 15 Β· MUSCLE_BUILDER 10 Β· SKILL_SEEKER 7 Β· STRENGTH_BUILDER 5 Β· JUST_START 3 Β· PAIN_FREE 2 Β· AGE_STRONG 1. Nearly all abandoned the $97 annual. 0 of 44 ever attached a card β€” these are price decliners, not cart abandoners; copy that says "you left something in your cart" is false and stays banned.

πŸ”΄ The three denominators, reconciled β€” the estate carried two conflicting prize sizings because each read a different feed, and both were "right":

Feed Sees Why it differs
Stripe incompletes (our sync) 44/30d β‰ˆ 1.5/day only people whose page minted a sub (post defer-mint: typed an email into Stripe's machinery)
Tag NEW quiz-reached-checkout (63773) β‰ˆ 5/day (from 7 Aug) paywall RENDER β€” but only counting from this morning
Nic's send log β‰ˆ 10.5/day checkout view with email on the page β€” the widest true audience

Re-derived 7 Aug PM, and it is the soundest number we have: on the two COMPLETE days in the log (5–6 Aug) the abandon email sent 21 times = 10.5/day, and it is one per person β€” 32 distinct recipients across 32 sends, zero repeats. So the recoverable pool is β‰ˆ 315/month, band 250–350 given only two clean days.

At the 3–5% recovery benchmark for a considered $97 purchase: β‰ˆ $730–$1,700/mo [MODELLED] β€” superseding both the earlier "$130–430" (Stripe denominator) and "$900–1,500" (job-register denominator) figures, and superseding this document's own first-pass "$440–1,450", which was built on a pool estimate lower than the measured send rate. Never quote the $4,268 attempted total as the prize.

πŸ”΄ A trap this document walked into, recorded so nobody repeats it. The morning pass cited "42 sends, 4–7 Aug" from the per-message log. Four hours later the same query returned 32 β€” the 3–4 day retention floor had advanced and 4 Aug fell from 11 sends to 5. Per-message counts are a moving snapshot and are not reproducible; the full-day rate is the stable statistic. Cite the rate, never the raw total, and never read the earliest day in the log as a start date.

Why Stripe sees so much less β€” the mechanism, worth knowing before anyone "fixes" the gap: the quiz hands the checkout a prefilled email, so Nic's system has an address at page load, while the Stripe record only mints on email blur β€” an interaction a prefilled field never fires. Hence 10.5/day of real abandoners against 1.5/day visible in Stripe. Nothing is broken; the two systems are watching different events, and the tag (63773) is the one that matches the audience.

The $97 window is UI-only today [LIVE probe 4 Aug, AGA-68]: TMA_QC_OFFER_EXPIRED never returned true β€” including for a 90-hour-old abandoner β€” and lapsed cache entries re-mint a fresh 48h. Aga ruled the window binding (4 Aug) but the enforcement is assigned to nobody (AGA-109 open, NIC-44 stranded at moved-to-aga). Until enforcement ships, no email may claim a deadline β€” which is why E3/E4 carry do-not-paste banners and only E1/E2 can launch.

3b Β· v3 checkouts β€” nothing to recover yet

[LIVE]: 5 weeks = 53 GA4 visitors β†’ 4 cards β†’ 1 payment; 90 days (8 May–6 Aug) = 66 starts, 8 matured trials, 0 conversions.

πŸ”΄ AND THE ONE "SALE" IS NOT A SALE. Re-derived 7 Aug PM from tools/finance/quiz_sales_by_version.json: the single paid record on this rail is nic+testtracking+1@themovementathlete.com, created 22 Jul, since cancelled. checkouts-v3 has produced ZERO real revenue in 90 days β€” the "$24.97" figure that circulates for this rail is a test transaction. Anything that quotes v3 revenue is quoting Nic testing his own checkout.

My dry-run of the reconcile bridge (7 Aug): 19 PIs in 48h, 0 abandoned carrying an email. The 117-subscriptions/2.2-per-visitor phantom-mint figure (NIC-92) was measured over a window that is 90% pre-defer-mint β€” likely debris; no post-3-Aug census exists yet. The 16 past_due subs with real addresses are a dunning population (retention rail), not abandonment.

Verdict: v3 abandonment recovery is worth β‰ˆ $0/month today β€” the funnel has β‰ˆ 0 identifiable abandoners AND zero real sales to recover toward. The binding constraint is traffic, not machinery. What we do anyway is cheap and makes the audience visible from day one β€” Β§5.4.

3c Β· The wider funnel, for scale (Intercom census, 90-day-active with email [LIVE 7 Aug])

A survey-unfinished 4,356 Β· B finished-no-workout 6,775 Β· C workouts-no-trial 2,809 Β· checkout-reached 432 Β· payers ~1,126. The quiz stage tags will grow into these numbers from 7 Aug forward. Audiences A–C belong to the funnel-stage export lane (Task 11) and nurture/ re-engagement β€” mapped here so nobody double-sends, owned there.


4 Β· THE AUDIENCES β€” in priority order, with what we may say to each

# Audience Size Data tier Owner The one objection to answer
1 Quiz checkout decliners β‰ˆ315/mo (band 250–350) Tier A (results-seen: all 10 fields) from 7 Aug; Tier B (persona+plan via Stripe) before this fleet "is $97 worth it / is now the time" β€” sale-first, mirror THEIR plan
2 Card-started non-payers 1 tagged so far; S6 historically ~0 Tier A this fleet (do not build a lane β€” volume β‰ˆ 0; the S5 arc covers them) payment friction, not doubt
3 Results-seen, never reached checkout growing from 7 Aug (4 on day one) Tier A SPEC-REENGAGEMENT hand-off β€” design next cycle they never saw a price; price copy is nonsense to them
4 Email-given, never saw results 8/day flow; historic pool in census A LEAD tier ONLY SPEC-REENGAGEMENT / nurture "finish what you started" β€” completion nudge, never a sales email
5 v3 abandoners β‰ˆ 0 today Stripe metadata when they exist this fleet, deferred (Β§5.4) n/a yet
6 Cancelled members (win-back) 976 holds 306–331; wb_tag 24 tagged reason tags from exit survey; πŸ”΄ LEAD-tier fields only (contract Β§8) retention fleet + this doc Β§5.5 reason-to-return, never a deeper price than the save

Buyer-safety law on every lane, unchanged: a buyer can never receive a recovery email β€” paid check at sync AND at send, fails safe (any Stripe error = treat as paid, skip).


5 Β· STRATEGY β€” one recovery conversation, sale-first, free-fallback last

The design principle everything below serves: an abandoner should experience ONE coherent conversation that remembers them β€” not five systems firing independently. The sale is asked for while the intent is warm; the free door opens only after the sale has been genuinely declined; every touch proves we remember who they are (their goal, their obstacle, their words).

5.1 Β· D1 β€” Nic's email MOVES to day 4. It should not die, and it must not stay at T+1h.

The case against T+1h (our own coverage audit, 4 Aug, now backed by measurement): arriving free one hour after someone declined $97 actively undercuts the live window and teaches the reader that declining a price produces a better one. 90.7% of annual sales in 24 months went at exactly 50% off — this list already waits for edges; training decline→free is the most expensive lesson we can teach. External data agrees: over-generous free access cannibalises paid (~2.6% freemium conversion vs 31.4% opt-out trial).

The case against stopping it: it is the ONLY reliably-firing recovery asset TMA has β€” 10.5 sends/day, 29.3% opens, zero maintenance β€” and its content (start free, no card, exact-level plan) is genuinely the right last door: an honest fallback for the person who was never going to pay $97 this week. Killing it recovers nothing and discards a working asset.

Therefore: move it to day 4 β€” the slot the master doc's Decision 1 already recommended and Aga never ruled on. It becomes E4's free door (alongside the quarterly $49.97 standard β€” see the E4 note below). One Nic change (timing), ~15 min. Interim, already enforced in code: our arc holds at 24h+ (quiz_abandon_sync.py --hours 24, docstring-locked), so nothing collides regardless of when he ships. πŸ”΄ If Aga instead rules "keep at T+1h", the arc below still works β€” E1 then leads with the sale AND acknowledges the free email ("you'll have my note with your login β€” that stands"), and we never repeat its content. If she rules "stop entirely", day 4 loses its free door and E4 becomes quarterly-only. The arc is robust to any D1 outcome; the recommendation changes only which door opens last.

While it lives, wherever it lives: the copy carries 4.9β˜… (banned β€” 4.7/4.8 only) and the wrong accelerator spelling in the repo version β€” one-line fixes for Nic in the same deploy [UNVERIFIED against the live template β€” verify then fix].

5.2 Β· The quiz recovery arc β€” the rebuild spec

Feed fix first (fleet, no Nic, ~1h): re-point quiz_abandon_sync.py's supply from Stripe incompletes to tag 63773 NEW quiz-reached-checkout (Stripe stays as the buyer-safety check and the KPI read). This alone takes the lane from seeing 1.5/day to ~5/day β€” and it is the same audience definition Nic's email uses, so the two systems finally agree on who an abandoner is.

The arc (756 = E1–E3 Β· 757 = E4; fresh AC shells β€” the current ones are defective and the E11 row describes a dead July build):

Beat Clock Job Personalisation (Tier A) Offer discipline
E1 ≀60min after window entry (interim: 24h until D1 ships) Mirror THEIR plan β€” "20 answers built you a plan; it's still yours." Never "you left something in your cart." goal + weakest fundamental ("your plan starts with {weakest}") + obstacle line No discount, no deadline claim (D2). Full $97.
E2 +21h The objection, by persona; 60-day guarantee as the risk-reverser; ONE provenance-passed testimonial obstacle-conditional content; stakes ECHO (never quote their words back verbatim) No discount.
E3 ~+40h Honest window close: $97 vs $157 math ($1.86/wk vs $3.02/wk) persona πŸ”΄ Ships ONLY after D2 enforcement is live. Until then the arc is E1β†’E2β†’E4.
E4 day 4 (757) The smaller doors: quarterly $49.97 standard, no coupon β€” and (per D1) Nic's free-app email as the final door persona Ladder descends only post-refusal; one offer per email β€” so E4 leads quarterly, and the free door follows as its own beat (day 5) if D1 ships, never stacked in one email.
exit β€” Designed hand-off, not the 758 dead-end: tag for 991 NEW Re-Engagement eligibility at β‰₯day 5 unconverted β€” SPEC-REENGAGEMENT owns β‰₯day-5.

The personalised rebuild (FLEET-42, queued): the current E1–E4 pass lint but use ONLY persona β€” zero of the ten new fields. The rebuild adds the obstacle line and stakes echo for Tier-A leads with AC conditional content, a LEAD-tier-only fallback variant for Tier B, and every placement passes the empty-string test. %TMA_QUIZ_OFFER%/%TMA_QUIZ_PLAN% (declared, never used) get used or dropped. E4 gains the email/path params its CTA currently lacks.

The hello@ 1:1 lane stays exactly as designed (HELLO rules: no formatting, no urgency, no discount, converts on replies) β€” it is the interim surface until 756/757 arm, then retires. D4: the 39 waiting drafts should go this week β€” the oldest are already past the window where E-arc copy would make sense to them. Two wiring gaps to close when sending: STOP/reply capture for the subject "your signup didn't come through" is not in reply_fulfil.py (HELLO rule 6), and the AGA-125 row title still quotes the dead v1 subject.

Burst hygiene this arc depends on (owned elsewhere, tracked here): 750 E1/E2 five-minute gap + E1's 1.14% unsub (email fleet / EVOLUTION LAW slot work) Β· verify-email double-welcome into 992 β€” πŸ”΄ this doc previously said "(NIC row exists)"; the 7 Aug estate sweep proved NO such row existed. Minted 7 Aug PM (see the queue); J1 in the journey master kills the email at its root Β· [Chosen Password] literal in the results email (Nic, one line) Β· on 756 entry the contact is DELETEd from 750/751 with no re-enrol β€” acceptable for the window, but the exit hand-off (above) must exist so completers don't fall into silence.

5.2b Β· πŸ”΄ THE ONE TIMING TABLE β€” every surface, one place, reconciled against the WINDOW

Six files carried four different E2/E3 timings, and the master contradicted itself between Β§5.2 and Β§5.3. This table is now the only timing authority in the estate; nothing else restates a number.

Clock Touch Surface Live?
T+0 results-ready Β· verify-account Β· 750 E1 Β· 750 E2 (+5min) Django + AC LIVE (defects mapped in Β§1a)
T+1h "Your login is in this email" — free workouts + login Django→SendGrid LIVE (D1 would move this to day 4)
T+24h E1 β€” mirror their plan, full $97, no deadline claim AC arc interim floor; ≀60min once D1 frees the hour
T+45h E2 β€” objection by persona + 60-day guarantee AC arc +21h
T+24h→7d the hello@ 1:1 note (one per person, ever) Gmail LIVE — 39 sent
T+48h πŸ”΄ the $97 window dies here the checkout claim only; enforcement unbuilt (D2)
T+~66h E3 β€” the window beat AC arc πŸ”΄ see the conflict below
day 4 E4 β€” quarterly $49.97 standard; + the free door if D1 ships AC arc after D2/D1
β‰₯day 5 exit β†’ tag for 991 NEW Re-Engagement eligibility AC undesigned before this doc

πŸ”΄ THE CONFLICT NOBODY HAD SPOTTED, and it invalidates E3 as written. Under the interim timing, E3 lands at ~66h β€” but the window dies at 48h. So E3's subject ("Tonight, $97 becomes $157") fires ~18 hours after the price already reverted. The original design (E1 ≀60min β†’ E3 at ~40h) was coherent; the 24h collision shift broke E3's premise and no document noticed. Two valid resolutions, and the fleet must pick one in the rebuild:

  • (a) If D1 ships (Nic's email moves to day 4): E1 returns to ≀60min, E2 ~+21h (22h), E3 ~40h β€” inside the window, and the arc is coherent as originally designed. Preferred.
  • (b) While the interim 24h floor stands: the honest window beat is E2 at 45h (~3h left), and E3 must be rewritten as a "the window closed" beat, not a "closes tonight" one. Shipping E3's current copy on the interim timing is a false claim regardless of D2.

Two different 24-hour floors exist and must never be conflated: MIN_AGE_H = 24 in quiz_abandon_manual_notes.py governs the 1:1 note; --hours 24 in quiz_abandon_sync.py governs the AC arc's supply. Same number, two scripts, two purposes β€” change one and the estate reads as if both moved. πŸ”΄ And the 1:1 lane has a 7-day ceiling (MAX_AGE_H = 168) with no equivalent on the arc, which means a >7-day abandoner today receives nothing at all β€” a silent exit no document stated until now.

5.3 Β· Collision architecture β€” one map, three systems

Until D1 ships, the timeline for a fresh decliner is: Django burst (T+0) β†’ Nic free email (T+1h) β†’ quiet gap (enforced by --hours 24) β†’ our E1 (24h) β†’ E2 (45h) β†’ [E3 66h once D2] β†’ hello@ note only if the arc hasn't touched them (the manual lane excludes synced contacts) β†’ E4/free door day 4–5 β†’ hand-off. Frequency stays inside the Core-tier ceiling (max 1/day after T+0 β€” the T+0 burst itself is the defect being fixed, not a budget). Rule for every future collision check: AC-only checks are incomplete β€” Nic's Django sends must be checked explicitly, from reference_transactional_email_architecture_and_abandon_trigger / the SendGrid instrument.

5.4 Β· v3 β€” capture now, sequence later

  1. Wire cart_abandon_reconcile.py hourly now (launchd + register row + status file, ~30 min fleet work). It is dry-run-verified correct and idle; wiring it costs nothing and means the day v3 gets traffic, the audience is already tagged (AC tag 63598 + field 56 β€” currently orphaned assets, claimed by this).
  2. No v3 sequence until the audience exists. Trigger to build: β‰₯20 identified v3 abandoners in a rolling 30 days (the reconcile status file makes this a number someone can watch). When built: separate AC shells (756/757 are the quiz lane's β€” never shared), reusing the quiz arc's structure with plan-family-aware copy.
  3. Fix the recovery path defects that bite returners today: NIC-59 (the 50off expired-state button 404s β€” and expired-state is NORMAL for returners because of the evergreen localStorage countdown) is on Nic's brief; upgrade/ links never go to logged-out audiences.
  4. 16 past_due v3 subs β†’ dunning lane (already flagged to retention).

5.5 Β· Win-back β€” fix the gate, then the offer, in that order

  1. πŸ”΄ FLEET-10 first, this week (30 min, fleet): the limit=10 Stripe pagination bug is still live in check_saves.py/r1_daily.py while wb_tag.py tags wb-eligible daily β€” a paying member on an 11th record can be mis-tagged expired. The day 976's offer ever fires, that's a paying member receiving a win-back discount. Fix the pagination (or pause wb_tag.py the same day).
  2. D3 β€” the price contradiction, Aga's call: 21 Jul canonical ruling says reason-gated, price branch ANNUAL SAVE30 $109.90, reply-based, no link, $9.99-forever RETIRED; OFFER_LADDER Β§1b says re-offer $9.99-ONGOING; FLEET-32 currently instructs the fleet to wire the latter. Recommendation: SAVE30 reason-gated (newer, built on the exit-survey evidence that 65–71% would pay nothing β€” blanket discounting is out; ~89% of discount-saved re-churn). Whichever wins, the loser gets banner-stamped the same day.
  3. Then FLEET-32: move 976's ask into a step that actually sends, with the D3 offer β€” the current state (offer at step 6/day 38 with 0 sends, holding a banned /30trial/ link) is "cleanest offer in the estate" on paper and unreachable in practice.
  4. The Β§1b gate stands: no checkout link to cancelled payers until Nic answers (a) does a reactivation link/coupon exist, (b) cancel-event semantics (85/112 recent cancellers still have access β€” a checkout link double-bills them). Reply-based delivery (no link in the email) sidesteps this and is already the 21 Jul design.
  5. R1 rail: untouched. It works (1 save, 5 dunning recoveries, 4 replies); its economics are the retention fleet's, mapped here only so nobody double-sends its audiences.

6 Β· MEASUREMENT β€” what the KPI is, and what is honestly readable

KPI: recovered purchases within 96h of a recovery touch, read from Stripe money. Never opens (MPP-inflated), and β€” until AGA-130 is decided β€” never clicks (tracking is OFF account-wide; every clicks_count in the estate is 0 by construction). The zero-risk click path: UTMs on the recovery CTAs (quiz-root law) read in GA4 β€” already how E1–E4's links are built.

Readable today DARK until
Sends/delivers/opens per Django email (SendGrid, 3–4d window) Β· abandoner counts + personas (sync status) Β· recovered money (Stripe) Β· tag-stage funnel from 7 Aug (census tool) per-email clicks (AGA-130) Β· any pre-7-Aug stage baseline (none exists) Β· Nic's email's downstream free-user activation (needs the app-signup join β€” Task 11 lane)

Weekly persona-split audit β†’ _lifecycle-audits/, findings feed SPEC-CART-ABANDONMENT. Experiments (one at a time; ~5–10/day volume means weeks per read, so sequence them): QCA-02 E1 latency (24h vs ≀60min β€” becomes readable the day D1 ships) β†’ QCA-03 persona-conditional vs universal β†’ QCA-01 E4 quarterly vs free-door order. At this volume a formal split on anything subtler is theatre β€” ship the doctrine, read Stripe monthly.


6b Β· ⭐ WHAT MAKES THIS WORLD-CLASS β€” and the three things we built to get there

Benchmarked against standard practice, the engine was already ahead (real per-person prose with graceful degradation and a per-run audit manifest β€” most DTC brands never get past token substitution). Three gaps separated us from top-tier. All three are now built.

(1) Honest incrementality β€” the holdout Β· quiz_abandon_recovery_read.py

Almost every "recovery revenue" number in the industry is fiction, because it counts people who would have bought anyway. RevenueCat's 2026 win-back figure is the same shape: ~20% of lapsed users reactivate with zero intervention, so any campaign claiming credit below that baseline is claiming credit for gravity. The master cited that law in its own footnote and never applied it to this lane.

The instrument now reports two rates and refuses to conflate them: TOUCHED vs HOLDOUT (a stable per-person hash, so nobody flips arms between runs). The lift is the difference. The lift is the only honest claim.

First read, 7 Aug PM [LIVE]: touched 39 Β· 0 recovered (sends were ~3h old β€” the 96h window has barely opened; the honest read is ~11 Aug). Untouched-and-not-holdout: 5 Β· 1 recovered (20%) β€” a tiny sample, but exactly the natural conversion a holdout exists to measure, and a preview of why the holdout matters.

πŸ”΄ The holdout is DISABLED by default (HOLDOUT_EVERY = 0) and is a decision for Aga, not an engineering default β€” deliberately not emailing someone who might buy is a commercial choice. D7 below. Until it is on, the tool prints: "NO HOLDOUT β€” recovery is NOT attributable. The touched rate is an upper bound."

(2) One collision map, enforced in code Β· abandon_collision_gate.py

Every collision check in the estate before this read ActiveCampaign only β€” incomplete by construction, because Nic's Django send appears in no AC automation, trigger or calendar. An all-clear from an AC-only check was never an all-clear.

One gate, one person, four systems: Gmail sent logs · Django→SendGrid live Email Activity (impossible before the read-only key existed on 7 Aug) · ActiveCampaign current automations and unsubscribe status · Stripe live buyer check, failing safe. Exit 0 clear · 1 HOLD · 2 undetermined-treat-as-HOLD.

It caught a real collision on its first run [LIVE 7 Aug]: a live abandoner held because "Nic's abandon email sent 9.8h ago (< 20h) β€” would double-touch", plus a warning that they are currently inside 750 TMA | Welcome Sequence After Quiz. That is the exact double-send the estate could not see before today.

πŸ”΄ It also closes a real suppression hole: the 1:1 lane checked only suppress.csv, so someone who unsubscribed in ActiveCampaign could still receive a personal note. The gate now treats an AC unsubscribe as a HOLD.

(3) Reply-as-a-channel Β· reply_fulfil.py (quiz-abandon rule)

Every one of the 39 notes ends "just hit reply", and replies are the hottest signal this lane produces β€” a human who declined a price and then chose to write back. There was no fulfilment path: a reply saying "yes, send me the link" met a generic draft with no link in it, no SLA, and no owner. It had already cost money β€” a reply promised a comped premium (AGA-127) with no budget rule behind it.

Now: intent is classified (link-wanted vs question), the draft carries that person's own persona checkout resolved live and HTTP-verified β€” never a generic link β€” STOP suppresses instantly, and the run surfaces SLA 12h: a reply from a price-decliner is answered the same working day. Still drafts-only; Aga sends.


6c Β· πŸ”΄ D6 + D7 β€” two more decisions this work surfaced

# Decision Recommendation
D6 The note template breaks its own lane's law: 8 bold markers (HELLO rule 1: "No bold… A person writing to one other person uses none of these") and 3 claims of a 48-hour deadline the code does not enforce (AGA-68's probe: TMA_QC_OFFER_EXPIRED never returns true; lapsed entries re-mint). The 39 already sent carry both β€” this ruling is prospective, governing the next batch and the AC arc βœ… RULED (Aga, 7 Aug): soften the deadline, strip the bold. Keep the $97-vs-$157 argument; remove the one false claim. No code needed from Nic
D7 Turn the holdout on? At HOLDOUT_EVERY = 5, one abandoner in five is deliberately not emailed, giving ~60/month of control and an honest lift read in 6–8 weeks. The cost is real: some held-back people would have converted from the email Yes β€” 1 in 5. Without it every recovery number we ever publish is an upper bound we cannot defend. At ~10/day the cost is small and the credibility is permanent. Your call: it is a commercial trade, not an engineering one

And one defect the timing work exposed, already fixed: the 39 sent notes linked to the checkout without &email=, so the page could not resolve that person's real deadline and minted a fresh 48h window instead β€” the exact defect the coverage audit fixed in E1–E3 in early August. Corrected in the engine as part of D6.

7 Β· BUILD PLAN β€” ordered, owned, sized

Phase 1 β€” fleet, this week, no approvals beyond this doc:

# Move Time Unblocks
1 FLEET-10 pagination fix (win-back mis-tag gate) 30m makes wb_tag safe; unblocks ever arming 976
2 ~~Re-feed quiz_abandon_sync.py from tag 63773~~ βœ… DONE 7 Aug eve β€” both feeds merged (Stripe wins where it holds someone; the tag fills the gap). First pass: 46 Stripe + 6 tag-only = 52 abandoners vs 44 before; the tag began stamping 7 Aug so its share grows. Buyer-safety verified source-agnostic; QA gate still 18/18 done lane now sees the true audience
3 E1–E4 personalised rebuild through /email-fleet + /abandon-fleet gates (FLEET-42) 3–4h the arc Aga loads into shells
4 Wire cart_abandon_reconcile.py hourly + register row 30m v3 audience visible from day one
5 Hygiene: STOP/reply wiring for the notes subject Β· AGA-125 title fix Β· stale _briefs/02 correction Β· E11 row cleanup 1h truthful surfaces

Phase 2 β€” Aga (~1h total): D1–D5 rulings Β· build 4 fresh AC shells (3 in a new-generation 756, 1 in 757) and paste the fleet's HTML (~20 min, click-by-click spec exists) Β· un-pause 756 only Β· one end-to-end test contact Β· create QUIZ_ABANDON_ENROL_ARMED (the QA gate is already 18/18 green, and re-runs before arming) Β· send the 39 notes (D4).

Phase 3 β€” Nic (~1–2h total, via his brief once Aga approves): move the free email to day 4 + fix its 4.9β˜…/spelling (D1) Β· $97 window enforcement at charge time (D2 / AGA-109 β€” the engineering half currently assigned to nobody) Β· [Chosen Password] one-liner Β· (already on his brief: NIC-59 dead return-path, NIC-92 post-ship mint census, RETURN_URL).

Sequencing truth: Phase 1 and the 39 notes recover money in week 1. E1/E2 can arm without either Nic item (the arc simply starts at 24h with no deadline claims). E3 waits on D2 enforcement; the day-4 free door waits on D1. Nothing waits on v3 or win-back β€” they run on their own tracks.


8 Β· STANDING LAWS + the contradictions this sweep found

Laws carried forward, unchanged: buyer-safety fails-safe Β· stage-gated personalisation + empty-string test (Β§8 of the contract) Β· HELLO rules on 1:1 (no formatting/urgency/discounts) Β· one offer per email, ladder descends only post-refusal Β· E4 quarterly $49.97 standard, no coupons Β· never "you left something in your cart" Β· never claim a deadline the code doesn't enforce Β· 4.7/4.8β˜… only Β· AC-only collision checks are incomplete (Django sends exist) Β· TAG=state FIELD=data.

The estate contradictions found by this sweep β€” each with its disposition:

# Contradiction Disposition
1 976 state: ACTIVE/331 (registry) vs PAUSED (R1 doc) vs "unarmed" (FLEET-10) Resolved here: shell active, steps 1–5 sent historically, offer step never fired, entry gated on wb-eligible. Registry note to be refreshed on next live pull.
2 Win-back price: SAVE30 $109.90 vs $9.99-ongoing D3 β€” Aga. Loser gets banner-stamped.
3 Recovery prize: $130–430 vs $900–1,500/mo Resolved Β§3a. πŸ”΄ $730–1,700/mo [MODELLED] β€” this row itself said $440–1,450 until 7 Aug PM, when the send rate was re-derived on complete log days only. Β§3a owns the figure; nothing else restates it.
4 E11 row described the dead July build (field 56, 3 emails) βœ… DONE β€” _corrected_2026_08_07 in tools/email-plan/email_status.json, old text preserved. πŸ”΄ But its OWNER was still ambiguous β€” E11 says Nic in 4 places, Β§7 Phase 2 says Aga. Settled in Β§7: Aga builds the shells (builder-only work, no API path exists).
5 "v3 still has no email capture" (7 Aug handoff) vs NIC-19 ch.1 shipped 3 Aug Resolved: capture EXISTS at email-blur; what's missing is traffic typing emails.
6 NIC-92's 2.2 mints/visitor measured 90% pre-ship On Nic's brief already: post-3-Aug-only census before any further build.
7 _briefs/02-subscriber-brief.json claimed no obstacle/age/gender fields exist βœ… DONE 7 Aug PM β€” corrected with the Tier A/B surface + the empty-string law; superseded text kept for provenance. β‡’ FLEET-41 is closable.
8 AGA-125 title quoted the dead v1 subject βœ… DONE β€” title now carries the live subject + why v1 was rejected.
9 SPEC-REENGAGEMENT pre-dates the 6 Aug rung-3 ruling and thinks 979 may be live (it was retired for 991) Owner: email fleet, next self-learn cycle.
10 No dead-end ledger artefact exists; post-arc destination was undesigned Β§5.2 exit hand-off designs it for THIS lane; the estate-wide ledger stays the architect seat's obligation.

Provenance: three read-only exploration agents (v3 estate Β· quiz lane Β· win-back/constraints), 7 Aug 2026, every claim file-quoted; SendGrid census via the read-only key created the same day; AC census via quiz_lead_signal_check.py; Stripe via quiz_abandon_sync.py dry / the reconcile dry-run; external benchmarks: Klaviyo/Attribuly/Sendtric 2026 cart-recovery series + RevenueCat 2026 (win-back natural-reactivation baseline ~20%, a campaign must beat it to claim credit).

Win-back

Cancellation & win-back β€” the source of truth

What is actually true Β· the industry standard Β· the target journey Β· one price, two mechanisms

CANCELLATION_AND_WINBACK_STRATEGY_2026-08-11.md

CANCELLATION & WIN-BACK β€” the strategy of record

Built 11 Aug 2026 Β· every number measured live the same day against Stripe, ActiveCampaign and the operational cancel queue β€” never from a doc. Owner seats: tma-retention-lead (the lane) Β· tma-email-offer-strategist (the offer) Β· tma-email-architect (the arcs).

πŸ”΄ Read this before touching 976 Cancelled Win-Back, the hello@ R1 lane, the in-app cancel downsell, or any "come back" offer. It supersedes the working assumption β€” carried by OFFER_LADDER.md Β§1b and by FLEET-32 β€” that the cancelled-payer lane's job is to re-offer the $9.99-ongoing rate. That is the wrong offer for 73% of the people in it, and it is not the floor it claims to be.


Β§0 β€” The decisions this document exists to force

# Decision Owner Status
WB1 Split the lane in two β€” re-earn (access still running) vs win-back (access ended) β€” and stop sending one arc to both Aga proposed here
WB2 Make the offer plan-aware: annual cancellers get annual at 50%, not $9.99/mo Aga βœ… APPROVED 11 Aug β€” "yes we can try that". The standard is Β§10
WB6 Reinstate PAUSE, plan-branched: pause billing (monthly) Β· freeze the term (annual) Aga β†’ Nic proposed β€” Β§5a; the 40–60% resume rate is the strongest number in the whole lane
WB7 Ask WHY before offering anything on the cancel screen Aga β†’ Nic proposed β€” Β§9 stage 0; the standard's rule 1
WB3 Move 976 Cancelled Win-Back's entry to ACCESS-END β€” βœ… DISSOLVED β€” it ALREADY fires at access-end (Β§6a, measured). No builder change needed.
WB4 Turn on subscription_update in the portal β€” πŸ”΄ RETRACTED β€” a better-reasoned decision already exists. The 10 Jul R1 rebuild ruled "leave OFF until Nic confirms webhook consumption β€” would desync entitlements". The portal writes to Stripe only; the app learns via webhooks nobody has verified. See Β§5a.
WB5 Make the in-app cancel downsell plan-aware + reachable outside the cancel click Nic NIC-101 (held β€” needs Aga's sign-off)
WB3 Which win-back price is canon? β€” βœ… RESOLVED β€” Β§10: $78.50, one price, two mechanisms. Supersedes the 21 Jul SAVE30/$109.90 recommendation

Β§0a β€” WHAT IS ACTUALLY TRUE (the source-of-truth block)

Everything below was measured live on 11 Aug 2026 against Stripe, ActiveCampaign and the operational cancel queue. Where a belief the estate held is overturned, the old belief is named.

# What is true Evidence What it replaces
1 88 of 115 cancellers still HAVE access β€” median 106 days, max 322; 84 are annual cancel_queue_export.csv "cancelled = gone"
2 976 Cancelled Win-Back already fires at ACCESS-END entry βˆ’ access-end = 0 days for 14/14; gap to cancel-date 3–347 days Aga's "it fires at cancel"; my "the trigger is unknowable"
3 $9.99 does not save β€” it defers. 516 takers, 493 cancelled, median life 2.0 months, $34/taker all-status Stripe pull on nine-ninetynine my own "21 people are on it" (survivors only β€” understated 25Γ—)
4 Annual at 50% = $78.50 banked on day one the checkout's own title: "$78.50 billed today" "we don't have a checkout for this"
5 πŸ”΄ A save at cancel is a COUPON, never a checkout a live subscription + a checkout = a second subscription my own NIC-101 draft, and /50promo/ in July
6 Exit reasons: PRICE 25% Β· TIME 23% Β· APP/TECH 19% Β· NOT-USING 12% β€” and price answers are often value answers 210 exit surveys, regex, directional "they leave over price"
7 hello@ ∩ 976 = ZERO β€” today's cancellers never enter the win-back arc 105 of 115 resolved in AC "the win-back arc covers cancellers"
8 The billing portal offers only the exit subscription_cancel ON Β· subscription_update + subscription_pause OFF β€”
9 No forever-discount exists β€” all 100 Stripe coupons are duration: once /coupons "$9.99-ongoing can be couponed"
10 0 double-bills across 648 active subscriptions; the /30trial/ link has 0 human clicks full sweep, both accounts my own "live safety defect" β€” it is an offer defect

πŸ”‘ The one-line version: three in four people we call "cancelled" are still paying customers with three months left, and the only thing we say to them is a price cut for a product they told us they weren't using.


Β§1 β€” The measurement that reframes everything

tools/retention/cancel_queue_export.csv, read 11 Aug 2026 β€” 115 people:

Plan mix 84 annual Β· 23 quarterly Β· 8 monthly
Access STILL RUNNING 88
Access already ended 27
Days of access left (still-running) min 0 Β· median 106 Β· max 322
R1 segments A 32 Β· B 58 Β· C 25

πŸ”΄ Three in four have not left. They switched auto-renew off and are still training β€” on average for another ~3.5 months. "Win-back" is the wrong job for them. A checkout link is also the wrong mechanic for them β€” not because it reliably double-bills (Β§2c measured that: 355 of 370 repeat purchases resolve cleanly), but because modifying the subscription they already hold is cleaner, keeps auto-renew intact, and avoids the 4.1% of cases where something does slip.

Context: retention is the company's binding constraint (76 cancel-ends MTD, ~$1,090 MRR lost; churn out-running sales 52:38 in 7 days). This is the lane where that is decided.


Β§2 β€” πŸ”΄ THE OFFER IS WRONG, AND THE ARITHMETIC IS THE PROOF

OFFER_LADDER.md Β§1b states: "$9.99-ongoing is the floor; you cannot out-discount the save." For an annual member β€” 73% of this queue β€” that is arithmetically false.

Offer What the member pays per YEAR What we bank Commitment
$9.99/mo ongoing (the "deepest" save) $119.88 ~$27 [MODELLED] none β€” monthly, churns
Annual at 50% $78.50 $78.50 banked 12 months locked

Annual-at-50% is $41.38 a year CHEAPER for the member AND roughly 2.9Γ— the cash for us. The modelled $27 is $9.99 Γ— the ~2.7-month mean life implied by the monthly cohort's 36.7%/mo churn (annual churns 9.1%/mo, and is locked regardless). Tagged [MODELLED] β€” the multiple is directional, the direction is not in doubt.

β‡’ We have been treating our worst offer as our best one, because "deepest monthly discount" was mistaken for "deepest discount". The member's own decision is annual-shaped; the save should be too.

The plan-aware rule this sets:

Who cancels Offer Why Door
Annual (84/115) Annual at 50% cheaper for them per year, 12-month lock for us βœ… checkouts-v3/50off/12month/ β€” live, verified 200
Monthly / quarterly (31/115) $9.99/mo ongoing genuinely lowest friction; no lump sum ⚠️ no self-serve door (Β§4) β€” reply-handled

πŸ”΄ Open, and it collides with this: AGA-114 β€” we currently quote two different prices for annual ($97 on the quiz paywall, $78.50 via the email offer). Settle that before this ships, or a win-back reader can be shown a third number.


Β§2a β€” πŸ”΄ AGA IS RIGHT: RETIRE $9.99/mo AS THE DEFAULT CANCEL-SAVE

Aga, 11 Aug: "in the past we said that $9.99 doesn't make sense to us monthly and it really undermines our app… it's also really hard to build revenue and MRR on this." Agreed, and the evidence sharpens it three ways.

1. It is not even our cheapest price. Annual at 50% is $78.50/yr = $6.54/mo. The "deepest discount" is 53% MORE per month than what we sell to brand-new customers every day (90.7% of new annual sales go at 50% off). It is not a deep discount β€” it is a small single payment. We have been calling one the other.

2. It cannot build MRR β€” now MEASURED, not modelled. πŸ”΄ Correction to my own first pass: I said "21 people are on it" β€” that was survivors only, and it understated the footprint 25Γ—. Every subscription ever created on nine-ninetynine, all statuses: 516, of which 493 have cancelled, 20 remain active, take rate β‰ˆ 6.5 people/month over 79 months.

Measured life of the 493 who took it and left
Median 2.0 months
Mean 3.4 months
Gone within 1 month 32%
Gone within 3 months 63%
Lasted β‰₯12 months 6%
Revenue per taker $20 median Β· $34 mean

The save does not save β€” it defers, by two months, at 40% of the price. Against annual-at-50%, which banks $78.50 on day one, the $9.99 rate returns $34. (My earlier [MODELLED] ~$27 was close; this supersedes it with measurement.)

πŸ”‘ The break-even that makes this testable: annual-50% only needs 43% of $9.99's take rate to win ($34 Γ· $78.50). It can convert less than half as often and still be the better offer.

3. It is aimed at a reason that mostly is not price. 210 exit-survey responses (tools/retention/reason_log.csv), keyword-classified β€” crude, multi-label, 30% unclassified, so read as DIRECTIONAL not precise:

Reason share
(unclassified) 30%
PRICE 25%
TIME 23%
APP / TECH 19%
NOT USING 12%
INJURY / HEALTH 3%

And the price answers are frequently value answers in disguise β€” verbatim: "Too costly, but not using it enough to justify" Β· "Price and dont use". A discount cannot fix "I wasn't using it" or "the app kept crashing." It only makes the same loss cheaper.

β‡’ The replacement: branch on the REASON, not on the plan alone

Their reason The right save Why Status
Price (~25%) Annual at 50% β€” $78.50/yr ($6.54/mo) genuinely cheaper for them per month AND per year, and it discounts into commitment rather than out of it πŸ”΄ a COUPON on their existing sub β€” see Β§2b/Β§2c
Time / not using (~35%) PAUSE β†’ reason-capture + a renew nudge; pause only on reply, by hand πŸ”΄ My pause proposal is WITHDRAWN β€” see Β§5a. Pause was already rejected with data on 10 Jul, and it barely applies to an annual base: an annual member has already paid, so there is nothing to pause manual, on reply
App / tech (~19%) A fix, not an offer discounting someone whose app does not work is insulting; note the live Android sign-in defect route to support

Keep $9.99/mo as a rare human-granted exception on the hello@ rail β€” never an automated popup. That preserves the tool for the one-off case Jesse judges worth it, without teaching the list that cancelling is how you get a better price. Do not disturb the 21 members already on it.

πŸ”‘ The reframe: a cancel-save is not a discount mechanism, it is a diagnosis mechanism. We were answering every reason with the same price, and it was the wrong price.

Β§2b β€” πŸ”΄ THE SAVE AT CANCEL IS A COUPON, NEVER A CHECKOUT (correcting myself)

I nearly shipped a double-billing instruction to Nic. I recommended pointing the cancel screen at checkouts-v3/50off/12month/. At the cancel moment the member still holds an ACTIVE subscription β€” 88 of 115 have access, median 106 days β€” so a fresh checkout opens a SECOND subscription and bills them twice.

This is not a new discovery. The 10 Jul R1 rebuild had already caught the identical defect in the old sequence: "/50promo/ = fresh checkout = double-billing risk for still-active subs." I proposed the same mistake in a new place because I did not read the prior work first.

Moment Mechanism
At cancel (subscription still ACTIVE) A coupon applied to the EXISTING subscription β€” a 50%-off duration: once coupon (e.g. tma_v3_50off_once, which exists) so their next renewal bills $78.50 and auto-renew stays on. No checkout. No new subscription.
At access-end (no live subscription) checkouts-v3/50off/12month/ is correct and safe β€” this is the 976 Cancelled Win-Back arc

πŸ”‘ The rule β€” CORRECTED 11 Aug, see Β§2c: PREFER modifying a live subscription over selling a new one, but the checkout path is proven, not forbidden.


Β§2c β€” πŸ”΄ "A LIVE SUB + A CHECKOUT = DOUBLE-BILL" IS FALSE AS AN ABSOLUTE (Aga's catch, then measured)

I wrote that rule twice in this document and once in the memory index. Aga rejected it: "it doesn't double bill β€” our system recognises their email and they can upgrade and rebuy." She is right, and rather than argue it I measured every customer since 2024.

Customers since 2024 3,478
With more than one subscription 370
πŸ”΄ Who actually PAID for two overlapping periods 15 β€” 4.1% of multi-sub, 0.43% of all

355 of 370 repeat purchases resolved cleanly. The upgrade/rebuy path works as she said.

πŸ”‘ My error was generalising a documented RISK into a claimed OUTCOME without measuring it β€” the same shape as reading a queue row as a state. πŸ”‘ And the test matters: overlapping subscription date ranges returns 193 hits and is meaningless, because that is exactly what a normal upgrade looks like. The honest test is overlapping paid invoice periods (lines[].period > 3 days apart, across two different subscription ids).

The corrected rule

Prefer modifying a live subscription over selling a new one β€” but the checkout path is proven, not forbidden. At a 4.1% slip rate, any flow that sends a live subscriber to a checkout must end the old subscription.

The cancel-screen save is still a coupon (Β§10) β€” because it is the cleaner mechanic and keeps auto-renew intact, not because a checkout would double-bill.

πŸ’Έ But 15 people did pay twice β€” $1,662.86 on the redundant subscription

Overlap Paid twice Account
314 d $157.00 andreszeledon@gmail.com
290 d $141.30 mattlahana@gmail.com
176 d $78.50 bmwparts@outlook.com
90 d $235.50 moniquetavian@gmail.com
75 d $78.50 amiar@outlook.com
44 d $235.50 stoica.ciprian@gmail.com
43 d $199.88 tomifaludi@gmail.com
39 d $24.98 garethakelly@gmail.com
31 d $89.91 sophia.marie.hartman@gmail.com
31 d $49.94 timdclarke@gmail.com
27 d $78.50 dkwaskey@gmail.com
20 d $78.50 tarabeeny@gmail.com
12 d $59.94 am3_92@hotmail.com
10 d $69.96 wunder.nora@gmail.com
4 d $84.95 spencer.petey@gmail.com

Rare, but each is a real person charged for something they already had β€” and nothing in the estate was watching for it.

πŸ”΄ STANDING RULE β€” REFUNDS ARE NEVER THE FLEET'S (Aga, 11 Aug 2026): "you are not allowed to do refund ever… THIS IS NOT ALLOWED, DO NOT EVER DO THAT." I had closed this section by offering to execute the refunds via the Stripe API once she picked which ones. The offer itself was the violation β€” a refund is money leaving the business and an irreversible act on a real person's card, so both the decision AND the execution are a human's. The fleet's whole job here ends at surfacing the finding with the accounts and the money named. No fleet, seat or session proposes, shortlists or performs a refund. This applies estate-wide, not just to this section.

What the fleet MAY do next, and it needs nobody: diagnose how these happened β€” whether a specific path leaves the old subscription running β€” from Stripe alone. That is a defect investigation, not a money movement.


Β§3 β€” Two jobs, two rails, two arcs

RE-EARN β€” access still running (88) WIN BACK β€” access ended (27 + flow)
Who they are still paying, still training, median 106 days left genuinely lapsed
Rail hello@ R1 β€” already live and already correct 976 Cancelled Win-Back β€” already correctly entered at access-end (Β§6a)
The ask a REPLY, or resume renewal the offer, per Β§2
Checkout πŸ”΄ not the mechanic here β€” modify the live sub instead (Β§2c) βœ… correct: no live subscription to collide with
Job to be done make the product work again inside the window they already paid for give a reason to return, then price

What is missing today, and it is the biggest gap in the lane: we ask the still-inside 88 one question at cancel, then go silent for ~106 days while they are still paying us. The only thing that reverses a cancel decision is the product working again.

Proposed re-earn arc (no offer at any point): 1. T+0 β€” the existing R1 note. One question, no pitch. Keep exactly as is. 2. ~day 30 of remaining access β€” behaviour-gated: "you've got N days left β€” here's the one thing to do with them." Aimed at a session, not a sale. 3. ~day 75 β€” the honest checkpoint; surface what they'd lose that isn't the subscription (calibration, skill-tree position). 4. At access-end β€” hand off to the win-back arc, which is where price finally appears.

Proposed win-back arc (976, entering at access-end): reason-to-return β†’ proof β†’ the plan-aware offer as the last beat, once, with a real deadline. Never re-pitch an identical-or-worse offer than the one they already declined at cancel β€” which is exactly what it does today.


Β§4 β€” What is actually blocked, and on whom (I got this wrong first)

Correction, same session. I first told Aga the $9.99 checkout was a Nic build. It is not needed for the recommended path β€” the annual-50% door already exists and is the better offer (Β§2). Recording the retraction because the wrong version was said out loud.

Thing Blocked on Evidence
Annual-50% win-back checkout NOBODY β€” it is live checkouts-v3/50off/12month/ returns 200; provision_buyer_from_intent is already built and called by both success pages, so a buyer is provisioned end-to-end with no new code
A self-serve $9.99-ongoing door genuinely unbuilt Every coupon in the Stripe account is duration: once β€” 100 read, zero forever. The rate is the nine-ninetynine PRICE, applied in-app/by support: all 21 active subs on it carry EMPTY metadata vs the stamped v3 rail. Our API key is 403 on /payment_links, and a raw link would not provision app access anyway
Cancel downsell being plan-aware + reachable Nic (NIC-101) app-side UI
Portal self-service a Stripe toggle, not code Β§5
976 Cancelled Win-Back firing at access-end NOBODY β€” it already does Β§6a: entry date minus access-end = 0 days for 14 of 14 sampled. Entry triggers are not API-readable, but the behaviour is, and it is decisive

Β§5 β€” πŸ”΄ Our self-serve billing portal offers exactly one door, and it is the exit

Live config bpc_1PVK9OI6s6pBAbnm6Al4K8la (default, active), read 11 Aug:

Feature State
subscription_cancel βœ… enabled (mode: at_period_end)
subscription_update ❌ disabled
subscription_pause ❌ disabled

A member can cancel themselves and cannot change plan, downgrade to $9.99, or pause. The only self-service action we offer is leaving. Enabling subscription_update is a configuration change, not development, and it turns the $9.99 rate from a popup that appears at the single worst moment into a standing alternative to cancelling.

⚠️ UNVERIFIED, and it needs one look: whether the portal still offers "Renew" on a cancel_at_period_end subscription with subscription_update off. Both R1 templates promise it β€” "turning renewal back on takes about a minute… renewal is one click from there." If it does not, every cancel-save email we send points at a button that isn't there.

Β§5a β€” PAUSE: withdrawn, then REINSTATED by Aga β€” and the research says she is right

I proposed pause, then withdrew it after reading the 10 Jul rejection. Aga pushed back: "maybe we could leverage the pause for a different type of user β€” maybe this still makes sense." She is right, and the industry evidence is not close (Β§8): pausers resume at 40–60%, against 3–10% win-back rates for fully-cancelled subscribers, and ~25% of would-be churners pause rather than cancel. Pause is the single most effective mechanism in a cancel flow. Nothing else we can build competes with a 5–10Γ— multiple.

What July got right, and it still binds: the mechanic must differ by plan.

Who Mechanic Cash cost
Monthly / quarterly (31 of 115) pause billing β€” the standard mechanic deferred, not lost
Annual (84 of 115) πŸ”‘ extend the access term β€” "freeze your year" ZERO β€” we already hold the cash

πŸ”‘ The annual insight is the one everybody misses. An annual member has already paid, so "stop billing me" is meaningless to them β€” but "I'm injured for eight weeks and my year is burning" is exactly why they cancel. Freezing the term costs us nothing and converts a cancel into a deferral. That is the different type of user Aga was pointing at, and it is 73% of the queue. (July measured pause against monthly-only and correctly found it thin β€” the term-extension framing was never on the table.)

Two hard requirements before it ships:

  1. πŸ”΄ Entitlement gating. Stripe's pause_collection leaves status: active, so an app that gates access on status alone hands out free accounts. Documented 10 Jul, re-proved 11 Aug.
  2. Not a portal toggle. The portal field reads "subscription_pause": {"enabled": false} and the feature is deprecated; the real path is native POST /subscriptions/{id}/pause or an app-side term extension. πŸ”΄ And our web cancel button is CUSTOM, not the Stripe portal (Aga, 10 Jul), so portal settings do not govern that screen at all.

Record correction: the July note says Stripe "removed subscription_pause… no such feature field". Measured 11 Aug, the field exists and reads enabled: false. Whether it can still be set is [UNVERIFIED] β€” deprecated, and testing means writing to live billing config. Immaterial to the plan: the native path is the one we want anyway.

And WB4 stays retracted. "Toggle plan-switching OFF until Nic confirms webhook consumption β€” would desync entitlements" (10 Jul). The portal writes to Stripe only; the app learns via webhooks nobody has verified (our key is 403 on webhook_endpoints).


Β§6 β€” Today's cancellers never enter the win-back arc at all

Of the 115 in the cancel queue, 105 resolve in ActiveCampaign and ZERO are enrolled in 976.

Good: the two systems do not collide. Bad: when a current canceller's access ends, nothing fires. 976's 331 entrants are a different, historic population β€” it has sent 1,476 emails across 6 campaigns at ~0.2% CTR and produced no measurable return.

And it is armed wrongly: 2 of its messages carry /30trial/, banned for this audience β€” message 13204 has already sent to 219 people. No harm resulted (0 human clicks; the one recorded click is IP 127.0.0.1 / OkHttp, a prefetch β€” and 0 of 648 active subscriptions are duplicated), but it remains live. Now machine-watched: tools/retention/audience_banned_link_check.py, registered in estate_selfcheck.py.


Β§6a β€” πŸ”΄ 976 Cancelled Win-Back ALREADY FIRES AT ACCESS-END (measured, and it corrects us both)

Aga believed it fired at cancel-click; I had said the trigger was unknowable from the API and proposed moving it. Both wrong β€” and her instinct that it was checkable was right. Entry triggers are not API-readable, but you can infer the trigger from behaviour: join each entrant's AC adddate to their Stripe canceled_at and current_period_end.

entered canceled_at access_end entry βˆ’ cancel entry βˆ’ access-end
2026-08-11 2026-02-15 2026-08-11 177 d 0
2026-08-11 2025-11-02 2026-08-10 281 d 0
2026-08-08 2026-08-04 2026-08-07 3 d 0
2026-08-07 2025-12-13 2026-08-07 236 d 0
2026-08-05 2025-08-22 2026-08-04 347 d 0

14 of 14 sampled entered on the day their access ended. The gap to cancel-date ranges 3–347 days. A member who cancelled 22 Aug 2025 entered on 5 Aug 2026 β€” the day their annual expired. Independently corroborated by Β§6: the live cancel queue (people who still HAVE access) has zero overlap with this automation.

What this changes

  1. WB3 dissolves. No builder change. The arc's entry is already correct β€” the split in Β§3 is how it already behaves, not a migration.
  2. πŸ”» The /30trial/ finding is DOWNGRADED from a safety defect to an offer defect. The ban exists because "a still-paid-up annual who clicks it starts a SECOND subscription" β€” but nobody in this arc still holds a live subscription when they enter. That matches the measured outcome exactly: 0 double-bills across 648 active subs. It is still the wrong offer (the acquisition rate, and worse than what they refused at cancel β€” Β§2), and it still must be replaced. It is not the live hazard I first called it.
  3. The re-earn gap is the whole story. With win-back already correctly placed at access-end, the untouched opportunity is the 88 people with a median 106 days of paid access left, whom nothing addresses after the single T+0 note.

πŸ”‘ Method worth reusing: when a config is unreadable, measure the behaviour it produces. "Not API-readable" is a statement about one endpoint, not about the question.


Β§8 β€” THE INDUSTRY STANDARD we are being measured against

Researched 11 Aug 2026. These are other people's numbers, not ours β€” use them as the bar, not as evidence about TMA.

Benchmark The standard
Save rate at the cancel screen 15–35% of people who reach it; Churnkey's platform average 34%
πŸ”΄ Pause vs win-back pausers resume at 40–60% Β· fully-cancelled win back at 3–10% Β· ~25% of would-be churners pause instead of cancelling
Discount responsiveness ~34% of churned customers say a discount would have kept them β€” if price was their reason
First win-back touch within 24 hours of cancelling, while still in the product's mental frame
Best win-back window 60–90 days. Beyond 90 β†’ 10–12%. Beyond 180 β†’ 2–4%
Sequence length 3–4 emails, branched on the cancel reason, the offer in the last one (~14.7% cumulative on a 4-email escalation; 5–15% typical)
πŸ”΄ Natural reactivation monthly subscribers reactivate 13.7% within 12 months with NO intervention β€” a win-back claim without a holdout is fiction

The two rules that indict our current flow

  1. Survey FIRST, then an offer matched to the stated reason β€” downgrade for an oversized plan, pause for "not the right time", a discount only for price. πŸ”΄ We show one price to everyone before we have asked anything.
  2. πŸ”΄ Measure the 90-day retention of the people you save, not the save rate. The standard's own words: "a save that only delays cancellation by two weeks is not a real save." Our $9.99 save has a median life of 2.0 months (Β§2a). We are the textbook anti-pattern, and the only reason we could not see it is that nobody had measured what happened after the save.

Sources: Finsi β€” cancel-flow best practice Β· Userpilot β€” cancellation flow examples 2026 Β· Subscription Index β€” cancellation flow Β· Recurly β€” pause as a retention tactic Β· Chargebee β€” the power of pause Β· Finsi β€” win-back timing Β· Airbridge β€” subscription-app win-back Β· Klaviyo β€” win-back examples


Β§9 β€” THE TARGET JOURNEY β€” what should happen to someone who cancels

Stage Trigger What happens Offer
0 Β· The cancel screen they click cancel πŸ”΄ ASK WHY FIRST. Then branch: price β†’ the save Β· time/injury β†’ pause / freeze the year Β· app problem β†’ support, no offer Β· not using β†’ re-onboard reason-matched, never one-size
1 Β· T+0 cancel confirmed the existing hello@ R1 note β€” one question, no pitch. βœ… already correct: the standard says first touch inside 24h, and we do it none
2 Β· The 106 days access still running πŸ”΄ THE EMPTY SPACE β€” the biggest gap in the lane. Two product beats (~day 30, ~day 75), behaviour-gated, aimed at getting them into a session none
3 Β· T-30 pre-expiry 30 days before access ends the existing watcher fires. The last moment a coupon can save the renewal coupon on the LIVE subscription β†’ $78.50
4 Β· Access-end access dies 976 Cancelled Win-Back β€” 3–4 emails, branched on the captured reason, reason-to-return first checkout β†’ $78.50, in the LAST email only
5 Β· Sunset >180 days lapsed reactivation is 2–4%; stop paying to mail them none

What changes versus today: stage 0 asks nothing and offers the wrong price · stage 2 is silent · stage 4 exists but pitches the acquisition offer and reaches an audience the hello@ lane never hands over (hello@ ∩ 976 = ZERO, §6). Stages 1 and 3 already work and should not be touched.


Β§10 β€” THE OFFER STANDARD β€” one price, two mechanisms

πŸ”΄ Four different win-back prices are live in our own records. This section ends that.

Price Where it comes from Verdict
$9.99/mo ongoing OFFER_LADDER §1b ❌ $119.88/yr, measured $34/taker, 2.0-month median life
$9.99 first month (/30trial/) what 976 actually ships today ❌ the acquisition offer, banned for this audience
SAVE30 = $109.90 the 21 Jul doc, and open decision WB3 ❌ see below
βœ… Annual at 50% = $78.50 90.7% of all new annual sales βœ… THE STANDARD

Why $78.50 and not $109.90 β€” and the argument is fairness, not arithmetic. 90.7% of new annual sales already go at exactly 50% off. $78.50 is simply what a stranger pays. Quoting a returning member $109.90 charges loyalty a premium, and $9.99/mo bills them $119.88 a year for the same product. A member who comes back should never pay more than a cold lead.

The mechanism differs by moment; the number the member hears never does.

Moment Their subscription Mechanism
At cancel / T-30 ALIVE 50%-off duration: once coupon on the existing subscription (tma_v3_50off_once exists) β€” next renewal bills $78.50, auto-renew stays on. πŸ”΄ Not a checkout (Β§2b/Β§2c: modify the live sub β€” cleaner, and it avoids the 4.1% slip)
After access-end DEAD checkouts-v3/50off/12month/ β€” safe, and provision_buyer_from_intent already provisions

πŸ”‘ One price keeps the copy honest. A member who sees the T-30 email and the win-back email should never be quoted two numbers β€” which is exactly the trap AGA-114 already flags ($97 vs $78.50 for the same annual plan). Settle AGA-114 before this ships.

The rule for every other sequence β€” CORRECTED, see Β§2c

Prefer modifying a live subscription over selling a new one. The checkout path works β€” Aga was right and the measurement backs her β€” but at a 4.1% slip rate any flow sending a live subscriber to a checkout must end the old subscription.


Β§7 β€” Verify any of this yourself

python3 tools/retention/audience_banned_link_check.py         # banned links live in a cancel arc
python3 -c "import csv;r=list(csv.DictReader(open('tools/retention/cancel_queue_export.csv')));print(len(r))"
# Stripe: /billing_portal/configurations Β· /coupons (all duration=once) Β· /prices unit_amount=999

Companions: OFFER_LADDER.md Β§1b (amended 11 Aug with the measured answer) Β· CHECKOUTS_V3_URL_MAP_AND_USE_CASES_2026-08-04.md (never invent a checkout URL) Β· tools/retention/HELLO_EMAIL_RULES.md (the 1:1 rail's copy law) Β· AUDIENCE_INTEGRITY_MASTER_2026-08-07.md (segment gate before any send).

The engine (technical)

How a per-person email is built

The slots Β· the renderer/adapter split Β· why ActiveCampaign cannot branch

ABANDONMENT_PERSONALISATION_ENGINE_2026-08-07.md

βš™οΈ THE PERSONALISATION ENGINE β€” how one person's email is actually built

β­πŸ”΄ CANONICAL for how TMA renders per-person copy. Written 7 Aug 2026 on Aga's directive: "the mechanism / engine of this is very important as it's building the personalisation that we need to expand on our abandonment sequence." Sub-doc of ABANDONMENT_STRATEGY_2026-08-07.md (Β§0a is the register). Companions: the ABANDONMENT_OPERATIONS_MANUAL (when things happen) and the ABANDONMENT_DATA_FALLBACK_LADDER (what to do when a field is missing).

The engine is tools/retention/quiz_abandon_manual_notes.py. The code is the canon β€” this document explains it and must be checked against it, never the reverse. Verify any claim here with python3 tools/retention/quiz_abandon_manual_notes.py --example --tier A|B.


1 Β· WHY THIS IS THE IMPORTANT PART

Most "personalised" email is token substitution: Hi %FIRSTNAME%, still interested in %PRODUCT%? Everyone can do it and nobody is fooled by it.

This engine renders a different sentence per person β€” chosen from what we actually know about them, degrading gracefully when we don't, and carrying a real member's verbatim words matched to their goal, age and gender. A Tier-A abandoner receives:

subject: "about the plateau you mentioned" The quiz built you a muscle plan that starts at your actual strength level. And you told the quiz that hitting a plateau is what's been holding you back. For men in their forties, with years of gym training behind them, progressive calisthenics is actually perfect. And the quiz says that's exactly where you are. "…" β€” a real member, matched on theme Γ— age band Γ— gender.

That is the asset. The 1:1 hello@ note is only the first surface it drives β€” the same engine is what makes the ActiveCampaign arc (756/757) personalised instead of persona-generic, and what will make any future audience (results-abandoners, v3, win-back) personalised on day one.


2 Β· THE ARCHITECTURE β€” a pure renderer plus interchangeable adapters

Function Role Surface-specific?
assemble(persona, prof, age_h) β†’ tier, subject, slots THE RENDERER. Detects the data tier from which fields actually returned values, then picks one rendered sentence per slot No
choose_checkout(persona_stripe, prof) β†’ (url, why, http_status) The right checkout for this person, HTTP-200-verified this run No
pick_quote(persona, obstacle, age, gender) Scores every curated quote: 2Γ—theme overlap + 2 if age band matches + 1 if gender matches No
jly_block(persona, prof) The demographic sentence, three rungs deep No
build_body() + BODY + put_draft() Plain-text assembly β†’ Gmail draft πŸ”΄ YES β€” this is the adapter
the manifest writer One audit row per person per run No

πŸ”‘ Everything above the adapter line is surface-agnostic. Adding ActiveCampaign does not mean rewriting the engine β€” it means adding a second adapter (Β§5).

The slot inventory (what the renderer produces)

Slot Type Rung 1 Rung 2 Floor Never
subject line obstacle-specific persona "the plan the quiz built you is still sitting here" age/gender in a subject
goal_line sentence persona-specific β€” neutral "built from your answers, at your specific level" render the enum (FAT_BURNER)
obstacle_block paragraph obstacle-specific β€” omitted entirely guess an obstacle
jly_block paragraph gender + age ("for men in their forties, with years of gym training behind them") age or background only goal-anchored, no demographics claim a demographic we don't hold
member_intro + member_quote proof theme Γ— age Γ— gender theme only strongest default edit a quote; drop the member's own real demographics
checkout URL link all-mastered β†’ ?path=ALL_MASTERED Stripe persona (authoritative β€” it's what they were buying) Aga's default checkout ship a URL that didn't return 200 this run
{when} phrase from Stripe created β€” always exists β€” β€” β€”

Full rung logic and the conflict rules live in the FALLBACK LADDER; this table is the index.

Tier detection β€” measured, never assumed

tier = "A" if any(prof.get(k) for k in ("obstacle","age","gender","background","recency")) else "B"

Per person, from which fields actually returned values β€” not from the date. A Tier-A person with a half-written profile automatically gets Tier-B rungs for the missing slots. That single line is why the engine cannot fabricate: absent data isn't defaulted, it changes the rung.


3 Β· πŸ”΄ THE CONSTRAINT THAT SHAPES EVERYTHING DOWNSTREAM: ActiveCampaign CANNOT BRANCH

AC has no HTML conditional content. Verified three ways: data-ac-condition|%IF|%ELSE% matched 0 of 539 live AC message bodies in the repo's API-pulled backups; the scan of 69 live bodies across five automations found 0; and the only two files in the entire repo containing data-ac-condition are our own E1.html (7 blocks) and E2.html (8) β€” a marker our own fleet invented so a human could rebuild the blocks by hand in AC's designer.

β‡’ Load E1 as-is and every reader sees all seven persona paragraphs stacked. Neither file has a default/else block. Liquid {{ }} is separately hard-banned by email_lint.py (AC prints it literally).

What AC provably does: straight merge-tag substitution of a custom field, including inside an href query string. Nothing else.

Therefore the only design that works is PRE-RENDERING β€” which is exactly what this engine already does. The persona prose moves out of the HTML and into the engine; the email carries one merge tag per slot.


4 Β· THE AC ADAPTER β€” designed, not yet built (FLEET-42)

The delta is BODY.format(...) β†’ PUT /api/3/contacts/{cid}. The slot values assemble() returns already are the rendered sentences.

Field ids β€” live census 7 Aug 2026 (read-only): 54 custom fields, max id 79. - FREE: 57, 72, 73, 74, 75, 76. Six slots, enough for the 3–4 sentence fields needed. - 58/59/60 are utm_source/utm_campaign/utm_content (previously unknown in our docs). - πŸ”΄ All 54 fields are type: text. There are ZERO textarea fields in the account. Field 56 holds a full checkout URL, so text takes ~70+ chars, but our sentences run 100–200. β‡’ STEP ONE IS TO PROVE IT: write one full sentence to one test contact, read it back, confirm no truncation. If it truncates, mint as textarea. Do not mint six fields on an assumption.

Write shape β€” one call, not four:

PUT /api/3/contacts/{cid}
{"contact": {"fieldValues": [{"field": 72, "value": goal_line},
                             {"field": 73, "value": objection_line},
                             {"field": 74, "value": ps_line}]}}

Proven at 11,500 contacts by funnel_stage_export.py:200. Rate ceiling ~4–5 req/s (sleep 0.18–0.25), retry on 429/5xx, and abort after 200 errors so a systemic fault cannot masquerade as progress. Field creation already exists: ensure_fields() in quiz_abandon_sync.py mints a missing field from a 4-line dict edit.

Four laws for the adapter, each learned the hard way:

  1. πŸ”΄ EVERY SLOT IS ALWAYS WRITTEN, with a non-empty default. An unset AC field renders as an empty string β€” it does not skip the sentence. "the thing standing in your way is " is a shipped defect. A slot that is legitimately absent (the obstacle block) gets a universal sentence, not a blank.
  2. πŸ”΄ SLOTS FREEZE AT ENROL β€” one writer, no mid-arc rewrites. The arc sends over four days, but the fields are shared mutable state on the contact. If anything rewrites them mid-arc (a re-abandon, a second sync pass, a future audience build), E3 renders copy that contradicts what E1 promised. Any writer must refuse to touch a contact whose state is enrolled-*, and the manifest records what was written so the sent copy stays reconstructible.
  3. READ BACK β€” the write is not the truth. Re-read a sample of stamped contacts live, per funnel_stage_export.py:241. And note the one thing that cannot be verified from here: that AC renders the tag at send. That is observable only on a real send.
  4. The email must render sensibly with every tag empty. That is the test for whether the default is doing its job.

The collapse this buys: E1's 7 divs β†’ <p>%TMA_ABANDON_GOAL_LINE%</p>. E2's 7 β†’ one objection tag. E2's 8th (the JUST_START P.S.) needs a full-sentence field with a universal default, because an empty field inside <div class="ps"> leaves a styled empty block. 15 fake conditionals β†’ ~3 merge tags, and 30 lines of persona prose move into a dict.


5 Β· HOW TO EXTEND IT

Add a SLOT (a new personalised sentence): add its lines dict + its rungs, add it to assemble()'s return, add its placeholder to BODY, add its rung row to the FALLBACK LADDER, and check --example on both tiers. A slot with no floor rung is not finished.

Add a SURFACE (a new delivery target): write a new adapter that consumes assemble()'s output. Do not touch the renderer. For AC, follow Β§4's four laws. For anything with a character limit (push, SMS), the adapter owns truncation β€” never the renderer.

Add an AUDIENCE (results-abandoners, v3, win-back): the renderer is reusable, but the copy is not β€” a results-abandoner never saw a price, so price copy is nonsense to them. New audience = new BODY + new subject/goal lines + a stated data tier (a win-back contact holds LEAD-tier fields only). The coverage doc's own line applies: "Nothing about it is quiz-specific except the data source."

Change COPY: edit the engine's constants β†’ the fleet loop (CRO seat β†’ humanizer β†’ Aga) β†’ --example both tiers β†’ --rebuild to re-render existing drafts in place β†’ log the version to FLEET_LOG.jsonl. Locked facts: $97/$157 Β· under $2/week Β· 4.8β˜… (never 4.9) Β· "LEAD Sports Tech Accelerator β€” run by the Adidas family and ex-Red Bull executives" exactly Β· the 60-day guarantee Β· max 2 dashes.


6 Β· OPERATOR TRAPS β€” inherited from the sessions that paid for them

  • πŸ”΄ Person-grammar. "for men in their forties, with years of gym training behind them" β€” when a slot switches to third person, every phrase joining it must switch too. That is why a separate BACKGROUND_THIRD/AGE_THIRD map exists next to the second-person one. A mixed sentence ("for men in their forties… behind you") is the tell that a template assembled the copy.
  • πŸ”΄ A multi-line source string defeats a single-line replace. One humanizer edit silently missed because the string was split across lines. Always grep-verify after a bulk replace.
  • πŸ”΄ An inherited ANTHROPIC_API_KEY outranks the Mac's claude.ai login in the claude CLI ("Credit balance is too low"). Strip ANTHROPIC_* from the env before shelling out.
  • πŸ”΄ A card on a board β‰  uploaded. 756/757 were "built" β€” meaning files on disk β€” while live AC held one old 50%-off email and literal placeholder subjects ("r"). Always ac_email_read.py the automation.
  • πŸ”΄ Never reconstruct a send from memory. quiz_abandon_sent_log.csv + quiz_abandon_verify_manifest.json hold the timestamps and every per-person choice.
  • Pre-7-Aug leads have no AC profile beyond persona (field 61 only, 38/39 findable) β€” but Stripe always had persona, so checkout routing never degrades for old cohorts.

7 Β· VERIFY THE ENGINE IS BEHAVING

python3 tools/retention/quiz_abandon_manual_notes.py --example --tier A   # a full rendered email
python3 tools/retention/quiz_abandon_manual_notes.py --example --tier B   # the degraded version
python3 tools/retention/quiz_abandon_manual_notes.py --verify             # manifest, no drafts
python3 tools/retention/abandon_collision_gate.py --email <addr>          # safe to send?
python3 tools/retention/quiz_abandon_recovery_read.py                     # did it recover money?

Every run writes quiz_abandon_verify_manifest.json: per person β€” tier, which fields were present, persona sources + mismatch flag, the chosen checkout + why + its HTTP status that run, the quote id, and the decision or skip reason. That file is how anyone checks the ladder was obeyed without trusting this document.

Operations

Operations manual

Hour by hour Β· the rules and where each is enforced in code Β· the runbook

ABANDONMENT_OPERATIONS_MANUAL_2026-08-07.md

πŸ“– ABANDONMENT OPERATIONS MANUAL β€” what happens, when, to whom, under which rules

β­πŸ”΄ THE operating manual for the quiz-checkout abandonment machine. Ordered by Aga, 7 Aug 2026: "deeply detailed documentation on what is happening, when β€” hardened, followed rules." Companion to the STRATEGY master (ABANDONMENT_STRATEGY_2026-08-07.md β€” the why and the decisions); THIS doc is the how, hour by hour. Every time, count and rule below was read from the live system on 7 Aug 2026, and every section names its enforcement point β€” a rule that lives only in prose is not a rule.

Β§0 Β· THE REGISTER β€” it lives in the MASTER, not here

πŸ”΄ One register, one place: ABANDONMENT_STRATEGY_2026-08-07.md Β§0a. This file used to carry a second copy, which is how two registers with different memberships appear in one estate. If you are looking for "what documents/tools/seats exist", go there.

Scope of THIS file: the quiz-checkout lane only. v3 abandoners and cancelled-member win-back are in the master (Β§5.4, Β§5.5) and have no operations of their own yet β€” that is a real gap, not an omission here.

Β§1 Β· THE TIMELINE OF ONE ABANDONER β€” hour by hour, system by system

A person finishes the quiz, sees results, opens the $97 checkout, starts paying, stops.

Clock What happens System State 7 Aug 2026
T+0 Stripe subscription minted incomplete, stamped checkout_version: quiz_checkout + email, persona, plan, offer. This stamp IS the estate separation β€” v3 checkouts stamp v3 and can never enter this machine (hard assert in the lane) Stripe LIVE
T+0 Welcome emails fire (results-ready, verify-account, 750 TMA \| Welcome Sequence After Quiz) β€” not abandonment emails; mapped in the strategy master Β§1a with their defects Django + AC LIVE
β‰ˆT+1h "Your login is in this email" β€” free first workouts + login. Nic's Django send; the de-facto first recovery touch. Our lane must never repeat its offer and never land near it Djangoβ†’SendGrid LIVE since 7 Aug
every 30 min com.tma.quizabandon runs quiz_abandon_sync.py --sync: TWO FEEDS merged β€” unpaid quiz subs from Stripe + (since 7 Aug) tag 63773 NEW quiz-reached-checkout, which catches the ~7Γ— of abandoners Stripe cannot see (the quiz PREFILLS the email, so the Stripe record never mints on blur). β†’ AC fields (persona/offer/plan) + tag quiz-checkout-abandoned. Tags and syncs only β€” emails nobody. Enrol mode exists but REFUSES without the QA gate + Aga's sentinel (not armed; 756/757 paused) launchd LIVE
T+24h β†’ 7 days The person becomes eligible for the ONE personal note. quiz_abandon_manual_notes.py (manual run, drafts-first) assembles their email per the fallback ladder: identity from Stripe β†’ profile from AC by stable id β†’ persona checkout verified 200 β†’ tiered copy β†’ matched member proof β†’ manifest row. Draft lands in hello@ the lane LIVE β€” first batch of 39 SENT 7 Aug (quiz_abandon_sent_log.csv)
morning after Aga reviews and sends. Nothing auto-sends; no --live mode exists in the lane; on her explicit GO the fleet may send via API with a final per-person buyer-check (the 7 Aug pattern) Aga AGA-125 βœ… done 7 Aug
after send Replies land in hello@ β†’ the R1 reply-watcher composes a genuine Jesse-voice reply draft to the member's ACTUAL email (Pablo law, 7 Aug). STOP β†’ suppressed instantly check_replies.py 05:35 daily LIVE
any moment They pay β†’ they exit everything. The paid-check runs at sync time, enrol time, draft time AND rebuild time, and fails safe (a Stripe error counts as paid) all lanes LIVE
day 4+ The AC arc (756 E1–E3 in-window / 757 E4 quarterly) takes over once rebuilt, uploaded, QA-passed and armed β€” then this manual lane RETIRES AC NOT YET β€” FLEET-42 β†’ E11 β†’ sentinel

Β§2 Β· THE RULES IN FORCE β€” each with its source and its enforcement point

# Rule Source Enforced by
1 Quiz estate only β€” a v3 (trial/50off/normal/upgrade/30trial) abandoner can never enter Aga 7 Aug ("separate the quiz checkout from the normal checkout") quiz_subs() metadata filter + hard assert in the lane loop
2 A buyer can never receive an abandonment email NIC-21 batch paid-set + live has_paid() per person per run, fails safe
3 One note per person, ever lane design quiz_abandon_manual_notes.csv state
4 24h floor / 7-day ceiling Django T+1h collision law (Nic, 7 Aug) MIN_AGE_H = 24 / MAX_AGE_H = 168
5 Right checkout, verified β€” all-mastered β†’ ALL_MASTERED; else Stripe persona; else Aga's default https://app.themovementathlete.com/payments/quiz-checkout/; every URL HTTP-200-checked THIS run or the person is skipped Aga's checkout map + default ruling, 7 Aug choose_checkout() + verify_url()
6 Missing data degrades, never fabricates β€” no guessed age/gender/obstacle, enums never raw the fallback ladder tier detection from actual field values; obstacle block omitted when absent
7 Never mention login / free workouts collision with Nic's T+1h send copy canon (template contains neither) + fleet seat law
8 Copy changes go through the fleet β€” CRO seat + humanizer + Aga review. Locked facts: $97/$157 Β· under $2/week Β· 4.8β˜… never 4.9 Β· "LEAD Sports Tech Accelerator β€” run by the Adidas family and ex-Red Bull executives" exactly Β· the 60-day guarantee Β· max 2 dashes. πŸ”΄ "48h" IS NO LONGER A LOCKED FACT β€” Aga ruled 7 Aug PM (D6): the code does not enforce the window (AGA-68's probe: TMA_QC_OFFER_EXPIRED never returns true, lapsed entries re-mint), so no copy may claim it until D2 enforcement ships. The template now says the price is held and offers to sort a lapse by reply EMAIL RULE + Aga's D6 ruling template lives in the lane script; rebuilt only via --rebuild; version logged in the fleet ledger
9 Proof is real and verbatim β€” quotes from TESTIMONIALS-DATABASE with the member's true demographics TESTIMONIAL RULE curated QUOTES block, ids kept, provenance-gated source
10 Nothing ships unverified Aga 7 Aug ("nothing shifts until you verify all the data") the manifest, written on EVERY run incl. dry
11 Suppression + STOP honoured forever HELLO rules suppress.csv checked pre-draft; STOP handling in the reply watcher
12 Segment checks before any bulk audience work tma-segment-integrity-officer (7 Aug) blocking seat for any AC-side audience this fleet builds
13 πŸ”΄ ActiveCampaign CANNOT do conditional content β€” never ship an email that branches inside HTML measured: data-ac-condition\|%IF\|%ELSE% = 0 of 539 live AC bodies; the only 2 files carrying it are our own E1/E2 the engine pre-renders one sentence per slot into a custom field; the email carries a bare merge tag. ABANDONMENT_PERSONALISATION_ENGINE_2026-08-07.md Β§3–4
14 πŸ”΄ Cross-system suppression β€” an AC unsubscribe blocks a hello@ note too the 1:1 lane checked only suppress.csv, so an AC unsubscriber could still receive a personal note abandon_collision_gate.py treats an AC unsub as HOLD; run it before any send
15 πŸ”΄ Rendered slots FREEZE at enrol β€” one writer, no mid-arc rewrites the AC arc sends over 4 days while the fields are shared mutable state; a rewrite makes E3 contradict E1 any writer must refuse a contact whose state is enrolled-*; the manifest records what was written

Β§3 Β· WHO DOES WHAT

Aga: sends (AGA-125), rules on decisions D1–D5, arms the AC sentinel. Nic: 756/757 upload + activation (E11), the 48h enforcement (AGA-109), his Django send (D1). The fleet: everything else β€” tma-quiz-checkout-abandon-owner chairs; the data analyst proves the audience (Stripe money only); the copywriter writes inside the ladder; FLEET-42 = the E1–E4 personalised rebuild. The machine: sync every 30 min; the lane on demand; the reply watcher daily at 05:35; all visible in the cockpit's βš™οΈ Daily Jobs tracking band.

Β§4 Β· THE RUNBOOK β€” the five commands

python3 tools/retention/quiz_abandon_manual_notes.py            # dry: who would get what + manifest
python3 tools/retention/quiz_abandon_manual_notes.py --verify   # audit only, verbose
python3 tools/retention/quiz_abandon_manual_notes.py --draft    # draft NEW eligible people into hello@
python3 tools/retention/quiz_abandon_manual_notes.py --rebuild  # re-render existing drafts (copy change)
python3 tools/retention/quiz_abandon_manual_notes.py --example --tier A|B   # print a full example email

Sending = Aga in Gmail, or the fleet on her explicit "send them" (throttled, ids logged).

Β§5 Β· HARDENING β€” how this cannot silently drift

The estate-separation assert crashes loudly if a non-quiz sub ever appears Β· every checkout URL re-verified per run Β· the manifest makes every run auditable after the fact Β· the daily tracking band shows the sync's health every morning Β· the QA gate + sentinel stand between the AC arc and real sends Β· the copy canon lives in ONE file under version control, changed only through the fleet loop and logged to FLEET_LOG.jsonl Β· and all four docs of this estate are now pointed to from every abandonment seat and the runbook (verified 7 Aug PM: 4/4 in each of tma-quiz-checkout-abandon-owner, tma-abandonment-copywriter, tma-abandonment-data-analyst and /abandon-fleet) β€” πŸ”‘ this sentence previously claimed that and it was FALSE: the manual was referenced by ZERO seats and the master by one of three. A hardening claim that is not measured is the most dangerous line in a document, because it is the one a future session trusts.

Three instruments make the claims above checkable: quiz_abandon_recovery_read.py (did it recover money β€” with the holdout caveat) Β· abandon_collision_gate.py (all three sending systems + Stripe + AC unsub, before any send) Β· --example --tier A|B (the rendered email).

Missing data

The fallback ladder

Tier A/B Β· what the copy does when a field is absent Β· degrade, never fabricate

ABANDONMENT_DATA_FALLBACK_LADDER_2026-08-07.md

πŸͺœ THE ABANDONMENT DATA FALLBACK LADDER β€” what we do when a field is missing

β­πŸ”΄ CANONICAL for the Abandonment Recovery Fleet (/abandon-fleet). Ordered by Aga, 7 Aug 2026: "They need to understand: if you're missing data, here's what we do." Enforced in code: tools/retention/quiz_abandon_manual_notes.py implements every rung; quiz_abandon_verify_manifest.json records every resolution, every run. Carried by: tma-quiz-checkout-abandon-owner Β· tma-abandonment-copywriter Β· tma-abandonment-data-analyst. Data dictionary: LEAD_SIGNAL_CONTRACT_2026-08-06.md Β§8.

0a Β· πŸ”΄ THIS LADDER IS SURFACE-AGNOSTIC β€” it governs every abandonment surface

The rungs below are not a property of the hello@ 1:1 note. They are the contract of the renderer (assemble() in quiz_abandon_manual_notes.py), so the same ladder governs:

  • the hello@ 1:1 note (live β€” 39 sent 7 Aug),
  • the ActiveCampaign arc 756/757 once the rendered slots are delivered as merge fields,
  • any future audience (results-abandoners, v3, win-back) that reuses the engine.

πŸ”΄ The AC surface makes the floor rung load-bearing, not optional. ActiveCampaign renders an unset field as an empty string β€” it does not skip the sentence. So a slot whose rung-3 is "omit entirely" (the obstacle block) cannot be omitted on that surface: it must carry a universal sentence instead, or the email ships with a hole in it. And AC cannot branch at all (data-ac-condition is not real AC code β€” 0 of 539 live bodies), which is exactly why the rendering happens here and not in the HTML. See ABANDONMENT_PERSONALISATION_ENGINE_2026-08-07.md Β§3–4.

Scope note: the cohort boundary and field ids below are the QUIZ estate's. A win-back or broadcast audience holds LEAD-tier fields only (LEAD_SIGNAL_CONTRACT Β§8a) β€” a different, shallower ladder that has not been written yet.

0 Β· The one law above the ladder

A missing field DEGRADES the sentence gracefully. It never fabricates it. Personalisation that guesses ("JUST LIKE YOU, in your 40s" to an unknown-age reader) is worse than none β€” one wrong guess converts the email from "Jesse knows me" to "this is a template that missed." Enum values never render raw. Every degradation is recorded in the manifest row.

1 Β· The two cohorts β€” the data boundary is a TIMESTAMP, not a guess

The quiz began writing the full profile into ActiveCampaign at 2026-08-07 02:47 (NIC-54's first complete lead). That instant splits every abandoner into:

Cohort Who Data that EXISTS Measured [LIVE 7 Aug, manifest]
Tier B β€” abandoned before 7 Aug 02:47 the current 39 Stripe only: email Β· persona (THE GOAL) Β· plan Β· offer Β· abandoned-at. AC holds a copy of persona (field 61, written by our own sync) and nothing else 39/39 persona present Β· 38/39 findable in AC Β· 0 Stripe-vs-AC conflicts
Tier A β€” abandoned after everyone from now on Stripe (above) + AC Β§8 by stable id: 64 obstacle Β· 65 stakes Β· 66 age Β· 67 gender Β· 68 all-mastered Β· 69 life-context Β· 70 background Β· 71 recency Β· 77 weakest census: quiz_lead_signal_check.py

Tier is detected per person at build time from which fields actually return values β€” never assumed from the date. A Tier-A person with a half-written profile automatically gets the Tier-B rungs for the missing slots.

2 Β· The ladder β€” slot by slot

Slot Rung 1 (best) Rung 2 Rung 3 (floor) NEVER
Checkout link (πŸ”΄ always carries &email= β€” without it the page cannot resolve THIS person's deadline and mints a fresh 48h window instead; fixed 7 Aug PM) AC 68 all-mastered = yes β†’ ?path=ALL_MASTERED Stripe persona (authoritative β€” it's the plan they were buying) β†’ ?path=<PERSONA>; AC 61 only if Stripe blank the bare default checkout https://app.themovementathlete.com/payments/quiz-checkout/ (Aga's pre-ship ruling, 7 Aug β€” verified 200), flagged in manifest. πŸ”΄ Know what the reader will SEE when they click it: the bare URL renders the GENERIC hero β€” "Get strong, mobile & pain-free." / "Have fun mastering 100+ calisthenics skills, at your exact level." β€” NOT a persona hero. Confirmed by Nic 7 Aug and verified live the same day by reading the served page's PATHS map (GENERIC is the match:'' default; RECOVERY is an alias of PAIN_FREE and only fires on an explicit ?path=/?persona=). So a rung-3 email must not promise a persona-specific page β€” if the copy says "your recovery programme is waiting" and the link lands on the generic hero, the promise breaks at the click. Rung 3 copy stays neutral. ship a URL that didn't return HTTP 200 this run β€” if even the default fails, the person is SKIPPED
Subject obstacle (AC 64): "you said staying consistent is the hard part" persona: "your muscle plan is still sitting here" "the plan the quiz built you is still sitting here" put age/gender in a subject
Goal paragraph persona-specific line β€” neutral "built from your answers, at your specific level" render the enum ("FAT_BURNER")
Obstacle paragraph AC 64 β†’ "And you told the quiz that X is the hard part…" β€” OMITTED entirely guess an obstacle
JUST-LIKE-YOU line gender + age β†’ "For men in their forties, with years of gym training behind them, progressive calisthenics is actually perfect. And the quiz says that's exactly where you are." age or background only β†’ "…for someone JUST LIKE YOU, {phrase}" goal-anchored: "For people who want to get lean without grinding cardio, progressive calisthenics is actually perfect." claim a demographic we don't hold
Member proof quote matched on theme(goal+obstacle) + age band + gender theme only default strongest quote fabricate/edit a quote; the member's OWN real demographics ("a guy in his mid-40s…") appear at EVERY rung β€” that's how demographic proof stays present even when the recipient's demographics are unknown
"{when}" phrase Stripe created (always exists) β€” β€” β€”

3 Β· Where each datum comes from, in priority order

  1. Stripe subscription metadata β€” identity, persona, plan, offer, timestamps. Always present on this estate (the checkout stamps itself: checkout_version: quiz_checkout). πŸ”΄ The estate-separation belt: a v3 sub can never enter this lane (hard assert).
  2. ActiveCampaign fields by STABLE ID (61/64–71/77) β€” the deep profile, Tier A only. Never looked up by name; the 5,623 legacy junk tags are never read.
  3. The testimonial database (tagged: gender, age band, 18 themes) β€” the member side of the personalisation; always real, always verbatim.

Conflict rule: Stripe wins for the checkout (it is what they were buying); AC wins for who they are. Conflicts are counted in the manifest (persona_mismatch) β€” currently 0.

4 Β· Verification β€” how anyone checks this is being obeyed

Every run of the lane (dry, verify, draft, rebuild) writes tools/retention/quiz_abandon_verify_manifest.json: one row per person β€” tier, fields present, persona sources + mismatch flag, chosen checkout + WHY + its HTTP status this run, quote id, decision or skip reason. --verify produces it without touching a draft. Buyer-safety (live has-paid, fails safe), suppression, one-note-ever, the 24h Django-collision floor and the 7-day ceiling all run before any body is assembled.

Your decisions

The calls only you can make

Everything else on this page is fleet work. Open rows first β€” each with what I’d do and why. Settled ones stay collapsed below, as the audit trail of what you already ruled.

WB6needs you Reinstate PAUSE β€” and for annual members it is 'freeze your year'

You pushed back on my withdrawal and the research backs you: pausers RESUME at 40–60%, against 3–10% win-back for the fully cancelled. It is the single most effective mechanism in a cancel flow. The mechanic differs: monthly/quarterly pause BILLING; annual members have already paid, so the equivalent is EXTENDING THE TERM β€” which costs us nothing.

Recommendation Needs Nic: entitlement gating (Stripe's pause_collection leaves status=active β‡’ FREE access), and our cancel button is CUSTOM so portal settings do not govern it. Sized against a 40–60% resume rate, this is the highest-value build in the lane.

WB7needs you Ask WHY before offering anything on the cancel screen

The industry rule we break hardest: survey first, THEN an offer matched to the stated reason. Our exit data says PRICE 25% Β· TIME 23% Β· APP/TECH 19% Β· NOT-USING 12% β€” so a price cut is the wrong answer for roughly three quarters of them, and you cannot discount your way out of 'the app kept crashing'.

Recommendation Branch: price β†’ the $78.50 save Β· time/injury β†’ pause/freeze Β· app problem β†’ support, no offer Β· not using β†’ re-onboard. Part of NIC-101.

WB2you approved The win-back price is settled: $78.50 β€” one price, two mechanisms

Aga, 11 Aug: β€œyes we can try that.” Annual leavers get annual-at-50% instead of $9.99/mo. Measured: $9.99 had 516 takers, 493 cancelled, MEDIAN LIFE 2.0 MONTHS, $34/taker β€” it defers churn, it does not stop it. The argument for $78.50 is fairness, not arithmetic: 90.7% of new annual sales already go at exactly 50% off, so it is simply what a stranger pays.

Outcome πŸ”΄ Mechanism differs by moment, the NUMBER NEVER DOES: while the subscription is ALIVE (cancel screen, T-30) it is a 50%-off duration=once COUPON on the existing sub β€” never a checkout, which would open a SECOND subscription and double-bill. Once access has ENDED, the 50off/12month checkout is correct. See the Win-back tab Β§10.

W-D1–D4reframed The welcome rebuild β€” SUPERSEDED by Aga 10 Aug: evolve, don't rebuild

She challenged it: β€œwe have a spine, why create a new one? we can personalise the current emails.” Correct β€” 5 of the 6 deltas retrofit into 750 TMA | Welcome Sequence After Quiz with no builder time: a FIELD does the stage-aware E1 (the branch was never load-bearing), J5 personalises each slot via the API, slots get pruned one at a time, and the workout-1 pitch ships as its own 1-email automation. Only recovery-pausing-the-welcome needs the builder.

Outcome No GO needed β€” the new-automation route was the EVOLUTION LAW's exception; slot-by-slot evolution is its default. The spine is shelved unless measured evolution stalls.

J5-NEXTreframed The welcome's worst slot β€” premise PRE-DATES Aga's own 10 Aug arc rebuild; fleet re-measures first

The 10 Aug scoreboard named campaign 2985 (1.39% unsub) as the slot to rewrite β€” but later that same morning AGA REBUILT the whole 750 TMA | Welcome Sequence After Quiz arc in the builder (A/B/C splits on every slot; live flow now = campaigns 4146–4153, verified via ac_email_read). The 2985 premise may no longer exist in the flow she built.

Outcome No GO to give yet: the fleet re-runs the worst-eligible-slot read on the NEW arc once it has β‰₯200 sends per slot, then asks β€” one slot under test at a time, Evolution-Law legal.

D1reframed Nic's T+1h free email β€” reframed by you, folded into J1

Your challenge stood: the free app is the PRODUCT, not a discount. Moving the email to day 4 was the wrong frame.

Outcome It merges into the ONE arrival email (results + login + first workout), and the PITCH moves to after workout 1. Ruled inside J1.

D3resolved Which win-back price is canon? β€” ANSWERED, see WB2

Two canonical docs disagree: 21 Jul says annual SAVE30 at $109.90, reply-based; OFFER_LADDER Β§1b says $9.99-ongoing β€” and FLEET-32 currently wires the second.

Outcome SAVE30: more cash sooner ($109.90 upfront vs ~$70 dribbled), everyone in the lane already DECLINED $9.99 at cancel, and reply-based delivery sidesteps the two open Nic questions.

J1–J6settled The post-quiz journey β€” all six RULED (row AGA-133 closed 12 Aug)

J1 ONE arrival email β€” simplified by Aga 7 Aug eve (one input parked as a Nic question) Β· J2 behaviour-gating on the live funnel-stage fields β€” yes, nightly grain Β· J3 the evergreen arc enters at the journey exit, and Aga ruled 12 Aug that 760 School of Strength (NEW) is for School-of-Strength lead signups ONLY β€” it is not the bridge, the AGA-113 repoint is retired Β· J4 $97 while the quiz window lives, $78.50 only in the evergreen 50% event β€” re-ratified 12 Aug (AGA-114 option c, written into OFFER_LADDER Β§2b) Β· J5 750 fixes stay surgical per the Evolution Law Β· J6 the 3-email activation micro-arc.

Outcome Nothing left to rule here β€” execution is fleet work. The live follow-ons are J5-NEXT (slot 2985) and W-D5 (pull campaign 3715), both in this list.

W-D5settled Pull campaign 3715 from the welcome's E1 β€” CLOSED 10 Aug: Aga did it herself (AGA-135)

Closed on artifact evidence: campaign 3715 is GONE from 750 TMA | Welcome Sequence After Quiz β€” Aga's 10 Aug arc rebuild removed it (live flow verified via ac_email_read: campaigns 4146–4153 only; backups in _AC_750_SPLIT_BACKUPS_2026-08-10/).

Outcome Nothing further β€” the 4 Aug ruling is executed and verified against the live automation, not a doc.

BUYER-EXITsettled The 'If customer β€” END' buyer-exit goal EXISTS β€” Aga checked the builder (10 Aug)

Aga's ruling, 10 Aug 2026: the buyer-exit exists, on ALL sequences. Goals are API-invisible, so her eyes in the builder were the only possible instrument β€” this closes the two-source contradiction (the Aug citations were right; June's exit_on_conversion=0 was wrong or stale).

Outcome Nothing further. Every arc's assumption that buyers exit is now CONFIRMED, not assumed β€” the one caveat that remains is that a goal exits people who BUY; it does not pause anything for abandoners (that is the separate recovery-pause item).

D2settled The $97 window β€” you ruled BINDING

Your words: 'for now we just say it is binding.' The Nic enforcement task is deferred, not urgent (AGA-132 is your approval gate for later).

Outcome Until enforcement ships, no email may claim the deadline β€” the code still never expires anyone.

D5settled Build order β€” approved

Phase 1 opened as FLEET-46: the pagination gate, the tag re-feed (before any copy), the E1–E4 personalised rebuild, the v3 capture job.

Outcome Order matters: re-feed before rebuild β€” a better email to 1.5 people/day is worth less than the same email to 10.

D7settled Recovery holdout β€” approved and ARMED at 1-in-5

One abandoner in five is now deliberately held back, hash-bucketed so nobody flips arms.

Outcome holdout: 0 for the first days is CORRECT β€” the arm fills ~2/day from new abandoners; honest lift read at ~20/arm (β‰ˆ6-8 weeks).

D4+D6settled The 39 notes β€” sent Β· the template β€” fixed

39/39 sent 7 Aug 12:37, zero failures, live buyer-check at send. Your D6 ruling executed: bold stripped, the unenforced 48h claim softened, the missing &email= added.

Outcome Replies get a drafted answer within 12h carrying that person's own checkout. Recovery read ~11 Aug.