Skip to content

Commit 97018ce

Browse files
mobtownlabsclaude
andcommitted
Add Media Kit Discovery documentation
Covers MediaKitClient usage, public vs authenticated access, identity-based tiering, multi-seller aggregation, and data models with cross-links to seller docs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0a22dc6 commit 97018ce

3 files changed

Lines changed: 354 additions & 1 deletion

File tree

.beads/PROGRESS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,4 @@
4242
| \[ ] | buyer-j95 | 3G: Infrastructure-as-Code Deployment (CloudFormation/Terraform) | P3 || |
4343

4444
---
45-
*Last updated: 2026-03-10 19:32 UTC — auto-generated by beads*
45+
*Last updated: 2026-03-10 20:43 UTC — auto-generated by beads*

docs/api/media-kit.md

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
# Media Kit Discovery
2+
3+
The media kit is a seller's **inventory catalog** — a curated collection of ad packages that buyer agents browse to discover available inventory. The buyer agent includes a dedicated `MediaKitClient` for consuming seller media kits.
4+
5+
## Overview
6+
7+
```mermaid
8+
sequenceDiagram
9+
participant Buyer as Buyer Agent
10+
participant Client as MediaKitClient
11+
participant Seller as Seller Agent
12+
13+
Buyer->>Client: Browse seller inventory
14+
Client->>Seller: GET /media-kit
15+
Seller-->>Client: Overview (featured + all packages)
16+
Client->>Seller: GET /media-kit/packages/pkg-001
17+
Seller-->>Client: Package details (tier-gated)
18+
Client->>Seller: POST /media-kit/search
19+
Seller-->>Client: Search results
20+
Buyer->>Buyer: Select package → Book via OpenDirect
21+
```
22+
23+
## MediaKitClient
24+
25+
The `MediaKitClient` handles all media kit interactions. Authentication is optional — unauthenticated requests receive public views with price ranges; authenticated requests receive exact pricing and full placement details.
26+
27+
### Initialization
28+
29+
```python
30+
from ad_buyer.media_kit import MediaKitClient
31+
32+
# Public access (no auth) — price ranges only
33+
public_client = MediaKitClient()
34+
35+
# Authenticated access — exact pricing + placements
36+
auth_client = MediaKitClient(api_key="your-seller-api-key")
37+
```
38+
39+
When an API key is provided, the client includes an `X-API-Key` header on all requests.
40+
41+
## Browsing a Seller's Media Kit
42+
43+
### Get Media Kit Overview
44+
45+
Fetch the seller's full catalog with featured packages highlighted:
46+
47+
```python
48+
async with MediaKitClient(api_key="my-key") as client:
49+
kit = await client.get_media_kit("http://seller.example.com:8001")
50+
51+
print(f"Seller: {kit.seller_name}")
52+
print(f"Total packages: {kit.total_packages}")
53+
54+
for pkg in kit.featured:
55+
print(f" Featured: {pkg.name}{pkg.price_range}")
56+
```
57+
58+
**curl equivalent:**
59+
60+
```bash
61+
# Public (no auth)
62+
curl http://seller.example.com:8001/media-kit
63+
64+
# Authenticated
65+
curl http://seller.example.com:8001/media-kit \
66+
-H "X-API-Key: your-key"
67+
```
68+
69+
### List Packages
70+
71+
Filter packages by layer or featured status:
72+
73+
```python
74+
# All packages
75+
packages = await client.list_packages("http://seller.example.com:8001")
76+
77+
# Only curated packages
78+
curated = await client.list_packages(
79+
"http://seller.example.com:8001",
80+
layer="curated",
81+
)
82+
83+
# Only featured packages
84+
featured = await client.list_packages(
85+
"http://seller.example.com:8001",
86+
featured_only=True,
87+
)
88+
```
89+
90+
### Get Package Detail
91+
92+
Retrieve a single package. Authenticated requests receive a `PackageDetail` with exact pricing and placements:
93+
94+
```python
95+
pkg = await client.get_package(
96+
"http://seller.example.com:8001",
97+
"pkg-abc12345",
98+
)
99+
100+
if isinstance(pkg, PackageDetail):
101+
# Authenticated view
102+
print(f"Exact price: ${pkg.exact_price} CPM")
103+
print(f"Floor price: ${pkg.floor_price} CPM")
104+
for p in pkg.placements:
105+
print(f" Product: {p.product_name} ({p.ad_formats})")
106+
else:
107+
# Public view
108+
print(f"Price range: {pkg.price_range}")
109+
```
110+
111+
### Search Packages
112+
113+
Keyword search across package names, descriptions, tags, and content categories:
114+
115+
```python
116+
from ad_buyer.media_kit.models import SearchFilter
117+
118+
# Simple search
119+
results = await client.search_packages(
120+
"http://seller.example.com:8001",
121+
query="sports video",
122+
)
123+
124+
# Search with identity context (may unlock better pricing)
125+
results = await client.search_packages(
126+
"http://seller.example.com:8001",
127+
query="sports video",
128+
filters=SearchFilter(
129+
buyer_tier="agency",
130+
agency_id="omnicom-456",
131+
advertiser_id="coca-cola",
132+
),
133+
)
134+
```
135+
136+
### Aggregate Across Multiple Sellers
137+
138+
Query multiple sellers in parallel and combine their packages:
139+
140+
```python
141+
sellers = [
142+
"http://seller-a.example.com:8001",
143+
"http://seller-b.example.com:8002",
144+
"http://seller-c.example.com:8003",
145+
]
146+
147+
all_packages = await client.aggregate_across_sellers(sellers)
148+
print(f"Found {len(all_packages)} packages across {len(sellers)} sellers")
149+
```
150+
151+
Failed sellers are silently skipped — the client logs warnings but continues with responsive sellers.
152+
153+
## Unauthenticated vs Authenticated Access
154+
155+
The seller returns different views depending on whether an `X-API-Key` header is present:
156+
157+
### Public View (No API Key)
158+
159+
| Field | Included |
160+
|-------|----------|
161+
| Package name, description | Yes |
162+
| Ad formats, device types | Yes |
163+
| Content categories (`cat`) | Yes |
164+
| Geo targets, tags | Yes |
165+
| Featured status | Yes |
166+
| **Price range** (e.g. "$28–$42 CPM") | Yes |
167+
| Exact price | **No** |
168+
| Floor price | **No** |
169+
| Placements (products) | **No** |
170+
| Audience segments | **No** |
171+
| Negotiation flags | **No** |
172+
173+
### Authenticated View (With API Key)
174+
175+
All public fields **plus**:
176+
177+
| Field | Description |
178+
|-------|-------------|
179+
| `exact_price` | Tier-adjusted CPM (e.g. `33.25`) |
180+
| `floor_price` | Minimum acceptable price |
181+
| `currency` | ISO 4217 code (e.g. `"USD"`) |
182+
| `placements` | Individual products with ad formats, device types, weights |
183+
| `audience_segment_ids` | IAB Audience Taxonomy 1.1 IDs |
184+
| `negotiation_enabled` | Whether this tier can negotiate |
185+
| `volume_discounts_available` | Whether volume discounts apply |
186+
187+
### Pricing Tiers
188+
189+
Sellers apply tier-based discounts. Higher identity revelation unlocks better pricing:
190+
191+
| Tier | Identity Required | Typical Discount |
192+
|------|-------------------|-----------------|
193+
| PUBLIC | None | 0% (range only) |
194+
| SEAT | API key (DSP seat) | ~5% |
195+
| AGENCY | Agency ID header | ~10% |
196+
| ADVERTISER | Advertiser ID header | ~15% |
197+
198+
!!! tip "Progressive Identity Revelation"
199+
Start with public browsing to evaluate inventory, then authenticate to see exact pricing. Provide agency/advertiser identity during search to unlock the best rates.
200+
201+
## Identity-Based Access
202+
203+
Beyond the API key, the buyer can reveal its identity for seller-side tier resolution. The `SearchFilter` supports this:
204+
205+
```python
206+
# Search with full identity context
207+
results = await client.search_packages(
208+
seller_url,
209+
query="premium video",
210+
filters=SearchFilter(
211+
buyer_tier="advertiser",
212+
agency_id="omnicom-456",
213+
advertiser_id="coca-cola",
214+
),
215+
)
216+
```
217+
218+
Alternatively, the `BuyerIdentity` model can generate identity headers for direct HTTP calls:
219+
220+
```python
221+
from ad_buyer.models.buyer_identity import BuyerIdentity
222+
223+
identity = BuyerIdentity(
224+
seat_id="ttd-seat-123",
225+
agency_id="omnicom-456",
226+
advertiser_id="coca-cola",
227+
)
228+
229+
# Produces: {"X-DSP-Seat-ID": "...", "X-Agency-ID": "...", "X-Advertiser-ID": "..."}
230+
headers = identity.to_header_dict()
231+
```
232+
233+
## Data Models
234+
235+
### PackageSummary (Public)
236+
237+
```python
238+
@dataclass
239+
class PackageSummary:
240+
package_id: str
241+
name: str
242+
description: Optional[str]
243+
ad_formats: list[str] # ["video", "banner"]
244+
device_types: list[int] # [3, 4, 5] (CTV, Phone, Tablet)
245+
cat: list[str] # ["IAB19"] (IAB Content Taxonomy)
246+
geo_targets: list[str] # ["US", "US-NY"]
247+
tags: list[str] # ["premium", "sports"]
248+
price_range: str # "$28-$42 CPM"
249+
is_featured: bool
250+
seller_url: Optional[str] # Set when aggregating across sellers
251+
```
252+
253+
### PackageDetail (Authenticated)
254+
255+
Extends `PackageSummary` with:
256+
257+
```python
258+
@dataclass
259+
class PackageDetail(PackageSummary):
260+
exact_price: Optional[float] # 33.25
261+
floor_price: Optional[float] # 29.40
262+
currency: str # "USD"
263+
placements: list[PlacementDetail]
264+
audience_segment_ids: list[str] # IAB Audience Taxonomy 1.1
265+
negotiation_enabled: bool
266+
volume_discounts_available: bool
267+
```
268+
269+
### MediaKit (Overview)
270+
271+
```python
272+
@dataclass
273+
class MediaKit:
274+
seller_url: str
275+
seller_name: str
276+
total_packages: int
277+
featured: list[PackageSummary]
278+
all_packages: list[PackageSummary]
279+
```
280+
281+
## Package Layers
282+
283+
Sellers organize inventory into three layers:
284+
285+
| Layer | Source | Description |
286+
|-------|--------|-------------|
287+
| **synced** | Ad server import | Auto-created from GAM/FreeWheel inventory |
288+
| **curated** | Publisher manual | Hand-built premium bundles |
289+
| **dynamic** | Agent-assembled | Created on-the-fly during negotiations |
290+
291+
Filter by layer when listing:
292+
293+
```python
294+
# Only show synced (ad server) packages
295+
synced = await client.list_packages(seller_url, layer="synced")
296+
297+
# Only show curated (premium) packages
298+
curated = await client.list_packages(seller_url, layer="curated")
299+
```
300+
301+
## Error Handling
302+
303+
The client raises `MediaKitError` on failures:
304+
305+
```python
306+
from ad_buyer.media_kit.models import MediaKitError
307+
308+
try:
309+
kit = await client.get_media_kit(seller_url)
310+
except MediaKitError as e:
311+
print(f"Failed: {e}")
312+
print(f"Seller: {e.seller_url}")
313+
print(f"HTTP status: {e.status_code}")
314+
```
315+
316+
Common error scenarios:
317+
318+
| Scenario | Behavior |
319+
|----------|----------|
320+
| Seller unreachable | `MediaKitError` with no status code |
321+
| Request timeout | `MediaKitError` (default 30s timeout) |
322+
| HTTP 4xx/5xx | `MediaKitError` with status code |
323+
| Failed seller in aggregation | Silently skipped, warning logged |
324+
325+
## Workflow: From Media Kit to Booking
326+
327+
The media kit is the first step in the deal lifecycle:
328+
329+
```mermaid
330+
graph LR
331+
A[Browse Media Kit] --> B[Select Package]
332+
B --> C[Get Exact Pricing]
333+
C --> D[Request Quote]
334+
D --> E[Negotiate]
335+
E --> F[Book Deal]
336+
```
337+
338+
1. **Browse** the seller's media kit (public or authenticated)
339+
2. **Select** a package that matches campaign requirements
340+
3. **Get exact pricing** by authenticating with your API key
341+
4. **Request a quote** via the OpenDirect API for specific products in the package
342+
5. **Negotiate** if your tier allows it (Agency/Advertiser)
343+
6. **Book** the deal through the standard booking flow
344+
345+
See [Booking Lifecycle](bookings.md) and [Seller Agent Integration](../integration/seller-agent.md) for the full workflow.
346+
347+
## Related
348+
349+
- [Seller Agent Media Kit Setup](https://iabtechlab.github.io/seller-agent/guides/media-kit/) — How publishers configure their media kit
350+
- [Authentication](authentication.md) — API key setup for authenticated access
351+
- [Products](products.md) — Product search endpoint
352+
- [Seller Agent Integration](../integration/seller-agent.md) — Full integration guide

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ nav:
6161
- MCP Client: api/mcp-client.md
6262
- A2A Client: api/a2a-client.md
6363
- Authentication: api/authentication.md
64+
- Media Kit: api/media-kit.md
6465
- Bookings: api/bookings.md
6566
- Products: api/products.md
6667
- Architecture:

0 commit comments

Comments
 (0)