Skip to content

feat(schema): redesign optimization_goal as optimization_goals array#1177

Merged
bokelley merged 9 commits intomainfrom
bokelley/optimization-goal-schema
Feb 25, 2026
Merged

feat(schema): redesign optimization_goal as optimization_goals array#1177
bokelley merged 9 commits intomainfrom
bokelley/optimization-goal-schema

Conversation

@bokelley
Copy link
Contributor

@bokelley bokelley commented Feb 23, 2026

Summary

Redesigns optimization goals as a composable, multi-goal system for media buy packages.

  • optimization_goal (singular) → optimization_goals (array) on packages, with optional priority (1 = highest) for multi-goal optimization
  • Discriminated union on kind: "metric" (seller-native delivery metrics) and "event" (advertiser-tracked conversions)
  • Three orthogonal target kinds: cost_per (CPC, CPA, etc.), threshold_rate (minimum per-impression value like CTR, viewability, attention), per_ad_spend (ROAS with value_field/value_factor)
  • event_sources array of structs with per-entry event_source_id, event_type, value_field, value_factor — supports multi-source dedup by event_id, refund netting, and unit conversion
  • Expanded metric enum: clicks, views, completed_views, viewed_seconds, attention_seconds, attention_score
  • Product capability declaration: supported_optimization_strategies with target_cost_per, target_threshold_rate, target_per_ad_spend
  • No target = maximize within budget (no separate "maximize" strategy needed)
  • Documentation clarifications for dedup precedence, pricing vs. optimization, reach/frequency scope, and audio completions

Test plan

  • All 304 unit tests pass
  • 331 schemas validate (structure, cross-refs, registry consistency)
  • 13 example validations pass
  • OpenAPI spec is up to date
  • TypeScript typechecks clean
  • Mintlify link check passes (no broken links)
  • Merge conflict with main resolved (account naming)
  • Expert review by ad-tech-protocol-expert and adtech-product-expert — no schema changes recommended
  • Code review — all findings addressed

🤖 Generated with Claude Code

Replaces the singular optimization_goal object with optimization_goals
(array) on packages, using a discriminated union on kind:

- kind: "event" — optimize for advertiser-tracked conversion events
  (purchase, lead, app_install, etc.) with target cpa or roas;
  requires event_source_id
- kind: "metric" — optimize for seller-native delivery metrics
  (clicks, views, completed_views) with target cpc, cpv, or cpcv;
  no event source required

Both kinds support an optional priority field (1 = highest) for
multi-goal packages where metric goals serve as proxy signals until
event data accumulates. Adds custom_event_name to event goals for
custom event types, and extends product.supported_optimization_strategies
with target_cpc, target_cpv, target_cpcv.

Updates all docs, examples, and the OpenAPI spec accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Collaborator

@nastassiafulconis nastassiafulconis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed file: static/schemas/source/protocol/get-adcp-capabilities-response.json line 317 still references the old singular field:

"...the buyer can choose from via optimization_goal.attribution_window on packages."

This schema wasn't touched by this PR but needs to be updated to reference the new array structure (e.g., optimization_goals[].attribution_window).

bokelley and others added 3 commits February 24, 2026 10:07
- Add "type": "object" to all oneOf variants and nested target variants
- Add maximize_clicks/views/completed_views to supported_optimization_strategies
  for metric goals without a cost target
- Add valid metric/target pairings table to docs (clicks→cpc, views→cpv,
  completed_views→cpcv)
- Add "choosing a strategy" quick-reference table covering all 9 strategies
- Add pairing constraint note to target description in schema

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A metric goal without a target already implicitly maximizes that metric
within budget — no separate strategy declaration needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace redundant metric-specific target kinds (cpc/cpv/cpcv/cpa/roas)
with orthogonal target kinds that separate "what kind of target" from
"what metric/event":

- cost_per: cost per unit, works for both metric and event goals
- rate: metric rate as proportion of impressions (CTR, VCR)
- per_ad_spend: return ratio requiring value_field on event goals

Event goals with per_ad_spend targets require a value_field property
identifying which custom_data field carries the monetary value. This
enables ROAS, profit-per-spend, and other value-based optimizations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Collaborator

@nastassiafulconis nastassiafulconis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static/schemas/source/protocol/get-adcp-capabilities-response.json line 317 still references the old optimization_goal.attribution_window path

bokelley and others added 3 commits February 25, 2026 07:26
Event goals now use an event_sources array of source-type pair structs
instead of a single event_source_id/event_type. Each entry can specify
value_field and value_factor for per_ad_spend targets. The seller
deduplicates by event_id across sources — same conversion from web
analytics and an MMP counts once.

Rename rate → threshold_rate ("at least X per impression") and drop
maximum:1 constraint so the target works for both proportions (CTR,
viewability) and durations (attention seconds, time in view).

Add metrics: viewed_seconds, attention_seconds, attention_score.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Dedup value_field resolution: use array-position precedence (first matching
  entry in event_sources) instead of nondeterministic first-received
