Why
The v3.1 catalog-sync cluster (PR #4767) is most useful behind a client-side abstraction. SDK users should get an always-up-to-date in-memory replica of every connected agent's catalog with zero polling code — the same posture as the existing RegistrySync in @adcp/client.
Per the project_sdk_both_sides memory: SDKs ship server primitives + testing utilities; conformance runs via Addie's test_adcp_agent tool. This issue covers both the consumer-side CatalogSync API and the server-side webhook delivery worker that sellers will use to emit events.
Scope
Track 1: TypeScript @adcp/client (lands first)
Consumer-side CatalogSync — mirrors RegistrySync:
import { CatalogSync } from '@adcp/client';
const catalog = new CatalogSync({
agents: [
{ url: 'https://sales.example.com', account: { account_id: 'acct_123' } },
{ url: 'https://signals.example.com' },
],
apiKey: 'sk_...',
});
await catalog.start(); // bootstrap + begin polling
// In-memory queries
const product = catalog.products.get('https://sales.example.com', 'prod_premium_ctv_us');
const signals = catalog.signals.search({ category: 'auto', minCoverage: 10 });
// React to changes
catalog.on('product.priced', (event) => {
console.log(`${event.entity_id} priced via ${event.applies_to.scope}`);
});
Behavior:
- Bootstrap via
wholesale enumeration; persist (cache_scope, catalog_version) per agent
- Poll
GET /catalog/events?cursor={last_event_id} every 30s
- Apply events to in-memory indexes incrementally
- Honor
applies_to.scope for cache invalidation (public layer cascade vs. account overlay)
- Handle
catalog.bulk_change by re-bootstrapping that agent
- Cursor persistence to disk (configurable) so restarts resume from last position
- Conditional-fetch automatic — SDK caches versions and sends
if_catalog_version on subsequent identical request shapes
- Per-
filters canonicalization on the client side (matches the seller's canonicalization rule)
Server-side webhook delivery worker — for sellers building on @adcp/client:
- Subscription CRUD endpoints with SSRF guards (URL validation, DNS rebinding defense, metadata-IP block, delivery-target challenge)
- HMAC signer over
{timestamp}\n{delivery_seq}\n{body} canonical string with X-AdCP-Catalog-* headers
- Retry policy: 3 attempts, exp backoff (30s / 5m / 30m); suspension after 24h
- Jittered coalescing 60-300s per subscriber
- Per-principal subscription cap (default 10)
- Secret rotation primitive with overlap window
- Drop-in primitives so existing seller deployments can mount under
/catalog/*
Track 2: Python SDK (adcp package)
Port CatalogSync to Python. Idiomatic shape (async context manager). Same bootstrap + poll + event-emitter pattern.
Track 3: Go SDK (adcp-go)
Port CatalogSync to Go. Idiomatic shape (Channel-based event subscription). Same semantics.
Testing
- Each SDK runs the buyer-side storyboard (Storyboard A from the storyboards issue) against the training agent in CI
- Per the
project_storyboard_client_integration memory: storyboards drive testing in @adcp/client, replacing scenario-based comply
- Cross-SDK conformance: same scenario must produce identical event-stream side effects in TS / Go / Python clients
Acceptance
Dependencies
Related
Why
The v3.1 catalog-sync cluster (PR #4767) is most useful behind a client-side abstraction. SDK users should get an always-up-to-date in-memory replica of every connected agent's catalog with zero polling code — the same posture as the existing
RegistrySyncin@adcp/client.Per the
project_sdk_both_sidesmemory: SDKs ship server primitives + testing utilities; conformance runs via Addie'stest_adcp_agenttool. This issue covers both the consumer-sideCatalogSyncAPI and the server-side webhook delivery worker that sellers will use to emit events.Scope
Track 1: TypeScript
@adcp/client(lands first)Consumer-side
CatalogSync— mirrorsRegistrySync:Behavior:
wholesaleenumeration; persist(cache_scope, catalog_version)per agentGET /catalog/events?cursor={last_event_id}every 30sapplies_to.scopefor cache invalidation (public layer cascade vs. account overlay)catalog.bulk_changeby re-bootstrapping that agentif_catalog_versionon subsequent identical request shapesfilterscanonicalization on the client side (matches the seller's canonicalization rule)Server-side webhook delivery worker — for sellers building on
@adcp/client:{timestamp}\n{delivery_seq}\n{body}canonical string withX-AdCP-Catalog-*headers/catalog/*Track 2: Python SDK (
adcppackage)Port
CatalogSyncto Python. Idiomatic shape (async context manager). Same bootstrap + poll + event-emitter pattern.Track 3: Go SDK (
adcp-go)Port
CatalogSyncto Go. Idiomatic shape (Channel-based event subscription). Same semantics.Testing
project_storyboard_client_integrationmemory: storyboards drive testing in@adcp/client, replacing scenario-based complyAcceptance
CatalogSyncships in@adcp/clientnext minor release after PR feat(catalog-sync): v3.1 catalog sync cluster (#4761, #4762, #4763) #4767 mergesgetting startedrecipeDependencies
Related