One of three companion v3.1 proposals for catalog sync between AdCP agents and consumers. Companion to #4761 (catalog_version conditional fetch) and a forthcoming per-agent inventory change-feed proposal.
Problem
get_signals today requires either signal_spec (natural-language description) or signal_ids (specific IDs). There is no protocol-conformant way to ask a signals agent "what's in your catalog?" — i.e., to enumerate the agent's full priced signal inventory without first knowing what to look for.
This creates an asymmetry with get_products, which gained buying_mode: \"wholesale\" in v3 specifically for catalog enumeration. The result: storefronts, federated marketplaces, and any consumer that wants to maintain a synchronized local view of a signals agent's catalog must either (a) issue broad probe queries with signal_spec: \"all available segments\" (hacky, seller-dependent), or (b) require out-of-band catalog exchange. Neither composes with the rest of the protocol.
The gap also blocks symmetric multi-agent use cases. Embedded signals agents (operated by a storefront) and 3P signals agents (e.g., data marketplaces) should expose the same enumerate-and-price surface, so consumers can build composition layers (signals × targeting × pricing) uniformly across both.
Goals
- A signals agent can return its complete priced catalog in response to a single (paginated) call.
- The contract is symmetric with
get_products buying_mode: \"wholesale\" — synchronous, paginated, firm-priced, with partial-completion declared via incomplete[].
- Marketplace signals remain attributable to their upstream data provider via existing
adagents.json machinery — wholesale enumeration does not collapse provenance.
- Backward-compatible: pre-v3.1 agents that don't honor wholesale mode continue to work via the existing
signal_spec/signal_ids paths.
Design Principles
- Symmetry with
get_products. Same mode-discriminator pattern, same timing rules, same pagination/incomplete shape.
- Catalog enumeration is a read, not a search. Wholesale callers don't get inference or RAG — they get the agent's declared catalog. Response time SHOULD be sub-second per page.
- Provenance preserved. Each returned signal carries
signal_type and data_provider exactly as in signal_spec mode. signal_type: \"marketplace\" signals SHOULD be verifiable against the data provider's adagents.json.
- Pricing-first. Wholesale enumeration without prices is not useful for composition.
pricing_options[] MUST be populated for authenticated callers entitled to see pricing.
Request Shape
Add discovery_mode to the get_signals request:
| Parameter |
Type |
Required |
Description |
discovery_mode |
string |
No (v3.1+ SHOULD include) |
\"brief\" (default) or \"wholesale\". \"brief\": existing behavior — signal_spec or signal_ids required, agent performs inference/RAG. \"wholesale\": raw catalog enumeration — signal_spec and signal_ids MUST NOT be provided; agent returns its full priced catalog, paginated. Timing: \"wholesale\" is a catalog read — agents SHOULD respond synchronously and MUST NOT route through the async/Submitted arm. Partial completion uses incomplete[]. Agents receiving requests from pre-v3.1 clients without discovery_mode MUST default to \"brief\". |
signal_spec |
string |
Conditional |
Required when discovery_mode is \"brief\" (and signal_ids is absent). MUST NOT be provided when discovery_mode is \"wholesale\". |
signal_ids |
SignalID[] |
Conditional |
Required when discovery_mode is \"brief\" (and signal_spec is absent). MUST NOT be provided when discovery_mode is \"wholesale\". |
All other request parameters (account, destinations, countries, filters, pagination) work identically in both modes. In wholesale mode they constrain the catalog returned — e.g., filters.data_providers: [\"acme-data\"] returns only that provider's signals from the agent's catalog.
Capability declaration
Signals agents that support wholesale mode declare it in get_adcp_capabilities:
{
\"signals\": {
\"discovery_modes\": [\"brief\", \"wholesale\"]
}
}
Agents not declaring \"wholesale\" MAY return INVALID_REQUEST for wholesale calls. The capability declaration is the canonical signal.
Response Shape
Identical to brief-mode get_signals response — signals[], each with signal_agent_segment_id, name, description, signal_type, data_provider, deployments[], pricing_options[] (existing spec).
Two additions:
| Response Field |
Type |
Description |
pagination |
object |
has_more, cursor, optional total_count — same shape as get_products. Required for wholesale (catalogs may be large). |
incomplete |
IncompleteEntry[] |
Same shape as get_products. Use scope: \"pricing\" when a signal was returned without pricing because the agent could not confirm prices in time_budget. Use scope: \"catalog\" when whole-catalog enumeration could not complete. |
Pricing in wholesale responses
pricing_options[] MUST be populated for authenticated callers when the agent has firm prices to declare. Pricing models are the existing set: cpm, percent_of_media, flat_fee, per_unit, custom.
When pricing varies by buyer account (rate cards), the caller passes account and the agent returns account-scoped prices. When account is omitted, the agent returns its default rate-card pricing or omits pricing_options[] (in which case the caller MUST re-query with account before composing).
Unauthenticated callers MAY receive catalog metadata without pricing — same posture as get_products.
Authorization & Provenance
Marketplace signals (signal_type: \"marketplace\") must remain attributable to their upstream data provider. Wholesale enumeration does not collapse this:
- Each marketplace signal carries
data_provider (string identifier).
- Consumers SHOULD verify provider authorization via the provider's
adagents.json — same mechanism as today. The signals agent's URL must appear in the provider's adagents.json authorization list for that signal class.
- Storefronts and registries (e.g., the property registry catalog) MAY use wholesale enumeration plus
adagents.json cross-reference to materialize a "data publisher catalog" view: for each known data provider, the set of signals available via authorized signals agents, with prices.
This composes cleanly with the existing agent.profile_updated and publisher.adagents_changed events from specs/registry-change-feed.md: when a data provider authorizes a new signals agent (or revokes one), the registry's existing event stream surfaces the change without any new event types.
Worked Example
Request:
{
\"tool\": \"get_signals\",
\"arguments\": {
\"discovery_mode\": \"wholesale\",
\"account\": { \"buyer_agent_url\": \"https://buyer.example.com\", \"account_id\": \"acct_123\" },
\"filters\": {
\"catalog_types\": [\"marketplace\", \"owned\"],
\"data_providers\": [\"acme-data\", \"nova-insights\"]
},
\"pagination\": { \"max_results\": 50 }
}
}
Response:
{
\"message\": \"Returning 50 of 312 signals in wholesale mode.\",
\"context_id\": \"ctx-wholesale-001\",
\"signals\": [
{
\"signal_agent_segment_id\": \"sigagent_seg_4421\",
\"name\": \"Luxury Auto Intenders\",
\"description\": \"Households researching premium vehicles in the last 30 days.\",
\"signal_type\": \"marketplace\",
\"data_provider\": \"acme-data\",
\"coverage_percentage\": 18.4,
\"deployments\": [
{ \"type\": \"platform\", \"platform\": \"the-trade-desk\", \"is_live\": true,
\"activation_key\": { \"type\": \"segment_id\", \"segment_id\": \"ttd_seg_99821\" } }
],
\"pricing_options\": [
{ \"pricing_option_id\": \"po_cpm_1\", \"model\": \"cpm\", \"cpm\": 2.50, \"currency\": \"USD\" }
]
}
],
\"pagination\": { \"has_more\": true, \"cursor\": \"eyJvIjo1MH0=\", \"total_count\": 312 }
}
Backward Compatibility
- v3.1 schema addition:
discovery_mode enum field on get_signals request. Default \"brief\".
- Pre-v3.1 clients omitting
discovery_mode behave exactly as today (brief mode).
- Pre-v3.1 agents that don't recognize
discovery_mode: \"wholesale\" MAY return INVALID_REQUEST. Clients SHOULD probe via get_adcp_capabilities first.
Implementation Phases
- Schema + spec. Add
discovery_mode to v3.1 request schema. Update get_signals.mdx task reference. Add discovery_modes to the signals capability declaration in get_adcp_capabilities.
- Reference implementation. Implement in the AdCP reference signals agent (and prebid salesagent's signals path). Conformance test vector: "wholesale catalog enumeration returns N signals, each priced, with stable cursors."
- Cross-reference with registry. Document materialized data-publisher catalog views built from wholesale enumeration +
adagents.json cross-reference. No new endpoints — purely a consumer pattern.
Open Questions
- Should
discovery_mode use buying_mode instead? Signals aren't "bought" in the same sense as inventory. discovery_mode is the more honest verb (signals are discovered then activated). But symmetry argues for buying_mode. Recommendation: discovery_mode. Happy to align with buying_mode if reviewers prefer symmetry over verb-accuracy.
- Default pagination size.
get_products is 50, get_signals brief mode is 50. Keep 50. Cap at 100.
- Empty catalog semantics. Agent has no signals matching filters: return
signals: [], pagination.has_more: false, no error. Same as get_products.
Happy to follow up with a PR adding this to specs/ and updating get_signals.mdx if maintainers are aligned on the direction. Reference implementation will land in the prebid salesagent as part of our v3.1 conformance prep.
Problem
get_signalstoday requires eithersignal_spec(natural-language description) orsignal_ids(specific IDs). There is no protocol-conformant way to ask a signals agent "what's in your catalog?" — i.e., to enumerate the agent's full priced signal inventory without first knowing what to look for.This creates an asymmetry with
get_products, which gainedbuying_mode: \"wholesale\"in v3 specifically for catalog enumeration. The result: storefronts, federated marketplaces, and any consumer that wants to maintain a synchronized local view of a signals agent's catalog must either (a) issue broad probe queries withsignal_spec: \"all available segments\"(hacky, seller-dependent), or (b) require out-of-band catalog exchange. Neither composes with the rest of the protocol.The gap also blocks symmetric multi-agent use cases. Embedded signals agents (operated by a storefront) and 3P signals agents (e.g., data marketplaces) should expose the same enumerate-and-price surface, so consumers can build composition layers (signals × targeting × pricing) uniformly across both.
Goals
get_products buying_mode: \"wholesale\"— synchronous, paginated, firm-priced, with partial-completion declared viaincomplete[].adagents.jsonmachinery — wholesale enumeration does not collapse provenance.signal_spec/signal_idspaths.Design Principles
get_products. Same mode-discriminator pattern, same timing rules, same pagination/incomplete shape.signal_typeanddata_providerexactly as insignal_specmode.signal_type: \"marketplace\"signals SHOULD be verifiable against the data provider'sadagents.json.pricing_options[]MUST be populated for authenticated callers entitled to see pricing.Request Shape
Add
discovery_modeto theget_signalsrequest:discovery_mode\"brief\"(default) or\"wholesale\".\"brief\": existing behavior —signal_specorsignal_idsrequired, agent performs inference/RAG.\"wholesale\": raw catalog enumeration —signal_specandsignal_idsMUST NOT be provided; agent returns its full priced catalog, paginated. Timing:\"wholesale\"is a catalog read — agents SHOULD respond synchronously and MUST NOT route through the async/Submitted arm. Partial completion usesincomplete[]. Agents receiving requests from pre-v3.1 clients withoutdiscovery_modeMUST default to\"brief\".signal_specdiscovery_modeis\"brief\"(andsignal_idsis absent). MUST NOT be provided whendiscovery_modeis\"wholesale\".signal_idsdiscovery_modeis\"brief\"(andsignal_specis absent). MUST NOT be provided whendiscovery_modeis\"wholesale\".All other request parameters (
account,destinations,countries,filters,pagination) work identically in both modes. In wholesale mode they constrain the catalog returned — e.g.,filters.data_providers: [\"acme-data\"]returns only that provider's signals from the agent's catalog.Capability declaration
Signals agents that support wholesale mode declare it in
get_adcp_capabilities:{ \"signals\": { \"discovery_modes\": [\"brief\", \"wholesale\"] } }Agents not declaring
\"wholesale\"MAY returnINVALID_REQUESTfor wholesale calls. The capability declaration is the canonical signal.Response Shape
Identical to brief-mode
get_signalsresponse —signals[], each withsignal_agent_segment_id,name,description,signal_type,data_provider,deployments[],pricing_options[](existing spec).Two additions:
paginationhas_more,cursor, optionaltotal_count— same shape asget_products. Required for wholesale (catalogs may be large).incompleteget_products. Usescope: \"pricing\"when a signal was returned without pricing because the agent could not confirm prices intime_budget. Usescope: \"catalog\"when whole-catalog enumeration could not complete.Pricing in wholesale responses
pricing_options[]MUST be populated for authenticated callers when the agent has firm prices to declare. Pricing models are the existing set:cpm,percent_of_media,flat_fee,per_unit,custom.When pricing varies by buyer account (rate cards), the caller passes
accountand the agent returns account-scoped prices. Whenaccountis omitted, the agent returns its default rate-card pricing or omitspricing_options[](in which case the caller MUST re-query withaccountbefore composing).Unauthenticated callers MAY receive catalog metadata without pricing — same posture as
get_products.Authorization & Provenance
Marketplace signals (
signal_type: \"marketplace\") must remain attributable to their upstream data provider. Wholesale enumeration does not collapse this:data_provider(string identifier).adagents.json— same mechanism as today. The signals agent's URL must appear in the provider'sadagents.jsonauthorization list for that signal class.adagents.jsoncross-reference to materialize a "data publisher catalog" view: for each known data provider, the set of signals available via authorized signals agents, with prices.This composes cleanly with the existing
agent.profile_updatedandpublisher.adagents_changedevents fromspecs/registry-change-feed.md: when a data provider authorizes a new signals agent (or revokes one), the registry's existing event stream surfaces the change without any new event types.Worked Example
Request:
{ \"tool\": \"get_signals\", \"arguments\": { \"discovery_mode\": \"wholesale\", \"account\": { \"buyer_agent_url\": \"https://buyer.example.com\", \"account_id\": \"acct_123\" }, \"filters\": { \"catalog_types\": [\"marketplace\", \"owned\"], \"data_providers\": [\"acme-data\", \"nova-insights\"] }, \"pagination\": { \"max_results\": 50 } } }Response:
{ \"message\": \"Returning 50 of 312 signals in wholesale mode.\", \"context_id\": \"ctx-wholesale-001\", \"signals\": [ { \"signal_agent_segment_id\": \"sigagent_seg_4421\", \"name\": \"Luxury Auto Intenders\", \"description\": \"Households researching premium vehicles in the last 30 days.\", \"signal_type\": \"marketplace\", \"data_provider\": \"acme-data\", \"coverage_percentage\": 18.4, \"deployments\": [ { \"type\": \"platform\", \"platform\": \"the-trade-desk\", \"is_live\": true, \"activation_key\": { \"type\": \"segment_id\", \"segment_id\": \"ttd_seg_99821\" } } ], \"pricing_options\": [ { \"pricing_option_id\": \"po_cpm_1\", \"model\": \"cpm\", \"cpm\": 2.50, \"currency\": \"USD\" } ] } ], \"pagination\": { \"has_more\": true, \"cursor\": \"eyJvIjo1MH0=\", \"total_count\": 312 } }Backward Compatibility
discovery_modeenum field onget_signalsrequest. Default\"brief\".discovery_modebehave exactly as today (brief mode).discovery_mode: \"wholesale\"MAY returnINVALID_REQUEST. Clients SHOULD probe viaget_adcp_capabilitiesfirst.Implementation Phases
discovery_modeto v3.1 request schema. Updateget_signals.mdxtask reference. Adddiscovery_modesto the signals capability declaration inget_adcp_capabilities.adagents.jsoncross-reference. No new endpoints — purely a consumer pattern.Open Questions
discovery_modeusebuying_modeinstead? Signals aren't "bought" in the same sense as inventory.discovery_modeis the more honest verb (signals are discovered then activated). But symmetry argues forbuying_mode. Recommendation:discovery_mode. Happy to align withbuying_modeif reviewers prefer symmetry over verb-accuracy.get_productsis 50,get_signalsbrief mode is 50. Keep 50. Cap at 100.signals: [],pagination.has_more: false, no error. Same asget_products.Happy to follow up with a PR adding this to
specs/and updatingget_signals.mdxif maintainers are aligned on the direction. Reference implementation will land in the prebid salesagent as part of our v3.1 conformance prep.