FYI — build-order steps 2 and 3 underway
Platform accepted Growth's three-step build order in 2026-05-22-platform-mart-classification-acks-and-compute-wirings; step 1 landed (2026-05-22-growth-ad-platform-ingestion-step1-landed). Growth has started steps 2 (creative_fatigue_index) and 3 (the attribution-window trio) together and is building now.
This memo is informational — no response required. It records what Growth is building (the silver), what will land on Platform's plate (the compute and registry), and the design decisions Growth has made. Growth will flag each silver piece on this thread as it lands, exactly as with step 1, so Platform can pick up the dependent compute then. Platform should reply only to correct a decision below; silence is fine and Growth will proceed as stated.
The ingestion/compute boundary
Step 1 set the pattern and steps 2-3 keep it: Growth owns the Meta API integration and lands everything as operational silver; Platform's mart compute reads Growth silver cross-warehouse and never calls Meta. Every Meta-sourced input named below is pulled by Growth and written to a Growth silver column or table — Platform holds no Meta credentials.
Step 2 — creative_fatigue_index
Growth is building creative_fatigue_index as a composite of two named sub-components. The mart will persist three fields per creative per week — ctr_decay_score, frequency_score, and creative_fatigue_index — so the sub-scores stay independently inspectable: Platform can retune weights without recomputing history, and a Growth analyst can audit which signal drove a rotation flag.
The compute that will land on Platform
When Growth's silver lands, Platform implements these in lib/mart/sections/growth.ts:
ctr_decay_score — windowed CTR decline for one creative.
- Baseline window: days 1–3 of the creative's run. Current window: trailing 7 days.
- Require at least 500 impressions in the baseline window for the score to be valid; below that,
ctr_decay_scoreis null, not zero. ctr_decay_score = max(0, (baseline_ctr − current_ctr) / baseline_ctr), capped at 1.0.
frequency_score — ad-set exposure saturation.
- Input: ad-set-level frequency, weekly granularity.
frequency_score = min(1.0, max(0, (frequency − onset) / span)), withonset = 3andspan = 7as starting values.onsetandspanmust be configurable mart parameters, not hardcoded, and resolvable per market — the fatigue-onset threshold for Dallas (50,000-person addressable market) is not the same as Oahu (20,000-person).
creative_fatigue_index — the weighted composite.
creative_fatigue_index = (0.6 × ctr_decay_score) + (0.4 × frequency_score).- Fallback: if
ctr_decay_scoreis null (insufficient baseline impressions — common for a new creative), the composite isfrequency_scorealone, not null. Please document this fallback in the mart so it is not read as a bug. - Rotation-flag threshold:
0.65, also a mart parameter.
Parameters Platform should store as per-market mart config: frequency_onset (3), frequency_span (7), ctr_weight (0.6), frequency_weight (0.4), ctr_baseline_min_impressions (500), rotation_threshold (0.65).
The silver Growth is building for step 2
Creative-grain spend, in a new table campaign_creative_spend_daily. Growth is extending the Meta sync to also pull ad-grain Insights (level=ad) and write one row per (campaign, date, source, creative) — creative = the Meta ad_id — carrying per-day spend, impressions, and clicks. Growth is using a separate table rather than amending campaign_spend_daily: mixing creative rows and the campaign-aggregate row in one table would risk double-counting in Platform's existing ad_impressions / ad_clicks compute. campaign_spend_daily and step 1's path stay untouched. This table is the input for ctr_decay_score.
Ad-set weekly reach/frequency. Frequency cannot be derived from creative-grain rows — reach does not dedup across ads in an ad set. Growth is adding a new table campaign_adset_frequency_weekly (campaignId, ad-set external id and name, weekStart, source, reach, impressions, frequency), populated from ad-set-level Insights (level=adset) weekly. This is the sole input for frequency_score.
What lands on Platform's plate for step 2
- The three compute fields in
lib/mart/sections/growth.tsper the formulas above. - The six per-market config parameters, stored as mart config.
- Registry (
lib/mart/registry.ts): updatecreative_fatigue_index(todaynull_emit_only, no formula), and decide whetherctr_decay_score/frequency_scoreget their own registry entries — Growth's preference is three first-class entries so the sub-scores are queryable.
Step 3 — the attribution-window trio
7-day and 30-day click windows
Growth is building the attribution-enrichment job. It sets the row-level flags attributed_within_7d_click and attributed_within_30d_click on lead_attribution (columns already exist, migration 20260519100000). Platform's growth.touchpoint dbt model then projects them and Platform writes the two compute functions.
Decision Growth has made: the qualifying conversion event is the lead intake itself — the lead_intake form-submission row, not the downstream qualified intake. The window is measured from the touch's capturedAt to the intake's createdAt. This keeps the enrichment self-contained in Growth data with no cross-domain dependency on Sales qualification events, and is well-defined for today's single-touch intakes and for multi-touch once visitor-id stitching lands. Flag on this thread if the registry intends the qualified intake — that would make the job depend on Sales events and is a different build.
view_only — a modeling note
The silver-gap memo sketched attribution_window_view_only as a row-level flag. That does not model cleanly: lead_attribution rows are click/submit touches, and Growth has no view-through events of its own. Growth is therefore building it as an aggregate from Meta's attributed-conversions data — Insights actions with action_attribution_windows (1d_view / 7d_view vs 7d_click / 28d_click) — landed in a new table campaign_attributed_conversion_daily (campaignId, date, source, attribution-window, conversion count). Platform computes the view-through share or count from that table.
Two notes for Platform: (1) the registry entry for attribution_window_view_only should expect an aggregate, not a row-level flag. (2) Growth's enrichment job computes a true 30-day calendar window for attribution_window_30d_click; Meta's API reports a 28-day window. They will diverge by design — flag if the registry intends 28-day.
The one genuine open item — the dbt model
The build order said "Growth extends the growth.touchpoint dbt model," but growth.touchpoint is Platform's dbt model and the Growth repo hosts no dbt project. Growth owns the operational columns; it cannot extend a model it does not host. Growth's working assumption: Growth delivers Platform a column spec for the new lead_attribution flags and Platform projects them into growth.touchpoint. This is the one item Growth would like Platform to confirm or correct — if Platform would rather Growth contribute the dbt SQL as a PR, point Growth at the dbt project.
What lands on Platform's plate — summary
- Step 2: three compute fields, six per-market config parameters, registry entries.
- Step 3: two click-window compute functions; the view-through compute over
campaign_attributed_conversion_daily; thegrowth.touchpointdbt projection of the new flags; registry entries for all three attribution metrics. - Corrections welcome on: the conversion-event semantics (intake vs qualified), view_only as aggregate, 30-day vs 28-day, and the dbt-model handoff.
Sequencing
Growth is building the silver in this order, flagging each on this thread as it lands: (1) creative-grain campaign_spend_daily plus the constraint amendment; (2) campaign_adset_frequency_weekly; (3) the lead_attribution click-window enrichment job; (4) the campaign_attributed_conversion_daily surface for view_only. All four share the Meta credential already in place, so none is gated on credential work.
References
- Inbound:
2026-05-22-platform-mart-classification-acks-and-compute-wirings - Build order:
2026-05-22-growth-mart-silver-gap-classification - Step 1:
2026-05-22-growth-ad-platform-ingestion-step1-landed - Parent thread:
2026-05-19-platform-mart-100-percent-deployment-per-domain-asks - Compute / registry:
platform/lib/mart/sections/growth.ts,platform/lib/mart/registry.ts - Growth schema:
campaign_spend_daily,lead_attribution(migrations20260519080000,20260519090000,20260519100000)