The Movement Athlete · audience data & safety

Audience Integrity — what we know about every contact, and the gate that protects every send

Built + hardened 7 Aug 2026 · every number measured live that day · canonical sources: AUDIENCE_INTEGRITY_MASTER_2026-08-07.md (machine) + AUDIENCE_DATA_DICTIONARY_2026-08-07.md (data) — this page is their browsable twin; on conflict the .md wins

1 · What this is, and why it exists

The problem: thousands of people download the app, get partway, and stall. The app records where — but that knowledge lived only in Intercom, so ActiveCampaign (where we email from) couldn't use it. Result: ~11,000 warm people we sent nothing to.

The build: every night at 02:30, a job reads Intercom, works out one label per person — where they stopped — and writes it onto their existing ActiveCampaign contact. Now we can email each group the one message that fits, at £0 ad cost.

The hardening (same day): Aga caught a member celebrated as "141 workouts without paying" who was actually a $497 lifetime member since 2022. A payer check on live subscriptions cannot see lifetime (a one-time charge) or PayPal/app-store buyers (no Stripe record). Measured: the 25 heaviest "free" users were 23/25 payers. So this system now carries a four-layer money check and a blocking gate no send can bypass.

The two rulings that shape everything: ① people who reached checkout (segment D) belong to the Abandonment Fleet — this pipeline never touches them. ② this system stamps labels and gates audiences — it never sends anything; every email still goes through /email-fleet and Aga's approval.

2 · The tags & fields — what exists on a contact

Two families, from two journeys. TAG = a yes/no fact (for picking who). FIELD = a value (for writing what). If a tag's name would differ between two people, it must be a field — that law exists because 5,623 junk tags were once minted by values leaking into tag names.

Family 1 — quiz signals live 7 Aug 2026