- Note completed_views applies to audio completions too
- Add "Pricing model vs. optimization goal" section distinguishing billing
  from delivery allocation
- Add "Reach and frequency" section documenting that reach goals use CPP
  pricing and frequency_cap, not optimization_goals
- Add "maximize clicks" example showing a metric goal without a target

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Kept optimization_goals (plural) with per_ad_spend target from this branch,
adopted account (nested object) naming from main.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Collaborator

@nastassiafulconis nastassiafulconis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did another pass on the latest commits — most of the earlier stuff is resolved, nice work on the cost_per/threshold_rate/per_ad_spend redesign and the event_sources array. few more things:

still-open stale refs (not in this diff so can't inline):

  1. static/schemas/source/protocol/get-adcp-capabilities-response.json line 317 — still says optimization_goal.attribution_window (flagged this before, still there)

  2. static/schemas/source/index.json line 299 — schema registry description still says "target ROAS/CPA", should reflect the new target kinds

  3. docs/media-buy/task-reference/update_media_buy.mdx line 545 — the "What Can Be Updated" checklist still says "Optimization goal (event source, event type, target ROAS/CPA)" — not in a diff hunk so can't inline this either

"type": "object",
"description": "Minimum per-impression rate for this metric. The metric defines the units: proportions for count metrics (e.g., 0.001 for 0.1% CTR, 0.70 for 70% viewability), seconds for duration metrics (e.g., 3.0 for 3s in view), or score for score metrics.",
"properties": {
"kind": { "type": "string", "const": "threshold_rate" },
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small naming thought — threshold_rate works great for proportions (CTR 0.001, viewability 0.70, VCR 0.85) but reads a bit odd for duration metrics like viewed_seconds: 3.0 or attention_seconds: 5.0 — those aren't really "rates" in the conventional sense.

not blocking on this, the docs explain it well. but have you considered just threshold or minimum_per_impression? would cascade to target_threshold / target_minimum_per_impression in supported_optimization_strategies too so it's not free. just flagging it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good instinct — "rate" is a bit misleading for duration metrics. We considered threshold and minimum_per_impression during design. Brian specifically chose threshold_rate because plain threshold sounded like a count cap ("stop after 100 clicks") rather than a per-impression minimum. The value is always per-impression — clicks/impression, seconds/impression, score/impression — so "rate" is technically correct across all metric types, even if it reads more naturally for proportions.

The cascade cost is real too (target_threshold_rate in strategies, docs, all examples). Given the docs explain the units per metric and Brian signed off on the name, keeping it as-is.

"type": "string",
"description": "Field on the event's custom_data that carries the monetary value. Required on at least one entry when target.kind is 'per_ad_spend'. The field must contain a numeric value in the buy currency. Common values: 'value', 'order_total', 'profit_margin'."
},
"value_factor": {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value_factor has no constraints — a value of 0 would silently zero out all values from this source. could be intentional as a soft-disable, but could also be a nasty footgun if someone typos it. worth adding a not: { const: 0 } or at least a description note that 0 zeroes everything out?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Added a description note: "A value of 0 zeroes out this source's value contribution (the source still counts for event dedup)."

This documents the behavior without blocking it — there's a legitimate (if rare) use case for including a source purely for dedup/counting without contributing to the value calculation. A typo of 0 would show up as $0 ROAS in reporting, which is debuggable.

"supported_optimization_strategies": {
"type": "array",
"description": "Optimization strategies this product supports when an optimization_goal is set on a package",
"description": "Optimization strategies this product supports when optimization_goals are set on a package. Target kinds: cost_per (cost per metric unit or conversion event), threshold_rate (minimum per-impression metric rate), per_ad_spend (return on ad spend — requires value_field on event sources). A goal without a target implicitly maximizes that metric or event within budget — no separate strategy declaration needed.",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the description says "a goal without a target implicitly maximizes within budget — no separate strategy needed" which is great. but what happens if a product has conversion_tracking but omits supported_optimization_strategies entirely? can buyers still set target-less goals (maximize within budget)? might be worth a one-liner clarifying the interaction.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great question. Yes — target-less goals (maximize within budget) work without any strategy declaration. Added a clarification to the description:

"When this field is omitted but conversion_tracking is present, buyers can still set target-less optimization goals (maximize within budget); they just cannot set specific cost_per, threshold_rate, or per_ad_spend targets."

…on clarity

- Document that value_factor: 0 zeroes out value contribution but source
  still participates in event dedup
- Clarify that omitting supported_optimization_strategies still allows
  target-less optimization goals (maximize within budget)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Collaborator

@nastassiafulconis nastassiafulconis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still missing updating some stale references but approving to not block before i log off!

- get-adcp-capabilities-response.json: attribution_windows description
  referenced old optimization_goal.attribution_window path
- release-notes.mdx: referenced singular optimization_goal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bokelley bokelley merged commit 5b25ccd into main Feb 25, 2026
7 checks passed
@bokelley bokelley deleted the bokelley/optimization-goal-schema branch February 25, 2026 17:56
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.

2 participants