004 ·  Selected work  ·  Healthtech marketplace ·  2020

CAC payback,
measured on a real cohort.

Rebuilding the cohort-economics layer at a growth-stage healthtech marketplace — and proving that two of its highest-spend channels were losing money on every patient they acquired.

Duration: 8 weeks  ·  Team: 2  ·  Programs: Diagnose · Advance  ·  Year: 2020

Brief

The ask

We were asked to figure out which paid channels at a Series-B healthtech marketplace were actually paying back, and on what horizon. The growth team had been buying off blended CAC for eighteen months. The board was asking, in plain language, whether the unit economics worked. The growth lead suspected they didn't — and couldn't prove it either way.

Starting state: a two-sided marketplace pairing patients with vetted dental providers, ~$8M ARR, paid acquisition across Google, Meta, and three affiliate networks. Bookings lived in a Postgres app database. Spend lived in three ad platforms. Provider-side revenue lived in a separate billing system. The constraint we came in with was honest — eight weeks, two people, an answer the CFO would defend in the next board meeting.

Problem

The real diagnosis

The reported symptom was "our CAC payback is opaque." The actual problem was that the growth team and the finance team were running on different revenue definitions and neither was wrong on its own terms. Marketing counted a booking the moment it was made. Finance counted revenue when the provider's appointment was completed and the patient hadn't refunded. Those two definitions diverged by 28–34% depending on the cohort — a third of booked revenue was being claimed by marketing that finance never recognized.

Underneath that: spend was attributed last-click at the ad platform level, but the patient's first appointment often happened six to ten weeks after the booking, and 18% of bookings were cancelled or rescheduled in ways the platforms had no visibility into. Repeat-booking behavior — the dominant LTV driver on this marketplace — was invisible to any platform-side report. The growth team was making allocation calls on a number that systematically overstated early channels and understated channels with longer payback tails.

The diagnosis: not an attribution problem, a cohort-economics problem. Until we could read every dollar of spend against the actual completed-revenue curve of the patients it brought in, the board question couldn't be answered honestly. Two channels suspected to be unprofitable could not be killed because nobody could prove it.

Approach

Five moves, in order

Move 01

Pick one definition of an activated patient. Get it signed.

Week one was a written contract between growth, finance, and product on what an "activated patient" was: first appointment completed, kept, not refunded inside a thirty-day window. We put it on one page, the CFO signed it, and every downstream metric was rebuilt against that definition. The hardest part of the engagement was the agreement, not the model.

Move 02

Stitch the patient journey end-to-end into one event spine.

Ad-click → landing → signup → booking → appointment completed → repeat booking, all keyed to a single patient pseudo-ID resolved across the app database and the marketing-platform identifiers. Fivetran into BigQuery for the ad platforms, a CDC stream off Postgres for the app events, dbt for the joins. Bronze was load-as-is. Silver was where identity got resolved.

Move 03

Build the cohort-LTV mart that finance and growth could read together.

One row per acquisition cohort × channel × month-since-acquisition. Completed revenue net of refunds, accumulating month over month. Payback is then a straightforward read against fully-loaded acquisition spend in the cohort's acquisition month. This is the model that ended the argument:

-- models/marts/mart_cohort_payback.sql
-- One row per (acquisition_cohort_month, channel, months_since_ack).
-- Tracks cumulative completed-net revenue per acquired patient
-- against fully-loaded acquisition spend in the cohort month.

with cohorts as (
    select
        patient_pseudo_id,
        acquisition_channel,
        date_trunc('month', activated_at) as cohort_month
    from {{ ref('silver_patient_activations') }}
    where activated = true             -- per metric contract:
                                       --   completed + kept + not refunded
),
revenue as (
    select
        c.cohort_month,
        c.acquisition_channel,
        date_diff('month',
                  c.cohort_month,
                  date_trunc('month', a.completed_at)
        )                                          as months_since_ack,
        sum(a.net_revenue_cad)                     as revenue_in_month,
        count(distinct c.patient_pseudo_id)        as cohort_size
    from cohorts c
    join {{ ref('silver_appointments_completed') }} a
      on a.patient_pseudo_id = c.patient_pseudo_id
     and a.refunded = false
    group by 1, 2, 3
),
spend as (
    select
        date_trunc('month', spend_date) as cohort_month,
        channel                         as acquisition_channel,
        sum(spend_cad)
          + sum(platform_fee_cad)
          + sum(creative_amort_cad)     as fully_loaded_spend
    from {{ ref('silver_acquisition_spend') }}
    group by 1, 2
)
select
    r.cohort_month,
    r.acquisition_channel,
    r.months_since_ack,
    r.cohort_size,
    r.revenue_in_month,
    sum(r.revenue_in_month) over (
        partition by r.cohort_month, r.acquisition_channel
        order by r.months_since_ack
        rows between unbounded preceding and current row
    )                                              as cum_revenue,
    s.fully_loaded_spend,
    safe_divide(
        sum(r.revenue_in_month) over (
            partition by r.cohort_month, r.acquisition_channel
            order by r.months_since_ack
            rows between unbounded preceding and current row
        ),
        s.fully_loaded_spend
    )                                              as payback_ratio
from revenue r
left join spend s
       on s.cohort_month         = r.cohort_month
      and s.acquisition_channel  = r.acquisition_channel

Move 04

Read the curve. Kill what doesn't pay back.

Two channels — one mid-funnel display network and one affiliate vertical — had payback ratios under 0.7 at month twelve. They weren't paying back on any reasonable horizon. The growth lead walked the curves into a finance review in week six and the spend was cut the following Monday. The reallocated budget went to a Google brand-search line that had a six-month payback on the same cohort math.

Move 05

Hand it to growth. Document the contract.

The mart, the metric contract, and a Metabase dashboard with cohort curves overlaid on payback thresholds — shipped with a fifteen-page Notion that the next analyst hire would read on day one. We were out by week eight. The growth team owns the model. The contract is still the document on the wall.

Outcome

14 mo
to 6 mo.

Blended CAC payback, measured on completed-net revenue, halved inside two cohorts. The board question was answerable. The answer wasn't the one growth had been telling itself — and that's the engagement.

14 mo → 6 mo

Blended CAC payback after channel cohorts were measured on the same definition of revenue.

2 channels killed

Spend categories that looked profitable on last-click but lost money on a six-month cohort read.

1 definition

Of 'activated patient' — first completed appointment, kept, not refunded — written, owned, and used everywhere.

8 weeks

Total engagement, from kickoff to a marketing team running its own cohort dashboard.

What this enabled, beyond the headline: the growth team stopped running blended-CAC dashboards and started running one cohort dashboard finance signed off on. The two killed channels freed roughly a quarter of the paid budget, which reallocated into channels with proven sub-six-month payback. Provider-side supply economics, previously invisible inside marketing reporting, were pulled into the same mart in a follow-on engagement — but that's a different story.

Credits

Who shipped this

Lead Analyst
Haris Naeem
Analytics Engineering
Contractor — anonymized
Marketing Analytics
Internal team — anonymized
Client Stakeholder
Head of Growth — anonymized
Client Stakeholder
Director, Performance Marketing — anonymized
Client Sponsor
VP Marketing — anonymized

Client-side names withheld for confidentiality. Named references available on request after a discovery call.

Next

Want a reference for an engagement like this?

Send a note describing the channel mix, the payback horizon you're defending, and where your spend and revenue currently disagree. If it's a fit, a named reference from this engagement is available after a discovery call.

© 2026 Coded Level — Toronto analytics studio.Toronto, ON · Canada