WhatNamesCarries
15 tags — how far they gotNEW quiz-email-captured · NEW quiz-questions-done · NEW quiz-results-seen · NEW quiz-reached-checkout · NEW quiz-card-started · NEW quiz-all-fundamentals-mastered · NEW quiz-gave-email · 8 × NEW quiz-path-*State — segmentable, fixed set. Everything real carries the NEW quiz- prefix; tags without it are legacy junk.
10 fields — who they are%TMA_QUIZ_PERSONA% (the goal) · %TMA_QUIZ_OBSTACLE% · %TMA_QUIZ_FUTURE_STAKES% (their own words) · age · gender · all-fundamentals-mastered · life-context · background · training-recency · weakest-fundamentalData — usable in copy as merge tags.
🔴 Stage-gated: persona + obstacle exist on every lead; the other 8 only for people who saw their results. An unset field renders as an EMPTY STRING — never make a merge tag the subject of a sentence. And these signals start 7 Aug 2026 — the old ~83k list has none of them. This is also the complete quiz→AC set (Aga's ruling): if it isn't listed here, it doesn't exist.

Family 2 — app funnel stage live 7 Aug 2026 · nightly

Field valueWho they are (clean census)The natural message
survey_unfinishedsigned up in the app, never finished setup (3,276 stamped)"Finish setup — 2 minutes"
no_workoutsetup done, never finished a workout (5,872 stamped)"Your 3 free workouts are waiting"
no_trialtrains on free, genuinely never paid (1,909 stamped post-purge — the raw 2,809 was ~36% contaminated)"Here's what week 2 looks like"
(blank)payer · checkout-abandoner (abandon-fleet's) · >90d inactiveBY DESIGN — leave them alone

AC fields: TMA Funnel Stage (id 78, merge tag %TMA_FUNNEL_STAGE%) · TMA Funnel Stage At (id 79 — the date it was last computed).

3 · How to use them — click by click (for Aga)

4 · The machine — how a label gets onto a contact

📱 Intercom — the app syncs its funnel state here continuously (95,698 contacts, 31 attributes, with email)
↓ nightly 02:30 · com.tma.funnelstage
⚙️ funnel_stage_export.py --apply — computes one stage per 90d-active user · excludes payers four ways (Intercom plan · live Stripe subs · full Stripe charge history · AC lifetime tags) · clears the label the day someone converts · never creates a contact · self-verifies 8 read-backs · refuses to run on a stale ever-paid set or a truncated pull
↓ writes fields 78/79, changes only
✉️ ActiveCampaign%TMA_FUNNEL_STAGE% on each existing contact
↓ any send / bulk write / automation trigger must pass ↓
🔒 tma-segment-integrity-officer (blocking seat) → runs segment_guard.py: pipeline health · live segment re-pull · every email vs live Stripe · ever-paid set · freshness · 5 eyeballed members · PASS expires in 24h
↓ PASS pasted verbatim into the approval
👤 Aga approves — nothing sends without her
Self-healing: if the Mac was asleep at 02:30 the job runs on wake; if a run is missed or fails, com.tma.jobguardian (every 30 min) kickstarts it and alerts (macOS notification + email to hello@). The cockpit's 06:00 watchdog also runs a funnelstage health check — a sick pipeline shows up as DRIFT in the daily cockpit email without anyone asking.

5 · The send gate — "verified and checked multiple times"

No send, no bulk tag/field write (>10 contacts), no automation entry-trigger — from any fleet — without the officer's PASS. Wired as a non-skippable step into /email-fleet (Step 1.4c) and /abandon-fleet, carried by 9 send-arming seats, and bound in the project rules every session reads.

The five checks: ① pipeline healthy ② the audience pulled live from AC (never a doc count) ③ every email against live Stripe and the ever-paid set (lifetime is a charge; the AC lifetime tags catch PayPal/store buyers with no Stripe record) ④ labels fresh (≥95% under 48h) ⑤ five real members eyeballed against the copy's premise.

6 · The 8 laws — each paid for with a real error on 7 Aug

1 · Lifetime is a CHARGE, not a subscription. A live-sub check read a $497 lifetime member as "never paid". Money truth = charge history + lifetime tags + plan.
2 · Sweep-by-tag beats per-contact reads. One member passed a per-contact check and was caught lifetime by the bulk sweep.
3 · Workout counters are ALL-TIME. "141 workouts" accumulated since 2022, much of it while paying. Recency lives in last_seen_at only.
4 · Intercom search skips absent attributes. Segment A read 1,400 by search and is 4,356 by code census. Only a full-pull code census is honest.
5 · TAG = state, FIELD = data. A value in a tag name is the machine that minted 5,623 junk tags. The AC tag total (baseline 5,845) is the tripwire.
6 · Never create a contact as a side effect. App users who never opted into marketing stay off the list.
7 · A label clears the day the person converts. A segment must never contain yesterday's people.
8 · The honest residual, stated: a PayPal/store EX-subscriber (never lifetime, no Stripe) is invisible to all four layers once Intercom goes stale. Shrunk, not zero — never claim zero.

7 · Verify any of this yourself — never trust this page

# Is the pipeline healthy?  (HEALTHY / SICK with reasons)
python3 tools/retention/funnel_stage_export.py --health

# Full read-only census — the real counts, freshness, samples
python3 tools/retention/funnel_stage_export.py --dry-run

# Gate an audience before ANY send  (exit 0 = the only PASS)
python3 tools/retention/segment_guard.py --stage no_workout
python3 tools/retention/segment_guard.py --tag "NEW quiz-results-seen"

# Rebuild the ever-paid exclusion set  (required <48h before any apply)
python3 tools/retention/ever_paid_build.py

# Quiz signals census + tag-leak tripwire
python3 tools/retention/quiz_lead_signal_check.py --sample 2

Owner seats: tma-segment-integrity-officer (the gate) · tma-email-lead (email) · tma-intercom-expert (in-app/push on the same segments). Decision record: FUNNEL_STAGE_EXPORT_ANALYSIS_AND_PLAN_2026-08-07.md (Aga's D1/D2). Job register: AUTOMATION_JOB_REGISTER.md. This page ships with the cockpit deploy (estate_sync.py) — edits to the .md masters must be mirrored here.