|
| 1 | +# Seller Discovery |
| 2 | + |
| 3 | +The buyer agent discovers seller agents through the **IAB AAMP agent registry** — a centralized directory where agents register their identity, capabilities, and connection details. The `RegistryClient` handles discovery, agent card fetching, buyer registration, and trust verification, with built-in caching to minimize registry round-trips. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +```mermaid |
| 8 | +sequenceDiagram |
| 9 | + participant Buyer as Buyer Agent |
| 10 | + participant Client as RegistryClient |
| 11 | + participant Cache as SellerCache |
| 12 | + participant Registry as AAMP Registry |
| 13 | + participant Seller as Seller Agent |
| 14 | +
|
| 15 | + Buyer->>Client: discover_sellers(["ctv"]) |
| 16 | + Client->>Cache: Check cache |
| 17 | + Cache-->>Client: Miss |
| 18 | + Client->>Registry: GET /agents?agent_type=seller&capabilities=ctv |
| 19 | + Registry-->>Client: Agent list |
| 20 | + Client->>Cache: Store results (TTL 5m) |
| 21 | + Client-->>Buyer: list[AgentCard] |
| 22 | + Buyer->>Client: fetch_agent_card(seller_url) |
| 23 | + Client->>Seller: GET /.well-known/agent.json |
| 24 | + Seller-->>Client: Agent card JSON |
| 25 | + Client-->>Buyer: AgentCard |
| 26 | + Buyer->>Client: verify_agent(seller_url) |
| 27 | + Client->>Registry: GET /verify?agent_url=... |
| 28 | + Registry-->>Client: Trust status |
| 29 | + Client-->>Buyer: AgentTrustInfo |
| 30 | +``` |
| 31 | + |
| 32 | +## RegistryClient |
| 33 | + |
| 34 | +The `RegistryClient` manages all interactions with the AAMP agent registry. It wraps HTTP calls with caching, error handling, and Pydantic validation. |
| 35 | + |
| 36 | +### Initialization |
| 37 | + |
| 38 | +```python |
| 39 | +from ad_buyer.registry import RegistryClient |
| 40 | + |
| 41 | +# Default — local development registry |
| 42 | +client = RegistryClient() |
| 43 | + |
| 44 | +# Production registry with custom TTL and timeout |
| 45 | +client = RegistryClient( |
| 46 | + registry_url="https://registry.aamp.iab.com/agent-registry", |
| 47 | + cache_ttl_seconds=600, # 10 minute cache |
| 48 | + timeout=30.0, # 30 second HTTP timeout |
| 49 | +) |
| 50 | +``` |
| 51 | + |
| 52 | +| Parameter | Type | Default | Description | |
| 53 | +|-----------|------|---------|-------------| |
| 54 | +| `registry_url` | `str` | `http://localhost:8080/agent-registry` | Base URL of the AAMP agent registry | |
| 55 | +| `cache_ttl_seconds` | `float` | `300.0` | TTL for cached entries (seconds) | |
| 56 | +| `timeout` | `float` | `15.0` | HTTP request timeout (seconds) | |
| 57 | + |
| 58 | +## Discovering Sellers |
| 59 | + |
| 60 | +### Discover All Sellers |
| 61 | + |
| 62 | +Query the registry for all registered seller agents: |
| 63 | + |
| 64 | +```python |
| 65 | +sellers = await client.discover_sellers() |
| 66 | + |
| 67 | +for seller in sellers: |
| 68 | + print(f"{seller.name} ({seller.agent_id})") |
| 69 | + print(f" URL: {seller.url}") |
| 70 | + print(f" Protocols: {seller.protocols}") |
| 71 | + print(f" Trust: {seller.trust_level.value}") |
| 72 | +``` |
| 73 | + |
| 74 | +### Filter by Capability |
| 75 | + |
| 76 | +Pass a capabilities filter to narrow results to sellers that support specific inventory types: |
| 77 | + |
| 78 | +```python |
| 79 | +# Only CTV sellers |
| 80 | +ctv_sellers = await client.discover_sellers( |
| 81 | + capabilities_filter=["ctv"], |
| 82 | +) |
| 83 | + |
| 84 | +# Sellers with both display and video |
| 85 | +av_sellers = await client.discover_sellers( |
| 86 | + capabilities_filter=["display", "video"], |
| 87 | +) |
| 88 | +``` |
| 89 | + |
| 90 | +The filter is sent as a comma-separated query parameter. The registry returns sellers that match any of the requested capabilities. |
| 91 | + |
| 92 | +### Fetch an Agent Card Directly |
| 93 | + |
| 94 | +If you already know a seller's URL, fetch its agent card from the standard `.well-known/agent.json` endpoint without going through the registry: |
| 95 | + |
| 96 | +```python |
| 97 | +card = await client.fetch_agent_card("https://seller.example.com") |
| 98 | + |
| 99 | +if card: |
| 100 | + print(f"Agent: {card.name}") |
| 101 | + for cap in card.capabilities: |
| 102 | + print(f" Capability: {cap.name} — {cap.description}") |
| 103 | +else: |
| 104 | + print("Agent card not available") |
| 105 | +``` |
| 106 | + |
| 107 | +This is useful when you have a known seller URL (e.g., from a previous deal or a direct partnership) and want to refresh its capabilities without a full registry query. |
| 108 | + |
| 109 | +### Register the Buyer |
| 110 | + |
| 111 | +Register the buyer agent in the registry so that sellers can discover and verify it: |
| 112 | + |
| 113 | +```python |
| 114 | +from ad_buyer.registry import AgentCard, AgentCapability |
| 115 | + |
| 116 | +buyer_card = AgentCard( |
| 117 | + agent_id="buyer-ttd-001", |
| 118 | + name="IAB Buyer Agent (TTD)", |
| 119 | + url="https://buyer.example.com", |
| 120 | + protocols=["opendirect-2.1", "openrtb-2.6"], |
| 121 | + capabilities=[ |
| 122 | + AgentCapability( |
| 123 | + name="ctv", |
| 124 | + description="Connected TV buying", |
| 125 | + tags=["premium", "programmatic"], |
| 126 | + ), |
| 127 | + AgentCapability( |
| 128 | + name="display", |
| 129 | + description="Display ad buying", |
| 130 | + tags=["programmatic"], |
| 131 | + ), |
| 132 | + ], |
| 133 | +) |
| 134 | + |
| 135 | +success = await client.register_buyer(buyer_card) |
| 136 | +if success: |
| 137 | + print("Buyer registered in AAMP registry") |
| 138 | +``` |
| 139 | + |
| 140 | +### Verify Agent Trust |
| 141 | + |
| 142 | +Before transacting with a seller, verify its trust status in the registry: |
| 143 | + |
| 144 | +```python |
| 145 | +trust = await client.verify_agent("https://seller.example.com") |
| 146 | + |
| 147 | +print(f"Registered: {trust.is_registered}") |
| 148 | +print(f"Trust level: {trust.trust_level.value}") |
| 149 | +print(f"Registry ID: {trust.registry_id}") |
| 150 | + |
| 151 | +if trust.trust_level == TrustLevel.BLOCKED: |
| 152 | + print("WARNING: This agent is blocked — do not transact") |
| 153 | +``` |
| 154 | + |
| 155 | +## SellerCache |
| 156 | + |
| 157 | +The `SellerCache` is an in-memory TTL cache that sits in front of all registry calls. It stores both individual `AgentCard` objects and lists of cards returned by discovery queries. |
| 158 | + |
| 159 | +### Caching Strategy |
| 160 | + |
| 161 | +| Behavior | Detail | |
| 162 | +|----------|--------| |
| 163 | +| **Storage** | In-memory dictionary (no external dependencies) | |
| 164 | +| **TTL** | Configurable via `cache_ttl_seconds` (default 5 minutes) | |
| 165 | +| **Expiry** | Lazy — expired entries are evicted on access, not on a timer | |
| 166 | +| **Scope** | Per-`RegistryClient` instance | |
| 167 | +| **Key format** | `discover:<sorted-capabilities>` for lists, `card:<url>` for individual cards | |
| 168 | + |
| 169 | +### Cache Behavior by Method |
| 170 | + |
| 171 | +| Method | Cache Key | Cache Type | |
| 172 | +|--------|-----------|------------| |
| 173 | +| `discover_sellers()` | `discover:` (empty filter) | List cache | |
| 174 | +| `discover_sellers(["ctv"])` | `discover:ctv` | List cache | |
| 175 | +| `discover_sellers(["ctv", "display"])` | `discover:ctv,display` | List cache | |
| 176 | +| `fetch_agent_card(url)` | `card:<url>` | Individual cache | |
| 177 | +| `verify_agent(url)` | Not cached | — | |
| 178 | + |
| 179 | +Discovery results are also cross-cached: each individual `AgentCard` from a discovery response is stored in the individual cache by `agent_id`, so subsequent lookups by ID avoid re-fetching. |
| 180 | + |
| 181 | +### Manual Cache Management |
| 182 | + |
| 183 | +```python |
| 184 | +# Access the cache directly |
| 185 | +cache = client._cache |
| 186 | + |
| 187 | +# Invalidate a specific seller |
| 188 | +cache.invalidate("card:https://seller.example.com") |
| 189 | + |
| 190 | +# Clear everything (e.g., after a known registry update) |
| 191 | +cache.clear() |
| 192 | +``` |
| 193 | + |
| 194 | +!!! tip "When to Invalidate" |
| 195 | + Invalidate a seller's cache entry after a failed transaction or when you receive an error suggesting the seller's capabilities have changed. Clear the entire cache when the registry announces a bulk update. |
| 196 | + |
| 197 | +## Data Models |
| 198 | + |
| 199 | +### AgentCard |
| 200 | + |
| 201 | +Represents a discovered agent (seller or buyer) in the registry. Modeled after the [A2A agent card](https://google.github.io/A2A/) served at `.well-known/agent.json`. |
| 202 | + |
| 203 | +```python |
| 204 | +from ad_buyer.registry import AgentCard |
| 205 | + |
| 206 | +class AgentCard(BaseModel): |
| 207 | + agent_id: str # Unique identifier |
| 208 | + name: str # Human-readable name |
| 209 | + url: str # Base URL of the agent |
| 210 | + protocols: list[str] = [] # ["opendirect-2.1", "openrtb-2.6"] |
| 211 | + capabilities: list[AgentCapability] = [] # What the agent can do |
| 212 | + trust_level: TrustLevel = "unknown" # Registry trust status |
| 213 | +``` |
| 214 | + |
| 215 | +### AgentCapability |
| 216 | + |
| 217 | +A declared capability of an agent — what inventory types or deal types it supports: |
| 218 | + |
| 219 | +```python |
| 220 | +from ad_buyer.registry import AgentCapability |
| 221 | + |
| 222 | +class AgentCapability(BaseModel): |
| 223 | + name: str # "ctv", "display", "video", "audio" |
| 224 | + description: str # Human-readable description |
| 225 | + tags: list[str] = [] # ["premium", "programmatic", "direct"] |
| 226 | +``` |
| 227 | + |
| 228 | +### TrustLevel |
| 229 | + |
| 230 | +Trust status of an agent as determined by the AAMP registry: |
| 231 | + |
| 232 | +| Level | Value | Meaning | |
| 233 | +|-------|-------|---------| |
| 234 | +| `UNKNOWN` | `"unknown"` | Not found in any registry | |
| 235 | +| `REGISTERED` | `"registered"` | Found in the AAMP registry | |
| 236 | +| `VERIFIED` | `"verified"` | Verified by the registry operator | |
| 237 | +| `PREFERRED` | `"preferred"` | Strategic partner with elevated trust | |
| 238 | +| `BLOCKED` | `"blocked"` | Explicitly blocked — do not transact | |
| 239 | + |
| 240 | +### AgentTrustInfo |
| 241 | + |
| 242 | +Result of a trust verification check against the registry: |
| 243 | + |
| 244 | +```python |
| 245 | +from ad_buyer.registry import AgentTrustInfo |
| 246 | + |
| 247 | +class AgentTrustInfo(BaseModel): |
| 248 | + agent_url: str # URL that was verified |
| 249 | + is_registered: bool # Whether agent is in registry |
| 250 | + trust_level: TrustLevel = "unknown" # Trust status |
| 251 | + registry_id: Optional[str] = None # Registry-assigned ID (if registered) |
| 252 | +``` |
| 253 | + |
| 254 | +## Multi-Seller Discovery Workflow |
| 255 | + |
| 256 | +A typical buying workflow discovers multiple sellers, verifies trust, then shops across their media kits: |
| 257 | + |
| 258 | +```mermaid |
| 259 | +graph LR |
| 260 | + A[Discover Sellers] --> B[Filter by Capability] |
| 261 | + B --> C[Verify Trust] |
| 262 | + C --> D[Fetch Agent Cards] |
| 263 | + D --> E[Browse Media Kits] |
| 264 | + E --> F[Compare & Book] |
| 265 | +``` |
| 266 | + |
| 267 | +### Portfolio Shopping Example |
| 268 | + |
| 269 | +Discover CTV sellers, verify trust, and aggregate their media kits for comparison: |
| 270 | + |
| 271 | +```python |
| 272 | +from ad_buyer.registry import RegistryClient, TrustLevel |
| 273 | +from ad_buyer.media_kit import MediaKitClient |
| 274 | + |
| 275 | +registry = RegistryClient( |
| 276 | + registry_url="https://registry.aamp.iab.com/agent-registry", |
| 277 | +) |
| 278 | + |
| 279 | +# Step 1: Discover CTV sellers |
| 280 | +sellers = await registry.discover_sellers( |
| 281 | + capabilities_filter=["ctv"], |
| 282 | +) |
| 283 | +print(f"Found {len(sellers)} CTV sellers") |
| 284 | + |
| 285 | +# Step 2: Verify trust — only transact with registered+ sellers |
| 286 | +trusted_sellers = [] |
| 287 | +for seller in sellers: |
| 288 | + trust = await registry.verify_agent(seller.url) |
| 289 | + if trust.trust_level in ( |
| 290 | + TrustLevel.REGISTERED, |
| 291 | + TrustLevel.VERIFIED, |
| 292 | + TrustLevel.PREFERRED, |
| 293 | + ): |
| 294 | + trusted_sellers.append(seller) |
| 295 | + else: |
| 296 | + print(f"Skipping {seller.name} (trust: {trust.trust_level.value})") |
| 297 | + |
| 298 | +print(f"{len(trusted_sellers)} trusted sellers") |
| 299 | + |
| 300 | +# Step 3: Browse media kits across trusted sellers |
| 301 | +async with MediaKitClient(api_key="my-key") as media_client: |
| 302 | + seller_urls = [s.url for s in trusted_sellers] |
| 303 | + all_packages = await media_client.aggregate_across_sellers(seller_urls) |
| 304 | + |
| 305 | +print(f"Found {len(all_packages)} packages across {len(trusted_sellers)} sellers") |
| 306 | +``` |
| 307 | + |
| 308 | +### Capability-Based Routing |
| 309 | + |
| 310 | +Route campaign requirements to the right sellers based on declared capabilities: |
| 311 | + |
| 312 | +```python |
| 313 | +# Campaign needs CTV + display |
| 314 | +campaign_needs = ["ctv", "display"] |
| 315 | + |
| 316 | +# Find sellers that support these capabilities |
| 317 | +matching_sellers = await registry.discover_sellers( |
| 318 | + capabilities_filter=campaign_needs, |
| 319 | +) |
| 320 | + |
| 321 | +# Group by capability for mixed-media campaigns |
| 322 | +ctv_sellers = [ |
| 323 | + s for s in matching_sellers |
| 324 | + if any(c.name == "ctv" for c in s.capabilities) |
| 325 | +] |
| 326 | +display_sellers = [ |
| 327 | + s for s in matching_sellers |
| 328 | + if any(c.name == "display" for c in s.capabilities) |
| 329 | +] |
| 330 | +``` |
| 331 | + |
| 332 | +## Error Handling |
| 333 | + |
| 334 | +The `RegistryClient` handles errors gracefully — failed calls return empty results or `None` rather than raising exceptions: |
| 335 | + |
| 336 | +| Scenario | Method | Return Value | |
| 337 | +|----------|--------|-------------| |
| 338 | +| Registry unreachable | `discover_sellers()` | `[]` (empty list) | |
| 339 | +| Registry returns non-200 | `discover_sellers()` | `[]` (empty list) | |
| 340 | +| Agent card not found | `fetch_agent_card()` | `None` | |
| 341 | +| Agent card endpoint down | `fetch_agent_card()` | `None` | |
| 342 | +| Verification fails | `verify_agent()` | `AgentTrustInfo(is_registered=False, trust_level=UNKNOWN)` | |
| 343 | +| Registration fails | `register_buyer()` | `False` | |
| 344 | +| Invalid JSON / validation error | Any | Logged warning, graceful fallback | |
| 345 | + |
| 346 | +All errors are logged at `WARNING` or `DEBUG` level. The client never raises `httpx.HTTPError` or `pydantic.ValidationError` to the caller. |
| 347 | + |
| 348 | +!!! warning "Silent Failures" |
| 349 | + Because the client returns empty results on failure, always check the length of discovery results. An empty list could mean "no sellers match" or "the registry is down." Monitor logs for `WARNING`-level messages to distinguish the two. |
| 350 | + |
| 351 | +## Related |
| 352 | + |
| 353 | +- [Seller Agent Discovery](https://iabtechlab.github.io/seller-agent/api/agent-discovery/) — How seller agents register and expose their agent cards |
| 354 | +- [Media Kit Discovery](media-kit.md) — Browse seller inventory after discovering sellers |
| 355 | +- [Authentication](authentication.md) — API key setup for accessing seller endpoints |
| 356 | +- [A2A Client](a2a-client.md) — Agent-to-agent communication protocol |
| 357 | +- [Seller Agent Integration](../integration/seller-agent.md) — Full integration guide |
0 commit comments