@adcp/sdk 6.8.0 → 6.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADCP_VERSION +1 -1
- package/compliance/cache/{3.0.5 → 3.0.6}/domains/media-buy/scenarios/inventory_list_targeting.yaml +5 -0
- package/compliance/cache/{3.0.5 → 3.0.6}/index.json +2 -2
- package/compliance/cache/{3.0.5 → 3.0.6}/protocols/media-buy/scenarios/inventory_list_targeting.yaml +5 -0
- package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/sales-guaranteed/index.yaml +1 -1
- package/compliance/cache/{3.0.5 → 3.0.6}/universal/storyboard-schema.yaml +15 -0
- package/dist/lib/core/AgentClient.d.ts.map +1 -1
- package/dist/lib/core/AgentClient.js +20 -0
- package/dist/lib/core/AgentClient.js.map +1 -1
- package/dist/lib/mock-server/creative-template/seed-data.d.ts +1 -1
- package/dist/lib/mock-server/creative-template/seed-data.d.ts.map +1 -1
- package/dist/lib/mock-server/creative-template/seed-data.js +53 -0
- package/dist/lib/mock-server/creative-template/seed-data.js.map +1 -1
- package/dist/lib/mock-server/creative-template/server.d.ts.map +1 -1
- package/dist/lib/mock-server/creative-template/server.js +12 -0
- package/dist/lib/mock-server/creative-template/server.js.map +1 -1
- package/dist/lib/schemas-data/3.0/a2ui/bound-value.json +1 -1
- package/dist/lib/schemas-data/3.0/a2ui/component.json +1 -1
- package/dist/lib/schemas-data/3.0/a2ui/si-catalog.json +25 -25
- package/dist/lib/schemas-data/3.0/a2ui/surface.json +2 -2
- package/dist/lib/schemas-data/3.0/a2ui/user-action.json +1 -1
- package/dist/lib/schemas-data/3.0/account/get-account-financials-request.json +5 -5
- package/dist/lib/schemas-data/3.0/account/get-account-financials-response.json +10 -10
- package/dist/lib/schemas-data/3.0/account/list-accounts-request.json +4 -4
- package/dist/lib/schemas-data/3.0/account/list-accounts-response.json +6 -6
- package/dist/lib/schemas-data/3.0/account/report-usage-request.json +5 -5
- package/dist/lib/schemas-data/3.0/account/report-usage-response.json +4 -4
- package/dist/lib/schemas-data/3.0/account/sync-accounts-request.json +9 -9
- package/dist/lib/schemas-data/3.0/account/sync-accounts-response.json +12 -12
- package/dist/lib/schemas-data/3.0/account/sync-governance-request.json +5 -5
- package/dist/lib/schemas-data/3.0/account/sync-governance-response.json +8 -8
- package/dist/lib/schemas-data/3.0/adagents.json +32 -32
- package/dist/lib/schemas-data/3.0/brand/acquire-rights-request.json +8 -8
- package/dist/lib/schemas-data/3.0/brand/acquire-rights-response.json +14 -14
- package/dist/lib/schemas-data/3.0/brand/creative-approval-request.json +4 -4
- package/dist/lib/schemas-data/3.0/brand/creative-approval-response.json +10 -10
- package/dist/lib/schemas-data/3.0/brand/get-brand-identity-request.json +3 -3
- package/dist/lib/schemas-data/3.0/brand/get-brand-identity-response.json +8 -8
- package/dist/lib/schemas-data/3.0/brand/get-rights-request.json +7 -7
- package/dist/lib/schemas-data/3.0/brand/get-rights-response.json +9 -9
- package/dist/lib/schemas-data/3.0/brand/revocation-notification.json +4 -4
- package/dist/lib/schemas-data/3.0/brand/rights-pricing-option.json +5 -5
- package/dist/lib/schemas-data/3.0/brand/rights-terms.json +3 -3
- package/dist/lib/schemas-data/3.0/brand/update-rights-request.json +4 -4
- package/dist/lib/schemas-data/3.0/brand/update-rights-response.json +9 -9
- package/dist/lib/schemas-data/3.0/brand.json +14 -14
- package/dist/lib/schemas-data/3.0/bundled/content-standards/calibrate-content-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/content-standards/calibrate-content-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/content-standards/create-content-standards-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/content-standards/create-content-standards-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/content-standards/get-content-standards-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/content-standards/get-content-standards-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/content-standards/get-media-buy-artifacts-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/content-standards/get-media-buy-artifacts-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/content-standards/list-content-standards-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/content-standards/list-content-standards-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/content-standards/update-content-standards-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/content-standards/update-content-standards-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/content-standards/validate-content-delivery-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/content-standards/validate-content-delivery-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/core/tasks-get-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/core/tasks-get-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/core/tasks-list-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/core/tasks-list-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/creative/get-creative-delivery-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/creative/get-creative-delivery-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/creative/get-creative-features-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/creative/get-creative-features-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/creative/list-creative-formats-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/creative/list-creative-formats-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/creative/list-creatives-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/creative/list-creatives-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/creative/preview-creative-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/creative/preview-creative-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/creative/sync-creatives-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/creative/sync-creatives-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/build-creative-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/build-creative-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/create-media-buy-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/create-media-buy-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/get-media-buy-delivery-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/get-media-buy-delivery-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/get-media-buys-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/get-media-buys-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/get-products-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/get-products-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/list-creative-formats-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/list-creative-formats-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/log-event-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/log-event-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/package-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/provide-performance-feedback-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/provide-performance-feedback-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/sync-audiences-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/sync-audiences-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/sync-catalogs-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/sync-catalogs-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/sync-event-sources-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/sync-event-sources-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/update-media-buy-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/media-buy/update-media-buy-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/property/create-property-list-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/property/create-property-list-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/property/delete-property-list-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/property/delete-property-list-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/property/get-property-list-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/property/get-property-list-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/property/list-property-lists-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/property/list-property-lists-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/property/update-property-list-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/property/update-property-list-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/property/validate-property-delivery-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/property/validate-property-delivery-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/protocol/get-adcp-capabilities-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/protocol/get-adcp-capabilities-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/signals/activate-signal-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/signals/activate-signal-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/signals/get-signals-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/signals/get-signals-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/sponsored-intelligence/si-get-offering-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/sponsored-intelligence/si-get-offering-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/sponsored-intelligence/si-initiate-session-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/sponsored-intelligence/si-initiate-session-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/sponsored-intelligence/si-send-message-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/sponsored-intelligence/si-send-message-response.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/sponsored-intelligence/si-terminate-session-request.json +2 -2
- package/dist/lib/schemas-data/3.0/bundled/sponsored-intelligence/si-terminate-session-response.json +2 -2
- package/dist/lib/schemas-data/3.0/collection/base-collection-source.json +3 -3
- package/dist/lib/schemas-data/3.0/collection/collection-list-changed-webhook.json +2 -2
- package/dist/lib/schemas-data/3.0/collection/collection-list-filters.json +7 -7
- package/dist/lib/schemas-data/3.0/collection/collection-list.json +5 -5
- package/dist/lib/schemas-data/3.0/collection/create-collection-list-request.json +7 -7
- package/dist/lib/schemas-data/3.0/collection/create-collection-list-response.json +4 -4
- package/dist/lib/schemas-data/3.0/collection/delete-collection-list-request.json +4 -4
- package/dist/lib/schemas-data/3.0/collection/delete-collection-list-response.json +3 -3
- package/dist/lib/schemas-data/3.0/collection/get-collection-list-request.json +4 -4
- package/dist/lib/schemas-data/3.0/collection/get-collection-list-response.json +10 -10
- package/dist/lib/schemas-data/3.0/collection/list-collection-lists-request.json +5 -5
- package/dist/lib/schemas-data/3.0/collection/list-collection-lists-response.json +5 -5
- package/dist/lib/schemas-data/3.0/collection/update-collection-list-request.json +7 -7
- package/dist/lib/schemas-data/3.0/collection/update-collection-list-response.json +4 -4
- package/dist/lib/schemas-data/3.0/content-standards/artifact-webhook-payload.json +3 -3
- package/dist/lib/schemas-data/3.0/content-standards/artifact.json +7 -7
- package/dist/lib/schemas-data/3.0/content-standards/calibrate-content-request.json +4 -4
- package/dist/lib/schemas-data/3.0/content-standards/calibrate-content-response.json +8 -8
- package/dist/lib/schemas-data/3.0/content-standards/content-standards.json +7 -7
- package/dist/lib/schemas-data/3.0/content-standards/create-content-standards-request.json +7 -7
- package/dist/lib/schemas-data/3.0/content-standards/create-content-standards-response.json +6 -6
- package/dist/lib/schemas-data/3.0/content-standards/get-content-standards-request.json +3 -3
- package/dist/lib/schemas-data/3.0/content-standards/get-content-standards-response.json +7 -7
- package/dist/lib/schemas-data/3.0/content-standards/get-media-buy-artifacts-request.json +4 -4
- package/dist/lib/schemas-data/3.0/content-standards/get-media-buy-artifacts-response.json +8 -8
- package/dist/lib/schemas-data/3.0/content-standards/list-content-standards-request.json +5 -5
- package/dist/lib/schemas-data/3.0/content-standards/list-content-standards-response.json +8 -8
- package/dist/lib/schemas-data/3.0/content-standards/update-content-standards-request.json +7 -7
- package/dist/lib/schemas-data/3.0/content-standards/update-content-standards-response.json +6 -6
- package/dist/lib/schemas-data/3.0/content-standards/validate-content-delivery-request.json +4 -4
- package/dist/lib/schemas-data/3.0/content-standards/validate-content-delivery-response.json +8 -8
- package/dist/lib/schemas-data/3.0/core/account-ref.json +2 -2
- package/dist/lib/schemas-data/3.0/core/account.json +9 -9
- package/dist/lib/schemas-data/3.0/core/activation-key.json +1 -1
- package/dist/lib/schemas-data/3.0/core/ad-inventory-config.json +1 -1
- package/dist/lib/schemas-data/3.0/core/agent-encryption-key.json +1 -1
- package/dist/lib/schemas-data/3.0/core/agent-signing-key.json +1 -1
- package/dist/lib/schemas-data/3.0/core/app-item.json +4 -4
- package/dist/lib/schemas-data/3.0/core/assets/asset-union.json +15 -15
- package/dist/lib/schemas-data/3.0/core/assets/audio-asset.json +3 -3
- package/dist/lib/schemas-data/3.0/core/assets/brief-asset.json +2 -2
- package/dist/lib/schemas-data/3.0/core/assets/catalog-asset.json +2 -2
- package/dist/lib/schemas-data/3.0/core/assets/css-asset.json +2 -2
- package/dist/lib/schemas-data/3.0/core/assets/daast-asset.json +4 -4
- package/dist/lib/schemas-data/3.0/core/assets/html-asset.json +2 -2
- package/dist/lib/schemas-data/3.0/core/assets/image-asset.json +2 -2
- package/dist/lib/schemas-data/3.0/core/assets/javascript-asset.json +3 -3
- package/dist/lib/schemas-data/3.0/core/assets/markdown-asset.json +2 -2
- package/dist/lib/schemas-data/3.0/core/assets/text-asset.json +2 -2
- package/dist/lib/schemas-data/3.0/core/assets/url-asset.json +3 -3
- package/dist/lib/schemas-data/3.0/core/assets/vast-asset.json +4 -4
- package/dist/lib/schemas-data/3.0/core/assets/video-asset.json +7 -7
- package/dist/lib/schemas-data/3.0/core/assets/webhook-asset.json +7 -7
- package/dist/lib/schemas-data/3.0/core/async-response-data.json +25 -25
- package/dist/lib/schemas-data/3.0/core/attribution-window.json +4 -4
- package/dist/lib/schemas-data/3.0/core/audience-member.json +3 -3
- package/dist/lib/schemas-data/3.0/core/audience-selector.json +4 -4
- package/dist/lib/schemas-data/3.0/core/brand-id.json +1 -1
- package/dist/lib/schemas-data/3.0/core/brand-ref.json +2 -2
- package/dist/lib/schemas-data/3.0/core/business-entity.json +2 -2
- package/dist/lib/schemas-data/3.0/core/cancellation-policy.json +2 -2
- package/dist/lib/schemas-data/3.0/core/catalog-field-mapping.json +2 -2
- package/dist/lib/schemas-data/3.0/core/catalog.json +7 -7
- package/dist/lib/schemas-data/3.0/core/catchment.json +5 -5
- package/dist/lib/schemas-data/3.0/core/collection-distribution.json +2 -2
- package/dist/lib/schemas-data/3.0/core/collection-list-ref.json +1 -1
- package/dist/lib/schemas-data/3.0/core/collection-selector.json +1 -1
- package/dist/lib/schemas-data/3.0/core/collection.json +13 -13
- package/dist/lib/schemas-data/3.0/core/content-rating.json +2 -2
- package/dist/lib/schemas-data/3.0/core/context.json +1 -1
- package/dist/lib/schemas-data/3.0/core/creative-asset.json +6 -6
- package/dist/lib/schemas-data/3.0/core/creative-assignment.json +1 -1
- package/dist/lib/schemas-data/3.0/core/creative-brief.json +4 -4
- package/dist/lib/schemas-data/3.0/core/creative-consumption.json +1 -1
- package/dist/lib/schemas-data/3.0/core/creative-filters.json +4 -4
- package/dist/lib/schemas-data/3.0/core/creative-item.json +1 -1
- package/dist/lib/schemas-data/3.0/core/creative-manifest.json +7 -7
- package/dist/lib/schemas-data/3.0/core/creative-policy.json +3 -3
- package/dist/lib/schemas-data/3.0/core/creative-variable.json +1 -1
- package/dist/lib/schemas-data/3.0/core/creative-variant.json +5 -5
- package/dist/lib/schemas-data/3.0/core/data-provider-signal-selector.json +1 -1
- package/dist/lib/schemas-data/3.0/core/date-range.json +1 -1
- package/dist/lib/schemas-data/3.0/core/datetime-range.json +1 -1
- package/dist/lib/schemas-data/3.0/core/daypart-target.json +2 -2
- package/dist/lib/schemas-data/3.0/core/deadline-policy.json +1 -1
- package/dist/lib/schemas-data/3.0/core/delivery-forecast.json +7 -7
- package/dist/lib/schemas-data/3.0/core/delivery-metrics.json +5 -5
- package/dist/lib/schemas-data/3.0/core/deployment.json +3 -3
- package/dist/lib/schemas-data/3.0/core/destination-item.json +4 -4
- package/dist/lib/schemas-data/3.0/core/destination.json +1 -1
- package/dist/lib/schemas-data/3.0/core/diagnostic-issue.json +1 -1
- package/dist/lib/schemas-data/3.0/core/duration.json +1 -1
- package/dist/lib/schemas-data/3.0/core/education-item.json +4 -4
- package/dist/lib/schemas-data/3.0/core/error.json +1 -1
- package/dist/lib/schemas-data/3.0/core/event-custom-data.json +2 -2
- package/dist/lib/schemas-data/3.0/core/event-source-health.json +3 -3
- package/dist/lib/schemas-data/3.0/core/event.json +6 -6
- package/dist/lib/schemas-data/3.0/core/ext.json +1 -1
- package/dist/lib/schemas-data/3.0/core/feature-requirement.json +1 -1
- package/dist/lib/schemas-data/3.0/core/flight-item.json +4 -4
- package/dist/lib/schemas-data/3.0/core/forecast-point.json +18 -18
- package/dist/lib/schemas-data/3.0/core/forecast-range.json +1 -1
- package/dist/lib/schemas-data/3.0/core/format-id.json +1 -1
- package/dist/lib/schemas-data/3.0/core/format.json +42 -42
- package/dist/lib/schemas-data/3.0/core/frequency-cap.json +4 -4
- package/dist/lib/schemas-data/3.0/core/generation-credential.json +3 -3
- package/dist/lib/schemas-data/3.0/core/geo-breakdown-support.json +3 -3
- package/dist/lib/schemas-data/3.0/core/hotel-item.json +4 -4
- package/dist/lib/schemas-data/3.0/core/identifier.json +2 -2
- package/dist/lib/schemas-data/3.0/core/industry-identifier.json +2 -2
- package/dist/lib/schemas-data/3.0/core/insertion-order.json +1 -1
- package/dist/lib/schemas-data/3.0/core/installment-deadlines.json +2 -2
- package/dist/lib/schemas-data/3.0/core/installment.json +9 -9
- package/dist/lib/schemas-data/3.0/core/job-item.json +3 -3
- package/dist/lib/schemas-data/3.0/core/limited-series.json +1 -1
- package/dist/lib/schemas-data/3.0/core/material-deadline.json +1 -1
- package/dist/lib/schemas-data/3.0/core/mcp-webhook-payload.json +5 -5
- package/dist/lib/schemas-data/3.0/core/measurement-readiness.json +5 -5
- package/dist/lib/schemas-data/3.0/core/measurement-terms.json +3 -3
- package/dist/lib/schemas-data/3.0/core/measurement-window.json +1 -1
- package/dist/lib/schemas-data/3.0/core/media-buy-features.json +1 -1
- package/dist/lib/schemas-data/3.0/core/media-buy.json +7 -7
- package/dist/lib/schemas-data/3.0/core/offering-asset-group.json +15 -15
- package/dist/lib/schemas-data/3.0/core/offering.json +5 -5
- package/dist/lib/schemas-data/3.0/core/optimization-goal.json +6 -6
- package/dist/lib/schemas-data/3.0/core/outcome-measurement.json +2 -2
- package/dist/lib/schemas-data/3.0/core/overlay.json +1 -1
- package/dist/lib/schemas-data/3.0/core/package.json +14 -14
- package/dist/lib/schemas-data/3.0/core/pagination-request.json +1 -1
- package/dist/lib/schemas-data/3.0/core/pagination-response.json +1 -1
- package/dist/lib/schemas-data/3.0/core/performance-feedback.json +3 -3
- package/dist/lib/schemas-data/3.0/core/performance-standard.json +4 -4
- package/dist/lib/schemas-data/3.0/core/placement-definition.json +5 -5
- package/dist/lib/schemas-data/3.0/core/placement.json +2 -2
- package/dist/lib/schemas-data/3.0/core/planned-delivery.json +5 -5
- package/dist/lib/schemas-data/3.0/core/price.json +1 -1
- package/dist/lib/schemas-data/3.0/core/pricing-option.json +10 -10
- package/dist/lib/schemas-data/3.0/core/product-allocation.json +4 -4
- package/dist/lib/schemas-data/3.0/core/product-filters.json +16 -16
- package/dist/lib/schemas-data/3.0/core/product.json +28 -28
- package/dist/lib/schemas-data/3.0/core/property-id.json +1 -1
- package/dist/lib/schemas-data/3.0/core/property-list-ref.json +1 -1
- package/dist/lib/schemas-data/3.0/core/property-tag.json +1 -1
- package/dist/lib/schemas-data/3.0/core/property.json +6 -6
- package/dist/lib/schemas-data/3.0/core/proposal.json +6 -6
- package/dist/lib/schemas-data/3.0/core/protocol-envelope.json +3 -3
- package/dist/lib/schemas-data/3.0/core/provenance.json +6 -6
- package/dist/lib/schemas-data/3.0/core/publisher-property-selector.json +3 -3
- package/dist/lib/schemas-data/3.0/core/push-notification-config.json +2 -2
- package/dist/lib/schemas-data/3.0/core/real-estate-item.json +4 -4
- package/dist/lib/schemas-data/3.0/core/reference-asset.json +1 -1
- package/dist/lib/schemas-data/3.0/core/reporting-capabilities.json +5 -5
- package/dist/lib/schemas-data/3.0/core/reporting-webhook.json +3 -3
- package/dist/lib/schemas-data/3.0/core/requirements/asset-requirements.json +13 -13
- package/dist/lib/schemas-data/3.0/core/requirements/audio-asset-requirements.json +1 -1
- package/dist/lib/schemas-data/3.0/core/requirements/catalog-field-binding.json +4 -4
- package/dist/lib/schemas-data/3.0/core/requirements/catalog-requirements.json +5 -5
- package/dist/lib/schemas-data/3.0/core/requirements/css-asset-requirements.json +1 -1
- package/dist/lib/schemas-data/3.0/core/requirements/daast-asset-requirements.json +1 -1
- package/dist/lib/schemas-data/3.0/core/requirements/html-asset-requirements.json +1 -1
- package/dist/lib/schemas-data/3.0/core/requirements/image-asset-requirements.json +2 -2
- package/dist/lib/schemas-data/3.0/core/requirements/javascript-asset-requirements.json +1 -1
- package/dist/lib/schemas-data/3.0/core/requirements/markdown-asset-requirements.json +1 -1
- package/dist/lib/schemas-data/3.0/core/requirements/offering-asset-constraint.json +4 -4
- package/dist/lib/schemas-data/3.0/core/requirements/text-asset-requirements.json +1 -1
- package/dist/lib/schemas-data/3.0/core/requirements/url-asset-requirements.json +1 -1
- package/dist/lib/schemas-data/3.0/core/requirements/vast-asset-requirements.json +1 -1
- package/dist/lib/schemas-data/3.0/core/requirements/video-asset-requirements.json +6 -6
- package/dist/lib/schemas-data/3.0/core/requirements/webhook-asset-requirements.json +1 -1
- package/dist/lib/schemas-data/3.0/core/response.json +1 -1
- package/dist/lib/schemas-data/3.0/core/rights-constraint.json +4 -4
- package/dist/lib/schemas-data/3.0/core/seller-agent-ref.json +1 -1
- package/dist/lib/schemas-data/3.0/core/signal-definition.json +3 -3
- package/dist/lib/schemas-data/3.0/core/signal-filters.json +2 -2
- package/dist/lib/schemas-data/3.0/core/signal-id.json +1 -1
- package/dist/lib/schemas-data/3.0/core/signal-pricing-option.json +2 -2
- package/dist/lib/schemas-data/3.0/core/signal-pricing.json +6 -6
- package/dist/lib/schemas-data/3.0/core/signal-targeting.json +4 -4
- package/dist/lib/schemas-data/3.0/core/special.json +2 -2
- package/dist/lib/schemas-data/3.0/core/start-timing.json +1 -1
- package/dist/lib/schemas-data/3.0/core/store-item.json +3 -3
- package/dist/lib/schemas-data/3.0/core/talent.json +2 -2
- package/dist/lib/schemas-data/3.0/core/targeting.json +20 -20
- package/dist/lib/schemas-data/3.0/core/tasks-get-request.json +3 -3
- package/dist/lib/schemas-data/3.0/core/tasks-get-response.json +7 -7
- package/dist/lib/schemas-data/3.0/core/tasks-list-request.json +11 -11
- package/dist/lib/schemas-data/3.0/core/tasks-list-response.json +6 -6
- package/dist/lib/schemas-data/3.0/core/user-match.json +3 -3
- package/dist/lib/schemas-data/3.0/core/vehicle-item.json +4 -4
- package/dist/lib/schemas-data/3.0/core/vendor-pricing-option.json +2 -2
- package/dist/lib/schemas-data/3.0/core/x-entity-types.json +1 -1
- package/dist/lib/schemas-data/3.0/creative/asset-types/index.json +5 -5
- package/dist/lib/schemas-data/3.0/creative/creative-feature-result.json +2 -2
- package/dist/lib/schemas-data/3.0/creative/get-creative-delivery-request.json +5 -5
- package/dist/lib/schemas-data/3.0/creative/get-creative-delivery-response.json +7 -7
- package/dist/lib/schemas-data/3.0/creative/get-creative-features-request.json +5 -5
- package/dist/lib/schemas-data/3.0/creative/get-creative-features-response.json +8 -8
- package/dist/lib/schemas-data/3.0/creative/list-creative-formats-request.json +11 -11
- package/dist/lib/schemas-data/3.0/creative/list-creative-formats-response.json +7 -7
- package/dist/lib/schemas-data/3.0/creative/list-creatives-request.json +8 -8
- package/dist/lib/schemas-data/3.0/creative/list-creatives-response.json +14 -14
- package/dist/lib/schemas-data/3.0/creative/preview-creative-request.json +11 -11
- package/dist/lib/schemas-data/3.0/creative/preview-creative-response.json +12 -12
- package/dist/lib/schemas-data/3.0/creative/preview-render.json +1 -1
- package/dist/lib/schemas-data/3.0/creative/sync-creatives-async-response-input-required.json +3 -3
- package/dist/lib/schemas-data/3.0/creative/sync-creatives-async-response-submitted.json +3 -3
- package/dist/lib/schemas-data/3.0/creative/sync-creatives-async-response-working.json +3 -3
- package/dist/lib/schemas-data/3.0/creative/sync-creatives-request.json +7 -7
- package/dist/lib/schemas-data/3.0/creative/sync-creatives-response.json +13 -13
- package/dist/lib/schemas-data/3.0/enums/account-scope.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/account-status.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/action-source.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/adcp-protocol.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/adjustment-kind.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/advertiser-industry.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/age-verification-method.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/assessment-status.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/asset-content-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/attribution-model.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/audience-source.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/audience-status.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/audio-channel-layout.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/auth-scheme.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/available-metric.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/billing-party.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/binary-verdict.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/brand-agent-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/canceled-by.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/catalog-action.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/catalog-item-status.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/catalog-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/channels.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/cloud-storage-protocol.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/co-branding-requirement.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/collection-cadence.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/collection-kind.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/collection-relationship.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/collection-status.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/consent-basis.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/content-id-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/content-rating-system.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/creative-action.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/creative-agent-capability.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/creative-approval-status.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/creative-identifier-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/creative-quality.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/creative-sort-field.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/creative-status.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/daast-tracking-event.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/daast-version.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/day-of-week.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/delegation-authority.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/delivery-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/demographic-system.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/derivative-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/device-platform.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/device-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/digital-source-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/dimension-unit.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/disclosure-persistence.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/disclosure-position.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/distance-unit.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/distribution-identifier-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/error-code.json +3 -3
- package/dist/lib/schemas-data/3.0/enums/escalation-severity.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/event-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/exclusivity.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/feature-check-status.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/feed-format.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/feedback-source.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/forecast-method.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/forecast-range-unit.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/forecastable-metric.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/format-id-parameter.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/frame-rate-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/frequency-cap-scope.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/genre-taxonomy.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/geo-level.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/gop-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/governance-decision.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/governance-domain.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/governance-mode.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/governance-phase.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/history-entry-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/http-method.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/identifier-types.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/installment-status.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/javascript-module-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/landing-page-requirement.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/makegood-remedy.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/markdown-flavor.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/match-id-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/match-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/media-buy-status.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/media-buy-valid-action.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/metric-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/metro-system.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/moov-atom-position.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/notification-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/outcome-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/pacing.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/payment-terms.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/performance-standard-metric.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/policy-category.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/policy-enforcement.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/postal-system.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/preview-output-format.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/pricing-model.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/production-quality.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/property-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/proposal-status.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/publisher-identifier-types.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/purchase-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/reach-unit.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/reporting-frequency.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/response-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/restricted-attribute.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/right-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/right-use.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/rights-billing-period.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/scan-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/si-session-status.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/signal-catalog-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/signal-source.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/signal-value-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/snapshot-unavailable-reason.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/sort-direction.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/sort-metric.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/special-category.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/specialism.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/talent-role.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/task-status.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/task-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/transport-mode.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/travel-time-unit.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/uid-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/universal-macro.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/update-frequency.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/url-asset-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/validation-mode.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/vast-tracking-event.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/vast-version.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/viewability-standard.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/wcag-level.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/webhook-response-type.json +1 -1
- package/dist/lib/schemas-data/3.0/enums/webhook-security-method.json +1 -1
- package/dist/lib/schemas-data/3.0/error-details/account-setup-required.json +1 -1
- package/dist/lib/schemas-data/3.0/error-details/audience-too-small.json +1 -1
- package/dist/lib/schemas-data/3.0/error-details/budget-too-low.json +1 -1
- package/dist/lib/schemas-data/3.0/error-details/conflict.json +1 -1
- package/dist/lib/schemas-data/3.0/error-details/creative-rejected.json +1 -1
- package/dist/lib/schemas-data/3.0/error-details/policy-violation.json +1 -1
- package/dist/lib/schemas-data/3.0/error-details/rate-limited.json +1 -1
- package/dist/lib/schemas-data/3.0/error-details/vendor-error-codes.json +1 -1
- package/dist/lib/schemas-data/3.0/extensions/extension-meta.json +1 -1
- package/dist/lib/schemas-data/3.0/extensions/index.json +2 -2
- package/dist/lib/schemas-data/3.0/governance/attribute-definition.json +1 -1
- package/dist/lib/schemas-data/3.0/governance/audience-constraints.json +3 -3
- package/dist/lib/schemas-data/3.0/governance/check-governance-request.json +7 -7
- package/dist/lib/schemas-data/3.0/governance/check-governance-response.json +6 -6
- package/dist/lib/schemas-data/3.0/governance/get-plan-audit-logs-request.json +4 -4
- package/dist/lib/schemas-data/3.0/governance/get-plan-audit-logs-response.json +9 -9
- package/dist/lib/schemas-data/3.0/governance/policy-category-definition.json +2 -2
- package/dist/lib/schemas-data/3.0/governance/policy-entry.json +6 -6
- package/dist/lib/schemas-data/3.0/governance/policy-ref.json +1 -1
- package/dist/lib/schemas-data/3.0/governance/report-plan-outcome-request.json +6 -6
- package/dist/lib/schemas-data/3.0/governance/report-plan-outcome-response.json +4 -4
- package/dist/lib/schemas-data/3.0/governance/sync-plans-request.json +13 -13
- package/dist/lib/schemas-data/3.0/governance/sync-plans-response.json +4 -4
- package/dist/lib/schemas-data/3.0/index.json +364 -364
- package/dist/lib/schemas-data/3.0/manifest.json +5 -5
- package/dist/lib/schemas-data/3.0/manifest.schema.json +1 -1
- package/dist/lib/schemas-data/3.0/media-buy/build-creative-async-response-input-required.json +4 -4
- package/dist/lib/schemas-data/3.0/media-buy/build-creative-async-response-submitted.json +3 -3
- package/dist/lib/schemas-data/3.0/media-buy/build-creative-async-response-working.json +3 -3
- package/dist/lib/schemas-data/3.0/media-buy/build-creative-request.json +11 -11
- package/dist/lib/schemas-data/3.0/media-buy/build-creative-response.json +17 -17
- package/dist/lib/schemas-data/3.0/media-buy/create-media-buy-async-response-input-required.json +4 -4
- package/dist/lib/schemas-data/3.0/media-buy/create-media-buy-async-response-submitted.json +3 -3
- package/dist/lib/schemas-data/3.0/media-buy/create-media-buy-async-response-working.json +3 -3
- package/dist/lib/schemas-data/3.0/media-buy/create-media-buy-request.json +12 -12
- package/dist/lib/schemas-data/3.0/media-buy/create-media-buy-response.json +15 -15
- package/dist/lib/schemas-data/3.0/media-buy/get-media-buy-delivery-request.json +17 -17
- package/dist/lib/schemas-data/3.0/media-buy/get-media-buy-delivery-response.json +24 -24
- package/dist/lib/schemas-data/3.0/media-buy/get-media-buys-request.json +7 -7
- package/dist/lib/schemas-data/3.0/media-buy/get-media-buys-response.json +19 -19
- package/dist/lib/schemas-data/3.0/media-buy/get-products-async-response-input-required.json +4 -4
- package/dist/lib/schemas-data/3.0/media-buy/get-products-async-response-submitted.json +3 -3
- package/dist/lib/schemas-data/3.0/media-buy/get-products-async-response-working.json +3 -3
- package/dist/lib/schemas-data/3.0/media-buy/get-products-request.json +11 -11
- package/dist/lib/schemas-data/3.0/media-buy/get-products-response.json +8 -8
- package/dist/lib/schemas-data/3.0/media-buy/list-creative-formats-request.json +11 -11
- package/dist/lib/schemas-data/3.0/media-buy/list-creative-formats-response.json +7 -7
- package/dist/lib/schemas-data/3.0/media-buy/log-event-request.json +4 -4
- package/dist/lib/schemas-data/3.0/media-buy/log-event-response.json +6 -6
- package/dist/lib/schemas-data/3.0/media-buy/package-request.json +12 -12
- package/dist/lib/schemas-data/3.0/media-buy/package-update.json +13 -13
- package/dist/lib/schemas-data/3.0/media-buy/provide-performance-feedback-request.json +6 -6
- package/dist/lib/schemas-data/3.0/media-buy/provide-performance-feedback-response.json +6 -6
- package/dist/lib/schemas-data/3.0/media-buy/sync-audiences-request.json +7 -7
- package/dist/lib/schemas-data/3.0/media-buy/sync-audiences-response.json +9 -9
- package/dist/lib/schemas-data/3.0/media-buy/sync-catalogs-async-response-input-required.json +3 -3
- package/dist/lib/schemas-data/3.0/media-buy/sync-catalogs-async-response-submitted.json +3 -3
- package/dist/lib/schemas-data/3.0/media-buy/sync-catalogs-async-response-working.json +3 -3
- package/dist/lib/schemas-data/3.0/media-buy/sync-catalogs-request.json +7 -7
- package/dist/lib/schemas-data/3.0/media-buy/sync-catalogs-response.json +9 -9
- package/dist/lib/schemas-data/3.0/media-buy/sync-event-sources-request.json +5 -5
- package/dist/lib/schemas-data/3.0/media-buy/sync-event-sources-response.json +10 -10
- package/dist/lib/schemas-data/3.0/media-buy/update-media-buy-async-response-input-required.json +3 -3
- package/dist/lib/schemas-data/3.0/media-buy/update-media-buy-async-response-submitted.json +3 -3
- package/dist/lib/schemas-data/3.0/media-buy/update-media-buy-async-response-working.json +3 -3
- package/dist/lib/schemas-data/3.0/media-buy/update-media-buy-request.json +10 -10
- package/dist/lib/schemas-data/3.0/media-buy/update-media-buy-response.json +10 -10
- package/dist/lib/schemas-data/3.0/pricing-options/cpa-option.json +4 -4
- package/dist/lib/schemas-data/3.0/pricing-options/cpc-option.json +4 -4
- package/dist/lib/schemas-data/3.0/pricing-options/cpcv-option.json +4 -4
- package/dist/lib/schemas-data/3.0/pricing-options/cpm-option.json +4 -4
- package/dist/lib/schemas-data/3.0/pricing-options/cpp-option.json +5 -5
- package/dist/lib/schemas-data/3.0/pricing-options/cpv-option.json +4 -4
- package/dist/lib/schemas-data/3.0/pricing-options/flat-rate-option.json +4 -4
- package/dist/lib/schemas-data/3.0/pricing-options/price-breakdown.json +2 -2
- package/dist/lib/schemas-data/3.0/pricing-options/price-guidance.json +1 -1
- package/dist/lib/schemas-data/3.0/pricing-options/time-option.json +4 -4
- package/dist/lib/schemas-data/3.0/pricing-options/vcpm-option.json +4 -4
- package/dist/lib/schemas-data/3.0/property/authorization-result.json +1 -1
- package/dist/lib/schemas-data/3.0/property/base-property-source.json +4 -4
- package/dist/lib/schemas-data/3.0/property/create-property-list-request.json +7 -7
- package/dist/lib/schemas-data/3.0/property/create-property-list-response.json +4 -4
- package/dist/lib/schemas-data/3.0/property/delete-property-list-request.json +4 -4
- package/dist/lib/schemas-data/3.0/property/delete-property-list-response.json +3 -3
- package/dist/lib/schemas-data/3.0/property/delivery-record.json +3 -3
- package/dist/lib/schemas-data/3.0/property/get-property-list-request.json +4 -4
- package/dist/lib/schemas-data/3.0/property/get-property-list-response.json +7 -7
- package/dist/lib/schemas-data/3.0/property/list-property-lists-request.json +5 -5
- package/dist/lib/schemas-data/3.0/property/list-property-lists-response.json +5 -5
- package/dist/lib/schemas-data/3.0/property/property-error.json +2 -2
- package/dist/lib/schemas-data/3.0/property/property-feature-definition.json +2 -2
- package/dist/lib/schemas-data/3.0/property/property-feature-result.json +4 -4
- package/dist/lib/schemas-data/3.0/property/property-feature-value.json +2 -2
- package/dist/lib/schemas-data/3.0/property/property-feature.json +1 -1
- package/dist/lib/schemas-data/3.0/property/property-list-changed-webhook.json +2 -2
- package/dist/lib/schemas-data/3.0/property/property-list-filters.json +5 -5
- package/dist/lib/schemas-data/3.0/property/property-list.json +6 -6
- package/dist/lib/schemas-data/3.0/property/update-property-list-request.json +7 -7
- package/dist/lib/schemas-data/3.0/property/update-property-list-response.json +4 -4
- package/dist/lib/schemas-data/3.0/property/validate-property-delivery-request.json +5 -5
- package/dist/lib/schemas-data/3.0/property/validate-property-delivery-response.json +4 -4
- package/dist/lib/schemas-data/3.0/property/validation-result.json +5 -5
- package/dist/lib/schemas-data/3.0/protocol/get-adcp-capabilities-request.json +3 -3
- package/dist/lib/schemas-data/3.0/protocol/get-adcp-capabilities-response.json +25 -25
- package/dist/lib/schemas-data/3.0/signals/activate-signal-request.json +5 -5
- package/dist/lib/schemas-data/3.0/signals/activate-signal-response.json +7 -7
- package/dist/lib/schemas-data/3.0/signals/get-signals-request.json +8 -8
- package/dist/lib/schemas-data/3.0/signals/get-signals-response.json +10 -10
- package/dist/lib/schemas-data/3.0/sponsored-intelligence/si-capabilities.json +1 -1
- package/dist/lib/schemas-data/3.0/sponsored-intelligence/si-get-offering-request.json +3 -3
- package/dist/lib/schemas-data/3.0/sponsored-intelligence/si-get-offering-response.json +4 -4
- package/dist/lib/schemas-data/3.0/sponsored-intelligence/si-identity.json +1 -1
- package/dist/lib/schemas-data/3.0/sponsored-intelligence/si-initiate-session-request.json +5 -5
- package/dist/lib/schemas-data/3.0/sponsored-intelligence/si-initiate-session-response.json +7 -7
- package/dist/lib/schemas-data/3.0/sponsored-intelligence/si-send-message-request.json +3 -3
- package/dist/lib/schemas-data/3.0/sponsored-intelligence/si-send-message-response.json +7 -7
- package/dist/lib/schemas-data/3.0/sponsored-intelligence/si-terminate-session-request.json +3 -3
- package/dist/lib/schemas-data/3.0/sponsored-intelligence/si-terminate-session-response.json +5 -5
- package/dist/lib/schemas-data/3.0/sponsored-intelligence/si-ui-element.json +1 -1
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/server/decisioning/account.d.ts +14 -0
- package/dist/lib/server/decisioning/account.d.ts.map +1 -1
- package/dist/lib/server/decisioning/account.js.map +1 -1
- package/dist/lib/server/decisioning/runtime/entity-hydration.generated.js +1 -1
- package/dist/lib/server/decisioning/runtime/from-platform.d.ts.map +1 -1
- package/dist/lib/server/decisioning/runtime/from-platform.js +36 -11
- package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -1
- package/dist/lib/server/index.d.ts +1 -0
- package/dist/lib/server/index.d.ts.map +1 -1
- package/dist/lib/server/index.js +7 -1
- package/dist/lib/server/index.js.map +1 -1
- package/dist/lib/server/socket-mode/conformance-client.d.ts +83 -0
- package/dist/lib/server/socket-mode/conformance-client.d.ts.map +1 -0
- package/dist/lib/server/socket-mode/conformance-client.js +117 -0
- package/dist/lib/server/socket-mode/conformance-client.js.map +1 -0
- package/dist/lib/server/socket-mode/index.d.ts +15 -0
- package/dist/lib/server/socket-mode/index.d.ts.map +1 -0
- package/dist/lib/server/socket-mode/index.js +20 -0
- package/dist/lib/server/socket-mode/index.js.map +1 -0
- package/dist/lib/server/socket-mode/ws-transport.d.ts +26 -0
- package/dist/lib/server/socket-mode/ws-transport.d.ts.map +1 -0
- package/dist/lib/server/socket-mode/ws-transport.js +93 -0
- package/dist/lib/server/socket-mode/ws-transport.js.map +1 -0
- package/dist/lib/testing/comply-controller.d.ts +42 -1
- package/dist/lib/testing/comply-controller.d.ts.map +1 -1
- package/dist/lib/testing/comply-controller.js +13 -1
- package/dist/lib/testing/comply-controller.js.map +1 -1
- package/dist/lib/testing/storyboard/request-builder.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/request-builder.js +63 -40
- package/dist/lib/testing/storyboard/request-builder.js.map +1 -1
- package/dist/lib/types/core.generated.d.ts +54 -14
- package/dist/lib/types/core.generated.d.ts.map +1 -1
- package/dist/lib/types/core.generated.js +2 -2
- package/dist/lib/types/error-codes.d.ts +2 -2
- package/dist/lib/types/index.d.ts +1 -1
- package/dist/lib/types/index.d.ts.map +1 -1
- package/dist/lib/types/index.js.map +1 -1
- package/dist/lib/types/manifest.generated.d.ts +5 -5
- package/dist/lib/types/manifest.generated.js +3 -3
- package/dist/lib/types/manifest.generated.js.map +1 -1
- package/dist/lib/types/schemas.generated.d.ts +465 -381
- package/dist/lib/types/schemas.generated.d.ts.map +1 -1
- package/dist/lib/types/schemas.generated.js +171 -143
- package/dist/lib/types/schemas.generated.js.map +1 -1
- package/dist/lib/types/tools.generated.d.ts +42 -14
- package/dist/lib/types/tools.generated.d.ts.map +1 -1
- package/dist/lib/version.d.ts +7 -7
- package/dist/lib/version.d.ts.map +1 -1
- package/dist/lib/version.js +6 -5
- package/dist/lib/version.js.map +1 -1
- package/docs/llms.txt +4 -3
- package/examples/hello_creative_adapter_ad_server.ts +22 -3
- package/examples/hello_creative_adapter_template.ts +61 -6
- package/examples/hello_seller_adapter_guaranteed.ts +85 -10
- package/examples/hello_seller_adapter_multi_tenant.ts +39 -5
- package/examples/hello_seller_adapter_non_guaranteed.ts +54 -3
- package/examples/hello_seller_adapter_social.ts +1 -1
- package/examples/hello_signals_adapter_marketplace.ts +34 -29
- package/package.json +5 -4
- package/skills/SHAPE-GOTCHAS.md +18 -0
- package/skills/build-brand-rights-agent/SKILL.md +61 -597
- package/skills/build-creative-agent/SKILL.md +69 -750
- package/skills/build-generative-seller-agent/SKILL.md +43 -583
- package/skills/build-governance-agent/SKILL.md +63 -959
- package/skills/build-holdco-agent/SKILL.md +8 -0
- package/skills/build-retail-media-agent/SKILL.md +46 -480
- package/skills/build-seller-agent/SKILL.md +53 -1797
- package/skills/build-si-agent/SKILL.md +50 -368
- package/skills/build-signals-agent/SKILL.md +49 -539
- package/skills/cross-cutting.md +84 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/domains/brand/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/domains/creative/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/domains/governance/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/domains/media-buy/creative-reception.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/domains/media-buy/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/domains/media-buy/scenarios/create_media_buy_async.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/domains/media-buy/scenarios/creative_fate_after_cancellation.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/domains/media-buy/scenarios/delivery_reporting.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/domains/media-buy/scenarios/governance_approved.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/domains/media-buy/scenarios/governance_conditions.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/domains/media-buy/scenarios/governance_denied.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/domains/media-buy/scenarios/governance_denied_recovery.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/domains/media-buy/scenarios/invalid_transitions.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/domains/media-buy/scenarios/inventory_list_no_match.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/domains/media-buy/scenarios/measurement_terms_rejected.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/domains/media-buy/scenarios/pending_creatives_to_start.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/domains/media-buy/scenarios/proposal_finalize.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/domains/media-buy/scenarios/refine_products.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/domains/media-buy/state-machine.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/domains/signals/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/domains/sponsored-intelligence/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/protocols/brand/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/protocols/creative/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/protocols/governance/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/protocols/media-buy/creative-reception.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/protocols/media-buy/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/protocols/media-buy/scenarios/create_media_buy_async.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/protocols/media-buy/scenarios/creative_fate_after_cancellation.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/protocols/media-buy/scenarios/delivery_reporting.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/protocols/media-buy/scenarios/governance_approved.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/protocols/media-buy/scenarios/governance_conditions.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/protocols/media-buy/scenarios/governance_denied.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/protocols/media-buy/scenarios/governance_denied_recovery.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/protocols/media-buy/scenarios/invalid_transitions.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/protocols/media-buy/scenarios/inventory_list_no_match.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/protocols/media-buy/scenarios/measurement_terms_rejected.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/protocols/media-buy/scenarios/pending_creatives_to_start.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/protocols/media-buy/scenarios/proposal_finalize.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/protocols/media-buy/scenarios/refine_products.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/protocols/media-buy/state-machine.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/protocols/signals/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/protocols/sponsored-intelligence/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/audience-sync/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/brand-rights/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/brand-rights/scenarios/governance_denied.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/collection-lists/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/content-standards/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/creative-ad-server/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/creative-generative/generative-seller.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/creative-generative/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/creative-template/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/governance-aware-seller/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/governance-delivery-monitor/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/governance-spend-authority/denied.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/governance-spend-authority/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/property-lists/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/sales-broadcast-tv/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/sales-catalog-driven/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/sales-non-guaranteed/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/sales-proposal-mode/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/sales-social/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/signal-marketplace/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/signal-marketplace/scenarios/governance_denied.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/specialisms/signal-owned/index.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-kits/acme-outdoor.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-kits/bistro-oranje.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-kits/nova-motors.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-kits/osei-natural.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-kits/signed-requests-runner.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-kits/substitution-observer-runner.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-kits/summit-foods.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-kits/webhook-receiver-runner.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/plan-hash/001-minimal-plan.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/plan-hash/002-full-plan.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/plan-hash/003-bookkeeping-stripped.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/plan-hash/004a-human-review-omitted.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/plan-hash/004b-human-review-explicit-null.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/plan-hash/005a-policy-categories-order-1.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/plan-hash/005b-policy-categories-order-2.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/plan-hash/006a-ext-trace-v1.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/plan-hash/006b-ext-trace-v2.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/plan-hash/007-unicode-objectives.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/plan-hash/008-numeric-canonicalization.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/README.md +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/canonicalization.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/keys.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/001-no-signature-header.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/002-wrong-tag.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/003-expired-signature.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/004-window-too-long.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/005-alg-not-allowed.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/006-missing-covered-component.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/007-missing-content-digest.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/008-unknown-keyid.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/009-key-ops-missing-verify.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/010-content-digest-mismatch.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/011-malformed-header.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/012-missing-expires-param.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/013-expires-le-created.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/014-missing-nonce-param.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/015-signature-invalid.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/016-replayed-nonce.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/017-key-revoked.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/018-digest-covered-when-forbidden.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/019-signature-without-signature-input.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/020-rate-abuse.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/021-duplicate-signature-input-label.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/022-multi-valued-content-type.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/023-multi-valued-content-digest.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/024-unquoted-string-param.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/025-jwk-alg-crv-mismatch.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/026-non-ascii-host.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/negative/027-webhook-registration-authentication-unsigned.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/positive/001-basic-post.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/positive/002-post-with-content-digest.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/positive/003-es256-post.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/positive/004-multiple-signature-labels.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/positive/005-default-port-stripped.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/positive/006-dot-segment-path.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/positive/007-query-byte-preserved.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/positive/008-percent-encoded-path.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/positive/009-percent-encoded-unreserved-decoded.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/positive/010-percent-encoded-slash-preserved.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/positive/011-ipv6-authority.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/request-signing/positive/012-ipv6-authority-default-port-stripped.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/README.md +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/keys.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/negative/001-wrong-tag.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/negative/002-expired-signature.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/negative/003-window-too-long.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/negative/004-alg-not-allowed.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/negative/005-missing-authority-component.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/negative/006-missing-content-digest.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/negative/007-unknown-keyid.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/negative/008-wrong-adcp-use.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/negative/009-content-digest-mismatch.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/negative/010-malformed-signature-input.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/negative/011-signature-without-input.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/negative/012-missing-expires-param.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/negative/013-expires-le-created.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/negative/014-missing-nonce-param.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/negative/015-signature-invalid.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/negative/016-replayed-nonce.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/negative/017-key-revoked.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/negative/018-rate-abuse.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/negative/019-revocation-stale.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/negative/020-key-ops-missing-verify.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/negative/021-base64-alphabet-mixing.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/positive/001-basic-post.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/positive/002-es256-post.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/positive/003-multiple-signature-labels.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/positive/004-default-port-stripped.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/positive/005-percent-encoded-path.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/positive/006-query-byte-preserved.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/test-vectors/webhook-signing/positive/007-body-without-idempotency-key.json +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/universal/capability-discovery.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/universal/collection-lists-pagination-integrity.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/universal/content-standards-pagination-integrity.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/universal/deterministic-testing.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/universal/error-compliance.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/universal/fictional-entities.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/universal/get-media-buys-pagination-integrity.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/universal/get-signals-pagination-integrity.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/universal/idempotency.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/universal/pagination-integrity-creative-formats.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/universal/pagination-integrity-list-accounts.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/universal/pagination-integrity.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/universal/property-lists-pagination-integrity.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/universal/runner-output-contract.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/universal/schema-validation.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/universal/security.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/universal/signed-requests.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/universal/v3-envelope-integrity.yaml +0 -0
- /package/compliance/cache/{3.0.5 → 3.0.6}/universal/webhook-emission.yaml +0 -0
|
@@ -5,1831 +5,87 @@ description: Use when building an AdCP seller agent — a publisher, SSP, or ret
|
|
|
5
5
|
|
|
6
6
|
# Build a Seller Agent
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
A seller agent receives briefs from buyers, returns products, accepts media buys, manages creatives, and reports delivery. The fastest path to a passing agent is to **fork a worked adapter** and replace its `// SWAP:` markers with calls to your backend. Each `// SWAP:` comment marks one line you change to your platform's backend call — change only those lines, run the gate, ship. ([`examples/CONTRIBUTING.md`](../../examples/CONTRIBUTING.md) covers the SWAP-marker convention in detail.) This skill tells you which adapter to fork and what cross-cutting rules apply across all of them.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## Pick your fork target
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Each of these is a worked, currently-running, three-gate-tested reference adapter. Fork it, swap the upstream, ship.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
| Specialism | Fork this | Mock upstream | Storyboard |
|
|
15
|
+
| --- | --- | --- | --- |
|
|
16
|
+
| `sales-guaranteed` | [`hello_seller_adapter_guaranteed.ts`](../../examples/hello_seller_adapter_guaranteed.ts) | `npx adcp mock-server sales-guaranteed` | `sales_guaranteed` |
|
|
17
|
+
| `sales-non-guaranteed` | [`hello_seller_adapter_non_guaranteed.ts`](../../examples/hello_seller_adapter_non_guaranteed.ts) | `npx adcp mock-server sales-non-guaranteed` | `sales_non_guaranteed` |
|
|
18
|
+
| `sales-social` | [`hello_seller_adapter_social.ts`](../../examples/hello_seller_adapter_social.ts) | `npx adcp mock-server sales-social` | `sales_social` |
|
|
19
|
+
| Multi-tenant holdco hub | [`hello_seller_adapter_multi_tenant.ts`](../../examples/hello_seller_adapter_multi_tenant.ts) | composed | per specialism |
|
|
15
20
|
|
|
16
|
-
-
|
|
17
|
-
- User mentions publisher, SSP, retail media, or media network in the context of AdCP
|
|
18
|
-
- User references `get_products`, `create_media_buy`, or the media buy protocol
|
|
21
|
+
The other sales-* specialisms (`sales-broadcast-tv`, `sales-streaming-tv`, `sales-exchange`, `sales-proposal-mode`) currently fork from `sales-guaranteed` or `sales-non-guaranteed` and apply specialism deltas — see `specialisms/<id>.md`. `sales-streaming-tv` and `sales-exchange` are preview specialisms with placeholder storyboards (claim them to advertise intent; baseline is all that's enforced today).
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- Buying ad inventory → that's a buyer/DSP agent (see `docs/getting-started.md`)
|
|
23
|
-
- Serving audience segments → `skills/build-signals-agent/`
|
|
24
|
-
- Rendering creatives from briefs → that's a creative agent
|
|
25
|
-
|
|
26
|
-
**Often claimed alongside:** [`audience-sync`](../build-seller-agent/SKILL.md) (walled-garden social + identity provider patterns), [`signal-marketplace`](../build-signals-agent/SKILL.md) (DSP-side data surface), [`sales-catalog-driven`](../build-retail-media-agent/SKILL.md) (retail-media catalog + dynamic-creative). See [Common multi-specialism bundles](../../examples/README.md#common-multi-specialism-bundles).
|
|
27
|
-
|
|
28
|
-
## <a name="the-baseline-what-every-sales--agent-must-implement"></a>The baseline: what every sales-\* agent MUST implement
|
|
29
|
-
|
|
30
|
-
Every sales-_ specialism (including `sales-social`, `sales-broadcast-tv`, `sales-retail-media`, `sales-catalog-driven`, etc.) is **additive on top of this baseline**. If you claim any `sales-_` specialism, you implement these tools regardless of the specialism-specific deltas below.
|
|
31
|
-
|
|
32
|
-
**Required tools** (tested by the `media_buy_seller` storyboard bundle at `compliance/cache/3.0.0/protocols/media-buy/`):
|
|
33
|
-
|
|
34
|
-
| Tool | Purpose | `SalesPlatform` method |
|
|
35
|
-
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- |
|
|
36
|
-
| `get_adcp_capabilities` | Declare protocols + specialisms + features | auto (framework) |
|
|
37
|
-
| `sync_accounts` | Advertiser onboarding, per-tenant account creation | `accounts.upsert` |
|
|
38
|
-
| `list_accounts` | Account lookup by brand/operator; buyers listing their accounts on your platform | `accounts.list` |
|
|
39
|
-
| `get_products` | Product catalog discovery from a brief; returns `{ products: [...] }`. **Surface `Product.forecast: DeliveryForecast`** when you have per-targeting estimates — see [`sales-guaranteed`](specialisms/sales-guaranteed.md#forecast-productforecast) and [`sales-social`](specialisms/sales-social.md#planning-surface) for projection patterns. | `sales.getProducts` |
|
|
40
|
-
| `list_creative_formats` | Formats your agent accepts | `sales.listCreativeFormats` |
|
|
41
|
-
| `create_media_buy` | Accept a campaign with packages, budget, flight dates | `sales.createMediaBuy` |
|
|
42
|
-
| `update_media_buy` | Bid, budget, status, package mutations over the campaign lifecycle | `sales.updateMediaBuy` |
|
|
43
|
-
| `get_media_buys` | Read campaigns back with full state (status, budget, packages, targeting overlays) | `sales.getMediaBuys` |
|
|
44
|
-
| `sync_creatives` | Accept creative assets and return per-asset status | `sales.syncCreatives` |
|
|
45
|
-
| `list_creatives` | Read the creative library back with pagination | `sales.listCreatives` |
|
|
46
|
-
| `get_media_buy_delivery` | Delivery + spend reporting with `reporting_period`, per-package billing rows | `sales.getMediaBuyDelivery` |
|
|
47
|
-
|
|
48
|
-
> **`sales-guaranteed` minimum tool surface** — register ALL of these or storyboard scenarios will cascade-skip with `skip_reason: missing_tool`:
|
|
49
|
-
> `get_adcp_capabilities`, `sync_accounts`, `list_accounts`, `get_products`, `list_creative_formats`, `create_media_buy`, `update_media_buy`, `get_media_buys`, `sync_creatives`, `get_media_buy_delivery`
|
|
50
|
-
>
|
|
51
|
-
> (`list_creatives` is optional — only required if the seller hosts its own creative library; creative-agent delegates omit it)
|
|
52
|
-
|
|
53
|
-
**Minimum platform skeleton** — every sales-\* seller starts here, then adds specialism-specific behavior on top:
|
|
54
|
-
|
|
55
|
-
```ts
|
|
56
|
-
import { createAdcpServerFromPlatform, type DecisioningPlatform, type SalesPlatform, type AccountStore } from '@adcp/sdk/server';
|
|
57
|
-
|
|
58
|
-
class MySeller implements DecisioningPlatform<{ networkId: string }, MyMeta> {
|
|
59
|
-
capabilities = {
|
|
60
|
-
specialisms: ['sales-non-guaranteed'] as const,
|
|
61
|
-
pricingModels: ['cpm'] as const,
|
|
62
|
-
channels: ['display'] as const,
|
|
63
|
-
config: { networkId: 'NET_42' },
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
accounts: AccountStore<MyMeta> = {
|
|
67
|
-
resolve: async (ref, ctx) => { /* … */ },
|
|
68
|
-
upsert: async (params, ctx) => { /* … */ },
|
|
69
|
-
list: async (params, ctx) => { /* … */ },
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
sales: SalesPlatform<MyMeta> = {
|
|
73
|
-
getProducts: async (params, ctx) => { /* … */ },
|
|
74
|
-
listCreativeFormats: async () => ({ formats: [...] }),
|
|
75
|
-
createMediaBuy: async (params, ctx) => { /* … */ },
|
|
76
|
-
updateMediaBuy: async (id, patch, ctx) => { /* … */ },
|
|
77
|
-
getMediaBuys: async (params, ctx) => { /* … */ },
|
|
78
|
-
syncCreatives: async (creatives, ctx) => { /* … */ },
|
|
79
|
-
listCreatives: async (params, ctx) => { /* … */ },
|
|
80
|
-
getMediaBuyDelivery: async (filter, ctx) => { /* … */ },
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const server = createAdcpServerFromPlatform(new MySeller(), {
|
|
85
|
-
name: 'my-seller',
|
|
86
|
-
version: '1.0.0',
|
|
87
|
-
});
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
The `createAdcpServerFromPlatform` path wraps a typed `DecisioningPlatform` with compile-time specialism enforcement (claim `sales-non-guaranteed`, miss a required `sales.*` method, fail compile), ctx_metadata round-trip + auto-hydration, idempotency-principal synthesis, status mappers, and webhook auto-emit. **Reach for the lower-level `createAdcpServer` from `@adcp/sdk/server/legacy/v5` only when you need fine control over individual handlers, are mid-migration from a v5 codebase, or have custom tools the platform interface doesn't yet model.**
|
|
91
|
-
|
|
92
|
-
> **On a hydration miss, the framework leaves the hydrated field undefined and runs the handler anyway** — the cache is a hint, not source-of-truth. Your handler keeps its existence check (`patch.media_buy ?? (await db.findMediaBuy(id))`) and throws a typed `MediaBuyNotFoundError` / `PackageNotFoundError` / `ProductNotFoundError` from `@adcp/sdk/server` when both the hydrate and the DB-fallback come up empty. Full rationale in [Auto-hydration error contract](../../docs/migration-5.x-to-6.x.md#auto-hydration-error-contract).
|
|
93
|
-
|
|
94
|
-
If a specialism's storyboard doesn't exercise one of these tools, the tool is **not optional** — the storyboard is just focused elsewhere (e.g. `sales-social` covers audience sync + DPA + events; the media buy flow itself is covered by `sales-non-guaranteed` or `sales-guaranteed` which you also claim). See § [Tools and Required Response Shapes](#tools-and-required-response-shapes) below for the exact response shape each tool must return.
|
|
95
|
-
|
|
96
|
-
## Specialisms This Skill Covers
|
|
97
|
-
|
|
98
|
-
Your compliance obligations come from the specialisms you claim in `get_adcp_capabilities`. Each specialism has a storyboard bundle at `compliance/cache/latest/specialisms/<id>/` that the AAO compliance runner executes. Pick one or more.
|
|
99
|
-
|
|
100
|
-
**Specialisms are additive on top of [the baseline](#the-baseline-what-every-sales--agent-must-implement).** A specialism's storyboard exercises the ADDITIONAL behaviors it requires; it does not displace the baseline 11-tool surface above. If the storyboard skips a baseline tool (because that tool is already covered by `sales-non-guaranteed` / `sales-guaranteed`), that doesn't mean the tool is optional for your agent — it means the test is focused elsewhere. Check the storyboard's `agent.capabilities` — if it lists `sells_media` / `accepts_briefs`, the baseline still applies.
|
|
101
|
-
|
|
102
|
-
**Claim multiple specialisms.** A typical social seller claims `sales-non-guaranteed` + `sales-social`. A typical broadcast seller claims `sales-guaranteed` + `sales-broadcast-tv`. A typical social seller doing audience sync claims `sales-non-guaranteed` + `sales-social` + `audience-sync`.
|
|
103
|
-
|
|
104
|
-
| Specialism | Status | Delta from baseline | See |
|
|
105
|
-
| ---------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
|
106
|
-
| `sales-guaranteed` | stable | `create_media_buy` has **three return shapes** (IO-task envelope / `pending_creatives` / `active`) plus `TERMS_REJECTED` for aggressive `measurement_terms`. **Read the companion before coding** — applying only the task-envelope path fails 5 storyboard `create_media_buy` steps that probe the synchronous branches. | [§ sales-guaranteed](#specialism-sales-guaranteed) |
|
|
107
|
-
| `sales-non-guaranteed` | stable | Instant `status: 'active'` with `confirmed_at`; accept `bid_price` on packages; expose `update_media_buy` for bid/budget changes | [§ sales-non-guaranteed](#specialism-sales-non-guaranteed) |
|
|
108
|
-
| `sales-broadcast-tv` | stable | Top-level `agency_estimate_number`; per-package `measurement_terms.billing_measurement`; Ad-ID `industry_identifiers` on creatives; `measurement_windows` (Live/C3/C7) on delivery | [§ sales-broadcast-tv](#specialism-sales-broadcast-tv) |
|
|
109
|
-
| `sales-streaming-tv` | preview | v3.1 placeholder (empty `phases`) — ship the baseline, declare `channels: ['ctv'] as const` on products | Baseline only |
|
|
110
|
-
| `sales-social` | stable | **Additive**: baseline `get_products` + `create_media_buy` still apply (Snap/Meta/TikTok all have product catalogs and campaigns). Adds `sync_audiences` (audience push), `sync_creatives` (native formats), `sync_catalogs` (dynamic product ads), `log_event` (conversion tracking), `get_account_financials` (prepaid-balance monitoring), and `sync_accounts` with `account_scope`/`payment_terms`/`setup` for advertiser onboarding. Declare `sales-social` **alongside** `sales-non-guaranteed` (or `-guaranteed`) — don't replace it. | [§ sales-social](#specialism-sales-social) |
|
|
111
|
-
| `sales-exchange` | preview | v3.1 placeholder — target `sales-non-guaranteed` baseline; PMP / deal IDs / auction transparency pending | Baseline only |
|
|
112
|
-
| `sales-proposal-mode` | stable | `get_products` returns `proposals[]` with `budget_allocations`; handle `buying_mode: 'refine'`; accept via `create_media_buy` with `proposal_id` + `total_budget` and no `packages` | [§ sales-proposal-mode](#specialism-sales-proposal-mode) |
|
|
113
|
-
| `audience-sync` | stable | Track: `audiences`. Implement `sync_audiences` (handles discovery, add, and delete) and `list_accounts`. Hashed identifiers (SHA-256 lowercased+trimmed). Match-rate telemetry on response. | [§ audience-sync](#specialism-audience-sync) |
|
|
114
|
-
| `signed-requests` | preview | RFC 9421 HTTP Signature verification on mutating requests. Advertise `request_signing.supported: true` in capabilities; graded against conformance vectors — positive vectors must produce non-4xx; negative vectors must return `401` with `WWW-Authenticate: Signature error="<code>"` matching the vector's `expected_outcome.error_code` byte-for-byte. | [§ signed-requests](#specialism-signed-requests) |
|
|
115
|
-
|
|
116
|
-
**Not in this skill:** `sales-catalog-driven` and `sales-retail-media` (both in `skills/build-retail-media-agent/` — catalog-driven applies to restaurants, travel, and local commerce too, not only retail).
|
|
117
|
-
|
|
118
|
-
Specialism ID (kebab-case) = storyboard directory. The storyboard's `id:` field (snake_case, e.g. `media_buy_broadcast_seller`) is the category name, not the specialism name. One specialism can apply to multiple product lines — a seller with both CTV inventory and broadcast TV inventory can claim `sales-streaming-tv` and `sales-broadcast-tv` simultaneously.
|
|
119
|
-
|
|
120
|
-
## Protocol-Wide Requirements (AdCP 3.0 GA)
|
|
121
|
-
|
|
122
|
-
Three requirements apply to **every** production seller, regardless of which specialism you claim. Don't reinvent any of them — the SDK provides helpers for each.
|
|
123
|
-
|
|
124
|
-
### `idempotency_key` is required on every mutating request
|
|
125
|
-
|
|
126
|
-
`create_media_buy`, `update_media_buy`, `sync_accounts`, `sync_creatives`, `sync_audiences`, `sync_catalogs`, `sync_event_sources`, `provide_performance_feedback` — every mutating call carries a client-supplied `idempotency_key`. Wire `createIdempotencyStore` into `createAdcpServerFromPlatform(platform, { idempotency })` and the framework handles replay detection, payload-hash conflict (`IDEMPOTENCY_CONFLICT`), expiry (`IDEMPOTENCY_EXPIRED`), and in-flight parallelism. Don't implement this in handler code. See [§ Idempotency](#idempotency) below for the full wire-up.
|
|
127
|
-
|
|
128
|
-
### Authentication is mandatory
|
|
129
|
-
|
|
130
|
-
An agent that accepts unauthenticated requests is non-compliant — the universal `security_baseline` storyboard enforces this. Wire `serve({ authenticate })` with `verifyApiKey`, `verifyBearer`, or `anyOf(...)` before you claim any specialism. See [§ Protecting your agent](#protecting-your-agent) below.
|
|
131
|
-
|
|
132
|
-
### Don't break when RFC 9421 Signature headers arrive
|
|
133
|
-
|
|
134
|
-
Even if you don't claim `signed-requests`, a buyer may send `Signature-Input` / `Signature` headers. Your MCP transport must pass the request through without rejecting it. If you do claim the specialism, verify per [§ signed-requests](#specialism-signed-requests) below.
|
|
135
|
-
|
|
136
|
-
### Resolve-then-authorize — uniform errors for not-found / not-yours
|
|
137
|
-
|
|
138
|
-
AdCP spec § error-handling MUSTs that you return **byte-equivalent responses** for "the id exists but the caller lacks access" vs "the id does not exist." Distinguishing the two leaks cross-tenant existence information — an attacker who learns that `mb_0x1234` returns `PERMISSION_DENIED` while `mb_0xabcd` returns `REFERENCE_NOT_FOUND` can enumerate every live id across every tenant you host.
|
|
139
|
-
|
|
140
|
-
The rule applies to every observable channel: `error.code` / `message` / `field` / `details`, HTTP status, A2A `task.status.state`, MCP `isError`, response headers (`ETag`, `Cache-Control`, rate-limit, CDN tags), webhook/audit dispatch, logs with tenant correlation, same work on both paths.
|
|
141
|
-
|
|
142
|
-
**How to get it right:**
|
|
143
|
-
|
|
144
|
-
- Both paths return `REFERENCE_NOT_FOUND` (or the domain-specific `*_NOT_FOUND` code). Never `PERMISSION_DENIED` or `FORBIDDEN` on an id lookup.
|
|
145
|
-
- Don't echo the probed id in `error.details` — or echo it in both paths identically.
|
|
146
|
-
- Route both paths through the same response constructor so headers (including `ETag`, `Cache-Control`) are set identically.
|
|
147
|
-
- Do the same work on both paths: don't short-circuit on "id format invalid" with a faster path — an attacker will measure latency and notice.
|
|
148
|
-
|
|
149
|
-
`adcp fuzz` runs a paired-probe invariant that enforces this automatically. Pass two test tenants via `--auth-token` + `--auth-token-cross-tenant` for full coverage (see [VALIDATE-YOUR-AGENT.md § Uniform-error-response invariant](../../docs/guides/VALIDATE-YOUR-AGENT.md#uniform-error-response-invariant-paired-probe)). The invariant fails loudly with a byte-level diff pointing at the offending channel.
|
|
150
|
-
|
|
151
|
-
<a name="composing-oauth-signing-and-idempotency"></a>
|
|
152
|
-
|
|
153
|
-
### Composing OAuth, signing, and idempotency
|
|
154
|
-
|
|
155
|
-
Each concern above is straightforward in isolation. The pitfalls are at their boundaries. A production seller that claims both `sales-guaranteed` and `signed-requests` and sits behind OAuth wires them through `serve()`'s composition hooks — not external Express middleware.
|
|
156
|
-
|
|
157
|
-
**The pipeline.** `serve({ authenticate, preTransport })` runs steps in this order and buffers the request body into `req.rawBody` so the signature verifier can hash it without racing the MCP transport:
|
|
158
|
-
|
|
159
|
-
```typescript
|
|
160
|
-
import { serve } from '@adcp/sdk';
|
|
161
|
-
// verifyBearer / verifyApiKey / anyOf live on the server subpath, not the root barrel:
|
|
162
|
-
import { verifyBearer } from '@adcp/sdk/server';
|
|
163
|
-
// Low-level verifier is preTransport-shaped: use it instead of createExpressVerifier
|
|
164
|
-
// (which is Express (req, res, next) middleware and won't type-check against preTransport):
|
|
165
|
-
import {
|
|
166
|
-
verifyRequestSignature,
|
|
167
|
-
RequestSignatureError,
|
|
168
|
-
StaticJwksResolver,
|
|
169
|
-
InMemoryReplayStore,
|
|
170
|
-
InMemoryRevocationStore,
|
|
171
|
-
type VerifierCapability,
|
|
172
|
-
} from '@adcp/sdk/signing/server';
|
|
173
|
-
|
|
174
|
-
const capability: VerifierCapability = {
|
|
175
|
-
supported: true,
|
|
176
|
-
required_for: ['create_media_buy', 'update_media_buy', 'acquire_rights'],
|
|
177
|
-
supported_for: ['sync_creatives', 'sync_audiences', 'sync_accounts'],
|
|
178
|
-
covers_content_digest: 'required',
|
|
179
|
-
};
|
|
180
|
-
const jwks = new StaticJwksResolver([
|
|
181
|
-
// JWKs array — each must carry its own `kid`.
|
|
182
|
-
]);
|
|
183
|
-
const replayStore = new InMemoryReplayStore();
|
|
184
|
-
const revocationStore = new InMemoryRevocationStore({
|
|
185
|
-
issuer: 'https://seller.example.com/mcp',
|
|
186
|
-
updated: new Date().toISOString(),
|
|
187
|
-
next_update: new Date(Date.now() + 24 * 3600_000).toISOString(),
|
|
188
|
-
revoked_kids: [],
|
|
189
|
-
revoked_jtis: [],
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
serve(createAgent, {
|
|
193
|
-
publicUrl: 'https://seller.example.com/mcp',
|
|
194
|
-
|
|
195
|
-
// 1. authenticate runs first. Bad/missing bearer → 401 Bearer challenge.
|
|
196
|
-
// serve() populates extra.authInfo, which the framework surfaces as ctx.authInfo.
|
|
197
|
-
authenticate: verifyBearer({
|
|
198
|
-
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
|
|
199
|
-
issuer: 'https://auth.example.com',
|
|
200
|
-
audience: 'https://seller.example.com/mcp',
|
|
201
|
-
requiredScopes: ['adcp:seller'],
|
|
202
|
-
}),
|
|
203
|
-
protectedResource: { authorization_servers: ['https://auth.example.com'] },
|
|
204
|
-
|
|
205
|
-
// 2. preTransport: raw http (req, res) => Promise<boolean>. Verify the
|
|
206
|
-
// RFC 9421 signature here, using req.rawBody pre-buffered by serve().
|
|
207
|
-
// Return true only if you wrote the response yourself; return false to
|
|
208
|
-
// continue into MCP dispatch. Throwing produces a generic 500.
|
|
209
|
-
preTransport: async (req, res) => {
|
|
210
|
-
try {
|
|
211
|
-
await verifyRequestSignature(
|
|
212
|
-
{ method: req.method!, url: req.url!, headers: req.headers, body: req.rawBody ?? '' },
|
|
213
|
-
{
|
|
214
|
-
capability,
|
|
215
|
-
jwks,
|
|
216
|
-
replayStore,
|
|
217
|
-
revocationStore,
|
|
218
|
-
operation: resolveOperation(req), // your function: extract the AdCP operation name from the request
|
|
219
|
-
}
|
|
220
|
-
);
|
|
221
|
-
} catch (err) {
|
|
222
|
-
if (err instanceof RequestSignatureError) {
|
|
223
|
-
res.statusCode = 401;
|
|
224
|
-
res.setHeader('WWW-Authenticate', `Signature error="${err.code}"`);
|
|
225
|
-
res.end();
|
|
226
|
-
return true; // handled
|
|
227
|
-
}
|
|
228
|
-
throw err;
|
|
229
|
-
}
|
|
230
|
-
return false; // continue to MCP dispatch
|
|
231
|
-
},
|
|
232
|
-
|
|
233
|
-
// 3. MCP transport parses JSON and dispatches to the framework server.
|
|
234
|
-
// 4. Framework applies the idempotency store per handler — you don't mount it.
|
|
235
|
-
});
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
**Principal threading.** `resolveSessionKey(ctx)` receives only `{toolName, params, account}` — no auth info. To compose the OAuth subject into the idempotency key you need `resolveIdempotencyPrincipal`, which receives the full `HandlerContext` including `ctx.authInfo` (populated by `verifyBearer` through MCP's `extra.authInfo`):
|
|
239
|
-
|
|
240
|
-
```typescript
|
|
241
|
-
createAdcpServerFromPlatform(myPlatform, {
|
|
242
|
-
// ...
|
|
243
|
-
// SessionKeyContext has no authInfo — use this for coarse per-account scoping:
|
|
244
|
-
resolveSessionKey: ctx => ctx.account?.id,
|
|
245
|
-
|
|
246
|
-
// HandlerContext has authInfo — use this when the idempotency namespace must
|
|
247
|
-
// be scoped to the authenticated principal:
|
|
248
|
-
resolveIdempotencyPrincipal: ctx => {
|
|
249
|
-
const clientId = ctx.authInfo?.clientId;
|
|
250
|
-
if (!clientId)
|
|
251
|
-
throw new Error('unauthenticated request reached idempotency scope — check authenticate is configured');
|
|
252
|
-
// Multi-tenant AS: if the JWT carries a tenant claim, verifyBearer surfaces
|
|
253
|
-
// it in ctx.authInfo.extra. Compose so sub collisions across tenants don't
|
|
254
|
-
// share a replay namespace:
|
|
255
|
-
const tenant = ctx.authInfo?.extra?.tenant_id as string | undefined;
|
|
256
|
-
return tenant ? `${tenant}:${clientId}` : clientId;
|
|
257
|
-
},
|
|
258
|
-
});
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
Composing the verified signing `keyid` in is possible but lives outside the handler context: the signing middleware stashes it on `req.verifiedSigner.keyid` (raw HTTP request), which doesn't flow into `HandlerContext` by default. Either accept that the idempotency namespace is OAuth-principal-only (most setups), or write a custom `authenticate` that promotes the verified keyid into `authInfo.extra` so your `resolveIdempotencyPrincipal` can read it uniformly.
|
|
262
|
-
|
|
263
|
-
**401 disambiguation.** A request can fail both OAuth and signature verification. Per RFC 7235 you can emit multiple `WWW-Authenticate` challenges — order them so the client's most promising next step is first. OAuth's Bearer challenge always fires first (the client can't sign correctly until it has a valid identity); Signature challenge only fires when the request is authenticated but signed wrong.
|
|
264
|
-
|
|
265
|
-
```typescript
|
|
266
|
-
// Inside preTransport, after a RequestSignatureError is raised on an authenticated request:
|
|
267
|
-
res.statusCode = 401;
|
|
268
|
-
res.setHeader(
|
|
269
|
-
'WWW-Authenticate',
|
|
270
|
-
[
|
|
271
|
-
// If the bearer also failed, the Bearer challenge would have been emitted by `authenticate`
|
|
272
|
-
// before preTransport ran — you only reach this branch on authenticated-but-bad-signature.
|
|
273
|
-
`Signature error="${err.code}"`,
|
|
274
|
-
// If you want to emit both (e.g., you implement your own authenticator that doesn't
|
|
275
|
-
// short-circuit on missing bearer), the Bearer challenge goes first:
|
|
276
|
-
// 'Bearer error="invalid_token", resource_metadata="https://seller.example.com/.well-known/oauth-protected-resource"',
|
|
277
|
-
].join(', ')
|
|
278
|
-
);
|
|
279
|
-
res.end();
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
Matrix:
|
|
283
|
-
|
|
284
|
-
- No/expired bearer → framework emits `Bearer error="invalid_token", resource_metadata=...`. Request never reaches `preTransport`.
|
|
285
|
-
- Valid bearer, signature invalid → your `preTransport` emits `Signature error="<code>"` byte-matching the test vector's `expected_outcome.error_code`.
|
|
286
|
-
- Valid bearer, signature absent on a `supported_for`-only operation → accept; signature is advisory on `supported_for`. Only `required_for` rejects unsigned.
|
|
287
|
-
|
|
288
|
-
**Idempotency semantics for `submitted` responses.** The framework caches **every successful mutation** including async `submitted` envelopes — not only terminal ones. A replay of the same key within the TTL returns the cached `submitted` response with `replayed: true` injected. A second IO is **not** created. Parallel calls with the same key within the 120-second in-flight window get `adcpError('SERVICE_UNAVAILABLE', { retry_after: 1 })` and should retry — buyer SDKs auto-retry on the `transient` class. The framework emits this for you; you don't handle it in handler code.
|
|
289
|
-
|
|
290
|
-
This means: the `task_id` you return on a `sales-guaranteed` `create_media_buy` is stable under replay. The buyer polls (or gets webhooks on) the same task handle on any retry within the replay window — you don't create a second IO.
|
|
291
|
-
|
|
292
|
-
**The three idempotency error codes the framework emits:**
|
|
293
|
-
|
|
294
|
-
| Code | When | Buyer's next step |
|
|
295
|
-
| ---------------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
296
|
-
| `SERVICE_UNAVAILABLE` (`retry_after: 1`) | Parallel call with the same key, still within the 120s in-flight window | Wait the `retry_after` seconds and retry — eventually replays the cached response or hits CONFLICT |
|
|
297
|
-
| `IDEMPOTENCY_CONFLICT` | Same key, different payload hash | Don't retry — buyer has a client bug generating the same key for different requests |
|
|
298
|
-
| `IDEMPOTENCY_EXPIRED` | Key replayed after the TTL (default 24h, configurable 1h–7d) | Mint a new key and retry |
|
|
299
|
-
|
|
300
|
-
<a name="webhooks-async-completion-signed-outbound"></a>
|
|
301
|
-
|
|
302
|
-
## Webhooks (async completion, signed outbound)
|
|
303
|
-
|
|
304
|
-
Most seller flows need outbound webhooks — `sales-guaranteed` fires on IO completion, `sales-broadcast-tv` fires `window_update` deliveries as C3/C7 data matures, `update_media_buy` fires on bid/budget application. **Don't hand-roll `fetch` with HMAC**. Pass `webhooks: { signerKey }` to `createAdcpServerFromPlatform` and call `ctx.emitWebhook(...)` from any handler — the framework handles RFC 9421 signing, nonce minting, stable `idempotency_key` across retries, 5xx/429 backoff, byte-identical JSON serialization, and the "don't retry on signature failures" terminal behavior.
|
|
305
|
-
|
|
306
|
-
```typescript
|
|
307
|
-
import {
|
|
308
|
-
createAdcpServerFromPlatform,
|
|
309
|
-
serve,
|
|
310
|
-
type DecisioningPlatform,
|
|
311
|
-
type SalesPlatform,
|
|
312
|
-
type AccountStore,
|
|
313
|
-
} from '@adcp/sdk/server';
|
|
314
|
-
|
|
315
|
-
// Dev: generate a signer JWK once at boot. Production: load from KMS/env with a stable `kid`,
|
|
316
|
-
// and publish the public half at your `jwks_uri` so buyers can verify without OOB exchange.
|
|
317
|
-
import { generateKeyPairSync, randomUUID } from 'node:crypto';
|
|
318
|
-
const { privateKey } = generateKeyPairSync('ed25519');
|
|
319
|
-
const signerJwk = {
|
|
320
|
-
...privateKey.export({ format: 'jwk' }),
|
|
321
|
-
kid: 'seller-webhook-kid-2026',
|
|
322
|
-
alg: 'EdDSA',
|
|
323
|
-
adcp_use: 'webhook-signing',
|
|
324
|
-
key_ops: ['sign'],
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
class WebhookSeller implements DecisioningPlatform {
|
|
328
|
-
capabilities = {
|
|
329
|
-
specialisms: ['sales-guaranteed'] as const,
|
|
330
|
-
pricingModels: ['cpm'] as const,
|
|
331
|
-
channels: ['display'] as const,
|
|
332
|
-
config: {},
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
accounts: AccountStore = {
|
|
336
|
-
resolve: async ref => ({
|
|
337
|
-
id: 'account_id' in ref ? ref.account_id : 'default',
|
|
338
|
-
operator: 'me',
|
|
339
|
-
ctx_metadata: {},
|
|
340
|
-
}),
|
|
341
|
-
upsert: async () => ({ ok: true, items: [] }),
|
|
342
|
-
list: async () => ({ items: [], nextCursor: null }),
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
sales: SalesPlatform = {
|
|
346
|
-
getProducts: async () => ({ products: [] }),
|
|
347
|
-
createMediaBuy: async (req, ctx) => {
|
|
348
|
-
// sales-guaranteed: IO signing completes async. Emit the final result on completion.
|
|
349
|
-
const taskId = `task_${randomUUID()}`;
|
|
350
|
-
|
|
351
|
-
// Capture ctx.emitWebhook into a local BEFORE scheduling — the handler returns
|
|
352
|
-
// immediately, but the closure outlives the request; ctx may be recycled.
|
|
353
|
-
const emit = ctx.emitWebhook!; // non-null: guaranteed populated when webhooks config is set
|
|
354
|
-
|
|
355
|
-
queueIoReview(req, async outcome => {
|
|
356
|
-
await emit({
|
|
357
|
-
url: (req as { push_notification_config?: { url: string } }).push_notification_config!.url,
|
|
358
|
-
payload: {
|
|
359
|
-
task: {
|
|
360
|
-
task_id: taskId,
|
|
361
|
-
status: outcome.approved ? 'completed' : 'rejected',
|
|
362
|
-
result: outcome.approved ? { media_buy_id: outcome.media_buy_id, packages: outcome.packages } : undefined,
|
|
363
|
-
},
|
|
364
|
-
},
|
|
365
|
-
operation_id: `create_media_buy.${taskId}`, // stable across retries — framework reuses same idempotency_key
|
|
366
|
-
});
|
|
367
|
-
});
|
|
368
|
-
return { status: 'submitted', task_id: taskId }; // synchronous response is the task envelope
|
|
369
|
-
},
|
|
370
|
-
updateMediaBuy: async (id, patch) => ({ media_buy_id: id, status: 'active' }),
|
|
371
|
-
getMediaBuys: async () => ({ media_buys: [] }),
|
|
372
|
-
getMediaBuyDelivery: async () => ({ deliveries: [] }),
|
|
373
|
-
syncCreatives: async () => [],
|
|
374
|
-
listCreativeFormats: async () => ({ formats: [] }),
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
serve(() =>
|
|
379
|
-
createAdcpServerFromPlatform(new WebhookSeller(), {
|
|
380
|
-
name: 'My Seller',
|
|
381
|
-
version: '1.0.0',
|
|
382
|
-
webhooks: {
|
|
383
|
-
signerKey: { keyid: signerJwk.kid, alg: 'ed25519', privateKey: signerJwk },
|
|
384
|
-
// Optional: retries, idempotencyKeyStore (swap memory → pg for multi-replica)
|
|
385
|
-
},
|
|
386
|
-
})
|
|
387
|
-
);
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
**`ctx.emitWebhook` is typed optional** (`emitWebhook?:`) even when you configure `webhooks` on the server. The framework populates it on every handler once `webhooks.signerKey` is set; use `ctx.emitWebhook!` or a local guard. Strict-mode assert-once-at-boot works too.
|
|
391
|
-
|
|
392
|
-
**Return envelope — use `taskToolResponse`, not the default `mediaBuyResponse` wrap.** The framework auto-wraps `createMediaBuy` returns with `mediaBuyResponse`, which stamps `revision`/`confirmed_at`/`valid_actions` onto the response — semantically wrong on a `submitted` envelope. For submitted returns, import `taskToolResponse` from `@adcp/sdk/server` and wrap explicitly (see [§ sales-guaranteed](#specialism-sales-guaranteed) for the full pattern).
|
|
393
|
-
|
|
394
|
-
**`operation_id` must be stable across retries.** The emitter hashes `operation_id` into the outbound `idempotency_key` so receivers can dedupe retried deliveries. Regenerating `operation_id` on retry is the top at-least-once-delivery bug the webhook conformance runner catches — use an ID derived from the logical event (the task_id, media_buy_id, or report batch), not a timestamp or fresh UUID.
|
|
395
|
-
|
|
396
|
-
**Terminal errors.** The emitter stops retrying on 4xx and on 401 responses carrying `WWW-Authenticate: Signature error="webhook_signature_*"` — signature failures are deterministic and retrying produces identical rejection. 5xx and 429 retry with exponential backoff.
|
|
397
|
-
|
|
398
|
-
**Legacy buyers.** If a buyer registered `push_notification_config.authentication` with HMAC-SHA256 or Bearer credentials, the emitter honors that mode automatically (deprecated in 4.0 but supported for backward compatibility). Omit `authentication` to opt into the RFC 9421 webhook profile by default.
|
|
399
|
-
|
|
400
|
-
**Revocation webhooks (brand-rights).** When your agent revokes a rights grant, `ctx.emitWebhook` against the buyer's `revocation_webhook` URL — see `skills/build-brand-rights-agent/SKILL.md` for the payload shape.
|
|
401
|
-
|
|
402
|
-
## Before Writing Code
|
|
403
|
-
|
|
404
|
-
Determine these five things. Ask the user — don't guess.
|
|
405
|
-
|
|
406
|
-
### 1. What Kind of Seller?
|
|
407
|
-
|
|
408
|
-
- **Premium publisher** — guaranteed inventory, fixed pricing, IO approval (ESPN, NYT)
|
|
409
|
-
- **SSP / Exchange** — non-guaranteed, auction-based, instant activation
|
|
410
|
-
- **Retail media network** — both guaranteed and non-guaranteed, proposals, catalog-driven creative, conversion tracking
|
|
411
|
-
|
|
412
|
-
### 2. Guaranteed or Non-Guaranteed?
|
|
413
|
-
|
|
414
|
-
- **Guaranteed** — `delivery_type: "guaranteed"`, may require async approval (`submitted` → `pending_approval` → `confirmed`)
|
|
415
|
-
- **Non-guaranteed** — `delivery_type: "non_guaranteed"`, buyer sets `bid_price`, instant activation
|
|
416
|
-
|
|
417
|
-
Many sellers support both — different products can have different delivery types.
|
|
418
|
-
|
|
419
|
-
### 3. Products and Pricing
|
|
420
|
-
|
|
421
|
-
Get specific inventory. Each product needs:
|
|
422
|
-
|
|
423
|
-
- `product_id`, `name`, `description`
|
|
424
|
-
- `publisher_properties` — at least one `{ publisher_domain: 'example.com', selection_type: 'all' }` (discriminated union: `'all'` | `'by_id'` with `property_ids` | `'by_tag'` with `tags`)
|
|
425
|
-
- `format_ids` — array of `{ agent_url: string, id: string }` referencing creative formats
|
|
426
|
-
- `delivery_type` — `'guaranteed'` or `'non_guaranteed'`
|
|
427
|
-
- `pricing_options` — at least one (see below)
|
|
428
|
-
- `reporting_capabilities` — `{ available_reporting_frequencies: ['daily'], expected_delay_minutes: 240, timezone: 'UTC', supports_webhooks: false, available_metrics: ['impressions', 'spend', 'clicks'], date_range_support: 'date_range' }`
|
|
429
|
-
- Optional: `channels` — use `as const` to avoid `string[]` inference: `channels: ['display', 'olv'] as const`
|
|
430
|
-
|
|
431
|
-
Pricing models (all require `pricing_option_id` and `currency`):
|
|
432
|
-
|
|
433
|
-
- `cpm` — `{ pricing_option_id: 'cpm-1', pricing_model: "cpm", fixed_price: 12.00, currency: "USD" }`
|
|
434
|
-
- `cpc` — `{ pricing_option_id: 'cpc-1', pricing_model: "cpc", fixed_price: 1.50, currency: "USD" }`
|
|
435
|
-
- Auction — `{ pricing_option_id: 'auction-1', pricing_model: "cpm", floor_price: 5.00, currency: "USD" }` (buyer bids above floor)
|
|
436
|
-
|
|
437
|
-
Each pricing option can set `min_spend_per_package` to enforce minimum budgets.
|
|
438
|
-
|
|
439
|
-
For all `PricingOption` variants and `Product` required fields, see [`docs/TYPE-SUMMARY.md`](../../docs/TYPE-SUMMARY.md).
|
|
440
|
-
|
|
441
|
-
### 4. Forecast & planning surface
|
|
442
|
-
|
|
443
|
-
`Product.forecast: DeliveryForecast` is how sellers answer the planning question every buyer asks first — "what reach for $X," "what budget hits Y conversions," "is this guaranteed inventory still available." The protocol covers forward, reverse, availability, package, and reach/frequency curves via `forecast_range_unit`. Read the spec for the full surface, including budget-curve, availability, GRP, and outcome-forecast worked examples: [Delivery Forecasts](https://adcontextprotocol.org/docs/media-buy/product-discovery/media-products#delivery-forecasts).
|
|
444
|
-
|
|
445
|
-
Per-specialism canonical units (SDK-skill guidance — pick what fits your seller type):
|
|
446
|
-
|
|
447
|
-
| Specialism | `forecast_range_unit` | What the curve says |
|
|
448
|
-
| ---------------------- | ----------------------------- | --------------------------------------------- |
|
|
449
|
-
| `sales-non-guaranteed` | `'spend'` | Forward — impressions/clicks per budget tier |
|
|
450
|
-
| `sales-guaranteed` | `'availability'` | Reserved inventory check, budget omitted |
|
|
451
|
-
| `sales-broadcast-tv` | `'reach_freq'` or `'package'` | GRP curves OR daypart packages |
|
|
452
|
-
| `sales-social` | `'conversions'` / `'clicks'` | Reverse — Meta-style goal-based planning |
|
|
453
|
-
| `sales-retail-media` | `'conversions'` | Outcome-based (purchase-attribution use case) |
|
|
454
|
-
|
|
455
|
-
Adapters typically populate this from existing upstream forecast/availability/reach-estimation endpoints (GAM `forecastService.getDeliveryForecast`, Meta `/delivery_estimate`, walled-garden `/audience_reach_estimate`, etc.) — the mapping is one-to-one once you pick the right unit.
|
|
456
|
-
|
|
457
|
-
### 5. Approval Workflow
|
|
458
|
-
|
|
459
|
-
For guaranteed buys, choose one:
|
|
460
|
-
|
|
461
|
-
- **Instant confirmation** — `create_media_buy` returns completed with confirmed status. Simplest.
|
|
462
|
-
- **Async approval** — returns `submitted`, buyer polls `get_media_buys`. Use `registerAdcpTaskTool`.
|
|
463
|
-
- **Human-in-the-loop** — returns `input-required` with a setup URL for IO signing.
|
|
464
|
-
|
|
465
|
-
Non-guaranteed buys are always instant confirmation.
|
|
466
|
-
|
|
467
|
-
### 6. Creative Management
|
|
468
|
-
|
|
469
|
-
- **Standard** — `list_creative_formats` + `sync_creatives`. Buyer uploads assets, seller validates.
|
|
470
|
-
- **Catalog-driven** — buyer syncs product catalog via `sync_catalogs`. Common for retail media.
|
|
471
|
-
- **None** — creative handled out-of-band. Omit creative tools.
|
|
472
|
-
|
|
473
|
-
## <a name="tools-and-required-response-shapes"></a>Tools and Required Response Shapes
|
|
474
|
-
|
|
475
|
-
> **Before writing any handler's return statement, fetch [`docs/llms.txt`](../../docs/llms.txt) and grep for `#### \`<tool_name>\``(e.g.`#### \`create_media_buy\``) to read the exact required + optional field list.** The schema-derived contract lives there; this skill covers patterns, gotchas, and domain-specific examples. Strict response validation is on by default in dev — it will tell you the exact field path if you drift, so write the obvious thing and trust the contract.
|
|
476
|
-
>
|
|
477
|
-
> **Cross-cutting pitfalls matrix runs keep catching:**
|
|
478
|
-
>
|
|
479
|
-
> - **Declare `capabilities.specialisms: ['sales-guaranteed']` (or your actual specialism) on the platform you pass to `createAdcpServerFromPlatform`.** Value is `string[]` of enum ids (not `[{id, version}]`). Agents that don't declare their specialism fail the grader with "No applicable tracks found" even if every tool works — tracks are gated on the specialism claim.
|
|
480
|
-
> - `get_media_buy_delivery` response requires **top-level `currency: string`** (ISO 4217) — per-row `spend.currency` is NOT enough.
|
|
481
|
-
> - `get_media_buy_delivery /media_buy_deliveries[i]/by_package[j]` rows are strict: each requires `package_id`, `spend` (number), `pricing_model`, `rate` (number), and `currency`. A mock that returns `{package_id, impressions, clicks}` fails validation — include the billing quintet on every package row.
|
|
482
|
-
> - `get_media_buy_delivery /reporting_period/start` and `/end` are ISO 8601 **date-time** strings (`YYYY-MM-DDTHH:MM:SS.sssZ` via `new Date().toISOString()`), not date-only. A mock that returns `'2026-04-21'` fails the format check in GA.
|
|
483
|
-
> - `get_media_buys /media_buys[i]` rows require **`media_buy_id`, `status`, `currency`, `total_budget`, `packages`**. When you persist a buy in `create_media_buy`, save `currency` and `total_budget` so the `get_media_buys` response can echo them verbatim — reconstructing later drops one of the required fields in ~every Claude build we've tested.
|
|
484
|
-
> - `sync_accounts` response: each row in `accounts[]` requires **`action: 'created' | 'updated' | 'unchanged' | 'failed'`** (not just `account_id`, `status`). Compare to sync_creatives — same pattern. Omitting `action` fails schema validation at `/accounts/0/action` and blocks every downstream stateful step in the storyboard. Type your row array as `SyncAccountsResponseRow[]` (exported from `@adcp/sdk`) to catch the missing-`action` drift at compile time instead of runtime.
|
|
485
|
-
|
|
486
|
-
**`get_adcp_capabilities`** — register first, empty `{}` schema
|
|
487
|
-
|
|
488
|
-
```
|
|
489
|
-
capabilitiesResponse({
|
|
490
|
-
adcp: { major_versions: [3] },
|
|
491
|
-
supported_protocols: ['media_buy'],
|
|
492
|
-
})
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
**`sync_accounts`** — `SyncAccountsRequestSchema.shape`
|
|
496
|
-
|
|
497
|
-
```
|
|
498
|
-
taskToolResponse({
|
|
499
|
-
accounts: [{
|
|
500
|
-
account_id: string, // required - your platform's ID
|
|
501
|
-
brand: { domain: string },// required - echo back from request
|
|
502
|
-
operator: string, // required - echo back from request
|
|
503
|
-
action: 'created' | 'updated', // required
|
|
504
|
-
status: 'active' | 'pending_approval', // required
|
|
505
|
-
}]
|
|
506
|
-
})
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
**`sync_governance`** — `SyncGovernanceRequestSchema.shape`
|
|
510
|
-
|
|
511
|
-
```
|
|
512
|
-
taskToolResponse({
|
|
513
|
-
accounts: [{
|
|
514
|
-
account: { brand: {...}, operator: string }, // required - echo back
|
|
515
|
-
status: 'synced', // required
|
|
516
|
-
governance_agents: [{ url: string, categories?: string[] }], // required
|
|
517
|
-
}]
|
|
518
|
-
})
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
**`get_products`** — `GetProductsRequestSchema.shape`
|
|
522
|
-
|
|
523
|
-
```
|
|
524
|
-
productsResponse({
|
|
525
|
-
products: [{
|
|
526
|
-
product_id: 'prod-1',
|
|
527
|
-
name: 'Homepage Display',
|
|
528
|
-
description: 'Premium display ads on homepage',
|
|
529
|
-
publisher_properties: [{ publisher_domain: 'example.com', selection_type: 'all' }],
|
|
530
|
-
format_ids: [{ agent_url: 'https://creative.example.com/mcp', id: 'display-300x250' }],
|
|
531
|
-
delivery_type: 'guaranteed',
|
|
532
|
-
pricing_options: [{
|
|
533
|
-
pricing_option_id: 'cpm-standard',
|
|
534
|
-
pricing_model: 'cpm',
|
|
535
|
-
fixed_price: 12.00,
|
|
536
|
-
currency: 'USD',
|
|
537
|
-
}],
|
|
538
|
-
reporting_capabilities: DEFAULT_REPORTING_CAPABILITIES, // from @adcp/sdk/server — stays in sync with schema
|
|
539
|
-
}],
|
|
540
|
-
sandbox: true, // for mock data
|
|
541
|
-
})
|
|
542
|
-
```
|
|
543
|
-
|
|
544
|
-
`DEFAULT_REPORTING_CAPABILITIES` is the SDK-provided default. Hand-rolling this object is an ongoing drift tax — the spec adds required fields (most recently `date_range_support`) and every copy of the literal gets stale. Reach for the constant unless you have a concrete reason to override a field.
|
|
545
|
-
|
|
546
|
-
**`create_media_buy`** — `CreateMediaBuyRequestSchema.shape`
|
|
547
|
-
|
|
548
|
-
Return `adcpError(...)` for all business validation failures. Error-code matrix — all spec-defined rejections on `create_media_buy` / `update_media_buy`:
|
|
549
|
-
|
|
550
|
-
| Tool | Condition | Code |
|
|
551
|
-
| ------------------ | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
552
|
-
| `create_media_buy` | `performance_standards` or `measurement_terms` on a package are unacceptable | `adcpError('TERMS_REJECTED', { message: '...' })` |
|
|
553
|
-
| `create_media_buy` | `product_id` on a package not in catalog | `adcpError('PRODUCT_NOT_FOUND', { field: 'packages[N].product_id' })` |
|
|
554
|
-
| `create_media_buy` | product exists but inventory sold out / unavailable for the requested flight | `adcpError('PRODUCT_UNAVAILABLE', { field: 'packages[N].product_id' })` |
|
|
555
|
-
| `create_media_buy` | budget below the product's floor price | `adcpError('BUDGET_TOO_LOW', { message: '...' })` |
|
|
556
|
-
| `create_media_buy` | reversed dates, schema violation | `adcpError('INVALID_REQUEST', { message: '...' })` |
|
|
557
|
-
| `update_media_buy` | `media_buy_id` not found | `adcpError('MEDIA_BUY_NOT_FOUND', { field: 'media_buy_id' })` |
|
|
558
|
-
| `update_media_buy` | `package_id` within a valid buy not found | `adcpError('PACKAGE_NOT_FOUND', { field: 'package_id' })` |
|
|
559
|
-
|
|
560
|
-
```
|
|
561
|
-
// Success — revision, confirmed_at, and valid_actions are auto-set:
|
|
562
|
-
mediaBuyResponse({
|
|
563
|
-
media_buy_id: string, // required
|
|
564
|
-
status: 'pending_creatives',// triggers valid_actions auto-population
|
|
565
|
-
packages: [{ // required
|
|
566
|
-
package_id: string,
|
|
567
|
-
product_id: string,
|
|
568
|
-
pricing_option_id: string,
|
|
569
|
-
budget: number,
|
|
570
|
-
}],
|
|
571
|
-
})
|
|
572
|
-
|
|
573
|
-
// Validation failure (reversed dates, budget too low, unknown product):
|
|
574
|
-
adcpError('INVALID_REQUEST', { message: 'start_time must be before end_time' })
|
|
575
|
-
```
|
|
576
|
-
|
|
577
|
-
**`get_media_buys`** — `GetMediaBuysRequestSchema.shape`
|
|
578
|
-
|
|
579
|
-
```
|
|
580
|
-
getMediaBuysResponse({
|
|
581
|
-
media_buys: [{
|
|
582
|
-
media_buy_id: string, // required
|
|
583
|
-
status: 'active' | 'pending_start' | 'pending_creatives' | ..., // required
|
|
584
|
-
currency: 'USD', // required
|
|
585
|
-
total_budget: 5000, // required — numeric, same currency as `currency`
|
|
586
|
-
confirmed_at: string, // required for guaranteed approval — ISO timestamp
|
|
587
|
-
packages: [{
|
|
588
|
-
package_id: string, // required
|
|
589
|
-
}],
|
|
590
|
-
}]
|
|
591
|
-
})
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
When you persist a media buy, save `currency` + `total_budget` from the `create_media_buy` request (budgets sum across packages) so subsequent `get_media_buys` calls can return them verbatim. Missing either field on any row fails schema validation and every subsequent step depending on that media_buy's history.
|
|
595
|
-
|
|
596
|
-
**`list_creative_formats`** — `ListCreativeFormatsRequestSchema.shape`
|
|
597
|
-
|
|
598
|
-
```
|
|
599
|
-
listCreativeFormatsResponse({
|
|
600
|
-
formats: [{
|
|
601
|
-
format_id: { agent_url: string, id: string }, // required
|
|
602
|
-
name: string, // required
|
|
603
|
-
renders: [{ // required — at least one render
|
|
604
|
-
role: 'primary', // required
|
|
605
|
-
// oneOf: specify dimensions OR parameters_from_format_id, not both
|
|
606
|
-
dimensions: { width: 300, height: 250 }, // object — defaults to px
|
|
607
|
-
// parameters_from_format_id: true, // alternative: parameters come from format_id
|
|
608
|
-
}],
|
|
609
|
-
}]
|
|
610
|
-
})
|
|
611
|
-
```
|
|
612
|
-
|
|
613
|
-
#### Format asset slots — translating platform-native constraints to AdCP
|
|
614
|
-
|
|
615
|
-
Social and retail-media sellers translate platform-native format catalogs (Meta, Pinterest, TikTok, Criteo, CitrusAd, UniversalAds) into AdCP's `Format.assets[]`. Four recurring footguns fail strict response validation even when the data is "there":
|
|
616
|
-
|
|
617
|
-
1. **Wrong field name for file types.** The spec uses `formats` on image requirements and `containers` on video requirements — NOT `file_types`. Platforms commonly carry `file_types: ['mp4']`; remap to `containers: ['mp4']` for video, `formats: ['jpg', 'png']` for image.
|
|
618
|
-
2. **Wrong unit on duration.** AdCP uses `min_duration_ms` / `max_duration_ms` (milliseconds). Platforms often carry `min_duration_seconds`. Multiply by 1000 on translation.
|
|
619
|
-
3. **Aspect ratios are single-valued per format.** Image pattern `^\d+(\.\d+)?:\d+(\.\d+)?$` allows decimals (`1.91:1`); video is integer-only `^\d+:\d+$`. Comma-joined values (`"1:1,16:9"`) fail — emit separate format variants per ratio.
|
|
620
|
-
4. **`min_count` / `max_count` live on the repeatable_group wrapper.** Carousels, collections, story-pin frames, product showcases are `repeatable_group` assets with `assets[]` inside. Putting counts on an individual asset slot is a spec violation that strict validation rejects.
|
|
621
|
-
|
|
622
|
-
Retail-media sponsored-products formats often reach for `asset_type: 'promoted_offerings'`. That value isn't in the AdCP enum — the correct choice is `asset_type: 'catalog'` with a `CatalogRequirements` object declaring `catalog_type: 'product'` (or `offering`, etc.), `min_items`, `max_items`, and the expected `feed_formats`.
|
|
623
|
-
|
|
624
|
-
Use the typed slot builders — they inject `item_type` and `asset_type`, and the `requirements` object is strictly typed per asset_type, so `file_types`, `min_duration_seconds`, and `min_count` on an individual asset all fail at compile time:
|
|
625
|
-
|
|
626
|
-
```typescript
|
|
627
|
-
import {
|
|
628
|
-
imageAssetSlot,
|
|
629
|
-
videoAssetSlot,
|
|
630
|
-
catalogAssetSlot,
|
|
631
|
-
repeatableGroup,
|
|
632
|
-
imageGroupAsset,
|
|
633
|
-
textGroupAsset,
|
|
634
|
-
} from '@adcp/sdk';
|
|
635
|
-
|
|
636
|
-
// Single image asset slot
|
|
637
|
-
imageAssetSlot({
|
|
638
|
-
asset_id: 'hero_image',
|
|
639
|
-
required: true,
|
|
640
|
-
requirements: { aspect_ratio: '1:1', formats: ['jpg', 'png', 'webp'], max_file_size_kb: 5120 },
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
// Carousel: 2–5 images. Counts on the GROUP, not the individual image.
|
|
644
|
-
repeatableGroup({
|
|
645
|
-
asset_group_id: 'carousel_items',
|
|
646
|
-
required: true,
|
|
647
|
-
min_count: 2,
|
|
648
|
-
max_count: 5,
|
|
649
|
-
selection_mode: 'sequential',
|
|
650
|
-
assets: [
|
|
651
|
-
imageGroupAsset({
|
|
652
|
-
asset_id: 'card_image',
|
|
653
|
-
required: true,
|
|
654
|
-
requirements: { aspect_ratio: '1:1', formats: ['jpg', 'png'] },
|
|
655
|
-
}),
|
|
656
|
-
textGroupAsset({ asset_id: 'card_headline', required: true, requirements: { max_length: 40 } }),
|
|
657
|
-
],
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
// Sponsored-products: catalog slot, not 'promoted_offerings'
|
|
661
|
-
catalogAssetSlot({
|
|
662
|
-
asset_id: 'products',
|
|
663
|
-
required: true,
|
|
664
|
-
requirements: {
|
|
665
|
-
catalog_type: 'product',
|
|
666
|
-
min_items: 3,
|
|
667
|
-
max_items: 10,
|
|
668
|
-
feed_formats: ['google_merchant_center'],
|
|
669
|
-
},
|
|
670
|
-
});
|
|
671
|
-
```
|
|
672
|
-
|
|
673
|
-
Grouped namespace `FormatAsset.image(...)`, `FormatAsset.group(...)`, etc. is available when constructing several slot types together.
|
|
674
|
-
|
|
675
|
-
**`sync_creatives`** — `SyncCreativesRequestSchema.shape`
|
|
676
|
-
|
|
677
|
-
```
|
|
678
|
-
syncCreativesResponse({
|
|
679
|
-
creatives: [{
|
|
680
|
-
creative_id: string, // required - echo from request
|
|
681
|
-
action: 'created' | 'updated', // required
|
|
682
|
-
}]
|
|
683
|
-
})
|
|
684
|
-
```
|
|
685
|
-
|
|
686
|
-
**`get_media_buy_delivery`** — `GetMediaBuyDeliveryRequestSchema.shape`
|
|
687
|
-
|
|
688
|
-
```
|
|
689
|
-
deliveryResponse({
|
|
690
|
-
reporting_period: { start: string, end: string }, // required - ISO timestamps
|
|
691
|
-
currency: 'USD', // required — top-level currency for all totals
|
|
692
|
-
media_buy_deliveries: [{
|
|
693
|
-
media_buy_id: string, // required
|
|
694
|
-
status: 'active', // required
|
|
695
|
-
totals: { impressions: number, spend: number }, // required
|
|
696
|
-
by_package: [], // required (can be empty)
|
|
697
|
-
}]
|
|
698
|
-
})
|
|
699
|
-
```
|
|
700
|
-
|
|
701
|
-
Top-level `currency` is **required** per `get-media-buy-delivery-response.json`. Pull it from the persisted media buy (see `createMediaBuy` above — we flatten request `total_budget.currency` into the buy's top-level `currency` field for this reason). `get_creative_delivery` has the same top-level `currency` requirement.
|
|
702
|
-
|
|
703
|
-
### Context and Ext Passthrough
|
|
704
|
-
|
|
705
|
-
The framework auto-echoes the request's `context` into every response — **do not set `context` yourself in your handler return values.** The framework injects it post-handler only when the field isn't already present.
|
|
706
|
-
|
|
707
|
-
**Crucial:** `context` is schema-typed as an object. If your handler hand-sets a string or narrative description (e.g., "E2E test run", a scenario label, `campaign_context` from the request body), validation fails with `/context: must be object` and the framework does not overwrite. Leave the field out entirely; the framework handles it.
|
|
708
|
-
|
|
709
|
-
Some schemas also define an `ext` field for vendor-namespaced extensions. If your request schema includes `ext`, accept it without error. Tools with explicit `ext` support: `sync_governance`, `provide_performance_feedback`, `sync_event_sources`.
|
|
710
|
-
|
|
711
|
-
## Compliance Testing (Required for deterministic_testing storyboard)
|
|
712
|
-
|
|
713
|
-
To pass the `deterministic_testing` storyboard — and the rejection-branch steps in most other storyboards (`governance_denied`, `invalid_transitions`, `measurement_terms_rejected`, etc.) — your agent must expose the `comply_test_controller` tool. Without it, the grader can only observe the happy path; forced state transitions, error-condition seeding, and simulation all silently degrade to skips or fail with `controller_detected: false`.
|
|
714
|
-
|
|
715
|
-
**Pick by state shape — not by helper quality.** Both helpers below call the same underlying primitives; the split is about the shape of the state you're mutating, not a gradient of abstraction.
|
|
716
|
-
|
|
717
|
-
| Your domain state is… | Use | Worked example |
|
|
718
|
-
| ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
|
|
719
|
-
| Simple — each scenario maps cleanly to one repository method (`seed_creative` → `creativeRepo.upsert`) | `createComplyController` | [`examples/comply-controller-seller.ts`](../../examples/comply-controller-seller.ts) |
|
|
720
|
-
| Typed — media buys with packages / revision / history, creatives with format*id / manifest, seed must populate the same records `get*\*` reads | `registerTestController` + hand-rolled `TestControllerStore` | [`examples/seller-test-controller.ts`](../../examples/seller-test-controller.ts) |
|
|
721
|
-
|
|
722
|
-
### Option A: `createComplyController` (adapter surface)
|
|
723
|
-
|
|
724
|
-
Handles dispatch + validation + re-seed idempotency + sandbox gating for you. Your adapter bodies run; the helper routes:
|
|
725
|
-
|
|
726
|
-
```ts
|
|
727
|
-
import { createComplyController } from '@adcp/sdk/testing';
|
|
728
|
-
|
|
729
|
-
const controller = createComplyController({
|
|
730
|
-
sandboxGate: input => input.auth?.sandbox === true,
|
|
731
|
-
seed: {
|
|
732
|
-
product: params => productRepo.upsert(params.product_id, params.fixture),
|
|
733
|
-
creative: params => creativeRepo.upsert(params.creative_id, params.fixture),
|
|
734
|
-
plan: params => planRepo.upsert(params.plan_id, params.fixture),
|
|
735
|
-
media_buy: params => mediaBuyRepo.upsert(params.media_buy_id, params.fixture),
|
|
736
|
-
},
|
|
737
|
-
force: {
|
|
738
|
-
creative_status: params => creativeRepo.transition(params.creative_id, params.status),
|
|
739
|
-
media_buy_status: params => mediaBuyRepo.transition(params.media_buy_id, params.status),
|
|
740
|
-
account_status: params => accountRepo.setStatus(params.account_id, params.status),
|
|
741
|
-
},
|
|
742
|
-
simulate: {
|
|
743
|
-
delivery: params => deliveryRepo.simulate(params),
|
|
744
|
-
budget_spend: params => budgetRepo.spendPercentage(params),
|
|
745
|
-
},
|
|
746
|
-
});
|
|
747
|
-
|
|
748
|
-
controller.register(server);
|
|
749
|
-
```
|
|
750
|
-
|
|
751
|
-
Omit adapters you don't support — they auto-return `UNKNOWN_SCENARIO` (not schema errors). Throw `TestControllerError('INVALID_TRANSITION', msg, currentState)` from an adapter when the state machine disallows the transition; the helper emits the typed error envelope.
|
|
752
|
-
|
|
753
|
-
Registration auto-emits the `capabilities.compliance_testing.scenarios` block per AdCP 3.0 — `controller.register(server)` wires the tool AND declares capability. Don't add `compliance_testing` to `supported_protocols`; per spec it's a capability block, not a protocol.
|
|
754
|
-
|
|
755
|
-
Validate with: `adcp storyboard run <agent> deterministic_testing --auth $TOKEN`.
|
|
756
|
-
|
|
757
|
-
### Seeding fixtures for compliance
|
|
758
|
-
|
|
759
|
-
Group A storyboards call `comply_test_controller.seed_product` (and `seed_pricing_option`, `seed_creative`, `seed_plan`, `seed_media_buy`) to install a storyboard-specific fixture before hitting the spec tools. Two SDK pieces make this round-trip work without hand-rolling the merge + lookup plumbing.
|
|
760
|
-
|
|
761
|
-
**1. `mergeSeed*` helpers** — permissive merge over your seller defaults. Storyboard fixtures declare only the fields they want to override; everything else (delivery type, channels, reporting capabilities, ...) comes from your baseline. Arrays replace by default; id-keyed lists (`pricing_options`, `publisher_properties`, `packages`, `assets`, plan `findings`) overlay by their id so seeding one entry doesn't wipe the rest.
|
|
762
|
-
|
|
763
|
-
```ts
|
|
764
|
-
import { mergeSeedProduct } from '@adcp/sdk/testing';
|
|
765
|
-
|
|
766
|
-
const baseline: Partial<Product> = {
|
|
767
|
-
delivery_type: 'guaranteed',
|
|
768
|
-
channels: ['display'],
|
|
769
|
-
reporting_capabilities: DEFAULT_REPORTING_CAPABILITIES,
|
|
770
|
-
};
|
|
771
|
-
|
|
772
|
-
// Storyboard seeds sparse fixture: { product_id: 'prd-1', name: 'Homepage' }
|
|
773
|
-
const merged = mergeSeedProduct(baseline, fixture);
|
|
774
|
-
productRepo.upsert(merged.product_id, merged);
|
|
775
|
-
```
|
|
776
|
-
|
|
777
|
-
**2. `bridgeFromTestControllerStore`** — wires your seeded `Map` into `get_products` responses automatically. Sandbox requests see seeded + handler products merged (with seeded winning collisions); production traffic (no sandbox marker, or resolved non-sandbox account) skips the bridge entirely.
|
|
778
|
-
|
|
779
|
-
```ts
|
|
780
|
-
import {
|
|
781
|
-
createAdcpServerFromPlatform,
|
|
782
|
-
bridgeFromTestControllerStore,
|
|
783
|
-
DEFAULT_REPORTING_CAPABILITIES,
|
|
784
|
-
} from '@adcp/sdk/server';
|
|
785
|
-
|
|
786
|
-
const seedStore = new Map<string, unknown>();
|
|
787
|
-
|
|
788
|
-
const server = createAdcpServerFromPlatform(myPlatform, {
|
|
789
|
-
name: 'My Seller',
|
|
790
|
-
version: '1.0.0',
|
|
791
|
-
testController: bridgeFromTestControllerStore(seedStore, {
|
|
792
|
-
delivery_type: 'guaranteed',
|
|
793
|
-
channels: ['display'],
|
|
794
|
-
reporting_capabilities: DEFAULT_REPORTING_CAPABILITIES,
|
|
795
|
-
}),
|
|
796
|
-
});
|
|
797
|
-
|
|
798
|
-
// Wire your createComplyController seed.product adapter to populate seedStore.
|
|
799
|
-
```
|
|
800
|
-
|
|
801
|
-
Your `getSeededProducts` callback — whether you write it by hand or get it via `bridgeFromTestControllerStore` — MUST re-verify that `ctx.account` (or an equivalent scope) is a sandbox account. The framework's sandbox check is a namespace selector, not an authority boundary.
|
|
802
|
-
|
|
803
|
-
### Option B: `registerTestController` (flat store surface)
|
|
804
|
-
|
|
805
|
-
Pick this when your seed and force handlers must mutate typed domain records (`MediaBuyState` with packages / revision / history) that your production tools already read. Session-scoped store factories close over a loaded session so every mutation lands in the same records `get_media_buy` / `sync_creatives` return — the drift class the flat store prevents.
|
|
806
|
-
|
|
807
|
-
See [`examples/seller-test-controller.ts`](../../examples/seller-test-controller.ts) for the end-to-end pattern (typed `MediaBuyState` + `CreativeState`, per-request factory, `enforceMapCap` + `createSeedFixtureCache`, transition guards shared with production code). Sketch:
|
|
808
|
-
|
|
809
|
-
```
|
|
810
|
-
import { registerTestController, type TestControllerStore } from '@adcp/sdk/testing';
|
|
811
|
-
|
|
812
|
-
const store: TestControllerStore = {
|
|
813
|
-
async forceAccountStatus(accountId, status) {
|
|
814
|
-
const prev = accounts.get(accountId);
|
|
815
|
-
if (!prev) throw new TestControllerError('NOT_FOUND', `Account ${accountId} not found`);
|
|
816
|
-
accounts.set(accountId, status);
|
|
817
|
-
return { success: true, previous_state: prev, current_state: status };
|
|
818
|
-
},
|
|
819
|
-
async forceMediaBuyStatus(mediaBuyId, status) {
|
|
820
|
-
const prev = mediaBuys.get(mediaBuyId);
|
|
821
|
-
if (!prev) throw new TestControllerError('NOT_FOUND', `Media buy ${mediaBuyId} not found`);
|
|
822
|
-
const terminal = ['completed', 'rejected', 'canceled'];
|
|
823
|
-
if (terminal.includes(prev))
|
|
824
|
-
throw new TestControllerError('INVALID_TRANSITION', `Cannot transition from ${prev}`, prev);
|
|
825
|
-
mediaBuys.set(mediaBuyId, status);
|
|
826
|
-
return { success: true, previous_state: prev, current_state: status };
|
|
827
|
-
},
|
|
828
|
-
async forceCreativeStatus(creativeId, status, rejectionReason) {
|
|
829
|
-
const prev = creatives.get(creativeId);
|
|
830
|
-
if (!prev) throw new TestControllerError('NOT_FOUND', `Creative ${creativeId} not found`);
|
|
831
|
-
// archived blocks transitions to active states, but archived → rejected is valid (compliance override)
|
|
832
|
-
const activeStatuses = ['processing', 'pending_review', 'approved'];
|
|
833
|
-
if (prev === 'archived' && activeStatuses.includes(status))
|
|
834
|
-
throw new TestControllerError('INVALID_TRANSITION', `Cannot transition from archived to ${status}`, prev);
|
|
835
|
-
creatives.set(creativeId, status);
|
|
836
|
-
return { success: true, previous_state: prev, current_state: status };
|
|
837
|
-
},
|
|
838
|
-
async simulateDelivery(mediaBuyId, params) {
|
|
839
|
-
// params: { impressions?: number, clicks?: number, reported_spend?: { amount, currency }, conversions?: number }
|
|
840
|
-
return { success: true, simulated: { ...params }, cumulative: { ...params } };
|
|
841
|
-
},
|
|
842
|
-
async simulateBudgetSpend(params) {
|
|
843
|
-
return { success: true, simulated: { spend_percentage: params.spend_percentage } };
|
|
844
|
-
},
|
|
845
|
-
};
|
|
846
|
-
|
|
847
|
-
registerTestController(server, store);
|
|
848
|
-
```
|
|
849
|
-
|
|
850
|
-
`registerTestController` auto-emits the `capabilities.compliance_testing.scenarios` block per AdCP 3.0 — scenarios come from the factory's static list or are inferred from the plain store's method presence. Don't add `compliance_testing` to `supported_protocols`; per spec it's a capability block, not a protocol. Unimplemented methods are excluded from `list_scenarios` automatically.
|
|
851
|
-
|
|
852
|
-
The storyboard tests state machine correctness:
|
|
853
|
-
|
|
854
|
-
- `NOT_FOUND` when forcing transitions on unknown entities
|
|
855
|
-
- `INVALID_TRANSITION` when transitioning from terminal states (completed, rejected, canceled for media buys; archived blocks active states like processing/pending_review/approved, but archived → rejected is valid)
|
|
856
|
-
- Successful transitions between valid states
|
|
857
|
-
|
|
858
|
-
Throw `TestControllerError` from store methods for typed errors. The SDK validates status enum values before calling your store.
|
|
859
|
-
|
|
860
|
-
Validate with: `adcp storyboard run <agent> deterministic_testing --json`
|
|
861
|
-
|
|
862
|
-
### Advanced patterns (session-backed stores, map caps, custom wrappers)
|
|
863
|
-
|
|
864
|
-
For production test controllers with persisted-session state (Postgres/Redis/JSONB), the per-session factory shape, `enforceMapCap` for bounded session maps, and custom MCP wrappers with `AsyncLocalStorage` or sandbox gating — see [`docs/guides/BUILD-AN-AGENT.md`](../../docs/guides/BUILD-AN-AGENT.md) § createTaskCapableServer.
|
|
865
|
-
|
|
866
|
-
Key SDK pieces you'll import from `@adcp/sdk`: `CONTROLLER_SCENARIOS`, `enforceMapCap`, `SESSION_ENTRY_CAP`, `handleTestControllerRequest`, `toMcpResponse`, `TOOL_INPUT_SHAPE`.
|
|
867
|
-
|
|
868
|
-
### Opting into `upstream_traffic` (anti-façade contract)
|
|
869
|
-
|
|
870
|
-
Storyboards on `audience-sync`, `sales-social`, `signal-marketplace`, and any specialism with a real upstream platform may declare `check: upstream_traffic` validations (per spec PR adcontextprotocol/adcp#3816). The runner queries your `comply_test_controller`'s `query_upstream_traffic` scenario after each step and asserts your adapter actually called the upstream platform with the storyboard-supplied identifiers — distinguishing a real adapter from one that returns shape-valid AdCP responses without touching upstream.
|
|
871
|
-
|
|
872
|
-
**Prereq:** you've already wired `comply_test_controller` per [§ Compliance Testing](#compliance-testing-required-for-deterministic_testing-storyboard) above. The recorder plugs into the existing controller's scenario dispatch — it doesn't stand up a new tool. If you skipped that section, go back and finish it before continuing here.
|
|
873
|
-
|
|
874
|
-
**Opt-in is voluntary** — adopters who don't advertise `query_upstream_traffic` in `list_scenarios` grade the check `not_applicable`. But adopters who advertise and observe zero calls grade `failed` (the façade signal). Since the storyboards exist to verify your adapter does what it claims, opting in is the load-bearing way to claim the specialism.
|
|
875
|
-
|
|
876
|
-
#### Two traffic surfaces — don't conflate them
|
|
877
|
-
|
|
878
|
-
The SDK has two anti-façade-traffic patterns. They're complementary, not alternatives:
|
|
879
|
-
|
|
880
|
-
```
|
|
881
|
-
buyer-agent / runner
|
|
882
|
-
│
|
|
883
|
-
│ AdCP wire
|
|
884
|
-
▼
|
|
885
|
-
┌──────────────┐ outbound HTTP ┌──────────────────┐
|
|
886
|
-
│ adopter │ ────────────────────────────► │ upstream │
|
|
887
|
-
│ adapter │ (recorder.wrapFetch here) │ platform / mock │
|
|
888
|
-
└──────────────┘ └──────────────────┘
|
|
889
|
-
│ │
|
|
890
|
-
│ comply_test_controller.query_upstream_traffic │ GET /_debug/traffic
|
|
891
|
-
│ (recorder.query() here) │ (mock-server only)
|
|
892
|
-
▼ ▼
|
|
893
|
-
runner verifies matrix harness verifies
|
|
894
|
-
adopter's outbound mock received inbound
|
|
895
|
-
calls + payloads route hits
|
|
896
|
-
```
|
|
897
|
-
|
|
898
|
-
- **Adopter-side `query_upstream_traffic`** (this section): full request payloads with JSONPath assertions, per-principal scoping. Required when the runner can't reach upstream (production sandbox, real platform). The recorder records adapter outbound calls.
|
|
899
|
-
- **Mock-side `/_debug/traffic`** (`src/lib/mock-server/*/server.ts`): per-route hit counts on the mock platform itself. Used by the matrix harness which has direct access to the mock. Anonymous, no payload detail.
|
|
900
|
-
|
|
901
|
-
Don't try to use one for the other. The recorder is an adapter-side concern; mocks expose `_debug/traffic` because they ARE the upstream and have nothing to record outbound.
|
|
902
|
-
|
|
903
|
-
#### What the runner asserts (and what it can't catch)
|
|
904
|
-
|
|
905
|
-
Per spec PR adcontextprotocol/adcp#3816, the runner's `upstream_traffic` validator on each storyboard step:
|
|
906
|
-
|
|
907
|
-
1. Queries `comply_test_controller.query_upstream_traffic` with `since_timestamp = step request start − 250ms`, scoped to the calling principal.
|
|
908
|
-
2. Filters `recorded_calls[]` by the optional `endpoint_pattern` glob.
|
|
909
|
-
3. Checks `min_count` (default 1) — adopters who advertise the scenario but observe zero calls grade `failed` here.
|
|
910
|
-
4. Checks each `payload_must_contain` entry's JSONPath against at least one matching call's `payload`. Non-JSON payloads fall back to substring matching for `match: present`; `equals` / `contains_any` against non-JSON grade `not_applicable`.
|
|
911
|
-
5. Checks `identifier_paths` — extracts every value at each path from the storyboard's `sample_request` and asserts ALL resolved values appear at any depth in some matched call's payload. The recorder records what your adapter sent; the runner matches against what the storyboard sent. Forward identifiers as-received (don't transform them mid-flight, or your recorded payloads won't match).
|
|
912
|
-
|
|
913
|
-
What the recorder CAN'T catch on its own: a thoughtful façade can call `recorder.record()` synthetically with fabricated payloads, or replay-attack with hardcoded recorded*calls, or wrap fetch but never invoke it. The load-bearing anti-façade defense is the storyboard's `identifier_paths` matching against payloads only a real adapter integration would produce. The recorder is the \_surfacing primitive*; the storyboards are where the contract is enforced. Treat opt-in honestly.
|
|
914
|
-
|
|
915
|
-
#### Wire-up — the four steps
|
|
916
|
-
|
|
917
|
-
```ts
|
|
918
|
-
import { createUpstreamRecorder, toQueryUpstreamTrafficResponse } from '@adcp/sdk/upstream-recorder';
|
|
919
|
-
import { registerTestController } from '@adcp/sdk/server';
|
|
920
|
-
|
|
921
|
-
// ─── 1. Boot the recorder (sandbox-only)
|
|
922
|
-
const recorder = createUpstreamRecorder({
|
|
923
|
-
enabled: process.env.NODE_ENV !== 'production',
|
|
924
|
-
// strict: true during integration tests — calls outside runWithPrincipal
|
|
925
|
-
// throw UpstreamRecorderScopeError instead of silently dropping. Surfaces
|
|
926
|
-
// the unwrapped call site as a stack trace, not a mystery zero-result.
|
|
927
|
-
strict: process.env.ADCP_RECORDER_STRICT === '1',
|
|
928
|
-
});
|
|
929
|
-
|
|
930
|
-
// ─── 2. Wrap your HTTP layer (one-time at boot, replaces step 2 if you don't use fetch — see below)
|
|
931
|
-
const recordedFetch = recorder.wrapFetch(globalThis.fetch);
|
|
932
|
-
|
|
933
|
-
// ─── 3. Scope every outbound call inside your AdCP handler to the resolving principal
|
|
934
|
-
async function syncAudiences(req, ctx) {
|
|
935
|
-
await recorder.runWithPrincipal(resolvePrincipal(ctx), async () => {
|
|
936
|
-
await recordedFetch('https://platform.example/v1/audience/upload', {
|
|
937
|
-
/* ... */
|
|
938
|
-
});
|
|
939
|
-
});
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
/**
|
|
943
|
-
* The same string MUST be returned at record-time (here) and at
|
|
944
|
-
* query-time (step 4). Mismatch returns zero — that's the spec's
|
|
945
|
-
* security floor (cross-tenant isolation), not a bug.
|
|
946
|
-
*
|
|
947
|
-
* - OAuth client_credentials sellers: return the resolved `client_id`.
|
|
948
|
-
* - Session-token sellers: return `ctx.account.id`.
|
|
949
|
-
* - Anonymous / public sandbox: a stable per-session identifier
|
|
950
|
-
* (literal "anonymous" collides across tenants and defeats isolation).
|
|
951
|
-
*/
|
|
952
|
-
function resolvePrincipal(ctx) {
|
|
953
|
-
return ctx.account.id; // adjust per your auth layer
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
// ─── 4. Add `queryUpstreamTraffic` as a method on your existing TestControllerStore
|
|
957
|
-
// (this is an entry in the SAME store you registered above for force/seed/simulate
|
|
958
|
-
// scenarios — NOT a separate handler. registerTestController auto-advertises
|
|
959
|
-
// `query_upstream_traffic` in list_scenarios when this method is present).
|
|
960
|
-
registerTestController(adcpServer, {
|
|
961
|
-
// ... your existing force_*, seed_*, simulate_* methods ...
|
|
962
|
-
|
|
963
|
-
queryUpstreamTraffic: async params =>
|
|
964
|
-
toQueryUpstreamTrafficResponse(
|
|
965
|
-
recorder.query({
|
|
966
|
-
principal: resolvePrincipal(ctxFromInput(params)), // same resolver as step 3
|
|
967
|
-
sinceTimestamp: params.since_timestamp,
|
|
968
|
-
endpointPattern: params.endpoint_pattern,
|
|
969
|
-
limit: params.limit,
|
|
970
|
-
})
|
|
971
|
-
),
|
|
972
|
-
});
|
|
973
|
-
```
|
|
974
|
-
|
|
975
|
-
#### Multi-tenant adopters: factory shape with per-request principal resolution
|
|
976
|
-
|
|
977
|
-
The wire-up above hardcodes `RECORDER_PRINCIPAL` because the example uses static-API-key auth (one principal for the whole process). Multi-tenant adopters — OAuth `client_credentials` sellers, session-token sellers, anyone whose `comply_test_controller` request can come from different tenants — MUST resolve the principal **per request** so cross-tenant isolation actually holds. Use `registerTestController`'s factory shape, which runs a fresh `createStore` per request with access to the controller's input:
|
|
978
|
-
|
|
979
|
-
```ts
|
|
980
|
-
import { CONTROLLER_SCENARIOS } from '@adcp/sdk/server';
|
|
981
|
-
|
|
982
|
-
registerTestController(adcpServer, {
|
|
983
|
-
// Static capability list — answered without invoking createStore so
|
|
984
|
-
// list_scenarios pings don't pay a session-rehydration cost.
|
|
985
|
-
scenarios: [
|
|
986
|
-
CONTROLLER_SCENARIOS.FORCE_CREATIVE_STATUS,
|
|
987
|
-
// ... your existing canonical scenarios ...
|
|
988
|
-
'query_upstream_traffic', // extension scenario, advertised verbatim
|
|
989
|
-
],
|
|
990
|
-
createStore: async input => {
|
|
991
|
-
// input is the parsed comply_test_controller request — { scenario,
|
|
992
|
-
// params, context, ext, account } as the buyer sent it. Resolve the
|
|
993
|
-
// calling principal from your auth layer here.
|
|
994
|
-
const principal = await resolvePrincipal(input);
|
|
995
|
-
|
|
996
|
-
return {
|
|
997
|
-
// ... your existing force/seed/simulate methods ...
|
|
998
|
-
|
|
999
|
-
queryUpstreamTraffic: async params =>
|
|
1000
|
-
toQueryUpstreamTrafficResponse(
|
|
1001
|
-
recorder.query({
|
|
1002
|
-
principal,
|
|
1003
|
-
sinceTimestamp: params.since_timestamp,
|
|
1004
|
-
endpointPattern: params.endpoint_pattern,
|
|
1005
|
-
limit: params.limit,
|
|
1006
|
-
})
|
|
1007
|
-
),
|
|
1008
|
-
};
|
|
1009
|
-
},
|
|
1010
|
-
});
|
|
1011
|
-
```
|
|
1012
|
-
|
|
1013
|
-
The `resolvePrincipal(input)` resolver is where adopter auth-layer choice lives:
|
|
1014
|
-
|
|
1015
|
-
```ts
|
|
1016
|
-
async function resolvePrincipal(input: Record<string, unknown>): Promise<string> {
|
|
1017
|
-
// OAuth client_credentials: derive from your token-introspection cache,
|
|
1018
|
-
// keyed off the bearer the request arrived with. The same principal
|
|
1019
|
-
// string MUST be returned by your handler-side `runWithPrincipal(p, fn)`
|
|
1020
|
-
// wrapper — keep both call sites pointing at one resolver.
|
|
1021
|
-
const oauth = await loadOAuthSession(input);
|
|
1022
|
-
if (oauth) return oauth.client_id;
|
|
1023
|
-
|
|
1024
|
-
// Session-token sellers: resolve account from the session id and use
|
|
1025
|
-
// its stable account.id.
|
|
1026
|
-
const session = (input.context as { session_id?: string })?.session_id;
|
|
1027
|
-
if (session) {
|
|
1028
|
-
const acct = await accountFromSession(session);
|
|
1029
|
-
return acct.id;
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
// Public-sandbox adopters serving anonymous traffic: a stable
|
|
1033
|
-
// per-session identifier you mint at session start. The literal
|
|
1034
|
-
// string "anonymous" collides across tenants — don't use it.
|
|
1035
|
-
throw new Error('No auth context available; cannot scope upstream-traffic query');
|
|
1036
|
-
}
|
|
1037
|
-
```
|
|
1038
|
-
|
|
1039
|
-
On the AdCP-handler side, call the **same** `resolvePrincipal` from `runWithPrincipal`. The `ctx` an AdCP handler receives carries the same auth shape your controller-side `input` does (often `ctx.account`, sometimes `ctx.authInfo` for the raw transport layer). Wrap a thin shim:
|
|
1040
|
-
|
|
1041
|
-
```ts
|
|
1042
|
-
async function syncAudiences(req, ctx) {
|
|
1043
|
-
const principal = await resolvePrincipal(adcpCtxToControllerInput(ctx));
|
|
1044
|
-
await recorder.runWithPrincipal(principal, async () => {
|
|
1045
|
-
await fetch('https://platform.example/v1/audience/upload', {
|
|
1046
|
-
/* ... */
|
|
1047
|
-
});
|
|
1048
|
-
});
|
|
1049
|
-
}
|
|
1050
|
-
```
|
|
1051
|
-
|
|
1052
|
-
If the two call sites pick the principal differently — for example, controller-side resolves from `input.account.operator` but handler-side uses `ctx.account.id` directly — your storyboard runs grade `failed` with `matched_count: 0` and the diagnostic looks identical to a façade. Use one resolver, call it from both ends.
|
|
1053
|
-
|
|
1054
|
-
#### Adopters not on `fetch`
|
|
1055
|
-
|
|
1056
|
-
`recorder.wrapFetch` covers most adapters. If you use `axios` / `got` / native `node:http`, **replace step 2** with a per-client interceptor + manual `recorder.record()` (don't combine with `wrapFetch` — pick one):
|
|
1057
|
-
|
|
1058
|
-
```ts
|
|
1059
|
-
// Axios — interceptor on the response side so we have the status code
|
|
1060
|
-
axiosInstance.interceptors.response.use(
|
|
1061
|
-
resp => {
|
|
1062
|
-
recorder.record({
|
|
1063
|
-
method: resp.config.method?.toUpperCase() ?? 'GET',
|
|
1064
|
-
url: resp.config.url ?? '',
|
|
1065
|
-
content_type: resp.config.headers['content-type'] ?? '',
|
|
1066
|
-
headers: resp.config.headers,
|
|
1067
|
-
payload: resp.config.data,
|
|
1068
|
-
status_code: resp.status,
|
|
1069
|
-
});
|
|
1070
|
-
return resp;
|
|
1071
|
-
},
|
|
1072
|
-
err => {
|
|
1073
|
-
/* still record on error — same shape, omit status_code */ throw err;
|
|
1074
|
-
}
|
|
1075
|
-
);
|
|
1076
|
-
|
|
1077
|
-
// got — afterResponse hook
|
|
1078
|
-
got.extend({
|
|
1079
|
-
hooks: {
|
|
1080
|
-
afterResponse: [
|
|
1081
|
-
response => {
|
|
1082
|
-
recorder.record({
|
|
1083
|
-
method: response.request.options.method,
|
|
1084
|
-
url: response.request.requestUrl.toString(),
|
|
1085
|
-
content_type: response.request.options.headers['content-type'] ?? '',
|
|
1086
|
-
headers: response.request.options.headers,
|
|
1087
|
-
payload: response.request.options.body,
|
|
1088
|
-
status_code: response.statusCode,
|
|
1089
|
-
});
|
|
1090
|
-
return response;
|
|
1091
|
-
},
|
|
1092
|
-
],
|
|
1093
|
-
},
|
|
1094
|
-
});
|
|
1095
|
-
```
|
|
1096
|
-
|
|
1097
|
-
`runWithPrincipal` (step 3) still applies — adapters not on fetch still need to scope their handler bodies so the recorder knows which principal each call belongs to.
|
|
1098
|
-
|
|
1099
|
-
#### Debugging "my storyboard says zero calls but I see them in my logs"
|
|
1100
|
-
|
|
1101
|
-
Three checks, in order:
|
|
1102
|
-
|
|
1103
|
-
1. **`recorder.debug()` from inside your handler.** Returns `{ enabled, bufferSize, bufferedEntries, principals, lastRecordedAt, activePrincipal, strict }`. Verify `enabled: true`, `bufferedEntries > 0`, and that `activePrincipal` matches what your controller handler will pass at query time. Mismatch is the most common adopter bug — a typo or stale-cache resolver that returns a different string at record vs query time silently returns `[]`.
|
|
1104
|
-
2. **`strict: true`** during integration test runs. Calls outside `runWithPrincipal` throw `UpstreamRecorderScopeError` instead of silently dropping. Surfaces unwrapped call sites as stack traces.
|
|
1105
|
-
3. **`onError` hook** for production-style observability — fires on classifier throws, URL parse failures, payload-build failures, and unscoped records. Wire to your logger.
|
|
1106
|
-
|
|
1107
|
-
`enabled: false` returns a no-op recorder for production builds — zero per-call overhead. See `@adcp/sdk/upstream-recorder`'s module-level docs for the full surface (`strict`, `debug()`, `onError`, `redactPattern`, `maxPayloadBytes`, `purpose` classifier, `bufferSize` / `ttlMs` tuning).
|
|
1108
|
-
|
|
1109
|
-
**Worked reference adapter**: [`examples/hello_signals_adapter_marketplace.ts`](../../examples/hello_signals_adapter_marketplace.ts) wires the recorder end-to-end against the SDK's `signal-marketplace` mock. CI gates that example via `test/examples/hello-signals-adapter-marketplace.test.js` — type-checks under strictest tsc, runs the storyboard with zero failed steps, asserts every expected upstream route was hit. Fork the example and replace `UpstreamClient` with your real backend.
|
|
1110
|
-
|
|
1111
|
-
## SDK Quick Reference
|
|
1112
|
-
|
|
1113
|
-
| SDK piece | Usage |
|
|
1114
|
-
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1115
|
-
| `createAdcpServerFromPlatform(platform, opts)` | Build a server from a typed `DecisioningPlatform` — compile-time specialism enforcement, ctx_metadata round-trip, idempotency-principal synthesis, status mappers, webhook auto-emit |
|
|
1116
|
-
| `createAdcpServer(config)` _(legacy)_ | v5 handler-bag entry. Mid-migration / escape-hatch only; reach via `@adcp/sdk/server/legacy/v5` |
|
|
1117
|
-
| `serve(() => createAdcpServerFromPlatform(platform, opts))` | Start HTTP server on `:3001/mcp` |
|
|
1118
|
-
| `ctx.store` | State store in every handler — `get`, `put`, `patch`, `delete`, `list` |
|
|
1119
|
-
| `InMemoryStateStore` | Default state store (dev/testing) |
|
|
1120
|
-
| `PostgresStateStore` | Production state store (shared across instances) |
|
|
1121
|
-
| `DEFAULT_REPORTING_CAPABILITIES` | Use as `reporting_capabilities: DEFAULT_REPORTING_CAPABILITIES` on products |
|
|
1122
|
-
| `checkGovernance(options)` | Call governance agent before financial commits |
|
|
1123
|
-
| `governanceDeniedError(result)` | Convert governance denial to GOVERNANCE_DENIED error |
|
|
1124
|
-
| `mediaBuyResponse(data)` | Auto-applied for `createMediaBuy` (sets revision, confirmed_at, valid_actions) |
|
|
1125
|
-
| `adcpError(code, { message })` | Structured error (e.g., `BUDGET_TOO_LOW`, `PRODUCT_NOT_FOUND`) |
|
|
1126
|
-
| `registerTestController(server, store \| { scenarios, createStore })` | Add `comply_test_controller`. Plain store or per-request factory. |
|
|
1127
|
-
| `TestControllerError(code, message)` | Typed error from store methods |
|
|
1128
|
-
| `handleTestControllerRequest(store, input)` | Low-level dispatch for custom MCP wrappers |
|
|
1129
|
-
| `toMcpResponse(response)` / `TOOL_INPUT_SHAPE` | MCP envelope + Zod input schema for custom wrappers |
|
|
1130
|
-
| `enforceMapCap(map, key, label, cap?)` | Reject net-new keys once a session Map hits `SESSION_ENTRY_CAP` (1000) |
|
|
1131
|
-
| `expectControllerError(result, code)` / `expectControllerSuccess(result)` | Unit-test assertions — narrow responses to error or success arms |
|
|
1132
|
-
|
|
1133
|
-
Response builders (`productsResponse`, `mediaBuyResponse`, `deliveryResponse`, etc.) are auto-applied by the framework — you return the data, the framework wraps it. You only need to call them directly for tools without a dedicated builder.
|
|
1134
|
-
|
|
1135
|
-
Import everything from `@adcp/sdk`. Types from `@adcp/sdk` with `import type`.
|
|
1136
|
-
|
|
1137
|
-
## Setup
|
|
1138
|
-
|
|
1139
|
-
```bash
|
|
1140
|
-
npm init -y
|
|
1141
|
-
npm install @adcp/sdk
|
|
1142
|
-
npm install -D typescript @types/node
|
|
1143
|
-
```
|
|
1144
|
-
|
|
1145
|
-
Minimal `tsconfig.json`:
|
|
1146
|
-
|
|
1147
|
-
```json
|
|
1148
|
-
{
|
|
1149
|
-
"compilerOptions": {
|
|
1150
|
-
"target": "ES2022",
|
|
1151
|
-
"module": "Node16",
|
|
1152
|
-
"moduleResolution": "Node16",
|
|
1153
|
-
"strict": true,
|
|
1154
|
-
"skipLibCheck": true,
|
|
1155
|
-
"outDir": "dist"
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
```
|
|
1159
|
-
|
|
1160
|
-
`skipLibCheck: true` avoids false-positive errors from transitive `.d.ts` files (e.g., `@opentelemetry/api`).
|
|
1161
|
-
|
|
1162
|
-
## Implementation
|
|
1163
|
-
|
|
1164
|
-
Use `createAdcpServerFromPlatform` — it auto-wires schemas, response builders, and `get_adcp_capabilities` from the typed `DecisioningPlatform` you provide. Handlers receive `(params, ctx)` where `ctx.store` persists state, `ctx.account` is the resolved account, and `ctx.ctxMetadata` is the resource-keyed cache (when wired).
|
|
1165
|
-
|
|
1166
|
-
**Imports**: most things live at `@adcp/sdk`. The idempotency store helpers (`createIdempotencyStore`, `memoryBackend`, `pgBackend`) live at the narrower `@adcp/sdk/server` subpath. Both are re-exported from the root — either works — but splitting them makes intent obvious.
|
|
1167
|
-
|
|
1168
|
-
```typescript
|
|
1169
|
-
import { randomUUID } from 'node:crypto';
|
|
1170
|
-
import {
|
|
1171
|
-
createAdcpServerFromPlatform,
|
|
1172
|
-
serve,
|
|
1173
|
-
adcpError,
|
|
1174
|
-
InMemoryStateStore,
|
|
1175
|
-
checkGovernance,
|
|
1176
|
-
governanceDeniedError,
|
|
1177
|
-
createIdempotencyStore,
|
|
1178
|
-
memoryBackend,
|
|
1179
|
-
type DecisioningPlatform,
|
|
1180
|
-
type SalesPlatform,
|
|
1181
|
-
type AccountStore,
|
|
1182
|
-
} from '@adcp/sdk/server';
|
|
1183
|
-
import type { ServeContext, MediaBuyStatus } from '@adcp/sdk';
|
|
1184
|
-
|
|
1185
|
-
// Publisher-typed metadata blob round-tripped via Account.ctx_metadata.
|
|
1186
|
-
// Whatever shape your adapter wants — the SDK doesn't inspect it.
|
|
1187
|
-
interface MySellerMeta {
|
|
1188
|
-
governanceUrl?: string;
|
|
1189
|
-
brand?: string;
|
|
1190
|
-
operator?: string;
|
|
1191
|
-
[key: string]: unknown;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
const stateStore = new InMemoryStateStore(); // shared across requests
|
|
1195
|
-
|
|
1196
|
-
// Idempotency — required for any AdCP-3-compliant seller that accepts
|
|
1197
|
-
// mutating requests. `createIdempotencyStore` throws if `ttlSeconds` is
|
|
1198
|
-
// outside the spec bounds (3600–604800).
|
|
1199
|
-
const idempotency = createIdempotencyStore({
|
|
1200
|
-
backend: memoryBackend(), // pgBackend(pool) for production
|
|
1201
|
-
ttlSeconds: 86400, // 24 hours
|
|
1202
|
-
});
|
|
1203
|
-
|
|
1204
|
-
class MySeller implements DecisioningPlatform<{}, MySellerMeta> {
|
|
1205
|
-
capabilities = {
|
|
1206
|
-
specialisms: ['sales-non-guaranteed'] as const,
|
|
1207
|
-
pricingModels: ['cpm'] as const,
|
|
1208
|
-
channels: ['display'] as const,
|
|
1209
|
-
config: {},
|
|
1210
|
-
};
|
|
1211
|
-
|
|
1212
|
-
accounts: AccountStore<MySellerMeta> = {
|
|
1213
|
-
// accounts.resolve runs BEFORE idempotency / handler dispatch. If it
|
|
1214
|
-
// returns null for a valid-shape reference, every mutating request
|
|
1215
|
-
// short-circuits as ACCOUNT_NOT_FOUND — which masks idempotency
|
|
1216
|
-
// conformance (missing-key / replay tests fail with the wrong code).
|
|
1217
|
-
// Handle BOTH branches of AccountReference:
|
|
1218
|
-
// { account_id } — your own persisted accounts.
|
|
1219
|
-
// { brand: { domain }, operator } — the canonical spec shape.
|
|
1220
|
-
// Conformance storyboards use this by default (e.g. brand.domain
|
|
1221
|
-
// "acmeoutdoor.example", operator "pinnacle-agency.example").
|
|
1222
|
-
resolve: async (ref, ctx) => {
|
|
1223
|
-
if ('account_id' in ref) {
|
|
1224
|
-
const acc = await stateStore.get('accounts', ref.account_id);
|
|
1225
|
-
return acc ?? null;
|
|
1226
|
-
}
|
|
1227
|
-
if ('brand' in ref && ref.brand?.domain && ref.operator) {
|
|
1228
|
-
// Dev/compliance mode: auto-materialize for any valid brand+operator
|
|
1229
|
-
// so conformance tests reach the handler. Production replaces this
|
|
1230
|
-
// with a real lookup against your tenant registry; returning null
|
|
1231
|
-
// for unknown tenants surfaces ACCOUNT_NOT_FOUND correctly.
|
|
1232
|
-
return {
|
|
1233
|
-
id: `${ref.operator}:${ref.brand.domain}`,
|
|
1234
|
-
operator: ref.operator,
|
|
1235
|
-
ctx_metadata: { brand: ref.brand.domain, operator: ref.operator },
|
|
1236
|
-
};
|
|
1237
|
-
}
|
|
1238
|
-
return null;
|
|
1239
|
-
},
|
|
1240
|
-
upsert: async (params, ctx) => {
|
|
1241
|
-
/* sync_accounts impl */
|
|
1242
|
-
return { ok: true, items: [] };
|
|
1243
|
-
},
|
|
1244
|
-
list: async (params, ctx) => ({ items: [], nextCursor: null }),
|
|
1245
|
-
};
|
|
1246
|
-
|
|
1247
|
-
sales: SalesPlatform<MySellerMeta> = {
|
|
1248
|
-
getProducts: async (req, ctx) => {
|
|
1249
|
-
return { products: PRODUCTS, sandbox: true };
|
|
1250
|
-
// productsResponse() auto-applied by framework
|
|
1251
|
-
},
|
|
1252
|
-
|
|
1253
|
-
createMediaBuy: async (req, ctx) => {
|
|
1254
|
-
// Governance check for financial commitment. The publisher's
|
|
1255
|
-
// governance URL rides on Account.ctx_metadata so any per-tenant
|
|
1256
|
-
// override is read at request time.
|
|
1257
|
-
const govUrl = ctx.account?.ctx_metadata?.governanceUrl;
|
|
1258
|
-
if (typeof govUrl === 'string') {
|
|
1259
|
-
const gov = await checkGovernance({
|
|
1260
|
-
agentUrl: govUrl,
|
|
1261
|
-
planId: (req as { plan_id?: string }).plan_id ?? 'default',
|
|
1262
|
-
caller: 'https://my-agent.com/mcp',
|
|
1263
|
-
tool: 'create_media_buy',
|
|
1264
|
-
payload: req,
|
|
1265
|
-
});
|
|
1266
|
-
if (!gov.approved) return governanceDeniedError(gov);
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
// Use randomUUID (not Date.now) so ids are unguessable — a guessable
|
|
1270
|
-
// media_buy_id lets another buyer probe or cancel. Same applies to
|
|
1271
|
-
// any seller-issued id (package_id, creative_id, etc.).
|
|
1272
|
-
// `currency` + `total_budget` are REQUIRED on get_media_buys response
|
|
1273
|
-
// rows. The request carries them under `total_budget: { amount, currency }`.
|
|
1274
|
-
// Flatten to top-level fields at create time — storing only
|
|
1275
|
-
// `packages[].budget` and reconstructing later fails schema validation
|
|
1276
|
-
// in get_media_buys/update_media_buy.
|
|
1277
|
-
const totalBudget = req.total_budget;
|
|
1278
|
-
const currency = typeof totalBudget === 'object' && totalBudget ? (totalBudget.currency ?? 'USD') : 'USD';
|
|
1279
|
-
const amount =
|
|
1280
|
-
typeof totalBudget === 'object' && totalBudget
|
|
1281
|
-
? (totalBudget.amount ?? 0)
|
|
1282
|
-
: typeof totalBudget === 'number'
|
|
1283
|
-
? totalBudget
|
|
1284
|
-
: 0;
|
|
1285
|
-
|
|
1286
|
-
const buy = {
|
|
1287
|
-
media_buy_id: `mb_${randomUUID()}`,
|
|
1288
|
-
status: 'pending_creatives' as const,
|
|
1289
|
-
currency,
|
|
1290
|
-
total_budget: amount,
|
|
1291
|
-
packages:
|
|
1292
|
-
req.packages?.map(pkg => ({
|
|
1293
|
-
package_id: `pkg_${randomUUID()}`,
|
|
1294
|
-
product_id: pkg.product_id,
|
|
1295
|
-
pricing_option_id: pkg.pricing_option_id,
|
|
1296
|
-
budget: pkg.budget,
|
|
1297
|
-
})) ?? [],
|
|
1298
|
-
};
|
|
1299
|
-
await ctx.store.put('media_buys', buy.media_buy_id, buy);
|
|
1300
|
-
return buy; // mediaBuyResponse() auto-applied (sets revision, confirmed_at, valid_actions)
|
|
1301
|
-
},
|
|
1302
|
-
|
|
1303
|
-
updateMediaBuy: async (mediaBuyId, patch, ctx) => {
|
|
1304
|
-
const existing = await ctx.store.get('media_buys', mediaBuyId);
|
|
1305
|
-
if (!existing) {
|
|
1306
|
-
return adcpError('MEDIA_BUY_NOT_FOUND', {
|
|
1307
|
-
message: `No media buy with id ${mediaBuyId}`,
|
|
1308
|
-
field: 'media_buy_id',
|
|
1309
|
-
});
|
|
1310
|
-
}
|
|
1311
|
-
// Only merge the fields you want to persist — do NOT spread `patch`
|
|
1312
|
-
// wholesale. The patch carries envelope fields (idempotency_key,
|
|
1313
|
-
// context) that have no business in your domain state. Spreading
|
|
1314
|
-
// them pollutes `get_media_buys` responses and breaks dedup.
|
|
1315
|
-
|
|
1316
|
-
// State machine: creative_assignments arriving advances pending_creatives.
|
|
1317
|
-
// pending_creatives → pending_start (start_time in future) or active (start_time now/past).
|
|
1318
|
-
let status = existing.status as MediaBuyStatus;
|
|
1319
|
-
if (patch.paused === true) {
|
|
1320
|
-
status = 'paused';
|
|
1321
|
-
} else if (
|
|
1322
|
-
status === 'pending_creatives' &&
|
|
1323
|
-
(patch.packages ?? []).some(
|
|
1324
|
-
(p: { creative_assignments?: unknown[] }) => (p.creative_assignments ?? []).length > 0
|
|
1325
|
-
)
|
|
1326
|
-
) {
|
|
1327
|
-
const startTime = existing.start_time ? new Date(existing.start_time as string) : null;
|
|
1328
|
-
status = startTime && startTime > new Date() ? 'pending_start' : 'active';
|
|
1329
|
-
} else if (patch.paused === false && status === 'paused') {
|
|
1330
|
-
status = 'active';
|
|
1331
|
-
}
|
|
23
|
+
For exact response shapes, error codes, and optional fields, `docs/llms.txt` is the canonical reference. The fork target stays in sync with the spec because PR #1394's three-gate contract fails CI when it drifts.
|
|
1332
24
|
|
|
1333
|
-
|
|
1334
|
-
await ctx.store.put('media_buys', mediaBuyId, updated);
|
|
1335
|
-
return {
|
|
1336
|
-
media_buy_id: mediaBuyId,
|
|
1337
|
-
status: updated.status as MediaBuyStatus,
|
|
1338
|
-
// `affected_packages` is `Package[]` (per `/schemas/latest/core/package.json`)
|
|
1339
|
-
// — objects with at minimum `package_id`. Don't return bare strings;
|
|
1340
|
-
// the update-media-buy-response oneOf discriminates against them and
|
|
1341
|
-
// the error looks like `/affected_packages/0: must be object`.
|
|
1342
|
-
affected_packages: (existing.packages ?? []).map((p: { package_id: string }) => ({
|
|
1343
|
-
package_id: p.package_id,
|
|
1344
|
-
})),
|
|
1345
|
-
};
|
|
1346
|
-
},
|
|
25
|
+
## When to use this skill
|
|
1347
26
|
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
return { media_buys: result.items };
|
|
1351
|
-
},
|
|
27
|
+
- User wants to sell ad inventory (publisher, SSP, retail media network)
|
|
28
|
+
- User mentions `get_products`, `create_media_buy`, or the media buy protocol
|
|
1352
29
|
|
|
1353
|
-
|
|
1354
|
-
/* ... */
|
|
1355
|
-
return {
|
|
1356
|
-
currency: 'USD',
|
|
1357
|
-
reporting_period: {
|
|
1358
|
-
start: filter.start_date ?? '2026-01-01',
|
|
1359
|
-
end: filter.end_date ?? '2026-01-31',
|
|
1360
|
-
},
|
|
1361
|
-
media_buy_deliveries: [],
|
|
1362
|
-
};
|
|
1363
|
-
},
|
|
1364
|
-
|
|
1365
|
-
listCreativeFormats: async (params, ctx) => ({ formats: [] }),
|
|
1366
|
-
|
|
1367
|
-
// Response is `creatives: [{ creative_id, action }]` per the spec response
|
|
1368
|
-
// schema — NOT `synced_creatives`. v6 takes the creatives array directly;
|
|
1369
|
-
// the framework unpacks the request envelope.
|
|
1370
|
-
syncCreatives: async (creatives, ctx) =>
|
|
1371
|
-
creatives.map(c => ({
|
|
1372
|
-
creative_id: (c as { creative_id?: string }).creative_id ?? `cr_${randomUUID()}`,
|
|
1373
|
-
action: 'created' as const,
|
|
1374
|
-
})),
|
|
1375
|
-
};
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
const platform = new MySeller();
|
|
1379
|
-
|
|
1380
|
-
function createAgent({ taskStore }: ServeContext) {
|
|
1381
|
-
return createAdcpServerFromPlatform(platform, {
|
|
1382
|
-
name: 'My Seller Agent',
|
|
1383
|
-
version: '1.0.0',
|
|
1384
|
-
taskStore,
|
|
1385
|
-
stateStore,
|
|
1386
|
-
idempotency,
|
|
1387
|
-
// Principal scoping for idempotency. MUST never return undefined — or
|
|
1388
|
-
// every mutating request rejects as SERVICE_UNAVAILABLE. A constant is
|
|
1389
|
-
// fine for a demo; for multi-tenant production use `ctx.account.id`.
|
|
1390
|
-
resolveSessionKey: () => 'default-principal',
|
|
1391
|
-
});
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
serve(createAgent);
|
|
1395
|
-
```
|
|
1396
|
-
|
|
1397
|
-
Key points:
|
|
1398
|
-
|
|
1399
|
-
1. Single `.ts` file — one `DecisioningPlatform` class passed to `createAdcpServerFromPlatform`
|
|
1400
|
-
2. `get_adcp_capabilities` is auto-generated from your handlers — don't register it manually (idempotency capability is auto-declared too)
|
|
1401
|
-
3. Response builders are auto-applied — just return the data
|
|
1402
|
-
4. Use `ctx.store` for state — persists across stateless HTTP requests
|
|
1403
|
-
5. Set `sandbox: true` on all mock/demo responses
|
|
1404
|
-
6. Use `adcpError()` for business validation failures — see the error-code matrix in § create_media_buy above
|
|
1405
|
-
7. Use `as const` on string literal arrays and union-typed fields in product definitions — TypeScript infers `string[]` from `['display', 'olv']` but the SDK requires specific union types like `MediaChannel[]`. Apply `as const` to `channels`, `delivery_type`, `selection_type`, and `pricing_model` values.
|
|
1406
|
-
8. `pending_creatives` is a transient state — `update_media_buy` MUST advance it to `pending_start` or `active` when `creative_assignments` arrive (see state-machine logic in § update_media_buy above)
|
|
1407
|
-
|
|
1408
|
-
## Governance
|
|
1409
|
-
|
|
1410
|
-
The Implementation example above already shows the baseline `checkGovernance()` call on `create_media_buy`. This section covers the regulated-category flow: when a buyer's governance plan marks a media buy as requiring human review, you MUST route it to a human approver before committing spend.
|
|
1411
|
-
|
|
1412
|
-
### GDPR Art 22 / EU AI Act — when to require human review
|
|
1413
|
-
|
|
1414
|
-
**Why.** GDPR Article 22 bars fully automated decisions with legal or similarly significant effect on the data subject in certain contexts. The EU AI Act classifies some advertising use cases as Annex III high-risk — employment ads, credit offers, housing, insurance, education. For regulated verticals, fully automated media buys are non-compliant; the seller must put a human in the loop and preserve the decision record.
|
|
1415
|
-
|
|
1416
|
-
The buyer signals this by setting `plan.human_review_required: true` on the governance plan. AdCP 3.0 GA made this the canonical field — replacing `budget.authority_level: 'human_required'` from earlier drafts.
|
|
1417
|
-
|
|
1418
|
-
**Seller obligation.** On `create_media_buy`:
|
|
1419
|
-
|
|
1420
|
-
1. Read `plan.human_review_required` from the buyer's governance plan (which you already fetched / synced via `sync_plans` or the inbound governance check).
|
|
1421
|
-
2. If `true` — enqueue the buy for human approval and return `status: 'submitted'` with a `task_id` the buyer can poll. Do NOT execute the buy until an approver signs off.
|
|
1422
|
-
3. On approval, construct the override artifact with `buildHumanOverride({ reason, approver, approvedAt })` and persist it alongside the completed buy. The override is the compliance evidence that a human authorized the automated path.
|
|
1423
|
-
4. If `false` — proceed with the normal `checkGovernance()` flow and commit.
|
|
1424
|
-
|
|
1425
|
-
### Worked example
|
|
1426
|
-
|
|
1427
|
-
```typescript
|
|
1428
|
-
import {
|
|
1429
|
-
createAdcpServerFromPlatform,
|
|
1430
|
-
serve,
|
|
1431
|
-
adcpError,
|
|
1432
|
-
buildHumanOverride,
|
|
1433
|
-
checkGovernance,
|
|
1434
|
-
governanceDeniedError,
|
|
1435
|
-
taskToolResponse,
|
|
1436
|
-
type DecisioningPlatform,
|
|
1437
|
-
type SalesPlatform,
|
|
1438
|
-
type AccountStore,
|
|
1439
|
-
type AdcpStateStore,
|
|
1440
|
-
} from '@adcp/sdk/server';
|
|
1441
|
-
import { randomUUID } from 'node:crypto';
|
|
1442
|
-
|
|
1443
|
-
interface RegulatedMeta {
|
|
1444
|
-
governanceUrl?: string;
|
|
1445
|
-
[key: string]: unknown;
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
class RegulatedPublisher implements DecisioningPlatform<{}, RegulatedMeta> {
|
|
1449
|
-
capabilities = {
|
|
1450
|
-
specialisms: ['sales-guaranteed'] as const,
|
|
1451
|
-
pricingModels: ['cpm'] as const,
|
|
1452
|
-
channels: ['display'] as const,
|
|
1453
|
-
config: {},
|
|
1454
|
-
};
|
|
1455
|
-
|
|
1456
|
-
accounts: AccountStore<RegulatedMeta> = {
|
|
1457
|
-
resolve: async ref => db.findAccount(ref),
|
|
1458
|
-
upsert: async () => ({ ok: true, items: [] }),
|
|
1459
|
-
list: async () => ({ items: [], nextCursor: null }),
|
|
1460
|
-
};
|
|
1461
|
-
|
|
1462
|
-
sales: SalesPlatform<RegulatedMeta> = {
|
|
1463
|
-
getProducts: async () => ({ products: [] }),
|
|
1464
|
-
|
|
1465
|
-
createMediaBuy: async (req, ctx) => {
|
|
1466
|
-
if (!ctx.account) {
|
|
1467
|
-
return adcpError('ACCOUNT_NOT_FOUND', { field: 'account' });
|
|
1468
|
-
}
|
|
1469
|
-
const plan = await ctx.store.get('governance_plans', (req as { plan_id?: string }).plan_id ?? '');
|
|
1470
|
-
if (!plan) return adcpError('PLAN_NOT_FOUND', { field: 'plan_id' });
|
|
1471
|
-
|
|
1472
|
-
// Human-review gate — GDPR Art 22 / EU AI Act Annex III.
|
|
1473
|
-
if (plan.human_review_required === true) {
|
|
1474
|
-
const taskId = `task_${randomUUID()}`;
|
|
1475
|
-
await ctx.store.put('pending_reviews', taskId, {
|
|
1476
|
-
plan_id: (req as { plan_id?: string }).plan_id,
|
|
1477
|
-
params: req,
|
|
1478
|
-
enqueued_at: new Date().toISOString(),
|
|
1479
|
-
account_id: ctx.account.id,
|
|
1480
|
-
// Buyer's webhook target for async completion, if they supplied one.
|
|
1481
|
-
webhook_url: (req as { push_notification_config?: { url: string } }).push_notification_config?.url,
|
|
1482
|
-
});
|
|
1483
|
-
// Route this task_id to your human-review queue (Slack approval,
|
|
1484
|
-
// ops ticket, internal UI — whatever your reviewers use).
|
|
1485
|
-
await humanReviewQueue.enqueue(taskId);
|
|
1486
|
-
// Submitted envelope per CreateMediaBuySubmitted. Do NOT return a
|
|
1487
|
-
// populated MediaBuy here — media_buy_id and packages land on the
|
|
1488
|
-
// completion artifact once a human approves. taskToolResponse bypasses
|
|
1489
|
-
// the default mediaBuyResponse wrap, which would stamp revision /
|
|
1490
|
-
// confirmed_at / valid_actions — fields that don't belong on a task
|
|
1491
|
-
// envelope.
|
|
1492
|
-
return taskToolResponse({ status: 'submitted', task_id: taskId });
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
// Non-regulated path — normal governance check, commit synchronously.
|
|
1496
|
-
const govUrl = ctx.account.ctx_metadata?.governanceUrl;
|
|
1497
|
-
if (typeof govUrl === 'string') {
|
|
1498
|
-
const gov = await checkGovernance({
|
|
1499
|
-
agentUrl: govUrl,
|
|
1500
|
-
planId: (req as { plan_id?: string }).plan_id ?? 'default',
|
|
1501
|
-
caller: 'https://my-publisher.com/mcp',
|
|
1502
|
-
tool: 'create_media_buy',
|
|
1503
|
-
payload: req,
|
|
1504
|
-
});
|
|
1505
|
-
if (!gov.approved) return governanceDeniedError(gov);
|
|
1506
|
-
}
|
|
1507
|
-
return executeBuy(req, ctx.store);
|
|
1508
|
-
},
|
|
1509
|
-
|
|
1510
|
-
updateMediaBuy: async (id, patch) => ({ media_buy_id: id, status: 'active' }),
|
|
1511
|
-
getMediaBuys: async () => ({ media_buys: [] }),
|
|
1512
|
-
getMediaBuyDelivery: async () => ({ deliveries: [] }),
|
|
1513
|
-
syncCreatives: async () => [],
|
|
1514
|
-
listCreativeFormats: async () => ({ formats: [] }),
|
|
1515
|
-
};
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
serve(() =>
|
|
1519
|
-
createAdcpServerFromPlatform(new RegulatedPublisher(), {
|
|
1520
|
-
name: 'Regulated Publisher',
|
|
1521
|
-
version: '1.0.0',
|
|
1522
|
-
})
|
|
1523
|
-
);
|
|
1524
|
-
|
|
1525
|
-
// Called by the human-review UI when a reviewer signs off. Lives outside any
|
|
1526
|
-
// request handler, so it takes its own AdcpStateStore — the same instance you
|
|
1527
|
-
// passed to the framework via `stateStore` option. No ctx in scope here.
|
|
1528
|
-
async function onHumanApproval(store: AdcpStateStore, taskId: string, approver: string, reason: string): Promise<void> {
|
|
1529
|
-
const pending = await store.get('pending_reviews', taskId);
|
|
1530
|
-
if (!pending) throw new Error(`No pending review with id ${taskId}`);
|
|
1531
|
-
|
|
1532
|
-
// Validates reason ≥ 20 chars, approver as email, no control chars,
|
|
1533
|
-
// ISO 8601 approved_at.
|
|
1534
|
-
const override = buildHumanOverride({
|
|
1535
|
-
reason,
|
|
1536
|
-
approver,
|
|
1537
|
-
approvedAt: new Date(),
|
|
1538
|
-
});
|
|
1539
|
-
|
|
1540
|
-
const buy = await executeBuy(pending.params, store);
|
|
1541
|
-
await store.put('media_buys', buy.media_buy_id, {
|
|
1542
|
-
...buy,
|
|
1543
|
-
human_override: override,
|
|
1544
|
-
plan_id: pending.plan_id,
|
|
1545
|
-
account_id: pending.account_id,
|
|
1546
|
-
});
|
|
1547
|
-
await store.delete('pending_reviews', taskId);
|
|
1548
|
-
|
|
1549
|
-
// Notify the buyer. Two options, pick based on what your server wires up:
|
|
1550
|
-
// 1. If you configured `webhooks` on the framework server and the buyer sent
|
|
1551
|
-
// push_notification_config.url, POST the completion event from the
|
|
1552
|
-
// emitter built at boot (hoisted outside the framework constructor so it's
|
|
1553
|
-
// reachable here). See § Guaranteed delivery / IO signing for the
|
|
1554
|
-
// emitter construction.
|
|
1555
|
-
// 2. Otherwise the buyer polls — they already have the task_id and will
|
|
1556
|
-
// discover the committed buy via get_media_buys once it lands in
|
|
1557
|
-
// 'media_buys'.
|
|
1558
|
-
}
|
|
1559
|
-
```
|
|
1560
|
-
|
|
1561
|
-
### Decision table
|
|
1562
|
-
|
|
1563
|
-
| Plan shape | `human_review_required` | Who approves | Artifact required |
|
|
1564
|
-
| -------------------------------------------------------------------------- | ----------------------- | -------------------- | ----------------------------------------------- |
|
|
1565
|
-
| General consumer CPG, travel, retail | `false` (or absent) | Automated governance | `governance_context` echoed through lifecycle |
|
|
1566
|
-
| Employment, credit, housing, insurance, education (Annex III high-risk) | `true` (required) | Human reviewer | `human_override` built via `buildHumanOverride` |
|
|
1567
|
-
| Fair-housing, fair-lending, fair-employment, pharmaceutical (US regulated) | `true` (required) | Human reviewer | `human_override` built via `buildHumanOverride` |
|
|
1568
|
-
| Explicit `policy_ids: ['eu_ai_act_annex_iii']` | `true` (required) | Human reviewer | `human_override` built via `buildHumanOverride` |
|
|
1569
|
-
|
|
1570
|
-
`REGULATED_HUMAN_REVIEW_CATEGORIES` (exported from `@adcp/sdk`) is the client-side minimum: `['fair_housing', 'fair_lending', 'fair_employment', 'pharmaceutical_advertising']`. `ANNEX_III_POLICY_IDS` is `['eu_ai_act_annex_iii']`. Governance agents resolve synonyms and per-publisher extensions server-side; these constants exist so pre-submit validation doesn't round-trip. Extend with your own vertical list if needed.
|
|
1571
|
-
|
|
1572
|
-
### Pitfalls
|
|
1573
|
-
|
|
1574
|
-
- Don't silently flip `human_review_required: true → false` on re-sync without `buildHumanOverride`. That's a compliance violation — the whole point of the field is that downgrades require documented human authorization.
|
|
1575
|
-
- `buildHumanOverride` throws if `reason` trims to fewer than 20 characters, if `approver` fails the email regex, if either has control characters, or if `approvedAt` isn't a `Date` / parseable ISO 8601 string. Validate your UI's approval form against the same rules.
|
|
1576
|
-
- Thread `governance_context` through `create_media_buy` → `update_media_buy` → delivery / lifecycle events. Dropping it breaks the audit chain — downstream governance checks need the opaque token to reconcile decisions.
|
|
1577
|
-
|
|
1578
|
-
<a name="idempotency"></a>
|
|
1579
|
-
|
|
1580
|
-
## Idempotency
|
|
1581
|
-
|
|
1582
|
-
AdCP v3 requires an `idempotency_key` on every mutating request. For sellers, that's `create_media_buy`, `update_media_buy`, `sync_creatives`, and any `sync_*` tools you implement. Idempotency is wired in the Implementation example above — this section explains what the framework does for you and the subtleties to know.
|
|
1583
|
-
|
|
1584
|
-
**What the framework handles when you pass `idempotency` to `createAdcpServerFromPlatform`:**
|
|
1585
|
-
|
|
1586
|
-
- Rejects missing or malformed `idempotency_key` with `INVALID_REQUEST`. The spec pattern is `^[A-Za-z0-9_.:-]{16,255}$` — a test key like `"key1"` will be rejected for length, not idempotency logic. **Ordering gotcha**: idempotency runs AFTER `resolveAccount`. If your `resolveAccount` returns null for a valid-shape reference, the buyer gets `ACCOUNT_NOT_FOUND` — NOT the missing-key error they expected — and conformance tests fail with the wrong code. Either handle both AccountReference branches (see Implementation above) or accept dev-mode brand+operator wildcards so compliance graders reach the idempotency layer.
|
|
1587
|
-
- Hashes the request payload with RFC 8785 JCS. The emitted error codes and their semantics are in the table at [§ Composing OAuth, signing, and idempotency](#composing-oauth-signing-and-idempotency).
|
|
1588
|
-
- Injects `replayed: true` on `result.structuredContent.replayed` when returning a cached response; fresh executions omit the field.
|
|
1589
|
-
- Auto-declares `adcp.idempotency.replay_ttl_seconds` on `get_adcp_capabilities`.
|
|
1590
|
-
- Only caches successful responses — errors re-execute on retry so transient failures don't lock into the cache. This applies to every `recovery` class including `terminal`: the AdCP terminal catalog (`ACCOUNT_SUSPENDED`, `BUDGET_EXHAUSTED`, `ACCOUNT_PAYMENT_REQUIRED`, `ACCOUNT_SETUP_REQUIRED`) is mostly state-dependent, and caching would return stale errors after the buyer remediates. Only `UNSUPPORTED_FEATURE` and `ACCOUNT_NOT_FOUND` are truly immutable, and re-executing them is cheap.
|
|
1591
|
-
- Atomic claim on `check()` so concurrent retries with a fresh key don't all race to execute side effects.
|
|
1592
|
-
|
|
1593
|
-
**Handler contract: mutate last.** The framework releases the idempotency claim on ANY error path — `return adcpError(...)`, `throw adcpError(...)` (auto-unwrapped by the dispatcher), and uncaught exceptions all release. A handler that writes state then errors will double-write on retry:
|
|
1594
|
-
|
|
1595
|
-
```typescript
|
|
1596
|
-
// BROKEN: write happens, error releases claim, retry re-writes
|
|
1597
|
-
await db.insert(mediaBuy);
|
|
1598
|
-
if (!budgetApproved) return adcpError('BUDGET_EXHAUSTED', { ... }); // claim released, insert already persisted
|
|
1599
|
-
|
|
1600
|
-
// CORRECT: validate first, write last
|
|
1601
|
-
if (!budgetApproved) return adcpError('BUDGET_EXHAUSTED', { ... }); // no write yet, safe to release
|
|
1602
|
-
await db.insert(mediaBuy);
|
|
1603
|
-
return mediaBuyResponse({ ... });
|
|
1604
|
-
```
|
|
1605
|
-
|
|
1606
|
-
If the validation can only run after a partial write (rare), make the write itself idempotent — natural-key upsert or the `ctx.store.get` → merge pattern — so re-execution converges on the same state.
|
|
1607
|
-
|
|
1608
|
-
**Scoping**: the principal comes from `resolveSessionKey` (or override with `resolveIdempotencyPrincipal(ctx, params, toolName)` for per-tool custom scopes). Two callers with the same principal share a cache namespace; different principals are isolated.
|
|
1609
|
-
|
|
1610
|
-
**Two things to know**:
|
|
1611
|
-
|
|
1612
|
-
1. `ttlSeconds` must be `3600` (1h) to `604800` (7d) — out of range throws at `createIdempotencyStore` construction. Don't pass minutes thinking they're seconds.
|
|
1613
|
-
2. If you register mutating handlers without passing `idempotency`, the framework logs an error at server-creation time (v3 non-compliance). Silence it by either wiring idempotency or setting `capabilities.idempotency.replay_ttl_seconds` in your config (declares non-compliance to buyers).
|
|
1614
|
-
|
|
1615
|
-
**Buyer-side crash recovery.** When your buyers' processes die mid-retry they need to know whether to re-send. Point them at [`docs/guides/idempotency-crash-recovery.md`](../../docs/guides/idempotency-crash-recovery.md) — worked recipe for natural-key lookup, `IdempotencyConflictError` / `IdempotencyExpiredError`, and `metadata.replayed` as the side-effect gate.
|
|
1616
|
-
|
|
1617
|
-
**Known grader limitation (tracked upstream as [#678](https://github.com/adcontextprotocol/adcp-client/issues/678)).** The `idempotency` storyboard's missing-key step probes your agent with a raw HTTP POST (bypassing the SDK's `idempotency_key` auto-injection) but may not negotiate the MCP Streamable HTTP `Accept` header correctly, returning `Not Acceptable: Client must accept both application/json and text/event-stream` instead of the expected `INVALID_REQUEST`. This is a grader-side issue — your framework wiring is still correct, it'll pass once #678 lands. Ignore this specific sub-step failure in the interim.
|
|
1618
|
-
|
|
1619
|
-
## Going to Production
|
|
1620
|
-
|
|
1621
|
-
The quick-start uses `memoryBackend()` + `InMemoryStateStore` — both reset on process restart and don't scale across replicas. Production swaps three pieces: `createIdempotencyStore({ backend: pgBackend(pool) })`, `PostgresStateStore(pool)`, `PostgresTaskStore(pool)`. Run the three migrations at boot (`getIdempotencyMigration()`, `getAdcpStateMigration()`, `MCP_TASKS_MIGRATION`), wire `cleanupExpiredIdempotency(pool)` on an hourly cron, and set `resolveAccount` to hit your real DB instead of `InMemoryStateStore`. Full worked example with Pool sizing and multi-tenant principal resolution lives in [`docs/guides/BUILD-AN-AGENT.md`](../../docs/guides/BUILD-AN-AGENT.md) § Going to Production.
|
|
1622
|
-
|
|
1623
|
-
**Critical: probe the pool at boot.** `pg.Pool` is lazy — `new Pool({ connectionString })` does not validate the URL. A bad `DATABASE_URL` lets the server start, advertise `IdempotencySupported`, and then silently fail every mutating call. Wire `readinessCheck` on `serve()` so the server never accepts traffic with a broken pool:
|
|
1624
|
-
|
|
1625
|
-
```ts
|
|
1626
|
-
const store = createIdempotencyStore({ backend: pgBackend(pool), ttlSeconds: 86400 });
|
|
1627
|
-
pool.on('error', err => console.error('pg pool error', err)); // prevent crash on idle-client errors
|
|
1628
|
-
serve(createAgent, {
|
|
1629
|
-
readinessCheck: () => store.probe(), // throws with a descriptive error if pool/table is broken
|
|
1630
|
-
});
|
|
1631
|
-
```
|
|
1632
|
-
|
|
1633
|
-
Auth is not wired in the example — see [§ Protecting your agent](#protecting-your-agent) below.
|
|
1634
|
-
|
|
1635
|
-
## Deployment beyond single-host HTTP
|
|
1636
|
-
|
|
1637
|
-
`serve(createAgent, { port, authenticate })` is sufficient for single-host HTTP — including every compliance storyboard. For other deployment shapes (multi-host, Express composition with OAuth Authorization Server routes, stdio, per-host OAuth), see [`deployment.md`](./deployment.md) — covers `createExpressAdapter`, multi-host dispatch, `resolveHost`, per-host OAuth providers, and the stdio transport.
|
|
1638
|
-
|
|
1639
|
-
<a name="protecting-your-agent"></a>
|
|
1640
|
-
|
|
1641
|
-
## Protecting your agent
|
|
1642
|
-
|
|
1643
|
-
**An AdCP agent that accepts unauthenticated requests is non-compliant.** The compliance runner enforces this via the `security_baseline` storyboard (every agent regardless of specialism). You MUST pick at least one of:
|
|
1644
|
-
|
|
1645
|
-
- **API key** — static bearer tokens looked up in your database or a constant map. Best for B2B integrations with a known counterparty.
|
|
1646
|
-
- **OAuth 2.0** — JWTs signed by an IdP (WorkOS, Auth0, Clerk, Okta, a self-hosted authorization server). Best when buyers authenticate as themselves.
|
|
1647
|
-
- **Both** — accept either at runtime via `anyOf(verifyApiKey(...), verifyBearer(...))`.
|
|
1648
|
-
|
|
1649
|
-
Ask the operator which mechanism they want before generating code. "API key, OAuth, or both?" is the first question.
|
|
1650
|
-
|
|
1651
|
-
### API key
|
|
1652
|
-
|
|
1653
|
-
```typescript
|
|
1654
|
-
import { serve } from '@adcp/sdk';
|
|
1655
|
-
import { verifyApiKey } from '@adcp/sdk/server';
|
|
1656
|
-
|
|
1657
|
-
serve(createAgent, {
|
|
1658
|
-
authenticate: verifyApiKey({
|
|
1659
|
-
verify: async token => {
|
|
1660
|
-
const row = await db.api_keys.findUnique({ where: { token } });
|
|
1661
|
-
if (!row) return null; // framework replies 401 with WWW-Authenticate
|
|
1662
|
-
return { principal: row.account_id };
|
|
1663
|
-
},
|
|
1664
|
-
}),
|
|
1665
|
-
});
|
|
1666
|
-
```
|
|
1667
|
-
|
|
1668
|
-
For local development use the static `keys` map: `verifyApiKey({ keys: { sk_test: { principal: 'dev' } } })`.
|
|
1669
|
-
|
|
1670
|
-
### OAuth
|
|
1671
|
-
|
|
1672
|
-
```typescript
|
|
1673
|
-
import { serve } from '@adcp/sdk';
|
|
1674
|
-
import { verifyBearer } from '@adcp/sdk/server';
|
|
1675
|
-
|
|
1676
|
-
const AGENT_URL = 'https://my-agent.example.com/mcp';
|
|
1677
|
-
|
|
1678
|
-
serve(createAgent, {
|
|
1679
|
-
publicUrl: AGENT_URL, // canonical RFC 8707 audience
|
|
1680
|
-
authenticate: verifyBearer({
|
|
1681
|
-
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
|
|
1682
|
-
issuer: 'https://auth.example.com',
|
|
1683
|
-
audience: AGENT_URL, // MUST equal publicUrl
|
|
1684
|
-
}),
|
|
1685
|
-
protectedResource: {
|
|
1686
|
-
authorization_servers: ['https://auth.example.com'],
|
|
1687
|
-
scopes_supported: ['read', 'write'],
|
|
1688
|
-
},
|
|
1689
|
-
});
|
|
1690
|
-
```
|
|
1691
|
-
|
|
1692
|
-
Set `publicUrl` to the canonical https:// URL clients use — the framework serves `/.well-known/oauth-protected-resource/mcp` with that exact `resource` value, and the JWT `audience` check rejects tokens minted for any other URL. Deriving the resource URL from `publicUrl` (not the incoming `Host` header) is what stops a phishing attacker from making your server advertise `https://evil.example/mcp` as the audience.
|
|
1693
|
-
|
|
1694
|
-
### Both
|
|
30
|
+
**Not this skill:**
|
|
1695
31
|
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
32
|
+
- Building an agency / holdco hub hosting multiple specialisms → `skills/build-holdco-agent/`
|
|
33
|
+
- Catalog-driven inventory (retail media, restaurants, travel) → `skills/build-retail-media-agent/`
|
|
34
|
+
- AI ad network coupling generation with sales → `skills/build-generative-seller-agent/`
|
|
35
|
+
- Buying inventory → `docs/getting-started.md` covers the buyer side
|
|
1699
36
|
|
|
1700
|
-
|
|
1701
|
-
publicUrl: AGENT_URL,
|
|
1702
|
-
authenticate: anyOf(verifyApiKey({ verify: lookupApiKey }), verifyBearer({ jwksUri, issuer, audience: AGENT_URL })),
|
|
1703
|
-
protectedResource: { authorization_servers: [issuer] },
|
|
1704
|
-
});
|
|
1705
|
-
```
|
|
37
|
+
**Often claimed alongside:** [`audience-sync`](specialisms/audience-sync.md) (walled-garden social + identity provider patterns), `signal-marketplace` (DSP-side data surface), `sales-catalog-driven` (retail-media catalog + dynamic-creative). See [Common multi-specialism bundles](../../examples/README.md#common-multi-specialism-bundles).
|
|
1706
38
|
|
|
1707
|
-
|
|
39
|
+
## Cross-cutting rules
|
|
1708
40
|
|
|
1709
|
-
|
|
41
|
+
Every sales-* seller hits the cross-cutting rules in [`../cross-cutting.md`](../cross-cutting.md). The high-traffic ones for sellers (deep-linked to the rule):
|
|
1710
42
|
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
43
|
+
- [`idempotency_key`](../cross-cutting.md#idempotency_key-is-required-on-every-mutating-call) on every mutating request — `create_media_buy`, `update_media_buy`, `sync_creatives`, `sync_audiences`, etc.
|
|
44
|
+
- [Resolve-then-authorize](../cross-cutting.md#resolve-then-authorize--uniform-errors-for-not-found--not-yours) — byte-equivalent errors for `media_buy_id` cross-tenant lookups
|
|
45
|
+
- [Authentication](../cross-cutting.md#authentication-is-mandatory) — `serve({ authenticate })` baseline
|
|
46
|
+
- [Account resolution](../cross-cutting.md#account-resolution-pick-a-security-preset) — `createTenantStore` for multi-advertiser sellers
|
|
47
|
+
- [Webhooks](../cross-cutting.md#webhooks-stable-operation_id-across-retries) — stable `operation_id` for IO-approval / delivery-completion notifications
|
|
1714
48
|
|
|
1715
|
-
##
|
|
49
|
+
## Specialism deltas
|
|
1716
50
|
|
|
1717
|
-
|
|
51
|
+
The fork target covers the baseline. Specialism subpages cover the deltas — what to add when you claim that specialism on top of the baseline. Each entry below is **specialism → when to read it → what's in it**.
|
|
1718
52
|
|
|
1719
|
-
**
|
|
53
|
+
- [`specialisms/sales-guaranteed.md`](specialisms/sales-guaranteed.md) — **read before coding if you claim `sales-guaranteed`**: IO-task envelope, three `create_media_buy` return shapes (task envelope / `pending_creatives` / `active`), `TERMS_REJECTED` on aggressive `measurement_terms`. Applying only the task-envelope path fails 5 storyboard `create_media_buy` steps.
|
|
54
|
+
- [`specialisms/sales-non-guaranteed.md`](specialisms/sales-non-guaranteed.md) — **read if your inventory clears at request time** (programmatic auction, no HITL approval): sync confirmation, `bid_price`, `update_media_buy` for in-flight changes.
|
|
55
|
+
- [`specialisms/sales-broadcast-tv.md`](specialisms/sales-broadcast-tv.md) — **read if you sell linear / broadcast TV**: `agency_estimate_number`, Ad-ID `industry_identifiers`, `measurement_windows` (Live/C3/C7).
|
|
56
|
+
- [`specialisms/sales-social.md`](specialisms/sales-social.md) — **read if you're a social platform** (Snap, Meta, TikTok, etc.): additive — claim alongside `sales-non-guaranteed`. Adds `sync_audiences`, `sync_catalogs` (DPA), `log_event` (conversions), `get_account_financials`.
|
|
57
|
+
- [`specialisms/sales-proposal-mode.md`](specialisms/sales-proposal-mode.md) — **read if you negotiate via proposals before line-itemizing**: `proposals[]` with `budget_allocations`, `buying_mode: 'refine'`.
|
|
58
|
+
- [`specialisms/audience-sync.md`](specialisms/audience-sync.md) — **read if buyers push audiences to you** (walled-garden + identity-provider patterns): `sync_audiences` track. Hashed identifiers, match-rate telemetry.
|
|
59
|
+
- [`specialisms/signed-requests.md`](specialisms/signed-requests.md) — **read if you accept buyer-signed requests on mutating tools**: RFC 9421 verification, `WWW-Authenticate: Signature error="<code>"` on rejection.
|
|
1720
60
|
|
|
1721
|
-
|
|
1722
|
-
npx tsx agent.ts &
|
|
1723
|
-
```
|
|
61
|
+
`sales-catalog-driven` and `sales-retail-media` live in `skills/build-retail-media-agent/` because catalog-driven applies beyond retail (restaurants, travel, local commerce).
|
|
1724
62
|
|
|
1725
|
-
|
|
63
|
+
## Validate locally
|
|
1726
64
|
|
|
1727
65
|
```bash
|
|
1728
|
-
#
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
# Your specialism bundle (one of: sales_guaranteed, sales_non_guaranteed,
|
|
1732
|
-
# sales_broadcast_tv, sales_streaming_tv, sales_social, sales_proposal_mode)
|
|
1733
|
-
npx @adcp/sdk@latest storyboard run http://localhost:3001/mcp sales_guaranteed --auth $TOKEN
|
|
66
|
+
# Run the fork-matrix gate for your adapter (~9s, deterministic)
|
|
67
|
+
npm run compliance:fork-matrix -- --test-name-pattern="hello-seller-adapter-guaranteed"
|
|
1734
68
|
|
|
1735
|
-
#
|
|
1736
|
-
|
|
1737
|
-
--
|
|
1738
|
-
|
|
1739
|
-
# Webhook conformance (if you claim async task lifecycles)
|
|
1740
|
-
npx @adcp/sdk@latest storyboard run http://localhost:3001/mcp webhook_emission \
|
|
1741
|
-
--webhook-receiver --auth $TOKEN
|
|
69
|
+
# Or validate your forked agent directly against its storyboard
|
|
70
|
+
adcp storyboard run http://127.0.0.1:3004/mcp sales_guaranteed \
|
|
71
|
+
--bearer "$ADCP_AUTH_TOKEN" --include-bundles --json
|
|
1742
72
|
```
|
|
1743
73
|
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
```bash
|
|
1747
|
-
npx @adcp/sdk@latest fuzz http://localhost:3001/mcp \
|
|
1748
|
-
--tools get_products,get_media_buys,list_creative_formats \
|
|
1749
|
-
--auth-token $TOKEN
|
|
1750
|
-
```
|
|
1751
|
-
|
|
1752
|
-
**Request signing (if you claim `signed-requests`):** point `adcp grade request-signing` at your sandbox — see [VALIDATE-YOUR-AGENT.md § Request signing](../../docs/guides/VALIDATE-YOUR-AGENT.md#request-signing--adcp-grade-request-signing).
|
|
1753
|
-
|
|
1754
|
-
**Multi-instance (before production):** run with two `--url` flags to catch `(brand, account)`-scoped state that lives per-process. See [VALIDATE-YOUR-AGENT.md § Multi-instance](../../docs/guides/VALIDATE-YOUR-AGENT.md#multi-instance-testing).
|
|
1755
|
-
|
|
1756
|
-
Common failure decoder:
|
|
1757
|
-
|
|
1758
|
-
- `response_schema` → response doesn't match Zod schema
|
|
1759
|
-
- `field_present` → required field missing
|
|
1760
|
-
- `mcp_error` → check tool registration (schema, name)
|
|
1761
|
-
- `authority_level` / `human_review_required` mismatch → check governance plan shape — schema moved in AdCP 3.0 GA
|
|
1762
|
-
|
|
1763
|
-
**Keep iterating until all steps pass.** If you can't bind ports locally, skip `tsx agent.ts` and run `npm run compliance:skill-matrix -- --filter seller` — it builds an isolated sandbox and grades end-to-end.
|
|
1764
|
-
|
|
1765
|
-
## Storyboards
|
|
1766
|
-
|
|
1767
|
-
| Storyboard | Use case |
|
|
1768
|
-
| ------------------------------- | ------------------------------------------------------ |
|
|
1769
|
-
| `media_buy_seller` | Full lifecycle — every seller should pass this |
|
|
1770
|
-
| `media_buy_non_guaranteed` | Auction flow with bid adjustment |
|
|
1771
|
-
| `media_buy_guaranteed_approval` | IO approval workflow |
|
|
1772
|
-
| `media_buy_proposal_mode` | AI-generated proposals |
|
|
1773
|
-
| `media_buy_catalog_creative` | Catalog sync + conversions |
|
|
1774
|
-
| `schema_validation` | Schema compliance + date validation errors |
|
|
1775
|
-
| `deterministic_testing` | State machine correctness via `comply_test_controller` |
|
|
1776
|
-
|
|
1777
|
-
## Common Mistakes
|
|
1778
|
-
|
|
1779
|
-
| Mistake | Fix |
|
|
1780
|
-
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1781
|
-
| Using `createTaskCapableServer` + `server.tool()` | Use `createAdcpServerFromPlatform(platform, opts)` — handles schemas, response builders, capabilities, ctx_metadata round-trip, idempotency-principal synthesis |
|
|
1782
|
-
| Calling `createAdcpServer` directly in new code | Reach for `createAdcpServerFromPlatform` first; `createAdcpServer` lives at `@adcp/sdk/server/legacy/v5` for mid-migration / escape-hatch use only |
|
|
1783
|
-
| Using module-level Maps for state | Use `ctx.store` — persists across HTTP requests, swappable for postgres |
|
|
1784
|
-
| Return raw JSON without response builders | The framework auto-applies response builders — just return the data |
|
|
1785
|
-
| Missing `brand`/`operator` in sync_accounts response | Echo them back from the request — they're required |
|
|
1786
|
-
| sync_governance returns wrong shape | Must include `status: 'synced'` and `governance_agents` array |
|
|
1787
|
-
| `sandbox: false` on mock data | Buyers may treat mock data as real |
|
|
1788
|
-
| Returns raw JSON for validation failures | Use `adcpError('INVALID_REQUEST', { message })` — storyboards validate the `adcp_error` structure |
|
|
1789
|
-
| IO-signing setup URL at top level of media buy response | Nest it in `account.setup`: `{ account: { setup: { url, message } } }`. Response builders reject a top-level `setup` at runtime. |
|
|
1790
|
-
| Bypassing response builders and forgetting `valid_actions` | `mediaBuyResponse` and `updateMediaBuyResponse` auto-populate `valid_actions` from `status` — use them. For `get_media_buys`, populate each buy with `validActionsForStatus(status)`. |
|
|
1791
|
-
| Missing `publisher_properties` or `format_ids` on Product | Both are required — see product example in `get_products` section |
|
|
1792
|
-
| format_ids in products don't match list_creative_formats | Buyers echo format_ids from products into sync_creatives — if your validation rejects your own format_ids, the buyer can't fulfill creative requirements |
|
|
1793
|
-
| Missing `@types/node` in devDependencies | `process.env` doesn't resolve without it — see Setup section |
|
|
1794
|
-
| Dropping `context` from responses | Echo `args.context` back unchanged in every response — buyers use it for correlation |
|
|
1795
|
-
| `channels` typed as `string[]` instead of `MediaChannel[]` | Use `as const` on channel arrays: `channels: ['display', 'olv'] as const`. TypeScript infers `string[]` from array literals, but the SDK requires the `MediaChannel` union type. |
|
|
1796
|
-
|
|
1797
|
-
### Translating storyboard runner output
|
|
74
|
+
The fork-matrix gate is the three-gate contract from [`docs/guides/EXAMPLE-TEST-CONTRACT.md`](../../docs/guides/EXAMPLE-TEST-CONTRACT.md): tsc strict / storyboard zero-failures / upstream façade. Adopters who fork a hello adapter inherit the gate by extending the test file with their own adapter path and `expectedRoutes`.
|
|
1798
75
|
|
|
1799
|
-
|
|
76
|
+
If the gate fails on a storyboard step (not on tsc), re-run the `adcp storyboard run ... --json` command above for the human-readable `💡 Hint:` lines — node:test's assertion formatting compresses them.
|
|
1800
77
|
|
|
1801
|
-
|
|
1802
|
-
| ------------------------------------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
1803
|
-
| `✗ Response matches <tool>-response.json schema` | Your return shape doesn't match the spec response schema | Return fields the schema requires; don't add top-level fields the schema rejects |
|
|
1804
|
-
| `✗ field_present` (path: …) | Required field missing or at the wrong path | Check the spec's `*-response.json` for the field; common miss: `context.correlation_id` not echoed back |
|
|
1805
|
-
| `✗ field_value` expected X got Y | Value mismatch on a specific path | Most often `context.correlation_id` drift or a status enum value that's stale |
|
|
1806
|
-
| `mcp_error -32602: Input validation error` | SDK Zod schema rejected the **incoming** request — your handler never ran | Drift between the SDK schema and the storyboard yaml. File upstream if the storyboard is authoritative |
|
|
1807
|
-
| `Agent did not advertise tool "X"` (as a warning) | Storyboard expects a tool you haven't registered | Register the tool; if it lives in another agent (e.g., governance tools from a seller), ignore the warning |
|
|
1808
|
-
| Missing `idempotency_key` → handler never runs | Mutating request without an idempotency key | SDK rejects at the idempotency layer. File runner bug if the storyboard yaml's `sample_request` omits it |
|
|
78
|
+
For deeper validation (fuzz, request-signing grading, multi-instance, custom invariants): [`docs/guides/VALIDATE-YOUR-AGENT.md`](../../docs/guides/VALIDATE-YOUR-AGENT.md).
|
|
1809
79
|
|
|
1810
|
-
##
|
|
80
|
+
## Deployment
|
|
1811
81
|
|
|
1812
|
-
|
|
82
|
+
Single-host HTTP from `serve(...)` is the default. Multi-host, Express, or stdio transports: [`deployment.md`](deployment.md).
|
|
1813
83
|
|
|
1814
|
-
|
|
1815
|
-
| ---------------------- | ------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
1816
|
-
| `sales-guaranteed` | stable | [`specialisms/sales-guaranteed.md`](./specialisms/sales-guaranteed.md) |
|
|
1817
|
-
| `sales-non-guaranteed` | stable | [`specialisms/sales-non-guaranteed.md`](./specialisms/sales-non-guaranteed.md) |
|
|
1818
|
-
| `sales-broadcast-tv` | stable | [`specialisms/sales-broadcast-tv.md`](./specialisms/sales-broadcast-tv.md) |
|
|
1819
|
-
| `sales-streaming-tv` | preview | baseline only |
|
|
1820
|
-
| `sales-social` | stable | [`specialisms/sales-social.md`](./specialisms/sales-social.md) |
|
|
1821
|
-
| `sales-exchange` | preview | baseline only |
|
|
1822
|
-
| `sales-proposal-mode` | stable | [`specialisms/sales-proposal-mode.md`](./specialisms/sales-proposal-mode.md) |
|
|
1823
|
-
| `audience-sync` | stable | [`specialisms/audience-sync.md`](./specialisms/audience-sync.md) |
|
|
1824
|
-
| `signed-requests` | preview | [`specialisms/signed-requests.md`](./specialisms/signed-requests.md) — cross-cutting; applies to every mutating agent |
|
|
84
|
+
## Common shape gotchas
|
|
1825
85
|
|
|
1826
|
-
|
|
86
|
+
`BuildCreativeReturn` has 4 valid shapes (framework auto-wraps the bare manifest). `VASTAsset` requires an embedded `delivery_type` discriminator. Targeting overlay echo on `get_media_buys` requires `createMediaBuyStore`. See [`SHAPE-GOTCHAS.md`](../SHAPE-GOTCHAS.md) — schema validators catch these at runtime; type checkers don't.
|
|
1827
87
|
|
|
1828
|
-
##
|
|
88
|
+
## Migration notes
|
|
1829
89
|
|
|
1830
|
-
- `docs/
|
|
1831
|
-
- `docs/
|
|
1832
|
-
- `docs/TYPE-SUMMARY.md` — curated type signatures
|
|
1833
|
-
- `storyboards/media_buy_seller.yaml` — full buyer interaction sequence
|
|
1834
|
-
- `examples/error-compliant-server.ts` — seller with error handling
|
|
1835
|
-
- `src/lib/server/create-adcp-server.ts` — framework source (for TypeScript autocomplete exploration)
|
|
90
|
+
- 6.6 → 6.7: **Two seller-affecting breaking changes — audit before bumping**: `accounts.resolution: 'implicit'` now refuses inline `{account_id}` references (#10), and `SalesPlatform` split into `SalesCorePlatform & SalesIngestionPlatform` (#11) — all methods individually optional, self-announcing under `tsc --noEmit`. See [`docs/migration-6.6-to-6.7.md`](../../docs/migration-6.6-to-6.7.md) for the worked diff plus 15 additive recipes around `definePlatform`, `composeMethod`, typed errors, `BuyerAgentRegistry`.
|
|
91
|
+
- 4.x → 5.x: [`docs/migration-4.x-to-5.x.md`](../../docs/migration-4.x-to-5.x.md). Full v5 path including `createAdcpServer`, `serve({ authenticate })`, and the 5.13 pin to AdCP 3.0.0 GA.
|