One of three companion v3.1 proposals for catalog sync between AdCP agents and consumers (storefronts, federated marketplaces, registries). The other two address #4762 (get_signals wholesale mode) and a per-agent #4763 (inventory change-feed) — issues to follow.
Problem
Even with get_products buying_mode: "wholesale" (and the proposed get_signals discovery_mode: "wholesale"), a consumer mirroring an agent's catalog must re-fetch the full paginated result on every poll cycle to detect changes. For agents whose catalogs are stable for hours or days, this wastes consumer bandwidth and seller compute.
A separate proposal (per-agent inventory change-feed) solves this more comprehensively for agents that implement it. But that proposal will have a long adoption tail. Those consumers still want a cheap "has anything changed?" probe.
The web solved this in 1999 with HTTP ETag / If-None-Match. The protocol-level equivalent is a catalog_version token: opaque, agent-defined, returned on every wholesale response, and submittable on subsequent requests so the agent can short-circuit with "no changes."
This is the cheapest of the three companion proposals to spec and the lowest implementation cost for sellers — yet it removes the majority of wasted polling traffic for stable catalogs. It is independent of, and complementary to, the change-feed proposal: agents MAY implement either, both, or neither.
Goals
- A consumer that just synced an agent's catalog can ask "has anything changed since version X?" in one cheap call, regardless of catalog size.
- The mechanism works for
get_products (any buying_mode) and get_signals (any discovery_mode), not only wholesale enumeration. Brief-mode results MAY also carry a version so consumers can cache.
- The token is opaque to consumers — agents choose their own versioning scheme (monotonic counter, content hash, commit SHA, etc.).
- Backward-compatible: pre-v3.1 agents ignore the token; consumers see no version returned and fall through to full re-fetch.
Design Principles
- Opaque tokens. Consumers MUST treat
catalog_version as opaque. No format, no ordering, no inspection.
- Cheap unchanged response. When the agent confirms "no change," the response carries no
products[]/signals[] payload — just the metadata confirming the version is still current.
- No new endpoint. This works on the existing
get_products / get_signals tasks. Adding parameters and response fields, not surfaces.
- Two-level versioning (optional). A separate
pricing_version lets agents tell consumers "structure is unchanged but pricing moved" — common for rate-card updates that don't change inventory metadata.
Request Shape
Add optional fields to get_products and get_signals:
| Parameter |
Type |
Required |
Description |
if_catalog_version |
string |
No |
An opaque catalog_version token returned by a prior response from this agent. When provided, the agent compares against its current catalog version and MAY return an unchanged: true response (omitting products/signals) if nothing has changed. |
if_pricing_version |
string |
No |
An opaque pricing_version token from a prior response. Same behavior, narrower scope: structure must be unchanged AND pricing must be unchanged. Optional — agents that don't track pricing separately ignore this and fall back to if_catalog_version semantics. |
Both fields are scoped to the (agent, caller_account, filters) tuple. Versions are NOT comparable across agents, across accounts, or across different filter sets. Consumers MUST cache the version returned alongside the request parameters used.
Response Shape
Add response metadata fields:
| Field |
Type |
Description |
catalog_version |
string |
Opaque token representing the version of the catalog state used to compose this response. Agents implementing this proposal MUST return it on every response. Agents not implementing it MAY omit. |
pricing_version |
string |
Optional opaque token representing the version of the pricing layer. When the agent supports independent pricing versioning, pricing_version changes when prices move but catalog_version changes only when structure/metadata moves. Agents not separating these MAY omit pricing_version and use catalog_version for both. |
unchanged |
boolean |
true ONLY when the request carried if_catalog_version (and/or if_pricing_version) matching the agent's current version. When true, products[]/signals[] MUST be omitted; catalog_version (echoed) MUST still be present. |
Unchanged response example
Request:
{
"buying_mode": "wholesale",
"if_catalog_version": "v2026-05-18T08:00:00Z-acme-rev412"
}
Response (catalog unchanged):
{
"message": "Catalog unchanged since v2026-05-18T08:00:00Z-acme-rev412.",
"context_id": "ctx-abc-789",
"unchanged": true,
"catalog_version": "v2026-05-18T08:00:00Z-acme-rev412",
"pricing_version": "v2026-05-18T08:00:00Z-acme-rev412"
}
Response (catalog changed):
{
"message": "Returning 312 products.",
"context_id": "ctx-abc-790",
"unchanged": false,
"catalog_version": "v2026-05-18T10:15:00Z-acme-rev415",
"pricing_version": "v2026-05-18T10:15:00Z-acme-rev415",
"products": [ ... ],
"pagination": { "has_more": true, "cursor": "..." }
}
Consumers receiving unchanged: true MUST NOT mutate their local catalog mirror.
Versioning Scope
A returned catalog_version is scoped to the request parameters that produced it. The agent MUST return a version that is sensitive to: buying_mode / discovery_mode, account, filters, property_list / catalog.
The simplest correct implementation is: hash the request parameters with the current catalog state. Sophisticated agents may use a global version that's safe to reuse across requests; naive agents may use per-request hashes. Both are conformant.
Pagination Interaction
catalog_version is associated with the catalog as a whole, not individual pages. When a consumer is mid-pagination and the catalog mutates between pages, the agent SHOULD either (1) return the new catalog_version on each page and let the consumer decide whether to restart (RECOMMENDED), or (2) snapshot the catalog at the start of pagination and serve subsequent pages from that snapshot. Consumers receiving a catalog_version change mid-pagination SHOULD restart pagination from cursor: null.
Backward Compatibility
- Pre-v3.1 agents that ignore
if_catalog_version simply return the full payload — semantically correct, just inefficient. Same as missing-ETag behavior in HTTP.
- Pre-v3.1 consumers that don't send
if_catalog_version continue to receive full payloads.
- Agents MAY implement only the response side (always return
catalog_version, never honor if_catalog_version requests) as a transitional step.
Implementation Phases
- Spec + reference behavior. Add
catalog_version, pricing_version, unchanged to response schemas; if_catalog_version, if_pricing_version to request schemas. Update task reference docs.
- Reference implementation in the AdCP reference sales agent + signals agent. Conformance test vector: "GET with matching version returns
unchanged: true and no payload."
- Client SDK helpers —
@adcp/client caches the most recent catalog_version per (agent, account, filters) tuple and automatically sends if_catalog_version on subsequent identical requests.
Open Questions
- Should
unchanged be an HTTP-level signal (304 Not Modified) instead of a body field? AdCP runs over MCP and A2A, not HTTP semantics. A protocol-level field is more portable. Recommendation: body field.
- TTL on versions. Should the spec require a minimum retention for version recognition? Recommendation: no minimum; agents that can't recognize the token return full payload, same as the unchanged-server path.
pricing_version necessity. A simpler proposal collapses pricing into catalog_version. Recommendation: keep pricing_version as optional. Many sellers update prices far more often than structure, and the separation lets storefronts re-price compositions without re-rendering catalogs.
Happy to follow up with a PR adding this to specs/ and updating the get_products / get_signals task references if maintainers are aligned on the direction. Reference implementation will land in the prebid salesagent as part of our v3.1 conformance prep.
Problem
Even with
get_products buying_mode: "wholesale"(and the proposedget_signals discovery_mode: "wholesale"), a consumer mirroring an agent's catalog must re-fetch the full paginated result on every poll cycle to detect changes. For agents whose catalogs are stable for hours or days, this wastes consumer bandwidth and seller compute.A separate proposal (per-agent inventory change-feed) solves this more comprehensively for agents that implement it. But that proposal will have a long adoption tail. Those consumers still want a cheap "has anything changed?" probe.
The web solved this in 1999 with HTTP
ETag/If-None-Match. The protocol-level equivalent is acatalog_versiontoken: opaque, agent-defined, returned on every wholesale response, and submittable on subsequent requests so the agent can short-circuit with "no changes."This is the cheapest of the three companion proposals to spec and the lowest implementation cost for sellers — yet it removes the majority of wasted polling traffic for stable catalogs. It is independent of, and complementary to, the change-feed proposal: agents MAY implement either, both, or neither.
Goals
get_products(anybuying_mode) andget_signals(anydiscovery_mode), not only wholesale enumeration. Brief-mode results MAY also carry a version so consumers can cache.Design Principles
catalog_versionas opaque. No format, no ordering, no inspection.products[]/signals[]payload — just the metadata confirming the version is still current.get_products/get_signalstasks. Adding parameters and response fields, not surfaces.pricing_versionlets agents tell consumers "structure is unchanged but pricing moved" — common for rate-card updates that don't change inventory metadata.Request Shape
Add optional fields to
get_productsandget_signals:if_catalog_versioncatalog_versiontoken returned by a prior response from this agent. When provided, the agent compares against its current catalog version and MAY return anunchanged: trueresponse (omittingproducts/signals) if nothing has changed.if_pricing_versionpricing_versiontoken from a prior response. Same behavior, narrower scope: structure must be unchanged AND pricing must be unchanged. Optional — agents that don't track pricing separately ignore this and fall back toif_catalog_versionsemantics.Both fields are scoped to the (agent, caller_account, filters) tuple. Versions are NOT comparable across agents, across accounts, or across different filter sets. Consumers MUST cache the version returned alongside the request parameters used.
Response Shape
Add response metadata fields:
catalog_versionpricing_versionpricing_versionchanges when prices move butcatalog_versionchanges only when structure/metadata moves. Agents not separating these MAY omitpricing_versionand usecatalog_versionfor both.unchangedtrueONLY when the request carriedif_catalog_version(and/orif_pricing_version) matching the agent's current version. Whentrue,products[]/signals[]MUST be omitted;catalog_version(echoed) MUST still be present.Unchanged response example
Request:
{ "buying_mode": "wholesale", "if_catalog_version": "v2026-05-18T08:00:00Z-acme-rev412" }Response (catalog unchanged):
{ "message": "Catalog unchanged since v2026-05-18T08:00:00Z-acme-rev412.", "context_id": "ctx-abc-789", "unchanged": true, "catalog_version": "v2026-05-18T08:00:00Z-acme-rev412", "pricing_version": "v2026-05-18T08:00:00Z-acme-rev412" }Response (catalog changed):
{ "message": "Returning 312 products.", "context_id": "ctx-abc-790", "unchanged": false, "catalog_version": "v2026-05-18T10:15:00Z-acme-rev415", "pricing_version": "v2026-05-18T10:15:00Z-acme-rev415", "products": [ ... ], "pagination": { "has_more": true, "cursor": "..." } }Consumers receiving
unchanged: trueMUST NOT mutate their local catalog mirror.Versioning Scope
A returned
catalog_versionis scoped to the request parameters that produced it. The agent MUST return a version that is sensitive to:buying_mode/discovery_mode,account,filters,property_list/catalog.The simplest correct implementation is: hash the request parameters with the current catalog state. Sophisticated agents may use a global version that's safe to reuse across requests; naive agents may use per-request hashes. Both are conformant.
Pagination Interaction
catalog_versionis associated with the catalog as a whole, not individual pages. When a consumer is mid-pagination and the catalog mutates between pages, the agent SHOULD either (1) return the newcatalog_versionon each page and let the consumer decide whether to restart (RECOMMENDED), or (2) snapshot the catalog at the start of pagination and serve subsequent pages from that snapshot. Consumers receiving acatalog_versionchange mid-pagination SHOULD restart pagination fromcursor: null.Backward Compatibility
if_catalog_versionsimply return the full payload — semantically correct, just inefficient. Same as missing-ETag behavior in HTTP.if_catalog_versioncontinue to receive full payloads.catalog_version, never honorif_catalog_versionrequests) as a transitional step.Implementation Phases
catalog_version,pricing_version,unchangedto response schemas;if_catalog_version,if_pricing_versionto request schemas. Update task reference docs.unchanged: trueand no payload."@adcp/clientcaches the most recentcatalog_versionper (agent, account, filters) tuple and automatically sendsif_catalog_versionon subsequent identical requests.Open Questions
unchangedbe an HTTP-level signal (304 Not Modified) instead of a body field? AdCP runs over MCP and A2A, not HTTP semantics. A protocol-level field is more portable. Recommendation: body field.pricing_versionnecessity. A simpler proposal collapses pricing intocatalog_version. Recommendation: keeppricing_versionas optional. Many sellers update prices far more often than structure, and the separation lets storefronts re-price compositions without re-rendering catalogs.Happy to follow up with a PR adding this to
specs/and updating theget_products/get_signalstask references if maintainers are aligned on the direction. Reference implementation will land in the prebid salesagent as part of our v3.1 conformance prep.