Skip to content

Proposal: get_signals wholesale discovery mode (symmetric to get_products) #4762

@bokelley

Description

@bokelley

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

  1. A signals agent can return its complete priced catalog in response to a single (paginated) call.
  2. The contract is symmetric with get_products buying_mode: \"wholesale\" — synchronous, paginated, firm-priced, with partial-completion declared via incomplete[].
  3. Marketplace signals remain attributable to their upstream data provider via existing adagents.json machinery — wholesale enumeration does not collapse provenance.
  4. 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

  1. 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.
  2. 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."
  3. 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

  1. 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.
  2. Default pagination size. get_products is 50, get_signals brief mode is 50. Keep 50. Cap at 100.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    claude-triagedIssue has been triaged by the Claude Code triage routine. Remove to re-triage.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions