Skip to content

feat(compliance): per_creative_attribution capability bit + scenario (closes #4725)#4731

Merged
bokelley merged 1 commit into
mainfrom
bokelley/4725-per-creative-attribution
May 18, 2026
Merged

feat(compliance): per_creative_attribution capability bit + scenario (closes #4725)#4731
bokelley merged 1 commit into
mainfrom
bokelley/4725-per-creative-attribution

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

Lands the deferred per-creative conversion attribution work from #4642 as one PR with two coupled changes:

  • Schema: new boolean media_buy.conversion_tracking.per_creative_attribution in static/schemas/source/protocol/get-adcp-capabilities-response.json. Optional; defaults to false. Declares that the seller can attribute conversions per creative within a package and surface them via media_buy_deliveries[].by_package[].by_creative[].conversions.
  • Scenario: media_buy_seller/per_creative_conversion_attribution, gated on requires_capability: { path: media_buy.conversion_tracking.per_creative_attribution, equals: true }, added to sales-non-guaranteed.requires_scenarios (alphabetic position between pending_creatives_to_start and performance_buy_flow).

What was deferred from #4642 and why

performance_buy_flow (#4642) deliberately omits per-creative assertions because honest adopters report attribution at differing granularities — social platforms per-ad, retail-media networks (Criteo, Amazon Ads) per-line, MMP-mediated mobile (post-iOS-14) per-campaign / per-ad-set, broadcast and CTV performance products per-placement. Requiring per-creative in the base CPA scenario would have failed those honest implementations. The scenario's narrative explicitly tees this follow-up up. This PR closes that gap behind a sub-capability bit so the population is correctly partitioned into has the breakdown (asserted) vs does not (graded not_applicable).

What the new scenario asserts

Five phases:

  1. setupsync_accounts + get_products with required_capabilities: ["conversion_tracking"].
  2. event_source_bindingsync_event_sources registering a website source, with upstream_traffic anti-façade.
  3. register_two_creatives_and_create_buysync_creatives pushing creative_acme_1 and creative_acme_2 (display_300x250), then create_media_buy with one package whose creative_assignments references both and whose optimization_goals[0] is event-kind with target.kind: cost_per.
  4. log_conversions_against_both_creatives — two log_event calls with distinct event_ids. core/event.json does not carry creative_id, so attributing each event back to a specific creative is the seller's internal click / view-through correlation, not the buyer's. The seller spreads simulate_delivery's conversions across the two assigned creatives per its own correlation.
  5. assert_per_creative_breakdown_in_deliverysimulate_delivery with impressions + conversions + spend, then get_media_buy_delivery, asserting:
    • field_present on media_buy_deliveries[0].totals.conversions
    • field_present on media_buy_deliveries[0].by_package[0].by_creative[0].creative_id
    • field_present on media_buy_deliveries[0].by_package[0].by_creative[0].conversions — the discriminating assertion
    • field_present on media_buy_deliveries[0].by_package[0].by_creative[1].conversions — the asymmetry check that separates honest per-creative attribution from a single-row façade

Training-agent status

No training-agent changes. The reference implementation does not declare per_creative_attribution, so the scenario grades not_applicable and CI passes. Same anti-façade pattern as event_dedup_flow (#4664) and frequency_cap_enforcement (#4640): the bit gates the scenario, the assertion targets the runtime behavior the bit commits to.

Test plan

  • npm run build:schemas — clean
  • npm run build:compliance — all 12 lints green
  • npm run typecheck — no new errors (pre-existing failures in server/src/training-agent/* and server/src/types.ts for unrelated @adcp/sdk module resolution, confirmed against origin/main)
  • CI passes on the PR
  • Scenario grades not_applicable against the training agent (no per_creative_attribution advertised)

Refs: #4725 (capability bit + scenario), #4637 (capability-claim meta), #4642 (performance_buy_flow that deferred this), #4639 (supported_targets bit for the sibling ROAS gate), #4664 (event_dedup_flow precedent), #4640 (frequency_cap_enforcement precedent).

🤖 Generated with Claude Code

…loses #4725)

Lands the deferred per-creative conversion attribution work from #4642
as a single PR with two coupled changes:

- New boolean `media_buy.conversion_tracking.per_creative_attribution`
  capability bit in get-adcp-capabilities-response.json. Optional;
  defaults to false. Declares that the seller can attribute conversions
  per creative within a package and surface them via
  media_buy_deliveries[].by_package[].by_creative[].conversions.

- New scenario media_buy_seller/per_creative_conversion_attribution,
  gated on the new bit, added to sales-non-guaranteed.requires_scenarios.
  Registers two creatives via sync_creatives, creates a media buy
  assigning both to one package with a CPA goal, logs two conversion
  events, simulates delivery, and asserts the by_creative[] breakdown
  is populated for both creatives (creative_id + conversions on rows
  0 and 1) — the second-row assertion is the asymmetry check that
  separates honest per-creative attribution from a single-row façade.

Closes the gap deliberately left by performance_buy_flow (#4642), whose
narrative defers per-creative attribution because honest adopters
report at differing granularities: social per-ad, retail-media per-line,
MMP-mediated per-campaign, broadcast/CTV per-placement. Requiring
per-creative in the base CPA scenario would have failed those honest
implementations. The bit gates the scenario; sellers that don't
advertise it grade not_applicable.

log_event's payload (core/event.json) does not carry creative_id —
attributing each event to a specific creative is the seller's internal
click / view-through correlation responsibility, not the buyer's. The
scenario logs two events with distinct event_ids and relies on the
seller's correlation to spread simulate_delivery's conversions across
the two assigned creatives in the by_creative[] breakdown.

No training-agent changes — the reference implementation does not
declare per_creative_attribution, so the scenario grades not_applicable
and CI passes. Same anti-façade pattern as event_dedup_flow (#4664)
and frequency_cap_enforcement (#4640).

Refs: #4725 (capability bit + scenario), #4637 (capability-claim meta),
#4642 (performance_buy_flow that deferred this), #4639 (supported_targets
bit for the sibling ROAS gate).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 56b9b63 into main May 18, 2026
19 checks passed
@bokelley bokelley deleted the bokelley/4725-per-creative-attribution branch May 18, 2026 10:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant