Skip to content

SDK: CatalogSync in @adcp/client (TS, Go, Python) + webhook delivery worker #4789

Description

@bokelley

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

Metadata

Metadata

Assignees

No one assigned

    Labels

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

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions