diff --git a/data/taxonomies/agentic-audiences-draft-2026-01/README.md b/data/taxonomies/agentic-audiences-draft-2026-01/README.md new file mode 100644 index 0000000..5302f0d --- /dev/null +++ b/data/taxonomies/agentic-audiences-draft-2026-01/README.md @@ -0,0 +1,71 @@ +# IAB Agentic Audiences (DRAFT, 2026-01) + +This directory vendors the wire-format-relevant subset of the IAB Tech Lab +Agentic Audiences specification, formerly known as User Context Protocol (UCP). + +| Field | Value | +|-------|-------| +| Version | draft-2026-01 | +| Spec status | DRAFT (last upstream update 2026-01-28) | +| Source repo | https://github.com/IABTechLab/agentic-audiences | +| Fetched at | 2026-04-25T19:27:34Z | +| Spec license | CC-BY-4.0 (https://creativecommons.org/licenses/by/4.0/) | +| Reference impl license | Apache-2.0 (see `spec/LICENSE-APACHE`) | +| Attribution | "IAB Agentic Audiences" by IAB Tech Lab; spec is licensed under CC BY 4.0; reference implementations are licensed under Apache 2.0. | + +## What is vendored + +We vendor only the wire-format-relevant subset: + +``` +spec/ + UPSTREAM_README.md (upstream README — context only) + LICENSE (upstream license notice) + LICENSE-APACHE (Apache-2.0 for reference impl) + roadmap.md (upstream specs/roadmap.md — currently empty placeholder) + docs/ + systems-and-models.md (background on agentic systems and models) + v1.0/ + embedding-exchange.md (wire format for embedding exchange) + embedding-taxonomy.md (taxonomy of embedding signals) + schema/ + agent_interface.schema.json (currently empty upstream) + embedding_format.schema.json (JSON Schema for embedding payload) + examples/ + buyer_agent_request.json (currently empty upstream) + embedding_update.json (reference example) + seller_agent_response.json (currently empty upstream) +``` + +Empty files are reproduced verbatim from upstream — they are placeholders the +spec authors haven't filled in yet. Sizes are pinned in `../taxonomies.lock.json` +so any future fill-in will be detected. + +## Why this subset + +Per proposal §5.6, Agentic Audiences is the **dynamic carrier** for embedding +references; Standard / Contextual taxonomies are static IDs. The seller side +needs: + +1. The wire-format spec (`embedding-exchange.md`, `embedding_format.schema.json`) + to validate inbound `AudienceRef(type=agentic, ...)` payloads. +2. The taxonomy of signal types (`embedding-taxonomy.md`) to mirror in seller + `AgenticCapabilities.supported_signal_types`. +3. The reference example (`embedding_update.json`) to keep test fixtures aligned. + +We deliberately do NOT vendor the upstream `src/`, `prebid-module/`, or +`community/` directories — those are reference implementations, not wire format. + +## Versioning + +Spec version pinned at `draft-2026-01`. The exact `sha256` of each vendored file +is recorded in `../taxonomies.lock.json`. Refresh through +`scripts/update-taxonomies.py` (proposal §5.4); treat as a manual reviewed update +because the spec is still DRAFT and field shapes may change. + +## How this seller copy relates to the buyer copy + +The buyer ships the same vendored subset at +`ad_buyer_system/data/taxonomies/agentic-audiences-draft-2026-01/`. Both sides +emit the relevant `taxonomy_lock_hashes` in capability discovery (proposal §5.7) +so any drift between buyer and seller copies is detected at runtime. diff --git a/data/taxonomies/agentic-audiences-draft-2026-01/spec/LICENSE b/data/taxonomies/agentic-audiences-draft-2026-01/spec/LICENSE new file mode 100644 index 0000000..b48d5e1 --- /dev/null +++ b/data/taxonomies/agentic-audiences-draft-2026-01/spec/LICENSE @@ -0,0 +1,76 @@ +Agentic Audiences +Copyright (c) 2025 LiveRamp Holdings, Inc. + +====================================================================== +LICENSE OVERVIEW +====================================================================== + +This repository contains two types of materials, each with its own license: + +1. Specifications, schemas, and documentation + Licensed under: Creative Commons Attribution 4.0 International (CC BY 4.0) + See: https://creativecommons.org/licenses/by/4.0/ + +2. Reference implementations, SDKs, and source code + Licensed under: Apache License 2.0 + See: https://www.apache.org/licenses/LICENSE-2.0 + +====================================================================== +LICENSE DETAILS +====================================================================== + +---------------------------------------------------------------------- +A. Specification and Documentation License (CC BY 4.0) +---------------------------------------------------------------------- + +Files covered: +- All Markdown files under /specs and /docs +- All schema and example files (.json, .yaml, .yml, .md) in the specification tree +- Any whitepapers, diagrams, and explanatory materials describing Agentic Audiences + +Summary: +You are free to share and adapt the specification for any purpose, provided +that attribution is given to LiveRamp Holdings, Inc. + +Full license text available at: +https://creativecommons.org/licenses/by/4.0/legalcode.txt + +Example attribution: +"Portions of this work are based on Agentic Audiences, +developed by LiveRamp Holdings, Inc. (CC BY 4.0)." + +---------------------------------------------------------------------- +B. Software and Reference Implementation License (Apache 2.0) +---------------------------------------------------------------------- + +Files covered: +- Source code in /reference, /sdk, /tools, /tests, and related directories +- Build and configuration files used to execute or test reference implementations + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this software except in compliance with the License. +You may obtain a copy of the License at: + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +---------------------------------------------------------------------- +C. Contact +---------------------------------------------------------------------- + +For legal inquiries or permission requests, contact: + +LiveRamp Holdings, Inc. +Attn: Legal Department – Agentic Audiences +225 Bush Street, 17th Floor +San Francisco, CA 94104 +Email: legal@liveramp.com + +====================================================================== +END OF LICENSE FILE +====================================================================== \ No newline at end of file diff --git a/data/taxonomies/agentic-audiences-draft-2026-01/spec/LICENSE-APACHE b/data/taxonomies/agentic-audiences-draft-2026-01/spec/LICENSE-APACHE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/data/taxonomies/agentic-audiences-draft-2026-01/spec/LICENSE-APACHE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/data/taxonomies/agentic-audiences-draft-2026-01/spec/UPSTREAM_README.md b/data/taxonomies/agentic-audiences-draft-2026-01/spec/UPSTREAM_README.md new file mode 100644 index 0000000..95a259f --- /dev/null +++ b/data/taxonomies/agentic-audiences-draft-2026-01/spec/UPSTREAM_README.md @@ -0,0 +1,171 @@ +# Agentic Audiences + +An Open Protocol for Intelligent Interoperability Across Advertising Agents + +> **Note:** Agentic Audiences was formerly known as the User Context Protocol (UCP). + +> **Note:** This specification represents LiveRamp's initial proposal. We have open-sourced this repository to enable the community to collaboratively define and reach collective agreement on a standard for embedding exchange in agentic advertising. + +--- + +## Overview + +Agentic Audiences is an open standard proposed by LiveRamp to enable intelligent agents in advertising and marketing to interoperate through the exchange of **signals**—identity, contextual, and reinforcement information—that represent a consumer's true real-time intent and response to advertising. + +As the industry transitions into the agentic web, where autonomous buyer, seller, and measurement agents powered by AI/ML models act on behalf of users and organizations, advertising decisions increasingly rely on these models to process billions of signals per second. Agentic Audiences defines a protocol for agents to exchange **embeddings**—compact, learned vector representations that efficiently encode identity signals (who the user is), contextual signals (what they're doing right now), and reinforcement signals (how they respond to ads) in a privacy-preserving, interoperable format. + +This repository contains: +- **Technical specifications** for embedding exchange formats and schemas +- **AI/ML model architecture guidance** ([`/docs/AI_ML Models in Agentic Digital Advertising Era.pdf`](docs/AI_ML%20Models%20in%20Agentic%20Digital%20Advertising%20Era.pdf)) explaining how 15+ model categories across the advertising lifecycle consume and produce embeddings +- **Systems and models architecture** ([`/docs/systems-and-models.md`](docs/systems-and-models.md)) — a "10K foot view" of the end-to-end system: audience model, browser/edge layer, campaign scoring service, campaign training loop, component ownership, and model interoperability +- **Reference schemas and examples** demonstrating real-world protocol usage (in-progress) + +--- + +## Motivation + +### The Challenge: Agents, Models, and Signals + +Next-gen advertising will operate through **agentic AI systems** that make millions of autonomous decisions per second. These agents will rely on **AI/ML models**—from click prediction to conversion modeling to multi-touch attribution—that process vast arrays of **signals** to understand user intent and optimize outcomes. + +**Signals** come in three critical forms: +- **Identity signals**: Who the user is (hashed identifiers, segments, behavioral history) +- **Contextual signals**: What the user is doing right now (page content, time of day, device, engagement patterns) +- **Reinforcement signals**: How users respond to advertising (impressions, clicks, conversions, engagement metrics) + +Today's advertising systems struggle to efficiently exchange these signals: +- **Text-based prompts** are too verbose and slow for real-time bidding (<100ms response time) +- **Raw feature vectors** lack semantic meaning and don't transfer across systems +- **Proprietary formats** prevent interoperability between buyer, seller, and measurement agents + +### The Solution: Embeddings as Signal Carriers + +**Embeddings** solve this problem by encoding identity, contextual, and reinforcement signals into dense, learned vector representations that: +- **Compress information**: 256-1024 dimensions vs. thousands of raw features across all signal types +- **Capture semantics**: Similar intents and behaviors have similar embeddings (vector similarity) +- **Enable transfer learning**: Models trained by one agent can be understood by others +- **Preserve privacy**: Embeddings can represent intent and response patterns without exposing raw user data +- **Support real-time inference**: Fast vector operations enable sub-100ms decisions +- **Unify signal types**: A single embedding can simultaneously encode who the user is, what they're doing, and how they've responded to past interactions + +Agentic Audiences defines how agents exchange these embeddings, transforming advertising from prompt-driven coordination to embedding-based interoperability that spans the entire decision-feedback loop. + +1. **Phase 1 – Agent Interoperability Layer** + Enable existing LLM agents to exchange structured marketing context using standardized inputs and outputs. + Focus on context engineering, schema alignment, and real-time messaging between agents such as, but not limited to, buyer, seller, and measurement agents. + +2. **Phase 2 – Context Learning Layer** + Train deep learning models on the contextual and behavioral data exchanged through the protocol. + These models learn to represent user journeys, ad impressions, conversions, and marketplace signals as dynamic embeddings. + +3. **Phase 3 – Embedding Intelligence Layer** + Agents evolve from exchanging textual context to exchanging embeddings that encode understanding of user intent, campaign state, and performance. + These embeddings act as transferable memory between agents that share a compatible vector space, enabling near real-time optimization without large prompt contexts. + +> **📄 Deep Dive: AI/ML Models in Agentic Advertising** +> The [`/docs/AI_ML Models in Agentic Digital Advertising Era.pdf`](docs/AI_ML%20Models%20in%20Agentic%20Digital%20Advertising%20Era.pdf) whitepaper provides comprehensive coverage of the 15+ model categories—from Audience Discovery and Lifetime Value Prediction to Multi-Touch Attribution and Incrementality Measurement—that power agentic advertising systems. These models both **consume** embeddings (using them as input features) and **produce** embeddings (generating vector representations of users, contexts, and creatives) that are exchanged via Agentic Audiences. Understanding this model ecosystem is essential for implementing Agentic Audiences-compatible agents. + +--- + +## Core Principles + +- **Interoperability:** Define clear input and output contracts for all agent types. +- **Context Engineering:** Maintain relevant and bounded context to keep agents aligned on goals. +- **Incremental Evolution:** Support LLM agents and prompt orchestration today while enabling learned models tomorrow. +- **Identity and Privacy:** Preserve user trust with privacy-safe handling of identity and behavioral signals. +- **Composability:** Allow independent agents to cooperate through standardized schemas and embeddings. + +--- + +## Agent Ecosystem + +Agentic Audiences sits alongside IAB Tech Lab's [**buyer agent**](https://github.com/IABTechLab/buyer-agent) and [**seller agent**](https://github.com/IABTechLab/seller-agent)—reference implementations for buyer- and seller-side automation (discovery, negotiation, booking, activation)—complementing that stack with a dedicated data plane for embeddings. + +**How Agentic Audiences fits in:** + +- **[Buyer agent](https://github.com/IABTechLab/buyer-agent)** / **[seller agent](https://github.com/IABTechLab/seller-agent)** (IAB Tech Lab) — programmatic workflows between buyers and sellers (media kits, deals, OpenDirect-style fulfillment, platform activation) +- **Agentic Audiences** — the data plane: how agents exchange embeddings that encode identity, contextual, and reinforcement signals + +Together, these layers enable a complete agentic advertising ecosystem: + +| Layer | Focus | Purpose | +|-------|-------|---------| +| **Control** | IAB Tech Lab [buyer agent](https://github.com/IABTechLab/buyer-agent) / [seller agent](https://github.com/IABTechLab/seller-agent) | Connect buyers and sellers; activate audiences, execute buys, manage inventory | +| **Data** | Agentic Audiences | Agent-to-agent embedding exchange (share learned representations of users, contexts, and outcomes) | + +**Example flow:** +1. A buyer agent (for example, the Tech Lab [buyer agent](https://github.com/IABTechLab/buyer-agent)) discovers audience signals—for example, "Find premium sports enthusiasts interested in running shoes"—via seller-facing workflows +2. The seller stack (for example, the Tech Lab [seller agent](https://github.com/IABTechLab/seller-agent)) returns candidate audiences or features +3. The buyer agent uses **Agentic Audiences** to exchange contextual and identity embeddings with a seller agent +4. The seller agent uses embeddings to match inventory in real-time via vector similarity +5. Reinforcement signals (impressions, conversions) flow back through **Agentic Audiences** to update models +6. The measurement agent reports results through the buyer/seller automation path and uses **Agentic Audiences** to share learned embeddings + +Together with the IAB Tech Lab buyer and seller agent ecosystem and broader platform integrations, Agentic Audiences supports the transition from prompt-based advertising automation to embedding-based intelligence to drive efficiencies by eliminating the need for massive copies of user-level datasets across the ecosystem. + +--- + +## Technical Vision + +Agentic Audiences defines: + +1. **Protocol Interfaces** - APIs and schemas for exchanging context, signals, and results. +2. **Context Management** - Strategies for maintaining scoped, composable context windows in LLM-driven agents. +3. **Embedding Interoperability** - Standards for shared embedding structures, dimensional alignment, and vector-space identity. +4. **Agent Coordination Flows** - Request and response patterns for cross-agent actions. +5. **Privacy and Consent Controls** - Mechanisms for secure signal sharing, security and authentication, permissible uses, and time-to-live (TTL) of consented data. +6. **Agentic Attestation** - Ensures confidentiality and integrity of code and information accessed or executed through agents, including provenance and controlled execution environments. +7. **Token Exchange and Settlement** - Enables agents to exchange tokens or perform value transfers for advertising events, supporting integration with emerging payment and attribution protocols such as AP2 and X402. + +By evolving from structured text exchanges to compact vector exchanges, Agentic Audiences will enable major gains in speed, scale, and cost efficiency for campaign optimization. + +> **🏗 System Architecture Deep Dive** +> [`/docs/systems-and-models.md`](docs/systems-and-models.md) provides a 10K foot view of the end-to-end Agentic Audiences architecture — covering the audience model, browser/edge embedding storage (ATS.js + Prebid), campaign scoring service (Docker sidecar), campaign training loop, and component ownership. It also addresses model interoperability and answers common architectural questions. + +--- + +## Example Evolution Path + +1. **Today:** + - A buyer agent prompts a seller agent: + "Provide available CTV inventory for users interested in electric vehicles in San Francisco this week." + - The seller agent responds using the Agentic Audiences schema, returning JSON data on available segments. + - A measurement agent records conversions and feeds updates. + +2. **Future:** + - The buyer agent receives a user embedding representing current context. + - It queries seller embeddings directly in vector space to find optimal matches. + - Feedback embeddings from the measurement agent continuously refine the shared context model. + +--- + +## Roadmap + +1. **Specification Draft** - Schema and interfaces for prompt-driven interoperability. +2. **Reference SDKs** - Python and JavaScript libraries for MCP-compatible agents. +3. **Context Engine Framework** - Tools for managing context window updates and relevance. +4. **Embedding Schema Standard** - Common representation for learned user and campaign embeddings. +5. **Industry Working Group** - Partnership with open-source and adtech leaders to align adoption. + +--- + +## Contributing + +This repository hosts the evolving Agentic Audiences specification and reference implementations. +We welcome contributions from engineers, researchers, and organizations shaping the next generation of agentic advertising. + +To get involved: +- Read [`/docs/systems-and-models.md`](docs/systems-and-models.md) for a 10K foot view of the system architecture +- Read [`/docs/AI_ML Models in Agentic Digital Advertising Era.pdf`](docs/AI_ML%20Models%20in%20Agentic%20Digital%20Advertising%20Era.pdf) to understand the model ecosystem that Agentic Audiences enables +- Fork the repo and explore the `/specs` directory for technical specifications +- Propose changes via pull request +- Join or start a working group under `/community` + +--- + +## License + +- Specification and Documentation: Creative Commons Attribution 4.0 International (CC BY 4.0) +- Reference Implementations: Apache License 2.0 + +--- \ No newline at end of file diff --git a/data/taxonomies/agentic-audiences-draft-2026-01/spec/docs/systems-and-models.md b/data/taxonomies/agentic-audiences-draft-2026-01/spec/docs/systems-and-models.md new file mode 100644 index 0000000..dc919be --- /dev/null +++ b/data/taxonomies/agentic-audiences-draft-2026-01/spec/docs/systems-and-models.md @@ -0,0 +1,108 @@ +# Agentic Audiences Systems and Models + +--- + +## Overview + +This document describes the systems and models architecture for Agentic Audiences, presented as a "10K Foot View." + +Today's programmatic signals are sparse -- segment IDs, key-value pairs, and coarse demographic buckets. These representations are a poor fit for deep learning models that thrive on dense, high-dimensional inputs. Agentic Audiences addresses this by embedding richer information -- contextual, behavioral, and identity signals -- into the bid stream as vectors. The result is a shift from "is this user in segment X?" to "how appropriate is this campaign for this context right now?", without adding latency to the RTB critical path. + +This is an open, collaborative initiative developed with IAB involvement toward global standards. It is not a walled-garden approach. The protocol, embedding taxonomy, and a reference scoring implementation are provided openly so that any participant in the ecosystem can build toward the same specification. + +The diagram below depicts a header bidding flow for vectorized payloads, building on top of ATS and Prebid. This is not meant to be the only transport mechanism, but rather to demonstrate one such embodiment. + +--- + +## System Architecture + +The architecture is organized into the following layers: + +### Audience Model + +The audience model must produce quality embeddings from various types of inputs, including but not limited to; contextual data, identity data, behavioral data. Inputs are generated directly from a tag on browser or from the publisher's server or CDN. These embeddings may be produced from one or more models, often relying on specialized model providers to produce high quality embeddings. Quality embeddings from contextual data can be produced from open source models such as `MiniLM` or `bge-small`. This model, or set of models, should evolve towards a domain-specific semantic understanding of audience data, i.e. it will have a deep understanding of sequence of events leading to clicks/conversions. Embedding generation from inputs should be asynchronous (i.e. not in line with an RTB request). + +LiveRamp does not aim to build all models internally. The architecture supports a competitive marketplace where external model builders produce embeddings compatible with the protocol and compete on quality -- analogous to how Ramp IDs create a network of identity providers. An embedding taxonomy classifies models by the class of data they were trained on (contextual, behavioral, identity, etc.), which determines compatibility and whether alignment between models is feasible. + +### Browser / Edge + +The browser interacts with a publisher-side tag (such as LiveRamp's ATS.js) that manages embedding storage in a first-party cookie. The specification is open -- ATS.js is LiveRamp's implementation, but other parties can build their own implementations toward the same protocol. + +The tag does not crawl or scrape pages. Publishers provide title, keywords, and other page-level signals through standard locations (meta tags, data layers) or server-side. This keeps integration lightweight for publishers. + +Embeddings from the audience model are ideally stored in the first-party cookie (increasing privacy as well as decreasing latencies during embedding retrieval), but could reside server-side as well. Prebid.js will construct a BidRequest object, placing the embedding in an ORTB2 Segment ext object. IAB is adding an extension to this segment object to carry the embedding vector and its metadata (model, dimension, type) in a standardized format. + +[Link to ORTB2 Segment ext schema] + +Storing embeddings in first-party cookies provides a privacy advantage over server-side approaches. Reduced representations of embeddings can be transmitted back as feedback signals, providing an adjustable dial to meet any regulatory surface. + +### Campaign Scoring Service + +The responsibility of the campaign scoring service is to perform vector distance calculations between a user's embedding and a list of vectors representing the campaign. This container replaces the question of "is this user being targeted in any active campaigns?" with "how appropriate is it to show this campaign in this context right now?". Note that returning a valid BidResponse, controlling bidding logic such as budget pacing or arbitrage is outside the scope of this container. + +A reference implementation suitable for deployment in execution platforms is included in this repository at [`src/user-embedding-to-campaign-scoring/`](../src/user-embedding-to-campaign-scoring/). See its [README](../src/user-embedding-to-campaign-scoring/README.md) for API details and deployment instructions. The service is provided as a Docker image designed to run as a sidecar on DSP infrastructure. Its API surface covers three operations: registering campaign head weights, scoring user embeddings against those heads (cosine, dot product, or L2 similarity), and retrieving scoring analytics. + +The method of scoring, tagging and validation of compatible models, and normalization functions are registered along with the weights representing the campaign. Model configuration travels with the head registration, allowing different campaigns to use different embedding models without redeployment. + +Note on latencies -- a single GPU can support many thousands of campaigns with sub-millisecond latency. + +Downstream of the bid, standard RTB flows work as usual, making this compatible with any ad server that supports OpenRTB. Downstream use of user embeddings is not part of the current scope of this project, but it is not a large leap to see how they could be applied in bidding logic or creative rendering. + +### Campaign Training + +The responsibility of this component is to produce a vector of weights (or vector set of weights) representing the campaign. This is accomplished by training with data that the advertiser has access to, such as CRM and CAPI, to target embeddings that active in the bid stream data. Targeting the embedding space on the supply-side requires two things; an audience model to generate embeddings over the advertisers known universe and reinforcement signals being reported back from actual events (e.g. impression or click beacons, campaign scoring responses). + +The audience model on the supply side and the demand side is ideally the same model, however this training step can additionally serve as an alignment step for cross-model compatibility, using techniques facilitated by infrastructure providers such as LiveRamp. This allows the advertisers model to learn the geometry and optimal set of weights to target for a given campaign goal. + +The information transmitted back to a signal aggregator can be a **reduced representation** of the embedding that itself can be used as a feature in campaign training. The genericity of using embeddings allows us to reduce either contextual, event series, or identity signals with the same mechanism, giving us an adjustable dial to meet any regulatory surface. + +**Training loop:** (Marketplace, CRM, CAPI) -> Campaign Model Training -> Campaign Head -> Campaign Scoring -> Signal Aggregator -> Loop + +--- + +## Component Ownership + +A common question is "who runs what?" The table below clarifies operational responsibility for each component. + +| Component | Operated By | Notes | +|---|---|---| +| Tag (e.g. ATS.js) | Publisher (LiveRamp provides ATS.js; others can build to spec) | Manages 1P cookie and embedding storage | +| Inference Server | LiveRamp / Publisher | Generates embeddings from page content and brand data | +| Campaign Scoring Function | DSP | Provided as a Docker image; runs as a sidecar on DSP infrastructure | +| Campaign Training | Advertiser / Clean Room | Produces campaign head weights from CRM, CAPI, marketplace data | +| Signal Aggregator | LiveRamp | Collects reduced representations and event signals for the training loop | +| Prebid.js + OpenRTB Transport | Publisher / SSP | Standard header bidding flow; embedding travels in ORTB2 segment ext | + +--- + +## Model Interoperability + +Different models produce incomparable vector spaces. Two contextual models both producing 384-dimensional vectors are not inherently comparable -- the geometry of the learned space differs between models. This is a first-class architectural concern. + +The protocol addresses this in two ways. First, model metadata and an embedding taxonomy travel with every vector. The scoring service partitions heads by `model:embedding_type` and only scores against matching embeddings. The `embedding_space_id` and `compatible_with` fields in the campaign head schema (see the [scoring service README](../src/user-embedding-to-campaign-scoring/README.md)) make compatibility explicit at the protocol level. + +Second, alignment techniques can map one embedding space onto another when the underlying models were trained on the same class of data. Rotation is one such technique, but alignment can employ a range of methods -- some proprietary -- facilitated by infrastructure providers such as LiveRamp. The key insight is that models trained on the same class of data learn similar structure, making alignment feasible. The embedding taxonomy (contextual, identity, behavioral, reinforcement, CAPI, intent) is what determines whether alignment is possible and meaningful. + +--- + +## FAQ + +### Isn't this just contextual advertising? + +It is not, for two reasons. There is a deeper semantic understanding that is being taken advantage of provided by the underlying LLM or GNN. There is an inherent first party identity being used as well through the updates to an embedding already living in the 1P cookie. So the embedding doesn't just represent the current context, but actually the user's journey across this publisher's space. + +### What about third-party Identity? + +This prototype will limit itself to campaign level reporting and optimization, but Identity itself can also be included in the embedding through an experimental Identity model built by LiveRamp. This gives the system flexibility into the level of precision on which to optimize; from global campaign level all the way down to person level precision, all with the turn of a dial. + +### Embeddings aren't interoperable between models, so how does this work? + +Embedding providers are analogous to Identity providers in Agentic Audiences. There exist a handful of Identity providers in the industry, and IDs are not interoperable without setting up complicated ID syncs replete with conflicts. Embeddings are generally not interoperable out of the box (although in some cases they can be aligned with minimal effort). Embeddings have the advantage of being learnable by proprietary models, allowing data controllers with privileged data access to take full and unique advantage of their assets. + +### Why is the scoring function open source? + +To lower adoption barriers for DSPs. The scoring function must run at DSP scale and latency -- DSPs need to audit, customize, and trust the code running on their infrastructure. Open-sourcing the reference implementation aligns with the IAB standards work and signals that this is a collaborative, ecosystem-wide effort rather than a proprietary lock-in. + +### What data does a publisher need to provide? + +At minimum, title and keywords. Publishers provide this through standard locations (meta tags, data layers) or server-side. The tag does not crawl or scrape pages -- the integration is lightweight by design. Additional signals such as content categories or behavioral events improve embedding quality but are not required. diff --git a/data/taxonomies/agentic-audiences-draft-2026-01/spec/roadmap.md b/data/taxonomies/agentic-audiences-draft-2026-01/spec/roadmap.md new file mode 100644 index 0000000..e69de29 diff --git a/data/taxonomies/agentic-audiences-draft-2026-01/spec/v1.0/embedding-exchange.md b/data/taxonomies/agentic-audiences-draft-2026-01/spec/v1.0/embedding-exchange.md new file mode 100644 index 0000000..25baf7a --- /dev/null +++ b/data/taxonomies/agentic-audiences-draft-2026-01/spec/v1.0/embedding-exchange.md @@ -0,0 +1,138 @@ +# Agentic Audiences Contextual Embedding Exchange Specification (Draft v0.1) + +Status: Draft +Scope: Defines a vendor-neutral wire format for exchanging contextual embeddings between agents in the Agentic Audiences ecosystem. +Primary transport: HTTPS JSON (optionally NDJSON for streaming). Binary variants MAY use CBOR with identical field names. + +--- + +## 1. Design Goals + +- Interoperability: any agent that understands the same embedding space can consume vectors safely. +- Interpretable metadata: receivers can determine dimensionality, metric, normalization, tokenizer, pooling, projection, quantization, training domain, and license. +- Privacy and consent: explicit fields for consent, TTL, purposes, and permissible uses. +- Security: integrity, authentication, and optional attestation. +- Versioning: clear envelope and schema version semantics. + +--- + +## 2. Content Type and Versioning + +- HTTP Content-Type: `application/vnd.ucp.embedding+json; v=1` +- Top-level `spec_version`: semantic version string of this spec (e.g., `"1.0.0"`). +- Backwards-compatible additions use new optional fields. Breaking changes bump major version. + +--- + +## 3. Published Schemas + +- schema/embedding_format.schema.json + +--- + +## 4. Embedding Object + +Required fields: +- id string, unique per embedding within message. +- type one of context, creative, user_intent, inventory, query. +- Either vector (array of numbers) or quantized_b64 (base64). +- dimension integer equals model.dimension. +- dtype one of float32, float16, int8, uint8. + +Optional fields: +- scale number used for dequantization. +- compressed boolean. +- hash content hash of raw vector bytes after normalization. +- origin to explain how it was produced. +- usage_hints metric, thresholds, target agents. + +--- + +## 5. Model Descriptor + +Required: +- id, version, dimension, metric (cosine|dot|l2), type (encoder|llm|slm), embedding_space_id. + +Recommended: +- tokenizer name/version and canonical vocab id. +- pooling (mean|max|cls|weighted). +- normalization (none|l2_unit). +- projection info if PCA, whitening, or learned projection applied. +- quantization scheme if any. +- training_domain tags describing primary data regimes. +- licensing license id and URL. +- compatibility compatible_spaces and guidance. + +Rationale: receivers must know how to compare vectors and whether mixing spaces will degrade quality. + +--- + +## 6. Context Descriptor +- url, page_title, keywords (ordered, deduped), language BCP-47. +- content_hash hash of the processed text or DOM excerpt used to create the embedding. +- placement and device are optional but useful for reproducibility and analysis. +- geography is coarse-level only and MUST honor consent and policy. + +--- + +## 7. Consent, Purpose, and TTL +- consent.framework and consent_string reference the governing framework (e.g., IAB TCF, US state signals). +- permissible_uses enumerates allowed downstream uses. If absent, default is "activation_scoring" only. +- ttl_seconds defines the retention and re-use horizon for the embedding and associated metadata. + +Receivers MUST enforce TTL and purposes before storing or training. + +--- + +## 8. Security and Attestation +- Transport: HTTPS with mTLS is RECOMMENDED for inter-agent links. +- Integrity: sign the envelope; include key_id resolvable via JWKS or equivalent. +- Attestation: optional policy hash or enclave report so consumers can trust how vectors were produced, especially for on-device SLM execution. + +--- + +## 9. Error Handling +- 400: schema or consent invalid. +- 409: incompatible embedding_space_id or dimension. +- 422: metric or normalization mismatch. +- 429: throttling. +- 5xx: transient server errors. + +Responses SHOULD include message_id, status, and errors[] with code, field, detail. + +--- + +## 10. Streaming (NDJSON) + +When sending high-volume events: + +Content-Type: application/x-ndjson; charset=utf-8 + +Each line is a complete envelope. Receivers MAY accept batched arrays over HTTP/2 as an alternative. + +--- + +## 11. Security and Operational Guidance +- Authenticate producers and consumers via mTLS or OAuth 2.0 with private JWKS per tenant. +- Sign envelopes; reject messages with unknown key_id or invalid signature. +- Enforce consent.purposes and ttl_seconds at ingestion and storage layers. +- Validate embedding_space_id, dimension, and metric before indexing. +- Log message_id, context.context_id, and hash for auditability. + +--- + +## 12. Interoperability Rules +- Two vectors are comparable if and only if: +- embedding_space_id matches, or a known converter maps between spaces, and +- dimension, metric, and normalization are compatible, and +- any projection or quantization differences are accounted for. +- Receivers SHOULD normalize to the model’s declared normalization before scoring. + +--- + +## 13. IANA-like Registry Stubs (to be formalized) +- Embedding Space IDs: ucp://spaces/{domain}/{lang}-v{n} +- Metrics: cosine, dot, l2 +- Pooling: mean, max, cls, weighted +- Quantization: none, int8, uint8, pq, sq + diff --git a/data/taxonomies/agentic-audiences-draft-2026-01/spec/v1.0/embedding-taxonomy.md b/data/taxonomies/agentic-audiences-draft-2026-01/spec/v1.0/embedding-taxonomy.md new file mode 100644 index 0000000..a5a464e --- /dev/null +++ b/data/taxonomies/agentic-audiences-draft-2026-01/spec/v1.0/embedding-taxonomy.md @@ -0,0 +1,444 @@ +# Agentic Audiences Embedding Taxonomy + +**Version:** 0.1 (Draft) +**Status:** Proposal + +--- + +## Overview + +This document defines a taxonomy for classifying embeddings exchanged via Agentic Audiences. Embeddings encode different types of signals—identity, contextual, and reinforcement—and understanding their semantic purpose is critical for proper interpretation, combination, and usage by agents. + +The taxonomy categorizes embeddings along three dimensions: +1. **Signal Type** - What kind of information the embedding encodes +2. **Temporal Scope** - Time horizon the embedding represents +3. **Composition** - Whether it encodes a single signal type or multiple types + +--- + +## 1. Signal Type Classification + +### 1.1 Identity Embeddings + +**Purpose:** Represent a user's persistent identity, enabling cross-session recognition and historical behavior understanding. + +**Subcategories:** + +#### 1.1.1 PII-Derived Identity Embeddings +- **Description:** Learned representations of offline identity derived from personally identifiable information (PII) +- **Source Data:** Email addresses, phone numbers, postal addresses, device IDs (hashed/encrypted) +- **Model Type:** Transformer models trained on tokenized, anonymized PII +- **Use Cases:** Cross-device identity resolution, household linkage, deterministic matching +- **Privacy:** Must preserve k-anonymity; embeddings should not be reversible to raw PII +- **Example:** A text-based PII identifier transformer that encodes hashed email → 512-dim vector + +#### 1.1.2 Behavioral Identity Embeddings +- **Description:** Representations of a user based on long-term behavioral patterns +- **Source Data:** Purchase history, browsing patterns, app usage, content consumption over weeks/months +- **Model Type:** Recurrent networks (LSTM/GRU), transformers with temporal attention +- **Use Cases:** Lookalike modeling, lifetime value prediction, segment discovery +- **Temporal Scope:** Weeks to months of historical data +- **Example:** User journey embedding capturing 90-day behavioral fingerprint + +#### 1.1.3 Demographic Identity Embeddings +- **Description:** Representations of inferred or declared demographic attributes +- **Source Data:** Age, gender, income bracket, education, location, interests +- **Model Type:** Categorical embeddings, entity embeddings from tabular data +- **Use Cases:** Demographic targeting, audience extension, census-level aggregation +- **Example:** Combined demographic vector encoding {age_bucket, region, interest_category} + +#### 1.1.4 Graph-Based Identity Embeddings +- **Description:** Representations derived from identity graphs (device graphs, household graphs, social graphs) +- **Source Data:** Device co-occurrence, shared network connections, household relationships +- **Model Type:** Graph neural networks (GNN), node2vec, DeepWalk +- **Use Cases:** Probabilistic identity linking, fraud detection, household targeting +- **Example:** Node embedding in a device graph representing probabilistic linkage to 5 devices + +### 1.2 Contextual Embeddings + +**Purpose:** Represent the current situational context in which a user is operating, enabling real-time intent understanding. + +**Subcategories:** + +#### 1.2.1 Content Contextual Embeddings +- **Description:** Semantic representations of page/app content where ads may appear +- **Source Data:** Page text, article body, video metadata, app category +- **Model Type:** Sentence transformers (SBERT), BERT variants, multimodal models +- **Use Cases:** Contextual targeting, brand safety, semantic matching +- **Temporal Scope:** Instantaneous (current page view) +- **Example:** Page embedding from article about "electric vehicle financing" → 768-dim BERT embedding + +#### 1.2.2 Temporal Contextual Embeddings +- **Description:** Representations of time-based context (time of day, day of week, seasonality) +- **Source Data:** Timestamp, timezone, calendar features (weekday/weekend, holiday) +- **Model Type:** Sinusoidal encodings, learned temporal embeddings +- **Use Cases:** Dayparting optimization, seasonal campaign tuning +- **Example:** Time embedding encoding {Monday, 9am PST, non-holiday} → 64-dim vector + +#### 1.2.3 Geospatial Contextual Embeddings +- **Description:** Representations of location-based context +- **Source Data:** Country, region, DMA, postal code, lat/long (coarse), POI proximity +- **Model Type:** Geohashing embeddings, hierarchical location embeddings +- **Use Cases:** Local targeting, geo-fencing, regional campaign optimization +- **Privacy:** Must use coarse granularity (ZIP/postal code level, not GPS coordinates) +- **Example:** Location embedding for "San Francisco Bay Area, CA" → 128-dim vector + +#### 1.2.4 Device/Environment Contextual Embeddings +- **Description:** Representations of device and browsing environment +- **Source Data:** Device type, OS, browser, screen size, connection type, app vs web +- **Model Type:** Categorical embeddings, device fingerprint encoders +- **Use Cases:** Creative optimization (mobile vs desktop), format selection +- **Example:** Device embedding for {iOS, Safari, mobile, 5G} → 32-dim vector + +#### 1.2.5 Session Contextual Embeddings +- **Description:** Representations of current browsing session behavior +- **Source Data:** Pages visited in session, dwell time, scroll depth, engagement signals +- **Model Type:** Session RNNs, attention-based sequence models +- **Use Cases:** In-session intent prediction, urgency detection +- **Temporal Scope:** Minutes to hours (current session) +- **Example:** Session embedding capturing "researching → comparing → near-purchase" journey state + +### 1.3 Reinforcement Embeddings + +**Purpose:** Represent feedback signals from user interactions with advertising, enabling model updates and campaign optimization. + +**Subcategories:** + +#### 1.3.1 Engagement Reinforcement Embeddings +- **Description:** Representations of ad engagement behaviors short of conversion +- **Source Data:** Impressions, viewability, clicks, video completions, hover time, scroll-through +- **Model Type:** Event sequence encoders, survival models, interaction transformers +- **Use Cases:** Click prediction, viewability optimization, engagement modeling +- **Temporal Scope:** Seconds to hours post-exposure +- **Example:** Engagement embedding encoding {5 impressions, 2 clicks, 30s avg dwell} → 256-dim vector + +#### 1.3.2 Conversion Reinforcement Embeddings +- **Description:** Representations of conversion events and their context +- **Source Data:** Purchase, sign-up, download, form submission, attributed conversions +- **Model Type:** Conversion path encoders, attribution models +- **Use Cases:** Conversion rate prediction, incrementality measurement, ROAS optimization +- **Temporal Scope:** Hours to days post-exposure +- **Example:** Conversion embedding capturing {purchase, $150 AOV, 48hr lag, 3-touch path} → 512-dim vector + +#### 1.3.3 Attribution Reinforcement Embeddings +- **Description:** Representations of multi-touch attribution weights across touchpoints +- **Source Data:** Full conversion path, touchpoint timestamps, channel mix, credited value +- **Model Type:** Markov chain models, Shapley value calculators, path transformers +- **Use Cases:** Budget allocation, channel optimization, incrementality testing +- **Example:** Attribution embedding encoding contribution weights across {display, social, search} path + +#### 1.3.4 Feedback Reinforcement Embeddings +- **Description:** Representations of negative signals or policy violations +- **Source Data:** Ad fatigue indicators, frequency cap violations, user complaints, brand safety violations +- **Model Type:** Anomaly detection models, policy classifiers +- **Use Cases:** Frequency optimization, ad quality improvement, brand safety enforcement +- **Example:** Feedback embedding flagging {over-frequency, user opted out, creative underperforming} + +### 1.4 Creative Embeddings + +**Purpose:** Represent advertising creative assets, enabling semantic matching and creative optimization. + +**Subcategories:** + +#### 1.4.1 Visual Creative Embeddings +- **Description:** Representations of image/video creative elements +- **Source Data:** Image pixels, video frames, visual elements (objects, colors, composition) +- **Model Type:** CNN encoders (ResNet, EfficientNet), vision transformers (ViT), CLIP +- **Use Cases:** Visual similarity matching, A/B testing, dynamic creative optimization +- **Example:** Image embedding from display ad creative → 2048-dim ResNet embedding + +#### 1.4.2 Textual Creative Embeddings +- **Description:** Representations of ad copy, headlines, CTAs +- **Source Data:** Ad text, headlines, descriptions, call-to-action phrases +- **Model Type:** Sentence transformers, advertising-specific language models +- **Use Cases:** Copy testing, message-market fit, semantic creative matching +- **Example:** Text embedding from headline "Save 30% on Electric Vehicles" → 384-dim SBERT vector + +#### 1.4.3 Multimodal Creative Embeddings +- **Description:** Joint representations of visual + textual + audio creative elements +- **Source Data:** Combined image, text, audio from video ads or rich media +- **Model Type:** CLIP-style models, multimodal transformers, unified embedding spaces +- **Use Cases:** Holistic creative understanding, cross-modal retrieval, dynamic assembly +- **Example:** Multimodal embedding from 30s video ad with voiceover → 1024-dim joint vector + +#### 1.4.4 Creative Performance Embeddings +- **Description:** Representations combining creative features with performance history +- **Source Data:** Creative attributes + historical CTR/CVR/engagement metrics +- **Model Type:** Performance-aware encoders, metric-conditioned embeddings +- **Use Cases:** Creative ranking, performance prediction, winner prediction +- **Example:** Creative+performance embedding: {image_vector, historical_CTR=2.3%} → 768-dim vector + +### 1.5 Inventory Embeddings + +**Purpose:** Represent available advertising inventory, enabling supply-demand matching. + +**Subcategories:** + +#### 1.5.1 Publisher Inventory Embeddings +- **Description:** Representations of publisher properties and their characteristics +- **Source Data:** Domain, content category, audience reach, brand safety score, viewability rates +- **Model Type:** Publisher encoders, domain embedding models +- **Use Cases:** Publisher selection, PMPs, inventory quality scoring +- **Example:** Publisher embedding for "premium news site, politics category" → 256-dim vector + +#### 1.5.2 Placement Inventory Embeddings +- **Description:** Representations of specific ad placements/units +- **Source Data:** Format (banner/video/native), size, position (above/below fold), context +- **Model Type:** Placement feature encoders +- **Use Cases:** Format selection, placement optimization, yield management +- **Example:** Placement embedding for "300×250 banner, above-fold, homepage" → 128-dim vector + +#### 1.5.3 Audience Inventory Embeddings +- **Description:** Representations of targetable audience segments available in inventory +- **Source Data:** Segment definitions, reach, overlap, refresh rates, data source +- **Model Type:** Segment taxonomy embeddings, audience characteristic encoders +- **Use Cases:** Audience discovery, segment recommendation, overlap analysis +- **Example:** Segment embedding for "in-market auto shoppers, 2M reach" → 512-dim vector + +### 1.6 Query/Intent Embeddings + +**Purpose:** Represent user intent signals or agent queries for matching against inventory or audiences. + +**Subcategories:** + +#### 1.6.1 Search Query Embeddings +- **Description:** Representations of search queries indicating commercial intent +- **Source Data:** Search terms, query refinements, search session context +- **Model Type:** Query encoders, BERT-based search models +- **Use Cases:** Search retargeting, intent capture, keyword expansion +- **Example:** Query embedding for "best electric SUV 2025" → 768-dim vector + +#### 1.6.2 Buyer Intent Embeddings +- **Description:** Representations of what a buyer agent is seeking +- **Source Data:** Campaign goals, target audience description, creative requirements, budget constraints +- **Model Type:** Intent specification encoders, goal-aware transformers +- **Use Cases:** Inventory matching, seller discovery, programmatic negotiation +- **Example:** Buyer intent: "reach tech-savvy millennials interested in sustainable products" → 512-dim vector + +#### 1.6.3 Seller Offer Embeddings +- **Description:** Representations of what a seller agent is offering +- **Source Data:** Available inventory characteristics, pricing, audience profiles, context +- **Model Type:** Offer specification encoders +- **Use Cases:** Buyer-seller matching, marketplace efficiency, price discovery +- **Example:** Seller offer: "CTV inventory, sports content, 18-34 males, $15 CPM" → 512-dim vector + +--- + +## 2. Temporal Scope Classification + +Embeddings can be classified by the time horizon they represent: + +### 2.1 Persistent Embeddings +- **Time Horizon:** Weeks to months +- **Update Frequency:** Weekly to monthly +- **Examples:** PII-derived identity, behavioral identity, LTV predictions +- **Characteristics:** Stable, long-term representations + +### 2.2 Session Embeddings +- **Time Horizon:** Minutes to hours +- **Update Frequency:** Per session or hourly +- **Examples:** Session context, current intent, in-session behavior +- **Characteristics:** Medium-term, updated within browsing sessions + +### 2.3 Real-Time Embeddings +- **Time Horizon:** Seconds to minutes +- **Update Frequency:** Per event or continuously +- **Examples:** Current page context, immediate device context, ad request context +- **Characteristics:** Instantaneous, reflects current moment + +### 2.4 Retrospective Embeddings +- **Time Horizon:** Historical (post-event analysis) +- **Update Frequency:** Batch updates after campaigns complete +- **Examples:** Attribution embeddings, incrementality measurements, campaign performance +- **Characteristics:** Backward-looking, enable learning for future campaigns + +--- + +## 3. Composition Classification + +Embeddings can combine multiple signal types: + +### 3.1 Atomic Embeddings +- **Definition:** Encode a single signal type from a single source +- **Examples:** + - Pure content embedding (just the page text) + - Pure PII embedding (just the hashed email) + - Pure device embedding (just device characteristics) +- **Use Cases:** Building blocks for more complex representations, interpretability, debugging + +### 3.2 Composite Embeddings +- **Definition:** Combine multiple related signals of the same type +- **Examples:** + - User identity = PII embedding + behavioral embedding + demographic embedding + - Full context = content + temporal + device + geo embeddings +- **Method:** Concatenation, weighted averaging, learned fusion +- **Use Cases:** Richer representations, holistic understanding within a signal type + +### 3.3 Graph Embeddings +- **Definition:** Encode relational structures between entities +- **Examples:** + - Device graph embeddings (device-device relationships) + - User journey graph (page-to-page navigation) + - Conversion path graph (touchpoint sequences) + - Creative similarity graph (creative-creative relationships) +- **Model Type:** Graph Neural Networks (GNN), node2vec, GraphSAGE +- **Use Cases:** Relationship discovery, transitive inference, network effects + +### 3.4 Cross-Signal Fusion Embeddings +- **Definition:** Combine multiple signal types (identity + context + reinforcement) +- **Examples:** + - User-in-context: identity embedding + current contextual embedding + - Predictive fusion: identity + context → predicted engagement + - Feedback-informed identity: baseline identity + historical reinforcement signals +- **Method:** Multimodal fusion, cross-attention, gating mechanisms +- **Use Cases:** Comprehensive user understanding, real-time scoring, personalized predictions + +### 3.5 Hierarchical Embeddings +- **Definition:** Multi-level representations with coarse-to-fine granularity +- **Examples:** + - Geographic hierarchy: country → state → DMA → postal code + - Taxonomic hierarchy: IAB category L1 → L2 → L3 + - Temporal hierarchy: year → month → week → day → hour +- **Model Type:** Hierarchical encoders, tree-structured embeddings +- **Use Cases:** Multi-resolution targeting, privacy-aware aggregation, drill-down analysis + +--- + +## 4. Embedding Metadata Schema + +To properly interpret and use embeddings, agents must exchange metadata. The following fields should accompany embeddings: + +### Required Metadata +```json +{ + "embedding_id": "unique-id", + "taxonomy_class": { + "signal_type": "identity|contextual|reinforcement|creative|inventory|query", + "subtype": "pii_derived|behavioral|content|engagement|...", + "temporal_scope": "persistent|session|realtime|retrospective", + "composition": "atomic|composite|graph|fusion|hierarchical" + }, + "dimension": 512, + "model": { + "id": "model-identifier", + "version": "1.0.0", + "architecture": "transformer|cnn|gnn|...", + "embedding_space_id": "ucp://spaces/identity/pii-v1" + }, + "vector": [0.01, 0.02, ...], + "normalization": "l2_unit|none", + "metric": "cosine|dot|l2" +} +``` + +### Optional Metadata +```json +{ + "source_signals": ["hashed_email", "behavioral_history"], + "temporal_window": { + "start": "2025-01-01T00:00:00Z", + "end": "2025-01-24T00:00:00Z", + "scope": "90_days" + }, + "privacy": { + "k_anonymity": 100, + "differential_privacy": false, + "reversibility_risk": "low" + }, + "quality_metrics": { + "confidence": 0.95, + "coverage": 0.87, + "staleness_hours": 2 + }, + "interpretability": { + "top_features": ["feature1", "feature2"], + "attribution_method": "integrated_gradients" + } +} +``` + +--- + +## 5. Usage Guidelines + +### 5.1 Embedding Selection + +**For Buyer Agents:** +- Use **identity embeddings** (PII + behavioral) to understand who to target +- Use **contextual embeddings** (content + temporal + geo) to find when/where to show ads +- Use **creative embeddings** to select appropriate messaging +- Use **query/intent embeddings** to express what you're looking for + +**For Seller Agents:** +- Use **inventory embeddings** to represent what you're offering +- Use **contextual embeddings** to describe placement environment +- Use **audience embeddings** to communicate available segments + +**For Measurement Agents:** +- Use **reinforcement embeddings** (engagement + conversion) to provide feedback +- Use **attribution embeddings** to credit touchpoints +- Use **feedback embeddings** to flag quality issues + +### 5.2 Embedding Combination + +When combining embeddings from different classes: + +1. **Ensure compatible embedding spaces** - Check `embedding_space_id` and model compatibility +2. **Normalize before fusion** - Use consistent normalization (typically L2) +3. **Weight appropriately** - Identity may deserve higher weight than device context +4. **Consider temporal freshness** - Don't mix stale persistent embeddings with real-time context +5. **Preserve privacy** - Fusion should not reduce k-anonymity below thresholds + +### 5.3 Interoperability + +For cross-agent embedding exchange: + +- **Shared embedding spaces** - Agents using the same `embedding_space_id` can directly compare embeddings +- **Transfer learning** - Embeddings from compatible spaces can be projected into common space +- **Metadata transparency** - Always include taxonomy classification in metadata +- **Version compatibility** - Specify model version; newer versions should maintain backward compatibility when possible + +--- + +## 6. Future Extensions + +This taxonomy is a living document. Anticipated future additions: + +### 6.1 Additional Signal Types +- **Attention embeddings** - Representations of visual attention patterns (eye-tracking derived) +- **Emotional embeddings** - Affective responses to creative (sentiment, emotional arousal) +- **Trust embeddings** - Brand safety, verification, fraud risk signals +- **Privacy embeddings** - Consent state, privacy preferences, regulatory compliance signals + +### 6.2 Advanced Compositions +- **Causal embeddings** - Encode causal relationships (not just correlations) +- **Counterfactual embeddings** - "What would have happened without the ad?" +- **Ensemble embeddings** - Weighted combinations of multiple models' embeddings +- **Meta-embeddings** - Embeddings of embeddings (second-order representations) + +### 6.3 Dynamic Embeddings +- **Streaming embeddings** - Continuously updated via online learning +- **Adaptive embeddings** - Self-adjusting based on prediction accuracy +- **Context-conditional embeddings** - Same user/content but different embeddings based on query context + +--- + +## 7. References + +- Agentic Audiences Embedding Format Specification (`embedding_format.schema.json`) +- AI/ML Models in Agentic Digital Advertising Era (whitepaper) + +--- + +## 8. Change Log + +- **v0.1 (2025-10-24)**: Initial draft taxonomy proposal + - Defined 6 primary signal types with subcategories + - Established temporal scope and composition classifications + - Added metadata schema and usage guidelines + +--- + +**Maintainers:** LiveRamp Agentic Audiences Working Group +**Feedback:** Submit issues or PRs to the Agentic Audiences repository +**License:** Creative Commons Attribution 4.0 International (CC BY 4.0) diff --git a/data/taxonomies/agentic-audiences-draft-2026-01/spec/v1.0/examples/buyer_agent_request.json b/data/taxonomies/agentic-audiences-draft-2026-01/spec/v1.0/examples/buyer_agent_request.json new file mode 100644 index 0000000..e69de29 diff --git a/data/taxonomies/agentic-audiences-draft-2026-01/spec/v1.0/examples/embedding_update.json b/data/taxonomies/agentic-audiences-draft-2026-01/spec/v1.0/examples/embedding_update.json new file mode 100644 index 0000000..355ac1f --- /dev/null +++ b/data/taxonomies/agentic-audiences-draft-2026-01/spec/v1.0/examples/embedding_update.json @@ -0,0 +1,34 @@ +{ + "spec_version": "1.0.0", + "message_id": "3f6a5fe0-9c29-4b6c-8e1e-1f6f5e6d9b01", + "timestamp": "2025-10-22T17:05:00Z", + "model": { + "id": "sbert-mini-ctx-001", + "version": "1.0.0", + "type": "encoder", + "architecture": "transformer", + "dimension": 256, + "metric": "cosine", + "normalization": "l2_unit", + "embedding_space_id": "ucp://spaces/contextual/en-v1" + }, + "context": { + "url": "https://site.example/article", + "keywords": ["financing", "ev tax credit"], + "language": "en" + }, + "consent": { + "framework": "IAB-CTv2", + "purposes": ["optimization"], + "ttl_seconds": 604800 + }, + "embeddings": [ + { + "id": "vec-1", + "type": "context", + "vector": [0.01, 0.02, -0.03], + "dtype": "float32", + "dimension": 256 + } + ] + } \ No newline at end of file diff --git a/data/taxonomies/agentic-audiences-draft-2026-01/spec/v1.0/examples/seller_agent_response.json b/data/taxonomies/agentic-audiences-draft-2026-01/spec/v1.0/examples/seller_agent_response.json new file mode 100644 index 0000000..e69de29 diff --git a/data/taxonomies/agentic-audiences-draft-2026-01/spec/v1.0/schema/agent_interface.schema.json b/data/taxonomies/agentic-audiences-draft-2026-01/spec/v1.0/schema/agent_interface.schema.json new file mode 100644 index 0000000..e69de29 diff --git a/data/taxonomies/agentic-audiences-draft-2026-01/spec/v1.0/schema/embedding_format.schema.json b/data/taxonomies/agentic-audiences-draft-2026-01/spec/v1.0/schema/embedding_format.schema.json new file mode 100644 index 0000000..b0e74fb --- /dev/null +++ b/data/taxonomies/agentic-audiences-draft-2026-01/spec/v1.0/schema/embedding_format.schema.json @@ -0,0 +1,221 @@ +{ + "$id": "https://raw.githubusercontent.com/LiveRamp/user-context-protocol/main/specs/v1.0/schema/embedding_format.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": ["spec_version", "message_id", "timestamp", "model", "context", "embeddings"], + "properties": { + "spec_version": { "type": "string" }, + "message_id": { "type": "string" }, + "timestamp": { "type": "string", "format": "date-time" }, + "producer": { + "type": "object", + "properties": { + "agent_id": { "type": "string" }, + "agent_role": { "type": "string", "enum": ["publisher","seller","buyer","measurement","brand","other"] }, + "software": { "type": "string" }, + "software_version": { "type": "string" } + }, + "additionalProperties": false + }, + "model": { + "type": "object", + "required": ["id","version","type","architecture","dimension","metric","embedding_space_id"], + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "family": { "type": "string" }, + "vendor": { "type": "string" }, + "version": { "type": "string" }, + "type": { "type": "string", "enum": ["encoder","llm","slm"] }, + "architecture": { "type": "string" }, + "dimension": { "type": "integer", "minimum": 1 }, + "metric": { "type": "string", "enum": ["cosine","dot","l2"] }, + "normalization": { "type": "string", "enum": ["none","l2_unit"], "default": "none" }, + "tokenizer": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "version": { "type": "string" }, + "lowercase": { "type": "boolean" }, + "strip_accents": { "type": "boolean" }, + "vocab_id": { "type": "string" } + }, + "additionalProperties": false + }, + "pooling": { "type": "string", "enum": ["mean","max","cls","weighted","other"] }, + "projection": { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["none","pca","whitening","learned"] }, + "params_ref": { "type": "string" } + }, + "additionalProperties": false + }, + "quantization": { + "type": "object", + "properties": { + "scheme": { "type": "string", "enum": ["none","int8","uint8","pq","sq"] }, + "scale": { "type": "number" }, + "zero_point": { "type": "integer" }, + "pack_format": { "type": "string" } + }, + "additionalProperties": false + }, + "embedding_space_id": { "type": "string" }, + "training_domain": { "type": "array", "items": { "type": "string" } }, + "licensing": { + "type": "object", + "properties": { + "license": { "type": "string" }, + "usage_terms_url": { "type": "string", "format": "uri" } + }, + "additionalProperties": false + }, + "compatibility": { + "type": "object", + "properties": { + "compatible_spaces": { "type": "array", "items": { "type": "string" } }, + "notes": { "type": "string" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "context": { + "type": "object", + "required": ["language"], + "properties": { + "context_id": { "type": "string" }, + "url": { "type": "string", "format": "uri" }, + "page_title": { "type": "string" }, + "keywords": { "type": "array", "items": { "type": "string" } }, + "language": { "type": "string" }, + "content_hash": { "type": "string" }, + "publisher": { "type": "string" }, + "placement": { + "type": "object", + "properties": { + "ad_unit": { "type": "string" }, + "slot_id": { "type": "string" } + }, + "additionalProperties": false + }, + "device": { + "type": "object", + "properties": { + "ua_hash": { "type": "string" }, + "platform": { "type": "string" }, + "form_factor": { "type": "string" } + }, + "additionalProperties": false + }, + "geography": { + "type": "object", + "properties": { + "country": { "type": "string" }, + "region": { "type": "string" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "identity": { + "type": "object", + "properties": { + "namespace": { "type": "string" }, + "value_hash": { "type": "string" }, + "confidence": { "type": "number", "minimum": 0, "maximum": 1 } + }, + "additionalProperties": false + }, + "consent": { + "type": "object", + "required": ["purposes","ttl_seconds"], + "properties": { + "framework": { "type": "string" }, + "consent_string": { "type": "string" }, + "purposes": { "type": "array", "items": { "type": "string" } }, + "permissible_uses": { "type": "array", "items": { "type": "string" } }, + "ttl_seconds": { "type": "integer", "minimum": 0 }, + "legal_basis": { "type": "string", "enum": ["consent","contract","legitimate_interest","other"] }, + "policy_version": { "type": "string" }, + "opt_out": { "type": "boolean" } + }, + "additionalProperties": false + }, + "security": { + "type": "object", + "properties": { + "signature": { + "type": "object", + "properties": { + "alg": { "type": "string" }, + "key_id": { "type": "string" }, + "sig_b64": { "type": "string" } + }, + "additionalProperties": false + }, + "transport": { "type": "string" }, + "attestation": { + "type": "object", + "properties": { + "env": { "type": "string" }, + "mode": { "type": "string" }, + "policy_hash": { "type": "string" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "embeddings": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["id","type","dimension"], + "oneOf": [ + { "required": ["vector"] }, + { "required": ["quantized_b64"] } + ], + "properties": { + "id": { "type": "string" }, + "type": { "type": "string", "enum": ["context","creative","user_intent","inventory","query"] }, + "vector": { + "type": "array", + "items": { "type": "number" } + }, + "quantized_b64": { "type": "string" }, + "dtype": { "type": "string", "enum": ["float32","float16","int8","uint8"] }, + "dimension": { "type": "integer", "minimum": 1 }, + "scale": { "type": "number" }, + "compressed": { "type": "boolean" }, + "hash": { "type": "string" }, + "origin": { + "type": "object", + "properties": { + "text_window": { "type": "string" }, + "source_fields": { "type": "array", "items": { "type": "string" } }, + "on_device": { "type": "boolean" } + }, + "additionalProperties": false + }, + "usage_hints": { + "type": "object", + "properties": { + "score_metric": { "type": "string", "enum": ["cosine","dot","l2"] }, + "threshold": { "type": "number" }, + "intended_consumers": { "type": "array", "items": { "type": "string" } } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "extensions": { "type": "object" } + }, + "additionalProperties": false + } \ No newline at end of file diff --git a/data/taxonomies/audience-1.1/Audience Taxonomy 1.1.tsv b/data/taxonomies/audience-1.1/Audience Taxonomy 1.1.tsv new file mode 100644 index 0000000..b6f76d2 --- /dev/null +++ b/data/taxonomies/audience-1.1/Audience Taxonomy 1.1.tsv @@ -0,0 +1,1559 @@ + Unique ID Parent ID Condensed Name (1st, 2nd, Last Tier) Tier 1 Tier 2 Tier 3 Tier 4 Tier 5 Tier 6 *Extension Notes + 1 Demographic Demographic + 2 1 Demographic | Age Range Demographic Age Range + 3 2 Demographic | Age Range | 18-20 | Demographic Age Range 18-20 + 4 2 Demographic | Age Range | 21-24 | Demographic Age Range 21-24 + 5 2 Demographic | Age Range | 25-29 | Demographic Age Range 25-29 + 6 2 Demographic | Age Range | 30-34 | Demographic Age Range 30-34 + 7 2 Demographic | Age Range | 35-39 | Demographic Age Range 35-39 + 8 2 Demographic | Age Range | 40-44 | Demographic Age Range 40-44 + 9 2 Demographic | Age Range | 45-49 | Demographic Age Range 45-49 + 10 2 Demographic | Age Range | 50-54 | Demographic Age Range 50-54 + 11 2 Demographic | Age Range | 55-59 | Demographic Age Range 55-59 + 12 2 Demographic | Age Range | 60-64 | Demographic Age Range 60-64 + 13 2 Demographic | Age Range | 65-69 | Demographic Age Range 65-69 + 14 2 Demographic | Age Range | 70-74 | Demographic Age Range 70-74 + 15 2 Demographic | Age Range | 75+ | Demographic Age Range 75+ + 16 1 Demographic | Education & Occupation Demographic Education & Occupation + 17 16 Demographic | Education & Occupation | Education (Highest Level) | Demographic Education & Occupation Education (Highest Level) + 18 17 Demographic | Education & Occupation | Primary Education | Demographic Education & Occupation Education (Highest Level) Primary Education + 19 17 Demographic | Education & Occupation | Secondary Education | Demographic Education & Occupation Education (Highest Level) Secondary Education + 20 17 Demographic | Education & Occupation | College Education | Demographic Education & Occupation Education (Highest Level) College Education + 21 20 Demographic | Education & Occupation | Professional School | Demographic Education & Occupation Education (Highest Level) College Education Professional School + 22 20 Demographic | Education & Occupation | Postgraduate Education | Demographic Education & Occupation Education (Highest Level) College Education Postgraduate Education + 23 20 Demographic | Education & Occupation | Undergraduate Education | Demographic Education & Occupation Education (Highest Level) College Education Undergraduate Education + 24 16 Demographic | Education & Occupation | Employment Role | Demographic Education & Occupation Employment Role + 25 24 Demographic | Education & Occupation | Work from Home | Demographic Education & Occupation Employment Role Work from Home + 26 24 Demographic | Education & Occupation | Director/Managerial | Demographic Education & Occupation Employment Role Director/Managerial + 27 24 Demographic | Education & Occupation | Homemaker / Domestic Work | Demographic Education & Occupation Employment Role Homemaker / Domestic Work + 28 24 Demographic | Education & Occupation | Office Worker | Demographic Education & Occupation Employment Role Office Worker + 29 24 Demographic | Education & Occupation | Part-Time Worker | Demographic Education & Occupation Employment Role Part-Time Worker + 30 24 Demographic | Education & Occupation | Professional | Demographic Education & Occupation Employment Role Professional + 31 24 Demographic | Education & Occupation | Public Sector | Demographic Education & Occupation Employment Role Public Sector + 32 24 Demographic | Education & Occupation | Retired | Demographic Education & Occupation Employment Role Retired + 33 24 Demographic | Education & Occupation | Self Employed | Demographic Education & Occupation Employment Role Self Employed + 34 24 Demographic | Education & Occupation | Shop Worker | Demographic Education & Occupation Employment Role Shop Worker + 35 24 Demographic | Education & Occupation | Skilled/Manual Work | Demographic Education & Occupation Employment Role Skilled/Manual Work + 36 24 Demographic | Education & Occupation | Student | Demographic Education & Occupation Employment Role Student + 37 24 Demographic | Education & Occupation | Unemployed | Demographic Education & Occupation Employment Role Unemployed + 38 16 Demographic | Education & Occupation | Employment Sector / Industry | Demographic Education & Occupation Employment Sector / Industry + 39 16 Demographic | Education & Occupation | Employment Status | Demographic Education & Occupation Employment Status + 40 39 Demographic | Education & Occupation | Retired | Demographic Education & Occupation Employment Status Retired + 41 39 Demographic | Education & Occupation | Student | Demographic Education & Occupation Employment Status Student + 42 39 Demographic | Education & Occupation | Employed | Demographic Education & Occupation Employment Status Employed + 43 42 Demographic | Education & Occupation | Part-Time | Demographic Education & Occupation Employment Status Employed Part-Time + 44 42 Demographic | Education & Occupation | Full-Time | Demographic Education & Occupation Employment Status Employed Full-Time + 45 39 Demographic | Education & Occupation | Self-Employed | Demographic Education & Occupation Employment Status Self-Employed + 46 39 Demographic | Education & Occupation | Unemployed / Job Seeker | Demographic Education & Occupation Employment Status Unemployed / Job Seeker + 47 39 Demographic | Education & Occupation | Unemployed | Demographic Education & Occupation Employment Status Unemployed + 48 1 Demographic | Gender Demographic Gender + 49 48 Demographic | Gender | Female | Demographic Gender Female + 50 48 Demographic | Gender | Male | Demographic Gender Male + 51 48 Demographic | Gender | Other Gender | Demographic Gender Other Gender + 52 48 Demographic | Gender | Unknown Gender | Demographic Gender Unknown Gender + 53 1 Demographic | Household Data Demographic Household Data + 54 53 Demographic | Household Data | Home Location | Demographic Household Data Home Location + 55 54 Demographic | Household Data | *Country Extension | Demographic Household Data Home Location *Country Extension ISO-3166-1-alpha-3 + 56 54 Demographic | Household Data | *Region / State Extension | Demographic Household Data Home Location *Region / State Extension ISO-3166-2; 2-letter state code if USA. + 57 54 Demographic | Household Data | *City Extension | Demographic Household Data Home Location *City Extension City using United Nations Code for Trade & Transport Locations. + 58 54 Demographic | Household Data | *Metro / DMA Extension | Demographic Household Data Home Location *Metro / DMA Extension Google metro code; similar to but not exactly Nielsen DMAs + 59 54 Demographic | Household Data | *Zip or postal code Extension | Demographic Household Data Home Location *Zip or postal code Extension Zip or postal code + 60 53 Demographic | Household Data | Household Income (USD) | Demographic Household Data Household Income (USD) + 61 60 Demographic | Household Data | Less than $10,000 | Demographic Household Data Household Income (USD) Less than $10,000 + 62 60 Demographic | Household Data | $10,000-$14,999 | Demographic Household Data Household Income (USD) $10,000-$14,999 + 63 60 Demographic | Household Data | $15,000-$19,999 | Demographic Household Data Household Income (USD) $15,000-$19,999 + 64 60 Demographic | Household Data | $20000 - $39999 | Demographic Household Data Household Income (USD) $20000 - $39999 + 65 60 Demographic | Household Data | $40000 - $49999 | Demographic Household Data Household Income (USD) $40000 - $49999 + 66 60 Demographic | Household Data | $50000 - $74999 | Demographic Household Data Household Income (USD) $50000 - $74999 + 67 60 Demographic | Household Data | $75000 - $99999 | Demographic Household Data Household Income (USD) $75000 - $99999 + 68 60 Demographic | Household Data | $100000 - $149999 | Demographic Household Data Household Income (USD) $100000 - $149999 + 69 60 Demographic | Household Data | $150,000-$174,999 | Demographic Household Data Household Income (USD) $150,000-$174,999 + 70 60 Demographic | Household Data | $175,000-$199,999 | Demographic Household Data Household Income (USD) $175,000-$199,999 + 71 60 Demographic | Household Data | $200,000-$249,999 | Demographic Household Data Household Income (USD) $200,000-$249,999 + 72 60 Demographic | Household Data | $250,000+ | Demographic Household Data Household Income (USD) $250,000+ + 73 53 Demographic | Household Data | Length of Residence | Demographic Household Data Length of Residence + 74 73 Demographic | Household Data | Less Than 1 Year | Demographic Household Data Length of Residence Less Than 1 Year + 75 73 Demographic | Household Data | 1-3 Years | Demographic Household Data Length of Residence 1-3 Years + 76 75 Demographic | Household Data | 1 year | Demographic Household Data Length of Residence 1-3 Years 1 year + 77 75 Demographic | Household Data | 2 years | Demographic Household Data Length of Residence 1-3 Years 2 years + 78 75 Demographic | Household Data | 3 years | Demographic Household Data Length of Residence 1-3 Years 3 years + 79 73 Demographic | Household Data | 4-6 Years | Demographic Household Data Length of Residence 4-6 Years + 80 79 Demographic | Household Data | 4 years | Demographic Household Data Length of Residence 4-6 Years 4 years + 81 79 Demographic | Household Data | 5 years | Demographic Household Data Length of Residence 4-6 Years 5 years + 82 79 Demographic | Household Data | 6 years | Demographic Household Data Length of Residence 4-6 Years 6 years + 83 73 Demographic | Household Data | 7+ Years | Demographic Household Data Length of Residence 7+ Years + 84 83 Demographic | Household Data | 7 years | Demographic Household Data Length of Residence 7+ Years 7 years + 85 83 Demographic | Household Data | 8 years | Demographic Household Data Length of Residence 7+ Years 8 years + 86 83 Demographic | Household Data | 9 years | Demographic Household Data Length of Residence 7+ Years 9 years + 87 83 Demographic | Household Data | 10 years | Demographic Household Data Length of Residence 7+ Years 10 years + 88 83 Demographic | Household Data | 11 years | Demographic Household Data Length of Residence 7+ Years 11 years + 89 83 Demographic | Household Data | 12 years | Demographic Household Data Length of Residence 7+ Years 12 years + 90 83 Demographic | Household Data | 13 years | Demographic Household Data Length of Residence 7+ Years 13 years + 91 83 Demographic | Household Data | 14 years | Demographic Household Data Length of Residence 7+ Years 14 years + 92 83 Demographic | Household Data | 15 years | Demographic Household Data Length of Residence 7+ Years 15 years + 93 53 Demographic | Household Data | Life Stage | Demographic Household Data Life Stage + 94 53 Demographic | Household Data | Other | Demographic Household Data Life Stage Other + 95 93 Demographic | Household Data | Single Generation Household | Demographic Household Data Life Stage Single Generation Household + 96 93 Demographic | Household Data | Adults (no children) | Demographic Household Data Life Stage Single Generation Household Adults (no children) + 97 93 Demographic | Household Data | Multi Generation Household | Demographic Household Data Life Stage Multi Generation Household + 98 93 Demographic | Household Data | Parents with Children | Demographic Household Data Life Stage Multi Generation Household Parents with Children + 99 93 Demographic | Household Data | Grandparents, Parents and Children | Demographic Household Data Life Stage Multi Generation Household Grandparents, Parents and Children + 100 93 Demographic | Household Data | Grandparents with Children | Demographic Household Data Life Stage Multi Generation Household Grandparents with Children + 101 93 Demographic | Household Data | Empty Nest (Adults, Children left home) | Demographic Household Data Life Stage Multi Generation Household Empty Nest (Adults, Children left home) + 102 53 Demographic | Household Data | Median Home Value (USD) | Demographic Household Data Median Home Value (USD) + 103 102 Demographic | Household Data | $0-$99,999 | Demographic Household Data Median Home Value (USD) $0-$99,999 + 104 102 Demographic | Household Data | $100,000-$199,999 | Demographic Household Data Median Home Value (USD) $100,000-$199,999 + 105 102 Demographic | Household Data | $200,000-$299,999 | Demographic Household Data Median Home Value (USD) $200,000-$299,999 + 106 102 Demographic | Household Data | $300,000-$399,999 | Demographic Household Data Median Home Value (USD) $300,000-$399,999 + 107 102 Demographic | Household Data | $400,000-$499,999 | Demographic Household Data Median Home Value (USD) $400,000-$499,999 + 108 102 Demographic | Household Data | $500,000-$599,999 | Demographic Household Data Median Home Value (USD) $500,000-$599,999 + 109 102 Demographic | Household Data | $600,000-$699,999 | Demographic Household Data Median Home Value (USD) $600,000-$699,999 + 110 102 Demographic | Household Data | $700,000-$799,999 | Demographic Household Data Median Home Value (USD) $700,000-$799,999 + 111 102 Demographic | Household Data | $800,000-$899,999 | Demographic Household Data Median Home Value (USD) $800,000-$899,999 + 112 102 Demographic | Household Data | $900,000-$999,999 | Demographic Household Data Median Home Value (USD) $900,000-$999,999 + 113 102 Demographic | Household Data | $1,000,000+ | Demographic Household Data Median Home Value (USD) $1,000,000+ + 114 53 Demographic | Household Data | Monthly Housing Payment (USD) | Demographic Household Data Monthly Housing Payment (USD) + 115 114 Demographic | Household Data | 0 | Demographic Household Data Monthly Housing Payment (USD) $0 + 116 114 Demographic | Household Data | $1-$499 | Demographic Household Data Monthly Housing Payment (USD) $1-$499 + 117 114 Demographic | Household Data | $500-$999 | Demographic Household Data Monthly Housing Payment (USD) $500-$999 + 118 114 Demographic | Household Data | $1,000-$1,499 | Demographic Household Data Monthly Housing Payment (USD) $1,000-$1,499 + 119 114 Demographic | Household Data | $1,500-$1,999 | Demographic Household Data Monthly Housing Payment (USD) $1,500-$1,999 + 120 114 Demographic | Household Data | $2,000-$3,000 | Demographic Household Data Monthly Housing Payment (USD) $2,000-$3,000 + 121 53 Demographic | Household Data | Number of Adults | Demographic Household Data Number of Adults + 122 121 Demographic | Household Data | 1 Adult | Demographic Household Data Number of Adults 1 Adult + 123 121 Demographic | Household Data | 2 Adults | Demographic Household Data Number of Adults 2 Adults + 124 121 Demographic | Household Data | 3+ Adults | Demographic Household Data Number of Adults 3+ Adults + 125 53 Demographic | Household Data | Number of Children | Demographic Household Data Number of Children + 126 125 Demographic | Household Data | 0 Child | Demographic Household Data Number of Children 0 Child + 127 125 Demographic | Household Data | 1 Child | Demographic Household Data Number of Children 1 Child + 128 125 Demographic | Household Data | 2 Child | Demographic Household Data Number of Children 2 Child + 129 125 Demographic | Household Data | 3+ Child | Demographic Household Data Number of Children 3+ Child + 130 53 Demographic | Household Data | Number of Individuals | Demographic Household Data Number of Individuals + 131 130 Demographic | Household Data | 1 person | Demographic Household Data Number of Individuals 1 person + 132 130 Demographic | Household Data | 2 people | Demographic Household Data Number of Individuals 2 people + 133 130 Demographic | Household Data | 3 people | Demographic Household Data Number of Individuals 3 people + 134 130 Demographic | Household Data | 4 people | Demographic Household Data Number of Individuals 4 people + 135 130 Demographic | Household Data | 5 people | Demographic Household Data Number of Individuals 5 people + 136 130 Demographic | Household Data | 6+ People | Demographic Household Data Number of Individuals 6+ People + 137 53 Demographic | Household Data | Ownership | Demographic Household Data Ownership + 138 137 Demographic | Household Data | Home Owners | Demographic Household Data Ownership Home Owners + 139 137 Demographic | Household Data | Renters | Demographic Household Data Ownership Renters + 140 137 Demographic | Household Data | Owner | Demographic Household Data Ownership Owner + 141 137 Demographic | Household Data | Renter | Demographic Household Data Ownership Renter + 142 137 Demographic | Household Data | First Time Homeowner | Demographic Household Data Ownership First Time Homeowner + 143 53 Demographic | Household Data | Property Type | Demographic Household Data Property Type + 144 143 Demographic | Household Data | Multiple Family | Demographic Household Data Property Type Multiple Family + 145 143 Demographic | Household Data | Single Family | Demographic Household Data Property Type Single Family + 146 53 Demographic | Household Data | Urbanization | Demographic Household Data Urbanization + 147 146 Demographic | Household Data | Rural | Demographic Household Data Urbanization Rural + 148 146 Demographic | Household Data | 2K-4.9K People | Demographic Household Data Urbanization 2K-4.9K People + 149 146 Demographic | Household Data | 5K-9.9K People | Demographic Household Data Urbanization 5K-9.9K People + 150 146 Demographic | Household Data | 10K-19.9K People | Demographic Household Data Urbanization 10K-19.9K People + 151 146 Demographic | Household Data | 20K-49.9K People | Demographic Household Data Urbanization 20K-49.9K People + 152 146 Demographic | Household Data | 50K-99.9K People | Demographic Household Data Urbanization 50K-99.9K People + 153 146 Demographic | Household Data | 100K-199.9K People | Demographic Household Data Urbanization 100K-199.9K People + 154 146 Demographic | Household Data | 200K-2M People | Demographic Household Data Urbanization 200K-2M People + 155 146 Demographic | Household Data | Over 2M+ People | Demographic Household Data Urbanization Over 2M+ People + 156 1 Demographic | Language Demographic Language + 157 156 Demographic | Language | Other | Demographic Language Other + 158 156 Demographic | Language | *Language Extension | Demographic Language Other *Language Extension See ISO-639-1-alpha-2 + 159 1 Demographic | Marital Status Demographic Marital Status + 160 159 Demographic | Marital Status | Co-Habiting | Demographic Marital Status Co-Habiting + 161 159 Demographic | Marital Status | Married | Demographic Marital Status Married + 162 159 Demographic | Marital Status | Single | Demographic Marital Status Single + 163 1 Demographic | Personal Finance Demographic Personal Finance + 164 163 Demographic | Personal Finance | Income (USD) | Demographic Personal Finance Income (USD) + 165 164 Demographic | Personal Finance | $10,000-$14,999 | Demographic Personal Finance Income (USD) $10,000-$14,999 + 166 164 Demographic | Personal Finance | $15,000-$19,999 | Demographic Personal Finance Income (USD) $15,000-$19,999 + 167 164 Demographic | Personal Finance | $20000 - $39999 | Demographic Personal Finance Income (USD) $20000 - $39999 + 168 164 Demographic | Personal Finance | $40000 - $49999 | Demographic Personal Finance Income (USD) $40000 - $49999 + 169 164 Demographic | Personal Finance | $50000 - $74999 | Demographic Personal Finance Income (USD) $50000 - $74999 + 170 164 Demographic | Personal Finance | $75000 - $99999 | Demographic Personal Finance Income (USD) $75000 - $99999 + 171 164 Demographic | Personal Finance | $100000 - $149999 | Demographic Personal Finance Income (USD) $100000 - $149999 + 172 164 Demographic | Personal Finance | $150,000-$174,999 | Demographic Personal Finance Income (USD) $150,000-$174,999 + 173 164 Demographic | Personal Finance | $175,000-$199,999 | Demographic Personal Finance Income (USD) $175,000-$199,999 + 174 164 Demographic | Personal Finance | $200,000-$249,999 | Demographic Personal Finance Income (USD) $200,000-$249,999 + 175 164 Demographic | Personal Finance | $250,000+ | Demographic Personal Finance Income (USD) $250,000+ + 176 163 Demographic | Personal Finance | Personal Level Affluence (USD) | Demographic Personal Finance Personal Level Affluence (USD) + 177 176 Demographic | Personal Finance | Less than $10,000 | Demographic Personal Finance Personal Level Affluence (USD) Less than $10,000 + 178 176 Demographic | Personal Finance | $10,000-$14,999 | Demographic Personal Finance Personal Level Affluence (USD) $10,000-$14,999 + 179 176 Demographic | Personal Finance | $15,000-$19,999 | Demographic Personal Finance Personal Level Affluence (USD) $15,000-$19,999 + 180 176 Demographic | Personal Finance | $20000 - $39999 | Demographic Personal Finance Personal Level Affluence (USD) $20000 - $39999 + 181 176 Demographic | Personal Finance | $40000 - $49999 | Demographic Personal Finance Personal Level Affluence (USD) $40000 - $49999 + 182 176 Demographic | Personal Finance | $50000 - $74999 | Demographic Personal Finance Personal Level Affluence (USD) $50000 - $74999 + 183 176 Demographic | Personal Finance | $75000 - $99999 | Demographic Personal Finance Personal Level Affluence (USD) $75000 - $99999 + 184 176 Demographic | Personal Finance | $100000 - $149999 | Demographic Personal Finance Personal Level Affluence (USD) $100000 - $149999 + 185 176 Demographic | Personal Finance | $150,000-$174,999 | Demographic Personal Finance Personal Level Affluence (USD) $150,000-$174,999 + 186 176 Demographic | Personal Finance | $175,000-$199,999 | Demographic Personal Finance Personal Level Affluence (USD) $175,000-$199,999 + 187 176 Demographic | Personal Finance | $200,000-$249,999 | Demographic Personal Finance Personal Level Affluence (USD) $200,000-$249,999 + 188 176 Demographic | Personal Finance | $250,000-$500,000 | Demographic Personal Finance Personal Level Affluence (USD) $250,000-$500,000 + 189 176 Demographic | Personal Finance | $500,000-$1,000,000 | Demographic Personal Finance Personal Level Affluence (USD) $500,000-$1,000,000 + 190 176 Demographic | Personal Finance | $1,000,000+ | Demographic Personal Finance Personal Level Affluence (USD) $1,000,000+ + 191 163 Demographic | Personal Finance | Personal Level Affluence Band | Demographic Personal Finance Personal Level Affluence Band + 192 191 Demographic | Personal Finance | Negative Net Worth | Demographic Personal Finance Personal Level Affluence Band Negative Net Worth + 193 191 Demographic | Personal Finance | Very Low Net Worth | Demographic Personal Finance Personal Level Affluence Band Very Low Net Worth + 194 191 Demographic | Personal Finance | Low Net Worth | Demographic Personal Finance Personal Level Affluence Band Low Net Worth + 195 191 Demographic | Personal Finance | Mid Net Worth | Demographic Personal Finance Personal Level Affluence Band Mid Net Worth + 196 191 Demographic | Personal Finance | High Net Worth | Demographic Personal Finance Personal Level Affluence Band High Net Worth + 197 191 Demographic | Personal Finance | Super High Net Worth | Demographic Personal Finance Personal Level Affluence Band Super High Net Worth + 206 Interest Interest + 207 206 Interest | Academic Interests Interest Academic Interests + 208 207 Interest | Academic Interests | Arts and Humanities | Interest Academic Interests Arts and Humanities + 209 208 Interest | Academic Interests | Critical Thinking | Interest Academic Interests Arts and Humanities Critical Thinking + 210 208 Interest | Academic Interests | Counseling | Interest Academic Interests Arts and Humanities Counseling + 211 208 Interest | Academic Interests | History | Interest Academic Interests Arts and Humanities History + 212 208 Interest | Academic Interests | Music and Art | Interest Academic Interests Arts and Humanities Music and Art + 213 208 Interest | Academic Interests | Philosophy | Interest Academic Interests Arts and Humanities Philosophy + 214 207 Interest | Academic Interests | Language Learning | Interest Academic Interests Language Learning + 215 207 Interest | Academic Interests | Life Sciences | Interest Academic Interests Life Sciences + 216 215 Interest | Academic Interests | Animals and Veterinary Science | Interest Academic Interests Life Sciences Animals and Veterinary Science + 217 215 Interest | Academic Interests | Bioinformatics | Interest Academic Interests Life Sciences Bioinformatics + 218 215 Interest | Academic Interests | Biology | Interest Academic Interests Life Sciences Biology + 219 215 Interest | Academic Interests | Medicine and Healthcare | Interest Academic Interests Life Sciences Medicine and Healthcare + 220 215 Interest | Academic Interests | Nutrition | Interest Academic Interests Life Sciences Nutrition + 221 215 Interest | Academic Interests | Clinical Science | Interest Academic Interests Life Sciences Clinical Science + 222 215 Interest | Academic Interests | Genetics | Interest Academic Interests Life Sciences Genetics + 223 207 Interest | Academic Interests | Physical Science and Engineering | Interest Academic Interests Physical Science and Engineering + 224 223 Interest | Academic Interests | Electrical Engineering | Interest Academic Interests Physical Science and Engineering Electrical Engineering + 225 223 Interest | Academic Interests | Mechanical Engineering | Interest Academic Interests Physical Science and Engineering Mechanical Engineering + 226 223 Interest | Academic Interests | Chemistry | Interest Academic Interests Physical Science and Engineering Chemistry + 227 223 Interest | Academic Interests | Environmental Science and Sustainability | Interest Academic Interests Physical Science and Engineering Environmental Science and Sustainability + 228 223 Interest | Academic Interests | Research Methods | Interest Academic Interests Physical Science and Engineering Research Methods + 229 223 Interest | Academic Interests | Geography | Interest Academic Interests Physical Science and Engineering Geography + 230 223 Interest | Academic Interests | Geology | Interest Academic Interests Physical Science and Engineering Geology + 231 223 Interest | Academic Interests | Physics | Interest Academic Interests Physical Science and Engineering Physics + 232 223 Interest | Academic Interests | Space and Astronomy | Interest Academic Interests Physical Science and Engineering Space and Astronomy + 233 207 Interest | Academic Interests | Social Sciences | Interest Academic Interests Social Sciences + 234 233 Interest | Academic Interests | Psychology | Interest Academic Interests Social Sciences Psychology + 235 233 Interest | Academic Interests | International Relations | Interest Academic Interests Social Sciences International Relations + 236 233 Interest | Academic Interests | Research Paper Writing | Interest Academic Interests Social Sciences Research Paper Writing + 237 233 Interest | Academic Interests | Statistics | Interest Academic Interests Social Sciences Statistics + 238 233 Interest | Academic Interests | Law | Interest Academic Interests Social Sciences Law + 239 233 Interest | Academic Interests | Critical Thinking | Interest Academic Interests Social Sciences Critical Thinking + 240 233 Interest | Academic Interests | Economics | Interest Academic Interests Social Sciences Economics + 241 233 Interest | Academic Interests | Education | Interest Academic Interests Social Sciences Education + 242 233 Interest | Academic Interests | Governance and Society | Interest Academic Interests Social Sciences Governance and Society + 243 206 Interest | Automotive Interest Automotive + 244 243 Interest | Automotive | Auto Buying and Selling | Interest Automotive Auto Buying and Selling + 245 243 Interest | Automotive | Auto Shows | Interest Automotive Auto Shows + 246 243 Interest | Automotive | Auto Technology | Interest Automotive Auto Technology + 247 243 Interest | Automotive | Budget Cars | Interest Automotive Budget Cars + 248 243 Interest | Automotive | Car Culture | Interest Automotive Car Culture + 249 243 Interest | Automotive | Classic Cars | Interest Automotive Classic Cars + 250 243 Interest | Automotive | Concept Cars | Interest Automotive Concept Cars + 251 243 Interest | Automotive | Dash Cam Videos | Interest Automotive Dash Cam Videos + 252 243 Interest | Automotive | Driverless Cars | Interest Automotive Driverless Cars + 253 243 Interest | Automotive | Green Vehicles | Interest Automotive Green Vehicles + 254 243 Interest | Automotive | Luxury Cars | Interest Automotive Luxury Cars + 255 243 Interest | Automotive | Motorcycles | Interest Automotive Motorcycles + 256 243 Interest | Automotive | Performance Cars | Interest Automotive Performance Cars + 257 243 Interest | Automotive | Scooters | Interest Automotive Scooters + 258 206 Interest | Books and Literature Interest Books and Literature + 259 258 Interest | Books and Literature | Art and Photography Books | Interest Books and Literature Art and Photography Books + 260 258 Interest | Books and Literature | Biographies | Interest Books and Literature Biographies + 261 258 Interest | Books and Literature | Children's Literature | Interest Books and Literature Children's Literature + 262 258 Interest | Books and Literature | Comics and Graphic Novels | Interest Books and Literature Comics and Graphic Novels + 263 258 Interest | Books and Literature | Cookbooks | Interest Books and Literature Cookbooks + 264 258 Interest | Books and Literature | Fiction | Interest Books and Literature Fiction + 265 258 Interest | Books and Literature | Poetry | Interest Books and Literature Poetry + 266 258 Interest | Books and Literature | Travel Books | Interest Books and Literature Travel Books + 267 258 Interest | Books and Literature | Young Adult Literature | Interest Books and Literature Young Adult Literature + 268 206 Interest | Business and Finance Interest Business and Finance + 269 268 Interest | Business and Finance | Business | Interest Business and Finance Business + 270 269 Interest | Business and Finance | Business Accounting & Finance | Interest Business and Finance Business Business Accounting & Finance + 271 269 Interest | Business and Finance | Human Resources | Interest Business and Finance Business Human Resources + 272 269 Interest | Business and Finance | Large Business | Interest Business and Finance Business Large Business + 273 269 Interest | Business and Finance | Logistics | Interest Business and Finance Business Logistics + 274 269 Interest | Business and Finance | Marketing and Advertising | Interest Business and Finance Business Marketing and Advertising + 275 269 Interest | Business and Finance | Sales | Interest Business and Finance Business Sales + 276 269 Interest | Business and Finance | Small and Medium-sized Business | Interest Business and Finance Business Small and Medium-sized Business + 277 269 Interest | Business and Finance | Startups | Interest Business and Finance Business Startups + 278 269 Interest | Business and Finance | Business Administration | Interest Business and Finance Business Business Administration + 279 269 Interest | Business and Finance | Business Banking & Finance | Interest Business and Finance Business Business Banking & Finance + 280 279 Interest | Business and Finance | Angel Investment | Interest Business and Finance Business Business Banking & Finance Angel Investment + 281 279 Interest | Business and Finance | Bankruptcy | Interest Business and Finance Business Business Banking & Finance Bankruptcy + 282 279 Interest | Business and Finance | Business Loans | Interest Business and Finance Business Business Banking & Finance Business Loans + 283 279 Interest | Business and Finance | Debt Factoring & Invoice Discounting | Interest Business and Finance Business Business Banking & Finance Debt Factoring & Invoice Discounting + 284 279 Interest | Business and Finance | Mergers and Acquisitions | Interest Business and Finance Business Business Banking & Finance Mergers and Acquisitions + 285 279 Interest | Business and Finance | Private Equity | Interest Business and Finance Business Business Banking & Finance Private Equity + 286 279 Interest | Business and Finance | Sale & Lease Back | Interest Business and Finance Business Business Banking & Finance Sale & Lease Back + 287 279 Interest | Business and Finance | Venture Capital | Interest Business and Finance Business Business Banking & Finance Venture Capital + 288 269 Interest | Business and Finance | Business I.T. | Interest Business and Finance Business Business I.T. + 289 269 Interest | Business and Finance | Business Operations | Interest Business and Finance Business Business Operations + 290 269 Interest | Business and Finance | Consumer Issues | Interest Business and Finance Business Consumer Issues + 291 269 Interest | Business and Finance | Executive Leadership & Management | Interest Business and Finance Business Executive Leadership & Management + 292 269 Interest | Business and Finance | Government Business | Interest Business and Finance Business Government Business + 293 269 Interest | Business and Finance | Green Solutions | Interest Business and Finance Business Green Solutions + 294 269 Interest | Business and Finance | Business Utilities | Interest Business and Finance Business Business Utilities + 295 268 Interest | Business and Finance | Economy | Interest Business and Finance Economy + 296 295 Interest | Business and Finance | Commodities | Interest Business and Finance Economy Commodities + 297 295 Interest | Business and Finance | Currencies | Interest Business and Finance Economy Currencies + 298 295 Interest | Business and Finance | Financial Crisis | Interest Business and Finance Economy Financial Crisis + 299 295 Interest | Business and Finance | Financial Reform | Interest Business and Finance Economy Financial Reform + 300 295 Interest | Business and Finance | Financial Regulation | Interest Business and Finance Economy Financial Regulation + 301 295 Interest | Business and Finance | Gasoline Prices | Interest Business and Finance Economy Gasoline Prices + 302 295 Interest | Business and Finance | Housing Market | Interest Business and Finance Economy Housing Market + 303 295 Interest | Business and Finance | Interest Rates | Interest Business and Finance Economy Interest Rates + 304 295 Interest | Business and Finance | Job Market | Interest Business and Finance Economy Job Market + 305 268 Interest | Business and Finance | Industries | Interest Business and Finance Industries + 306 305 Interest | Business and Finance | Advertising Industry | Interest Business and Finance Industries Advertising Industry + 307 305 Interest | Business and Finance | Education industry | Interest Business and Finance Industries Education industry + 308 305 Interest | Business and Finance | Entertainment Industry | Interest Business and Finance Industries Entertainment Industry + 309 305 Interest | Business and Finance | Environmental Services Industry | Interest Business and Finance Industries Environmental Services Industry + 310 305 Interest | Business and Finance | Financial Industry | Interest Business and Finance Industries Financial Industry + 311 305 Interest | Business and Finance | Food Industry | Interest Business and Finance Industries Food Industry + 312 305 Interest | Business and Finance | Healthcare Industry | Interest Business and Finance Industries Healthcare Industry + 313 305 Interest | Business and Finance | Hospitality Industry | Interest Business and Finance Industries Hospitality Industry + 314 305 Interest | Business and Finance | Information Services Industry | Interest Business and Finance Industries Information Services Industry + 315 305 Interest | Business and Finance | Legal Services Industry | Interest Business and Finance Industries Legal Services Industry + 316 305 Interest | Business and Finance | Logistics and Transportation Industry | Interest Business and Finance Industries Logistics and Transportation Industry + 317 305 Interest | Business and Finance | Agriculture | Interest Business and Finance Industries Agriculture + 318 305 Interest | Business and Finance | Management Consulting Industry | Interest Business and Finance Industries Management Consulting Industry + 319 305 Interest | Business and Finance | Manufacturing Industry | Interest Business and Finance Industries Manufacturing Industry + 320 305 Interest | Business and Finance | Mechanical and Industrial Engineering Industry | Interest Business and Finance Industries Mechanical and Industrial Engineering Industry + 321 305 Interest | Business and Finance | Media Industry | Interest Business and Finance Industries Media Industry + 322 305 Interest | Business and Finance | Metals Industry | Interest Business and Finance Industries Metals Industry + 323 305 Interest | Business and Finance | Non-Profit Organizations | Interest Business and Finance Industries Non-Profit Organizations + 324 305 Interest | Business and Finance | Pharmaceutical Industry | Interest Business and Finance Industries Pharmaceutical Industry + 325 305 Interest | Business and Finance | Power and Energy Industry | Interest Business and Finance Industries Power and Energy Industry + 326 305 Interest | Business and Finance | Publishing Industry | Interest Business and Finance Industries Publishing Industry + 327 305 Interest | Business and Finance | Real Estate Industry | Interest Business and Finance Industries Real Estate Industry + 328 305 Interest | Business and Finance | Apparel Industry | Interest Business and Finance Industries Apparel Industry + 329 305 Interest | Business and Finance | Retail Industry | Interest Business and Finance Industries Retail Industry + 330 305 Interest | Business and Finance | Technology Industry | Interest Business and Finance Industries Technology Industry + 331 305 Interest | Business and Finance | Telecommunications Industry | Interest Business and Finance Industries Telecommunications Industry + 332 305 Interest | Business and Finance | Automotive Industry | Interest Business and Finance Industries Automotive Industry + 333 305 Interest | Business and Finance | Aviation Industry | Interest Business and Finance Industries Aviation Industry + 334 305 Interest | Business and Finance | Biotech and Biomedical Industry | Interest Business and Finance Industries Biotech and Biomedical Industry + 335 305 Interest | Business and Finance | Civil Engineering Industry | Interest Business and Finance Industries Civil Engineering Industry + 336 305 Interest | Business and Finance | Construction Industry | Interest Business and Finance Industries Construction Industry + 337 305 Interest | Business and Finance | Defense Industry | Interest Business and Finance Industries Defense Industry + 338 206 Interest | Careers Interest Careers + 339 338 Interest | Careers | Apprenticeships | Interest Careers Apprenticeships + 340 338 Interest | Careers | Career Advice | Interest Careers Career Advice + 341 338 Interest | Careers | Career Planning | Interest Careers Career Planning + 342 338 Interest | Careers | Remote Working | Interest Careers Remote Working + 343 338 Interest | Careers | Vocational Training | Interest Careers Vocational Training + 344 206 Interest | Education Interest Education + 345 344 Interest | Education | Adult Education | Interest Education Adult Education + 346 344 Interest | Education | Language Learning | Interest Education Language Learning + 347 344 Interest | Education | Online Education | Interest Education Online Education + 348 206 Interest | Family and Relationships Interest Family and Relationships + 350 348 Interest | Family and Relationships | Parenting | Interest Family and Relationships Parenting + 352 350 Interest | Family and Relationships | Daycare and Pre-School | Interest Family and Relationships Parenting Daycare and Pre-School + 353 350 Interest | Family and Relationships | Internet Safety | Interest Family and Relationships Parenting Internet Safety + 354 350 Interest | Family and Relationships | Parenting Babies and Toddlers | Interest Family and Relationships Parenting Parenting Babies and Toddlers + 355 350 Interest | Family and Relationships | Parenting Children Aged 4-11 | Interest Family and Relationships Parenting Parenting Children Aged 4-11 + 356 350 Interest | Family and Relationships | Parenting Teens | Interest Family and Relationships Parenting Parenting Teens + 359 206 Interest | Fine Art Interest Fine Art + 360 359 Interest | Fine Art | Costume | Interest Fine Art Costume + 361 359 Interest | Fine Art | Dance | Interest Fine Art Dance + 362 359 Interest | Fine Art | Design | Interest Fine Art Design + 363 359 Interest | Fine Art | Digital Arts | Interest Fine Art Digital Arts + 364 359 Interest | Fine Art | Fine Art Photography | Interest Fine Art Fine Art Photography + 365 359 Interest | Fine Art | Modern Art | Interest Fine Art Modern Art + 366 359 Interest | Fine Art | Opera | Interest Fine Art Opera + 367 359 Interest | Fine Art | Theater | Interest Fine Art Theater + 368 206 Interest | Food & Drink Interest Food & Drink + 369 368 Interest | Food & Drink | Alcoholic Beverages | Interest Food & Drink Alcoholic Beverages + 370 368 Interest | Food & Drink | Barbecues and Grilling | Interest Food & Drink Barbecues and Grilling + 371 368 Interest | Food & Drink | Cooking | Interest Food & Drink Cooking + 372 368 Interest | Food & Drink | Desserts and Baking | Interest Food & Drink Desserts and Baking + 373 368 Interest | Food & Drink | Dining Out | Interest Food & Drink Dining Out + 374 368 Interest | Food & Drink | Food Allergies | Interest Food & Drink Food Allergies + 375 368 Interest | Food & Drink | Food Movements | Interest Food & Drink Food Movements + 376 368 Interest | Food & Drink | Healthy Cooking and Eating | Interest Food & Drink Healthy Cooking and Eating + 377 368 Interest | Food & Drink | Non-Alcoholic Beverages | Interest Food & Drink Non-Alcoholic Beverages + 378 368 Interest | Food & Drink | Vegan Diets | Interest Food & Drink Vegan Diets + 379 368 Interest | Food & Drink | Vegetarian Diets | Interest Food & Drink Vegetarian Diets + 380 368 Interest | Food & Drink | World Cuisines | Interest Food & Drink World Cuisines + 381 206 Interest | Health and Medical Services Interest Health and Medical Services + 382 381 Interest | Health and Medical Services | Health & Pharma | Interest Health and Medical Services Health & Pharma + 383 382 Interest | Health and Medical Services | Medical Services | Interest Health and Medical Services Health & Pharma Medical Services + 385 383 Interest | Health and Medical Services | Health Services | Interest Health and Medical Services Health & Pharma Medical Services Health Services + 386 383 Interest | Health and Medical Services | Health Care and Physicians | Interest Health and Medical Services Health & Pharma Medical Services Health Care and Physicians + 387 383 Interest | Health and Medical Services | Alternative and Natural Medicine | Interest Health and Medical Services Health & Pharma Medical Services Alternative and Natural Medicine + 388 383 Interest | Health and Medical Services | Cosmetic Medical Services | Interest Health and Medical Services Health & Pharma Medical Services Cosmetic Medical Services + 389 383 Interest | Health and Medical Services | Drugstores and Pharmacies | Interest Health and Medical Services Health & Pharma Medical Services Drugstores and Pharmacies + 390 383 Interest | Health and Medical Services | Elder Care | Interest Health and Medical Services Health & Pharma Medical Services Elder Care + 391 383 Interest | Health and Medical Services | Vision Care | Interest Health and Medical Services Health & Pharma Medical Services Vision Care + 392 383 Interest | Health and Medical Services | Dental Care | Interest Health and Medical Services Health & Pharma Medical Services Dental Care + 393 383 Interest | Health and Medical Services | Massage Therapists | Interest Health and Medical Services Health & Pharma Medical Services Massage Therapists + 394 383 Interest | Health and Medical Services | Physical Therapists | Interest Health and Medical Services Health & Pharma Medical Services Physical Therapists + 395 383 Interest | Health and Medical Services | Chiropractors | Interest Health and Medical Services Health & Pharma Medical Services Chiropractors + 397 383 Interest | Health and Medical Services | Hospitals | Interest Health and Medical Services Health & Pharma Medical Services Hospitals + 398 383 Interest | Health and Medical Services | Skin Care Treatments | Interest Health and Medical Services Health & Pharma Medical Services Skin Care Treatments + 399 383 Interest | Health and Medical Services | Smoking Cessation | Interest Health and Medical Services Health & Pharma Medical Services Smoking Cessation + 400 383 Interest | Health and Medical Services | Clinical Research | Interest Health and Medical Services Health & Pharma Medical Services Clinical Research + 401 383 Interest | Health and Medical Services | Hair Loss Treatments | Interest Health and Medical Services Health & Pharma Medical Services Hair Loss Treatments + 404 383 Interest | Health and Medical Services | Vaccines | Interest Health and Medical Services Health & Pharma Medical Services Vaccines + 406 206 Interest | Healthy Living Interest Healthy Living + 407 406 Interest | Healthy Living | Children's Health | Interest Healthy Living Children's Health + 408 406 Interest | Healthy Living | Fitness and Exercise | Interest Healthy Living Fitness and Exercise + 409 408 Interest | Healthy Living | Participant Sports | Interest Healthy Living Fitness and Exercise Participant Sports + 410 408 Interest | Healthy Living | Running and Jogging | Interest Healthy Living Fitness and Exercise Running and Jogging + 411 406 Interest | Healthy Living | Men's Health | Interest Healthy Living Men's Health + 412 406 Interest | Healthy Living | Nutrition | Interest Healthy Living Nutrition + 413 406 Interest | Healthy Living | Senior Health | Interest Healthy Living Senior Health + 414 406 Interest | Healthy Living | Weight Loss | Interest Healthy Living Weight Loss + 415 406 Interest | Healthy Living | Wellness | Interest Healthy Living Wellness + 416 415 Interest | Healthy Living | Alternative Medicine | Interest Healthy Living Wellness Alternative Medicine + 417 416 Interest | Healthy Living | Herbs and Supplements | Interest Healthy Living Wellness Alternative Medicine Herbs and Supplements + 418 416 Interest | Healthy Living | Holistic Health | Interest Healthy Living Wellness Alternative Medicine Holistic Health + 419 415 Interest | Healthy Living | Physical Therapy | Interest Healthy Living Wellness Physical Therapy + 420 415 Interest | Healthy Living | Smoking Cessation | Interest Healthy Living Wellness Smoking Cessation + 421 406 Interest | Healthy Living | Women's Health | Interest Healthy Living Women's Health + 422 206 Interest | Hobbies & Interests Interest Hobbies & Interests + 423 422 Interest | Hobbies & Interests | Antiquing and Antiques | Interest Hobbies & Interests Antiquing and Antiques + 424 422 Interest | Hobbies & Interests | Arts and Crafts | Interest Hobbies & Interests Arts and Crafts + 425 424 Interest | Hobbies & Interests | Beadwork | Interest Hobbies & Interests Arts and Crafts Beadwork + 426 424 Interest | Hobbies & Interests | Candle and Soap Making | Interest Hobbies & Interests Arts and Crafts Candle and Soap Making + 427 424 Interest | Hobbies & Interests | Drawing and Sketching | Interest Hobbies & Interests Arts and Crafts Drawing and Sketching + 428 424 Interest | Hobbies & Interests | Jewelry Making | Interest Hobbies & Interests Arts and Crafts Jewelry Making + 429 424 Interest | Hobbies & Interests | Needlework | Interest Hobbies & Interests Arts and Crafts Needlework + 430 424 Interest | Hobbies & Interests | Painting | Interest Hobbies & Interests Arts and Crafts Painting + 431 424 Interest | Hobbies & Interests | Photography | Interest Hobbies & Interests Arts and Crafts Photography + 432 424 Interest | Hobbies & Interests | Scrapbooking | Interest Hobbies & Interests Arts and Crafts Scrapbooking + 433 424 Interest | Hobbies & Interests | Woodworking | Interest Hobbies & Interests Arts and Crafts Woodworking + 434 422 Interest | Hobbies & Interests | Beekeeping | Interest Hobbies & Interests Beekeeping + 435 422 Interest | Hobbies & Interests | Birdwatching | Interest Hobbies & Interests Birdwatching + 436 422 Interest | Hobbies & Interests | Cigars | Interest Hobbies & Interests Cigars + 437 422 Interest | Hobbies & Interests | Collecting | Interest Hobbies & Interests Collecting + 438 437 Interest | Hobbies & Interests | Comic Books | Interest Hobbies & Interests Collecting Comic Books + 439 437 Interest | Hobbies & Interests | Stamps and Coins | Interest Hobbies & Interests Collecting Stamps and Coins + 440 422 Interest | Hobbies & Interests | Content Production | Interest Hobbies & Interests Content Production + 441 440 Interest | Hobbies & Interests | Audio Production | Interest Hobbies & Interests Content Production Audio Production + 442 440 Interest | Hobbies & Interests | Freelance Writing | Interest Hobbies & Interests Content Production Freelance Writing + 443 440 Interest | Hobbies & Interests | Screenwriting | Interest Hobbies & Interests Content Production Screenwriting + 444 440 Interest | Hobbies & Interests | Video Production | Interest Hobbies & Interests Content Production Video Production + 445 422 Interest | Hobbies & Interests | Games and Puzzles | Interest Hobbies & Interests Games and Puzzles + 446 445 Interest | Hobbies & Interests | Board Games and Puzzles | Interest Hobbies & Interests Games and Puzzles Board Games and Puzzles + 447 445 Interest | Hobbies & Interests | Card Games | Interest Hobbies & Interests Games and Puzzles Card Games + 448 445 Interest | Hobbies & Interests | Roleplaying Games | Interest Hobbies & Interests Games and Puzzles Roleplaying Games + 449 422 Interest | Hobbies & Interests | Genealogy and Ancestry | Interest Hobbies & Interests Genealogy and Ancestry + 450 422 Interest | Hobbies & Interests | Magic and Illusion | Interest Hobbies & Interests Magic and Illusion + 451 422 Interest | Hobbies & Interests | Model Toys | Interest Hobbies & Interests Model Toys + 452 422 Interest | Hobbies & Interests | Musical Instruments | Interest Hobbies & Interests Musical Instruments + 453 422 Interest | Hobbies & Interests | Paranormal Phenomena | Interest Hobbies & Interests Paranormal Phenomena + 454 422 Interest | Hobbies & Interests | Radio Control | Interest Hobbies & Interests Radio Control + 455 422 Interest | Hobbies & Interests | Sci-fi and Fantasy | Interest Hobbies & Interests Sci-fi and Fantasy + 456 422 Interest | Hobbies & Interests | Workshops and Classes | Interest Hobbies & Interests Workshops and Classes + 457 206 Interest | Home & Garden Interest Home & Garden + 458 457 Interest | Home & Garden | Gardening | Interest Home & Garden Gardening + 459 457 Interest | Home & Garden | Home Entertaining | Interest Home & Garden Home Entertaining + 460 457 Interest | Home & Garden | Home Improvement | Interest Home & Garden Home Improvement + 461 457 Interest | Home & Garden | Interior Decorating | Interest Home & Garden Interior Decorating + 462 457 Interest | Home & Garden | Landscaping | Interest Home & Garden Landscaping + 463 457 Interest | Home & Garden | Outdoor Decorating | Interest Home & Garden Outdoor Decorating + 464 457 Interest | Home & Garden | Remodeling & Construction | Interest Home & Garden Remodeling & Construction + 465 457 Interest | Home & Garden | Smart Home | Interest Home & Garden Smart Home + 466 206 Interest | Medical Health Interest Medical Health + 467 206 Interest | Movies Interest Movies + 468 467 Interest | Movies | Action and Adventure Movies | Interest Movies Action and Adventure Movies + 469 467 Interest | Movies | Animation Movies | Interest Movies Animation Movies + 470 467 Interest | Movies | Comedy Movies | Interest Movies Comedy Movies + 471 467 Interest | Movies | Crime and Mystery Movies | Interest Movies Crime and Mystery Movies + 472 467 Interest | Movies | Documentary Movies | Interest Movies Documentary Movies + 473 467 Interest | Movies | Drama Movies | Interest Movies Drama Movies + 474 467 Interest | Movies | Family and Children Movies | Interest Movies Family and Children Movies + 475 467 Interest | Movies | Fantasy Movies | Interest Movies Fantasy Movies + 476 467 Interest | Movies | Horror Movies | Interest Movies Horror Movies + 477 467 Interest | Movies | Indie and Arthouse Movies | Interest Movies Indie and Arthouse Movies + 478 467 Interest | Movies | Romance Movies | Interest Movies Romance Movies + 479 467 Interest | Movies | Science Fiction Movies | Interest Movies Science Fiction Movies + 480 467 Interest | Movies | World Movies | Interest Movies World Movies + 481 206 Interest | Music and Audio Interest Music and Audio + 482 481 Interest | Music and Audio | Adult Album Alternative | Interest Music and Audio Adult Album Alternative + 483 481 Interest | Music and Audio | Adult Contemporary Music | Interest Music and Audio Adult Contemporary Music + 484 483 Interest | Music and Audio | Soft AC Music | Interest Music and Audio Adult Contemporary Music Soft AC Music + 485 483 Interest | Music and Audio | Urban AC Music | Interest Music and Audio Adult Contemporary Music Urban AC Music + 486 481 Interest | Music and Audio | Alternative Music | Interest Music and Audio Alternative Music + 487 481 Interest | Music and Audio | Blues | Interest Music and Audio Blues + 488 481 Interest | Music and Audio | Children's Music | Interest Music and Audio Children's Music + 489 481 Interest | Music and Audio | Classic Hits | Interest Music and Audio Classic Hits + 490 481 Interest | Music and Audio | Classical Music | Interest Music and Audio Classical Music + 491 481 Interest | Music and Audio | College Radio | Interest Music and Audio College Radio + 492 481 Interest | Music and Audio | Comedy (Music and Audio) | Interest Music and Audio Comedy (Music and Audio) + 493 481 Interest | Music and Audio | Contemporary Hits/Pop/Top 40 | Interest Music and Audio Contemporary Hits/Pop/Top 40 + 494 481 Interest | Music and Audio | Country Music | Interest Music and Audio Country Music + 495 481 Interest | Music and Audio | Dance and Electronic Music | Interest Music and Audio Dance and Electronic Music + 496 481 Interest | Music and Audio | Gospel Music | Interest Music and Audio Gospel Music + 497 481 Interest | Music and Audio | Hip Hop Music | Interest Music and Audio Hip Hop Music + 498 481 Interest | Music and Audio | Inspirational/New Age Music | Interest Music and Audio Inspirational/New Age Music + 499 481 Interest | Music and Audio | Jazz | Interest Music and Audio Jazz + 500 481 Interest | Music and Audio | Oldies/Adult Standards | Interest Music and Audio Oldies/Adult Standards + 501 481 Interest | Music and Audio | R&B/Soul/Funk | Interest Music and Audio R&B/Soul/Funk + 502 481 Interest | Music and Audio | Reggae | Interest Music and Audio Reggae + 503 481 Interest | Music and Audio | Religious (Music and Audio) | Interest Music and Audio Religious (Music and Audio) + 504 481 Interest | Music and Audio | Rock Music | Interest Music and Audio Rock Music + 505 504 Interest | Music and Audio | Album-oriented Rock | Interest Music and Audio Rock Music Album-oriented Rock + 506 504 Interest | Music and Audio | Alternative Rock | Interest Music and Audio Rock Music Alternative Rock + 507 504 Interest | Music and Audio | Classic Rock | Interest Music and Audio Rock Music Classic Rock + 508 504 Interest | Music and Audio | Hard Rock | Interest Music and Audio Rock Music Hard Rock + 509 504 Interest | Music and Audio | Soft Rock | Interest Music and Audio Rock Music Soft Rock + 510 481 Interest | Music and Audio | Songwriters/Folk | Interest Music and Audio Songwriters/Folk + 511 481 Interest | Music and Audio | Soundtracks, TV and Showtunes | Interest Music and Audio Soundtracks, TV and Showtunes + 512 481 Interest | Music and Audio | Sports Radio | Interest Music and Audio Sports Radio + 513 481 Interest | Music and Audio | Talk Radio | Interest Music and Audio Talk Radio + 514 513 Interest | Music and Audio | Business News Radio | Interest Music and Audio Talk Radio Business News Radio + 515 513 Interest | Music and Audio | Educational Radio | Interest Music and Audio Talk Radio Educational Radio + 516 513 Interest | Music and Audio | News Radio | Interest Music and Audio Talk Radio News Radio + 517 513 Interest | Music and Audio | News/Talk Radio | Interest Music and Audio Talk Radio News/Talk Radio + 518 513 Interest | Music and Audio | Public Radio | Interest Music and Audio Talk Radio Public Radio + 519 481 Interest | Music and Audio | Urban Contemporary Music | Interest Music and Audio Urban Contemporary Music + 520 481 Interest | Music and Audio | Variety (Music and Audio) | Interest Music and Audio Variety (Music and Audio) + 521 481 Interest | Music and Audio | World/International Music | Interest Music and Audio World/International Music + 522 206 Interest | News and Politics Interest News and Politics + 523 522 Interest | News and Politics | Crime | Interest News and Politics Crime + 524 522 Interest | News and Politics | Disasters | Interest News and Politics Disasters + 525 522 Interest | News and Politics | International News | Interest News and Politics International News + 526 522 Interest | News and Politics | Law | Interest News and Politics Law + 527 522 Interest | News and Politics | Local News | Interest News and Politics Local News + 528 522 Interest | News and Politics | National News | Interest News and Politics National News + 529 522 Interest | News and Politics | Politics | Interest News and Politics Politics + 530 529 Interest | News and Politics | Elections | Interest News and Politics Politics Elections + 531 529 Interest | News and Politics | Political Issues | Interest News and Politics Politics Political Issues + 532 529 Interest | News and Politics | War and Conflicts | Interest News and Politics Politics War and Conflicts + 533 522 Interest | News and Politics | Weather | Interest News and Politics Weather + 534 206 Interest | Personal Finance Interest Personal Finance + 535 534 Interest | Personal Finance | Frugal Living | Interest Personal Finance Frugal Living + 536 534 Interest | Personal Finance | Insurance | Interest Personal Finance Insurance + 537 534 Interest | Personal Finance | Personal Debt | Interest Personal Finance Personal Debt + 538 534 Interest | Personal Finance | Personal Investing | Interest Personal Finance Personal Investing + 539 534 Interest | Personal Finance | Personal Taxes | Interest Personal Finance Personal Taxes + 540 534 Interest | Personal Finance | Retirement Planning | Interest Personal Finance Retirement Planning + 541 206 Interest | Pets Interest Pets + 542 541 Interest | Pets | Birds | Interest Pets Birds + 543 541 Interest | Pets | Cats | Interest Pets Cats + 544 541 Interest | Pets | Dogs | Interest Pets Dogs + 545 541 Interest | Pets | Fish and Aquariums | Interest Pets Fish and Aquariums + 546 541 Interest | Pets | Horses and Equine | Interest Pets Horses and Equine + 547 541 Interest | Pets | Large Animals | Interest Pets Large Animals + 548 541 Interest | Pets | Pet Adoptions | Interest Pets Pet Adoptions + 549 541 Interest | Pets | Reptiles | Interest Pets Reptiles + 550 206 Interest | Pharmaceuticals, Conditions, and Symptoms Interest Pharmaceuticals, Conditions, and Symptoms + 551 550 Interest | Pharmaceuticals, Conditions, and Symptoms | Health & Pharma | Interest Pharmaceuticals, Conditions, and Symptoms Health & Pharma + 581 206 Interest | Pop Culture Interest Pop Culture + 582 581 Interest | Pop Culture | Humor and Satire | Interest Pop Culture Humor and Satire + 583 206 Interest | Real Estate Interest Real Estate + 584 583 Interest | Real Estate | Apartments | Interest Real Estate Apartments + 585 583 Interest | Real Estate | Developmental Sites | Interest Real Estate Developmental Sites + 586 583 Interest | Real Estate | Hotel Properties | Interest Real Estate Hotel Properties + 587 583 Interest | Real Estate | Houses | Interest Real Estate Houses + 588 583 Interest | Real Estate | Industrial Property | Interest Real Estate Industrial Property + 589 583 Interest | Real Estate | Land and Farms | Interest Real Estate Land and Farms + 590 583 Interest | Real Estate | Office Property | Interest Real Estate Office Property + 591 583 Interest | Real Estate | Real Estate Buying and Selling | Interest Real Estate Real Estate Buying and Selling + 592 583 Interest | Real Estate | Real Estate Renting and Leasing | Interest Real Estate Real Estate Renting and Leasing + 593 583 Interest | Real Estate | Retail Property | Interest Real Estate Retail Property + 594 583 Interest | Real Estate | Vacation Properties | Interest Real Estate Vacation Properties + 606 206 Interest | Shopping Interest Shopping + 607 206 Interest | Sports Interest Sports + 608 607 Interest | Sports | American Football | Interest Sports American Football + 609 607 Interest | Sports | Australian Rules Football | Interest Sports Australian Rules Football + 610 607 Interest | Sports | Auto Racing | Interest Sports Auto Racing + 611 610 Interest | Sports | Motorcycle Sports | Interest Sports Auto Racing Motorcycle Sports + 612 607 Interest | Sports | Badminton | Interest Sports Badminton + 613 607 Interest | Sports | Baseball | Interest Sports Baseball + 614 607 Interest | Sports | Basketball | Interest Sports Basketball + 615 607 Interest | Sports | Beach Volleyball | Interest Sports Beach Volleyball + 616 607 Interest | Sports | Bodybuilding | Interest Sports Bodybuilding + 617 607 Interest | Sports | Bowling | Interest Sports Bowling + 618 607 Interest | Sports | Boxing | Interest Sports Boxing + 619 607 Interest | Sports | Cheerleading | Interest Sports Cheerleading + 620 607 Interest | Sports | College Sports | Interest Sports College Sports + 621 620 Interest | Sports | College Football | Interest Sports College Sports College Football + 622 620 Interest | Sports | College Basketball | Interest Sports College Sports College Basketball + 623 620 Interest | Sports | College Baseball | Interest Sports College Sports College Baseball + 624 607 Interest | Sports | Cricket | Interest Sports Cricket + 625 607 Interest | Sports | Cycling | Interest Sports Cycling + 626 607 Interest | Sports | Darts | Interest Sports Darts + 627 607 Interest | Sports | Disabled Sports | Interest Sports Disabled Sports + 628 607 Interest | Sports | Diving | Interest Sports Diving + 629 607 Interest | Sports | Equine Sports | Interest Sports Equine Sports + 630 629 Interest | Sports | Horse Racing | Interest Sports Equine Sports Horse Racing + 631 607 Interest | Sports | Extreme Sports | Interest Sports Extreme Sports + 632 631 Interest | Sports | Canoeing and Kayaking | Interest Sports Extreme Sports Canoeing and Kayaking + 633 631 Interest | Sports | Climbing | Interest Sports Extreme Sports Climbing + 634 631 Interest | Sports | Paintball | Interest Sports Extreme Sports Paintball + 635 631 Interest | Sports | Scuba Diving | Interest Sports Extreme Sports Scuba Diving + 636 631 Interest | Sports | Skateboarding | Interest Sports Extreme Sports Skateboarding + 637 631 Interest | Sports | Snowboarding | Interest Sports Extreme Sports Snowboarding + 638 631 Interest | Sports | Surfing and Bodyboarding | Interest Sports Extreme Sports Surfing and Bodyboarding + 639 631 Interest | Sports | Waterskiing and Wakeboarding | Interest Sports Extreme Sports Waterskiing and Wakeboarding + 640 607 Interest | Sports | Fantasy Sports | Interest Sports Fantasy Sports + 641 607 Interest | Sports | Field Hockey | Interest Sports Field Hockey + 642 607 Interest | Sports | Figure Skating | Interest Sports Figure Skating + 643 607 Interest | Sports | Fishing Sports | Interest Sports Fishing Sports + 644 607 Interest | Sports | Golf | Interest Sports Golf + 645 607 Interest | Sports | Gymnastics | Interest Sports Gymnastics + 646 607 Interest | Sports | Hunting and Shooting | Interest Sports Hunting and Shooting + 647 607 Interest | Sports | Ice Hockey | Interest Sports Ice Hockey + 648 607 Interest | Sports | Inline Skating | Interest Sports Inline Skating + 649 607 Interest | Sports | Lacrosse | Interest Sports Lacrosse + 650 607 Interest | Sports | Martial Arts | Interest Sports Martial Arts + 651 607 Interest | Sports | Olympic Sports | Interest Sports Olympic Sports + 652 651 Interest | Sports | Summer Olympic Sports | Interest Sports Olympic Sports Summer Olympic Sports + 653 651 Interest | Sports | Winter Olympic Sports | Interest Sports Olympic Sports Winter Olympic Sports + 654 607 Interest | Sports | Poker and Professional Gambling | Interest Sports Poker and Professional Gambling + 655 607 Interest | Sports | Rodeo | Interest Sports Rodeo + 656 607 Interest | Sports | Rowing | Interest Sports Rowing + 657 607 Interest | Sports | Rugby | Interest Sports Rugby + 658 657 Interest | Sports | Rugby League | Interest Sports Rugby Rugby League + 659 657 Interest | Sports | Rugby Union | Interest Sports Rugby Rugby Union + 660 607 Interest | Sports | Sailing | Interest Sports Sailing + 661 607 Interest | Sports | Skiing | Interest Sports Skiing + 662 607 Interest | Sports | Snooker/Pool/Billiards | Interest Sports Snooker/Pool/Billiards + 663 607 Interest | Sports | Soccer | Interest Sports Soccer + 664 607 Interest | Sports | Softball | Interest Sports Softball + 665 607 Interest | Sports | Sports Equipment | Interest Sports Sports Equipment + 666 607 Interest | Sports | Squash | Interest Sports Squash + 667 607 Interest | Sports | Swimming | Interest Sports Swimming + 668 607 Interest | Sports | Table Tennis | Interest Sports Table Tennis + 669 607 Interest | Sports | Tennis | Interest Sports Tennis + 670 607 Interest | Sports | Track and Field | Interest Sports Track and Field + 671 607 Interest | Sports | Volleyball | Interest Sports Volleyball + 672 607 Interest | Sports | Walking | Interest Sports Walking + 673 607 Interest | Sports | Water Polo | Interest Sports Water Polo + 674 607 Interest | Sports | Weightlifting | Interest Sports Weightlifting + 675 607 Interest | Sports | Wrestling | Interest Sports Wrestling + 676 206 Interest | Style & Fashion Interest Style & Fashion + 677 676 Interest | Style & Fashion | Beauty | Interest Style & Fashion Beauty + 678 676 Interest | Style & Fashion | Body Art | Interest Style & Fashion Body Art + 679 676 Interest | Style & Fashion | Children's Clothing | Interest Style & Fashion Children's Clothing + 680 676 Interest | Style & Fashion | Designer Clothing | Interest Style & Fashion Designer Clothing + 681 676 Interest | Style & Fashion | Fashion Trends | Interest Style & Fashion Fashion Trends + 682 676 Interest | Style & Fashion | High Fashion | Interest Style & Fashion High Fashion + 683 676 Interest | Style & Fashion | Men's Fashion | Interest Style & Fashion Men's Fashion + 684 676 Interest | Style & Fashion | Personal Care | Interest Style & Fashion Personal Care + 685 676 Interest | Style & Fashion | Street Style | Interest Style & Fashion Street Style + 686 676 Interest | Style & Fashion | Women's Fashion | Interest Style & Fashion Women's Fashion + 687 206 Interest | Technology & Computing Interest Technology & Computing + 688 687 Interest | Technology & Computing | Artificial Intelligence | Interest Technology & Computing Artificial Intelligence + 689 687 Interest | Technology & Computing | Augmented Reality | Interest Technology & Computing Augmented Reality + 690 687 Interest | Technology & Computing | Computing | Interest Technology & Computing Computing + 691 690 Interest | Technology & Computing | Internet | Interest Technology & Computing Computing Internet + 692 690 Interest | Technology & Computing | Cloud Computing | Interest Technology & Computing Computing Cloud Computing + 693 690 Interest | Technology & Computing | Web Development | Interest Technology & Computing Computing Web Development + 694 690 Interest | Technology & Computing | Web Hosting | Interest Technology & Computing Computing Web Hosting + 695 690 Interest | Technology & Computing | Email | Interest Technology & Computing Computing Email + 696 690 Interest | Technology & Computing | Internet for Beginners | Interest Technology & Computing Computing Internet for Beginners + 697 690 Interest | Technology & Computing | Internet of Things | Interest Technology & Computing Computing Internet of Things + 698 690 Interest | Technology & Computing | IT and Internet Support | Interest Technology & Computing Computing IT and Internet Support + 699 690 Interest | Technology & Computing | Search | Interest Technology & Computing Computing Search + 700 690 Interest | Technology & Computing | Social Networking | Interest Technology & Computing Computing Social Networking + 701 690 Interest | Technology & Computing | Web Design and HTML | Interest Technology & Computing Computing Web Design and HTML + 702 690 Interest | Technology & Computing | Programming Languages | Interest Technology & Computing Computing Programming Languages + 703 687 Interest | Technology & Computing | Consumer Electronics | Interest Technology & Computing Consumer Electronics + 704 687 Interest | Technology & Computing | Robotics | Interest Technology & Computing Robotics + 705 687 Interest | Technology & Computing | Virtual Reality | Interest Technology & Computing Virtual Reality + 706 206 Interest | Television Interest Television + 707 706 Interest | Television | Animation TV | Interest Television Animation TV + 708 706 Interest | Television | Children's TV | Interest Television Children's TV + 709 706 Interest | Television | Comedy TV | Interest Television Comedy TV + 710 706 Interest | Television | Drama TV | Interest Television Drama TV + 711 706 Interest | Television | Factual TV | Interest Television Factual TV + 712 706 Interest | Television | Holiday TV | Interest Television Holiday TV + 713 706 Interest | Television | Music TV | Interest Television Music TV + 714 706 Interest | Television | Reality TV | Interest Television Reality TV + 715 706 Interest | Television | Science Fiction TV | Interest Television Science Fiction TV + 716 706 Interest | Television | Soap Opera TV | Interest Television Soap Opera TV + 717 706 Interest | Television | Special Interest TV | Interest Television Special Interest TV + 718 706 Interest | Television | Sports TV | Interest Television Sports TV + 719 206 Interest | Travel Interest Travel + 720 719 Interest | Travel | Adventure Travel | Interest Travel Adventure Travel + 721 719 Interest | Travel | Africa Travel | Interest Travel Africa Travel + 722 719 Interest | Travel | Asia Travel | Interest Travel Asia Travel + 723 719 Interest | Travel | Australia and Oceania Travel | Interest Travel Australia and Oceania Travel + 724 719 Interest | Travel | Beach Travel | Interest Travel Beach Travel + 725 719 Interest | Travel | Camping | Interest Travel Camping + 726 719 Interest | Travel | Day Trips | Interest Travel Day Trips + 727 719 Interest | Travel | Europe Travel | Interest Travel Europe Travel + 728 719 Interest | Travel | Family Travel | Interest Travel Family Travel + 729 719 Interest | Travel | North America Travel | Interest Travel North America Travel + 730 719 Interest | Travel | Polar Travel | Interest Travel Polar Travel + 731 719 Interest | Travel | Road Trips | Interest Travel Road Trips + 732 719 Interest | Travel | South America Travel | Interest Travel South America Travel + 733 206 Interest | Video Gaming Interest Video Gaming + 734 733 Interest | Video Gaming | Console Games | Interest Video Gaming Console Games + 735 733 Interest | Video Gaming | eSports | Interest Video Gaming eSports + 736 733 Interest | Video Gaming | Mobile Games | Interest Video Gaming Mobile Games + 737 733 Interest | Video Gaming | PC Games | Interest Video Gaming PC Games + 738 733 Interest | Video Gaming | Video Game Genres | Interest Video Gaming Video Game Genres + 739 738 Interest | Video Gaming | Action Video Games | Interest Video Gaming Video Game Genres Action Video Games + 740 738 Interest | Video Gaming | Role-Playing Video Games | Interest Video Gaming Video Game Genres Role-Playing Video Games + 741 738 Interest | Video Gaming | Simulation Video Games | Interest Video Gaming Video Game Genres Simulation Video Games + 742 738 Interest | Video Gaming | Sports Video Games | Interest Video Gaming Video Game Genres Sports Video Games + 743 738 Interest | Video Gaming | Strategy Video Games | Interest Video Gaming Video Game Genres Strategy Video Games + 744 738 Interest | Video Gaming | Action-Adventure Video Games | Interest Video Gaming Video Game Genres Action-Adventure Video Games + 745 738 Interest | Video Gaming | Adventure Video Games | Interest Video Gaming Video Game Genres Adventure Video Games + 746 738 Interest | Video Gaming | Casual Games | Interest Video Gaming Video Game Genres Casual Games + 747 738 Interest | Video Gaming | Educational Video Games | Interest Video Gaming Video Game Genres Educational Video Games + 748 738 Interest | Video Gaming | Exercise and Fitness Video Games | Interest Video Gaming Video Game Genres Exercise and Fitness Video Games + 749 738 Interest | Video Gaming | MMOs | Interest Video Gaming Video Game Genres MMOs + 750 738 Interest | Video Gaming | Music and Party Video Games | Interest Video Gaming Video Game Genres Music and Party Video Games + 751 738 Interest | Video Gaming | Puzzle Video Games | Interest Video Gaming Video Game Genres Puzzle Video Games + 752 Purchase Intent* Purchase Intent* See *Purchase Intent Classification* Extension + 753 752 Purchase Intent* | Apps Purchase Intent* Apps See *Purchase Intent Classification* Extension + 754 753 Purchase Intent* | Apps | Auto and Vehicles Apps | Purchase Intent* Apps Auto and Vehicles Apps See *Purchase Intent Classification* Extension + 755 753 Purchase Intent* | Apps | Books Apps | Purchase Intent* Apps Books Apps See *Purchase Intent Classification* Extension + 756 753 Purchase Intent* | Apps | Business Apps | Purchase Intent* Apps Business Apps See *Purchase Intent Classification* Extension + 757 753 Purchase Intent* | Apps | Education Apps | Purchase Intent* Apps Education Apps See *Purchase Intent Classification* Extension + 758 753 Purchase Intent* | Apps | Entertainment Apps | Purchase Intent* Apps Entertainment Apps See *Purchase Intent Classification* Extension + 759 753 Purchase Intent* | Apps | Finance Apps | Purchase Intent* Apps Finance Apps See *Purchase Intent Classification* Extension + 760 753 Purchase Intent* | Apps | Food and Drink Apps | Purchase Intent* Apps Food and Drink Apps See *Purchase Intent Classification* Extension + 761 753 Purchase Intent* | Apps | Games Apps | Purchase Intent* Apps Games Apps See *Purchase Intent Classification* Extension + 762 753 Purchase Intent* | Apps | Health and Fitness Apps | Purchase Intent* Apps Health and Fitness Apps See *Purchase Intent Classification* Extension + 763 753 Purchase Intent* | Apps | Lifestyle Apps | Purchase Intent* Apps Lifestyle Apps See *Purchase Intent Classification* Extension + 764 753 Purchase Intent* | Apps | Magazine and Newspapers Apps | Purchase Intent* Apps Magazine and Newspapers Apps See *Purchase Intent Classification* Extension + 765 753 Purchase Intent* | Apps | Medical Apps | Purchase Intent* Apps Medical Apps See *Purchase Intent Classification* Extension + 766 753 Purchase Intent* | Apps | Music Apps | Purchase Intent* Apps Music Apps See *Purchase Intent Classification* Extension + 767 753 Purchase Intent* | Apps | Navigation Apps | Purchase Intent* Apps Navigation Apps See *Purchase Intent Classification* Extension + 768 753 Purchase Intent* | Apps | News Apps | Purchase Intent* Apps News Apps See *Purchase Intent Classification* Extension + 769 753 Purchase Intent* | Apps | Photo and Video Apps | Purchase Intent* Apps Photo and Video Apps See *Purchase Intent Classification* Extension + 770 753 Purchase Intent* | Apps | Productivity Apps | Purchase Intent* Apps Productivity Apps See *Purchase Intent Classification* Extension + 771 753 Purchase Intent* | Apps | Reference Apps | Purchase Intent* Apps Reference Apps See *Purchase Intent Classification* Extension + 772 753 Purchase Intent* | Apps | Search Engine Apps | Purchase Intent* Apps Search Engine Apps See *Purchase Intent Classification* Extension + 773 753 Purchase Intent* | Apps | Shopping Apps | Purchase Intent* Apps Shopping Apps See *Purchase Intent Classification* Extension + 774 753 Purchase Intent* | Apps | Social Networking Apps | Purchase Intent* Apps Social Networking Apps See *Purchase Intent Classification* Extension + 775 753 Purchase Intent* | Apps | Sports Apps | Purchase Intent* Apps Sports Apps See *Purchase Intent Classification* Extension + 776 753 Purchase Intent* | Apps | Travel Apps | Purchase Intent* Apps Travel Apps See *Purchase Intent Classification* Extension + 777 753 Purchase Intent* | Apps | Utilities Apps | Purchase Intent* Apps Utilities Apps See *Purchase Intent Classification* Extension + 778 753 Purchase Intent* | Apps | Weather Apps | Purchase Intent* Apps Weather Apps See *Purchase Intent Classification* Extension + 779 752 Purchase Intent* | Arts and Entertainment Purchase Intent* Arts and Entertainment See *Purchase Intent Classification* Extension + 780 779 Purchase Intent* | Arts and Entertainment | Blogs/Forums/Social Networks | Purchase Intent* Arts and Entertainment Blogs/Forums/Social Networks See *Purchase Intent Classification* Extension + 781 779 Purchase Intent* | Arts and Entertainment | Culture and Fine Arts | Purchase Intent* Arts and Entertainment Culture and Fine Arts See *Purchase Intent Classification* Extension + 782 779 Purchase Intent* | Arts and Entertainment | Experiences and Events | Purchase Intent* Arts and Entertainment Experiences and Events See *Purchase Intent Classification* Extension + 783 782 Purchase Intent* | Arts and Entertainment | Concerts | Purchase Intent* Arts and Entertainment Experiences and Events Concerts See *Purchase Intent Classification* Extension + 784 782 Purchase Intent* | Arts and Entertainment | Theatre and Musicals | Purchase Intent* Arts and Entertainment Experiences and Events Theatre and Musicals See *Purchase Intent Classification* Extension + 785 782 Purchase Intent* | Arts and Entertainment | Museums and Galleries | Purchase Intent* Arts and Entertainment Experiences and Events Museums and Galleries See *Purchase Intent Classification* Extension + 786 782 Purchase Intent* | Arts and Entertainment | Sporting Events | Purchase Intent* Arts and Entertainment Experiences and Events Sporting Events See *Purchase Intent Classification* Extension + 787 782 Purchase Intent* | Arts and Entertainment | Cinemas and Movie Events | Purchase Intent* Arts and Entertainment Experiences and Events Cinemas and Movie Events See *Purchase Intent Classification* Extension + 788 782 Purchase Intent* | Arts and Entertainment | Aviation Shows | Purchase Intent* Arts and Entertainment Experiences and Events Aviation Shows See *Purchase Intent Classification* Extension + 789 782 Purchase Intent* | Arts and Entertainment | Fairs and Farmer's Markets | Purchase Intent* Arts and Entertainment Experiences and Events Fairs and Farmer's Markets See *Purchase Intent Classification* Extension + 790 782 Purchase Intent* | Arts and Entertainment | Exhibitions | Purchase Intent* Arts and Entertainment Experiences and Events Exhibitions See *Purchase Intent Classification* Extension + 791 782 Purchase Intent* | Arts and Entertainment | Theme and Amusement Parks | Purchase Intent* Arts and Entertainment Experiences and Events Theme and Amusement Parks See *Purchase Intent Classification* Extension + 792 782 Purchase Intent* | Arts and Entertainment | Parks and Wildlife | Purchase Intent* Arts and Entertainment Experiences and Events Parks and Wildlife See *Purchase Intent Classification* Extension + 793 782 Purchase Intent* | Arts and Entertainment | Auto Shows | Purchase Intent* Arts and Entertainment Experiences and Events Auto Shows See *Purchase Intent Classification* Extension + 794 782 Purchase Intent* | Arts and Entertainment | Nightclubs | Purchase Intent* Arts and Entertainment Experiences and Events Nightclubs See *Purchase Intent Classification* Extension + 795 782 Purchase Intent* | Arts and Entertainment | Fashion Events | Purchase Intent* Arts and Entertainment Experiences and Events Fashion Events See *Purchase Intent Classification* Extension + 796 782 Purchase Intent* | Arts and Entertainment | Comedy Events | Purchase Intent* Arts and Entertainment Experiences and Events Comedy Events See *Purchase Intent Classification* Extension + 797 782 Purchase Intent* | Arts and Entertainment | Zoos and Aquariums | Purchase Intent* Arts and Entertainment Experiences and Events Zoos and Aquariums See *Purchase Intent Classification* Extension + 798 782 Purchase Intent* | Arts and Entertainment | Fan Conventions | Purchase Intent* Arts and Entertainment Experiences and Events Fan Conventions See *Purchase Intent Classification* Extension + 799 779 Purchase Intent* | Arts and Entertainment | Fantasy Sports | Purchase Intent* Arts and Entertainment Fantasy Sports See *Purchase Intent Classification* Extension + 800 779 Purchase Intent* | Arts and Entertainment | Music and Video Streaming Services | Purchase Intent* Arts and Entertainment Music and Video Streaming Services See *Purchase Intent Classification* Extension + 801 779 Purchase Intent* | Arts and Entertainment | Online Entertainment | Purchase Intent* Arts and Entertainment Online Entertainment See *Purchase Intent Classification* Extension + 802 779 Purchase Intent* | Arts and Entertainment | Radio and Podcasts | Purchase Intent* Arts and Entertainment Radio and Podcasts See *Purchase Intent Classification* Extension + 803 779 Purchase Intent* | Arts and Entertainment | Ticket Services | Purchase Intent* Arts and Entertainment Ticket Services See *Purchase Intent Classification* Extension + 804 779 Purchase Intent* | Arts and Entertainment | TV | Purchase Intent* Arts and Entertainment TV See *Purchase Intent Classification* Extension + 805 752 Purchase Intent* | Automotive Ownership Purchase Intent* Automotive Ownership See *Purchase Intent Classification* Extension + 806 805 Purchase Intent* | Automotive Ownership | New Vehicles | Purchase Intent* Automotive Ownership New Vehicles See *Purchase Intent Classification* Extension + 807 806 Purchase Intent* | Automotive Ownership | Commercial Trucks | Purchase Intent* Automotive Ownership New Vehicles Commercial Trucks See *Purchase Intent Classification* Extension + 808 806 Purchase Intent* | Automotive Ownership | Sedan | Purchase Intent* Automotive Ownership New Vehicles Sedan See *Purchase Intent Classification* Extension + 809 806 Purchase Intent* | Automotive Ownership | Station Wagon | Purchase Intent* Automotive Ownership New Vehicles Station Wagon See *Purchase Intent Classification* Extension + 810 806 Purchase Intent* | Automotive Ownership | SUV | Purchase Intent* Automotive Ownership New Vehicles SUV See *Purchase Intent Classification* Extension + 811 806 Purchase Intent* | Automotive Ownership | Van | Purchase Intent* Automotive Ownership New Vehicles Van See *Purchase Intent Classification* Extension + 812 806 Purchase Intent* | Automotive Ownership | Convertible | Purchase Intent* Automotive Ownership New Vehicles Convertible See *Purchase Intent Classification* Extension + 813 806 Purchase Intent* | Automotive Ownership | Coupe | Purchase Intent* Automotive Ownership New Vehicles Coupe See *Purchase Intent Classification* Extension + 814 806 Purchase Intent* | Automotive Ownership | Crossover | Purchase Intent* Automotive Ownership New Vehicles Crossover See *Purchase Intent Classification* Extension + 815 806 Purchase Intent* | Automotive Ownership | Hatchback | Purchase Intent* Automotive Ownership New Vehicles Hatchback See *Purchase Intent Classification* Extension + 816 806 Purchase Intent* | Automotive Ownership | Microcar | Purchase Intent* Automotive Ownership New Vehicles Microcar See *Purchase Intent Classification* Extension + 817 806 Purchase Intent* | Automotive Ownership | Minivan | Purchase Intent* Automotive Ownership New Vehicles Minivan See *Purchase Intent Classification* Extension + 818 806 Purchase Intent* | Automotive Ownership | Off-Road Vehicles | Purchase Intent* Automotive Ownership New Vehicles Off-Road Vehicles See *Purchase Intent Classification* Extension + 819 806 Purchase Intent* | Automotive Ownership | Pickup Trucks | Purchase Intent* Automotive Ownership New Vehicles Pickup Trucks See *Purchase Intent Classification* Extension + 820 806 Purchase Intent* | Automotive Ownership | Budget Cars | Purchase Intent* Automotive Ownership New Vehicles Budget Cars See *Purchase Intent Classification* Extension + 821 806 Purchase Intent* | Automotive Ownership | Classic Cars | Purchase Intent* Automotive Ownership New Vehicles Classic Cars See *Purchase Intent Classification* Extension + 822 806 Purchase Intent* | Automotive Ownership | Concept Cars | Purchase Intent* Automotive Ownership New Vehicles Concept Cars See *Purchase Intent Classification* Extension + 823 806 Purchase Intent* | Automotive Ownership | Driverless Cars | Purchase Intent* Automotive Ownership New Vehicles Driverless Cars See *Purchase Intent Classification* Extension + 824 806 Purchase Intent* | Automotive Ownership | Green Vehicles | Purchase Intent* Automotive Ownership New Vehicles Green Vehicles See *Purchase Intent Classification* Extension + 825 806 Purchase Intent* | Automotive Ownership | Luxury Cars | Purchase Intent* Automotive Ownership New Vehicles Luxury Cars See *Purchase Intent Classification* Extension + 826 806 Purchase Intent* | Automotive Ownership | Performance Cars | Purchase Intent* Automotive Ownership New Vehicles Performance Cars See *Purchase Intent Classification* Extension + 827 806 Purchase Intent* | Automotive Ownership | Motorbikes | Purchase Intent* Automotive Ownership New Vehicles Motorbikes See *Purchase Intent Classification* Extension + 828 806 Purchase Intent* | Automotive Ownership | Other Vehicles | Purchase Intent* Automotive Ownership New Vehicles Other Vehicles See *Purchase Intent Classification* Extension + 829 805 Purchase Intent* | Automotive Ownership | Pre-Owned Vehicles | Purchase Intent* Automotive Ownership Pre-Owned Vehicles See *Purchase Intent Classification* Extension + 830 829 Purchase Intent* | Automotive Ownership | Commercial Trucks | Purchase Intent* Automotive Ownership Pre-Owned Vehicles Commercial Trucks See *Purchase Intent Classification* Extension + 831 829 Purchase Intent* | Automotive Ownership | Sedan | Purchase Intent* Automotive Ownership Pre-Owned Vehicles Sedan See *Purchase Intent Classification* Extension + 832 829 Purchase Intent* | Automotive Ownership | Station Wagon | Purchase Intent* Automotive Ownership Pre-Owned Vehicles Station Wagon See *Purchase Intent Classification* Extension + 833 829 Purchase Intent* | Automotive Ownership | SUV | Purchase Intent* Automotive Ownership Pre-Owned Vehicles SUV See *Purchase Intent Classification* Extension + 834 829 Purchase Intent* | Automotive Ownership | Van | Purchase Intent* Automotive Ownership Pre-Owned Vehicles Van See *Purchase Intent Classification* Extension + 835 829 Purchase Intent* | Automotive Ownership | Convertible | Purchase Intent* Automotive Ownership Pre-Owned Vehicles Convertible See *Purchase Intent Classification* Extension + 836 829 Purchase Intent* | Automotive Ownership | Coupe | Purchase Intent* Automotive Ownership Pre-Owned Vehicles Coupe See *Purchase Intent Classification* Extension + 837 829 Purchase Intent* | Automotive Ownership | Crossover | Purchase Intent* Automotive Ownership Pre-Owned Vehicles Crossover See *Purchase Intent Classification* Extension + 838 829 Purchase Intent* | Automotive Ownership | Hatchback | Purchase Intent* Automotive Ownership Pre-Owned Vehicles Hatchback See *Purchase Intent Classification* Extension + 839 829 Purchase Intent* | Automotive Ownership | Microcar | Purchase Intent* Automotive Ownership Pre-Owned Vehicles Microcar See *Purchase Intent Classification* Extension + 840 829 Purchase Intent* | Automotive Ownership | Minivan | Purchase Intent* Automotive Ownership Pre-Owned Vehicles Minivan See *Purchase Intent Classification* Extension + 841 829 Purchase Intent* | Automotive Ownership | Off-Road Vehicles | Purchase Intent* Automotive Ownership Pre-Owned Vehicles Off-Road Vehicles See *Purchase Intent Classification* Extension + 842 829 Purchase Intent* | Automotive Ownership | Pickup Trucks | Purchase Intent* Automotive Ownership Pre-Owned Vehicles Pickup Trucks See *Purchase Intent Classification* Extension + 843 829 Purchase Intent* | Automotive Ownership | Budget Cars | Purchase Intent* Automotive Ownership Pre-Owned Vehicles Budget Cars See *Purchase Intent Classification* Extension + 844 829 Purchase Intent* | Automotive Ownership | Classic Cars | Purchase Intent* Automotive Ownership Pre-Owned Vehicles Classic Cars See *Purchase Intent Classification* Extension + 845 829 Purchase Intent* | Automotive Ownership | Concept Cars | Purchase Intent* Automotive Ownership Pre-Owned Vehicles Concept Cars See *Purchase Intent Classification* Extension + 846 829 Purchase Intent* | Automotive Ownership | Driverless Cars | Purchase Intent* Automotive Ownership Pre-Owned Vehicles Driverless Cars See *Purchase Intent Classification* Extension + 847 829 Purchase Intent* | Automotive Ownership | Green Vehicles | Purchase Intent* Automotive Ownership Pre-Owned Vehicles Green Vehicles See *Purchase Intent Classification* Extension + 848 829 Purchase Intent* | Automotive Ownership | Luxury Cars | Purchase Intent* Automotive Ownership Pre-Owned Vehicles Luxury Cars See *Purchase Intent Classification* Extension + 849 829 Purchase Intent* | Automotive Ownership | Performance Cars | Purchase Intent* Automotive Ownership Pre-Owned Vehicles Performance Cars See *Purchase Intent Classification* Extension + 850 829 Purchase Intent* | Automotive Ownership | Motorbikes | Purchase Intent* Automotive Ownership Pre-Owned Vehicles Motorbikes See *Purchase Intent Classification* Extension + 851 829 Purchase Intent* | Automotive Ownership | Other Vehicles | Purchase Intent* Automotive Ownership Pre-Owned Vehicles Other Vehicles See *Purchase Intent Classification* Extension + 852 752 Purchase Intent* | Automotive Products Purchase Intent* Automotive Products See *Purchase Intent Classification* Extension + 853 852 Purchase Intent* | Automotive Products | Automotive Care Products | Purchase Intent* Automotive Products Automotive Care Products See *Purchase Intent Classification* Extension + 854 852 Purchase Intent* | Automotive Products | Automotive Parts and Accessories | Purchase Intent* Automotive Products Automotive Parts and Accessories See *Purchase Intent Classification* Extension + 855 854 Purchase Intent* | Automotive Products | Car Alarms | Purchase Intent* Automotive Products Automotive Parts and Accessories Car Alarms See *Purchase Intent Classification* Extension + 856 854 Purchase Intent* | Automotive Products | Car Amplifiers | Purchase Intent* Automotive Products Automotive Parts and Accessories Car Amplifiers See *Purchase Intent Classification* Extension + 857 854 Purchase Intent* | Automotive Products | Car Seats | Purchase Intent* Automotive Products Automotive Parts and Accessories Car Seats See *Purchase Intent Classification* Extension + 858 854 Purchase Intent* | Automotive Products | Car Speakers | Purchase Intent* Automotive Products Automotive Parts and Accessories Car Speakers See *Purchase Intent Classification* Extension + 859 854 Purchase Intent* | Automotive Products | Car Navigation Equipment | Purchase Intent* Automotive Products Automotive Parts and Accessories Car Navigation Equipment See *Purchase Intent Classification* Extension + 860 854 Purchase Intent* | Automotive Products | Automotive Tires | Purchase Intent* Automotive Products Automotive Parts and Accessories Automotive Tires See *Purchase Intent Classification* Extension + 861 752 Purchase Intent* | Automotive Services Purchase Intent* Automotive Services See *Purchase Intent Classification* Extension + 862 861 Purchase Intent* | Automotive Services | Auto Rental | Purchase Intent* Automotive Services Auto Rental See *Purchase Intent Classification* Extension + 863 861 Purchase Intent* | Automotive Services | Auto Towing and Repair | Purchase Intent* Automotive Services Auto Towing and Repair See *Purchase Intent Classification* Extension + 864 861 Purchase Intent* | Automotive Services | Car Wash | Purchase Intent* Automotive Services Car Wash See *Purchase Intent Classification* Extension + 865 752 Purchase Intent* | Beauty Services Purchase Intent* Beauty Services See *Purchase Intent Classification* Extension + 866 865 Purchase Intent* | Beauty Services | Beauty Salons and Tanning | Purchase Intent* Beauty Services Beauty Salons and Tanning See *Purchase Intent Classification* Extension + 867 865 Purchase Intent* | Beauty Services | Hair Salons | Purchase Intent* Beauty Services Hair Salons See *Purchase Intent Classification* Extension + 868 865 Purchase Intent* | Beauty Services | Nail Salons | Purchase Intent* Beauty Services Nail Salons See *Purchase Intent Classification* Extension + 869 865 Purchase Intent* | Beauty Services | Piercing and Tattooing | Purchase Intent* Beauty Services Piercing and Tattooing See *Purchase Intent Classification* Extension + 870 865 Purchase Intent* | Beauty Services | Spas | Purchase Intent* Beauty Services Spas See *Purchase Intent Classification* Extension + 871 752 Purchase Intent* | Business and Industrial Purchase Intent* Business and Industrial See *Purchase Intent Classification* Extension + 872 871 Purchase Intent* | Business and Industrial | Advertising and Marketing | Purchase Intent* Business and Industrial Advertising and Marketing See *Purchase Intent Classification* Extension + 873 871 Purchase Intent* | Business and Industrial | Auctions | Purchase Intent* Business and Industrial Auctions See *Purchase Intent Classification* Extension + 874 871 Purchase Intent* | Business and Industrial | Conferences/Events/Seminars | Purchase Intent* Business and Industrial Conferences/Events/Seminars See *Purchase Intent Classification* Extension + 875 871 Purchase Intent* | Business and Industrial | Construction | Purchase Intent* Business and Industrial Construction See *Purchase Intent Classification* Extension + 876 871 Purchase Intent* | Business and Industrial | Energy Industry | Purchase Intent* Business and Industrial Energy Industry See *Purchase Intent Classification* Extension + 877 876 Purchase Intent* | Business and Industrial | Energy Services | Purchase Intent* Business and Industrial Energy Industry Energy Services See *Purchase Intent Classification* Extension + 878 876 Purchase Intent* | Business and Industrial | Oil, Gas and Consumable Fuels | Purchase Intent* Business and Industrial Energy Industry Oil, Gas and Consumable Fuels See *Purchase Intent Classification* Extension + 879 876 Purchase Intent* | Business and Industrial | Electric Power Industry | Purchase Intent* Business and Industrial Energy Industry Electric Power Industry See *Purchase Intent Classification* Extension + 880 871 Purchase Intent* | Business and Industrial | Forestry and Logging | Purchase Intent* Business and Industrial Forestry and Logging See *Purchase Intent Classification* Extension + 881 871 Purchase Intent* | Business and Industrial | Government | Purchase Intent* Business and Industrial Government See *Purchase Intent Classification* Extension + 882 871 Purchase Intent* | Business and Industrial | Human Resources | Purchase Intent* Business and Industrial Human Resources See *Purchase Intent Classification* Extension + 883 871 Purchase Intent* | Business and Industrial | Industrial Storage | Purchase Intent* Business and Industrial Industrial Storage See *Purchase Intent Classification* Extension + 884 871 Purchase Intent* | Business and Industrial | Industrials | Purchase Intent* Business and Industrial Industrials See *Purchase Intent Classification* Extension + 885 884 Purchase Intent* | Business and Industrial | Aerospace and Defense | Purchase Intent* Business and Industrial Industrials Aerospace and Defense See *Purchase Intent Classification* Extension + 886 884 Purchase Intent* | Business and Industrial | Construction and Engineering | Purchase Intent* Business and Industrial Industrials Construction and Engineering See *Purchase Intent Classification* Extension + 887 884 Purchase Intent* | Business and Industrial | Industrial Conglomerates | Purchase Intent* Business and Industrial Industrials Industrial Conglomerates See *Purchase Intent Classification* Extension + 888 884 Purchase Intent* | Business and Industrial | Trading Companies and Distributors | Purchase Intent* Business and Industrial Industrials Trading Companies and Distributors See *Purchase Intent Classification* Extension + 889 884 Purchase Intent* | Business and Industrial | Transportation | Purchase Intent* Business and Industrial Industrials Transportation See *Purchase Intent Classification* Extension + 890 871 Purchase Intent* | Business and Industrial | Laundry and Dry Cleaning Services | Purchase Intent* Business and Industrial Laundry and Dry Cleaning Services See *Purchase Intent Classification* Extension + 891 871 Purchase Intent* | Business and Industrial | Law Enforcement | Purchase Intent* Business and Industrial Law Enforcement See *Purchase Intent Classification* Extension + 892 871 Purchase Intent* | Business and Industrial | Manufacturing | Purchase Intent* Business and Industrial Manufacturing See *Purchase Intent Classification* Extension + 893 871 Purchase Intent* | Business and Industrial | Material Handling | Purchase Intent* Business and Industrial Material Handling See *Purchase Intent Classification* Extension + 894 871 Purchase Intent* | Business and Industrial | Medical and Biotechnology | Purchase Intent* Business and Industrial Medical and Biotechnology See *Purchase Intent Classification* Extension + 895 871 Purchase Intent* | Business and Industrial | Mining and Quarrying | Purchase Intent* Business and Industrial Mining and Quarrying See *Purchase Intent Classification* Extension + 896 871 Purchase Intent* | Business and Industrial | Photographers | Purchase Intent* Business and Industrial Photographers See *Purchase Intent Classification* Extension + 897 871 Purchase Intent* | Business and Industrial | Printing/Fax/WiFi Services | Purchase Intent* Business and Industrial Printing/Fax/WiFi Services See *Purchase Intent Classification* Extension + 898 871 Purchase Intent* | Business and Industrial | Public Relations and Strategic Communication | Purchase Intent* Business and Industrial Public Relations and Strategic Communication See *Purchase Intent Classification* Extension + 899 871 Purchase Intent* | Business and Industrial | Retail | Purchase Intent* Business and Industrial Retail See *Purchase Intent Classification* Extension + 900 899 Purchase Intent* | Business and Industrial | Cell Phone Stores | Purchase Intent* Business and Industrial Retail Cell Phone Stores See *Purchase Intent Classification* Extension + 901 899 Purchase Intent* | Business and Industrial | Music Stores | Purchase Intent* Business and Industrial Retail Music Stores See *Purchase Intent Classification* Extension + 902 899 Purchase Intent* | Business and Industrial | Grocery Stores and Supermarkets | Purchase Intent* Business and Industrial Retail Grocery Stores and Supermarkets See *Purchase Intent Classification* Extension + 903 899 Purchase Intent* | Business and Industrial | Shopping Malls | Purchase Intent* Business and Industrial Retail Shopping Malls See *Purchase Intent Classification* Extension + 904 899 Purchase Intent* | Business and Industrial | Department Stores | Purchase Intent* Business and Industrial Retail Department Stores See *Purchase Intent Classification* Extension + 905 899 Purchase Intent* | Business and Industrial | Specialty Stores | Purchase Intent* Business and Industrial Retail Specialty Stores See *Purchase Intent Classification* Extension + 906 899 Purchase Intent* | Business and Industrial | Pawn Shops | Purchase Intent* Business and Industrial Retail Pawn Shops See *Purchase Intent Classification* Extension + 907 899 Purchase Intent* | Business and Industrial | Factory Outlet Stores | Purchase Intent* Business and Industrial Retail Factory Outlet Stores See *Purchase Intent Classification* Extension + 908 871 Purchase Intent* | Business and Industrial | Science and Laboratory | Purchase Intent* Business and Industrial Science and Laboratory See *Purchase Intent Classification* Extension + 909 871 Purchase Intent* | Business and Industrial | Signage | Purchase Intent* Business and Industrial Signage See *Purchase Intent Classification* Extension + 910 871 Purchase Intent* | Business and Industrial | Small Business | Purchase Intent* Business and Industrial Small Business See *Purchase Intent Classification* Extension + 911 871 Purchase Intent* | Business and Industrial | Telecom Services | Purchase Intent* Business and Industrial Telecom Services See *Purchase Intent Classification* Extension + 912 911 Purchase Intent* | Business and Industrial | Home Internet Services | Purchase Intent* Business and Industrial Telecom Services Home Internet Services See *Purchase Intent Classification* Extension + 913 911 Purchase Intent* | Business and Industrial | Home Television Services | Purchase Intent* Business and Industrial Telecom Services Home Television Services See *Purchase Intent Classification* Extension + 914 911 Purchase Intent* | Business and Industrial | Home Phone Services | Purchase Intent* Business and Industrial Telecom Services Home Phone Services See *Purchase Intent Classification* Extension + 915 911 Purchase Intent* | Business and Industrial | Mobile Phone Plans | Purchase Intent* Business and Industrial Telecom Services Mobile Phone Plans See *Purchase Intent Classification* Extension + 916 911 Purchase Intent* | Business and Industrial | Prepaid International Phone Services | Purchase Intent* Business and Industrial Telecom Services Prepaid International Phone Services See *Purchase Intent Classification* Extension + 917 911 Purchase Intent* | Business and Industrial | Business Telecom Services | Purchase Intent* Business and Industrial Telecom Services Business Telecom Services See *Purchase Intent Classification* Extension + 918 871 Purchase Intent* | Business and Industrial | Waste Disposal and Recycling | Purchase Intent* Business and Industrial Waste Disposal and Recycling See *Purchase Intent Classification* Extension + 919 752 Purchase Intent* | Clothing and Accessories Purchase Intent* Clothing and Accessories See *Purchase Intent Classification* Extension + 920 919 Purchase Intent* | Clothing and Accessories | Clothing | Purchase Intent* Clothing and Accessories Clothing See *Purchase Intent Classification* Extension + 921 920 Purchase Intent* | Clothing and Accessories | Children's Clothing | Purchase Intent* Clothing and Accessories Clothing Children's Clothing See *Purchase Intent Classification* Extension + 922 920 Purchase Intent* | Clothing and Accessories | Men's Clothing | Purchase Intent* Clothing and Accessories Clothing Men's Clothing See *Purchase Intent Classification* Extension + 923 920 Purchase Intent* | Clothing and Accessories | Women's Clothing | Purchase Intent* Clothing and Accessories Clothing Women's Clothing See *Purchase Intent Classification* Extension + 924 920 Purchase Intent* | Clothing and Accessories | Wedding Dresses/Bridal Wear/Tuxedos | Purchase Intent* Clothing and Accessories Clothing Wedding Dresses/Bridal Wear/Tuxedos See *Purchase Intent Classification* Extension + 925 920 Purchase Intent* | Clothing and Accessories | Maternity Clothing | Purchase Intent* Clothing and Accessories Clothing Maternity Clothing See *Purchase Intent Classification* Extension + 926 920 Purchase Intent* | Clothing and Accessories | Underwear and Lingerie | Purchase Intent* Clothing and Accessories Clothing Underwear and Lingerie See *Purchase Intent Classification* Extension + 927 920 Purchase Intent* | Clothing and Accessories | Sportswear | Purchase Intent* Clothing and Accessories Clothing Sportswear See *Purchase Intent Classification* Extension + 928 919 Purchase Intent* | Clothing and Accessories | Clothing Accessories | Purchase Intent* Clothing and Accessories Clothing Accessories See *Purchase Intent Classification* Extension + 929 919 Purchase Intent* | Clothing and Accessories | Costumes and Accessories | Purchase Intent* Clothing and Accessories Costumes and Accessories See *Purchase Intent Classification* Extension + 930 919 Purchase Intent* | Clothing and Accessories | Footwear | Purchase Intent* Clothing and Accessories Footwear See *Purchase Intent Classification* Extension + 931 919 Purchase Intent* | Clothing and Accessories | Footwear Accessories | Purchase Intent* Clothing and Accessories Footwear Accessories See *Purchase Intent Classification* Extension + 932 919 Purchase Intent* | Clothing and Accessories | Handbags and Wallets | Purchase Intent* Clothing and Accessories Handbags and Wallets See *Purchase Intent Classification* Extension + 933 919 Purchase Intent* | Clothing and Accessories | Jewelry and Watches | Purchase Intent* Clothing and Accessories Jewelry and Watches See *Purchase Intent Classification* Extension + 934 919 Purchase Intent* | Clothing and Accessories | Sunglasses | Purchase Intent* Clothing and Accessories Sunglasses See *Purchase Intent Classification* Extension + 935 752 Purchase Intent* | Collectables and Antiques Purchase Intent* Collectables and Antiques See *Purchase Intent Classification* Extension + 936 935 Purchase Intent* | Collectables and Antiques | Antiques | Purchase Intent* Collectables and Antiques Antiques See *Purchase Intent Classification* Extension + 937 935 Purchase Intent* | Collectables and Antiques | Coins and Paper Money | Purchase Intent* Collectables and Antiques Coins and Paper Money See *Purchase Intent Classification* Extension + 938 935 Purchase Intent* | Collectables and Antiques | Collectibles | Purchase Intent* Collectables and Antiques Collectibles See *Purchase Intent Classification* Extension + 939 935 Purchase Intent* | Collectables and Antiques | Entertainment Memorabilia | Purchase Intent* Collectables and Antiques Entertainment Memorabilia See *Purchase Intent Classification* Extension + 940 935 Purchase Intent* | Collectables and Antiques | Sports Memorabilia and Trading Cards | Purchase Intent* Collectables and Antiques Sports Memorabilia and Trading Cards See *Purchase Intent Classification* Extension + 941 935 Purchase Intent* | Collectables and Antiques | Stamps | Purchase Intent* Collectables and Antiques Stamps See *Purchase Intent Classification* Extension + 942 752 Purchase Intent* | Consumer Electronics Purchase Intent* Consumer Electronics See *Purchase Intent Classification* Extension + 943 942 Purchase Intent* | Consumer Electronics | Arcade Equipment | Purchase Intent* Consumer Electronics Arcade Equipment See *Purchase Intent Classification* Extension + 944 942 Purchase Intent* | Consumer Electronics | Audio | Purchase Intent* Consumer Electronics Audio See *Purchase Intent Classification* Extension + 945 944 Purchase Intent* | Consumer Electronics | CD Players | Purchase Intent* Consumer Electronics Audio CD Players See *Purchase Intent Classification* Extension + 946 944 Purchase Intent* | Consumer Electronics | Headphones | Purchase Intent* Consumer Electronics Audio Headphones See *Purchase Intent Classification* Extension + 947 942 Purchase Intent* | Consumer Electronics | Cameras and Photo | Purchase Intent* Consumer Electronics Cameras and Photo See *Purchase Intent Classification* Extension + 948 947 Purchase Intent* | Consumer Electronics | Camera and Photo Accessories | Purchase Intent* Consumer Electronics Cameras and Photo Camera and Photo Accessories See *Purchase Intent Classification* Extension + 949 947 Purchase Intent* | Consumer Electronics | Cameras | Purchase Intent* Consumer Electronics Cameras and Photo Cameras See *Purchase Intent Classification* Extension + 950 942 Purchase Intent* | Consumer Electronics | Circuit Boards and Components | Purchase Intent* Consumer Electronics Circuit Boards and Components See *Purchase Intent Classification* Extension + 951 942 Purchase Intent* | Consumer Electronics | Communications Electronics | Purchase Intent* Consumer Electronics Communications Electronics See *Purchase Intent Classification* Extension + 952 942 Purchase Intent* | Consumer Electronics | Components | Purchase Intent* Consumer Electronics Components See *Purchase Intent Classification* Extension + 953 942 Purchase Intent* | Consumer Electronics | Computers | Purchase Intent* Consumer Electronics Computers See *Purchase Intent Classification* Extension + 954 953 Purchase Intent* | Consumer Electronics | Laptops | Purchase Intent* Consumer Electronics Computers Laptops See *Purchase Intent Classification* Extension + 955 953 Purchase Intent* | Consumer Electronics | Desktops | Purchase Intent* Consumer Electronics Computers Desktops See *Purchase Intent Classification* Extension + 956 942 Purchase Intent* | Consumer Electronics | E-Readers | Purchase Intent* Consumer Electronics E-Readers See *Purchase Intent Classification* Extension + 957 942 Purchase Intent* | Consumer Electronics | Electronics Accessories | Purchase Intent* Consumer Electronics Electronics Accessories See *Purchase Intent Classification* Extension + 958 942 Purchase Intent* | Consumer Electronics | Home Theater Systems | Purchase Intent* Consumer Electronics Home Theater Systems See *Purchase Intent Classification* Extension + 959 942 Purchase Intent* | Consumer Electronics | Marine Electronics | Purchase Intent* Consumer Electronics Marine Electronics See *Purchase Intent Classification* Extension + 960 942 Purchase Intent* | Consumer Electronics | Mobile Phone Plans | Purchase Intent* Consumer Electronics Mobile Phone Plans See *Purchase Intent Classification* Extension + 961 942 Purchase Intent* | Consumer Electronics | Mobile Phones and Accessories | Purchase Intent* Consumer Electronics Mobile Phones and Accessories See *Purchase Intent Classification* Extension + 962 942 Purchase Intent* | Consumer Electronics | Networking | Purchase Intent* Consumer Electronics Networking See *Purchase Intent Classification* Extension + 963 942 Purchase Intent* | Consumer Electronics | Printers/Copiers/Scanners/Fax | Purchase Intent* Consumer Electronics Printers/Copiers/Scanners/Fax See *Purchase Intent Classification* Extension + 964 942 Purchase Intent* | Consumer Electronics | Security Devices | Purchase Intent* Consumer Electronics Security Devices See *Purchase Intent Classification* Extension + 965 942 Purchase Intent* | Consumer Electronics | Tablets | Purchase Intent* Consumer Electronics Tablets See *Purchase Intent Classification* Extension + 966 942 Purchase Intent* | Consumer Electronics | Televisions | Purchase Intent* Consumer Electronics Televisions See *Purchase Intent Classification* Extension + 967 942 Purchase Intent* | Consumer Electronics | Video | Purchase Intent* Consumer Electronics Video See *Purchase Intent Classification* Extension + 968 967 Purchase Intent* | Consumer Electronics | Blu-Ray Disc Players | Purchase Intent* Consumer Electronics Video Blu-Ray Disc Players See *Purchase Intent Classification* Extension + 969 967 Purchase Intent* | Consumer Electronics | Camcorders | Purchase Intent* Consumer Electronics Video Camcorders See *Purchase Intent Classification* Extension + 970 942 Purchase Intent* | Consumer Electronics | Video Game Console Accessories | Purchase Intent* Consumer Electronics Video Game Console Accessories See *Purchase Intent Classification* Extension + 971 942 Purchase Intent* | Consumer Electronics | Video Games and Consoles | Purchase Intent* Consumer Electronics Video Games and Consoles See *Purchase Intent Classification* Extension + 972 752 Purchase Intent* | Consumer Packaged Goods Purchase Intent* Consumer Packaged Goods See *Purchase Intent Classification* Extension + 973 972 Purchase Intent* | Consumer Packaged Goods | Edible | Purchase Intent* Consumer Packaged Goods Edible See *Purchase Intent Classification* Extension + 974 973 Purchase Intent* | Consumer Packaged Goods | Beverages | Purchase Intent* Consumer Packaged Goods Edible Beverages See *Purchase Intent Classification* Extension + 975 974 Purchase Intent* | Consumer Packaged Goods | Carbonated Soft Drinks | Purchase Intent* Consumer Packaged Goods Edible Beverages Carbonated Soft Drinks See *Purchase Intent Classification* Extension + 976 974 Purchase Intent* | Consumer Packaged Goods | Coffee & Tea | Purchase Intent* Consumer Packaged Goods Edible Beverages Coffee & Tea See *Purchase Intent Classification* Extension + 977 976 Purchase Intent* | Consumer Packaged Goods | Coffee | Purchase Intent* Consumer Packaged Goods Edible Beverages Coffee & Tea Coffee See *Purchase Intent Classification* Extension + 978 976 Purchase Intent* | Consumer Packaged Goods | Coffee Creamer | Purchase Intent* Consumer Packaged Goods Edible Beverages Coffee & Tea Coffee Creamer See *Purchase Intent Classification* Extension + 979 976 Purchase Intent* | Consumer Packaged Goods | Coffee Filters | Purchase Intent* Consumer Packaged Goods Edible Beverages Coffee & Tea Coffee Filters See *Purchase Intent Classification* Extension + 980 976 Purchase Intent* | Consumer Packaged Goods | Tea - Bags/loose | Purchase Intent* Consumer Packaged Goods Edible Beverages Coffee & Tea Tea - Bags/loose See *Purchase Intent Classification* Extension + 981 976 Purchase Intent* | Consumer Packaged Goods | Tea - Instant Tea Mixes | Purchase Intent* Consumer Packaged Goods Edible Beverages Coffee & Tea Tea - Instant Tea Mixes See *Purchase Intent Classification* Extension + 982 976 Purchase Intent* | Consumer Packaged Goods | Tea/Coffee - Ready-to-Drink | Purchase Intent* Consumer Packaged Goods Edible Beverages Coffee & Tea Tea/Coffee - Ready-to-Drink See *Purchase Intent Classification* Extension + 983 974 Purchase Intent* | Consumer Packaged Goods | Drink Mixes | Purchase Intent* Consumer Packaged Goods Edible Beverages Drink Mixes See *Purchase Intent Classification* Extension + 984 983 Purchase Intent* | Consumer Packaged Goods | Cocktail Mixes | Purchase Intent* Consumer Packaged Goods Edible Beverages Drink Mixes Cocktail Mixes See *Purchase Intent Classification* Extension + 985 983 Purchase Intent* | Consumer Packaged Goods | Drink Mixes | Purchase Intent* Consumer Packaged Goods Edible Beverages Drink Mixes Drink Mixes See *Purchase Intent Classification* Extension + 986 983 Purchase Intent* | Consumer Packaged Goods | Liquid Drink Enhancers | Purchase Intent* Consumer Packaged Goods Edible Beverages Drink Mixes Liquid Drink Enhancers See *Purchase Intent Classification* Extension + 987 983 Purchase Intent* | Consumer Packaged Goods | Milk Flavoring Cocoa Mixes | Purchase Intent* Consumer Packaged Goods Edible Beverages Drink Mixes Milk Flavoring Cocoa Mixes See *Purchase Intent Classification* Extension + 988 974 Purchase Intent* | Consumer Packaged Goods | Juices | Purchase Intent* Consumer Packaged Goods Edible Beverages Juices See *Purchase Intent Classification* Extension + 989 988 Purchase Intent* | Consumer Packaged Goods | Aseptic Juices | Purchase Intent* Consumer Packaged Goods Edible Beverages Juices Aseptic Juices See *Purchase Intent Classification* Extension + 990 988 Purchase Intent* | Consumer Packaged Goods | Bottled Juices | Purchase Intent* Consumer Packaged Goods Edible Beverages Juices Bottled Juices See *Purchase Intent Classification* Extension + 991 988 Purchase Intent* | Consumer Packaged Goods | Canned Juices | Purchase Intent* Consumer Packaged Goods Edible Beverages Juices Canned Juices See *Purchase Intent Classification* Extension + 992 988 Purchase Intent* | Consumer Packaged Goods | Juice/Drink Concentrate | Purchase Intent* Consumer Packaged Goods Edible Beverages Juices Juice/Drink Concentrate See *Purchase Intent Classification* Extension + 993 974 Purchase Intent* | Consumer Packaged Goods | Non-Fruit Drinks | Purchase Intent* Consumer Packaged Goods Edible Beverages Non-Fruit Drinks See *Purchase Intent Classification* Extension + 994 993 Purchase Intent* | Consumer Packaged Goods | Non-Fruit Drinks | Purchase Intent* Consumer Packaged Goods Edible Beverages Non-Fruit Drinks Non-Fruit Drinks See *Purchase Intent Classification* Extension + 995 993 Purchase Intent* | Consumer Packaged Goods | Powdered Milk | Purchase Intent* Consumer Packaged Goods Edible Beverages Non-Fruit Drinks Powdered Milk See *Purchase Intent Classification* Extension + 996 974 Purchase Intent* | Consumer Packaged Goods | Sports/Energy Drinks | Purchase Intent* Consumer Packaged Goods Edible Beverages Sports/Energy Drinks See *Purchase Intent Classification* Extension + 997 996 Purchase Intent* | Consumer Packaged Goods | Energy Drinks | Purchase Intent* Consumer Packaged Goods Edible Beverages Sports/Energy Drinks Energy Drinks See *Purchase Intent Classification* Extension + 998 996 Purchase Intent* | Consumer Packaged Goods | Sports Drinks | Purchase Intent* Consumer Packaged Goods Edible Beverages Sports/Energy Drinks Sports Drinks See *Purchase Intent Classification* Extension + 999 974 Purchase Intent* | Consumer Packaged Goods | Water | Purchase Intent* Consumer Packaged Goods Edible Beverages Water See *Purchase Intent Classification* Extension + 1000 999 Purchase Intent* | Consumer Packaged Goods | Bottled Water | Purchase Intent* Consumer Packaged Goods Edible Beverages Water Bottled Water See *Purchase Intent Classification* Extension + 1001 973 Purchase Intent* | Consumer Packaged Goods | Frozen | Purchase Intent* Consumer Packaged Goods Edible Frozen See *Purchase Intent Classification* Extension + 1002 1001 Purchase Intent* | Consumer Packaged Goods | Frozen Baked Goods | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Baked Goods See *Purchase Intent Classification* Extension + 1003 1002 Purchase Intent* | Consumer Packaged Goods | Bread/Dough | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Baked Goods Bread/Dough See *Purchase Intent Classification* Extension + 1004 1002 Purchase Intent* | Consumer Packaged Goods | Cookies | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Baked Goods Cookies See *Purchase Intent Classification* Extension + 1005 1002 Purchase Intent* | Consumer Packaged Goods | Pies | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Baked Goods Pies See *Purchase Intent Classification* Extension + 1006 1001 Purchase Intent* | Consumer Packaged Goods | Frozen Beverages | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Beverages See *Purchase Intent Classification* Extension + 1007 1006 Purchase Intent* | Consumer Packaged Goods | Coffee Creamer | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Beverages Coffee Creamer See *Purchase Intent Classification* Extension + 1008 1006 Purchase Intent* | Consumer Packaged Goods | Juices | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Beverages Juices See *Purchase Intent Classification* Extension + 1009 1001 Purchase Intent* | Consumer Packaged Goods | Frozen Desserts | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Desserts See *Purchase Intent Classification* Extension + 1010 1009 Purchase Intent* | Consumer Packaged Goods | Desserts/Toppings | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Desserts Desserts/Toppings See *Purchase Intent Classification* Extension + 1011 1009 Purchase Intent* | Consumer Packaged Goods | Ice Cream/Sherbet | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Desserts Ice Cream/Sherbet See *Purchase Intent Classification* Extension + 1012 1009 Purchase Intent* | Consumer Packaged Goods | Novelties | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Desserts Novelties See *Purchase Intent Classification* Extension + 1013 1001 Purchase Intent* | Consumer Packaged Goods | Frozen Fruits & Vegetables | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Fruits & Vegetables See *Purchase Intent Classification* Extension + 1014 1013 Purchase Intent* | Consumer Packaged Goods | Corn on the Cob | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Fruits & Vegetables Corn on the Cob See *Purchase Intent Classification* Extension + 1015 1013 Purchase Intent* | Consumer Packaged Goods | Fruit | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Fruits & Vegetables Fruit See *Purchase Intent Classification* Extension + 1016 1013 Purchase Intent* | Consumer Packaged Goods | Plain Vegetables | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Fruits & Vegetables Plain Vegetables See *Purchase Intent Classification* Extension + 1017 1013 Purchase Intent* | Consumer Packaged Goods | Potatoes/Onions | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Fruits & Vegetables Potatoes/Onions See *Purchase Intent Classification* Extension + 1018 1013 Purchase Intent* | Consumer Packaged Goods | Prepared Vegetables | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Fruits & Vegetables Prepared Vegetables See *Purchase Intent Classification* Extension + 1019 1001 Purchase Intent* | Consumer Packaged Goods | Frozen Meals | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Meals See *Purchase Intent Classification* Extension + 1020 1019 Purchase Intent* | Consumer Packaged Goods | Breakfast Food | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Meals Breakfast Food See *Purchase Intent Classification* Extension + 1021 1019 Purchase Intent* | Consumer Packaged Goods | Dinners/Entrees | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Meals Dinners/Entrees See *Purchase Intent Classification* Extension + 1022 1019 Purchase Intent* | Consumer Packaged Goods | Pasta | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Meals Pasta See *Purchase Intent Classification* Extension + 1023 1019 Purchase Intent* | Consumer Packaged Goods | Pizza | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Meals Pizza See *Purchase Intent Classification* Extension + 1024 1019 Purchase Intent* | Consumer Packaged Goods | Soups/Sides/Other | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Meals Soups/Sides/Other See *Purchase Intent Classification* Extension + 1025 1001 Purchase Intent* | Consumer Packaged Goods | Frozen Meat/Poultry/Seafood | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Meat/Poultry/Seafood See *Purchase Intent Classification* Extension + 1026 1025 Purchase Intent* | Consumer Packaged Goods | Meat | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Meat/Poultry/Seafood Meat See *Purchase Intent Classification* Extension + 1027 1025 Purchase Intent* | Consumer Packaged Goods | Poultry | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Meat/Poultry/Seafood Poultry See *Purchase Intent Classification* Extension + 1028 1025 Purchase Intent* | Consumer Packaged Goods | Processed Poultry | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Meat/Poultry/Seafood Processed Poultry See *Purchase Intent Classification* Extension + 1029 1025 Purchase Intent* | Consumer Packaged Goods | Seafood | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Meat/Poultry/Seafood Seafood See *Purchase Intent Classification* Extension + 1030 1001 Purchase Intent* | Consumer Packaged Goods | Frozen Snacks | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Snacks See *Purchase Intent Classification* Extension + 1031 1030 Purchase Intent* | Consumer Packaged Goods | Appetizers/Snack Rolls | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Snacks Appetizers/Snack Rolls See *Purchase Intent Classification* Extension + 1032 1030 Purchase Intent* | Consumer Packaged Goods | Other Snacks | Purchase Intent* Consumer Packaged Goods Edible Frozen Frozen Snacks Other Snacks See *Purchase Intent Classification* Extension + 1033 1001 Purchase Intent* | Consumer Packaged Goods | Other Frozen | Purchase Intent* Consumer Packaged Goods Edible Frozen Other Frozen See *Purchase Intent Classification* Extension + 1034 1033 Purchase Intent* | Consumer Packaged Goods | Baby Food | Purchase Intent* Consumer Packaged Goods Edible Frozen Other Frozen Baby Food See *Purchase Intent Classification* Extension + 1035 1033 Purchase Intent* | Consumer Packaged Goods | Other Food | Purchase Intent* Consumer Packaged Goods Edible Frozen Other Frozen Other Food See *Purchase Intent Classification* Extension + 1036 973 Purchase Intent* | Consumer Packaged Goods | General Food | Purchase Intent* Consumer Packaged Goods Edible General Food See *Purchase Intent Classification* Extension + 1037 1036 Purchase Intent* | Consumer Packaged Goods | Baby Food | Purchase Intent* Consumer Packaged Goods Edible General Food Baby Food See *Purchase Intent Classification* Extension + 1038 1037 Purchase Intent* | Consumer Packaged Goods | Baby Food | Purchase Intent* Consumer Packaged Goods Edible General Food Baby Food Baby Food See *Purchase Intent Classification* Extension + 1039 1037 Purchase Intent* | Consumer Packaged Goods | Baby Formula/Electrolytes | Purchase Intent* Consumer Packaged Goods Edible General Food Baby Food Baby Formula/Electrolytes See *Purchase Intent Classification* Extension + 1040 1036 Purchase Intent* | Consumer Packaged Goods | Bakery | Purchase Intent* Consumer Packaged Goods Edible General Food Bakery See *Purchase Intent Classification* Extension + 1041 1040 Purchase Intent* | Consumer Packaged Goods | Bagels/Bialys | Purchase Intent* Consumer Packaged Goods Edible General Food Bakery Bagels/Bialys See *Purchase Intent Classification* Extension + 1042 1040 Purchase Intent* | Consumer Packaged Goods | Bakery Snacks | Purchase Intent* Consumer Packaged Goods Edible General Food Bakery Bakery Snacks See *Purchase Intent Classification* Extension + 1043 1040 Purchase Intent* | Consumer Packaged Goods | English Muffins | Purchase Intent* Consumer Packaged Goods Edible General Food Bakery English Muffins See *Purchase Intent Classification* Extension + 1044 1040 Purchase Intent* | Consumer Packaged Goods | Fresh Bread & Rolls | Purchase Intent* Consumer Packaged Goods Edible General Food Bakery Fresh Bread & Rolls See *Purchase Intent Classification* Extension + 1045 1040 Purchase Intent* | Consumer Packaged Goods | Pastry/Doughnuts | Purchase Intent* Consumer Packaged Goods Edible General Food Bakery Pastry/Doughnuts See *Purchase Intent Classification* Extension + 1046 1040 Purchase Intent* | Consumer Packaged Goods | Pies & Cakes | Purchase Intent* Consumer Packaged Goods Edible General Food Bakery Pies & Cakes See *Purchase Intent Classification* Extension + 1047 1036 Purchase Intent* | Consumer Packaged Goods | Baking | Purchase Intent* Consumer Packaged Goods Edible General Food Baking See *Purchase Intent Classification* Extension + 1048 1047 Purchase Intent* | Consumer Packaged Goods | Baking Cups/Paper | Purchase Intent* Consumer Packaged Goods Edible General Food Baking Baking Cups/Paper See *Purchase Intent Classification* Extension + 1049 1047 Purchase Intent* | Consumer Packaged Goods | Baking Mixes | Purchase Intent* Consumer Packaged Goods Edible General Food Baking Baking Mixes See *Purchase Intent Classification* Extension + 1050 1047 Purchase Intent* | Consumer Packaged Goods | Baking Needs | Purchase Intent* Consumer Packaged Goods Edible General Food Baking Baking Needs See *Purchase Intent Classification* Extension + 1051 1047 Purchase Intent* | Consumer Packaged Goods | Baking Nuts | Purchase Intent* Consumer Packaged Goods Edible General Food Baking Baking Nuts See *Purchase Intent Classification* Extension + 1052 1047 Purchase Intent* | Consumer Packaged Goods | Baking Syrum/Molasses | Purchase Intent* Consumer Packaged Goods Edible General Food Baking Baking Syrum/Molasses See *Purchase Intent Classification* Extension + 1053 1047 Purchase Intent* | Consumer Packaged Goods | Dessert Toppings | Purchase Intent* Consumer Packaged Goods Edible General Food Baking Dessert Toppings See *Purchase Intent Classification* Extension + 1054 1047 Purchase Intent* | Consumer Packaged Goods | Egg Substitute | Purchase Intent* Consumer Packaged Goods Edible General Food Baking Egg Substitute See *Purchase Intent Classification* Extension + 1055 1047 Purchase Intent* | Consumer Packaged Goods | Evaporated/Condensed Milk | Purchase Intent* Consumer Packaged Goods Edible General Food Baking Evaporated/Condensed Milk See *Purchase Intent Classification* Extension + 1056 1047 Purchase Intent* | Consumer Packaged Goods | Flour/Meal | Purchase Intent* Consumer Packaged Goods Edible General Food Baking Flour/Meal See *Purchase Intent Classification* Extension + 1057 1047 Purchase Intent* | Consumer Packaged Goods | Frosting | Purchase Intent* Consumer Packaged Goods Edible General Food Baking Frosting See *Purchase Intent Classification* Extension + 1058 1047 Purchase Intent* | Consumer Packaged Goods | Frt & Veg Preservative/Pectin | Purchase Intent* Consumer Packaged Goods Edible General Food Baking Frt & Veg Preservative/Pectin See *Purchase Intent Classification* Extension + 1059 1047 Purchase Intent* | Consumer Packaged Goods | Gelatin/Pudding PRD and Mixes | Purchase Intent* Consumer Packaged Goods Edible General Food Baking Gelatin/Pudding PRD and Mixes See *Purchase Intent Classification* Extension + 1060 1047 Purchase Intent* | Consumer Packaged Goods | Glazed Fruits | Purchase Intent* Consumer Packaged Goods Edible General Food Baking Glazed Fruits See *Purchase Intent Classification* Extension + 1061 1047 Purchase Intent* | Consumer Packaged Goods | Ice Cream Cones/Mixes | Purchase Intent* Consumer Packaged Goods Edible General Food Baking Ice Cream Cones/Mixes See *Purchase Intent Classification* Extension + 1062 1047 Purchase Intent* | Consumer Packaged Goods | Marshmallows | Purchase Intent* Consumer Packaged Goods Edible General Food Baking Marshmallows See *Purchase Intent Classification* Extension + 1063 1047 Purchase Intent* | Consumer Packaged Goods | Shortening & Oil | Purchase Intent* Consumer Packaged Goods Edible General Food Baking Shortening & Oil See *Purchase Intent Classification* Extension + 1064 1047 Purchase Intent* | Consumer Packaged Goods | Spices/Seasonings | Purchase Intent* Consumer Packaged Goods Edible General Food Baking Spices/Seasonings See *Purchase Intent Classification* Extension + 1065 1047 Purchase Intent* | Consumer Packaged Goods | Sugar | Purchase Intent* Consumer Packaged Goods Edible General Food Baking Sugar See *Purchase Intent Classification* Extension + 1066 1047 Purchase Intent* | Consumer Packaged Goods | Sugar Substitutes | Purchase Intent* Consumer Packaged Goods Edible General Food Baking Sugar Substitutes See *Purchase Intent Classification* Extension + 1067 1036 Purchase Intent* | Consumer Packaged Goods | Breakfast | Purchase Intent* Consumer Packaged Goods Edible General Food Breakfast See *Purchase Intent Classification* Extension + 1068 1067 Purchase Intent* | Consumer Packaged Goods | Cold cereal | Purchase Intent* Consumer Packaged Goods Edible General Food Breakfast Cold cereal See *Purchase Intent Classification* Extension + 1069 1067 Purchase Intent* | Consumer Packaged Goods | Hot Cereal | Purchase Intent* Consumer Packaged Goods Edible General Food Breakfast Hot Cereal See *Purchase Intent Classification* Extension + 1070 1067 Purchase Intent* | Consumer Packaged Goods | Other Breakfast Foods | Purchase Intent* Consumer Packaged Goods Edible General Food Breakfast Other Breakfast Foods See *Purchase Intent Classification* Extension + 1071 1067 Purchase Intent* | Consumer Packaged Goods | Pancake Mixes | Purchase Intent* Consumer Packaged Goods Edible General Food Breakfast Pancake Mixes See *Purchase Intent Classification* Extension + 1072 1067 Purchase Intent* | Consumer Packaged Goods | Syrup | Purchase Intent* Consumer Packaged Goods Edible General Food Breakfast Syrup See *Purchase Intent Classification* Extension + 1073 1067 Purchase Intent* | Consumer Packaged Goods | Toaster Pastries/Tarts | Purchase Intent* Consumer Packaged Goods Edible General Food Breakfast Toaster Pastries/Tarts See *Purchase Intent Classification* Extension + 1074 1036 Purchase Intent* | Consumer Packaged Goods | Candy | Purchase Intent* Consumer Packaged Goods Edible General Food Candy See *Purchase Intent Classification* Extension + 1075 1074 Purchase Intent* | Consumer Packaged Goods | Breath Fresheners | Purchase Intent* Consumer Packaged Goods Edible General Food Candy Breath Fresheners See *Purchase Intent Classification* Extension + 1076 1074 Purchase Intent* | Consumer Packaged Goods | Chocolate Candy | Purchase Intent* Consumer Packaged Goods Edible General Food Candy Chocolate Candy See *Purchase Intent Classification* Extension + 1077 1074 Purchase Intent* | Consumer Packaged Goods | Gum | Purchase Intent* Consumer Packaged Goods Edible General Food Candy Gum See *Purchase Intent Classification* Extension + 1078 1074 Purchase Intent* | Consumer Packaged Goods | Non-Chocolate Candy | Purchase Intent* Consumer Packaged Goods Edible General Food Candy Non-Chocolate Candy See *Purchase Intent Classification* Extension + 1079 1036 Purchase Intent* | Consumer Packaged Goods | Condiments & Sauces | Purchase Intent* Consumer Packaged Goods Edible General Food Condiments & Sauces See *Purchase Intent Classification* Extension + 1080 1079 Purchase Intent* | Consumer Packaged Goods | Barbeque Sauce | Purchase Intent* Consumer Packaged Goods Edible General Food Condiments & Sauces Barbeque Sauce See *Purchase Intent Classification* Extension + 1081 1079 Purchase Intent* | Consumer Packaged Goods | Gravy/Sauce Mixes | Purchase Intent* Consumer Packaged Goods Edible General Food Condiments & Sauces Gravy/Sauce Mixes See *Purchase Intent Classification* Extension + 1082 1079 Purchase Intent* | Consumer Packaged Goods | Jellies/Jam/Honey | Purchase Intent* Consumer Packaged Goods Edible General Food Condiments & Sauces Jellies/Jam/Honey See *Purchase Intent Classification* Extension + 1083 1079 Purchase Intent* | Consumer Packaged Goods | Mayonnaise | Purchase Intent* Consumer Packaged Goods Edible General Food Condiments & Sauces Mayonnaise See *Purchase Intent Classification* Extension + 1084 1079 Purchase Intent* | Consumer Packaged Goods | Mustard & Ketchup | Purchase Intent* Consumer Packaged Goods Edible General Food Condiments & Sauces Mustard & Ketchup See *Purchase Intent Classification* Extension + 1085 1079 Purchase Intent* | Consumer Packaged Goods | Nut Butter | Purchase Intent* Consumer Packaged Goods Edible General Food Condiments & Sauces Nut Butter See *Purchase Intent Classification* Extension + 1086 1079 Purchase Intent* | Consumer Packaged Goods | Other Sauces | Purchase Intent* Consumer Packaged Goods Edible General Food Condiments & Sauces Other Sauces See *Purchase Intent Classification* Extension + 1087 1079 Purchase Intent* | Consumer Packaged Goods | Pickles/Relish/Olives | Purchase Intent* Consumer Packaged Goods Edible General Food Condiments & Sauces Pickles/Relish/Olives See *Purchase Intent Classification* Extension + 1088 1079 Purchase Intent* | Consumer Packaged Goods | Salad Dressing | Purchase Intent* Consumer Packaged Goods Edible General Food Condiments & Sauces Salad Dressing See *Purchase Intent Classification* Extension + 1089 1079 Purchase Intent* | Consumer Packaged Goods | Salad Toppings and Croutons | Purchase Intent* Consumer Packaged Goods Edible General Food Condiments & Sauces Salad Toppings and Croutons See *Purchase Intent Classification* Extension + 1090 1079 Purchase Intent* | Consumer Packaged Goods | Steak/Worcestershire Sauce | Purchase Intent* Consumer Packaged Goods Edible General Food Condiments & Sauces Steak/Worcestershire Sauce See *Purchase Intent Classification* Extension + 1091 1079 Purchase Intent* | Consumer Packaged Goods | Vinegar | Purchase Intent* Consumer Packaged Goods Edible General Food Condiments & Sauces Vinegar See *Purchase Intent Classification* Extension + 1092 1036 Purchase Intent* | Consumer Packaged Goods | Cookies & Crackers | Purchase Intent* Consumer Packaged Goods Edible General Food Cookies & Crackers See *Purchase Intent Classification* Extension + 1093 1092 Purchase Intent* | Consumer Packaged Goods | Cookies | Purchase Intent* Consumer Packaged Goods Edible General Food Cookies & Crackers Cookies See *Purchase Intent Classification* Extension + 1094 1092 Purchase Intent* | Consumer Packaged Goods | Crackers | Purchase Intent* Consumer Packaged Goods Edible General Food Cookies & Crackers Crackers See *Purchase Intent Classification* Extension + 1095 1036 Purchase Intent* | Consumer Packaged Goods | Ethnic | Purchase Intent* Consumer Packaged Goods Edible General Food Ethnic See *Purchase Intent Classification* Extension + 1096 1095 Purchase Intent* | Consumer Packaged Goods | Asian food | Purchase Intent* Consumer Packaged Goods Edible General Food Ethnic Asian food See *Purchase Intent Classification* Extension + 1097 1095 Purchase Intent* | Consumer Packaged Goods | Matzoh Food | Purchase Intent* Consumer Packaged Goods Edible General Food Ethnic Matzoh Food See *Purchase Intent Classification* Extension + 1098 1095 Purchase Intent* | Consumer Packaged Goods | Mexican Foods | Purchase Intent* Consumer Packaged Goods Edible General Food Ethnic Mexican Foods See *Purchase Intent Classification* Extension + 1099 1095 Purchase Intent* | Consumer Packaged Goods | Mexican Sauce | Purchase Intent* Consumer Packaged Goods Edible General Food Ethnic Mexican Sauce See *Purchase Intent Classification* Extension + 1100 1036 Purchase Intent* | Consumer Packaged Goods | Meals | Purchase Intent* Consumer Packaged Goods Edible General Food Meals See *Purchase Intent Classification* Extension + 1101 1100 Purchase Intent* | Consumer Packaged Goods | Baked Beans/Canned Bread | Purchase Intent* Consumer Packaged Goods Edible General Food Meals Baked Beans/Canned Bread See *Purchase Intent Classification* Extension + 1102 1100 Purchase Intent* | Consumer Packaged Goods | Breadcrumbs/Batters | Purchase Intent* Consumer Packaged Goods Edible General Food Meals Breadcrumbs/Batters See *Purchase Intent Classification* Extension + 1103 1100 Purchase Intent* | Consumer Packaged Goods | Dinners | Purchase Intent* Consumer Packaged Goods Edible General Food Meals Dinners See *Purchase Intent Classification* Extension + 1104 1100 Purchase Intent* | Consumer Packaged Goods | Dry Packaged Dinner Mixes | Purchase Intent* Consumer Packaged Goods Edible General Food Meals Dry Packaged Dinner Mixes See *Purchase Intent Classification* Extension + 1105 1100 Purchase Intent* | Consumer Packaged Goods | Grated Cheese | Purchase Intent* Consumer Packaged Goods Edible General Food Meals Grated Cheese See *Purchase Intent Classification* Extension + 1106 1100 Purchase Intent* | Consumer Packaged Goods | Instant Potatoes | Purchase Intent* Consumer Packaged Goods Edible General Food Meals Instant Potatoes See *Purchase Intent Classification* Extension + 1107 1100 Purchase Intent* | Consumer Packaged Goods | Meat | Purchase Intent* Consumer Packaged Goods Edible General Food Meals Meat See *Purchase Intent Classification* Extension + 1108 1100 Purchase Intent* | Consumer Packaged Goods | Pasta | Purchase Intent* Consumer Packaged Goods Edible General Food Meals Pasta See *Purchase Intent Classification* Extension + 1109 1100 Purchase Intent* | Consumer Packaged Goods | Pizza Products | Purchase Intent* Consumer Packaged Goods Edible General Food Meals Pizza Products See *Purchase Intent Classification* Extension + 1110 1100 Purchase Intent* | Consumer Packaged Goods | Rice | Purchase Intent* Consumer Packaged Goods Edible General Food Meals Rice See *Purchase Intent Classification* Extension + 1111 1100 Purchase Intent* | Consumer Packaged Goods | Seafood | Purchase Intent* Consumer Packaged Goods Edible General Food Meals Seafood See *Purchase Intent Classification* Extension + 1112 1100 Purchase Intent* | Consumer Packaged Goods | Soup | Purchase Intent* Consumer Packaged Goods Edible General Food Meals Soup See *Purchase Intent Classification* Extension + 1113 1100 Purchase Intent* | Consumer Packaged Goods | Spaghetti/Italian Sauce | Purchase Intent* Consumer Packaged Goods Edible General Food Meals Spaghetti/Italian Sauce See *Purchase Intent Classification* Extension + 1114 1100 Purchase Intent* | Consumer Packaged Goods | Stuffing Mixes | Purchase Intent* Consumer Packaged Goods Edible General Food Meals Stuffing Mixes See *Purchase Intent Classification* Extension + 1115 1036 Purchase Intent* | Consumer Packaged Goods | Snacks | Purchase Intent* Consumer Packaged Goods Edible General Food Snacks See *Purchase Intent Classification* Extension + 1116 1115 Purchase Intent* | Consumer Packaged Goods | Dip/Dip Mixes | Purchase Intent* Consumer Packaged Goods Edible General Food Snacks Dip/Dip Mixes See *Purchase Intent Classification* Extension + 1117 1115 Purchase Intent* | Consumer Packaged Goods | Dried Meat Snacks | Purchase Intent* Consumer Packaged Goods Edible General Food Snacks Dried Meat Snacks See *Purchase Intent Classification* Extension + 1118 1115 Purchase Intent* | Consumer Packaged Goods | Dry Fruit Snacks | Purchase Intent* Consumer Packaged Goods Edible General Food Snacks Dry Fruit Snacks See *Purchase Intent Classification* Extension + 1119 1115 Purchase Intent* | Consumer Packaged Goods | Other Snacks | Purchase Intent* Consumer Packaged Goods Edible General Food Snacks Other Snacks See *Purchase Intent Classification* Extension + 1120 1115 Purchase Intent* | Consumer Packaged Goods | Popcorn/Popcorn Oil | Purchase Intent* Consumer Packaged Goods Edible General Food Snacks Popcorn/Popcorn Oil See *Purchase Intent Classification* Extension + 1121 1115 Purchase Intent* | Consumer Packaged Goods | Rice/Popcorn Cakes | Purchase Intent* Consumer Packaged Goods Edible General Food Snacks Rice/Popcorn Cakes See *Purchase Intent Classification* Extension + 1122 1115 Purchase Intent* | Consumer Packaged Goods | Salty Snacks | Purchase Intent* Consumer Packaged Goods Edible General Food Snacks Salty Snacks See *Purchase Intent Classification* Extension + 1123 1115 Purchase Intent* | Consumer Packaged Goods | Snack Bars/Granola Bars | Purchase Intent* Consumer Packaged Goods Edible General Food Snacks Snack Bars/Granola Bars See *Purchase Intent Classification* Extension + 1124 1115 Purchase Intent* | Consumer Packaged Goods | Snack Nuts/Seeds/Corn Nuts | Purchase Intent* Consumer Packaged Goods Edible General Food Snacks Snack Nuts/Seeds/Corn Nuts See *Purchase Intent Classification* Extension + 1125 1036 Purchase Intent* | Consumer Packaged Goods | Fruit | Purchase Intent* Consumer Packaged Goods Edible General Food Fruit See *Purchase Intent Classification* Extension + 1126 1125 Purchase Intent* | Consumer Packaged Goods | Canned/Bottled Fruit | Purchase Intent* Consumer Packaged Goods Edible General Food Fruit Canned/Bottled Fruit See *Purchase Intent Classification* Extension + 1127 1125 Purchase Intent* | Consumer Packaged Goods | Dried Fruit | Purchase Intent* Consumer Packaged Goods Edible General Food Fruit Dried Fruit See *Purchase Intent Classification* Extension + 1128 1036 Purchase Intent* | Consumer Packaged Goods | Vegetables | Purchase Intent* Consumer Packaged Goods Edible General Food Vegetables See *Purchase Intent Classification* Extension + 1129 1128 Purchase Intent* | Consumer Packaged Goods | Dry Beans/Vegetables | Purchase Intent* Consumer Packaged Goods Edible General Food Vegetables Dry Beans/Vegetables See *Purchase Intent Classification* Extension + 1130 1128 Purchase Intent* | Consumer Packaged Goods | Tomato Products | Purchase Intent* Consumer Packaged Goods Edible General Food Vegetables Tomato Products See *Purchase Intent Classification* Extension + 1131 1128 Purchase Intent* | Consumer Packaged Goods | Vegetables | Purchase Intent* Consumer Packaged Goods Edible General Food Vegetables Vegetables See *Purchase Intent Classification* Extension + 1132 973 Purchase Intent* | Consumer Packaged Goods | Refrigerated | Purchase Intent* Consumer Packaged Goods Edible Refrigerated See *Purchase Intent Classification* Extension + 1133 1132 Purchase Intent* | Consumer Packaged Goods | Dairy | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Dairy See *Purchase Intent Classification* Extension + 1134 1133 Purchase Intent* | Consumer Packaged Goods | Butter/Butter Blends | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Dairy Butter/Butter Blends See *Purchase Intent Classification* Extension + 1135 1133 Purchase Intent* | Consumer Packaged Goods | Cottage Cheese | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Dairy Cottage Cheese See *Purchase Intent Classification* Extension + 1136 1133 Purchase Intent* | Consumer Packaged Goods | Cream Cheese/Cr Spread | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Dairy Cream Cheese/Cr Spread See *Purchase Intent Classification* Extension + 1137 1133 Purchase Intent* | Consumer Packaged Goods | Creams/Creamers | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Dairy Creams/Creamers See *Purchase Intent Classification* Extension + 1138 1133 Purchase Intent* | Consumer Packaged Goods | Fresh Eggs | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Dairy Fresh Eggs See *Purchase Intent Classification* Extension + 1139 1133 Purchase Intent* | Consumer Packaged Goods | Margarine/Spreads | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Dairy Margarine/Spreads See *Purchase Intent Classification* Extension + 1140 1133 Purchase Intent* | Consumer Packaged Goods | Milk | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Dairy Milk See *Purchase Intent Classification* Extension + 1141 1133 Purchase Intent* | Consumer Packaged Goods | Natural Cheese | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Dairy Natural Cheese See *Purchase Intent Classification* Extension + 1142 1133 Purchase Intent* | Consumer Packaged Goods | Processed Cheese | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Dairy Processed Cheese See *Purchase Intent Classification* Extension + 1143 1133 Purchase Intent* | Consumer Packaged Goods | Sour Cream | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Dairy Sour Cream See *Purchase Intent Classification* Extension + 1144 1133 Purchase Intent* | Consumer Packaged Goods | Whipped Toppings | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Dairy Whipped Toppings See *Purchase Intent Classification* Extension + 1145 1133 Purchase Intent* | Consumer Packaged Goods | Yogurt | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Dairy Yogurt See *Purchase Intent Classification* Extension + 1146 1132 Purchase Intent* | Consumer Packaged Goods | Other Refrigerated | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Other Refrigerated See *Purchase Intent Classification* Extension + 1147 1146 Purchase Intent* | Consumer Packaged Goods | Lard | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Other Refrigerated Lard See *Purchase Intent Classification* Extension + 1148 1146 Purchase Intent* | Consumer Packaged Goods | Tortilla/Eggroll/Wonton Wrap | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Other Refrigerated Tortilla/Eggroll/Wonton Wrap See *Purchase Intent Classification* Extension + 1149 1132 Purchase Intent* | Consumer Packaged Goods | Refrigerated Baked Goods | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Baked Goods See *Purchase Intent Classification* Extension + 1150 1149 Purchase Intent* | Consumer Packaged Goods | Baked Goods | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Baked Goods Baked Goods See *Purchase Intent Classification* Extension + 1151 1132 Purchase Intent* | Consumer Packaged Goods | Refrigerated Beverages | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Beverages See *Purchase Intent Classification* Extension + 1152 1151 Purchase Intent* | Consumer Packaged Goods | Juices/Drinks | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Beverages Juices/Drinks See *Purchase Intent Classification* Extension + 1153 1151 Purchase Intent* | Consumer Packaged Goods | Tea/Coffee | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Beverages Tea/Coffee See *Purchase Intent Classification* Extension + 1154 1132 Purchase Intent* | Consumer Packaged Goods | Refrigerated Condiments | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Condiments See *Purchase Intent Classification* Extension + 1155 1154 Purchase Intent* | Consumer Packaged Goods | Dips | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Condiments Dips See *Purchase Intent Classification* Extension + 1156 1154 Purchase Intent* | Consumer Packaged Goods | Other Condiments | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Condiments Other Condiments See *Purchase Intent Classification* Extension + 1157 1154 Purchase Intent* | Consumer Packaged Goods | Pickles/Relish | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Condiments Pickles/Relish See *Purchase Intent Classification* Extension + 1158 1154 Purchase Intent* | Consumer Packaged Goods | Salad Dressing | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Condiments Salad Dressing See *Purchase Intent Classification* Extension + 1159 1154 Purchase Intent* | Consumer Packaged Goods | Spreads | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Condiments Spreads See *Purchase Intent Classification* Extension + 1160 1132 Purchase Intent* | Consumer Packaged Goods | Refrigerated Desserts | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Desserts See *Purchase Intent Classification* Extension + 1161 1160 Purchase Intent* | Consumer Packaged Goods | Cheesecakes | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Desserts Cheesecakes See *Purchase Intent Classification* Extension + 1162 1160 Purchase Intent* | Consumer Packaged Goods | Desserts | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Desserts Desserts See *Purchase Intent Classification* Extension + 1163 1132 Purchase Intent* | Consumer Packaged Goods | Refrigerated Dough | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Dough See *Purchase Intent Classification* Extension + 1164 1163 Purchase Intent* | Consumer Packaged Goods | Dough/Biscuit Dough | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Dough Dough/Biscuit Dough See *Purchase Intent Classification* Extension + 1165 1163 Purchase Intent* | Consumer Packaged Goods | Pizza | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Dough Pizza See *Purchase Intent Classification* Extension + 1166 1132 Purchase Intent* | Consumer Packaged Goods | Refrigerated Meals | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Meals See *Purchase Intent Classification* Extension + 1167 1166 Purchase Intent* | Consumer Packaged Goods | Entrees | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Meals Entrees See *Purchase Intent Classification* Extension + 1168 1166 Purchase Intent* | Consumer Packaged Goods | Lunches | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Meals Lunches See *Purchase Intent Classification* Extension + 1169 1166 Purchase Intent* | Consumer Packaged Goods | Meat Pies | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Meals Meat Pies See *Purchase Intent Classification* Extension + 1170 1166 Purchase Intent* | Consumer Packaged Goods | Pasta | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Meals Pasta See *Purchase Intent Classification* Extension + 1171 1166 Purchase Intent* | Consumer Packaged Goods | Side Dishes | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Meals Side Dishes See *Purchase Intent Classification* Extension + 1172 1166 Purchase Intent* | Consumer Packaged Goods | Breakfast Meats | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Meats Breakfast Meats See *Purchase Intent Classification* Extension + 1173 1166 Purchase Intent* | Consumer Packaged Goods | Dinner Sausage | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Meats Dinner Sausage See *Purchase Intent Classification* Extension + 1174 1166 Purchase Intent* | Consumer Packaged Goods | Frankfurters | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Meats Frankfurters See *Purchase Intent Classification* Extension + 1175 1166 Purchase Intent* | Consumer Packaged Goods | Ham | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Meats Ham See *Purchase Intent Classification* Extension + 1176 1166 Purchase Intent* | Consumer Packaged Goods | Luncheon Meats | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Meats Luncheon Meats See *Purchase Intent Classification* Extension + 1177 1166 Purchase Intent* | Consumer Packaged Goods | Meat | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Meats Meat See *Purchase Intent Classification* Extension + 1178 1166 Purchase Intent* | Consumer Packaged Goods | Seafood | Purchase Intent* Consumer Packaged Goods Edible Refrigerated Refrigerated Meats Seafood See *Purchase Intent Classification* Extension + 1179 972 Purchase Intent* | Consumer Packaged Goods | Non-edible | Purchase Intent* Consumer Packaged Goods Non-edible See *Purchase Intent Classification* Extension + 1180 1179 Purchase Intent* | Consumer Packaged Goods | Beauty | Purchase Intent* Consumer Packaged Goods Non-edible Beauty See *Purchase Intent Classification* Extension + 1181 1180 Purchase Intent* | Consumer Packaged Goods | Cosmetics | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Cosmetics See *Purchase Intent Classification* Extension + 1182 1181 Purchase Intent* | Consumer Packaged Goods | Storage | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Cosmetics Storage See *Purchase Intent Classification* Extension + 1183 1181 Purchase Intent* | Consumer Packaged Goods | Eye | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Cosmetics Eye See *Purchase Intent Classification* Extension + 1184 1181 Purchase Intent* | Consumer Packaged Goods | Facial | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Cosmetics Facial See *Purchase Intent Classification* Extension + 1185 1181 Purchase Intent* | Consumer Packaged Goods | Lip | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Cosmetics Lip See *Purchase Intent Classification* Extension + 1186 1181 Purchase Intent* | Consumer Packaged Goods | Cosmetics-Nail | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Cosmetics Cosmetics-Nail See *Purchase Intent Classification* Extension + 1187 1181 Purchase Intent* | Consumer Packaged Goods | Cosmetics Accessories | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Cosmetics Cosmetics Accessories See *Purchase Intent Classification* Extension + 1188 1180 Purchase Intent* | Consumer Packaged Goods | Fragrance | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Fragrance See *Purchase Intent Classification* Extension + 1189 1188 Purchase Intent* | Consumer Packaged Goods | Fragrances - Women's | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Fragrance Fragrances - Women's See *Purchase Intent Classification* Extension + 1190 1188 Purchase Intent* | Consumer Packaged Goods | Shaving Lotion/Men's Fragrance | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Fragrance Shaving Lotion/Men's Fragrance See *Purchase Intent Classification* Extension + 1191 1180 Purchase Intent* | Consumer Packaged Goods | Grooming Supplies | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Grooming Supplies See *Purchase Intent Classification* Extension + 1192 1191 Purchase Intent* | Consumer Packaged Goods | Cotton Balls/Swabs | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Grooming Supplies Cotton Balls/Swabs See *Purchase Intent Classification* Extension + 1193 1191 Purchase Intent* | Consumer Packaged Goods | Electric Shaver Groomer | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Grooming Supplies Electric Shaver Groomer See *Purchase Intent Classification* Extension + 1194 1191 Purchase Intent* | Consumer Packaged Goods | Hair Appliances | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Grooming Supplies Hair Appliances See *Purchase Intent Classification* Extension + 1195 1191 Purchase Intent* | Consumer Packaged Goods | Other Grooming Supplies | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Grooming Supplies Other Grooming Supplies See *Purchase Intent Classification* Extension + 1196 1180 Purchase Intent* | Consumer Packaged Goods | Hair Care | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Hair Care See *Purchase Intent Classification* Extension + 1197 1196 Purchase Intent* | Consumer Packaged Goods | Hair Accessories | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Hair Care Hair Accessories See *Purchase Intent Classification* Extension + 1198 1196 Purchase Intent* | Consumer Packaged Goods | Hair Coloring | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Hair Care Hair Coloring See *Purchase Intent Classification* Extension + 1199 1196 Purchase Intent* | Consumer Packaged Goods | Hair Conditioner | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Hair Care Hair Conditioner See *Purchase Intent Classification* Extension + 1200 1196 Purchase Intent* | Consumer Packaged Goods | Hair Growth Products | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Hair Care Hair Growth Products See *Purchase Intent Classification* Extension + 1201 1196 Purchase Intent* | Consumer Packaged Goods | Hair Spray/Spritz | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Hair Care Hair Spray/Spritz See *Purchase Intent Classification* Extension + 1202 1196 Purchase Intent* | Consumer Packaged Goods | Hair Styling Gel/Mousse | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Hair Care Hair Styling Gel/Mousse See *Purchase Intent Classification* Extension + 1203 1196 Purchase Intent* | Consumer Packaged Goods | Home Permanent/Relaxer Kits | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Hair Care Home Permanent/Relaxer Kits See *Purchase Intent Classification* Extension + 1204 1196 Purchase Intent* | Consumer Packaged Goods | Shampoo | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Hair Care Shampoo See *Purchase Intent Classification* Extension + 1205 1180 Purchase Intent* | Consumer Packaged Goods | Personal Cleansing | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Personal Cleansing See *Purchase Intent Classification* Extension + 1206 1205 Purchase Intent* | Consumer Packaged Goods | Bath Products | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Personal Cleansing Bath Products See *Purchase Intent Classification* Extension + 1207 1205 Purchase Intent* | Consumer Packaged Goods | Bath/Body Scrubbers/Massagers | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Personal Cleansing Bath/Body Scrubbers/Massagers See *Purchase Intent Classification* Extension + 1208 1205 Purchase Intent* | Consumer Packaged Goods | Deodorant | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Personal Cleansing Deodorant See *Purchase Intent Classification* Extension + 1209 1205 Purchase Intent* | Consumer Packaged Goods | Moist Towelettes | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Personal Cleansing Moist Towelettes See *Purchase Intent Classification* Extension + 1210 1205 Purchase Intent* | Consumer Packaged Goods | Soap | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Personal Cleansing Soap See *Purchase Intent Classification* Extension + 1211 1180 Purchase Intent* | Consumer Packaged Goods | Shaving | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Shaving See *Purchase Intent Classification* Extension + 1212 1211 Purchase Intent* | Consumer Packaged Goods | Blades | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Shaving Blades See *Purchase Intent Classification* Extension + 1213 1211 Purchase Intent* | Consumer Packaged Goods | Razors | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Shaving Razors See *Purchase Intent Classification* Extension + 1214 1211 Purchase Intent* | Consumer Packaged Goods | Shaving Cream | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Shaving Shaving Cream See *Purchase Intent Classification* Extension + 1215 1180 Purchase Intent* | Consumer Packaged Goods | Skin Care | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Skin Care See *Purchase Intent Classification* Extension + 1216 1215 Purchase Intent* | Consumer Packaged Goods | Hand & Body Lotion | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Skin Care Hand & Body Lotion See *Purchase Intent Classification* Extension + 1217 1215 Purchase Intent* | Consumer Packaged Goods | Skin Care | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Skin Care Skin Care See *Purchase Intent Classification* Extension + 1218 1215 Purchase Intent* | Consumer Packaged Goods | Suntan Products | Purchase Intent* Consumer Packaged Goods Non-edible Beauty Skin Care Suntan Products See *Purchase Intent Classification* Extension + 1219 1179 Purchase Intent* | Consumer Packaged Goods | General Merchandise | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise See *Purchase Intent Classification* Extension + 1220 1219 Purchase Intent* | Consumer Packaged Goods | Automotive | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Automotive See *Purchase Intent Classification* Extension + 1221 1220 Purchase Intent* | Consumer Packaged Goods | Automobile Fluids/Antifreeze | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Automotive Automobile Fluids/Antifreeze See *Purchase Intent Classification* Extension + 1222 1220 Purchase Intent* | Consumer Packaged Goods | Automobile Waxes/Polishes | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Automotive Automobile Waxes/Polishes See *Purchase Intent Classification* Extension + 1223 1220 Purchase Intent* | Consumer Packaged Goods | Motor Oil | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Automotive Motor Oil See *Purchase Intent Classification* Extension + 1224 1219 Purchase Intent* | Consumer Packaged Goods | Barbeque | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Barbeque See *Purchase Intent Classification* Extension + 1225 1224 Purchase Intent* | Consumer Packaged Goods | Charcoal | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Barbeque Charcoal See *Purchase Intent Classification* Extension + 1226 1224 Purchase Intent* | Consumer Packaged Goods | Charcoal Lighter Fluids | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Barbeque Charcoal Lighter Fluids See *Purchase Intent Classification* Extension + 1227 1219 Purchase Intent* | Consumer Packaged Goods | Disposable Tableware | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Disposable Tableware See *Purchase Intent Classification* Extension + 1228 1227 Purchase Intent* | Consumer Packaged Goods | Cups & Plates | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Disposable Tableware Cups & Plates See *Purchase Intent Classification* Extension + 1229 1227 Purchase Intent* | Consumer Packaged Goods | Disposable Tableware | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Disposable Tableware Disposable Tableware See *Purchase Intent Classification* Extension + 1230 1219 Purchase Intent* | Consumer Packaged Goods | Electronics/Photography | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Electronics/Photography See *Purchase Intent Classification* Extension + 1231 1230 Purchase Intent* | Consumer Packaged Goods | Batteries | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Electronics/Photography Batteries See *Purchase Intent Classification* Extension + 1232 1230 Purchase Intent* | Consumer Packaged Goods | Blank Audio/Video Media | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Electronics/Photography Blank Audio/Video Media See *Purchase Intent Classification* Extension + 1233 1230 Purchase Intent* | Consumer Packaged Goods | Photography Supplies | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Electronics/Photography Photography Supplies See *Purchase Intent Classification* Extension + 1234 1219 Purchase Intent* | Consumer Packaged Goods | Foils, Wraps, & Bags | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Foils, Wraps, & Bags See *Purchase Intent Classification* Extension + 1235 1234 Purchase Intent* | Consumer Packaged Goods | Foil Pans | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Foils, Wraps, & Bags Foil Pans See *Purchase Intent Classification* Extension + 1236 1234 Purchase Intent* | Consumer Packaged Goods | Foils & Wraps | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Foils, Wraps, & Bags Foils & Wraps See *Purchase Intent Classification* Extension + 1237 1234 Purchase Intent* | Consumer Packaged Goods | Food & Trash Bags | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Foils, Wraps, & Bags Food & Trash Bags See *Purchase Intent Classification* Extension + 1238 1219 Purchase Intent* | Consumer Packaged Goods | Hosiery | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Hosiery See *Purchase Intent Classification* Extension + 1239 1238 Purchase Intent* | Consumer Packaged Goods | Pantyhose/Nylons | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Hosiery Pantyhose/Nylons See *Purchase Intent Classification* Extension + 1240 1238 Purchase Intent* | Consumer Packaged Goods | Socks | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Hosiery Socks See *Purchase Intent Classification* Extension + 1241 1238 Purchase Intent* | Consumer Packaged Goods | Tights | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Hosiery Tights See *Purchase Intent Classification* Extension + 1242 1219 Purchase Intent* | Consumer Packaged Goods | Household/Plastics/Storage | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Household/Plastics/Storage See *Purchase Intent Classification* Extension + 1243 1242 Purchase Intent* | Consumer Packaged Goods | Bottles | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Household/Plastics/Storage Bottles See *Purchase Intent Classification* Extension + 1244 1242 Purchase Intent* | Consumer Packaged Goods | Drinkware | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Household/Plastics/Storage Drinkware See *Purchase Intent Classification* Extension + 1245 1242 Purchase Intent* | Consumer Packaged Goods | Household Plastics | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Household/Plastics/Storage Household Plastics See *Purchase Intent Classification* Extension + 1246 1242 Purchase Intent* | Consumer Packaged Goods | Kitchen Storage | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Household/Plastics/Storage Kitchen Storage See *Purchase Intent Classification* Extension + 1247 1242 Purchase Intent* | Consumer Packaged Goods | Soap Dishes | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Household/Plastics/Storage Soap Dishes See *Purchase Intent Classification* Extension + 1248 1219 Purchase Intent* | Consumer Packaged Goods | Miscellaneous General Merch | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Miscellaneous General Merch See *Purchase Intent Classification* Extension + 1249 1248 Purchase Intent* | Consumer Packaged Goods | Candles | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Miscellaneous General Merch Candles See *Purchase Intent Classification* Extension + 1250 1248 Purchase Intent* | Consumer Packaged Goods | Cloth Dye | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Miscellaneous General Merch Cloth Dye See *Purchase Intent Classification* Extension + 1251 1248 Purchase Intent* | Consumer Packaged Goods | Culinary | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Miscellaneous General Merch Culinary See *Purchase Intent Classification* Extension + 1252 1248 Purchase Intent* | Consumer Packaged Goods | Firelog/Firestarter/Firewood | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Miscellaneous General Merch Firelog/Firestarter/Firewood See *Purchase Intent Classification* Extension + 1253 1248 Purchase Intent* | Consumer Packaged Goods | Flashlights | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Miscellaneous General Merch Flashlights See *Purchase Intent Classification* Extension + 1254 1248 Purchase Intent* | Consumer Packaged Goods | Frozen & Dry Ice | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Miscellaneous General Merch Frozen & Dry Ice See *Purchase Intent Classification* Extension + 1255 1248 Purchase Intent* | Consumer Packaged Goods | Gloves | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Miscellaneous General Merch Gloves See *Purchase Intent Classification* Extension + 1256 1248 Purchase Intent* | Consumer Packaged Goods | Household Lubricants | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Miscellaneous General Merch Household Lubricants See *Purchase Intent Classification* Extension + 1257 1248 Purchase Intent* | Consumer Packaged Goods | Ice Substitute | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Miscellaneous General Merch Ice Substitute See *Purchase Intent Classification* Extension + 1258 1248 Purchase Intent* | Consumer Packaged Goods | Light Bulbs | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Miscellaneous General Merch Light Bulbs See *Purchase Intent Classification* Extension + 1259 1248 Purchase Intent* | Consumer Packaged Goods | Lighters | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Miscellaneous General Merch Lighters See *Purchase Intent Classification* Extension + 1260 1248 Purchase Intent* | Consumer Packaged Goods | Matches | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Miscellaneous General Merch Matches See *Purchase Intent Classification* Extension + 1261 1248 Purchase Intent* | Consumer Packaged Goods | Outdoor/Lawn Fertilizer/Weed Killer | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Miscellaneous General Merch Outdoor/Lawn Fertilizer/Weed Killer See *Purchase Intent Classification* Extension + 1262 1248 Purchase Intent* | Consumer Packaged Goods | Playing Cards | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Miscellaneous General Merch Playing Cards See *Purchase Intent Classification* Extension + 1263 1248 Purchase Intent* | Consumer Packaged Goods | Pool/Spa Chemicals | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Miscellaneous General Merch Pool/Spa Chemicals See *Purchase Intent Classification* Extension + 1264 1248 Purchase Intent* | Consumer Packaged Goods | Shoe Polish & Accessories | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Miscellaneous General Merch Shoe Polish & Accessories See *Purchase Intent Classification* Extension + 1265 1248 Purchase Intent* | Consumer Packaged Goods | Vacuum Bags/Belts | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Miscellaneous General Merch Vacuum Bags/Belts See *Purchase Intent Classification* Extension + 1266 1219 Purchase Intent* | Consumer Packaged Goods | Office/School Supplies | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Office/School Supplies See *Purchase Intent Classification* Extension + 1267 1266 Purchase Intent* | Consumer Packaged Goods | Children's Art Supplies | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Office/School Supplies Children's Art Supplies See *Purchase Intent Classification* Extension + 1268 1266 Purchase Intent* | Consumer Packaged Goods | Computer Disks Frmtd/UnFrmtd | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Office/School Supplies Computer Disks Frmtd/UnFrmtd See *Purchase Intent Classification* Extension + 1269 1266 Purchase Intent* | Consumer Packaged Goods | Office Products | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Office/School Supplies Office Products See *Purchase Intent Classification* Extension + 1270 1266 Purchase Intent* | Consumer Packaged Goods | Writing Instruments | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Office/School Supplies Writing Instruments See *Purchase Intent Classification* Extension + 1271 1219 Purchase Intent* | Consumer Packaged Goods | Paper Products | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Paper Products See *Purchase Intent Classification* Extension + 1272 1271 Purchase Intent* | Consumer Packaged Goods | Facial Tissue | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Paper Products Facial Tissue See *Purchase Intent Classification* Extension + 1273 1271 Purchase Intent* | Consumer Packaged Goods | Paper Napkins | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Paper Products Paper Napkins See *Purchase Intent Classification* Extension + 1274 1271 Purchase Intent* | Consumer Packaged Goods | Paper Towels | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Paper Products Paper Towels See *Purchase Intent Classification* Extension + 1275 1271 Purchase Intent* | Consumer Packaged Goods | Toilet Tissue | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Paper Products Toilet Tissue See *Purchase Intent Classification* Extension + 1276 1219 Purchase Intent* | Consumer Packaged Goods | Pest Control | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Pest Control See *Purchase Intent Classification* Extension + 1277 1276 Purchase Intent* | Consumer Packaged Goods | Outdoor Insect/Rodent Control Chem | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Pest Control Outdoor Insect/Rodent Control Chem See *Purchase Intent Classification* Extension + 1278 1276 Purchase Intent* | Consumer Packaged Goods | Pest Control | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Pest Control Pest Control See *Purchase Intent Classification* Extension + 1279 1219 Purchase Intent* | Consumer Packaged Goods | Pet Care | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Pet Care See *Purchase Intent Classification* Extension + 1280 1279 Purchase Intent* | Consumer Packaged Goods | Cat/Dog Litter | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Pet Care Cat/Dog Litter See *Purchase Intent Classification* Extension + 1281 1279 Purchase Intent* | Consumer Packaged Goods | Pet Food | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Pet Care Pet Food See *Purchase Intent Classification* Extension + 1282 1279 Purchase Intent* | Consumer Packaged Goods | Pet Supplies | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Pet Care Pet Supplies See *Purchase Intent Classification* Extension + 1283 1279 Purchase Intent* | Consumer Packaged Goods | Pet Treats | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Pet Care Pet Treats See *Purchase Intent Classification* Extension + 1284 1219 Purchase Intent* | Consumer Packaged Goods | Water Treatment | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Water Treatment See *Purchase Intent Classification* Extension + 1285 1284 Purchase Intent* | Consumer Packaged Goods | Water Filter/Devices | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Water Treatment Water Filter/Devices See *Purchase Intent Classification* Extension + 1286 1284 Purchase Intent* | Consumer Packaged Goods | Water Softeners/Treatment | Purchase Intent* Consumer Packaged Goods Non-edible General Merchandise Water Treatment Water Softeners/Treatment See *Purchase Intent Classification* Extension + 1287 1179 Purchase Intent* | Consumer Packaged Goods | Household Appliances | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances See *Purchase Intent Classification* Extension + 1288 1287 Purchase Intent* | Consumer Packaged Goods | Air Conditioners | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Air Conditioners See *Purchase Intent Classification* Extension + 1289 1287 Purchase Intent* | Consumer Packaged Goods | Air Purifiers | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Air Purifiers See *Purchase Intent Classification* Extension + 1290 1287 Purchase Intent* | Consumer Packaged Goods | Blenders | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Blenders See *Purchase Intent Classification* Extension + 1291 1287 Purchase Intent* | Consumer Packaged Goods | Breadmakers | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Breadmakers See *Purchase Intent Classification* Extension + 1292 1287 Purchase Intent* | Consumer Packaged Goods | Coffee Grinders | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Coffee Grinders See *Purchase Intent Classification* Extension + 1293 1287 Purchase Intent* | Consumer Packaged Goods | Coffee Makers | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Coffee Makers See *Purchase Intent Classification* Extension + 1294 1287 Purchase Intent* | Consumer Packaged Goods | Deep Fryers | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Deep Fryers See *Purchase Intent Classification* Extension + 1295 1287 Purchase Intent* | Consumer Packaged Goods | Dehumidifiers | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Dehumidifiers See *Purchase Intent Classification* Extension + 1296 1287 Purchase Intent* | Consumer Packaged Goods | Dishwashers | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Dishwashers See *Purchase Intent Classification* Extension + 1297 1287 Purchase Intent* | Consumer Packaged Goods | Espresso Machines | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Espresso Machines See *Purchase Intent Classification* Extension + 1298 1287 Purchase Intent* | Consumer Packaged Goods | Fans | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Fans See *Purchase Intent Classification* Extension + 1299 1287 Purchase Intent* | Consumer Packaged Goods | Food Processors | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Food Processors See *Purchase Intent Classification* Extension + 1300 1287 Purchase Intent* | Consumer Packaged Goods | Freezers | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Freezers See *Purchase Intent Classification* Extension + 1301 1287 Purchase Intent* | Consumer Packaged Goods | Heaters | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Heaters See *Purchase Intent Classification* Extension + 1302 1287 Purchase Intent* | Consumer Packaged Goods | Humidifiers | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Humidifiers See *Purchase Intent Classification* Extension + 1303 1287 Purchase Intent* | Consumer Packaged Goods | Ice Cream Makers | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Ice Cream Makers See *Purchase Intent Classification* Extension + 1304 1287 Purchase Intent* | Consumer Packaged Goods | Juicers | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Juicers See *Purchase Intent Classification* Extension + 1305 1287 Purchase Intent* | Consumer Packaged Goods | Microwave Ovens | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Microwave Ovens See *Purchase Intent Classification* Extension + 1306 1287 Purchase Intent* | Consumer Packaged Goods | Mixers | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Mixers See *Purchase Intent Classification* Extension + 1307 1287 Purchase Intent* | Consumer Packaged Goods | Ovens | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Ovens See *Purchase Intent Classification* Extension + 1308 1287 Purchase Intent* | Consumer Packaged Goods | Ranges | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Ranges See *Purchase Intent Classification* Extension + 1309 1287 Purchase Intent* | Consumer Packaged Goods | Refrigerators | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Refrigerators See *Purchase Intent Classification* Extension + 1310 1287 Purchase Intent* | Consumer Packaged Goods | Sandwich Makers | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Sandwich Makers See *Purchase Intent Classification* Extension + 1311 1287 Purchase Intent* | Consumer Packaged Goods | Tea Kettles | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Tea Kettles See *Purchase Intent Classification* Extension + 1312 1287 Purchase Intent* | Consumer Packaged Goods | Toasters | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Toasters See *Purchase Intent Classification* Extension + 1313 1287 Purchase Intent* | Consumer Packaged Goods | Vacuums | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Vacuums See *Purchase Intent Classification* Extension + 1314 1287 Purchase Intent* | Consumer Packaged Goods | Washers and Dryers | Purchase Intent* Consumer Packaged Goods Non-edible Household Appliances Washers and Dryers See *Purchase Intent Classification* Extension + 1315 1179 Purchase Intent* | Consumer Packaged Goods | Home Care | Purchase Intent* Consumer Packaged Goods Non-edible Home Care See *Purchase Intent Classification* Extension + 1316 1315 Purchase Intent* | Consumer Packaged Goods | Air Fresheners | Purchase Intent* Consumer Packaged Goods Non-edible Home Care Air Fresheners See *Purchase Intent Classification* Extension + 1317 1315 Purchase Intent* | Consumer Packaged Goods | Household Cleaning | Purchase Intent* Consumer Packaged Goods Non-edible Home Care Household Cleaning See *Purchase Intent Classification* Extension + 1318 1317 Purchase Intent* | Consumer Packaged Goods | Cleaning Tools/Mops/Brooms | Purchase Intent* Consumer Packaged Goods Non-edible Home Care Household Cleaning Cleaning Tools/Mops/Brooms See *Purchase Intent Classification* Extension + 1319 1317 Purchase Intent* | Consumer Packaged Goods | Dish Detergent | Purchase Intent* Consumer Packaged Goods Non-edible Home Care Household Cleaning Dish Detergent See *Purchase Intent Classification* Extension + 1320 1317 Purchase Intent* | Consumer Packaged Goods | Floor Cleaners/Wax Removers | Purchase Intent* Consumer Packaged Goods Non-edible Home Care Household Cleaning Floor Cleaners/Wax Removers See *Purchase Intent Classification* Extension + 1321 1317 Purchase Intent* | Consumer Packaged Goods | Furniture Polish | Purchase Intent* Consumer Packaged Goods Non-edible Home Care Household Cleaning Furniture Polish See *Purchase Intent Classification* Extension + 1322 1317 Purchase Intent* | Consumer Packaged Goods | Household Cleaner | Purchase Intent* Consumer Packaged Goods Non-edible Home Care Household Cleaning Household Cleaner See *Purchase Intent Classification* Extension + 1323 1317 Purchase Intent* | Consumer Packaged Goods | Household Cleaner Cloths | Purchase Intent* Consumer Packaged Goods Non-edible Home Care Household Cleaning Household Cleaner Cloths See *Purchase Intent Classification* Extension + 1324 1317 Purchase Intent* | Consumer Packaged Goods | Multi Task Sheets | Purchase Intent* Consumer Packaged Goods Non-edible Home Care Household Cleaning Multi Task Sheets See *Purchase Intent Classification* Extension + 1325 1317 Purchase Intent* | Consumer Packaged Goods | Rug/Upholstery/Fabric Treatment | Purchase Intent* Consumer Packaged Goods Non-edible Home Care Household Cleaning Rug/Upholstery/Fabric Treatment See *Purchase Intent Classification* Extension + 1326 1317 Purchase Intent* | Consumer Packaged Goods | Sponges & Scouring Pads | Purchase Intent* Consumer Packaged Goods Non-edible Home Care Household Cleaning Sponges & Scouring Pads See *Purchase Intent Classification* Extension + 1327 1315 Purchase Intent* | Consumer Packaged Goods | Laundry | Purchase Intent* Consumer Packaged Goods Non-edible Home Care Laundry See *Purchase Intent Classification* Extension + 1328 1327 Purchase Intent* | Consumer Packaged Goods | Bleach | Purchase Intent* Consumer Packaged Goods Non-edible Home Care Laundry Bleach See *Purchase Intent Classification* Extension + 1329 1327 Purchase Intent* | Consumer Packaged Goods | Fabric Softener | Purchase Intent* Consumer Packaged Goods Non-edible Home Care Laundry Fabric Softener See *Purchase Intent Classification* Extension + 1330 1327 Purchase Intent* | Consumer Packaged Goods | Laundry Care | Purchase Intent* Consumer Packaged Goods Non-edible Home Care Laundry Laundry Care See *Purchase Intent Classification* Extension + 1331 1327 Purchase Intent* | Consumer Packaged Goods | Laundry Detergent | Purchase Intent* Consumer Packaged Goods Non-edible Home Care Laundry Laundry Detergent See *Purchase Intent Classification* Extension + 1332 1179 Purchase Intent* | Consumer Packaged Goods | Home and Garden Products | Purchase Intent* Consumer Packaged Goods Non-edible Home and Garden Products See *Purchase Intent Classification* Extension + 1333 1332 Purchase Intent* | Consumer Packaged Goods | Bathroom Accessories | Purchase Intent* Consumer Packaged Goods Non-edible Home and Garden Products Bathroom Accessories See *Purchase Intent Classification* Extension + 1334 1332 Purchase Intent* | Consumer Packaged Goods | Home Decor | Purchase Intent* Consumer Packaged Goods Non-edible Home and Garden Products Home Decor See *Purchase Intent Classification* Extension + 1335 1332 Purchase Intent* | Consumer Packaged Goods | Bedroom Furniture and Accessories | Purchase Intent* Consumer Packaged Goods Non-edible Home and Garden Products Bedroom Furniture and Accessories See *Purchase Intent Classification* Extension + 1336 1332 Purchase Intent* | Consumer Packaged Goods | Fireplaces | Purchase Intent* Consumer Packaged Goods Non-edible Home and Garden Products Fireplaces See *Purchase Intent Classification* Extension + 1337 1332 Purchase Intent* | Consumer Packaged Goods | Kitchen and Dining Products | Purchase Intent* Consumer Packaged Goods Non-edible Home and Garden Products Kitchen and Dining Products See *Purchase Intent Classification* Extension + 1338 1332 Purchase Intent* | Consumer Packaged Goods | Lawn and Garden Products | Purchase Intent* Consumer Packaged Goods Non-edible Home and Garden Products Lawn and Garden Products See *Purchase Intent Classification* Extension + 1339 1332 Purchase Intent* | Consumer Packaged Goods | Lighting | Purchase Intent* Consumer Packaged Goods Non-edible Home and Garden Products Lighting See *Purchase Intent Classification* Extension + 1340 1332 Purchase Intent* | Consumer Packaged Goods | Linens and Bedding | Purchase Intent* Consumer Packaged Goods Non-edible Home and Garden Products Linens and Bedding See *Purchase Intent Classification* Extension + 1341 1332 Purchase Intent* | Consumer Packaged Goods | Plants | Purchase Intent* Consumer Packaged Goods Non-edible Home and Garden Products Plants See *Purchase Intent Classification* Extension + 1342 1332 Purchase Intent* | Consumer Packaged Goods | Housewares | Purchase Intent* Consumer Packaged Goods Non-edible Home and Garden Products Housewares See *Purchase Intent Classification* Extension + 1343 1332 Purchase Intent* | Consumer Packaged Goods | Carpets and Rugs | Purchase Intent* Consumer Packaged Goods Non-edible Home and Garden Products Carpets and Rugs See *Purchase Intent Classification* Extension + 1344 1179 Purchase Intent* | Consumer Packaged Goods | Religious Items | Purchase Intent* Consumer Packaged Goods Non-edible Religious Items See *Purchase Intent Classification* Extension + 1345 1179 Purchase Intent* | Consumer Packaged Goods | Back to School Supplies | Purchase Intent* Consumer Packaged Goods Non-edible Back to School Supplies See *Purchase Intent Classification* Extension + 1346 1179 Purchase Intent* | Consumer Packaged Goods | Baby and Toddler Products | Purchase Intent* Consumer Packaged Goods Non-edible Baby and Toddler Products See *Purchase Intent Classification* Extension + 1347 1346 Purchase Intent* | Consumer Packaged Goods | Baby Bath and Potty Products | Purchase Intent* Consumer Packaged Goods Non-edible Baby and Toddler Products Baby Bath and Potty Products See *Purchase Intent Classification* Extension + 1348 1346 Purchase Intent* | Consumer Packaged Goods | Baby Gift Sets | Purchase Intent* Consumer Packaged Goods Non-edible Baby and Toddler Products Baby Gift Sets See *Purchase Intent Classification* Extension + 1349 1346 Purchase Intent* | Consumer Packaged Goods | Baby Toys and Activity Equipment | Purchase Intent* Consumer Packaged Goods Non-edible Baby and Toddler Products Baby Toys and Activity Equipment See *Purchase Intent Classification* Extension + 1350 1346 Purchase Intent* | Consumer Packaged Goods | Diapers | Purchase Intent* Consumer Packaged Goods Non-edible Baby and Toddler Products Diapers See *Purchase Intent Classification* Extension + 1351 1346 Purchase Intent* | Consumer Packaged Goods | Baby Safety Products | Purchase Intent* Consumer Packaged Goods Non-edible Baby and Toddler Products Baby Safety Products See *Purchase Intent Classification* Extension + 1352 1346 Purchase Intent* | Consumer Packaged Goods | Nursing and Feeding Products | Purchase Intent* Consumer Packaged Goods Non-edible Baby and Toddler Products Nursing and Feeding Products See *Purchase Intent Classification* Extension + 1353 1346 Purchase Intent* | Consumer Packaged Goods | Baby Bouncers and Rockers | Purchase Intent* Consumer Packaged Goods Non-edible Baby and Toddler Products Baby Bouncers and Rockers See *Purchase Intent Classification* Extension + 1354 1346 Purchase Intent* | Consumer Packaged Goods | Baby Carriers | Purchase Intent* Consumer Packaged Goods Non-edible Baby and Toddler Products Baby Carriers See *Purchase Intent Classification* Extension + 1355 1346 Purchase Intent* | Consumer Packaged Goods | High Chairs and Boosters | Purchase Intent* Consumer Packaged Goods Non-edible Baby and Toddler Products High Chairs and Boosters See *Purchase Intent Classification* Extension + 1356 1346 Purchase Intent* | Consumer Packaged Goods | Strollers and Joggers | Purchase Intent* Consumer Packaged Goods Non-edible Baby and Toddler Products Strollers and Joggers See *Purchase Intent Classification* Extension + 1357 1179 Purchase Intent* | Consumer Packaged Goods | Media | Purchase Intent* Consumer Packaged Goods Non-edible Media See *Purchase Intent Classification* Extension + 1358 1357 Purchase Intent* | Consumer Packaged Goods | Magazines and Newspapers | Purchase Intent* Consumer Packaged Goods Non-edible Media Magazines and Newspapers See *Purchase Intent Classification* Extension + 1359 1357 Purchase Intent* | Consumer Packaged Goods | DVDs | Purchase Intent* Consumer Packaged Goods Non-edible Media DVDs See *Purchase Intent Classification* Extension + 1360 1357 Purchase Intent* | Consumer Packaged Goods | Books and Audio Books | Purchase Intent* Consumer Packaged Goods Non-edible Media Books and Audio Books See *Purchase Intent Classification* Extension + 1361 1357 Purchase Intent* | Consumer Packaged Goods | CDs and Vinyl Records | Purchase Intent* Consumer Packaged Goods Non-edible Media CDs and Vinyl Records See *Purchase Intent Classification* Extension + 1362 1179 Purchase Intent* | Consumer Packaged Goods | Toys and Games | Purchase Intent* Consumer Packaged Goods Non-edible Toys and Games See *Purchase Intent Classification* Extension + 1363 1362 Purchase Intent* | Consumer Packaged Goods | Games | Purchase Intent* Consumer Packaged Goods Non-edible Toys and Games Games See *Purchase Intent Classification* Extension + 1364 1362 Purchase Intent* | Consumer Packaged Goods | Outdoor Play Equipment | Purchase Intent* Consumer Packaged Goods Non-edible Toys and Games Outdoor Play Equipment See *Purchase Intent Classification* Extension + 1365 1362 Purchase Intent* | Consumer Packaged Goods | Puzzles | Purchase Intent* Consumer Packaged Goods Non-edible Toys and Games Puzzles See *Purchase Intent Classification* Extension + 1366 1362 Purchase Intent* | Consumer Packaged Goods | Toys | Purchase Intent* Consumer Packaged Goods Non-edible Toys and Games Toys See *Purchase Intent Classification* Extension + 1367 1179 Purchase Intent* | Consumer Packaged Goods | Luggage and Bags | Purchase Intent* Consumer Packaged Goods Non-edible Luggage and Bags See *Purchase Intent Classification* Extension + 1368 752 Purchase Intent* | Education and Careers Purchase Intent* Education and Careers See *Purchase Intent Classification* Extension + 1369 1368 Purchase Intent* | Education and Careers | Adult Education | Purchase Intent* Education and Careers Adult Education See *Purchase Intent Classification* Extension + 1370 1368 Purchase Intent* | Education and Careers | Career Improvement and Advice | Purchase Intent* Education and Careers Career Improvement and Advice See *Purchase Intent Classification* Extension + 1371 1368 Purchase Intent* | Education and Careers | Colleges and Universities | Purchase Intent* Education and Careers Colleges and Universities See *Purchase Intent Classification* Extension + 1372 1368 Purchase Intent* | Education and Careers | Employment Agencies | Purchase Intent* Education and Careers Employment Agencies See *Purchase Intent Classification* Extension + 1373 1368 Purchase Intent* | Education and Careers | Language Learning | Purchase Intent* Education and Careers Language Learning See *Purchase Intent Classification* Extension + 1374 1368 Purchase Intent* | Education and Careers | Online Education | Purchase Intent* Education and Careers Online Education See *Purchase Intent Classification* Extension + 1375 1368 Purchase Intent* | Education and Careers | Study Skills | Purchase Intent* Education and Careers Study Skills See *Purchase Intent Classification* Extension + 1376 1368 Purchase Intent* | Education and Careers | Teaching Resources | Purchase Intent* Education and Careers Teaching Resources See *Purchase Intent Classification* Extension + 1377 752 Purchase Intent* | Family and Parenting Purchase Intent* Family and Parenting See *Purchase Intent Classification* Extension + 1378 1377 Purchase Intent* | Family and Parenting | Childcare | Purchase Intent* Family and Parenting Childcare See *Purchase Intent Classification* Extension + 1379 1378 Purchase Intent* | Family and Parenting | Childcare | Day Care Centers | Purchase Intent* Family and Parenting Childcare Day Care Centers See *Purchase Intent Classification* Extension + 1380 1378 Purchase Intent* | Family and Parenting | Childcare | Nanny Services | Purchase Intent* Family and Parenting Childcare Nanny Services See *Purchase Intent Classification* Extension + 1381 1377 Purchase Intent* | Family and Parenting | Genealogy and Family Trees | Purchase Intent* Family and Parenting Genealogy and Family Trees See *Purchase Intent Classification* Extension + 1382 1377 Purchase Intent* | Family and Parenting | Kids Activities | Purchase Intent* Family and Parenting Kids Activities See *Purchase Intent Classification* Extension + 1383 752 Purchase Intent* | Finance and Insurance Purchase Intent* Finance and Insurance See *Purchase Intent Classification* Extension + 1384 1383 Purchase Intent* | Finance and Insurance | Accountants | Purchase Intent* Finance and Insurance Accountants See *Purchase Intent Classification* Extension + 1385 1383 Purchase Intent* | Finance and Insurance | Banking | Purchase Intent* Finance and Insurance Banking See *Purchase Intent Classification* Extension + 1386 1383 Purchase Intent* | Finance and Insurance | Bookkeepers | Purchase Intent* Finance and Insurance Bookkeepers See *Purchase Intent Classification* Extension + 1387 1383 Purchase Intent* | Finance and Insurance | Credit and Debt Repair/Credit Reporting | Purchase Intent* Finance and Insurance Credit and Debt Repair/Credit Reporting See *Purchase Intent Classification* Extension + 1388 1383 Purchase Intent* | Finance and Insurance | Credit Cards | Purchase Intent* Finance and Insurance Credit Cards See *Purchase Intent Classification* Extension + 1389 1383 Purchase Intent* | Finance and Insurance | Insurance | Purchase Intent* Finance and Insurance Insurance See *Purchase Intent Classification* Extension + 1390 1389 Purchase Intent* | Finance and Insurance | Auto Insurance | Purchase Intent* Finance and Insurance Insurance Auto Insurance See *Purchase Intent Classification* Extension + 1391 1390 Purchase Intent* | Finance and Insurance | Home Insurance | Purchase Intent* Finance and Insurance Insurance Home Insurance See *Purchase Intent Classification* Extension + 1392 1390 Purchase Intent* | Finance and Insurance | Life Insurance | Purchase Intent* Finance and Insurance Insurance Life Insurance See *Purchase Intent Classification* Extension + 1393 1390 Purchase Intent* | Finance and Insurance | Medical Insurance | Purchase Intent* Finance and Insurance Insurance Medical Insurance See *Purchase Intent Classification* Extension + 1394 1383 Purchase Intent* | Finance and Insurance | Mortgage Lenders and Brokers | Purchase Intent* Finance and Insurance Mortgage Lenders and Brokers See *Purchase Intent Classification* Extension + 1395 1383 Purchase Intent* | Finance and Insurance | Payday and Emergency Loans | Purchase Intent* Finance and Insurance Payday and Emergency Loans See *Purchase Intent Classification* Extension + 1396 1383 Purchase Intent* | Finance and Insurance | Retirement Planning | Purchase Intent* Finance and Insurance Retirement Planning See *Purchase Intent Classification* Extension + 1397 1383 Purchase Intent* | Finance and Insurance | Stocks and Investments | Purchase Intent* Finance and Insurance Stocks and Investments See *Purchase Intent Classification* Extension + 1398 1383 Purchase Intent* | Finance and Insurance | Student Financial Aid | Purchase Intent* Finance and Insurance Student Financial Aid See *Purchase Intent Classification* Extension + 1399 1383 Purchase Intent* | Finance and Insurance | Tax Preparation Services | Purchase Intent* Finance and Insurance Tax Preparation Services See *Purchase Intent Classification* Extension + 1400 752 Purchase Intent* | Food and Beverage Services Purchase Intent* Food and Beverage Services See *Purchase Intent Classification* Extension + 1401 1400 Purchase Intent* | Food and Beverage Services | Bakeries | Purchase Intent* Food and Beverage Services Bakeries See *Purchase Intent Classification* Extension + 1402 1400 Purchase Intent* | Food and Beverage Services | Bars | Purchase Intent* Food and Beverage Services Bars See *Purchase Intent Classification* Extension + 1403 1400 Purchase Intent* | Food and Beverage Services | Catering | Purchase Intent* Food and Beverage Services Catering See *Purchase Intent Classification* Extension + 1404 1400 Purchase Intent* | Food and Beverage Services | Fast Food | Purchase Intent* Food and Beverage Services Fast Food See *Purchase Intent Classification* Extension + 1405 1400 Purchase Intent* | Food and Beverage Services | Food Delivery Services | Purchase Intent* Food and Beverage Services Food Delivery Services See *Purchase Intent Classification* Extension + 1406 1400 Purchase Intent* | Food and Beverage Services | Restaurants | Purchase Intent* Food and Beverage Services Restaurants See *Purchase Intent Classification* Extension + 1407 752 Purchase Intent* | Furniture Purchase Intent* Furniture See *Purchase Intent Classification* Extension + 1408 1407 Purchase Intent* | Furniture | Baby and Toddler Furniture | Purchase Intent* Furniture Baby and Toddler Furniture See *Purchase Intent Classification* Extension + 1409 1407 Purchase Intent* | Furniture | BBQ/Grills/Outdoor Dining | Purchase Intent* Furniture BBQ/Grills/Outdoor Dining See *Purchase Intent Classification* Extension + 1410 1407 Purchase Intent* | Furniture | Beds and Accessories | Purchase Intent* Furniture Beds and Accessories See *Purchase Intent Classification* Extension + 1411 1407 Purchase Intent* | Furniture | Benches | Purchase Intent* Furniture Benches See *Purchase Intent Classification* Extension + 1412 1407 Purchase Intent* | Furniture | Cabinets and Storage | Purchase Intent* Furniture Cabinets and Storage See *Purchase Intent Classification* Extension + 1413 1407 Purchase Intent* | Furniture | Chairs | Purchase Intent* Furniture Chairs See *Purchase Intent Classification* Extension + 1414 1407 Purchase Intent* | Furniture | Entertainment Centers and TV Stands | Purchase Intent* Furniture Entertainment Centers and TV Stands See *Purchase Intent Classification* Extension + 1415 1407 Purchase Intent* | Furniture | Furniture Sets | Purchase Intent* Furniture Furniture Sets See *Purchase Intent Classification* Extension + 1416 1407 Purchase Intent* | Furniture | Outdoor Furniture | Purchase Intent* Furniture Outdoor Furniture See *Purchase Intent Classification* Extension + 1417 1407 Purchase Intent* | Furniture | Shelving | Purchase Intent* Furniture Shelving See *Purchase Intent Classification* Extension + 1418 1407 Purchase Intent* | Furniture | Sofas | Purchase Intent* Furniture Sofas See *Purchase Intent Classification* Extension + 1419 1407 Purchase Intent* | Furniture | Tables | Purchase Intent* Furniture Tables See *Purchase Intent Classification* Extension + 1420 752 Purchase Intent* | Gifts and Holiday Items Purchase Intent* Gifts and Holiday Items See *Purchase Intent Classification* Extension + 1421 1420 Purchase Intent* | Gifts and Holiday Items | Flowers | Purchase Intent* Gifts and Holiday Items Flowers See *Purchase Intent Classification* Extension + 1422 1420 Purchase Intent* | Gifts and Holiday Items | Gift Baskets | Purchase Intent* Gifts and Holiday Items Gift Baskets See *Purchase Intent Classification* Extension + 1423 1420 Purchase Intent* | Gifts and Holiday Items | Gift Cards and Coupons | Purchase Intent* Gifts and Holiday Items Gift Cards and Coupons See *Purchase Intent Classification* Extension + 1424 1420 Purchase Intent* | Gifts and Holiday Items | Gift Certificates | Purchase Intent* Gifts and Holiday Items Gift Certificates See *Purchase Intent Classification* Extension + 1425 1420 Purchase Intent* | Gifts and Holiday Items | Greeting Cards | Purchase Intent* Gifts and Holiday Items Greeting Cards See *Purchase Intent Classification* Extension + 1426 1420 Purchase Intent* | Gifts and Holiday Items | Holiday and Ethnic Items | Purchase Intent* Gifts and Holiday Items Holiday and Ethnic Items See *Purchase Intent Classification* Extension + 1427 1420 Purchase Intent* | Gifts and Holiday Items | Party Goods | Purchase Intent* Gifts and Holiday Items Party Goods See *Purchase Intent Classification* Extension + 1428 752 Purchase Intent* | Hardware Supplies Purchase Intent* Hardware Supplies See *Purchase Intent Classification* Extension + 1429 1428 Purchase Intent* | Hardware Supplies | Agriculture and Farming Equipment | Purchase Intent* Hardware Supplies Agriculture and Farming Equipment See *Purchase Intent Classification* Extension + 1430 1428 Purchase Intent* | Hardware Supplies | Building Materials | Purchase Intent* Hardware Supplies Building Materials See *Purchase Intent Classification* Extension + 1431 1428 Purchase Intent* | Hardware Supplies | Building Products | Purchase Intent* Hardware Supplies Building Products See *Purchase Intent Classification* Extension + 1432 1428 Purchase Intent* | Hardware Supplies | Building Supplies | Purchase Intent* Hardware Supplies Building Supplies See *Purchase Intent Classification* Extension + 1433 1428 Purchase Intent* | Hardware Supplies | Carpentry Supplies | Purchase Intent* Hardware Supplies Carpentry Supplies See *Purchase Intent Classification* Extension + 1434 1428 Purchase Intent* | Hardware Supplies | Electrical Equipment | Purchase Intent* Hardware Supplies Electrical Equipment See *Purchase Intent Classification* Extension + 1435 1428 Purchase Intent* | Hardware Supplies | Fencing and Barriers | Purchase Intent* Hardware Supplies Fencing and Barriers See *Purchase Intent Classification* Extension + 1436 1428 Purchase Intent* | Hardware Supplies | Fuel Containers and Tanks | Purchase Intent* Hardware Supplies Fuel Containers and Tanks See *Purchase Intent Classification* Extension + 1437 1428 Purchase Intent* | Hardware Supplies | Hardware Accessories | Purchase Intent* Hardware Supplies Hardware Accessories See *Purchase Intent Classification* Extension + 1438 1428 Purchase Intent* | Hardware Supplies | Hardware Pumps | Purchase Intent* Hardware Supplies Hardware Pumps See *Purchase Intent Classification* Extension + 1439 1428 Purchase Intent* | Hardware Supplies | Heating/Ventilation/Air Conditioning | Purchase Intent* Hardware Supplies Heating/Ventilation/Air Conditioning See *Purchase Intent Classification* Extension + 1440 1428 Purchase Intent* | Hardware Supplies | Locks and Keys | Purchase Intent* Hardware Supplies Locks and Keys See *Purchase Intent Classification* Extension + 1441 1428 Purchase Intent* | Hardware Supplies | Machinery | Purchase Intent* Hardware Supplies Machinery See *Purchase Intent Classification* Extension + 1442 1428 Purchase Intent* | Hardware Supplies | Plumbing Supplies | Purchase Intent* Hardware Supplies Plumbing Supplies See *Purchase Intent Classification* Extension + 1443 1428 Purchase Intent* | Hardware Supplies | Power and Electrical Supplies | Purchase Intent* Hardware Supplies Power and Electrical Supplies See *Purchase Intent Classification* Extension + 1444 1428 Purchase Intent* | Hardware Supplies | Small Engines | Purchase Intent* Hardware Supplies Small Engines See *Purchase Intent Classification* Extension + 1445 1428 Purchase Intent* | Hardware Supplies | Storage Tanks | Purchase Intent* Hardware Supplies Storage Tanks See *Purchase Intent Classification* Extension + 1446 1428 Purchase Intent* | Hardware Supplies | Tools | Purchase Intent* Hardware Supplies Tools See *Purchase Intent Classification* Extension + 1447 1428 Purchase Intent* | Hardware Supplies | Work Safety Protective Gear | Purchase Intent* Hardware Supplies Work Safety Protective Gear See *Purchase Intent Classification* Extension + 1448 752 Purchase Intent* | Health and Medical Services Purchase Intent* Health and Medical Services See *Purchase Intent Classification* Extension + 1449 1448 Purchase Intent* | Health and Medical Services | Alternative and Natural Medicine | Purchase Intent* Health and Medical Services Alternative and Natural Medicine See *Purchase Intent Classification* Extension + 1451 1448 Purchase Intent* | Health and Medical Services | Chiropractors | Purchase Intent* Health and Medical Services Chiropractors See *Purchase Intent Classification* Extension + 1452 1448 Purchase Intent* | Health and Medical Services | Clinical Research | Purchase Intent* Health and Medical Services Clinical Research See *Purchase Intent Classification* Extension + 1453 1448 Purchase Intent* | Health and Medical Services | Cosmetic Medical Services | Purchase Intent* Health and Medical Services Cosmetic Medical Services See *Purchase Intent Classification* Extension + 1454 1448 Purchase Intent* | Health and Medical Services | Dental Care | Purchase Intent* Health and Medical Services Dental Care See *Purchase Intent Classification* Extension + 1455 1448 Purchase Intent* | Health and Medical Services | Drugstores and Pharmacies | Purchase Intent* Health and Medical Services Drugstores and Pharmacies See *Purchase Intent Classification* Extension + 1456 1448 Purchase Intent* | Health and Medical Services | Elder Care | Purchase Intent* Health and Medical Services Elder Care See *Purchase Intent Classification* Extension + 1458 1448 Purchase Intent* | Health and Medical Services | Hair Loss Treatments | Purchase Intent* Health and Medical Services Hair Loss Treatments See *Purchase Intent Classification* Extension + 1459 1448 Purchase Intent* | Health and Medical Services | Health Care and Physicians | Purchase Intent* Health and Medical Services Health Care and Physicians See *Purchase Intent Classification* Extension + 1460 1448 Purchase Intent* | Health and Medical Services | Health Services | Purchase Intent* Health and Medical Services Health Services See *Purchase Intent Classification* Extension + 1461 1448 Purchase Intent* | Health and Medical Services | Hospitals | Purchase Intent* Health and Medical Services Hospitals See *Purchase Intent Classification* Extension + 1462 1448 Purchase Intent* | Health and Medical Services | Massage Therapists | Purchase Intent* Health and Medical Services Massage Therapists See *Purchase Intent Classification* Extension + 1465 1448 Purchase Intent* | Health and Medical Services | Physical Therapists | Purchase Intent* Health and Medical Services Physical Therapists See *Purchase Intent Classification* Extension + 1466 1448 Purchase Intent* | Health and Medical Services | Skin Care Treatments | Purchase Intent* Health and Medical Services Skin Care Treatments See *Purchase Intent Classification* Extension + 1467 1448 Purchase Intent* | Health and Medical Services | Smoking Cessation | Purchase Intent* Health and Medical Services Smoking Cessation See *Purchase Intent Classification* Extension + 1469 1448 Purchase Intent* | Health and Medical Services | Vaccines | Purchase Intent* Health and Medical Services Vaccines See *Purchase Intent Classification* Extension + 1470 1448 Purchase Intent* | Health and Medical Services | Vision Care | Purchase Intent* Health and Medical Services Vision Care See *Purchase Intent Classification* Extension + 1471 752 Purchase Intent* | Hobbies and Interests Purchase Intent* Hobbies and Interests See *Purchase Intent Classification* Extension + 1472 1471 Purchase Intent* | Hobbies and Interests | Arts and Crafts | Purchase Intent* Hobbies and Interests Arts and Crafts See *Purchase Intent Classification* Extension + 1473 1471 Purchase Intent* | Hobbies and Interests | Musical Instruments and Accessories | Purchase Intent* Hobbies and Interests Musical Instruments and Accessories See *Purchase Intent Classification* Extension + 1474 1471 Purchase Intent* | Hobbies and Interests | Psychics and Astrology | Purchase Intent* Hobbies and Interests Psychics and Astrology See *Purchase Intent Classification* Extension + 1475 1471 Purchase Intent* | Hobbies and Interests | Workshops and Classes | Purchase Intent* Hobbies and Interests Workshops and Classes See *Purchase Intent Classification* Extension + 1476 752 Purchase Intent* | Home and Garden Services Purchase Intent* Home and Garden Services See *Purchase Intent Classification* Extension + 1477 1476 Purchase Intent* | Home and Garden Services | Appliance Repair | Purchase Intent* Home and Garden Services Appliance Repair See *Purchase Intent Classification* Extension + 1478 1476 Purchase Intent* | Home and Garden Services | Business and Home Security Services | Purchase Intent* Home and Garden Services Business and Home Security Services See *Purchase Intent Classification* Extension + 1479 1476 Purchase Intent* | Home and Garden Services | Carpeting and Flooring Services | Purchase Intent* Home and Garden Services Carpeting and Flooring Services See *Purchase Intent Classification* Extension + 1480 1476 Purchase Intent* | Home and Garden Services | Emergency Preparedness | Purchase Intent* Home and Garden Services Emergency Preparedness See *Purchase Intent Classification* Extension + 1481 1476 Purchase Intent* | Home and Garden Services | Flood, Fire and Gas Safety | Purchase Intent* Home and Garden Services Flood, Fire and Gas Safety See *Purchase Intent Classification* Extension + 1482 1476 Purchase Intent* | Home and Garden Services | Gas and Electric Services | Purchase Intent* Home and Garden Services Gas and Electric Services See *Purchase Intent Classification* Extension + 1483 1476 Purchase Intent* | Home and Garden Services | Home Improvement and Repair | Purchase Intent* Home and Garden Services Home Improvement and Repair See *Purchase Intent Classification* Extension + 1484 1476 Purchase Intent* | Home and Garden Services | Housekeeping Services | Purchase Intent* Home and Garden Services Housekeeping Services See *Purchase Intent Classification* Extension + 1485 1476 Purchase Intent* | Home and Garden Services | Landscaping Services | Purchase Intent* Home and Garden Services Landscaping Services See *Purchase Intent Classification* Extension + 1486 1476 Purchase Intent* | Home and Garden Services | Lawn and Garden Services | Purchase Intent* Home and Garden Services Lawn and Garden Services See *Purchase Intent Classification* Extension + 1487 1476 Purchase Intent* | Home and Garden Services | Pest Exterminators | Purchase Intent* Home and Garden Services Pest Exterminators See *Purchase Intent Classification* Extension + 1488 1476 Purchase Intent* | Home and Garden Services | Plumbers | Purchase Intent* Home and Garden Services Plumbers See *Purchase Intent Classification* Extension + 1489 1476 Purchase Intent* | Home and Garden Services | Pool and Spa Installation and Maintenance | Purchase Intent* Home and Garden Services Pool and Spa Installation and Maintenance See *Purchase Intent Classification* Extension + 1490 1476 Purchase Intent* | Home and Garden Services | Remodeling and Construction | Purchase Intent* Home and Garden Services Remodeling and Construction See *Purchase Intent Classification* Extension + 1491 1476 Purchase Intent* | Home and Garden Services | Water Services | Purchase Intent* Home and Garden Services Water Services See *Purchase Intent Classification* Extension + 1492 1476 Purchase Intent* | Home and Garden Services | Window Installation and Treatments | Purchase Intent* Home and Garden Services Window Installation and Treatments See *Purchase Intent Classification* Extension + 1493 752 Purchase Intent* | Legal Services Purchase Intent* Legal Services See *Purchase Intent Classification* Extension + 1494 1493 Purchase Intent* | Legal Services | Attorneys | Purchase Intent* Legal Services Attorneys See *Purchase Intent Classification* Extension + 1495 1493 Purchase Intent* | Legal Services | Bail Bonds | Purchase Intent* Legal Services Bail Bonds See *Purchase Intent Classification* Extension + 1496 752 Purchase Intent* | Life Events Purchase Intent* Life Events See *Purchase Intent Classification* Extension + 1497 1496 Purchase Intent* | Life Events | Anniversary | Purchase Intent* Life Events Anniversary See *Purchase Intent Classification* Extension + 1498 1496 Purchase Intent* | Life Events | Baby Showers | Purchase Intent* Life Events Baby Showers See *Purchase Intent Classification* Extension + 1499 1496 Purchase Intent* | Life Events | Bachelor and Bachelorette Parties | Purchase Intent* Life Events Bachelor and Bachelorette Parties See *Purchase Intent Classification* Extension + 1500 1496 Purchase Intent* | Life Events | Birthdays | Purchase Intent* Life Events Birthdays See *Purchase Intent Classification* Extension + 1501 1496 Purchase Intent* | Life Events | Births | Purchase Intent* Life Events Births See *Purchase Intent Classification* Extension + 1502 1496 Purchase Intent* | Life Events | Funeral Supplies and Services | Purchase Intent* Life Events Funeral Supplies and Services See *Purchase Intent Classification* Extension + 1503 1496 Purchase Intent* | Life Events | Graduations | Purchase Intent* Life Events Graduations See *Purchase Intent Classification* Extension + 1504 1496 Purchase Intent* | Life Events | Proms | Purchase Intent* Life Events Proms See *Purchase Intent Classification* Extension + 1505 1496 Purchase Intent* | Life Events | Wedding Services and Supplies | Purchase Intent* Life Events Wedding Services and Supplies See *Purchase Intent Classification* Extension + 1506 752 Purchase Intent* | Logistics and Delivery Purchase Intent* Logistics and Delivery See *Purchase Intent Classification* Extension + 1507 1506 Purchase Intent* | Logistics and Delivery | Shipping Services | Purchase Intent* Logistics and Delivery Shipping Services See *Purchase Intent Classification* Extension + 1508 1506 Purchase Intent* | Logistics and Delivery | Storage Facilities | Purchase Intent* Logistics and Delivery Storage Facilities See *Purchase Intent Classification* Extension + 1538 752 Purchase Intent* | Non-Profits Purchase Intent* Non-Profits See *Purchase Intent Classification* Extension + 1539 1538 Purchase Intent* | Non-Profits | Charities and Donations | Purchase Intent* Non-Profits Charities and Donations See *Purchase Intent Classification* Extension + 1540 1538 Purchase Intent* | Non-Profits | Civic Organizations | Purchase Intent* Non-Profits Civic Organizations See *Purchase Intent Classification* Extension + 1541 1538 Purchase Intent* | Non-Profits | Federations and Professional Associations | Purchase Intent* Non-Profits Federations and Professional Associations See *Purchase Intent Classification* Extension + 1542 1538 Purchase Intent* | Non-Profits | Military Organizations | Purchase Intent* Non-Profits Military Organizations See *Purchase Intent Classification* Extension + 1543 1538 Purchase Intent* | Non-Profits | NGOs | Purchase Intent* Non-Profits NGOs See *Purchase Intent Classification* Extension + 1544 1538 Purchase Intent* | Non-Profits | PSAs | Purchase Intent* Non-Profits PSAs See *Purchase Intent Classification* Extension + 1546 752 Purchase Intent* | Office Equipment and Supplies Purchase Intent* Office Equipment and Supplies See *Purchase Intent Classification* Extension + 1547 1546 Purchase Intent* | Office Equipment and Supplies | Office Furniture | Purchase Intent* Office Equipment and Supplies Office Furniture See *Purchase Intent Classification* Extension + 1548 1546 Purchase Intent* | Office Equipment and Supplies | Stationery | Purchase Intent* Office Equipment and Supplies Stationery See *Purchase Intent Classification* Extension + 1549 752 Purchase Intent* | Pet Services Purchase Intent* Pet Services See *Purchase Intent Classification* Extension + 1550 1549 Purchase Intent* | Pet Services | Pet Breeders | Purchase Intent* Pet Services Pet Breeders See *Purchase Intent Classification* Extension + 1551 1549 Purchase Intent* | Pet Services | Pet Grooming | Purchase Intent* Pet Services Pet Grooming See *Purchase Intent Classification* Extension + 1552 1549 Purchase Intent* | Pet Services | Pet Sitting | Purchase Intent* Pet Services Pet Sitting See *Purchase Intent Classification* Extension + 1553 1549 Purchase Intent* | Pet Services | Pet Stores | Purchase Intent* Pet Services Pet Stores See *Purchase Intent Classification* Extension + 1554 1549 Purchase Intent* | Pet Services | Veterinary Services | Purchase Intent* Pet Services Veterinary Services See *Purchase Intent Classification* Extension + 1555 752 Purchase Intent* | Pharmaceuticals, Conditions, and Symptoms Purchase Intent* Pharmaceuticals, Conditions, and Symptoms See *Purchase Intent Classification* Extension + 1585 752 Purchase Intent* | Real Estate Purchase Intent* Real Estate See *Purchase Intent Classification* Extension + 1586 1585 Purchase Intent* | Real Estate | Commercial Real Estate | Purchase Intent* Real Estate Commercial Real Estate See *Purchase Intent Classification* Extension + 1587 1585 Purchase Intent* | Real Estate | Real Estate Rentals | Purchase Intent* Real Estate Real Estate Rentals See *Purchase Intent Classification* Extension + 1588 1585 Purchase Intent* | Real Estate | Real Estate Sales | Purchase Intent* Real Estate Real Estate Sales See *Purchase Intent Classification* Extension + 1589 1585 Purchase Intent* | Real Estate | Real Estate Services For Owners | Purchase Intent* Real Estate Real Estate Services For Owners See *Purchase Intent Classification* Extension + 1590 1585 Purchase Intent* | Real Estate | Residential Real Estate | Purchase Intent* Real Estate Residential Real Estate See *Purchase Intent Classification* Extension + 1591 752 Purchase Intent* | Recreation and Fitness Activities Purchase Intent* Recreation and Fitness Activities See *Purchase Intent Classification* Extension + 1592 1591 Purchase Intent* | Recreation and Fitness Activities | Dance Studios | Purchase Intent* Recreation and Fitness Activities Dance Studios See *Purchase Intent Classification* Extension + 1593 1591 Purchase Intent* | Recreation and Fitness Activities | Gyms and Health Clubs | Purchase Intent* Recreation and Fitness Activities Gyms and Health Clubs See *Purchase Intent Classification* Extension + 1594 1591 Purchase Intent* | Recreation and Fitness Activities | Participant Sports Leagues | Purchase Intent* Recreation and Fitness Activities Participant Sports Leagues See *Purchase Intent Classification* Extension + 1595 1591 Purchase Intent* | Recreation and Fitness Activities | Personal Trainers | Purchase Intent* Recreation and Fitness Activities Personal Trainers See *Purchase Intent Classification* Extension + 1596 1591 Purchase Intent* | Recreation and Fitness Activities | Self Defense and Martial Arts Classes | Purchase Intent* Recreation and Fitness Activities Self Defense and Martial Arts Classes See *Purchase Intent Classification* Extension + 1597 1591 Purchase Intent* | Recreation and Fitness Activities | Swimming Facilities | Purchase Intent* Recreation and Fitness Activities Swimming Facilities See *Purchase Intent Classification* Extension + 1598 1591 Purchase Intent* | Recreation and Fitness Activities | Yoga Studios | Purchase Intent* Recreation and Fitness Activities Yoga Studios See *Purchase Intent Classification* Extension + 1599 752 Purchase Intent* | Software Purchase Intent* Software See *Purchase Intent Classification* Extension + 1600 1599 Purchase Intent* | Software | Computer Software | Purchase Intent* Software Computer Software See *Purchase Intent Classification* Extension + 1601 1600 Purchase Intent* | Software | 3-D Graphics | Purchase Intent* Software Computer Software 3-D Graphics See *Purchase Intent Classification* Extension + 1602 1600 Purchase Intent* | Software | Photo Editing Software | Purchase Intent* Software Computer Software Photo Editing Software See *Purchase Intent Classification* Extension + 1603 1600 Purchase Intent* | Software | Shareware and Freeware | Purchase Intent* Software Computer Software Shareware and Freeware See *Purchase Intent Classification* Extension + 1604 1600 Purchase Intent* | Software | Video Software | Purchase Intent* Software Computer Software Video Software See *Purchase Intent Classification* Extension + 1605 1600 Purchase Intent* | Software | Web Conferencing | Purchase Intent* Software Computer Software Web Conferencing See *Purchase Intent Classification* Extension + 1606 1600 Purchase Intent* | Software | Antivirus Software | Purchase Intent* Software Computer Software Antivirus Software See *Purchase Intent Classification* Extension + 1607 1600 Purchase Intent* | Software | Browsers | Purchase Intent* Software Computer Software Browsers See *Purchase Intent Classification* Extension + 1608 1600 Purchase Intent* | Software | Computer Animation | Purchase Intent* Software Computer Software Computer Animation See *Purchase Intent Classification* Extension + 1609 1600 Purchase Intent* | Software | Databases | Purchase Intent* Software Computer Software Databases See *Purchase Intent Classification* Extension + 1610 1600 Purchase Intent* | Software | Desktop Publishing | Purchase Intent* Software Computer Software Desktop Publishing See *Purchase Intent Classification* Extension + 1611 1600 Purchase Intent* | Software | Digital Audio | Purchase Intent* Software Computer Software Digital Audio See *Purchase Intent Classification* Extension + 1612 1600 Purchase Intent* | Software | Graphics Software | Purchase Intent* Software Computer Software Graphics Software See *Purchase Intent Classification* Extension + 1613 1600 Purchase Intent* | Software | Operating Systems | Purchase Intent* Software Computer Software Operating Systems See *Purchase Intent Classification* Extension + 1614 1600 Purchase Intent* | Software | Productivity Software | Purchase Intent* Software Computer Software Productivity Software See *Purchase Intent Classification* Extension + 1615 1600 Purchase Intent* | Software | Messaging Software | Purchase Intent* Software Computer Software Messaging Software See *Purchase Intent Classification* Extension + 1616 1600 Purchase Intent* | Software | Gaming Software | Purchase Intent* Software Computer Software Gaming Software See *Purchase Intent Classification* Extension + 1617 1599 Purchase Intent* | Software | Digital Goods and Currency | Purchase Intent* Software Digital Goods and Currency See *Purchase Intent Classification* Extension + 1618 752 Purchase Intent* | Sporting Goods Purchase Intent* Sporting Goods See *Purchase Intent Classification* Extension + 1619 1618 Purchase Intent* | Sporting Goods | Athletics Equipment | Purchase Intent* Sporting Goods Athletics Equipment See *Purchase Intent Classification* Extension + 1620 1619 Purchase Intent* | Sporting Goods | Baseball and Softball Equipment | Purchase Intent* Sporting Goods Athletics Equipment Baseball and Softball Equipment See *Purchase Intent Classification* Extension + 1621 1619 Purchase Intent* | Sporting Goods | Basketball Equipment | Purchase Intent* Sporting Goods Athletics Equipment Basketball Equipment See *Purchase Intent Classification* Extension + 1622 1619 Purchase Intent* | Sporting Goods | Boxing and Martial Arts Equipment | Purchase Intent* Sporting Goods Athletics Equipment Boxing and Martial Arts Equipment See *Purchase Intent Classification* Extension + 1623 1619 Purchase Intent* | Sporting Goods | Figure Skating and Hockey Equipment | Purchase Intent* Sporting Goods Athletics Equipment Figure Skating and Hockey Equipment See *Purchase Intent Classification* Extension + 1624 1619 Purchase Intent* | Sporting Goods | Football Equipment | Purchase Intent* Sporting Goods Athletics Equipment Football Equipment See *Purchase Intent Classification* Extension + 1625 1619 Purchase Intent* | Sporting Goods | General Purpose Athletic Equipment | Purchase Intent* Sporting Goods Athletics Equipment General Purpose Athletic Equipment See *Purchase Intent Classification* Extension + 1626 1619 Purchase Intent* | Sporting Goods | Gymnastics Equipment | Purchase Intent* Sporting Goods Athletics Equipment Gymnastics Equipment See *Purchase Intent Classification* Extension + 1627 1619 Purchase Intent* | Sporting Goods | Soccer Equipment | Purchase Intent* Sporting Goods Athletics Equipment Soccer Equipment See *Purchase Intent Classification* Extension + 1628 1619 Purchase Intent* | Sporting Goods | Tennis Equipment | Purchase Intent* Sporting Goods Athletics Equipment Tennis Equipment See *Purchase Intent Classification* Extension + 1629 1619 Purchase Intent* | Sporting Goods | Track and Field Equipment | Purchase Intent* Sporting Goods Athletics Equipment Track and Field Equipment See *Purchase Intent Classification* Extension + 1630 1619 Purchase Intent* | Sporting Goods | Volleyball Equipment | Purchase Intent* Sporting Goods Athletics Equipment Volleyball Equipment See *Purchase Intent Classification* Extension + 1631 1619 Purchase Intent* | Sporting Goods | Water Polo Equipment | Purchase Intent* Sporting Goods Athletics Equipment Water Polo Equipment See *Purchase Intent Classification* Extension + 1632 1619 Purchase Intent* | Sporting Goods | Wrestling Equipment | Purchase Intent* Sporting Goods Athletics Equipment Wrestling Equipment See *Purchase Intent Classification* Extension + 1633 1618 Purchase Intent* | Sporting Goods | Exercise and Fitness Equipment | Purchase Intent* Sporting Goods Exercise and Fitness Equipment See *Purchase Intent Classification* Extension + 1634 1618 Purchase Intent* | Sporting Goods | Indoor Games Equipment | Purchase Intent* Sporting Goods Indoor Games Equipment See *Purchase Intent Classification* Extension + 1635 1618 Purchase Intent* | Sporting Goods | Outdoor Recreation Equipment | Purchase Intent* Sporting Goods Outdoor Recreation Equipment See *Purchase Intent Classification* Extension + 1636 1635 Purchase Intent* | Sporting Goods | Boating and Water Sports Equipment | Purchase Intent* Sporting Goods Outdoor Recreation Equipment Boating and Water Sports Equipment See *Purchase Intent Classification* Extension + 1637 1635 Purchase Intent* | Sporting Goods | Camping and Hiking Equipment | Purchase Intent* Sporting Goods Outdoor Recreation Equipment Camping and Hiking Equipment See *Purchase Intent Classification* Extension + 1638 1635 Purchase Intent* | Sporting Goods | Climbing Equipment | Purchase Intent* Sporting Goods Outdoor Recreation Equipment Climbing Equipment See *Purchase Intent Classification* Extension + 1639 1635 Purchase Intent* | Sporting Goods | Bicycles and Cycling Equipment | Purchase Intent* Sporting Goods Outdoor Recreation Equipment Bicycles and Cycling Equipment See *Purchase Intent Classification* Extension + 1640 1635 Purchase Intent* | Sporting Goods | Equestrian Equipment | Purchase Intent* Sporting Goods Outdoor Recreation Equipment Equestrian Equipment See *Purchase Intent Classification* Extension + 1641 1635 Purchase Intent* | Sporting Goods | Fishing Equipment | Purchase Intent* Sporting Goods Outdoor Recreation Equipment Fishing Equipment See *Purchase Intent Classification* Extension + 1642 1635 Purchase Intent* | Sporting Goods | Golf Equipment | Purchase Intent* Sporting Goods Outdoor Recreation Equipment Golf Equipment See *Purchase Intent Classification* Extension + 1643 1635 Purchase Intent* | Sporting Goods | Hang Gliding and Skydiving Equipment | Purchase Intent* Sporting Goods Outdoor Recreation Equipment Hang Gliding and Skydiving Equipment See *Purchase Intent Classification* Extension + 1644 1635 Purchase Intent* | Sporting Goods | Hunting and Shooting Equipment | Purchase Intent* Sporting Goods Outdoor Recreation Equipment Hunting and Shooting Equipment See *Purchase Intent Classification* Extension + 1645 1635 Purchase Intent* | Sporting Goods | Inline and Roller Skating Equipment | Purchase Intent* Sporting Goods Outdoor Recreation Equipment Inline and Roller Skating Equipment See *Purchase Intent Classification* Extension + 1646 1635 Purchase Intent* | Sporting Goods | Outdoor Games Equipment | Purchase Intent* Sporting Goods Outdoor Recreation Equipment Outdoor Games Equipment See *Purchase Intent Classification* Extension + 1647 1635 Purchase Intent* | Sporting Goods | Skateboards and Accessories | Purchase Intent* Sporting Goods Outdoor Recreation Equipment Skateboards and Accessories See *Purchase Intent Classification* Extension + 1648 1635 Purchase Intent* | Sporting Goods | Winter Sports Equipment | Purchase Intent* Sporting Goods Outdoor Recreation Equipment Winter Sports Equipment See *Purchase Intent Classification* Extension + 1649 752 Purchase Intent* | Travel and Tourism Purchase Intent* Travel and Tourism See *Purchase Intent Classification* Extension + 1650 1649 Purchase Intent* | Travel and Tourism | Adventure Travel | Purchase Intent* Travel and Tourism Adventure Travel See *Purchase Intent Classification* Extension + 1651 1649 Purchase Intent* | Travel and Tourism | Air Travel | Purchase Intent* Travel and Tourism Air Travel See *Purchase Intent Classification* Extension + 1652 1649 Purchase Intent* | Travel and Tourism | Auto Rental | Purchase Intent* Travel and Tourism Auto Rental See *Purchase Intent Classification* Extension + 1653 1649 Purchase Intent* | Travel and Tourism | Beach Travel | Purchase Intent* Travel and Tourism Beach Travel See *Purchase Intent Classification* Extension + 1654 1649 Purchase Intent* | Travel and Tourism | Bed and Breakfasts | Purchase Intent* Travel and Tourism Bed and Breakfasts See *Purchase Intent Classification* Extension + 1655 1649 Purchase Intent* | Travel and Tourism | Budget Travel | Purchase Intent* Travel and Tourism Budget Travel See *Purchase Intent Classification* Extension + 1656 1649 Purchase Intent* | Travel and Tourism | Business Travel | Purchase Intent* Travel and Tourism Business Travel See *Purchase Intent Classification* Extension + 1657 1656 Purchase Intent* | Travel and Tourism | Taxi Services | Purchase Intent* Travel and Tourism Business Travel Taxi Services See *Purchase Intent Classification* Extension + 1658 1656 Purchase Intent* | Travel and Tourism | Ride-sharing Services | Purchase Intent* Travel and Tourism Business Travel Ride-sharing Services See *Purchase Intent Classification* Extension + 1659 1649 Purchase Intent* | Travel and Tourism | Camping | Purchase Intent* Travel and Tourism Camping See *Purchase Intent Classification* Extension + 1660 1649 Purchase Intent* | Travel and Tourism | Coach Travel | Purchase Intent* Travel and Tourism Coach Travel See *Purchase Intent Classification* Extension + 1661 1649 Purchase Intent* | Travel and Tourism | Cruise Travel | Purchase Intent* Travel and Tourism Cruise Travel See *Purchase Intent Classification* Extension + 1662 1649 Purchase Intent* | Travel and Tourism | Day Trips | Purchase Intent* Travel and Tourism Day Trips See *Purchase Intent Classification* Extension + 1663 1649 Purchase Intent* | Travel and Tourism | Family Travel | Purchase Intent* Travel and Tourism Family Travel See *Purchase Intent Classification* Extension + 1664 1649 Purchase Intent* | Travel and Tourism | Ferry Travel | Purchase Intent* Travel and Tourism Ferry Travel See *Purchase Intent Classification* Extension + 1665 1649 Purchase Intent* | Travel and Tourism | Honeymoons and Getaways | Purchase Intent* Travel and Tourism Honeymoons and Getaways See *Purchase Intent Classification* Extension + 1666 1649 Purchase Intent* | Travel and Tourism | Hotels and Resorts | Purchase Intent* Travel and Tourism Hotels and Resorts See *Purchase Intent Classification* Extension + 1667 1649 Purchase Intent* | Travel and Tourism | Motels | Purchase Intent* Travel and Tourism Motels See *Purchase Intent Classification* Extension + 1668 1649 Purchase Intent* | Travel and Tourism | Passenger Transportation | Purchase Intent* Travel and Tourism Passenger Transportation See *Purchase Intent Classification* Extension + 1669 1649 Purchase Intent* | Travel and Tourism | Rail Travel | Purchase Intent* Travel and Tourism Rail Travel See *Purchase Intent Classification* Extension + 1670 1649 Purchase Intent* | Travel and Tourism | Road Trips | Purchase Intent* Travel and Tourism Road Trips See *Purchase Intent Classification* Extension + 1671 1649 Purchase Intent* | Travel and Tourism | Sightseeing Tours and Activities | Purchase Intent* Travel and Tourism Sightseeing Tours and Activities See *Purchase Intent Classification* Extension + 1672 1649 Purchase Intent* | Travel and Tourism | Spas | Purchase Intent* Travel and Tourism Spas See *Purchase Intent Classification* Extension + 1673 1649 Purchase Intent* | Travel and Tourism | Timeshares | Purchase Intent* Travel and Tourism Timeshares See *Purchase Intent Classification* Extension + 1674 1649 Purchase Intent* | Travel and Tourism | Travel Agents and Online Travel Services | Purchase Intent* Travel and Tourism Travel Agents and Online Travel Services See *Purchase Intent Classification* Extension + 1675 1649 Purchase Intent* | Travel and Tourism | Travel Insurance | Purchase Intent* Travel and Tourism Travel Insurance See *Purchase Intent Classification* Extension + 1676 752 Purchase Intent* | Web Services Purchase Intent* Web Services See *Purchase Intent Classification* Extension + 1677 1676 Purchase Intent* | Web Services | Domain Services | Purchase Intent* Web Services Domain Services See *Purchase Intent Classification* Extension + 1678 1676 Purchase Intent* | Web Services | Internet Providers | Purchase Intent* Web Services Internet Providers See *Purchase Intent Classification* Extension + 1679 1676 Purchase Intent* | Web Services | Web Hosting and Cloud Computing | Purchase Intent* Web Services Web Hosting and Cloud Computing See *Purchase Intent Classification* Extension diff --git a/data/taxonomies/audience-1.1/README.md b/data/taxonomies/audience-1.1/README.md new file mode 100644 index 0000000..721140c --- /dev/null +++ b/data/taxonomies/audience-1.1/README.md @@ -0,0 +1,28 @@ +# IAB Audience Taxonomy 1.1 + +This directory vendors a verbatim copy of the IAB Tech Lab Audience Taxonomy version 1.1. + +| Field | Value | +|-------|-------| +| Version | 1.1 | +| Format | TSV | +| Source | https://raw.githubusercontent.com/InteractiveAdvertisingBureau/Taxonomies/main/Audience%20Taxonomies/Audience%20Taxonomy%201.1.tsv | +| Upstream repo | https://github.com/InteractiveAdvertisingBureau/Taxonomies | +| Fetched at | 2026-04-25T19:27:34Z | +| License | CC-BY-3.0 (https://creativecommons.org/licenses/by/3.0/) | +| Attribution | "Audience Taxonomy 1.1" by IAB Tech Lab is licensed under CC BY 3.0. | + +## Files + +- `Audience Taxonomy 1.1.tsv` — the upstream TSV, unchanged. Tier-1 splits into Demographic, Interest-based, and Purchase-intent. + +## Versioning + +IAB convention: major version = breaking change, minor version = additive. +The exact `sha256` of the vendored file is recorded in `../taxonomies.lock.json`. + +## How to refresh + +Vendor refreshes are gated through `scripts/update-taxonomies.py` (proposal §5.4) +which refetches the canonical URL, recomputes the hash, and updates the lock file. +Treat refreshes as manual reviewed updates, not silent upgrades. diff --git a/data/taxonomies/content-3.1/Content Taxonomy 3.1.tsv b/data/taxonomies/content-3.1/Content Taxonomy 3.1.tsv new file mode 100644 index 0000000..a3a76ad --- /dev/null +++ b/data/taxonomies/content-3.1/Content Taxonomy 3.1.tsv @@ -0,0 +1,706 @@ +Relational ID System Content Taxonomy v3.1 Tiered Categories Extension +Unique ID Parent Name Tier 1 Tier 2 Tier 3 Tier 4 +150 Attractions Attractions +151 150 Amusement and Theme Parks Attractions Amusement and Theme Parks +179 150 Bars & Restaurants Attractions Bars & Restaurants +181 150 Casinos & Gambling Attractions Casinos & Gambling +153 150 Historic Site and Landmark Tours Attractions Historic Site and Landmark Tours +154 150 Malls & Shopping Centers Attractions Malls & Shopping Centers +155 150 Museums & Galleries Attractions Museums & Galleries +158 150 Nightclubs Attractions Nightclubs +159 150 Outdoor Activities Attractions Outdoor Activities +160 150 Parks & Nature Attractions Parks & Nature +177 150 Theater Venues Attractions Theater Venues +178 150 Zoos & Aquariums Attractions Zoos & Aquariums +1 Automotive Automotive +2 1 Auto Body Styles Automotive Auto Body Styles +3 2 Commercial Trucks Automotive Auto Body Styles Commercial Trucks +8 2 Convertible Automotive Auto Body Styles Convertible +9 2 Coupe Automotive Auto Body Styles Coupe +10 2 Crossover Automotive Auto Body Styles Crossover +11 2 Hatchback Automotive Auto Body Styles Hatchback +12 2 Microcar Automotive Auto Body Styles Microcar +13 2 Minivan Automotive Auto Body Styles Minivan +14 2 Off-Road Vehicles Automotive Auto Body Styles Off-Road Vehicles +15 2 Pickup Trucks Automotive Auto Body Styles Pickup Trucks +4 2 Sedan Automotive Auto Body Styles Sedan +5 2 Station Wagon Automotive Auto Body Styles Station Wagon +6 2 SUV Automotive Auto Body Styles SUV +7 2 Van Automotive Auto Body Styles Van +30 1 Auto Buying and Selling Automotive Auto Buying and Selling +31 1 Auto Insurance Automotive Auto Insurance +32 1 Auto Parts Automotive Auto Parts +33 1 Auto Recalls Automotive Auto Recalls +41 1 Auto Rentals Automotive Auto Rentals +34 1 Auto Repair Automotive Auto Repair +35 1 Auto Safety Automotive Auto Safety +36 1 Auto Shows Automotive Auto Shows +37 1 Auto Technology Automotive Auto Technology +38 37 Auto Infotainment Technologies Automotive Auto Technology Auto Infotainment Technologies +39 37 Auto Navigation Systems Automotive Auto Technology Auto Navigation Systems +40 37 Auto Safety Technologies Automotive Auto Technology Auto Safety Technologies +16 1 Auto Type Automotive Auto Type +17 16 Budget Cars Automotive Auto Type Budget Cars +18 16 Certified Pre-Owned Cars Automotive Auto Type Certified Pre-Owned Cars +19 16 Classic Cars Automotive Auto Type Classic Cars +20 16 Concept Cars Automotive Auto Type Concept Cars +21 16 Driverless Cars Automotive Auto Type Driverless Cars +22 16 Green Vehicles Automotive Auto Type Green Vehicles +23 16 Luxury Cars Automotive Auto Type Luxury Cars +24 16 Performance Cars Automotive Auto Type Performance Cars +25 1 Car Culture Automotive Car Culture +26 1 Dash Cam Videos Automotive Dash Cam Videos +27 1 Motorcycles Automotive Motorcycles +28 1 Road-Side Assistance Automotive Road-Side Assistance +29 1 Scooters Automotive Scooters +42 Books and Literature Books and Literature +43 42 Art and Photography Books and Literature Art and Photography +46 42 Comics and Graphic Novels Books and Literature Comics and Graphic Novels +48 42 Fiction Books and Literature Fiction +49 42 Poetry Books and Literature Poetry +52 Business and Finance Business and Finance +53 52 Business Business and Finance Business +54 53 Business Accounting & Finance Business and Finance Business Business Accounting & Finance +62 53 Business Administration Business and Finance Business Business Administration +63 53 Business Banking & Finance Business and Finance Business Business Banking & Finance +64 63 Angel Investment Business and Finance Business Business Banking & Finance Angel Investment +65 63 Bankruptcy Business and Finance Business Business Banking & Finance Bankruptcy +66 63 Business Loans Business and Finance Business Business Banking & Finance Business Loans +67 63 Debt Factoring & Invoice Discounting Business and Finance Business Business Banking & Finance Debt Factoring & Invoice Discounting +68 63 Mergers and Acquisitions Business and Finance Business Business Banking & Finance Mergers and Acquisitions +69 63 Private Equity Business and Finance Business Business Banking & Finance Private Equity +70 63 Sale & Lease Back Business and Finance Business Business Banking & Finance Sale & Lease Back +71 63 Venture Capital Business and Finance Business Business Banking & Finance Venture Capital +72 53 Business I.T. Business and Finance Business Business I.T. +73 53 Business Operations Business and Finance Business Business Operations +79 53 Business Utilities Business and Finance Business Business Utilities +74 53 Consumer Issues Business and Finance Business Consumer Issues +75 74 Recalls Business and Finance Business Consumer Issues Recalls +76 53 Executive Leadership & Management Business and Finance Business Executive Leadership & Management +77 53 Government Business Business and Finance Business Government Business +78 53 Green Solutions Business and Finance Business Green Solutions +55 53 Human Resources Business and Finance Business Human Resources +56 53 Large Business Business and Finance Business Large Business +57 53 Logistics Business and Finance Business Logistics +58 53 Marketing and Advertising Business and Finance Business Marketing and Advertising +59 53 Sales Business and Finance Business Sales +60 53 Small and Medium-sized Business Business and Finance Business Small and Medium-sized Business +61 53 Startups Business and Finance Business Startups +80 52 Economy Business and Finance Economy +81 80 Commodities Business and Finance Economy Commodities +82 80 Currencies Business and Finance Economy Currencies +83 80 Financial Crisis Business and Finance Economy Financial Crisis +84 80 Financial Reform Business and Finance Economy Financial Reform +85 80 Financial Regulation Business and Finance Economy Financial Regulation +86 80 Gasoline Prices Business and Finance Economy Gasoline Prices +87 80 Housing Market Business and Finance Economy Housing Market +88 80 Interest Rates Business and Finance Economy Interest Rates +89 80 Job Market Business and Finance Economy Job Market +90 52 Industries Business and Finance Industries +91 90 Advertising Industry Business and Finance Industries Advertising Industry +102 90 Agriculture Business and Finance Industries Agriculture +113 90 Apparel Industry Business and Finance Industries Apparel Industry +117 90 Automotive Industry Business and Finance Industries Automotive Industry +118 90 Aviation Industry Business and Finance Industries Aviation Industry +119 90 Biotech and Biomedical Industry Business and Finance Industries Biotech and Biomedical Industry +120 90 Civil Engineering Industry Business and Finance Industries Civil Engineering Industry +121 90 Construction Industry Business and Finance Industries Construction Industry +122 90 Defense Industry Business and Finance Industries Defense Industry +92 90 Education industry Business and Finance Industries Education industry +93 90 Entertainment Industry Business and Finance Industries Entertainment Industry +94 90 Environmental Services Industry Business and Finance Industries Environmental Services Industry +95 90 Financial Industry Business and Finance Industries Financial Industry +96 90 Food Industry Business and Finance Industries Food Industry +97 90 Healthcare Industry Business and Finance Industries Healthcare Industry +98 90 Hospitality Industry Business and Finance Industries Hospitality Industry +99 90 Information Services Industry Business and Finance Industries Information Services Industry +100 90 Legal Services Industry Business and Finance Industries Legal Services Industry +101 90 Logistics and Transportation Industry Business and Finance Industries Logistics and Transportation Industry +103 90 Management Consulting Industry Business and Finance Industries Management Consulting Industry +104 90 Manufacturing Industry Business and Finance Industries Manufacturing Industry +105 90 Mechanical and Industrial Engineering Industry Business and Finance Industries Mechanical and Industrial Engineering Industry +106 90 Media Industry Business and Finance Industries Media Industry +107 90 Metals Industry Business and Finance Industries Metals Industry +108 90 Non-Profit Organizations Business and Finance Industries Non-Profit Organizations +109 90 Pharmaceutical Industry Business and Finance Industries Pharmaceutical Industry +110 90 Power and Energy Industry Business and Finance Industries Power and Energy Industry +111 90 Publishing Industry Business and Finance Industries Publishing Industry +112 90 Real Estate Industry Business and Finance Industries Real Estate Industry +114 90 Retail Industry Business and Finance Industries Retail Industry +115 90 Technology Industry Business and Finance Industries Technology Industry +116 90 Telecommunications Industry Business and Finance Industries Telecommunications Industry +123 Careers Careers +124 123 Apprenticeships Careers Apprenticeships +125 123 Career Advice Careers Career Advice +126 123 Career Planning Careers Career Planning +127 123 Job Search Careers Job Search +128 127 Job Fairs Careers Job Search Job Fairs +129 127 Resume Writing and Advice Careers Job Search Resume Writing and Advice +130 123 Remote Working Careers Remote Working +131 123 Vocational Training Careers Vocational Training +80DV8O Communication Communication +380 Crime Crime +381 Disasters Disasters +132 Education Education +133 132 Adult Education Education Adult Education +137 132 College Education Education College Education +138 137 College Planning Education College Education College Planning +139 137 Postgraduate Education Education College Education Postgraduate Education +140 139 Professional School Education College Education Postgraduate Education Professional School +141 137 Undergraduate Education Education College Education Undergraduate Education +142 132 Early Childhood Education Education Early Childhood Education +143 132 Educational Assessment Education Educational Assessment +144 143 Standardized Testing Education Educational Assessment Standardized Testing +145 132 Homeschooling Education Homeschooling +146 132 Homework and Study Education Homework and Study +147 132 Language Learning Education Language Learning +148 132 Online Education Education Online Education +149 132 Primary Education Education Primary Education +134 132 Private School Education Private School +135 132 Secondary Education Education Secondary Education SCD +136 132 Special Education Education Special Education +JLBCU7 Entertainment Entertainment +324 JLBCU7 Movies Entertainment Movies +338 JLBCU7 Music Entertainment Music +640 JLBCU7 Television Entertainment Television +8VZQHL Events Events +162 8VZQHL Awards Shows Events Awards Shows +180 8VZQHL Business Expos & Conferences Events Business Expos & Conferences +185 8VZQHL Fan Conventions Events Fan Conventions +186 Family and Relationships Family and Relationships SCD +187 186 Bereavement Family and Relationships Bereavement SCD +188 186 Dating Family and Relationships Dating +189 186 Divorce Family and Relationships Divorce +190 186 Eldercare Family and Relationships Eldercare +191 186 Marriage and Civil Unions Family and Relationships Marriage and Civil Unions +192 186 Parenting Family and Relationships Parenting +193 192 Adoption and Fostering Family and Relationships Parenting Adoption and Fostering +194 192 Daycare and Pre-School Family and Relationships Parenting Daycare and Pre-School +195 192 Internet Safety Family and Relationships Parenting Internet Safety +196 192 Parenting Babies and Toddlers Family and Relationships Parenting Parenting Babies and Toddlers +197 192 Parenting Children Aged 4-11 Family and Relationships Parenting Parenting Children Aged 4-11 SCD +198 192 Parenting Teens Family and Relationships Parenting Parenting Teens SCD +199 192 Special Needs Kids Family and Relationships Parenting Special Needs Kids +200 186 Single Life Family and Relationships Single Life +201 Fine Art Fine Art +202 201 Costume Fine Art Costume +203 201 Dance Fine Art Dance +204 201 Design Fine Art Design +205 201 Digital Arts Fine Art Digital Arts +206 201 Fine Art Photography Fine Art Fine Art Photography +207 201 Modern Art Fine Art Modern Art +208 201 Opera Fine Art Opera +209 201 Theater Fine Art Theater SCD +210 Food & Drink Food & Drink +211 210 Alcoholic Beverages Food & Drink Alcoholic Beverages +215 210 Barbecues and Grilling Food & Drink Barbecues and Grilling +216 210 Cooking Food & Drink Cooking +217 210 Desserts and Baking Food & Drink Desserts and Baking +218 210 Dining Out Food & Drink Dining Out +219 210 Food Allergies Food & Drink Food Allergies +220 210 Food Movements Food & Drink Food Movements +221 210 Healthy Cooking and Eating Food & Drink Healthy Cooking and Eating +222 210 Non-Alcoholic Beverages Food & Drink Non-Alcoholic Beverages +212 210 Vegan Diets Food & Drink Vegan Diets SCD +213 210 Vegetarian Diets Food & Drink Vegetarian Diets +214 210 World Cuisines Food & Drink World Cuisines +SPSHQ5 Genres Genres +VKIV56 SPSHQ5 Nature Genres Nature +325 SPSHQ5 Action/Adventure Genres Action/Adventure +641 SPSHQ5 Animation & Anime Genres Animation & Anime +44 SPSHQ5 Biographies Genres Biographies +646 SPSHQ5 Comedy Genres Comedy +332 SPSHQ5 Documentary Genres Documentary +647 SPSHQ5 Drama Genres Drama +648 SPSHQ5 Factual Genres Factual +645 SPSHQ5 Family/Children Genres Family/Children +335 SPSHQ5 Fantasy Genres Fantasy +EZWB7V SPSHQ5 History Genres History +649 SPSHQ5 Holiday Genres Holiday +336 SPSHQ5 Horror Genres Horror +TIFQA5 SPSHQ5 Lifestyle Genres Lifestyle +650 SPSHQ5 Music Video Genres Music Video +156 SPSHQ5 Musical Genres Musical +331 SPSHQ5 Mystery Genres Mystery +651 SPSHQ5 Reality TV Genres Reality TV +326 SPSHQ5 Romance Genres Romance +652 SPSHQ5 Science Fiction Genres Science Fiction +642 SPSHQ5 Soap Opera Genres Soap Opera +643 SPSHQ5 Special Interest (Indie/Art House) Genres Special Interest (Indie/Art House) +370 SPSHQ5 Sports Radio Genres Sports Radio +371 SPSHQ5 Talk Radio Genres Talk Radio +376 SPSHQ5 Public Radio Genres Talk Radio Public Radio +A0AH3G SPSHQ5 Talk Show Genres Talk Show +KHPC5A SPSHQ5 True Crime Genres True Crime +KHPC6A SPSHQ5 Western Genres Western SCD +51 SPSHQ5 Young Adult Genres Young Adult +700 SPSHQ5 Thriller Genres Thriller +223 Healthy Living Healthy Living +224 223 Children's Health Healthy Living Children's Health +225 223 Fitness and Exercise Healthy Living Fitness and Exercise +226 225 Participant Sports Healthy Living Fitness and Exercise Participant Sports +227 225 Running and Jogging Healthy Living Fitness and Exercise Running and Jogging +228 223 Men's Health Healthy Living Men's Health SCD +229 223 Nutrition Healthy Living Nutrition +230 223 Senior Health Healthy Living Senior Health +231 223 Weight Loss Healthy Living Weight Loss +232 223 Wellness Healthy Living Wellness +233 232 Alternative Medicine Healthy Living Wellness Alternative Medicine +234 233 Herbs and Supplements Healthy Living Wellness Alternative Medicine Herbs and Supplements SCD +235 233 Holistic Health Healthy Living Wellness Alternative Medicine Holistic Health +236 232 Physical Therapy Healthy Living Wellness Physical Therapy +237 232 Smoking Cessation Healthy Living Wellness Smoking Cessation +238 223 Women's Health Healthy Living Women's Health +239 Hobbies & Interests Hobbies & Interests +240 239 Antiquing and Antiques Hobbies & Interests Antiquing and Antiques +248 239 Arts and Crafts Hobbies & Interests Arts and Crafts +249 248 Beadwork Hobbies & Interests Arts and Crafts Beadwork +250 248 Candle and Soap Making Hobbies & Interests Arts and Crafts Candle and Soap Making +251 248 Drawing and Sketching Hobbies & Interests Arts and Crafts Drawing and Sketching +252 248 Jewelry Making Hobbies & Interests Arts and Crafts Jewelry Making +253 248 Needlework Hobbies & Interests Arts and Crafts Needlework +254 248 Painting Hobbies & Interests Arts and Crafts Painting +255 248 Photography Hobbies & Interests Arts and Crafts Photography +256 248 Scrapbooking Hobbies & Interests Arts and Crafts Scrapbooking +257 248 Woodworking Hobbies & Interests Arts and Crafts Woodworking +258 239 Beekeeping Hobbies & Interests Beekeeping +259 239 Birdwatching Hobbies & Interests Birdwatching +260 239 Cigars Hobbies & Interests Cigars +261 239 Collecting Hobbies & Interests Collecting +262 261 Comic Books Hobbies & Interests Collecting Comic Books +263 261 Stamps and Coins Hobbies & Interests Collecting Stamps and Coins +264 239 Content Production Hobbies & Interests Content Production +265 264 Audio Production Hobbies & Interests Content Production Audio Production +266 264 Freelance Writing Hobbies & Interests Content Production Freelance Writing +267 264 Screenwriting Hobbies & Interests Content Production Screenwriting +268 264 Video Production Hobbies & Interests Content Production Video Production +269 239 Games and Puzzles Hobbies & Interests Games and Puzzles +270 269 Board Games and Puzzles Hobbies & Interests Games and Puzzles Board Games and Puzzles +271 269 Card Games Hobbies & Interests Games and Puzzles Card Games +272 269 Roleplaying Games Hobbies & Interests Games and Puzzles Roleplaying Games +273 239 Genealogy and Ancestry Hobbies & Interests Genealogy and Ancestry +241 239 Magic and Illusion Hobbies & Interests Magic and Illusion +242 239 Model Toys Hobbies & Interests Model Toys +243 239 Musical Instruments Hobbies & Interests Musical Instruments +244 239 Paranormal Phenomena Hobbies & Interests Paranormal Phenomena +245 239 Radio Control Hobbies & Interests Radio Control +246 239 Sci-fi and Fantasy Hobbies & Interests Sci-fi and Fantasy +247 239 Workshops and Classes Hobbies & Interests Workshops and Classes +1KXCLD Holidays Holidays +157 1KXCLD National & Civic Holidays Holidays National & Civic Holidays +274 Home & Garden Home & Garden +275 274 Gardening Home & Garden Gardening +278 274 Home Appliances Home & Garden Home Appliances +279 274 Home Entertaining Home & Garden Home Entertaining +280 274 Home Improvement Home & Garden Home Improvement +281 274 Home Security Home & Garden Home Security +282 274 Indoor Environmental Quality Home & Garden Indoor Environmental Quality +283 274 Interior Decorating Home & Garden Interior Decorating +284 274 Landscaping Home & Garden Landscaping +285 274 Outdoor Decorating Home & Garden Outdoor Decorating +276 274 Remodeling & Construction Home & Garden Remodeling & Construction +277 274 Smart Home Home & Garden Smart Home SCD +383 Law Law SCD +286 Medical Health Medical Health SCD +323 286 Cosmetic Medical Services Medical Health Cosmetic Medical Services SCD +287 286 Diseases and Conditions Medical Health Diseases and Conditions SCD +288 287 Allergies Medical Health Diseases and Conditions Allergies SCD +306 287 Blood Disorders Medical Health Diseases and Conditions Blood Disorders SCD +312 287 Bone and Joint Conditions Medical Health Diseases and Conditions Bone and Joint Conditions SCD +313 287 Brain and Nervous System Disorders Medical Health Diseases and Conditions Brain and Nervous System Disorders SCD +314 287 Cancer Medical Health Diseases and Conditions Cancer SCD +315 287 Cold and Flu Medical Health Diseases and Conditions Cold and Flu SCD +316 287 Dental Health Medical Health Diseases and Conditions Dental Health SCD +317 287 Diabetes Medical Health Diseases and Conditions Diabetes SCD +318 287 Digestive Disorders Medical Health Diseases and Conditions Digestive Disorders SCD +289 287 Ear, Nose and Throat Conditions Medical Health Diseases and Conditions Ear, Nose and Throat Conditions SCD +290 287 Endocrine and Metabolic Diseases Medical Health Diseases and Conditions Endocrine and Metabolic Diseases SCD +291 290 Hormonal Disorders Medical Health Diseases and Conditions Endocrine and Metabolic Diseases Hormonal Disorders SCD +292 290 Menopause Medical Health Diseases and Conditions Endocrine and Metabolic Diseases Menopause SCD +293 290 Thyroid Disorders Medical Health Diseases and Conditions Endocrine and Metabolic Diseases Thyroid Disorders SCD +294 287 Eye and Vision Conditions Medical Health Diseases and Conditions Eye and Vision Conditions SCD +295 287 Foot Health Medical Health Diseases and Conditions Foot Health SCD +296 287 Heart and Cardiovascular Diseases Medical Health Diseases and Conditions Heart and Cardiovascular Diseases SCD +297 287 Infectious Diseases Medical Health Diseases and Conditions Infectious Diseases SCD +298 287 Injuries Medical Health Diseases and Conditions Injuries SCD +299 298 First Aid Medical Health Diseases and Conditions Injuries First Aid SCD +300 287 Lung and Respiratory Health Medical Health Diseases and Conditions Lung and Respiratory Health SCD +301 287 Mental Health Medical Health Diseases and Conditions Mental Health SCD +302 287 Reproductive Health Medical Health Diseases and Conditions Reproductive Health SCD +303 302 Birth Control Medical Health Diseases and Conditions Reproductive Health Birth Control SCD +304 302 Infertility Medical Health Diseases and Conditions Reproductive Health Infertility SCD +305 302 Pregnancy Medical Health Diseases and Conditions Reproductive Health Pregnancy SCD +307 287 Sexual Health Medical Health Diseases and Conditions Sexual Health SCD +308 307 Sexual Conditions Medical Health Diseases and Conditions Sexual Health Sexual Conditions SCD +309 287 Skin and Dermatology Medical Health Diseases and Conditions Skin and Dermatology SCD +310 287 Sleep Disorders Medical Health Diseases and Conditions Sleep Disorders SCD +311 287 Substance Abuse Medical Health Diseases and Conditions Substance Abuse SCD +319 286 Medical Tests Medical Health Medical Tests +320 286 Pharmaceutical Drugs Medical Health Pharmaceutical Drugs +321 286 Surgery Medical Health Surgery +322 286 Vaccines Medical Health Vaccines +342 338 Adult Album Alternative Entertainment Music Adult Album Alternative +339 338 Adult Contemporary Music Entertainment Music Adult Contemporary Music +340 339 Soft AC Music Entertainment Music Adult Contemporary Music Soft AC Music +341 339 Urban AC Music Entertainment Music Adult Contemporary Music Urban AC Music +343 338 Alternative Music Entertainment Music Alternative Music +360 338 Blues Entertainment Music Blues +344 338 Children's Music Entertainment Music Children's Music +345 338 Classic Hits Entertainment Music Classic Hits +346 338 Classical Music Entertainment Music Classical Music +347 338 College Radio Entertainment Music College Radio +348 338 Comedy (Music and Audio) Entertainment Music Comedy (Music and Audio) +349 338 Contemporary Hits/Pop/Top 40 Entertainment Music Contemporary Hits/Pop/Top 40 +350 338 Country Music Entertainment Music Country Music +351 338 Dance and Electronic Music Entertainment Music Dance and Electronic Music +354 338 Gospel Music Entertainment Music Gospel Music +355 338 Hip Hop Music Entertainment Music Hip Hop Music +356 338 Inspirational/New Age Music Entertainment Music Inspirational/New Age Music +357 338 Jazz Entertainment Music Jazz +358 338 Oldies/Adult Standards Entertainment Music Oldies/Adult Standards +362 338 R&B/Soul/Funk Entertainment Music R&B/Soul/Funk +359 338 Reggae Entertainment Music Reggae +361 338 Religious (Music and Audio) Entertainment Music Religious (Music and Audio) +363 338 Rock Music Entertainment Music Rock Music +364 363 Album-oriented Rock Entertainment Music Rock Music Album-oriented Rock +365 363 Alternative Rock Entertainment Music Rock Music Alternative Rock +366 363 Classic Rock Entertainment Music Rock Music Classic Rock +367 363 Hard Rock Entertainment Music Rock Music Hard Rock +368 363 Soft Rock Entertainment Music Rock Music Soft Rock +353 338 Songwriters/Folk Entertainment Music Songwriters/Folk +369 338 Soundtracks, TV and Showtunes Entertainment Music Soundtracks, TV and Showtunes +352 338 World/International Music Entertainment Music World/International Music +377 338 Urban Contemporary Music Entertainment Music Urban Contemporary Music +378 338 Variety (Music and Audio) Entertainment Music Variety (Music and Audio) +163 Personal Celebrations & Life Events Personal Celebrations & Life Events +164 163 Anniversary Personal Celebrations & Life Events Anniversary +166 163 Baby Shower Personal Celebrations & Life Events Baby Shower +167 163 Bachelor Party Personal Celebrations & Life Events Bachelor Party +168 163 Bachelorette Party Personal Celebrations & Life Events Bachelorette Party +169 163 Birth Personal Celebrations & Life Events Birth +170 163 Birthday Personal Celebrations & Life Events Birthday +171 163 Funeral Personal Celebrations & Life Events Funeral +172 163 Graduation Personal Celebrations & Life Events Graduation +173 163 Prom Personal Celebrations & Life Events Prom +165 163 Wedding Personal Celebrations & Life Events Wedding +391 Personal Finance Personal Finance +392 391 Consumer Banking Personal Finance Consumer Banking +393 391 Financial Assistance Personal Finance Financial Assistance +394 393 Government Support and Welfare Personal Finance Financial Assistance Government Support and Welfare +395 393 Student Financial Aid Personal Finance Financial Assistance Student Financial Aid +396 391 Financial Planning Personal Finance Financial Planning +397 391 Frugal Living Personal Finance Frugal Living +417 391 Home Utilities Personal Finance Home Utilities +418 417 Gas and Electric Personal Finance Home Utilities Gas and Electric +419 417 Internet Service Providers Personal Finance Home Utilities Internet Service Providers +420 417 Phone Services Personal Finance Home Utilities Phone Services +421 417 Water Services Personal Finance Home Utilities Water Services +398 391 Insurance Personal Finance Insurance +399 398 Health Insurance Personal Finance Insurance Health Insurance +400 398 Home Insurance Personal Finance Insurance Home Insurance +401 398 Life Insurance Personal Finance Insurance Life Insurance +402 398 Motor Insurance Personal Finance Insurance Motor Insurance +403 398 Pet Insurance Personal Finance Insurance Pet Insurance +404 398 Travel Insurance Personal Finance Insurance Travel Insurance +405 391 Personal Debt Personal Finance Personal Debt +406 405 Credit Cards Personal Finance Personal Debt Credit Cards SCD +407 405 Home Financing Personal Finance Personal Debt Home Financing +408 405 Personal Loans Personal Finance Personal Debt Personal Loans +409 405 Student Loans Personal Finance Personal Debt Student Loans +410 391 Personal Investing Personal Finance Personal Investing +411 410 Hedge Funds Personal Finance Personal Investing Hedge Funds +412 410 Mutual Funds Personal Finance Personal Investing Mutual Funds +413 410 Options Personal Finance Personal Investing Options +414 410 Stocks and Bonds Personal Finance Personal Investing Stocks and Bonds +415 391 Personal Taxes Personal Finance Personal Taxes +416 391 Retirement Planning Personal Finance Retirement Planning +422 Pets Pets +423 422 Birds Pets Birds +424 422 Cats Pets Cats +425 422 Dogs Pets Dogs +426 422 Fish and Aquariums Pets Fish and Aquariums +427 422 Large Animals Pets Large Animals +428 422 Pet Adoptions Pets Pet Adoptions +431 422 Pet Supplies Pets Pet Supplies SCD +429 422 Reptiles Pets Reptiles SCD +430 422 Veterinary Medicine Pets Veterinary Medicine +386 Politics Politics +8YPBBL 386 Civic affairs Politics Civic affairs +387 386 Elections Politics Elections +388 386 Political Issues & Policy Politics Political Issues & Policy +432 Pop Culture Pop Culture +433 432 Celebrity Deaths Pop Culture Celebrity Deaths +434 432 Celebrity Families Pop Culture Celebrity Families +435 432 Celebrity Homes Pop Culture Celebrity Homes +436 432 Celebrity Pregnancy Pop Culture Celebrity Pregnancy +437 432 Celebrity Relationships Pop Culture Celebrity Relationships +438 432 Celebrity Scandal Pop Culture Celebrity Scandal +439 432 Celebrity Style Pop Culture Celebrity Style +440 432 Humor and Satire Pop Culture Humor and Satire +W3CW2J 602 Productivity Technology & Computing Computing Computer Software and Applications Productivity +441 Real Estate Real Estate +442 441 Apartments Real Estate Apartments +445 441 Developmental Sites Real Estate Developmental Sites +446 441 Hotel Properties Real Estate Hotel Properties +447 441 Houses Real Estate Houses +448 441 Industrial Property Real Estate Industrial Property +449 441 Land and Farms Real Estate Land and Farms +450 441 Office Property Real Estate Office Property +451 441 Real Estate Buying and Selling Real Estate Real Estate Buying and Selling SCD +452 441 Real Estate Renting and Leasing Real Estate Real Estate Renting and Leasing SCD +443 441 Retail Property Real Estate Retail Property SCD +444 441 Vacation Properties Real Estate Vacation Properties SCD +453 Religion & Spirituality Religion & Spirituality SCD +454 453 Agnosticism Religion & Spirituality Agnosticism SCD +456 453 Astrology Religion & Spirituality Astrology SCD +457 453 Atheism Religion & Spirituality Atheism SCD +458 453 Buddhism Religion & Spirituality Buddhism SCD +459 453 Christianity Religion & Spirituality Christianity SCD +460 453 Hinduism Religion & Spirituality Hinduism SCD +461 453 Islam Religion & Spirituality Islam SCD +462 453 Judaism Religion & Spirituality Judaism +463 453 Sikhism Religion & Spirituality Sikhism +455 453 Spirituality Religion & Spirituality Spirituality SCD +464 Science Science +465 464 Biological Sciences Science Biological Sciences +466 464 Chemistry Science Chemistry +467 464 Environment Science Environment +468 464 Genetics Science Genetics +469 464 Geography Science Geography +470 464 Geology Science Geology +471 464 Physics Science Physics +472 464 Space and Astronomy Science Space and Astronomy +390 464 Weather Science Weather +v9i3On Sensitive Topics Sensitive Topics +Rm3SiT v9i3On Adult & Explicit Sexual Content Sensitive Topics Adult & Explicit Sexual Content +avbNf2 v9i3On Arms & Ammunition Sensitive Topics Arms & Ammunition +XtODT3 v9i3On Crime & Harmful Acts to Individuals, Society & Human Right Violations Sensitive Topics Crime & Harmful Acts to Individuals, Society & Human Right Violations +I4GWl6 v9i3On Death, Injury, or Military Conflict Sensitive Topics Death, Injury, or Military Conflict +Z7rJBM v9i3On Debated Sensitive Social Issues Sensitive Topics Debated Sensitive Social Issues +HxqYV1 v9i3On Hate Speech and Acts of Aggression Sensitive Topics Hate Speech and Acts of Aggression +pg0WhF v9i3On Illegal Drugs, Tobacco, eCigarettes, Vaping, Alcohol Sensitive Topics Illegal Drugs, Tobacco, eCigarettes, Vaping, Alcohol +j9PaO9 v9i3On Obscenity and Profanity Sensitive Topics Obscenity and Profanity +mm3UXx v9i3On Online Piracy Sensitive Topics Online Piracy +6i4dB6 v9i3On Spam or Harmful Content Sensitive Topics Spam or Harmful Content +8FD8nI v9i3On Terrorism Sensitive Topics Terrorism +473 Shopping Shopping +482 473 Children's Games and Toys Shopping Children's Games and Toys +474 473 Coupons and Discounts Shopping Coupons and Discounts +475 473 Flower Shopping Shopping Flower Shopping +476 473 Gifts and Greetings Cards Shopping Gifts and Greetings Cards +477 473 Grocery Shopping Shopping Grocery Shopping +478 473 Holiday Shopping Shopping Holiday Shopping +479 473 Household Supplies Shopping Household Supplies +480 473 Lotteries and Scratchcards Shopping Lotteries and Scratchcards +161 473 Party Supplies and Decorations Shopping Party Supplies and Decorations +481 473 Sales and Promotions Shopping Sales and Promotions +483 Sports Sports +484 483 American Football Sports American Football +507 483 Australian Rules Football Sports Australian Rules Football +518 483 Auto Racing Sports Auto Racing SCD +519 518 Motorcycle Sports Sports Auto Racing Motorcycle Sports +534 483 Badminton Sports Badminton +545 483 Baseball Sports Baseball +547 483 Basketball Sports Basketball +548 483 Beach Volleyball Sports Beach Volleyball +549 483 Bodybuilding Sports Bodybuilding +550 483 Bowling Sports Bowling +485 483 Boxing Sports Boxing +486 483 Cheerleading Sports Cheerleading +487 483 College Sports Sports College Sports +490 487 College Baseball Sports College Sports College Baseball +489 487 College Basketball Sports College Sports College Basketball +488 487 College Football Sports College Sports College Football +491 483 Cricket Sports Cricket +492 483 Cycling Sports Cycling +493 483 Darts Sports Darts +494 483 Disabled Sports Sports Disabled Sports +495 483 Diving Sports Diving +496 483 Equine Sports Sports Equine Sports +497 483 Horse Racing Sports Equine Sports Horse Racing +498 483 Extreme Sports Sports Extreme Sports +499 498 Canoeing and Kayaking Sports Extreme Sports Canoeing and Kayaking +500 498 Climbing Sports Extreme Sports Climbing +501 498 Paintball Sports Extreme Sports Paintball +502 498 Scuba Diving Sports Extreme Sports Scuba Diving +503 498 Skateboarding Sports Extreme Sports Skateboarding +504 498 Snowboarding Sports Extreme Sports Snowboarding +505 498 Surfing and Bodyboarding Sports Extreme Sports Surfing and Bodyboarding +506 498 Waterskiing and Wakeboarding Sports Extreme Sports Waterskiing and Wakeboarding +508 483 Fantasy Sports Sports Fantasy Sports +509 483 Field Hockey Sports Field Hockey +510 483 Figure Skating Sports Figure Skating +511 483 Fishing Sports Sports Fishing Sports +512 483 Golf Sports Golf +513 483 Gymnastics Sports Gymnastics +514 483 Hunting and Shooting Sports Hunting and Shooting +515 483 Ice Hockey Sports Ice Hockey +516 483 Inline Skating Sports Inline Skating +517 483 Lacrosse Sports Lacrosse +520 483 Martial Arts Sports Martial Arts +521 483 Olympic Sports Sports Olympic Sports +522 521 Summer Olympic Sports Sports Olympic Sports Summer Olympic Sports +523 521 Winter Olympic Sports Sports Olympic Sports Winter Olympic Sports +524 483 Poker and Professional Gambling Sports Poker and Professional Gambling +525 483 Rodeo Sports Rodeo +526 483 Rowing Sports Rowing +527 483 Rugby Sports Rugby +528 527 Rugby League Sports Rugby Rugby League +529 527 Rugby Union Sports Rugby Rugby Union +530 483 Sailing Sports Sailing +531 483 Skiing Sports Skiing +532 483 Snooker/Pool/Billiards Sports Snooker/Pool/Billiards +533 483 Soccer Sports Soccer +535 483 Softball Sports Softball +551 483 Sports Equipment Sports Sports Equipment +536 483 Squash Sports Squash +537 483 Swimming Sports Swimming +538 483 Table Tennis Sports Table Tennis +539 483 Tennis Sports Tennis +540 483 Track and Field Sports Track and Field +541 483 Volleyball Sports Volleyball +542 483 Walking Sports Walking +543 483 Water Polo Sports Water Polo +544 483 Weightlifting Sports Weightlifting +546 483 Wrestling Sports Wrestling +552 Style & Fashion Style & Fashion +553 552 Beauty Style & Fashion Beauty +554 553 Hair Care Style & Fashion Beauty Hair Care +555 553 Makeup and Accessories Style & Fashion Beauty Makeup and Accessories +556 553 Nail Care Style & Fashion Beauty Nail Care +557 553 Natural and Organic Beauty Style & Fashion Beauty Natural and Organic Beauty +558 553 Perfume and Fragrance Style & Fashion Beauty Perfume and Fragrance +559 553 Skin Care Style & Fashion Beauty Skin Care +574 552 Body Art Style & Fashion Body Art +575 552 Children's Clothing Style & Fashion Children's Clothing +576 552 Designer Clothing Style & Fashion Designer Clothing +577 552 Fashion Trends Style & Fashion Fashion Trends +578 552 High Fashion Style & Fashion High Fashion +579 552 Men's Fashion Style & Fashion Men's Fashion +580 579 Men's Accessories Style & Fashion Men's Fashion Men's Accessories +581 580 Men's Jewelry and Watches Style & Fashion Men's Fashion Men's Accessories Men's Jewelry and Watches +582 579 Men's Clothing Style & Fashion Men's Fashion Men's Clothing +583 582 Men's Business Wear Style & Fashion Men's Fashion Men's Clothing Men's Business Wear +584 582 Men's Casual Wear Style & Fashion Men's Fashion Men's Clothing Men's Casual Wear +585 582 Men's Formal Wear Style & Fashion Men's Fashion Men's Clothing Men's Formal Wear +586 582 Men's Outerwear Style & Fashion Men's Fashion Men's Clothing Men's Outerwear +587 582 Men's Sportswear Style & Fashion Men's Fashion Men's Clothing Men's Sportswear +588 582 Men's Underwear and Sleepwear Style & Fashion Men's Fashion Men's Clothing Men's Underwear and Sleepwear +589 579 Men's Shoes and Footwear Style & Fashion Men's Fashion Men's Shoes and Footwear +590 552 Personal Care Style & Fashion Personal Care +591 590 Bath and Shower Style & Fashion Personal Care Bath and Shower +592 590 Deodorant and Antiperspirant Style & Fashion Personal Care Deodorant and Antiperspirant +593 590 Oral care Style & Fashion Personal Care Oral care +594 590 Shaving Style & Fashion Personal Care Shaving +595 552 Street Style Style & Fashion Street Style +560 552 Women's Fashion Style & Fashion Women's Fashion +561 560 Women's Accessories Style & Fashion Women's Fashion Women's Accessories +562 561 Women's Glasses Style & Fashion Women's Fashion Women's Accessories Women's Glasses +563 561 Women's Handbags and Wallets Style & Fashion Women's Fashion Women's Accessories Women's Handbags and Wallets +564 561 Women's Hats and Scarves Style & Fashion Women's Fashion Women's Accessories Women's Hats and Scarves +565 561 Women's Jewelry and Watches Style & Fashion Women's Fashion Women's Accessories Women's Jewelry and Watches +566 560 Women's Clothing Style & Fashion Women's Fashion Women's Clothing +567 566 Women's Business Wear Style & Fashion Women's Fashion Women's Clothing Women's Business Wear +568 566 Women's Casual Wear Style & Fashion Women's Fashion Women's Clothing Women's Casual Wear +569 566 Women's Formal Wear Style & Fashion Women's Fashion Women's Clothing Women's Formal Wear +570 566 Women's Intimates and Sleepwear Style & Fashion Women's Fashion Women's Clothing Women's Intimates and Sleepwear +571 566 Women's Outerwear Style & Fashion Women's Fashion Women's Clothing Women's Outerwear +572 566 Women's Sportswear Style & Fashion Women's Fashion Women's Clothing Women's Sportswear +573 560 Women's Shoes and Footwear Style & Fashion Women's Fashion Women's Shoes and Footwear +596 Technology & Computing Technology & Computing +597 596 Artificial Intelligence Technology & Computing Artificial Intelligence +598 596 Augmented Reality Technology & Computing Augmented Reality +599 596 Computing Technology & Computing Computing +600 599 Computer Networking Technology & Computing Computing Computer Networking +601 599 Computer Peripherals Technology & Computing Computing Computer Peripherals +602 599 Computer Software and Applications Technology & Computing Computing Computer Software and Applications +603 602 3-D Graphics Technology & Computing Computing Computer Software and Applications 3-D Graphics +608 602 Antivirus Software Technology & Computing Computing Computer Software and Applications Antivirus Software +609 602 Browsers Technology & Computing Computing Computer Software and Applications Browsers +610 602 Computer Animation Technology & Computing Computing Computer Software and Applications Computer Animation +611 602 Databases Technology & Computing Computing Computer Software and Applications Databases +612 602 Desktop Publishing Technology & Computing Computing Computer Software and Applications Desktop Publishing +613 602 Digital Audio Technology & Computing Computing Computer Software and Applications Digital Audio +WQC6HR 602 Maps & Navigation Technology & Computing Computing Computer Software and Applications Maps & Navigation +614 602 Graphics Software Technology & Computing Computing Computer Software and Applications Graphics Software +615 602 Operating Systems Technology & Computing Computing Computer Software and Applications Operating Systems +604 602 Photo Editing Software Technology & Computing Computing Computer Software and Applications Photo Editing Software +605 602 Shareware and Freeware Technology & Computing Computing Computer Software and Applications Shareware and Freeware +606 602 Video Software Technology & Computing Computing Computer Software and Applications Video Software +607 602 Web Conferencing Technology & Computing Computing Computer Software and Applications Web Conferencing +616 599 Data Storage and Warehousing Technology & Computing Computing Data Storage and Warehousing +617 599 Desktops Technology & Computing Computing Desktops +618 599 Information and Network Security Technology & Computing Computing Information and Network Security +619 599 Internet Technology & Computing Computing Internet +620 619 Cloud Computing Technology & Computing Computing Internet Cloud Computing +623 619 Email Technology & Computing Computing Internet Email +624 619 Internet for Beginners Technology & Computing Computing Internet Internet for Beginners +625 619 Internet of Things Technology & Computing Computing Internet Internet of Things +626 619 IT and Internet Support Technology & Computing Computing Internet IT and Internet Support +627 619 Search Technology & Computing Computing Internet Search +628 619 Social Networking Technology & Computing Computing Internet Social Networking +629 619 Web Design and HTML Technology & Computing Computing Internet Web Design and HTML +621 619 Web Development Technology & Computing Computing Internet Web Development +622 619 Web Hosting Technology & Computing Computing Internet Web Hosting +630 599 Laptops Technology & Computing Computing Laptops +631 599 Programming Languages Technology & Computing Computing Programming Languages +632 596 Consumer Electronics Technology & Computing Consumer Electronics +633 632 Cameras and Camcorders Technology & Computing Consumer Electronics Cameras and Camcorders +634 632 Home Entertainment Systems Technology & Computing Consumer Electronics Home Entertainment Systems +635 632 Smartphones Technology & Computing Consumer Electronics Smartphones +636 632 Tablets and E-readers Technology & Computing Consumer Electronics Tablets and E-readers +637 632 Wearable Technology Technology & Computing Consumer Electronics Wearable Technology +638 596 Robotics Technology & Computing Robotics +639 596 Virtual Reality Technology & Computing Virtual Reality +653 Travel Travel +654 653 Travel Accessories Travel Travel Accessories +655 653 Travel Locations Travel Travel Locations +656 655 Africa Travel Travel Travel Locations Africa Travel +657 655 Asia Travel Travel Travel Locations Asia Travel +658 655 Australia and Oceania Travel Travel Travel Locations Australia and Oceania Travel +659 655 Europe Travel Travel Travel Locations Europe Travel +660 655 North America Travel Travel Travel Locations North America Travel +661 655 Polar Travel Travel Travel Locations Polar Travel +662 655 South America Travel Travel Travel Locations South America Travel +663 653 Travel Preparation and Advice Travel Travel Preparation and Advice +664 653 Travel Type Travel Travel Type +665 664 Adventure Travel Travel Travel Type Adventure Travel +672 664 Air Travel Travel Travel Type Air Travel +673 664 Beach Travel Travel Travel Type Beach Travel +674 664 Bed & Breakfasts Travel Travel Type Bed & Breakfasts +675 664 Budget Travel Travel Travel Type Budget Travel +676 664 Business Travel Travel Travel Type Business Travel +677 664 Camping Travel Travel Type Camping +678 664 Cruises Travel Travel Type Cruises +679 664 Day Trips Travel Travel Type Day Trips +666 664 Family Travel Travel Travel Type Family Travel +667 664 Honeymoons and Getaways Travel Travel Type Honeymoons and Getaways +668 664 Hotels and Motels Travel Travel Type Hotels and Motels +669 664 Rail Travel Travel Travel Type Rail Travel +670 664 Road Trips Travel Travel Type Road Trips +671 664 Spas Travel Travel Type Spas +680 Video Gaming Video Gaming +681 680 Console Games Video Gaming Console Games +682 680 eSports Video Gaming eSports +683 680 Mobile Games Video Gaming Mobile Games +684 680 PC Games Video Gaming PC Games +685 680 Video Game Genres Video Gaming Video Game Genres +686 685 Action Video Games Video Gaming Video Game Genres Action Video Games +691 685 Action-Adventure Video Games Video Gaming Video Game Genres Action-Adventure Video Games +MQ2XML 685 Adult Video Games Video Gaming Video Game Genres Adult Video Games +692 685 Adventure Video Games Video Gaming Video Game Genres Adventure Video Games +ZJG29S 685 Casino and Gambling Video Games Video Gaming Video Game Genres Casino and Gambling Video Games +693 685 Casual Games Video Gaming Video Game Genres Casual Games +694 685 Educational Video Games Video Gaming Video Game Genres Educational Video Games +695 685 Exercise and Fitness Video Games Video Gaming Video Game Genres Exercise and Fitness Video Games +VWGKS7 685 Family Video Games Video Gaming Video Game Genres Family Video Games +II436J 685 Horror Video Games Video Gaming Video Game Genres Horror Video Games +696 685 MMOs Video Gaming Video Game Genres MMOs +697 685 Music and Party Video Games Video Gaming Video Game Genres Music and Party Video Games +698 685 Puzzle Video Games Video Gaming Video Game Genres Puzzle Video Games +VK7KD0 685 Racing Video Games Video Gaming Video Game Genres Racing Video Games +687 685 Role-Playing Video Games Video Gaming Video Game Genres Role-Playing Video Games +688 685 Simulation Video Games Video Gaming Video Game Genres Simulation Video Games +689 685 Sports Video Games Video Gaming Video Game Genres Sports Video Games +690 685 Strategy Video Games Video Gaming Video Game Genres Strategy Video Games +389 War and Conflicts War and Conflicts diff --git a/data/taxonomies/content-3.1/README.md b/data/taxonomies/content-3.1/README.md new file mode 100644 index 0000000..98e4261 --- /dev/null +++ b/data/taxonomies/content-3.1/README.md @@ -0,0 +1,30 @@ +# IAB Content Taxonomy 3.1 + +This directory vendors a verbatim copy of the IAB Tech Lab Content Taxonomy version 3.1. + +| Field | Value | +|-------|-------| +| Version | 3.1 | +| Format | TSV | +| Source | https://raw.githubusercontent.com/InteractiveAdvertisingBureau/Taxonomies/main/Content%20Taxonomies/Content%20Taxonomy%203.1.tsv | +| Upstream repo | https://github.com/InteractiveAdvertisingBureau/Taxonomies | +| Fetched at | 2026-04-25T19:27:34Z | +| License | CC-BY-3.0 (https://creativecommons.org/licenses/by/3.0/) | +| Attribution | "Content Taxonomy 3.1" by IAB Tech Lab is licensed under CC BY 3.0. | + +## Files + +- `Content Taxonomy 3.1.tsv` — the upstream TSV, unchanged. ~1,500 hierarchical + category IDs, cross-mapped to CTV Genre, Podcast Genre, and Ad Product taxonomies. + +## Versioning notes + +Content Taxonomy 3.x is **not backwards compatible** with 2.x — deletions exist. +IAB ships an "IAB Mapper" tool for migration. Briefs referencing 2.x IDs that +were deleted in 3.x must fail loudly or run through Mapper (see proposal §7). + +The exact `sha256` of the vendored file is recorded in `../taxonomies.lock.json`. + +## How to refresh + +See ../audience-1.1/README.md. diff --git a/data/taxonomies/taxonomies.lock.json b/data/taxonomies/taxonomies.lock.json new file mode 100644 index 0000000..393f0ed --- /dev/null +++ b/data/taxonomies/taxonomies.lock.json @@ -0,0 +1,50 @@ +{ + "schema_version": "1", + "audience": { + "version": "1.1", + "source": "https://raw.githubusercontent.com/InteractiveAdvertisingBureau/Taxonomies/main/Audience%20Taxonomies/Audience%20Taxonomy%201.1.tsv", + "path": "audience-1.1/Audience Taxonomy 1.1.tsv", + "sha256": "0216547402f3dc028f5ec1bb78c648eed68a81ea3b3b94862e6f6caa9db3ad3b", + "fetched_at": "2026-04-25T19:27:34Z", + "license": "CC-BY-3.0", + "license_url": "https://creativecommons.org/licenses/by/3.0/", + "format": "tsv" + }, + "content": { + "version": "3.1", + "source": "https://raw.githubusercontent.com/InteractiveAdvertisingBureau/Taxonomies/main/Content%20Taxonomies/Content%20Taxonomy%203.1.tsv", + "path": "content-3.1/Content Taxonomy 3.1.tsv", + "sha256": "7212cdc496ba347a03e703b1932bdcdd4fd29089b058f4edeb4d3da1f1222ea7", + "fetched_at": "2026-04-25T19:27:34Z", + "license": "CC-BY-3.0", + "license_url": "https://creativecommons.org/licenses/by/3.0/", + "format": "tsv" + }, + "agentic": { + "version": "draft-2026-01", + "spec_url": "https://github.com/IABTechLab/agentic-audiences", + "source": "https://github.com/IABTechLab/agentic-audiences/tree/main", + "path": "agentic-audiences-draft-2026-01/spec", + "sha256": "fca42f8f274a806bb4b98ecda4ad99c68ad191e2c7b934cfb3b2693e4f3fd35b", + "sha256_method": "sha256(sorted lines of '\\t\\n')", + "fetched_at": "2026-04-25T19:27:34Z", + "license": "CC-BY-4.0 (spec) + Apache-2.0 (impl)", + "license_url_spec": "https://creativecommons.org/licenses/by/4.0/", + "license_url_impl": "https://www.apache.org/licenses/LICENSE-2.0", + "format": "directory", + "files": { + "spec/LICENSE": "6beddc683797cc09a87fa410ca8898cd5f2fd3c86d4cb950528bbe46ae8078bc", + "spec/LICENSE-APACHE": "58d1e17ffe5109a7ae296caafcadfdbe6a7d176f0bc4ab01e12a689b0499d8bd", + "spec/UPSTREAM_README.md": "15e1de07be1f2ff4820b8f90fae6837643be6a34930265a5d0a8c0f0f0d50393", + "spec/docs/systems-and-models.md": "918e5b51496c775bbc2dace66d7e0ffcb76796a2917ec1cac8a096db64050af2", + "spec/roadmap.md": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "spec/v1.0/embedding-exchange.md": "476c82bd0d7aa2c603d2816e4949cfce4543c7f1638bcd1f37cd2064f159d1ff", + "spec/v1.0/embedding-taxonomy.md": "19bc97157d34bddb7b52a792bf87e635eac8847874e586b3aa9a952c0b7e6dd7", + "spec/v1.0/examples/buyer_agent_request.json": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "spec/v1.0/examples/embedding_update.json": "7cd0d362e80a3486a9399cf3bee5c3c8e041e78d97339432d106cfc21ef6d32a", + "spec/v1.0/examples/seller_agent_response.json": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "spec/v1.0/schema/agent_interface.schema.json": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "spec/v1.0/schema/embedding_format.schema.json": "1a5dc7f0e2821f7b312f2bd52a7c1c035fb9ded808b546d81824dd8ec440581b" + } + } +} diff --git a/docs/api/mcp.md b/docs/api/mcp.md index dfd15ee..241d517 100644 --- a/docs/api/mcp.md +++ b/docs/api/mcp.md @@ -6,9 +6,11 @@ MCP is the **primary interface** for the seller agent. Publishers manage their a | Endpoint | Method | Description | |----------|--------|-------------| -| `/mcp` | POST / GET | Streamable HTTP transport (current MCP standard, protocol 2025-06-18) — recommended | +| `/mcp/mcp` | POST / GET | Streamable HTTP transport (current MCP standard, protocol 2025-06-18) — recommended | | `/mcp-sse/sse` | GET | Legacy HTTP+SSE transport (protocol 2024-11-05, deprecated) — kept for backwards compat | +> **Path note:** The MCP sub-app is mounted at `/mcp`, but Starlette sub-app routing means the working client URL is `/mcp/mcp`. Connecting to bare `/mcp` returns a 307 redirect that most MCP clients cannot follow. + ### Setup **Claude (Desktop & Web):** Settings > Integrations > Add Custom Integration > paste your MCP URL @@ -18,7 +20,7 @@ MCP is the **primary interface** for the seller agent. Publishers manage their a **Codex:** Add to `~/.codex/config.toml`: ```toml [mcp_servers.seller-agent] -url = "https://your-server.example.com/mcp" +url = "https://your-server.example.com/mcp/mcp" bearer_token_env_var = "SELLER_AGENT_API_KEY" ``` @@ -27,7 +29,7 @@ bearer_token_env_var = "SELLER_AGENT_API_KEY" { "mcpServers": { "seller-agent": { - "url": "https://your-server.example.com/mcp", + "url": "https://your-server.example.com/mcp/mcp", "headers": { "Authorization": "Bearer " } } } diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 44ba442..aca5069 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -18,7 +18,7 @@ graph LR end subgraph "Seller Agent" - MCP_S["/mcp (Streamable HTTP)
MCP Server"] + MCP_S["/mcp/mcp (Streamable HTTP)
MCP Server"] A2A_S["/a2a/seller/jsonrpc
A2A Server"] REST_S["REST API
58 endpoints"] NLP[NL Processing] @@ -59,7 +59,7 @@ graph TB subgraph "Seller Agent" subgraph "Protocol Layer" - MCP[MCP Server
/mcp Streamable HTTP] + MCP[MCP Server
/mcp/mcp Streamable HTTP] A2A[A2A Server
/a2a/seller/jsonrpc] API[REST API
58 endpoints, 19 tags] end @@ -184,7 +184,7 @@ graph LR BA -->|"1. Discover (GET /.well-known/agent.json)"| SA BA -->|"2. Get API Key (POST /auth/api-keys)"| SA - BA -->|"3. MCP: Structured tool calls (/mcp Streamable HTTP)"| SA + BA -->|"3. MCP: Structured tool calls (/mcp/mcp Streamable HTTP)"| SA BA -->|"4. A2A: Natural language (/a2a/seller/jsonrpc)"| SA BA -->|"5. REST: Browse, quote, book, negotiate"| SA diff --git a/docs/guides/chatgpt-setup.md b/docs/guides/chatgpt-setup.md index b0c47d8..19d0d4f 100644 --- a/docs/guides/chatgpt-setup.md +++ b/docs/guides/chatgpt-setup.md @@ -6,7 +6,7 @@ Connect your seller agent to ChatGPT, OpenAI Codex, Cursor, Windsurf, or any MCP Same as [Claude Desktop Setup](claude-desktop-setup.md) — your developer must have deployed the seller agent and generated credentials. -Your seller agent MCP endpoint: `https://your-publisher.example.com/mcp` +Your seller agent MCP endpoint: `https://your-publisher.example.com/mcp/mcp` --- @@ -26,7 +26,7 @@ ChatGPT natively supports MCP servers via Developer Mode. 1. Go to **Settings > Connectors** (or **Settings > Apps**) 2. Click **Create** -3. Enter your MCP server URL: `https://your-publisher.example.com/mcp` +3. Enter your MCP server URL: `https://your-publisher.example.com/mcp/mcp` 4. Name it: `Seller Agent` 5. Add a description: `Manage publisher inventory, deals, pricing, and buyer relationships` 6. Click **Create** @@ -65,7 +65,7 @@ Codex supports MCP servers via its config file. ### Option A: CLI ```bash -codex mcp add seller-agent --url https://your-publisher.example.com/mcp +codex mcp add seller-agent --url https://your-publisher.example.com/mcp/mcp ``` ### Option B: Config File @@ -74,7 +74,7 @@ Edit `~/.codex/config.toml` (global) or `.codex/config.toml` (project): ```toml [mcp_servers.seller-agent] -url = "https://your-publisher.example.com/mcp" +url = "https://your-publisher.example.com/mcp/mcp" bearer_token_env_var = "SELLER_AGENT_API_KEY" ``` @@ -102,7 +102,7 @@ Create `.cursor/mcp.json` in your project root: { "mcpServers": { "seller-agent": { - "url": "https://your-publisher.example.com/mcp", + "url": "https://your-publisher.example.com/mcp/mcp", "headers": { "Authorization": "Bearer sk-operator-XXXXX" } @@ -121,7 +121,7 @@ Create `~/.cursor/mcp.json` with the same format. { "mcpServers": { "seller-agent": { - "url": "https://your-publisher.example.com/mcp", + "url": "https://your-publisher.example.com/mcp/mcp", "headers": { "Authorization": "Bearer ${env:SELLER_AGENT_API_KEY}" } @@ -150,7 +150,7 @@ Edit `~/.codeium/windsurf/mcp_config.json`: { "mcpServers": { "seller-agent": { - "serverUrl": "https://your-publisher.example.com/mcp", + "serverUrl": "https://your-publisher.example.com/mcp/mcp", "headers": { "Authorization": "Bearer sk-operator-XXXXX" } diff --git a/docs/guides/claude-desktop-setup.md b/docs/guides/claude-desktop-setup.md index 4572d3b..e3cc768 100644 --- a/docs/guides/claude-desktop-setup.md +++ b/docs/guides/claude-desktop-setup.md @@ -23,7 +23,7 @@ Works on both **Claude Desktop** and **Claude on the web** (claude.ai): 1. Open Claude Desktop or go to [claude.ai](https://claude.ai) 2. Go to **Settings > Integrations** 3. Click **"+ Add Custom Integration"** -4. Enter your seller agent's MCP URL: `https://your-publisher.example.com/mcp` +4. Enter your seller agent's MCP URL: `https://your-publisher.example.com/mcp/mcp` 5. If prompted for authentication, enter your operator API key 6. Click **Save** diff --git a/docs/guides/developer-setup.md b/docs/guides/developer-setup.md index 79401c8..88e96b6 100644 --- a/docs/guides/developer-setup.md +++ b/docs/guides/developer-setup.md @@ -113,7 +113,7 @@ Create `claude_desktop_config.json` for your business team: { "mcpServers": { "seller-agent": { - "url": "http://your-server:8000/mcp", + "url": "http://your-server:8000/mcp/mcp", "headers": { "Authorization": "Bearer " } diff --git a/docs/guides/media-kit.md b/docs/guides/media-kit.md index c0b84ab..cda279d 100644 --- a/docs/guides/media-kit.md +++ b/docs/guides/media-kit.md @@ -60,7 +60,19 @@ curl -X POST http://localhost:8000/packages \ "description": "Live sports and highlights across CTV and mobile", "product_ids": ["prod-video-001", "prod-ctv-002"], "cat": ["IAB19", "IAB19-29"], - "audience_segment_ids": ["3", "4", "5"], + "cattax": "3", + "audience_capabilities": { + "standard_segment_ids": ["3", "4", "5"], + "standard_taxonomy_version": "1.1", + "contextual_segment_ids": ["IAB19-29"], + "contextual_taxonomy_version": "3.1", + "agentic_capabilities": { + "supported_signal_types": ["identity", "contextual"], + "embedding_dim_range": [256, 1024], + "spec_version": "draft-2026-01", + "consent_modes": ["gdpr", "tcfv2"] + } + }, "device_types": [3, 4, 5], "ad_formats": ["video"], "geo_targets": ["US"], @@ -77,12 +89,52 @@ curl -X POST http://localhost:8000/packages \ | Field | Format | Example | |-------|--------|---------| | `cat` | IAB Content Taxonomy v2/v3 IDs | `["IAB19", "IAB19-29"]` | -| `audience_segment_ids` | IAB Audience Taxonomy 1.1 IDs | `["3", "4", "5"]` | +| `cattax` | Content Taxonomy version | `"3"` (CT 3.x) | +| `audience_capabilities` | Typed audience block (see below) | object | | `device_types` | AdCOM DeviceType integers | `[3, 7]` (CTV, STB) | | `ad_formats` | OpenRTB format names | `["video", "banner"]` | | `geo_targets` | ISO 3166-2 codes | `["US", "US-NY"]` | | `tags` | Human-readable search terms | `["premium", "sports"]` | +#### `audience_capabilities` shape + +The flat `audience_segment_ids` field is **deprecated**. Packages now declare a typed `audience_capabilities` block describing what kinds of audiences a package can fulfill across the three IAB-standardized audience types: + +| Sub-field | Format | Notes | +|-----------|--------|-------| +| `standard_segment_ids` | IAB Audience Taxonomy 1.1 segment IDs | Replaces flat `audience_segment_ids` | +| `standard_taxonomy_version` | Version string | `"1.1"` | +| `contextual_segment_ids` | IAB Content Taxonomy 3.1 IDs | Audience-level *intent* (vs. `cat`, which is what the content IS) | +| `contextual_taxonomy_version` | Version string | `"3.1"` | +| `agentic_capabilities` | Block or `null` | Declared only by packages that support advertiser first-party activation | + +`agentic_capabilities` (when present) carries: + +| Sub-field | Format | Notes | +|-----------|--------|-------| +| `supported_signal_types` | Array of `"identity"`, `"contextual"`, `"reinforcement"` | Which IAB Agentic Audiences signal types the package can match on | +| `embedding_dim_range` | `[min, max]` integers | Embedding dimensions accepted, e.g. `[256, 1024]` | +| `spec_version` | Spec version string | `"draft-2026-01"` | +| `consent_modes` | Array of consent regimes | `["gdpr", "tcfv2", "ccpa"]` | + +!!! note "`cat` vs. `contextual_segment_ids`" + `cat` describes what the content IS (e.g. an automotive blog page tagged `IAB-2`). `contextual_segment_ids` describes what the audience is **reading or showing intent toward**. The two often overlap but carry different semantics --- `cat` is about the page; `contextual_segment_ids` is about the audience-level signal derived from page consumption. + +#### Positioning your packages + +Sellers position packages along three axes parallel to the three audience types. Pick whichever combination matches your inventory's strengths --- packages can sit on multiple axes at once. + +| Positioning | What you populate | Best for | +|-------------|-------------------|----------| +| **Standard direct response** | `standard_segment_ids` densely; leave `agentic_capabilities` null | Buyers running portable, third-party-aligned segment buys (CPA, ROAS) | +| **Content-adjacency contextual** | `contextual_segment_ids` paired with `cat` | Buyers running privacy-resilient adjacency targeting; cookieless contexts | +| **Agentic premium tier** | `agentic_capabilities` declared with the signal types you can match on | Buyers activating advertiser first-party signal, lookalikes, dynamic audiences. This is the premium tier --- requires consent infrastructure | + +A typical premium package declares all three. A pure direct-response package may only declare the standard tier. The buyer's Audience Planner composes a mixed `AudiencePlan` (Standard primary + Contextual constraint + Agentic extension is the canonical shape) and the buyer's pre-flight degrades the plan against your declared capabilities before booking. + +!!! tip "What to declare in each tier" + Start with what your ad server actually delivers reliably. A package that declares `agentic_capabilities` but cannot honor an embedding-driven match at fulfillment will be dropped from the buyer's seller pool after a single rejection. Better to under-declare and earn upgrades than over-declare and lose the seat. + ### Layer 3: Dynamic (Agent-Assembled) Buyer or seller agents can assemble custom packages on the fly from product IDs. The system computes blended pricing and merges inventory characteristics automatically. @@ -115,7 +167,9 @@ The media kit shows different levels of detail depending on the buyer's authenti | **Exact tier-adjusted price** | — | Yes | Yes | Yes | | **Floor price** | — | Yes | Yes | Yes | | **Placements** (product details) | — | Yes | Yes | Yes | -| **Audience segment IDs** | — | Yes | Yes | Yes | +| **Audience capability versions** (no segment lists) | Yes | Yes | Yes | Yes | +| **Audience segment lists** (`standard_segment_ids`, `contextual_segment_ids`) | — | Yes | Yes | Yes | +| **Agentic capabilities** (signal types, dim range, consent modes) | — | Yes | Yes | Yes | | **Negotiation enabled** | — | — | Yes | Yes | | **Volume discounts available** | — | — | — | Yes | @@ -227,12 +281,115 @@ curl http://localhost:8000/packages/pkg-abc12345 \ "weight": 1.0 } ], - "audience_segment_ids": ["3", "4", "5"], + "audience_capabilities": { + "standard_segment_ids": ["3", "4", "5"], + "standard_taxonomy_version": "1.1", + "contextual_segment_ids": ["IAB19-29"], + "contextual_taxonomy_version": "3.1", + "agentic_capabilities": { + "supported_signal_types": ["identity", "contextual"], + "embedding_dim_range": [256, 1024], + "spec_version": "draft-2026-01", + "consent_modes": ["gdpr", "tcfv2"] + } + }, "negotiation_enabled": true, "volume_discounts_available": false } ``` +The public view (`PublicPackageView`) exposes only the *versions* (`standard_taxonomy_version`, `contextual_taxonomy_version`, `agentic_capabilities.spec_version`) so buyers can pre-flight compatibility without seeing your segment lists. Authenticated views expose the full segment lists and signal-type details. + +## Audience-Aware Discovery + +Once a package declares `audience_capabilities`, three buyer-facing surfaces become available for audience-driven discovery and matching. + +### 1. Capability advertisement (`/.well-known/agent.json`) + +The seller's existing agent-card response carries an additional `audience_capabilities` block summarizing what the seller's media kit can fulfill at the seat level (capability snapshot, not segment lists): + +```json +{ + "seller_id": "seller-publisher-x", + "audience_capabilities": { + "schema_version": "1", + "standard_taxonomy_versions": ["1.1"], + "contextual_taxonomy_versions": ["3.0", "3.1"], + "agentic": { "supported": true, "spec_version": "draft-2026-01" }, + "supports_constraints": true, + "supports_extensions": true, + "supports_exclusions": false, + "max_refs_per_role": { "primary": 1, "constraints": 3, "extensions": 2, "exclusions": 0 }, + "taxonomy_lock_hashes": { + "audience": "sha256:9f2c...", + "content": "sha256:7b1e..." + } + } +} +``` + +A seller that omits this block is treated as **legacy** by the buyer: standard segments only, no constraints, no extensions, no exclusions, no agentic. That is the safe default. + +The buyer caches this response for **at most 1 hour** and honors `Cache-Control: max-age` if you set it. Bumping `schema_version` signals a breaking change; older buyers degrade their expectations conservatively. + +### 2. Audience filter on `/packages` + +Buyers can filter the package list by audience type and ID: + +```bash +# All packages that fulfill standard segment "3-7" +curl "http://localhost:8000/packages?audience_type=standard&audience_id=3-7" \ + -H "X-API-Key: buyer-key-123" + +# All packages that fulfill contextual category "IAB1-2" +curl "http://localhost:8000/packages?audience_type=contextual&audience_id=IAB1-2" + +# All packages that support agentic activation +curl "http://localhost:8000/packages?audience_type=agentic" +``` + +The filter matches against the corresponding sub-field of `audience_capabilities`. Multiple filters AND together. `POST /media-kit/search` also folds `audience_capabilities` into its scoring corpus. + +### 3. Agentic match endpoint + +Buyers send an `AudienceRef` of type `agentic` (carrying an embedding URI) and receive a match-confidence score: + +```bash +curl -X POST http://localhost:8000/agentic-audience/match \ + -H "Content-Type: application/vnd.iab.agentic-audiences+json; v=1" \ + -H "X-API-Key: buyer-key-123" \ + -d '{ + "audience_ref": { + "type": "agentic", + "identifier": "emb://buyer-x/campaign-q3-lookalike", + "taxonomy": "iab.agentic-audiences", + "version": "draft-2026-01" + }, + "package_id": "pkg-abc12345" + }' +``` + +Response includes a confidence score (0.0–1.0) and the package's matched signal types. Buyers typically threshold at 0.7 for "strong match." The endpoint also accepts the legacy `application/vnd.ucp.embedding+json; v=1` content type --- both refer to the same payload shape (see [Naming note](#taxonomy-reference)). + +### 4. Structured rejection: `audience_plan_unsupported` + +If a buyer sends a `DealBookingRequest` with an `AudiencePlan` that exceeds your declared capabilities (e.g. extensions when `supports_extensions: false`, or an unknown taxonomy version), reject with a structured error: + +```json +{ + "error": "audience_plan_unsupported", + "unsupported": [ + { "path": "extensions[0]", "reason": "extensions not supported by this seller" }, + { "path": "primary.taxonomy", "reason": "version 3.2 not supported" } + ] +} +``` + +The buyer's orchestrator catches this, applies degradation based on the structured paths, and retries once. If the retry still fails, the seller is marked incompatible for the campaign and the orchestrator routes elsewhere. + +!!! tip "End-to-end flow" + Capability advertisement + buyer-side pre-flight degradation + structured rejection together form a three-layer capability negotiation contract. See `docs/architecture/capability-negotiation.md` in the agent_range parent repo for the full negotiation flow, cache semantics, and graceful-degradation policy options. + ## Managing Your Media Kit ### Recommended Workflow @@ -294,12 +451,17 @@ All taxonomy fields use IAB standard identifiers as canonical values: |-------|----------|----------| | `cat` | IAB Content Taxonomy v2/v3 | `IAB1` (Arts), `IAB19` (Sports), `IAB19-29` (Football) | | `cattax` | Taxonomy version | `1` = CT1.0, `2` = CT2.0, `3` = CT3.0 | -| `audience_segment_ids` | IAB Audience Taxonomy 1.1 | Numeric IDs (`"3"`, `"4"`, `"5"`) | +| `audience_capabilities.standard_segment_ids` | IAB Audience Taxonomy 1.1 | Numeric IDs (`"3"`, `"4"`, `"5"`) | +| `audience_capabilities.contextual_segment_ids` | IAB Content Taxonomy 3.1 | Hierarchical IDs (`"IAB1-2"`, `"IAB19-29"`) | +| `audience_capabilities.agentic_capabilities` | IAB Agentic Audiences (DRAFT 2026-01) | Signal types + embedding dim range | | `device_types` | AdCOM DeviceType | `1`=Mobile, `2`=PC, `3`=CTV, `4`=Phone, `5`=Tablet, `6`=Connected, `7`=STB | | `ad_formats` | OpenRTB | `"banner"`, `"video"`, `"native"`, `"audio"` | | `geo_targets` | ISO 3166-2 | `"US"`, `"US-NY"`, `"US-CA"` | | `currency` | ISO 4217 | `"USD"`, `"EUR"`, `"GBP"` | +!!! info "Naming: Agentic Audiences (formerly UCP)" + The IAB renamed *User Context Protocol (UCP)* to *Agentic Audiences* in early 2026. The seller surface uses the new name; the buyer's wire content-type still emits the legacy `application/vnd.ucp.embedding+json; v=1` and accepts the alias `application/vnd.iab.agentic-audiences+json; v=1`. Both refer to the same payload shape. + ## Next Steps - [Pricing & Access Tiers](pricing-rules.md) — Configure tier discounts and negotiation rules diff --git a/docs/guides/troubleshooting.md b/docs/guides/troubleshooting.md index 772fcef..29cb78f 100644 --- a/docs/guides/troubleshooting.md +++ b/docs/guides/troubleshooting.md @@ -210,7 +210,7 @@ The seller agent's MCP endpoint uses Server-Sent Events. Connections must stay o **Symptom**: ChatGPT says "Could not connect to seller-agent" when a tool is called. -1. Verify the MCP URL is publicly reachable: `curl https://your-publisher.example.com/mcp` (Streamable HTTP) or `/mcp-sse/sse` (legacy SSE). +1. Verify the MCP URL is publicly reachable: `curl https://your-publisher.example.com/mcp/mcp` (Streamable HTTP) or `https://your-publisher.example.com/mcp-sse/sse` (legacy SSE). 2. ChatGPT requires a valid TLS certificate. Self-signed certs are rejected. 3. Confirm your API key is entered correctly in the connector settings (no leading/trailing spaces). 4. Check that `API_KEY_AUTH_ENABLED=true` and the key hasn't expired. diff --git a/docs/index.md b/docs/index.md index 745f4c7..1997659 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,7 +10,7 @@ Part of the IAB Tech Lab Agent Ecosystem --- see also the [Buyer Agent](https:// | Protocol | Endpoint | Best For | |----------|----------|----------| -| **[MCP](api/mcp.md)** | `/mcp` (Streamable HTTP), `/mcp-sse/sse` (legacy) | Primary interface — 41 tools for Claude, ChatGPT, Codex, Cursor, and buyer agents | +| **[MCP](api/mcp.md)** | `/mcp/mcp` (Streamable HTTP), `/mcp-sse/sse` (legacy) | Primary interface — 41 tools for Claude, ChatGPT, Codex, Cursor, and buyer agents | | **[A2A](api/a2a.md)** | `/a2a/seller/jsonrpc` | Conversational agent interactions — natural language, multi-turn | | **[REST API](api/overview.md)** | `/api/v1/*` | Programmatic access — 82 endpoints across 15 groups | diff --git a/src/ad_seller/engines/media_kit_service.py b/src/ad_seller/engines/media_kit_service.py index 1b8f1be..39e9b8b 100644 --- a/src/ad_seller/engines/media_kit_service.py +++ b/src/ad_seller/engines/media_kit_service.py @@ -10,7 +10,9 @@ import logging import uuid from datetime import datetime -from typing import Any, Optional +from typing import Any, Literal, Optional + +from pydantic import BaseModel, Field from ..models.buyer_identity import AccessTier, BuyerContext from ..models.flow_state import ProductDefinition @@ -21,6 +23,7 @@ PackagePlacement, PackageStatus, PublicPackageView, + _public_summary_from_capabilities, ) from ..storage.base import StorageBackend from .pricing_rules_engine import PricingRulesEngine @@ -28,6 +31,135 @@ logger = logging.getLogger(__name__) +# ============================================================================= +# Audience filter (proposal §5.7 + §6 row 10, bead ar-2wxa) +# ============================================================================= +# +# A typed filter object covering the three audience types described in +# proposal §4. Used by both `GET /packages` (discovery filter) and +# `POST /media-kit/search` (optional scoring restriction). Lives next to the +# service rather than on the wire so the API surface can construct it from +# query params or a request-body sub-object without callers reimplementing +# the matching predicate. + +AudienceType = Literal["standard", "contextual", "agentic"] + + +class AudienceFilter(BaseModel): + """Filter packages by their declared `audience_capabilities`. + + Semantics (per bead ar-2wxa): + + - When `audience_type` AND `audience_id` are both set, a package matches + iff its `audience_capabilities._segment_ids` contains the ID + (after taxonomy-version compatibility check). Agentic IDs are URIs and + are matched literally against any future agentic ref list -- §10 has + no per-segment list for agentic, so agentic+ID falls back to the + "supported" predicate (presence of `agentic_capabilities`). + - When only `audience_type` is set, a package matches iff it declares + ANY capability in that type (non-empty segment list, or non-null + `agentic_capabilities` for type=agentic). + - `taxonomy_version`, when set, requires the package's + `_taxonomy_version` to equal the requested version. When unset, + taxonomy version is not checked (the seller's lock-file version is + authoritative). + + The filter is permissive on the "no params" case: an empty filter matches + every package. Callers should construct one only when at least one of + `audience_type` / `audience_id` is present. + """ + + audience_type: Optional[AudienceType] = Field( + default=None, + description="Audience dimension to filter on: 'standard' | 'contextual' | 'agentic'", + ) + audience_id: Optional[str] = Field( + default=None, + description="Taxonomy ID (standard/contextual) or URI (agentic) to match", + ) + taxonomy_version: Optional[str] = Field( + default=None, + description=( + "Optional taxonomy version constraint. When unset, defaults to the " + "seller's current per `taxonomies.lock.json`." + ), + ) + + model_config = {"populate_by_name": True} + + def is_empty(self) -> bool: + """True when no filter dimension is set (skip filtering entirely).""" + return ( + self.audience_type is None + and self.audience_id is None + and self.taxonomy_version is None + ) + + def matches(self, package: Package) -> bool: + """Return True iff `package.audience_capabilities` satisfies this filter. + + Empty filter matches every package -- callers gate on `is_empty()` to + avoid the no-op pass. + """ + + if self.is_empty(): + return True + + caps = package.audience_capabilities + + # Type-only filter: package must declare ANY capability in that type. + if self.audience_type is None: + # `audience_id` without `audience_type` is ambiguous (which corpus + # do we look in?). Reject by matching nothing -- the API layer + # should validate before getting here, but defense-in-depth. + return False + + if self.audience_type == "standard": + return self._matches_standard(caps) + if self.audience_type == "contextual": + return self._matches_contextual(caps) + if self.audience_type == "agentic": + return self._matches_agentic(caps) + return False + + def _matches_standard(self, caps) -> bool: + if ( + self.taxonomy_version is not None + and caps.standard_taxonomy_version != self.taxonomy_version + ): + return False + if self.audience_id is None: + return bool(caps.standard_segment_ids) + return self.audience_id in caps.standard_segment_ids + + def _matches_contextual(self, caps) -> bool: + if ( + self.taxonomy_version is not None + and caps.contextual_taxonomy_version != self.taxonomy_version + ): + return False + if self.audience_id is None: + return bool(caps.contextual_segment_ids) + return self.audience_id in caps.contextual_segment_ids + + def _matches_agentic(self, caps) -> bool: + # Per ar-2wxa scope: agentic per-segment matching requires §11's + # /agentic-audience/match endpoint. Until then, agentic filtering + # collapses to the "supported" predicate -- the package declares + # agentic capabilities at all. taxonomy_version, when set, gates on + # AgenticCapabilities.spec_version. + if caps.agentic_capabilities is None: + return False + if ( + self.taxonomy_version is not None + and caps.agentic_capabilities.spec_version != self.taxonomy_version + ): + return False + # `audience_id` is accepted for symmetry but does not narrow further + # at this stage -- presence of agentic_capabilities is the gate. + return True + + class MediaKitService: """Service for package management and tier-gated inventory discovery. @@ -59,9 +191,12 @@ async def list_packages_public( self, layer: Optional[PackageLayer] = None, featured_only: bool = False, + audience_filter: Optional[AudienceFilter] = None, ) -> list[PublicPackageView]: """List active packages as public views (price ranges, no placements).""" - packages = await self._load_active_packages(layer=layer) + packages = await self._load_active_packages( + layer=layer, audience_filter=audience_filter + ) if featured_only: packages = [p for p in packages if p.is_featured] return [self._to_public_view(p) for p in packages] @@ -70,9 +205,12 @@ async def list_packages_authenticated( self, buyer_context: BuyerContext, layer: Optional[PackageLayer] = None, + audience_filter: Optional[AudienceFilter] = None, ) -> list[AuthenticatedPackageView]: """List active packages as authenticated views (exact pricing).""" - packages = await self._load_active_packages(layer=layer) + packages = await self._load_active_packages( + layer=layer, audience_filter=audience_filter + ) return [self._to_authenticated_view(p, buyer_context) for p in packages] async def get_package_public(self, package_id: str) -> Optional[PublicPackageView]: @@ -228,13 +366,21 @@ async def search_packages( self, query: str, buyer_context: Optional[BuyerContext] = None, + audience_filter: Optional[AudienceFilter] = None, ) -> list[PublicPackageView | AuthenticatedPackageView]: - """Search packages by keyword query. + """Search packages by keyword query, with audience-aware scoring. Tokenizes query and matches against name, description, tags, - and content category IDs. Featured items get a score boost. + content category IDs, AND audience capability segment IDs (proposal + §5.7 + bead ar-2wxa). Featured items get a score boost. + + When `audience_filter` is provided, scoring is restricted to packages + that match the filter -- non-matching packages drop out of results + regardless of keyword score. This lets buyers narrow a keyword search + to "packages that target IAB Audience 3-7" without rebuilding the + query. """ - packages = await self._load_active_packages() + packages = await self._load_active_packages(audience_filter=audience_filter) if not packages: return [] @@ -265,8 +411,9 @@ async def search_packages( async def _load_active_packages( self, layer: Optional[PackageLayer] = None, + audience_filter: Optional[AudienceFilter] = None, ) -> list[Package]: - """Load all active packages from storage.""" + """Load all active packages from storage, with optional filters.""" raw_list = await self._storage.list_packages() packages = [] for data in raw_list: @@ -275,6 +422,9 @@ async def _load_active_packages( continue if layer and pkg.layer != layer: continue + if audience_filter is not None and not audience_filter.is_empty(): + if not audience_filter.matches(pkg): + continue packages.append(pkg) return packages @@ -301,6 +451,11 @@ def _to_public_view(self, package: Package) -> PublicPackageView: device_types=package.device_types, cat=package.cat, cattax=package.cattax, + # Capability metadata only -- versions + supports flags. Segment + # lists stay behind the authenticated tier per proposal §5.7. + audience_capabilities=_public_summary_from_capabilities( + package.audience_capabilities + ), geo_targets=package.geo_targets, tags=package.tags, price_range=price_range, @@ -339,6 +494,9 @@ def _to_authenticated_view( device_types=package.device_types, cat=package.cat, cattax=package.cattax, + # Authenticated callers see the full typed capability object + # including segment lists. + audience_capabilities=package.audience_capabilities, geo_targets=package.geo_targets, tags=package.tags, price_range=price_range, @@ -348,15 +506,29 @@ def _to_authenticated_view( floor_price=package.floor_price, currency=package.currency, placements=package.placements, - audience_segment_ids=package.audience_segment_ids, negotiation_enabled=tier_config.negotiation_enabled, volume_discounts_available=tier_config.volume_discounts_enabled, ) def _score_package(self, package: Package, tokens: set[str]) -> float: - """Score a package against search tokens.""" + """Score a package against search tokens. + + Corpus includes name, description, tags, content categories, ad + formats, AND audience capability segment IDs (standard + contextual) + per proposal §5.7 + bead ar-2wxa. A query mentioning a known IAB + audience or content segment ID therefore ranks packages that declare + that segment higher than packages that don't. + + Agentic capabilities are NOT in the keyword corpus -- agentic refs + are URIs, not free-text terms, and per-segment matching for agentic + is §11's territory (the /agentic-audience/match endpoint). A query + token like "agentic" matches via `tags`/`description` if the seller + chose to surface that label there. + """ score = 0.0 + caps = package.audience_capabilities + # Searchable text fields searchable = " ".join( [ @@ -365,6 +537,11 @@ def _score_package(self, package: Package, tokens: set[str]) -> float: " ".join(t.lower() for t in package.tags), " ".join(c.lower() for c in package.cat), " ".join(package.ad_formats), + # Audience corpus: include standard + contextual segment IDs. + # Lower-cased so case-insensitive token matching works for IAB + # IDs that contain mixed case (e.g. "IAB1-2"). + " ".join(s.lower() for s in caps.standard_segment_ids), + " ".join(s.lower() for s in caps.contextual_segment_ids), ] ) diff --git a/src/ad_seller/flows/proposal_handling_flow.py b/src/ad_seller/flows/proposal_handling_flow.py index c67e761..2095a14 100644 --- a/src/ad_seller/flows/proposal_handling_flow.py +++ b/src/ad_seller/flows/proposal_handling_flow.py @@ -69,6 +69,10 @@ def __init__(self) -> None: super().__init__() self._settings = get_settings() self._audience_validation: dict = {} # Populated by validate_audience step + # Optional package list (Package objects or dicts) used by + # _aggregate_seller_segments() for hard-reject overlap checks. + # Tests / upstream code inject via attribute; default empty. + self._packages_for_audience_validation: dict | list = {} @start() async def receive_proposal(self) -> None: @@ -115,16 +119,42 @@ async def validate_audience(self) -> None: This step validates whether the proposal's audience targeting can be fulfilled by the product's audience capabilities. + + Per proposal §5.7 layer 3 (bead ar-sn8f): when the proposal carries a + structured `audience_plan`, the static-taxonomy paths (standard / + contextual) are HARD-REJECTED on zero overlap with the seller's + aggregated segment IDs. Agentic match scores remain a SOFT WARN + because the score is opinion (mock-quality in Epic 1). """ if self.state.status == ExecutionStatus.FAILED: return product_id = self.state.proposal_data.get("product_id") product = self.state.products.get(product_id) + + # ---- Hard-reject pass: structured audience_plan vs. seller segments + # Runs whether or not legacy `audience_targeting` is also present. + audience_plan = self.state.proposal_data.get("audience_plan") + if audience_plan: + hard_reject_reason = self._check_audience_plan_hard_rejects( + audience_plan + ) + if hard_reject_reason: + self.state.errors.append(hard_reject_reason) + self.state.status = ExecutionStatus.FAILED + self._audience_validation = { + "validated": False, + "coverage": 0.0, + "gaps": ["audience_plan_no_overlap"], + "similarity_score": None, + "targeting_compatible": False, + } + return + audience_targeting = self.state.proposal_data.get("audience_targeting", {}) if not audience_targeting: - # No audience targeting in proposal - skip validation + # No audience targeting in proposal - skip soft-warn validation. return if not product: @@ -187,6 +217,104 @@ async def validate_audience(self) -> None: "targeting_compatible": True, # Fallback to allow } + def _aggregate_seller_segments(self) -> tuple[set[str], set[str]]: + """Aggregate the seller's standard + contextual segment IDs across packages. + + Walks `self._packages_for_audience_validation` (instance attribute, + injected by tests / upstream callers) and pulls each package's + `audience_capabilities.standard_segment_ids` and + `contextual_segment_ids`. Falls back to an empty set when no packages + are wired in -- callers treat empty as 'seller has nothing in this + dimension' and defer to the existing soft-warn UCP path. + + Per proposal §5.7 layer 3 (bead ar-sn8f). + """ + + std: set[str] = set() + ctx: set[str] = set() + packages = getattr(self, "_packages_for_audience_validation", None) or {} + for pkg in packages.values() if isinstance(packages, dict) else packages: + caps = getattr(pkg, "audience_capabilities", None) + if caps is None and isinstance(pkg, dict): + caps = pkg.get("audience_capabilities") + if caps is None: + continue + std_ids = ( + getattr(caps, "standard_segment_ids", None) + if not isinstance(caps, dict) + else caps.get("standard_segment_ids", []) + ) + ctx_ids = ( + getattr(caps, "contextual_segment_ids", None) + if not isinstance(caps, dict) + else caps.get("contextual_segment_ids", []) + ) + if std_ids: + std.update(std_ids) + if ctx_ids: + ctx.update(ctx_ids) + return std, ctx + + def _check_audience_plan_hard_rejects( + self, audience_plan: dict + ) -> Optional[str]: + """Hard-reject when buyer's standard/contextual refs have zero overlap. + + Returns a human-readable rejection reason when zero overlap exists on + either dimension; returns None when the plan is acceptable (or when + the seller has no packages registered, which falls back to the + existing soft-warn UCP path). + + Per proposal §5.7 layer 3 (bead ar-sn8f). Agentic refs are NOT + checked here -- low agentic match scores remain soft warnings since + the score is opinion (mock-quality in Epic 1). + """ + + std_seller, ctx_seller = self._aggregate_seller_segments() + + # If seller has nothing registered in either dimension we can't + # meaningfully hard-reject -- defer to the soft-warn path. + if not std_seller and not ctx_seller: + return None + + def _collect(role_refs: list, want_type: str) -> set[str]: + ids: set[str] = set() + for ref in role_refs or []: + if isinstance(ref, dict) and ref.get("type") == want_type: + ident = ref.get("identifier") + if ident: + ids.add(ident) + return ids + + # Walk all roles for standard / contextual refs the buyer asked for. + all_refs: list = [] + primary = audience_plan.get("primary") + if isinstance(primary, dict): + all_refs.append(primary) + for role in ("constraints", "extensions", "exclusions"): + extra = audience_plan.get(role) or [] + if isinstance(extra, list): + all_refs.extend(extra) + + std_buyer = _collect(all_refs, "standard") + ctx_buyer = _collect(all_refs, "contextual") + + if std_buyer and not (std_buyer & std_seller): + return ( + "audience_plan rejected: zero overlap between buyer's standard " + f"refs {sorted(std_buyer)} and seller's standard segments " + f"{sorted(std_seller)} (proposal §5.7 layer 3)" + ) + + if ctx_buyer and not (ctx_buyer & ctx_seller): + return ( + "audience_plan rejected: zero overlap between buyer's contextual " + f"refs {sorted(ctx_buyer)} and seller's contextual segments " + f"{sorted(ctx_seller)} (proposal §5.7 layer 3)" + ) + + return None + def _get_product_capabilities( self, product_id: str, diff --git a/src/ad_seller/interfaces/api/main.py b/src/ad_seller/interfaces/api/main.py index 26a9aec..7b58c55 100644 --- a/src/ad_seller/interfaces/api/main.py +++ b/src/ad_seller/interfaces/api/main.py @@ -21,6 +21,25 @@ logger = logging.getLogger(__name__) +# Dedicated logger for booking-time forensic events. Per proposal §5.1 Step 2 +# / §6 row 14b, the seller logs the `audience_plan_id` hash at the moment a +# deal is minted carrying an audience plan. The buyer logs the same hash on +# its side via `ad_buyer.audience.booking`. Matching log entries are the +# forensic anchor for any future dispute about what was actually frozen. +booking_logger = logging.getLogger("ad_seller.audience.booking") + +# Wire-format media types accepted on `audience_plan`-bearing requests per +# proposal §5.6 + wire-format spec §8 (docs/api/audience_plan_wire_format.md). +# FastAPI's default body parsing reads JSON regardless of `Content-Type`, so +# both names parse cleanly without custom dependencies; the constants are +# kept here for documentation, header-echo on responses, and explicit test +# coverage of the dual-name acceptance contract. +_UCP_CONTENT_TYPE = "application/vnd.ucp.embedding+json; v=1" +_AGENTIC_AUDIENCES_CONTENT_TYPE = "application/vnd.iab.agentic-audiences+json; v=1" +_AUDIENCE_PLAN_CONTENT_TYPES = frozenset( + {_UCP_CONTENT_TYPE, _AGENTIC_AUDIENCES_CONTENT_TYPE} +) + app = FastAPI( title="Ad Seller System API", description=( @@ -183,13 +202,28 @@ class DiscoveryRequest(BaseModel): class PackageCreateRequest(BaseModel): - """Request to create a curated package.""" + """Request to create a curated package. + + Accepts the new typed `audience_capabilities` shape (proposal §5.7). + Legacy callers may still send `audience_segment_ids: list[str]` as + flat input -- the field is retained as deprecated and will be folded + into `audience_capabilities.standard_segment_ids` (with implicit + AT 1.1) by `create_package`. + """ name: str description: Optional[str] = None product_ids: list[str] = [] cat: list[str] = [] cattax: int = 2 + # New typed shape. Optional so legacy callers that only send + # audience_segment_ids do not break. When None and audience_segment_ids + # is present, create_package builds the capabilities dict from the + # legacy field. + audience_capabilities: Optional[dict] = None + # Deprecated; retained for backward compat. Folded into + # audience_capabilities.standard_segment_ids when audience_capabilities + # is None. audience_segment_ids: list[str] = [] device_types: list[int] = [] ad_formats: list[str] = [] @@ -208,6 +242,19 @@ class DynamicPackageRequest(BaseModel): product_ids: list[str] +class AudienceFilterModel(BaseModel): + """Optional audience filter sub-object on `POST /media-kit/search`. + + Mirrors the query-param triple on `GET /packages`: type + id + version. + When present, search results are restricted to packages whose + `audience_capabilities` match. See proposal §5.7 + bead ar-2wxa. + """ + + audience_type: Optional[str] = None + audience_id: Optional[str] = None + taxonomy_version: Optional[str] = None + + class MediaKitSearchRequest(BaseModel): """Request to search packages.""" @@ -215,6 +262,7 @@ class MediaKitSearchRequest(BaseModel): buyer_tier: str = "public" agency_id: Optional[str] = None advertiser_id: Optional[str] = None + audience_filter: Optional[AudienceFilterModel] = None class CounterOfferRequest(BaseModel): @@ -248,11 +296,31 @@ class QuoteRequestModel(BaseModel): class DealBookingRequestModel(BaseModel): - """API request model for POST /api/v1/deals.""" + """API request model for POST /api/v1/deals. + + `audience_plan` is optional and follows the wire shape documented at + `docs/api/audience_plan_wire_format.md`. When present the seller + pre-flights it against its own `audience_capabilities` and rejects + with a structured `audience_plan_unsupported` error if any part + cannot be honored (proposal §5.7 layer 3). + """ quote_id: str buyer_identity: Optional[QuoteBuyerIdentityModel] = None notes: Optional[str] = None + audience_plan: Optional[dict] = None + + +class AgenticAudienceMatchRequest(BaseModel): + """API request model for POST /agentic-audience/match (proposal §5.7). + + Accepts a single `AudienceRef` (must be `type=agentic`) and an optional + package_id scope. Returns a deterministic mock-quality match score and + quality bucket. Real model is Epic 2 / E2-2. + """ + + audience_ref: dict + package_id: Optional[str] = None # ============================================================================= @@ -354,6 +422,203 @@ def _get_api_settings(): return get_settings() +# Cached static product catalog. The seller's default catalog is hardcoded +# in ProductSetupFlow.create_default_products() but running the full flow +# per request is expensive (initialize_setup → ensure_seller_organization +# spins up an OpenDirect MCP session that hangs in session.initialize()). +# Read endpoints (`GET /products`, `GET /products/{id}`, `GET /.well-known/agent.json`) +# use this cached static catalog instead. POST endpoints that mutate state +# can keep their flow logic. +_STATIC_PRODUCT_CATALOG: Optional[dict[str, Any]] = None + + +def _get_static_product_catalog() -> dict[str, Any]: + """Return the seller's default product catalog without running the flow. + + Mirrors ProductSetupFlow.create_default_products() but without the + initialize_setup → ensure_seller_organization → OpenDirect MCP chain + that hangs read endpoints. IDs are generated once and cached so that + repeated reads return stable product_ids. + """ + global _STATIC_PRODUCT_CATALOG + if _STATIC_PRODUCT_CATALOG is not None: + return _STATIC_PRODUCT_CATALOG + + from ...models.core import DealType, PricingModel + from ...models.flow_state import ProductDefinition + + # Same default product list as ProductSetupFlow.create_default_products(). + # Keeping the data here avoids importing the flow (which pulls CrewAI + # plus the OpenDirect client chain). + default_products = [ + { + "name": "Premium Display - Homepage", + "description": "High-impact display on homepage", + "inventory_type": "display", + "base_cpm": 15.0, + "floor_cpm": 10.0, + "supported_deal_types": [ + DealType.PROGRAMMATIC_GUARANTEED, + DealType.PREFERRED_DEAL, + ], + "supported_pricing_models": [PricingModel.CPM], + }, + { + "name": "Standard Display - ROS", + "description": "Run of site display inventory", + "inventory_type": "display", + "base_cpm": 8.0, + "floor_cpm": 5.0, + "supported_deal_types": [DealType.PREFERRED_DEAL, DealType.PRIVATE_AUCTION], + "supported_pricing_models": [PricingModel.CPM], + }, + { + "name": "Pre-Roll Video", + "description": "In-stream pre-roll video ads", + "inventory_type": "video", + "base_cpm": 25.0, + "floor_cpm": 18.0, + "supported_deal_types": [ + DealType.PROGRAMMATIC_GUARANTEED, + DealType.PREFERRED_DEAL, + ], + "supported_pricing_models": [PricingModel.CPM, PricingModel.CPCV], + }, + { + "name": "CTV Premium Streaming", + "description": "Connected TV inventory on premium streaming apps", + "inventory_type": "ctv", + "base_cpm": 35.0, + "floor_cpm": 28.0, + "supported_deal_types": [DealType.PROGRAMMATIC_GUARANTEED], + "supported_pricing_models": [PricingModel.CPM], + }, + { + "name": "Mobile App Rewarded Video", + "description": "User-initiated rewarded video in mobile apps", + "inventory_type": "mobile_app", + "base_cpm": 20.0, + "floor_cpm": 15.0, + "supported_deal_types": [DealType.PREFERRED_DEAL, DealType.PRIVATE_AUCTION], + "supported_pricing_models": [PricingModel.CPM, PricingModel.CPCV], + }, + { + "name": "Native In-Feed", + "description": "Native ads in content feeds", + "inventory_type": "native", + "base_cpm": 12.0, + "floor_cpm": 8.0, + "supported_deal_types": [DealType.PREFERRED_DEAL], + "supported_pricing_models": [PricingModel.CPM, PricingModel.CPC], + }, + # Linear TV — Direct seller (NBCU) + { + "name": "NBC Primetime :30", + "description": "NBC broadcast primetime 30-second national spot", + "inventory_type": "linear_tv", + "base_cpm": 55.0, + "floor_cpm": 40.0, + "supported_deal_types": [DealType.PROGRAMMATIC_GUARANTEED], + "supported_pricing_models": [PricingModel.CPM], + }, + { + "name": "NBCU Cable Network :30 (Bravo/USA)", + "description": "NBCU cable network 30-second spot across Bravo, USA, CNBC", + "inventory_type": "linear_tv", + "base_cpm": 22.0, + "floor_cpm": 15.0, + "supported_deal_types": [ + DealType.PROGRAMMATIC_GUARANTEED, + DealType.PREFERRED_DEAL, + ], + "supported_pricing_models": [PricingModel.CPM], + }, + { + "name": "Telemundo Primetime :30", + "description": "Telemundo Spanish-language primetime 30-second spot", + "inventory_type": "linear_tv", + "base_cpm": 18.0, + "floor_cpm": 12.0, + "supported_deal_types": [ + DealType.PROGRAMMATIC_GUARANTEED, + DealType.PREFERRED_DEAL, + DealType.PRIVATE_AUCTION, + ], + "supported_pricing_models": [PricingModel.CPM], + }, + # Linear TV — MVPD operator (Comcast/Spectrum) + { + "name": "Comcast Local Avails — Top 10 DMAs", + "description": "Comcast Xfinity local cable insertion avails in top 10 markets", + "inventory_type": "linear_tv", + "base_cpm": 15.0, + "floor_cpm": 8.0, + "supported_deal_types": [DealType.PREFERRED_DEAL, DealType.PRIVATE_AUCTION], + "supported_pricing_models": [PricingModel.CPM], + }, + { + "name": "Comcast Addressable Linear — National", + "description": "Comcast addressable linear TV with household-level targeting", + "inventory_type": "linear_tv", + "base_cpm": 55.0, + "floor_cpm": 40.0, + "supported_deal_types": [ + DealType.PROGRAMMATIC_GUARANTEED, + DealType.PRIVATE_AUCTION, + ], + "supported_pricing_models": [PricingModel.CPM], + }, + # Linear TV — Reseller/SSP (PubMatic/Magnite) + { + "name": "Programmatic Linear Reach — A25-54 Primetime", + "description": "Aggregated primetime linear reach across multiple networks via SSP", + "inventory_type": "linear_tv", + "base_cpm": 30.0, + "floor_cpm": 20.0, + "supported_deal_types": [ + DealType.PROGRAMMATIC_GUARANTEED, + DealType.PRIVATE_AUCTION, + ], + "supported_pricing_models": [PricingModel.CPM], + }, + ] + + products: dict[str, ProductDefinition] = {} + for cfg in default_products: + product_def = ProductDefinition( + product_id=f"prod-{uuid.uuid4().hex[:8]}", + name=cfg["name"], + description=cfg.get("description"), + inventory_type=cfg["inventory_type"], + supported_deal_types=cfg["supported_deal_types"], + supported_pricing_models=cfg["supported_pricing_models"], + base_cpm=cfg["base_cpm"], + floor_cpm=cfg["floor_cpm"], + ) + products[product_def.product_id] = product_def + + inventory_types = sorted({p.inventory_type for p in products.values()}) + + _STATIC_PRODUCT_CATALOG = { + "products": products, + "inventory_types": inventory_types, + } + return _STATIC_PRODUCT_CATALOG + + +def _serialize_product(product: Any) -> dict[str, Any]: + """Serialize a ProductDefinition to the public JSON shape.""" + return { + "product_id": product.product_id, + "name": product.name, + "description": product.description, + "inventory_type": product.inventory_type, + "base_cpm": product.base_cpm, + "floor_cpm": product.floor_cpm, + "deal_types": [dt.value for dt in product.supported_deal_types], + } + + async def _resolve_and_enforce_agent( agent_url: Optional[str], ) -> tuple[Optional[Any], Optional[Any]]: @@ -400,50 +665,30 @@ async def health(): @app.get("/products", tags=["Products"]) async def list_products(): - """List all products in the catalog.""" - from ...flows import ProductSetupFlow - - flow = ProductSetupFlow() - await flow.kickoff_async() - - products = [] - for product in flow.state.products.values(): - products.append( - { - "product_id": product.product_id, - "name": product.name, - "description": product.description, - "inventory_type": product.inventory_type, - "base_cpm": product.base_cpm, - "floor_cpm": product.floor_cpm, - "deal_types": [dt.value for dt in product.supported_deal_types], - } - ) + """List all products in the catalog. - return {"products": products} + Reads from the cached static catalog (see `_get_static_product_catalog`) + instead of running ProductSetupFlow per request — kicking off the flow + spins up an OpenDirect MCP session that hangs in `session.initialize()`. + """ + catalog = _get_static_product_catalog() + return { + "products": [_serialize_product(p) for p in catalog["products"].values()], + } @app.get("/products/{product_id}", tags=["Products"]) async def get_product(product_id: str): - """Get a specific product.""" - from ...flows import ProductSetupFlow - - flow = ProductSetupFlow() - await flow.kickoff_async() + """Get a specific product. - product = flow.state.products.get(product_id) + Reads from the cached static catalog instead of running ProductSetupFlow + per request (see `list_products` for rationale). + """ + catalog = _get_static_product_catalog() + product = catalog["products"].get(product_id) if not product: raise HTTPException(status_code=404, detail="Product not found") - - return { - "product_id": product.product_id, - "name": product.name, - "description": product.description, - "inventory_type": product.inventory_type, - "base_cpm": product.base_cpm, - "floor_cpm": product.floor_cpm, - "deal_types": [dt.value for dt in product.supported_deal_types], - } + return _serialize_product(product) @app.post("/pricing", response_model=PricingResponse, tags=["Pricing"]) @@ -1184,6 +1429,60 @@ async def _get_media_kit_service(): return MediaKitService(storage, pricing) +_VALID_AUDIENCE_TYPES = {"standard", "contextual", "agentic"} + + +def _build_audience_filter( + audience_type: Optional[str], + audience_id: Optional[str], + audience_taxonomy_version: Optional[str], +): + """Convert raw query params to an `AudienceFilter`, or None if all unset. + + Validates `audience_type` and the type/id pairing rules: + + - Returns None when all three params are unset (skip filtering). + - 400 when `audience_type` is set but unrecognized. + - 400 when `audience_id` is set without `audience_type` (no corpus to + search in). + + Per bead ar-2wxa scope: agentic per-segment filtering is §11's + territory; agentic+id collapses to "package supports agentic" at this + stage and the filter accepts the param without error so existing buyer + code doesn't have to special-case the type. + """ + + from ...engines.media_kit_service import AudienceFilter + + if ( + audience_type is None + and audience_id is None + and audience_taxonomy_version is None + ): + return None + + if audience_type is not None and audience_type not in _VALID_AUDIENCE_TYPES: + raise HTTPException( + status_code=400, + detail=( + f"Invalid audience_type: {audience_type!r}. Must be one of " + f"{sorted(_VALID_AUDIENCE_TYPES)}." + ), + ) + + if audience_id is not None and audience_type is None: + raise HTTPException( + status_code=400, + detail="audience_id requires audience_type to disambiguate corpus.", + ) + + return AudienceFilter( + audience_type=audience_type, + audience_id=audience_id, + taxonomy_version=audience_taxonomy_version, + ) + + # ============================================================================= # Media Kit Endpoints (Public — no auth required) # ============================================================================= @@ -1208,8 +1507,15 @@ async def media_kit_overview(): async def list_media_kit_packages( layer: Optional[str] = None, featured_only: bool = False, + audience_type: Optional[str] = None, + audience_id: Optional[str] = None, + audience_taxonomy_version: Optional[str] = None, ): - """List packages with public view (price ranges, no exact pricing).""" + """List packages with public view (price ranges, no exact pricing). + + Accepts the same audience-filter triple as `GET /packages` so public + discovery callers can narrow by audience type without authenticating. + """ from ...models.media_kit import PackageLayer pkg_layer = None @@ -1219,8 +1525,16 @@ async def list_media_kit_packages( except ValueError: raise HTTPException(status_code=400, detail=f"Invalid layer: {layer}") + audience_filter = _build_audience_filter( + audience_type, audience_id, audience_taxonomy_version + ) + service = await _get_media_kit_service() - packages = await service.list_packages_public(layer=pkg_layer, featured_only=featured_only) + packages = await service.list_packages_public( + layer=pkg_layer, + featured_only=featured_only, + audience_filter=audience_filter, + ) return {"packages": [p.model_dump() for p in packages]} @@ -1239,7 +1553,17 @@ async def search_media_kit( request: MediaKitSearchRequest, api_key_record=Depends(_get_optional_api_key_record), ): - """Search packages by keyword. Authenticated buyers get richer results.""" + """Search packages by keyword. Authenticated buyers get richer results. + + Per proposal §5.7 + bead ar-2wxa, the scoring corpus now includes + `audience_capabilities.standard_segment_ids` + + `audience_capabilities.contextual_segment_ids` alongside keywords/tags + -- a query mentioning a known IAB segment ID ranks packages that + declare it higher than packages that don't. + + The optional `audience_filter` body field restricts results to packages + that match its type/id/version triple, parallel to `GET /packages`. + """ context = None if api_key_record is not None or request.buyer_tier != "public": context = _build_buyer_context( @@ -1249,8 +1573,18 @@ async def search_media_kit( api_key_record=api_key_record, ) + audience_filter = None + if request.audience_filter is not None: + audience_filter = _build_audience_filter( + request.audience_filter.audience_type, + request.audience_filter.audience_id, + request.audience_filter.taxonomy_version, + ) + service = await _get_media_kit_service() - results = await service.search_packages(request.query, buyer_context=context) + results = await service.search_packages( + request.query, buyer_context=context, audience_filter=audience_filter + ) return {"results": [r.model_dump() for r in results]} @@ -1265,9 +1599,25 @@ async def list_packages( agency_id: Optional[str] = None, advertiser_id: Optional[str] = None, layer: Optional[str] = None, + audience_type: Optional[str] = None, + audience_id: Optional[str] = None, + audience_taxonomy_version: Optional[str] = None, api_key_record=Depends(_get_optional_api_key_record), ): - """List packages with tier-gated view.""" + """List packages with tier-gated view. + + Audience filter (proposal §5.7 + bead ar-2wxa): + + - `audience_type`: one of `standard` | `contextual` | `agentic`. + - `audience_id`: taxonomy ID for standard/contextual; URI for agentic. + Requires `audience_type` to disambiguate which capability list to + search. + - `audience_taxonomy_version`: optional version constraint; when unset + the seller's lock-file version is authoritative. + + Empty results return `[]`, not 404 -- matches the existing behavior for + layer/featured filters. + """ from ...models.media_kit import PackageLayer pkg_layer = None @@ -1277,10 +1627,16 @@ async def list_packages( except ValueError: raise HTTPException(status_code=400, detail=f"Invalid layer: {layer}") + audience_filter = _build_audience_filter( + audience_type, audience_id, audience_taxonomy_version + ) + service = await _get_media_kit_service() if api_key_record is None and buyer_tier == "public": - packages = await service.list_packages_public(layer=pkg_layer) + packages = await service.list_packages_public( + layer=pkg_layer, audience_filter=audience_filter + ) else: context = _build_buyer_context( buyer_tier=buyer_tier, @@ -1288,7 +1644,9 @@ async def list_packages( advertiser_id=advertiser_id, api_key_record=api_key_record, ) - packages = await service.list_packages_authenticated(context, layer=pkg_layer) + packages = await service.list_packages_authenticated( + context, layer=pkg_layer, audience_filter=audience_filter + ) return {"packages": [p.model_dump() for p in packages]} @@ -1349,25 +1707,35 @@ async def create_package(request: PackageCreateRequest): ) ) - package = Package( - package_id=f"pkg-{_uuid.uuid4().hex[:8]}", - name=request.name, - description=request.description, - layer=PackageLayer.CURATED, - status=PackageStatus.ACTIVE, - placements=placements, - cat=request.cat, - cattax=request.cattax, - audience_segment_ids=request.audience_segment_ids, - device_types=request.device_types, - ad_formats=request.ad_formats, - geo_targets=request.geo_targets, - base_price=request.base_price, - floor_price=request.floor_price, - tags=request.tags, - is_featured=request.is_featured, - seasonal_label=request.seasonal_label, - ) + # Build kwargs for Package -- prefer the new typed audience_capabilities + # when supplied; otherwise pass the legacy audience_segment_ids and let + # the Package's model_validator(mode='before') shim migrate it. + package_kwargs: dict[str, Any] = { + "package_id": f"pkg-{_uuid.uuid4().hex[:8]}", + "name": request.name, + "description": request.description, + "layer": PackageLayer.CURATED, + "status": PackageStatus.ACTIVE, + "placements": placements, + "cat": request.cat, + "cattax": request.cattax, + "device_types": request.device_types, + "ad_formats": request.ad_formats, + "geo_targets": request.geo_targets, + "base_price": request.base_price, + "floor_price": request.floor_price, + "tags": request.tags, + "is_featured": request.is_featured, + "seasonal_label": request.seasonal_label, + } + if request.audience_capabilities is not None: + package_kwargs["audience_capabilities"] = request.audience_capabilities + elif request.audience_segment_ids: + # Legacy path: forward the flat list, shim will fold it into + # audience_capabilities at validation time. + package_kwargs["audience_segment_ids"] = request.audience_segment_ids + + package = Package(**package_kwargs) service = await _get_media_kit_service() created = await service.create_package(package) @@ -1575,7 +1943,6 @@ async def agent_card(): seller's capabilities, supported protocols, and inventory types. Buyer agents and registries fetch this to discover the seller. """ - from ...flows import ProductSetupFlow from ...models.agent_registry import ( AgentAuthentication, AgentCapabilities, @@ -1583,16 +1950,15 @@ async def agent_card(): AgentProvider, AgentSkill, ) + from ...models.audience_capabilities import build_capability_audience_block settings = _get_api_settings() - # Discover inventory types from product catalog - inventory_types = set() + # Read inventory types from the cached static catalog rather than running + # ProductSetupFlow per request (which hangs in OpenDirect MCP + # session.initialize() — see `_get_static_product_catalog` for context). try: - flow = ProductSetupFlow() - await flow.kickoff_async() - for product in flow.state.products.values(): - inventory_types.add(product.inventory_type) + inventory_types = set(_get_static_product_catalog()["inventory_types"]) except Exception: inventory_types = {"display", "video", "ctv", "native", "mobile_app"} @@ -1651,6 +2017,13 @@ async def agent_card(): ), inventory_types=sorted(inventory_types), supported_deal_types=["pg", "pmp", "preferred_deal", "private_auction"], + # Audience capability advertisement (proposal §5.7 layer 1). Demo / + # MVP defaults: agentic match endpoint not yet shipped (lands in §11), + # constraints filter not yet shipped (lands in §10) but we advertise + # support so buyers test the negotiation path. Lock-file hashes are + # loaded dynamically from data/taxonomies/taxonomies.lock.json so the + # block stays in sync if the lock file is regenerated. + audience_capabilities=build_capability_audience_block(), ) return card.model_dump() @@ -1799,7 +2172,6 @@ async def create_quote( from datetime import timedelta from ...engines.pricing_rules_engine import PricingRulesEngine - from ...flows import ProductSetupFlow from ...models.core import DealType from ...models.pricing_tiers import TieredPricingConfig from ...models.quotes import ( @@ -1838,11 +2210,11 @@ async def create_quote( }, ) - # Get product catalog - setup_flow = ProductSetupFlow() - await setup_flow.kickoff_async() - - product = setup_flow.state.products.get(request.product_id) + # Read product from cached static catalog rather than running + # ProductSetupFlow per request (hangs in OpenDirect MCP + # session.initialize() — see ar-uwad / `_get_static_product_catalog`). + catalog = _get_static_product_catalog() + product = catalog["products"].get(request.product_id) if not product: raise HTTPException( status_code=404, @@ -1998,6 +2370,29 @@ async def book_deal( The seller validates the quote, generates a Deal ID, and returns confirmed terms. This is the commit point — the quote becomes bound. + + **Wire format (proposal §5.6 + §6 row 14b):** the seller accepts both + audience-plan content types -- + ``application/vnd.ucp.embedding+json; v=1`` (legacy UCP carrier) and + ``application/vnd.iab.agentic-audiences+json; v=1`` (new IAB Agentic + Audiences alias). FastAPI's body parsing is content-type-permissive, so + both names round-trip the same Pydantic model with no custom dependency + needed; the dual acceptance is exercised by + ``tests/unit/test_deal_booking_snapshot.py``. + + **Snapshot (proposal §5.1 Step 2 + wire-format §6.5):** when the request + carries an ``audience_plan``, the seller persists it verbatim as + ``audience_plan_snapshot`` against the deal record and returns the + snapshot plus a per-role ``audience_match_summary`` so the buyer can + verify the booking. The snapshot is authoritative for the lifetime of + the deal -- if seller capabilities change mid-flight, the snapshot is + honored (see ``services/fulfillment.honor_audience_plan_snapshot``). + + **Forensic logging (proposal §5.1 Step 2):** the + ``audience_plan_id`` hash is logged at INFO via + ``ad_seller.audience.booking``. The buyer logs the same hash on its + side; matching entries are the cross-system anchor for dispute + resolution. """ import uuid from datetime import timedelta @@ -2042,6 +2437,25 @@ async def book_deal( }, ) + # Pre-flight: if the buyer sent an audience_plan with this booking, validate + # it against the seller's capability block. Per proposal §5.7 layer 3, any + # unsupported part triggers a structured `audience_plan_unsupported` 400 so + # the buyer's degrade_plan_for_seller() can retry. (Bead ar-sn8f.) + if request.audience_plan: + from ...models.audience_capabilities import build_capability_audience_block + from ...services.audience_plan_validator import validate_audience_plan + + seller_caps = build_capability_audience_block() + unsupported = validate_audience_plan(request.audience_plan, seller_caps) + if unsupported: + raise HTTPException( + status_code=400, + detail={ + "error": "audience_plan_unsupported", + "unsupported": unsupported, + }, + ) + # Generate deal now = datetime.utcnow() deal_id = f"DEMO-{uuid.uuid4().hex[:12].upper()}" @@ -2076,17 +2490,212 @@ async def book_deal( deal_data = deal.model_dump(mode="json") + # Freeze the audience plan onto the deal record + compute per-role match + # summary (proposal §5.1 Step 2 + wire-format §6.5). Both fields are added + # only when the buyer supplied an audience_plan; legacy bookings remain + # byte-for-byte identical. + if request.audience_plan: + plan_snapshot = dict(request.audience_plan) + deal_data["audience_plan_snapshot"] = plan_snapshot + deal_data["audience_match_summary"] = _build_audience_match_summary( + plan_snapshot + ) + + # Forensic anchor hash log (proposal §5.1 Step 2 / bead 14b). Buyer + # logs the same hash on its side via `ad_buyer.audience.booking`. + plan_id = plan_snapshot.get("audience_plan_id") or "" + booking_logger.info( + "deal_booking deal_id=%s audience_plan_id=%s quote_id=%s", + deal_id, + plan_id, + request.quote_id, + ) + # Update quote status to "booked" and link deal_id quote["status"] = QuoteStatus.BOOKED.value quote["deal_id"] = deal_id await storage.set_quote(request.quote_id, quote, ttl=86400) - # Store the deal in deal storage (coexists with proposal-based deals) + # Store the deal in deal storage (coexists with proposal-based deals). + # The snapshot fields land on the persisted record so + # `honor_audience_plan_snapshot()` can read them at fulfillment time. await storage.set_deal(deal_id, deal_data) return deal_data +# ============================================================================= +# Agentic Audience Match (proposal §5.7 + §6 row 11) +# ============================================================================= + + +def _agentic_match_quality(score: float) -> str: + """Bucket a [0, 1] match score into the spec's quality labels.""" + + if score >= 0.85: + return "STRONG" + if score >= 0.65: + return "MODERATE" + if score >= 0.4: + return "WEAK" + return "POOR" + + +def _deterministic_score(identifier: str) -> float: + """sha256-derived deterministic [0, 1] mock score. + + Mock-quality is fine here -- per proposal §7, the SHA256-seeded mock is + explicitly the load-bearing fake under every "agentic match score" we + display in Epic 1; the real model is Epic 2 / E2-2. + """ + + import hashlib + + digest = hashlib.sha256(identifier.encode("utf-8")).hexdigest() + # First 8 hex chars -> 32-bit unsigned int -> normalized to [0, 1]. + return int(digest[:8], 16) / 0xFFFFFFFF + + +# Wire-format §6.5 match-bucket labels. Note these differ from the +# `_agentic_match_quality` labels used by `/agentic-audience/match` +# (which uses POOR for the lowest bucket); the booking response uses the +# `BookingResponse.MatchEntry` enum from the wire-format spec, which has +# `NONE` instead of `POOR`. Keeping the two scales separate avoids breaking +# the §11 endpoint contract while satisfying §6.5 on the booking surface. +def _booking_match_label(score: float) -> str: + """Wire-format §6.5 match-bucket label for a [0, 1] score.""" + + if score >= 0.85: + return "STRONG" + if score >= 0.65: + return "MODERATE" + if score >= 0.4: + return "WEAK" + return "NONE" + + +def _score_for_ref(ref: dict[str, Any]) -> float: + """Deterministic mock match score for a single ref. + + Standard / contextual refs score against their `identifier`; agentic + refs score against their embedding URI. Score range [0, 1]. Real + similarity scoring is Epic 2 / E2-2; for Epic 1 the deterministic mock + matches the rest of the seller's scoring surface. + """ + + identifier = ref.get("identifier") or "" + return _deterministic_score(identifier) + + +def _match_entry_for_ref(ref: dict[str, Any]) -> dict[str, Any]: + """Build one wire-format §6.5 `MatchEntry` for a single ref.""" + + score = _score_for_ref(ref) + return {"match": _booking_match_label(score), "score": round(score, 4)} + + +def _build_audience_match_summary(plan: dict[str, Any]) -> dict[str, Any]: + """Assemble the wire-format §6.5 `audience_match_summary` for a plan. + + Returns the four-role shape (`primary`, `constraints`, `extensions`, + `exclusions`) -- per the schema, empty arrays MAY be omitted but + receivers MUST treat absence as empty, so we always emit them so the + buyer's typed parser has stable structure. + """ + + summary: dict[str, Any] = { + "primary": _match_entry_for_ref(plan.get("primary") or {}), + "constraints": [ + _match_entry_for_ref(r) for r in (plan.get("constraints") or []) + ], + "extensions": [ + _match_entry_for_ref(r) for r in (plan.get("extensions") or []) + ], + "exclusions": [ + _match_entry_for_ref(r) for r in (plan.get("exclusions") or []) + ], + } + return summary + + +@app.post("/agentic-audience/match", tags=["Audience"]) +async def agentic_audience_match(request: AgenticAudienceMatchRequest): + """Match a buyer-supplied agentic `AudienceRef` against this seller. + + Per proposal §5.7 + §6 row 11. Returns a match score and quality bucket. + The score is mock-quality (deterministic from sha256 of `identifier`); + the real embedding-similarity model is Epic 2 (E2-2). + + Behavior: + - Non-agentic refs return HTTP 400. + - Sellers with no top-level agentic capability (legacy / agentic + decommissioned) return `agentic_supported_by_seller=False`, + `match_quality="POOR"`, score 0. + - Otherwise the score is deterministic per `identifier` and bucketed + into `STRONG | MODERATE | WEAK | POOR`. + """ + + from ...models.audience_capabilities import build_capability_audience_block + + ref = request.audience_ref or {} + if ref.get("type") != "agentic": + raise HTTPException( + status_code=400, + detail={ + "error": "invalid_audience_ref", + "message": "POST /agentic-audience/match requires audience_ref.type='agentic'", + }, + ) + + identifier = ref.get("identifier") or "" + if not identifier: + raise HTTPException( + status_code=400, + detail={ + "error": "invalid_audience_ref", + "message": "audience_ref.identifier is required", + }, + ) + + seller_caps = build_capability_audience_block() + agentic_supported = bool(seller_caps.agentic.supported) + + if not agentic_supported: + return { + "audience_ref": ref, + "match_confidence": 0.0, + "match_quality": "POOR", + "matched_capabilities": [], + "agentic_supported_by_seller": False, + "rationale": ( + "Seller does not advertise top-level agentic capability " + "(audience_capabilities.agentic.supported=false); returning POOR." + ), + } + + score = _deterministic_score(identifier) + quality = _agentic_match_quality(score) + + # `matched_capabilities` is a placeholder for the real model's + # per-signal-type breakdown (E2-2). For now we mirror the seller's + # advertised top-level agentic flag as a single capability label. + matched: list[str] = [] + if quality != "POOR": + matched.append("agentic") + + return { + "audience_ref": ref, + "match_confidence": round(score, 4), + "match_quality": quality, + "matched_capabilities": matched, + "agentic_supported_by_seller": True, + "rationale": ( + f"Deterministic mock score {round(score, 4)} -> {quality}. " + "Real similarity model is tracked in Epic 2 (E2-2)." + ), + } + + @app.get("/api/v1/deals/{deal_id}", tags=["Deal Booking"]) async def get_deal_by_id(deal_id: str): """Get the current status of a deal. diff --git a/src/ad_seller/models/__init__.py b/src/ad_seller/models/__init__.py index f073bdd..087275b 100644 --- a/src/ad_seller/models/__init__.py +++ b/src/ad_seller/models/__init__.py @@ -15,6 +15,20 @@ RegistrySource, TrustStatus, ) +from .audience_capabilities import ( + AgenticCapabilities, + AgenticCapabilityFlag, + AudienceCapabilities, + CapabilityAudienceBlock, + MaxRefsPerRole, + TaxonomyLockHashes, + build_capability_audience_block, + load_taxonomy_lock_hashes, +) +from .audience_ref import ( + AudienceRef, + ComplianceContext, +) from .api_key import ( ApiKeyCreateRequest, ApiKeyCreateResponse, @@ -117,6 +131,7 @@ SupplyPoolEntry, ) from .media_kit import ( + AudienceCapabilityPublicSummary, AuthenticatedPackageView, Package, PackageLayer, @@ -246,6 +261,18 @@ "PackagePlacement", "PublicPackageView", "AuthenticatedPackageView", + "AudienceCapabilityPublicSummary", + # Audience capabilities (proposal §5.7) + "AudienceCapabilities", + "AgenticCapabilities", + "AgenticCapabilityFlag", + "AudienceRef", + "ComplianceContext", + "CapabilityAudienceBlock", + "MaxRefsPerRole", + "TaxonomyLockHashes", + "build_capability_audience_block", + "load_taxonomy_lock_hashes", # Negotiation "NegotiationAction", "NegotiationHistory", diff --git a/src/ad_seller/models/agent_registry.py b/src/ad_seller/models/agent_registry.py index 159a7d6..1509b29 100644 --- a/src/ad_seller/models/agent_registry.py +++ b/src/ad_seller/models/agent_registry.py @@ -24,6 +24,7 @@ from pydantic import BaseModel, Field +from .audience_capabilities import CapabilityAudienceBlock from .buyer_identity import AccessTier # ============================================================================= @@ -82,6 +83,19 @@ class AgentCard(BaseModel): supported_deal_types: list[str] = Field(default_factory=list) contact: Optional[str] = None tos_url: Optional[str] = None + # Per proposal §5.7 layer 1: audience capability advertisement. Optional + # for backward compatibility -- a missing block means "treat as legacy" + # (standard segments only, no constraints/extensions/exclusions, no + # agentic). Buyers pre-flight an AudiencePlan against these flags before + # sending a DealBookingRequest. + audience_capabilities: Optional[CapabilityAudienceBlock] = Field( + default=None, + description=( + "Audience capability discovery block (proposal §5.7 layer 1). " + "Schema version, supports_* flags, max_refs_per_role, and " + "taxonomy_lock_hashes for cross-repo schema-drift detection." + ), + ) # ============================================================================= diff --git a/src/ad_seller/models/audience_capabilities.py b/src/ad_seller/models/audience_capabilities.py new file mode 100644 index 0000000..f139ddc --- /dev/null +++ b/src/ad_seller/models/audience_capabilities.py @@ -0,0 +1,383 @@ +# Author: Green Mountain Systems AI Inc. +# Donated to IAB Tech Lab + +"""Typed `AudienceCapabilities` for seller `Package` positioning. + +Replaces the flat `audience_segment_ids: list[str]` on `Package` with a +typed three-dimension structure parallel to how `cat`/`cattax` already +handles content taxonomy versioning: + +- standard segments (IAB Audience Taxonomy 1.1 IDs, the existing dimension) +- contextual segments (IAB Content Taxonomy 3.1 IDs as audience-intent, + distinct from `cat` which describes the content itself) +- agentic capabilities (declares whether the package can match against + buyer-supplied embeddings, and on what signal types) + +A package optimized for direct response declares dense `standard_segment_ids` +and leaves `agentic_capabilities` null. A package selling content adjacency +declares `contextual_segment_ids`. A package supporting advertiser +first-party activation declares `agentic_capabilities` -- the premium tier. + +See proposal §5.7 and bead ar-roi5. +""" + +from __future__ import annotations + +import json +import threading +from pathlib import Path +from typing import Literal + +from pydantic import BaseModel, Field + +# Signal type discriminator mirrors the IAB Agentic Audiences (DRAFT 2026-01) +# spec's three-axis model. Held here as a Literal rather than an Enum so it +# stays JSON-friendly and matches the seller code style elsewhere. +SignalType = Literal["identity", "contextual", "reinforcement"] + + +class AgenticCapabilities(BaseModel): + """Declares a package's agentic-audience matching capabilities. + + A null `Package.audience_capabilities.agentic_capabilities` means the + seller does not support agentic matching for this package. A populated + object declares which signal types, embedding dimensions, and consent + modes the seller can match on. + + Defaults are conservative: empty signal types, the spec's documented + embedding-dim range, the current draft spec version. + """ + + supported_signal_types: list[SignalType] = Field( + default_factory=list, + description="Signal types this package can match on: 'identity', 'contextual', 'reinforcement'", + ) + embedding_dim_range: tuple[int, int] = Field( + default=(256, 1024), + description="Inclusive (min, max) embedding dimensions accepted", + ) + spec_version: str = Field( + default="draft-2026-01", + description="IAB Agentic Audiences spec version; bumped when ratified", + ) + consent_modes: list[str] = Field( + default_factory=list, + description="Accepted consent frameworks, e.g. ['IAB-TCFv2', 'GPP', 'advertiser-1p']", + ) + + model_config = {"populate_by_name": True} + + @classmethod + def modern_default(cls) -> "AgenticCapabilities": + """Build a 'modern' agentic-capable seller declaration. + + Per E2-2 + E2-6: a seller running the agent_range stack can match + against agentic refs minted by the buyer's locked sentence-transformers + model (384-dim, in [256, 1024]) on all three IAB Agentic Audiences + signal types (identity / contextual / reinforcement). Sellers opt in + by setting their package's `agentic_capabilities = AgenticCapabilities.modern_default()`; + the global default on `Package` stays None for backward compat. + """ + + return cls( + supported_signal_types=["identity", "contextual", "reinforcement"], + embedding_dim_range=(256, 1024), + spec_version="draft-2026-01", + consent_modes=["IAB-TCFv2", "GPP", "advertiser-1p"], + ) + + +class AudienceCapabilities(BaseModel): + """Typed audience-capability declaration for a `Package`. + + Replaces the flat `audience_segment_ids: list[str]` field. Carries: + + - standard_segment_ids + standard_taxonomy_version: IAB Audience + Taxonomy IDs the package can target. Was the legacy + `audience_segment_ids` field; defaults to AT 1.1. + - contextual_segment_ids + contextual_taxonomy_version: IAB Content + Taxonomy IDs interpreted as *audience intent* (what the audience is + reading), distinct from `cat` which describes the content itself. + Defaults to CT 3.1. + - agentic_capabilities: optional declaration that the package can match + against buyer-supplied embeddings. Null = not supported. + + The presence of any of these dimensions is what the seller advertises in + the public capability discovery response (versions + supports flags + only); segment lists are exposed in the authenticated view only. + """ + + standard_segment_ids: list[str] = Field( + default_factory=list, + description="IAB Audience Taxonomy 1.1 segment IDs, e.g. ['3', '4', '5']", + ) + standard_taxonomy_version: str = Field( + default="1.1", + description="IAB Audience Taxonomy version pinned by this package", + ) + contextual_segment_ids: list[str] = Field( + default_factory=list, + description="IAB Content Taxonomy 3.1 IDs as audience intent, e.g. ['IAB1-2']", + ) + contextual_taxonomy_version: str = Field( + default="3.1", + description="IAB Content Taxonomy version pinned by this package", + ) + agentic_capabilities: AgenticCapabilities | None = Field( + default=None, + description="Agentic embedding-match capabilities; null when unsupported", + ) + + model_config = {"populate_by_name": True} + + +# ============================================================================= +# Capability discovery block (proposal §5.7 layer 1) +# ============================================================================= +# +# `CapabilityAudienceBlock` is the seller-level audience capability advertised +# on the agent card / capability discovery response. It is *separate* from +# `AudienceCapabilities` (which is per-Package); this block describes what +# this seller agent as a whole can negotiate over. +# +# Buyers pre-flight an `AudiencePlan` against these flags before sending a +# `DealBookingRequest`. A seller that does not ship this block is treated as +# legacy (standard segments only, no constraints/extensions/exclusions, no +# agentic). See proposal §5.7 layers 1-3. +# +# `taxonomy_lock_hashes` carries the sha256 of the seller's vendored taxonomy +# files, loaded dynamically from `data/taxonomies/taxonomies.lock.json` so +# they stay in sync if the lock file is updated. Mismatch with the buyer's +# own lock-file hashes is the cheap inline drift-detection that keeps Epic 1 +# from needing heavier schema-hardening up front. + + +class AgenticCapabilityFlag(BaseModel): + """Top-level agentic-support flag for the capability discovery block. + + Distinct from per-package `AgenticCapabilities`: this is the seller's + *overall* declaration of whether agentic matching is on the menu at all. + Per-package detail (signal types, embedding dim range, spec version) lives + on each `Package.audience_capabilities.agentic_capabilities`. + """ + + supported: bool = Field( + default=False, + description="True if the seller offers agentic matching on any package", + ) + + model_config = {"populate_by_name": True} + + +class MaxRefsPerRole(BaseModel): + """Per-role reference cardinality limits the seller will accept. + + Mirrors the four `AudiencePlan` roles (primary / constraints / extensions + / exclusions). A seller that doesn't support extensions advertises 0 here; + the buyer's `degrade_plan_for_seller()` (§5.7 layer 2) drops refs in roles + where the seller's max is 0 before sending the plan. + """ + + primary: int = Field(default=1, ge=0, description="Max primary refs (typically 1)") + constraints: int = Field(default=3, ge=0, description="Max constraint refs") + extensions: int = Field(default=0, ge=0, description="Max extension refs") + exclusions: int = Field(default=0, ge=0, description="Max exclusion refs") + + model_config = {"populate_by_name": True} + + +class TaxonomyLockHashes(BaseModel): + """sha256 hashes of the seller's vendored taxonomy files. + + Cheap schema-drift backstop per proposal §5.7. Buyer compares against its + own lock-file hashes; mismatch logs a warning ("seller is on Content + Taxonomy 3.0, you are on 3.1") without blocking the deal. + + Always sourced dynamically from `data/taxonomies/taxonomies.lock.json` -- + never hard-coded -- so it stays in sync if the lock file is regenerated. + """ + + audience: str = Field( + description="sha256 of vendored Audience Taxonomy file, prefixed 'sha256:'", + ) + content: str = Field( + description="sha256 of vendored Content Taxonomy file, prefixed 'sha256:'", + ) + + model_config = {"populate_by_name": True} + + +class CapabilityAudienceBlock(BaseModel): + """`audience_capabilities` block on the seller's capability discovery response. + + Per proposal §5.7 layer 1. Carries: + + - `schema_version`: bumped on breaking changes; buyer degrades expectations + conservatively on unknown future versions. Missing/legacy = v0. + - `standard_taxonomy_versions` / `contextual_taxonomy_versions`: which + taxonomy versions the seller can interpret. Multi-version lists let a + seller advertise "I read both 3.0 and 3.1" during a migration window. + - `agentic.supported`: whether agentic matching is offered at all. + - `supports_constraints` / `supports_extensions` / `supports_exclusions`: + per-role gates so the buyer's `degrade_plan_for_seller()` can drop + unsupported roles before sending the plan. + - `max_refs_per_role`: cardinality caps per role. + - `taxonomy_lock_hashes`: sha256 of vendored taxonomy files (cheap inline + drift backstop). + + A seller that doesn't ship this block is treated as legacy: standard + segments only, no constraints/extensions/exclusions, no agentic. That's + the safe default. + """ + + schema_version: str = Field( + default="1", + description="Capability-block schema version; bumped on breaking changes", + ) + standard_taxonomy_versions: list[str] = Field( + default_factory=lambda: ["1.1"], + description="IAB Audience Taxonomy versions the seller can interpret", + ) + contextual_taxonomy_versions: list[str] = Field( + default_factory=lambda: ["3.1"], + description="IAB Content Taxonomy versions the seller can interpret", + ) + agentic: AgenticCapabilityFlag = Field( + default_factory=AgenticCapabilityFlag, + description="Top-level agentic-support flag; per-package detail lives on Package", + ) + supports_constraints: bool = Field( + default=True, + description="Whether the seller honors AudiencePlan.constraints (intersect)", + ) + supports_extensions: bool = Field( + default=False, + description="Whether the seller honors AudiencePlan.extensions (union)", + ) + supports_exclusions: bool = Field( + default=False, + description="Whether the seller honors AudiencePlan.exclusions (set-difference)", + ) + max_refs_per_role: MaxRefsPerRole = Field( + default_factory=MaxRefsPerRole, + description="Per-role reference cardinality caps", + ) + taxonomy_lock_hashes: TaxonomyLockHashes = Field( + description=( + "sha256 of seller's vendored taxonomy files (loaded dynamically " + "from data/taxonomies/taxonomies.lock.json -- never hard-coded)" + ), + ) + + model_config = {"populate_by_name": True} + + +# ============================================================================= +# Lock-file loader (process-level cache, lock-file-mtime invalidation) +# ============================================================================= + +# Default location of the vendored taxonomy lock file, relative to the seller +# package root. Tests pass an explicit path to avoid the cache. +_DEFAULT_LOCK_PATH = ( + Path(__file__).resolve().parents[3] / "data" / "taxonomies" / "taxonomies.lock.json" +) + +# Process-level cache: (mtime_ns, parsed_lock_dict). The mtime_ns key forces a +# reload when the lock file is rewritten. Wrapped in a lock so concurrent +# capability requests at startup don't double-read. +_lock_cache: dict[Path, tuple[int, dict]] = {} +_lock_cache_mutex = threading.Lock() + + +def _load_taxonomy_lock(path: Path | None = None) -> dict: + """Read the vendored taxonomy lock file, with mtime-keyed caching. + + Cache is keyed by absolute path AND mtime_ns, so a regenerated lock file + on disk (different mtime) forces a reload on next call. Tests can pass + an explicit path to bypass the cache for the default path. + """ + + resolved = (path or _DEFAULT_LOCK_PATH).resolve() + mtime_ns = resolved.stat().st_mtime_ns + with _lock_cache_mutex: + cached = _lock_cache.get(resolved) + if cached is not None and cached[0] == mtime_ns: + return cached[1] + with resolved.open("r", encoding="utf-8") as f: + data = json.load(f) + _lock_cache[resolved] = (mtime_ns, data) + return data + + +def load_taxonomy_lock_hashes( + lock_path: Path | None = None, +) -> TaxonomyLockHashes: + """Build a `TaxonomyLockHashes` from the vendored lock file. + + Reads `data/taxonomies/taxonomies.lock.json` (or `lock_path` when + provided), pulls the `audience.sha256` and `content.sha256` fields, and + returns a `TaxonomyLockHashes` with the `sha256:` prefix the wire spec + requires. + + The lock file is the single source of truth: this function NEVER returns + hard-coded hashes. If the file is missing or malformed, the underlying + OSError / KeyError / json.JSONDecodeError surfaces to the caller -- a + seller that can't read its own taxonomy lock is mis-configured. + """ + + data = _load_taxonomy_lock(lock_path) + return TaxonomyLockHashes( + audience=f"sha256:{data['audience']['sha256']}", + content=f"sha256:{data['content']['sha256']}", + ) + + +def _versions_from_lock( + section: str, lock_path: Path | None = None +) -> list[str]: + """Read a single taxonomy version from the lock file, return as list. + + The lock file pins a single version per taxonomy (e.g., audience 1.1). + Returning a list lets the seller advertise additional versions later + without changing the wire shape. + """ + + data = _load_taxonomy_lock(lock_path) + return [data[section]["version"]] + + +def build_capability_audience_block( + *, + lock_path: Path | None = None, + agentic_supported: bool = False, + supports_constraints: bool = True, + supports_extensions: bool = False, + supports_exclusions: bool = False, + max_refs_per_role: MaxRefsPerRole | None = None, +) -> CapabilityAudienceBlock: + """Build the `audience_capabilities` block for capability discovery. + + Demo / MVP defaults match the locked-in decisions from the proposal: + - `schema_version` = "1" + - taxonomy versions sourced from `data/taxonomies/taxonomies.lock.json` + - `agentic.supported` = False (match endpoint lands in §11) + - `supports_constraints` = True (filter lands in §10) + - `supports_extensions` = False, `supports_exclusions` = False + - `max_refs_per_role` = (1 / 3 / 0 / 0) + - `taxonomy_lock_hashes` ALWAYS read from the lock file (never hard-coded) + + The kwargs let future beads (§10, §11) flip individual flags without + changing the call sites that build the agent card. + """ + + return CapabilityAudienceBlock( + schema_version="1", + standard_taxonomy_versions=_versions_from_lock("audience", lock_path), + contextual_taxonomy_versions=_versions_from_lock("content", lock_path), + agentic=AgenticCapabilityFlag(supported=agentic_supported), + supports_constraints=supports_constraints, + supports_extensions=supports_extensions, + supports_exclusions=supports_exclusions, + max_refs_per_role=max_refs_per_role or MaxRefsPerRole(), + taxonomy_lock_hashes=load_taxonomy_lock_hashes(lock_path), + ) diff --git a/src/ad_seller/models/audience_ref.py b/src/ad_seller/models/audience_ref.py new file mode 100644 index 0000000..20f1870 --- /dev/null +++ b/src/ad_seller/models/audience_ref.py @@ -0,0 +1,134 @@ +# Author: Green Mountain Systems AI Inc. +# Donated to IAB Tech Lab + +"""Seller-side `AudienceRef` and `ComplianceContext` data types. + +These mirror the buyer's models (`ad_buyer.models.audience_plan.AudienceRef`, +`ad_buyer.models.audience_plan.ComplianceContext`) on the wire. The seller +defines its own copy rather than importing the buyer's models -- the wire +format spec at `docs/api/audience_plan_wire_format.md` is the source of +truth for cross-repo compatibility, and importing across repos would create +deployment coupling we explicitly do not want. + +Same shape, same validation rules, same JSON output. If the wire spec +changes, both sides update independently to match. + +Bead: ar-roi5 (proposal §5.7, §6 row 8). +""" + +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel, Field, model_validator + +# Type aliases mirroring the buyer-side definitions. +AudienceType = Literal["standard", "contextual", "agentic"] +AudienceSource = Literal["explicit", "resolved", "inferred"] + + +class ComplianceContext(BaseModel): + """Consent regime accompanying an audience reference. + + Required when `AudienceRef.type == 'agentic'`; optional otherwise + (consent for standard/contextual is usually attached at the campaign + level rather than per ref). + + Mirrors `ad_buyer.models.audience_plan.ComplianceContext` on the wire. + """ + + jurisdiction: str = Field( + ..., + description="Jurisdiction code, e.g. 'US', 'EU', 'GLOBAL'", + ) + consent_framework: str = Field( + ..., + description="Consent framework: 'IAB-TCFv2', 'GPP', 'advertiser-1p', 'none'", + ) + consent_string_ref: str | None = Field( + default=None, + description="Opaque pointer to the consent string (not the raw string)", + ) + attestation: str | None = Field( + default=None, + description="Hash or signature carrying any required attestation", + ) + embedding_provenance: Literal[ + "local_buyer", "advertiser_supplied", "hosted_external", "mock" + ] | None = Field( + default=None, + description=( + "Provenance of the embedding bytes (E2-7 Gap 6). Mirrors the " + "buyer-side ComplianceContext.embedding_provenance per the wire spec." + ), + ) + + model_config = {"populate_by_name": True} + + +class AudienceRef(BaseModel): + """A single audience reference (matches the buyer's wire shape). + + The `type` field discriminates the meaning of `identifier`: + - standard: IAB Audience Taxonomy ID (e.g. "3-7") + - contextual: IAB Content Taxonomy ID (e.g. "IAB1-2") + - agentic: embedding URI (e.g. "emb://buyer.example.com/audiences/x") + + Mirrors `ad_buyer.models.audience_plan.AudienceRef` on the wire. + """ + + type: AudienceType = Field( + ..., + description="Audience type: 'standard', 'contextual', or 'agentic'", + ) + identifier: str = Field( + ..., + min_length=1, + description="ID for standard/contextual; URI for agentic", + ) + taxonomy: str = Field( + ..., + min_length=1, + description="'iab-audience' | 'iab-content' | 'agentic-audiences'", + ) + version: str = Field( + ..., + min_length=1, + description="Taxonomy version, e.g. '1.1', '3.1', 'draft-2026-01'", + ) + source: AudienceSource = Field( + ..., + description="Provenance: 'explicit', 'resolved', or 'inferred'", + ) + confidence: float | None = Field( + default=None, + ge=0.0, + le=1.0, + description="Match confidence; set when source is resolved/inferred", + ) + compliance_context: ComplianceContext | None = Field( + default=None, + description="Consent context; required when type='agentic'", + ) + + model_config = {"populate_by_name": True} + + @model_validator(mode="after") + def _validate_compliance_for_agentic(self) -> AudienceRef: + """Agentic refs MUST carry a compliance_context.""" + + if self.type == "agentic" and self.compliance_context is None: + raise ValueError( + "AudienceRef.compliance_context is required when type='agentic'" + ) + return self + + @model_validator(mode="after") + def _validate_confidence_provenance(self) -> AudienceRef: + """Explicit refs should not carry a confidence score.""" + + if self.source == "explicit" and self.confidence is not None: + raise ValueError( + "AudienceRef.confidence must be None when source='explicit'" + ) + return self diff --git a/src/ad_seller/models/flow_state.py b/src/ad_seller/models/flow_state.py index 09f60e6..78f0015 100644 --- a/src/ad_seller/models/flow_state.py +++ b/src/ad_seller/models/flow_state.py @@ -194,21 +194,25 @@ class DealOutput(BaseModel): class SellerFlowState(BaseModel): """Complete state for seller workflow execution. - CrewAI 1.10+ constructs state eagerly at Flow.__init__ time, so all - fields need defaults. The flow's ``initialize_setup`` / equivalent - start step overwrites them with real values. + Required-field defaults are empty placeholders so CrewAI's + `_create_initial_state` (which calls `StateWithId()` with no args) + can construct the state. Each Flow's `start`-decorated step then + overwrites these with real values via `self.state.field = ...`. + Per ar-y7hn — without these defaults, every seller flow constructor + raised ``ValidationError`` on instantiation, and `GET /products` / + `GET /products/{id}` returned 500. """ # Workflow identity - flow_id: str = "" + flow_id: str = "" # set by Flow `start` step flow_type: str = "" # product_setup, proposal_handling, deal_generation, execution status: ExecutionStatus = ExecutionStatus.INITIALIZED started_at: datetime = Field(default_factory=datetime.utcnow) completed_at: Optional[datetime] = None # Seller identity - seller_organization_id: str = "" - seller_name: str = "" + seller_organization_id: str = "" # set by Flow `start` step + seller_name: str = "" # set by Flow `start` step # Product catalog state products: dict[str, ProductDefinition] = Field(default_factory=dict) diff --git a/src/ad_seller/models/media_kit.py b/src/ad_seller/models/media_kit.py index b56756f..d458d3b 100644 --- a/src/ad_seller/models/media_kit.py +++ b/src/ad_seller/models/media_kit.py @@ -8,7 +8,7 @@ standard identifiers as canonical values: - Content categories: IAB Content Taxonomy v2/v3 IDs (e.g. "IAB19" for sports) -- Audience segments: IAB Audience Taxonomy 1.1 numeric IDs +- Audience capabilities: typed AudienceCapabilities (standard/contextual/agentic) - Device types: AdCOM DeviceType integers (1=Mobile, 2=PC, 3=CTV, etc.) - Ad formats: OpenRTB imp sub-object names ("banner", "video", "native", "audio") - Geo targets: ISO 3166-2 codes ("US", "US-NY") @@ -20,14 +20,21 @@ inventory via Product → InventorySegment → inventory_references """ +import logging from datetime import datetime from enum import Enum -from typing import Optional +from typing import Any, Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, model_validator +from .audience_capabilities import AgenticCapabilities, AudienceCapabilities from .core import PricingModel +# Migration log for legacy `audience_segment_ids` -> `audience_capabilities` +# coming through StorageBackend rows or seed flows. Quiet INFO on a dedicated +# logger so callers can filter or count it. +_migration_logger = logging.getLogger("ad_seller.audience.migration") + class PackageLayer(str, Enum): """Layer indicating how a package was created.""" @@ -75,8 +82,11 @@ class Package(BaseModel): cat: list[str] = Field(default_factory=list) # e.g. ["IAB19", "IAB19-29"] cattax: int = 2 # 1=CT1.0, 2=CT2.0, 3=CT3.0 - # IAB Audience Taxonomy 1.1 segment IDs (canonical) - audience_segment_ids: list[str] = Field(default_factory=list) # e.g. ["3", "4", "5"] + # Typed audience capability declaration -- replaces the flat + # `audience_segment_ids: list[str]` field. Carries standard segments, + # contextual-as-audience-intent, and optional agentic capabilities. + # See proposal §5.7 / bead ar-roi5. + audience_capabilities: AudienceCapabilities = Field(default_factory=AudienceCapabilities) # AdCOM-aligned inventory classification (canonical) device_types: list[int] = Field( @@ -103,12 +113,104 @@ class Package(BaseModel): created_at: datetime = Field(default_factory=datetime.utcnow) updated_at: Optional[datetime] = None + @model_validator(mode="before") + @classmethod + def _migrate_legacy_audience_segment_ids(cls, data: Any) -> Any: + """Backward-compat shim: legacy `audience_segment_ids` -> typed shape. + + Accepts inputs containing the deprecated flat `audience_segment_ids: + list[str]` (from old SQLite rows, seed flows, or external callers + that have not yet migrated) and rewrites them into the typed + `audience_capabilities` field with `standard_taxonomy_version="1.1"` + (the implicit version the legacy field always carried). + + Logs each conversion at INFO on `ad_seller.audience.migration` so + operators can monitor the deprecation runway. + + No-op when: + - input is not a dict (e.g., already a Package instance) + - `audience_segment_ids` is absent + - both `audience_segment_ids` and `audience_capabilities` are + provided (caller is explicitly setting both -- last write wins, + but we drop the legacy field so it never gets stored) + """ + + if not isinstance(data, dict): + return data + if "audience_segment_ids" not in data: + return data + + legacy = data.pop("audience_segment_ids") + + # If caller already supplied audience_capabilities, prefer it and + # just drop the legacy alias (don't clobber explicit config). + if "audience_capabilities" in data: + _migration_logger.info( + "Dropping legacy audience_segment_ids=%r in favor of " + "explicit audience_capabilities for package %s", + legacy, + data.get("package_id", ""), + ) + return data + + legacy_list = list(legacy) if legacy else [] + data["audience_capabilities"] = { + "standard_segment_ids": legacy_list, + "standard_taxonomy_version": "1.1", + } + _migration_logger.info( + "Migrated legacy audience_segment_ids=%r -> " + "audience_capabilities(standard, AT 1.1) for package %s", + legacy_list, + data.get("package_id", ""), + ) + return data + + +class AudienceCapabilityPublicSummary(BaseModel): + """Capability-discovery view of a package's audience capabilities. + + Public-tier callers see only the *shape* of what the package can target: + taxonomy versions and "supports X?" flags. The segment lists themselves + are not disclosed -- they live behind the authenticated tier (proposal + §5.7: "Public view exposes capabilities only, Authenticated view exposes + segment lists"). This is the same separation `cat`/`cattax` already + has at the content layer, extended to audience. + """ + + standard_taxonomy_version: str = "1.1" + contextual_taxonomy_version: str = "3.1" + supports_standard: bool = False + supports_contextual: bool = False + supports_agentic: bool = False + agentic_spec_version: Optional[str] = None + + +def _public_summary_from_capabilities( + caps: AudienceCapabilities, +) -> AudienceCapabilityPublicSummary: + """Project full AudienceCapabilities -> public capability summary.""" + + return AudienceCapabilityPublicSummary( + standard_taxonomy_version=caps.standard_taxonomy_version, + contextual_taxonomy_version=caps.contextual_taxonomy_version, + supports_standard=bool(caps.standard_segment_ids), + supports_contextual=bool(caps.contextual_segment_ids), + supports_agentic=caps.agentic_capabilities is not None, + agentic_spec_version=( + caps.agentic_capabilities.spec_version + if caps.agentic_capabilities is not None + else None + ), + ) + class PublicPackageView(BaseModel): """Tier-gated public view of a package. Shown to unauthenticated buyers. Contains no exact pricing, - no placement details, no audience segment IDs. + no placement details, and ONLY capability metadata for audience + (taxonomy versions + supports-X flags); no segment lists. """ package_id: str @@ -118,6 +220,11 @@ class PublicPackageView(BaseModel): device_types: list[int] = Field(default_factory=list) cat: list[str] = Field(default_factory=list) cattax: int = 2 + # Capability metadata only -- versions + supports flags. Segment lists + # are deliberately absent at this tier. + audience_capabilities: AudienceCapabilityPublicSummary = Field( + default_factory=AudienceCapabilityPublicSummary + ) geo_targets: list[str] = Field(default_factory=list) tags: list[str] = Field(default_factory=list) price_range: str # "$28-$42 CPM" via PricingRulesEngine @@ -128,14 +235,21 @@ class PublicPackageView(BaseModel): class AuthenticatedPackageView(PublicPackageView): """Extended view for authenticated buyers. - Includes exact tier-adjusted pricing, placement details, - audience segment IDs, and negotiation availability. + Includes exact tier-adjusted pricing, placement details, the full typed + `audience_capabilities` object (with segment lists), and negotiation + availability. + + Note: `audience_capabilities` is *redeclared* here as the full + `AudienceCapabilities` type (overriding the public summary on the base + class) so authenticated callers see the segment lists. """ exact_price: float floor_price: float currency: str = "USD" placements: list[PackagePlacement] = Field(default_factory=list) - audience_segment_ids: list[str] = Field(default_factory=list) + audience_capabilities: AudienceCapabilities = Field( + default_factory=AudienceCapabilities + ) negotiation_enabled: bool = False volume_discounts_available: bool = False diff --git a/src/ad_seller/services/audience_plan_validator.py b/src/ad_seller/services/audience_plan_validator.py new file mode 100644 index 0000000..ed24178 --- /dev/null +++ b/src/ad_seller/services/audience_plan_validator.py @@ -0,0 +1,160 @@ +# Author: Green Mountain Systems AI Inc. +# Donated to IAB Tech Lab + +"""Validate an incoming buyer `AudiencePlan` against the seller's capabilities. + +Implements proposal §5.7 layer 3 (forward-compatible structured rejection): + +> If a seller receives a plan with a field or type it doesn't recognize +> (because the buyer pre-flight is missing or stale), it rejects the +> booking with a structured error: +> +> { +> "error": "audience_plan_unsupported", +> "unsupported": [ +> {"path": "extensions[0]", "reason": "extensions not supported by this seller"}, +> {"path": "primary.taxonomy", "reason": "version 3.2 not supported"} +> ] +> } + +The buyer's orchestrator catches this, applies degradation, and retries. +This module produces the structured `unsupported` list; the API layer +turns it into the 400 response. + +Bead: ar-sn8f (proposal §5.7 layer 3 + §6 row 11). +""" + +from __future__ import annotations + +from typing import Any + +from ..models.audience_capabilities import CapabilityAudienceBlock + +# JSON-path-ish keys we surface in the structured error. +_PRIMARY = "primary" +_CONSTRAINTS = "constraints" +_EXTENSIONS = "extensions" +_EXCLUSIONS = "exclusions" + + +def _ref_taxonomy_supported( + ref: dict[str, Any], + capabilities: CapabilityAudienceBlock, +) -> tuple[bool, str | None]: + """Return (ok, reason) for a single ref's taxonomy/version compatibility. + + Standard refs check `standard_taxonomy_versions`; contextual refs check + `contextual_taxonomy_versions`; agentic refs check `agentic.supported`. + Unknown ref types are reported as unsupported (forward-compat: the seller + doesn't know how to interpret them). + """ + + ref_type = ref.get("type") + version = ref.get("version", "") + + if ref_type == "standard": + if version and version not in capabilities.standard_taxonomy_versions: + return False, ( + f"standard taxonomy version {version!r} not supported " + f"(seller supports {sorted(capabilities.standard_taxonomy_versions)})" + ) + return True, None + + if ref_type == "contextual": + if version and version not in capabilities.contextual_taxonomy_versions: + return False, ( + f"contextual taxonomy version {version!r} not supported " + f"(seller supports {sorted(capabilities.contextual_taxonomy_versions)})" + ) + return True, None + + if ref_type == "agentic": + if not capabilities.agentic.supported: + return False, "agentic refs not supported by this seller" + return True, None + + # Unknown type -- forward-compat reject. + return False, f"unknown audience ref type {ref_type!r}" + + +def validate_audience_plan( + audience_plan: dict[str, Any] | None, + capabilities: CapabilityAudienceBlock, +) -> list[dict[str, str]]: + """Compare an audience_plan against the seller's capability block. + + Returns a list of `{"path": str, "reason": str}` entries describing every + unsupported part of the plan. An empty list means the plan is fully + supportable. + + The function is robust to a missing or empty plan (returns []) so callers + can pre-flight even on legacy booking requests that don't carry an + audience_plan. + """ + + if not audience_plan: + return [] + + unsupported: list[dict[str, str]] = [] + + # ---- primary ---- + primary = audience_plan.get(_PRIMARY) + if isinstance(primary, dict): + ok, reason = _ref_taxonomy_supported(primary, capabilities) + if not ok: + unsupported.append({"path": "primary.taxonomy", "reason": reason or ""}) + + # ---- per-role gates + per-ref taxonomy checks ---- + role_pairs = [ + (_CONSTRAINTS, capabilities.supports_constraints), + (_EXTENSIONS, capabilities.supports_extensions), + (_EXCLUSIONS, capabilities.supports_exclusions), + ] + role_caps = capabilities.max_refs_per_role.model_dump() + + for role, role_supported in role_pairs: + refs = audience_plan.get(role) or [] + if not isinstance(refs, list): + continue + + # Role gate: if role is non-empty but seller can't honor it, reject. + if refs and not role_supported: + unsupported.append( + { + "path": f"{role}[0]", + "reason": f"{role} not supported by this seller", + } + ) + # Once the role is gated off, no point reporting per-ref errors. + continue + + # Cardinality cap. + max_for_role = role_caps.get(role, 0) + if len(refs) > max_for_role: + unsupported.append( + { + "path": f"{role}", + "reason": ( + f"{len(refs)} refs in {role} exceeds max_refs_per_role.{role}" + f"={max_for_role}" + ), + } + ) + + # Per-ref taxonomy/version checks. + for idx, ref in enumerate(refs): + if not isinstance(ref, dict): + unsupported.append( + { + "path": f"{role}[{idx}]", + "reason": "ref is not an object", + } + ) + continue + ok, reason = _ref_taxonomy_supported(ref, capabilities) + if not ok: + unsupported.append( + {"path": f"{role}[{idx}].taxonomy", "reason": reason or ""} + ) + + return unsupported diff --git a/src/ad_seller/services/fulfillment.py b/src/ad_seller/services/fulfillment.py new file mode 100644 index 0000000..8072302 --- /dev/null +++ b/src/ad_seller/services/fulfillment.py @@ -0,0 +1,147 @@ +# Author: Green Mountain Systems AI Inc. +# Donated to IAB Tech Lab + +"""Fulfillment-adjacent helpers for deal-time audience-plan handling. + +Per proposal §5.1 Step 2 (snapshot honor policy): + +> If the seller drops support for an audience type after booking but before +> fulfillment (e.g., agentic capability is decommissioned), the seller honors +> the snapshot frozen at booking time. The buyer's `audience_plan_id` hash is +> the proof of what was agreed. + +`honor_audience_plan_snapshot()` is the minimal helper fulfillment paths can +call to retrieve the frozen plan and emit a structured warning when the +seller's *current* capabilities are weaker than what was promised at booking. + +Bead: ar-sn8f (proposal §5.1 Step 2 + §6 row 11). +""" + +from __future__ import annotations + +import logging +from typing import Any, Optional + +from ..models.audience_capabilities import CapabilityAudienceBlock + +logger = logging.getLogger(__name__) + + +def _capabilities_have_degraded( + snapshot_plan: dict[str, Any], + current_capabilities: CapabilityAudienceBlock, +) -> list[str]: + """Compare a frozen audience_plan against the seller's *current* caps. + + Returns a list of human-readable degradation messages. Empty list means + no degradation (current capabilities still cover the snapshot). + + The check is intentionally conservative: it flags any case where the + snapshot expects a capability the seller no longer advertises. Subtle + questions (e.g., a seller that *narrowed* its standard taxonomy versions) + are out of scope for this helper -- callers can layer richer checks on + top. + """ + + degradations: list[str] = [] + + # Did the snapshot include extensions, but the seller no longer supports them? + if snapshot_plan.get("extensions") and not current_capabilities.supports_extensions: + degradations.append( + "snapshot includes extensions[] but seller no longer supports extensions" + ) + + # Constraints + if snapshot_plan.get("constraints") and not current_capabilities.supports_constraints: + degradations.append( + "snapshot includes constraints[] but seller no longer supports constraints" + ) + + # Exclusions + if snapshot_plan.get("exclusions") and not current_capabilities.supports_exclusions: + degradations.append( + "snapshot includes exclusions[] but seller no longer supports exclusions" + ) + + # Agentic refs anywhere in the plan vs. seller-level agentic flag + def _has_agentic(refs: Any) -> bool: + if not refs: + return False + if isinstance(refs, dict): + refs = [refs] + return any( + isinstance(r, dict) and r.get("type") == "agentic" for r in refs + ) + + has_agentic_in_snapshot = ( + _has_agentic(snapshot_plan.get("primary")) + or _has_agentic(snapshot_plan.get("constraints")) + or _has_agentic(snapshot_plan.get("extensions")) + or _has_agentic(snapshot_plan.get("exclusions")) + ) + if has_agentic_in_snapshot and not current_capabilities.agentic.supported: + degradations.append( + "snapshot includes agentic refs but seller no longer supports agentic" + ) + + return degradations + + +def honor_audience_plan_snapshot( + deal_id: str, + deal_record: Optional[dict[str, Any]], + current_capabilities: CapabilityAudienceBlock, +) -> Optional[dict[str, Any]]: + """Return the frozen audience_plan snapshot for a booked deal. + + Per §5.1 Step 2: the snapshot is what the buyer and seller agreed to at + booking time. Even if the seller's *current* capabilities have shrunk, the + snapshot is honored -- the deal_id + audience_plan_id hash are the + forensic anchor. + + Args: + deal_id: The booked deal's ID. Used for log correlation. + deal_record: The seller's deal storage record (or None when missing). + Expected to carry `audience_plan_snapshot` keyed off the booking. + current_capabilities: The seller's *current* capability block. Used + only for the degradation warning -- it does NOT modify the + returned snapshot. + + Returns: + The frozen `audience_plan_snapshot` dict if present; None when the + deal record is missing or carries no snapshot. The returned object + is the snapshot exactly as stored (no rewriting, no degradation). + + Side effect: logs a WARNING when the seller's current capabilities are + weaker than the snapshot. The deal still proceeds; the warning is the + forensic surface fulfillment-adjacent code or audit jobs can scrape. + """ + + if not deal_record: + logger.info("honor_audience_plan_snapshot: deal_record missing for %s", deal_id) + return None + + snapshot = deal_record.get("audience_plan_snapshot") + if not snapshot: + # Pre-§11 deals booked before audience_plan_snapshot was wired. Not an + # error; just nothing to honor. Caller decides what to do. + logger.info( + "honor_audience_plan_snapshot: no snapshot on deal %s (pre-§11 booking)", + deal_id, + ) + return None + + degradations = _capabilities_have_degraded(snapshot, current_capabilities) + if degradations: + # Single structured warning so audit log scrapers can correlate by + # deal_id and audience_plan_id. + plan_id = snapshot.get("audience_plan_id", "") + logger.warning( + "honor_audience_plan_snapshot: capabilities degraded for deal=%s " + "audience_plan_id=%s degradations=%s -- snapshot honored per §5.1", + deal_id, + plan_id, + degradations, + ) + + return snapshot diff --git a/src/ad_seller/services/openrtb_parser.py b/src/ad_seller/services/openrtb_parser.py new file mode 100644 index 0000000..75d8cd5 --- /dev/null +++ b/src/ad_seller/services/openrtb_parser.py @@ -0,0 +1,331 @@ +# Author: Green Mountain Systems AI Inc. +# Donated to IAB Tech Lab + +"""OpenRTB parser: BidRequest fragments -> seller-side `AudienceRef` list. + +Per proposal §5.1 Step 4 / §6 row 15 / wire-format spec §9, the buyer flattens +audience semantics into three OpenRTB carriers at impression time: + +| Audience type | OpenRTB carrier | Seller parse rule | +|---------------|-----------------|-------------------| +| `standard` | ``user.data[].segment[].id`` | Honor only when ``data.name == "IAB_Taxonomy"`` | +| `contextual` | ``site.cat`` + ``site.cattax = 7`` | Honor only when ``cattax == 7`` (Content Taxonomy 3.1) | +| `agentic` | ``user.ext.iab_agentic_audiences.refs[]`` | Namespaced extension | + +The parser is intentionally **defensive**: unknown ``cattax`` values, missing +or malformed extension keys, and partial fragments all log a warning and +parse what they can without raising. Sellers downstream of this parser may +choose to enforce stricter validation by inspecting the warnings list. + +Future: this parser is invokable from tests today. When a real OpenRTB +ingestion endpoint lands on the seller, it imports `parse_openrtb_audience` +and feeds the resulting `AudienceRef` list into the existing audience-plan +matcher. + +Bead: ar-8vzg (proposal §6 row 15). +""" + +from __future__ import annotations + +import logging +from typing import Any + +from ad_seller.models.audience_ref import AudienceRef, ComplianceContext + +logger = logging.getLogger(__name__) + +# Seller-side mirror of buyer's constants (kept independent on purpose -- +# the wire-format spec is the source of truth, NOT a Python import). +CONTENT_TAXONOMY_31_CATTAX = 7 +IAB_AUDIENCE_TAXONOMY_DATA_NAME = "IAB_Taxonomy" +AGENTIC_USER_EXT_KEY = "iab_agentic_audiences" + +# When the wire payload omits ``compliance_context`` for an agentic ref -- +# which the buyer's Pydantic model forbids but a malformed third-party +# emitter MAY do -- we synthesize a minimal placeholder so downstream code +# does not crash. The synthesized value is clearly marked so audit trails +# can flag it. Sellers SHOULD reject such requests if strictness applies. +_FALLBACK_AGENTIC_COMPLIANCE = ComplianceContext( + jurisdiction="UNKNOWN", + consent_framework="none", + consent_string_ref=None, + attestation=None, +) + + +def _parse_standard_data_entries( + user_data: list[dict[str, Any]] | None, + warnings: list[str], +) -> list[AudienceRef]: + """Extract standard-type AudienceRefs from ``bidrequest.user.data[]``. + + Honors only entries whose ``name`` equals ``IAB_Taxonomy`` (per the + builder's emission rule). Reads ``ext.taxonomy_version`` to set the + ref's ``version`` field; falls back to ``"1.1"`` when absent. + """ + + refs: list[AudienceRef] = [] + if not user_data: + return refs + + for i, data_entry in enumerate(user_data): + if not isinstance(data_entry, dict): + warnings.append(f"user.data[{i}] is not an object; skipped") + continue + name = data_entry.get("name") + if name != IAB_AUDIENCE_TAXONOMY_DATA_NAME: + # Other data providers may legitimately appear in user.data -- + # we only consume the IAB_Taxonomy ones. + continue + + ext = data_entry.get("ext") or {} + version = ( + ext.get("taxonomy_version") if isinstance(ext, dict) else None + ) or "1.1" + + segments = data_entry.get("segment") or [] + if not isinstance(segments, list): + warnings.append( + f"user.data[{i}].segment is not an array; skipped" + ) + continue + + for j, seg in enumerate(segments): + if not isinstance(seg, dict): + warnings.append( + f"user.data[{i}].segment[{j}] is not an object; skipped" + ) + continue + seg_id = seg.get("id") + if not seg_id or not isinstance(seg_id, str): + warnings.append( + f"user.data[{i}].segment[{j}] missing 'id'; skipped" + ) + continue + try: + refs.append( + AudienceRef( + type="standard", + identifier=seg_id, + taxonomy="iab-audience", + version=version, + source="explicit", + ) + ) + except Exception as exc: # noqa: BLE001 - validation already strict + warnings.append( + f"user.data[{i}].segment[{j}] failed validation: {exc}" + ) + return refs + + +def _parse_contextual( + site: dict[str, Any] | None, + warnings: list[str], +) -> list[AudienceRef]: + """Extract contextual-type AudienceRefs from ``bidrequest.site``. + + Honors only ``cattax == 7`` (IAB Content Taxonomy 3.1) per the wire-format + spec. Other ``cattax`` values are ignored with a warning -- they may be + valid Content Taxonomy 2.x or other taxonomies, but the seller-side + parser does not implement those. + """ + + refs: list[AudienceRef] = [] + if not site or not isinstance(site, dict): + return refs + + cattax = site.get("cattax") + cats = site.get("cat") or [] + + if not cats: + return refs + + if cattax != CONTENT_TAXONOMY_31_CATTAX: + warnings.append( + f"site.cattax={cattax!r} not honored; only " + f"{CONTENT_TAXONOMY_31_CATTAX} (Content Taxonomy 3.1) supported" + ) + return refs + + if not isinstance(cats, list): + warnings.append("site.cat is not an array; skipped") + return refs + + for i, cat in enumerate(cats): + if not isinstance(cat, str) or not cat: + warnings.append(f"site.cat[{i}] is not a non-empty string; skipped") + continue + try: + refs.append( + AudienceRef( + type="contextual", + identifier=cat, + taxonomy="iab-content", + version="3.1", + source="explicit", + ) + ) + except Exception as exc: # noqa: BLE001 + warnings.append(f"site.cat[{i}] failed validation: {exc}") + return refs + + +def _parse_agentic_ext( + user_ext: dict[str, Any] | None, + warnings: list[str], +) -> list[AudienceRef]: + """Extract agentic-type AudienceRefs from the namespaced user.ext slot. + + Reads ``user.ext.iab_agentic_audiences.refs[]``. Each entry MUST carry + ``identifier`` and ``version``; ``source`` defaults to ``"explicit"`` + when absent; ``compliance_context`` defaults to a clearly-marked + placeholder (``jurisdiction='UNKNOWN'``, ``consent_framework='none'``) + when absent so the AudienceRef validator does not crash on a malformed + third-party request. Sellers that require strict consent SHOULD inspect + the warnings list and reject placeholders. + """ + + refs: list[AudienceRef] = [] + if not user_ext or not isinstance(user_ext, dict): + return refs + + agentic_block = user_ext.get(AGENTIC_USER_EXT_KEY) + if agentic_block is None: + return refs + if not isinstance(agentic_block, dict): + warnings.append( + f"user.ext.{AGENTIC_USER_EXT_KEY} is not an object; skipped" + ) + return refs + + entries = agentic_block.get("refs") or [] + if not isinstance(entries, list): + warnings.append( + f"user.ext.{AGENTIC_USER_EXT_KEY}.refs is not an array; skipped" + ) + return refs + + for i, entry in enumerate(entries): + if not isinstance(entry, dict): + warnings.append( + f"user.ext.{AGENTIC_USER_EXT_KEY}.refs[{i}] is not an object; skipped" + ) + continue + identifier = entry.get("identifier") + version = entry.get("version") + source = entry.get("source") or "explicit" + cc_payload = entry.get("compliance_context") + + if not identifier or not isinstance(identifier, str): + warnings.append( + f"user.ext.{AGENTIC_USER_EXT_KEY}.refs[{i}] missing 'identifier'; skipped" + ) + continue + if not version or not isinstance(version, str): + warnings.append( + f"user.ext.{AGENTIC_USER_EXT_KEY}.refs[{i}] missing 'version'; skipped" + ) + continue + + if cc_payload is None: + warnings.append( + f"user.ext.{AGENTIC_USER_EXT_KEY}.refs[{i}] missing " + "'compliance_context'; using fallback placeholder" + ) + compliance = _FALLBACK_AGENTIC_COMPLIANCE + else: + try: + compliance = ComplianceContext.model_validate(cc_payload) + except Exception as exc: # noqa: BLE001 + warnings.append( + f"user.ext.{AGENTIC_USER_EXT_KEY}.refs[{i}] " + f"compliance_context invalid: {exc}; using fallback" + ) + compliance = _FALLBACK_AGENTIC_COMPLIANCE + + try: + refs.append( + AudienceRef( + type="agentic", + identifier=identifier, + taxonomy="agentic-audiences", + version=version, + source=source, + compliance_context=compliance, + ) + ) + except Exception as exc: # noqa: BLE001 + warnings.append( + f"user.ext.{AGENTIC_USER_EXT_KEY}.refs[{i}] failed validation: {exc}" + ) + return refs + + +def parse_openrtb_audience(bidrequest: dict[str, Any]) -> dict[str, Any]: + """Parse a partial OpenRTB BidRequest into a flat list of `AudienceRef`s. + + Args: + bidrequest: The parsed JSON body of an OpenRTB v2.6 BidRequest. Only + the ``user`` and ``site`` top-level keys are consumed. Other + slots (``imp``, ``device``, ``app``, etc.) are ignored here -- + the matcher receives whatever shape its caller hands it. + + Returns: + A dict with two keys: + - ``"refs"``: a flat list of `AudienceRef` objects in the order + (standard from user.data, contextual from site.cat, agentic + from user.ext). Roles are NOT preserved -- OpenRTB does not + carry primary/constraints/extensions/exclusions semantics on + the wire (those live on the booking-time `AudiencePlan` + snapshot referenced by ``deal_id``). All refs returned here are + ``source='explicit'``. + - ``"warnings"``: a list of human-readable strings describing + anything skipped or fallback-substituted during parsing. + Sellers MAY surface these to the audit trail. + + Notes: + - This parser does NOT reconstruct an `AudiencePlan` -- the lossy + OpenRTB mapping cannot recover the role distinction. Callers + that need a full plan should look up the booked plan snapshot by + ``deal_id`` instead. + - For the round-trip parity tested in `test_openrtb_parser`, we + treat all refs as if they were primary-or-equivalent positive + targeting hints. + """ + + if not isinstance(bidrequest, dict): + return { + "refs": [], + "warnings": ["bidrequest is not an object"], + } + + warnings: list[str] = [] + + user = bidrequest.get("user") if isinstance(bidrequest.get("user"), dict) else None + site = bidrequest.get("site") if isinstance(bidrequest.get("site"), dict) else None + + refs: list[AudienceRef] = [] + refs.extend( + _parse_standard_data_entries( + (user or {}).get("data") if isinstance((user or {}).get("data"), list) else None, + warnings, + ) + ) + refs.extend(_parse_contextual(site, warnings)) + refs.extend( + _parse_agentic_ext( + (user or {}).get("ext") if isinstance((user or {}).get("ext"), dict) else None, + warnings, + ) + ) + + return {"refs": refs, "warnings": warnings} + + +__all__ = [ + "AGENTIC_USER_EXT_KEY", + "CONTENT_TAXONOMY_31_CATTAX", + "IAB_AUDIENCE_TAXONOMY_DATA_NAME", + "parse_openrtb_audience", +] diff --git a/tests/integration/test_packages_audience_filter.py b/tests/integration/test_packages_audience_filter.py new file mode 100644 index 0000000..a68cae5 --- /dev/null +++ b/tests/integration/test_packages_audience_filter.py @@ -0,0 +1,416 @@ +# Author: Green Mountain Systems AI Inc. +# Donated to IAB Tech Lab + +"""Integration tests for `GET /packages` audience filter and `POST /media-kit/search` +audience corpus (proposal §5.7 + §6 row 10, bead ar-2wxa). + +Drives the FastAPI app via httpx + ASGITransport, with the global storage +patched to an in-memory backend that we seed per-test. Mirrors the pattern +in `test_quote_endpoints.py` and `test_capability_audience_block.py`. + +Coverage: + +1. `GET /packages?audience_type=standard&audience_id=3-7` returns only + packages whose AudienceCapabilities include "3-7". +2. `GET /packages?audience_type=contextual&audience_id=IAB1-2` returns only + matching packages. +3. `GET /packages?audience_type=agentic` returns only packages whose + `audience_capabilities.agentic_capabilities` is non-null (empty list when + none exist -- not 404). +4. `GET /packages` (no audience params) returns all packages (backward + compat). +5. `POST /media-kit/search` ranks audience-matching packages higher when + the query mentions a known IAB segment ID. +6. `POST /media-kit/search` with no audience hint still works. +7. `POST /media-kit/search` with an `audience_filter` body field restricts + to matching packages (additive; backward compat preserved by tests above). +""" + +from __future__ import annotations + +import sys +from types import ModuleType + +# Stub broken flow modules (pre-existing @listen() bugs with CrewAI version +# mismatch) before importing main, mirroring test_quote_endpoints.py. +_broken_flows = [ + "ad_seller.flows.discovery_inquiry_flow", + "ad_seller.flows.execution_activation_flow", +] +for _mod_name in _broken_flows: + if _mod_name not in sys.modules: + _stub = ModuleType(_mod_name) + _cls_name = ( + _mod_name.rsplit(".", 1)[-1].replace("_", " ").title().replace(" ", "") + ) + setattr(_stub, _cls_name, type(_cls_name, (), {})) + sys.modules[_mod_name] = _stub + +from unittest.mock import AsyncMock, patch # noqa: E402 + +import httpx # noqa: E402 +import pytest # noqa: E402 +from httpx import ASGITransport # noqa: E402 + +from ad_seller.interfaces.api.main import ( # noqa: E402 + _get_optional_api_key_record, + app, +) +from ad_seller.models.audience_capabilities import ( # noqa: E402 + AgenticCapabilities, + AudienceCapabilities, +) +from ad_seller.models.media_kit import ( # noqa: E402 + Package, + PackageLayer, + PackageStatus, +) + + +# ============================================================================= +# Fixtures +# ============================================================================= + + +def _make_package_dict( + *, + package_id: str, + name: str = "Test Package", + standard_segment_ids: list[str] | None = None, + contextual_segment_ids: list[str] | None = None, + standard_taxonomy_version: str = "1.1", + contextual_taxonomy_version: str = "3.1", + agentic_capabilities: AgenticCapabilities | None = None, + tags: list[str] | None = None, + cat: list[str] | None = None, + is_featured: bool = False, + base_price: float = 20.0, + floor_price: float = 10.0, + description: str | None = None, +) -> dict: + pkg = Package( + package_id=package_id, + name=name, + description=description or f"Description for {name}", + layer=PackageLayer.CURATED, + status=PackageStatus.ACTIVE, + base_price=base_price, + floor_price=floor_price, + is_featured=is_featured, + tags=tags or [], + cat=cat or [], + audience_capabilities=AudienceCapabilities( + standard_segment_ids=standard_segment_ids or [], + standard_taxonomy_version=standard_taxonomy_version, + contextual_segment_ids=contextual_segment_ids or [], + contextual_taxonomy_version=contextual_taxonomy_version, + agentic_capabilities=agentic_capabilities, + ), + ) + return pkg.model_dump(mode="json") + + +@pytest.fixture +def seed_packages(): + """Four packages spanning the three types + one with no audience caps.""" + return [ + _make_package_dict( + package_id="pkg-std", + name="Standard Auto Intenders", + standard_segment_ids=["3-7", "3-12"], + ), + _make_package_dict( + package_id="pkg-ctx", + name="Contextual Automotive", + contextual_segment_ids=["IAB1-2"], + ), + _make_package_dict( + package_id="pkg-agt", + name="Agentic Premium", + agentic_capabilities=AgenticCapabilities( + supported_signal_types=["identity", "contextual"] + ), + ), + _make_package_dict( + package_id="pkg-none", + name="Legacy Direct Response", + tags=["direct response"], + ), + ] + + +@pytest.fixture +def mock_storage(seed_packages): + """In-memory storage stub that returns the seed packages from list_packages().""" + storage = AsyncMock() + storage.list_packages = AsyncMock(return_value=seed_packages) + return storage + + +@pytest.fixture +def patched_get_storage(mock_storage): + """Patch `get_storage` so the API's MediaKitService talks to our stub.""" + with patch( + "ad_seller.storage.factory.get_storage", + new=AsyncMock(return_value=mock_storage), + ): + yield mock_storage + + +@pytest.fixture +def client(patched_get_storage): + """httpx AsyncClient with FastAPI dependency overrides + storage patched.""" + app.dependency_overrides[_get_optional_api_key_record] = lambda: None + transport = ASGITransport(app=app) + c = httpx.AsyncClient(transport=transport, base_url="http://test") + yield c + app.dependency_overrides.clear() + + +# ============================================================================= +# GET /packages — audience filter +# ============================================================================= + + +class TestPackagesAudienceFilter: + """Verify audience query params filter the result set.""" + + @pytest.mark.asyncio + async def test_filter_by_standard_id_returns_only_matching(self, client): + async with client as c: + resp = await c.get( + "/packages", + params={"audience_type": "standard", "audience_id": "3-7"}, + ) + assert resp.status_code == 200 + ids = [p["package_id"] for p in resp.json()["packages"]] + assert ids == ["pkg-std"] + + @pytest.mark.asyncio + async def test_filter_by_contextual_id(self, client): + async with client as c: + resp = await c.get( + "/packages", + params={ + "audience_type": "contextual", + "audience_id": "IAB1-2", + }, + ) + assert resp.status_code == 200 + ids = [p["package_id"] for p in resp.json()["packages"]] + assert ids == ["pkg-ctx"] + + @pytest.mark.asyncio + async def test_filter_agentic_type_only(self, client): + async with client as c: + resp = await c.get( + "/packages", params={"audience_type": "agentic"} + ) + assert resp.status_code == 200 + ids = [p["package_id"] for p in resp.json()["packages"]] + assert ids == ["pkg-agt"] + + @pytest.mark.asyncio + async def test_no_match_returns_empty_list_not_404(self, client): + async with client as c: + resp = await c.get( + "/packages", + params={ + "audience_type": "standard", + "audience_id": "never-exists", + }, + ) + assert resp.status_code == 200 + assert resp.json()["packages"] == [] + + @pytest.mark.asyncio + async def test_backward_compat_no_audience_params_returns_all(self, client): + async with client as c: + resp = await c.get("/packages") + assert resp.status_code == 200 + ids = {p["package_id"] for p in resp.json()["packages"]} + assert ids == {"pkg-std", "pkg-ctx", "pkg-agt", "pkg-none"} + + @pytest.mark.asyncio + async def test_invalid_audience_type_returns_400(self, client): + async with client as c: + resp = await c.get( + "/packages", params={"audience_type": "bogus"} + ) + assert resp.status_code == 400 + assert "audience_type" in resp.json()["detail"] + + @pytest.mark.asyncio + async def test_audience_id_without_type_returns_400(self, client): + async with client as c: + resp = await c.get( + "/packages", params={"audience_id": "3-7"} + ) + assert resp.status_code == 400 + assert "audience_type" in resp.json()["detail"] + + @pytest.mark.asyncio + async def test_taxonomy_version_constraint(self, client): + """Mismatched version should drop the package even if ID matches.""" + async with client as c: + resp = await c.get( + "/packages", + params={ + "audience_type": "standard", + "audience_id": "3-7", + "audience_taxonomy_version": "9.9", + }, + ) + assert resp.status_code == 200 + assert resp.json()["packages"] == [] + + +# ============================================================================= +# GET /media-kit/packages — same audience filter (public surface) +# ============================================================================= + + +class TestMediaKitPackagesAudienceFilter: + """The public media-kit listing accepts the same audience triple.""" + + @pytest.mark.asyncio + async def test_public_listing_filtered_by_standard(self, client): + async with client as c: + resp = await c.get( + "/media-kit/packages", + params={"audience_type": "standard", "audience_id": "3-7"}, + ) + assert resp.status_code == 200 + ids = [p["package_id"] for p in resp.json()["packages"]] + assert ids == ["pkg-std"] + + +# ============================================================================= +# POST /media-kit/search — audience corpus + optional audience_filter +# ============================================================================= + + +class TestSearchAudienceCorpus: + """Search now scores against audience capability segment IDs.""" + + @pytest.mark.asyncio + async def test_query_with_segment_id_ranks_matching_first( + self, client, patched_get_storage + ): + # Override seed: two packages share keyword "premium"; only one declares "3-7" + patched_get_storage.list_packages.return_value = [ + _make_package_dict( + package_id="pkg-with", + name="Premium Auto", + standard_segment_ids=["3-7"], + tags=["premium"], + ), + _make_package_dict( + package_id="pkg-without", + name="Premium News", + tags=["premium"], + ), + ] + async with client as c: + resp = await c.post( + "/media-kit/search", json={"query": "premium 3-7"} + ) + assert resp.status_code == 200 + ids = [p["package_id"] for p in resp.json()["results"]] + # pkg-with hits both tokens; pkg-without hits only "premium". + assert ids[0] == "pkg-with" + assert "pkg-without" in ids + + @pytest.mark.asyncio + async def test_keyword_only_search_backward_compat( + self, client, patched_get_storage + ): + patched_get_storage.list_packages.return_value = [ + _make_package_dict( + package_id="pkg-sports", + name="Sports Bundle", + tags=["sports", "live events"], + ), + _make_package_dict( + package_id="pkg-news", + name="News Bundle", + tags=["news"], + ), + ] + async with client as c: + resp = await c.post("/media-kit/search", json={"query": "sports"}) + assert resp.status_code == 200 + ids = [p["package_id"] for p in resp.json()["results"]] + assert ids == ["pkg-sports"] + + @pytest.mark.asyncio + async def test_search_request_without_audience_filter_still_works( + self, client, patched_get_storage + ): + """Existing buyer code that doesn't ship `audience_filter` keeps working.""" + patched_get_storage.list_packages.return_value = [ + _make_package_dict( + package_id="pkg-a", name="Alpha Bundle", tags=["alpha"] + ), + ] + async with client as c: + resp = await c.post( + "/media-kit/search", + json={"query": "alpha", "buyer_tier": "public"}, + ) + assert resp.status_code == 200 + assert [p["package_id"] for p in resp.json()["results"]] == ["pkg-a"] + + @pytest.mark.asyncio + async def test_search_with_audience_filter_restricts_results( + self, client, patched_get_storage + ): + patched_get_storage.list_packages.return_value = [ + _make_package_dict( + package_id="pkg-std", + name="Premium Standard", + standard_segment_ids=["3-7"], + tags=["premium"], + ), + _make_package_dict( + package_id="pkg-ctx", + name="Premium Contextual", + contextual_segment_ids=["IAB1-2"], + tags=["premium"], + ), + ] + async with client as c: + # Without filter: both match "premium" + resp_all = await c.post( + "/media-kit/search", json={"query": "premium"} + ) + assert {p["package_id"] for p in resp_all.json()["results"]} == { + "pkg-std", + "pkg-ctx", + } + + # With filter restricted to contextual: only pkg-ctx + resp_filtered = await c.post( + "/media-kit/search", + json={ + "query": "premium", + "audience_filter": {"audience_type": "contextual"}, + }, + ) + assert resp_filtered.status_code == 200 + ids = [p["package_id"] for p in resp_filtered.json()["results"]] + assert ids == ["pkg-ctx"] + + @pytest.mark.asyncio + async def test_search_invalid_audience_filter_type_returns_400( + self, client + ): + async with client as c: + resp = await c.post( + "/media-kit/search", + json={ + "query": "premium", + "audience_filter": {"audience_type": "bogus"}, + }, + ) + assert resp.status_code == 400 diff --git a/tests/integration/test_schema_drift_canonical.py b/tests/integration/test_schema_drift_canonical.py new file mode 100644 index 0000000..f48e807 --- /dev/null +++ b/tests/integration/test_schema_drift_canonical.py @@ -0,0 +1,73 @@ +"""E2-10: cross-repo schema-drift hardening (seller-side mirror). + +Asserts that the seller's mirror models (`AudienceRef`, `ComplianceContext`) +emit JSON Schemas compatible with the canonical buyer-emitted snapshot at +`agent_range/docs/api/audience_plan_schemas.json`. The seller doesn't ship +its own `AudiencePlan` (the seller stores the buyer's plan as a snapshot, +not as a typed model), so this test only checks the two mirrored types. + +Compatibility, not byte-equality: the seller's `model_json_schema()` may +have minor stylistic differences (description text, $defs ordering) but +the `properties` shape MUST match — same field names, same types, same +required-flag, same enums. +""" + +from __future__ import annotations + +import json +import os +from pathlib import Path + +os.environ.setdefault("ANTHROPIC_API_KEY", "test-key-for-unit-tests") + +import pytest + +from ad_seller.models.audience_ref import AudienceRef, ComplianceContext + + +CANONICAL_SCHEMA_PATH = Path( + "/Users/aidancardella/dev/agent_range/.worktrees/audience-extension/docs/api/audience_plan_schemas.json" +) + + +def _load_canonical() -> dict: + if not CANONICAL_SCHEMA_PATH.exists(): + pytest.skip(f"Canonical schema snapshot not present: {CANONICAL_SCHEMA_PATH}") + return json.loads(CANONICAL_SCHEMA_PATH.read_text()) + + +def _shape(schema: dict) -> dict: + """Reduce a schema to its drift-relevant shape: properties + required + enums.""" + + props = schema.get("properties", {}) + reduced = {} + for name, p in props.items(): + if "enum" in p: + reduced[name] = {"enum": sorted(p["enum"])} + elif "anyOf" in p: + # Capture the type names + required flag, ignore description + types = sorted( + a.get("type", a.get("$ref", "ref")) for a in p.get("anyOf", []) + ) + reduced[name] = {"anyOf_types": types} + elif "type" in p: + reduced[name] = {"type": p["type"]} + else: + # $ref or fallback + reduced[name] = {"shape": "complex"} + return { + "properties": reduced, + "required": sorted(schema.get("required", [])), + } + + +class TestSellerMirrorMatchesCanonical: + def test_compliance_context_field_shape(self): + canonical = _load_canonical()["ComplianceContext"] + live = ComplianceContext.model_json_schema() + assert _shape(live) == _shape(canonical) + + def test_audience_ref_field_shape(self): + canonical = _load_canonical()["AudienceRef"] + live = AudienceRef.model_json_schema() + assert _shape(live) == _shape(canonical) diff --git a/tests/unit/test_agentic_audience_match.py b/tests/unit/test_agentic_audience_match.py new file mode 100644 index 0000000..e12bc92 --- /dev/null +++ b/tests/unit/test_agentic_audience_match.py @@ -0,0 +1,283 @@ +# Author: Green Mountain Systems AI Inc. +# Donated to IAB Tech Lab + +"""Unit tests for `POST /agentic-audience/match` (proposal §5.7 + §6 row 11). + +Covers bead ar-sn8f deliverable A: + +- Happy path (seller advertises agentic): deterministic mock score returned + with quality bucket and `agentic_supported_by_seller=True`. +- Legacy seller (top-level agentic.supported=False): returns POOR / 0.0 / + `agentic_supported_by_seller=False`. +- Non-agentic ref (`type='standard'`): rejected with HTTP 400. +""" + +from __future__ import annotations + +import sys +from types import ModuleType +from unittest.mock import patch + +import pytest + +# Stub broken flow modules before importing main, mirroring sibling tests. +_broken_flows = [ + "ad_seller.flows.discovery_inquiry_flow", + "ad_seller.flows.execution_activation_flow", +] +for _mod_name in _broken_flows: + if _mod_name not in sys.modules: + _stub = ModuleType(_mod_name) + _cls_name = ( + _mod_name.rsplit(".", 1)[-1].replace("_", " ").title().replace(" ", "") + ) + setattr(_stub, _cls_name, type(_cls_name, (), {})) + sys.modules[_mod_name] = _stub + +import httpx # noqa: E402 +from httpx import ASGITransport # noqa: E402 + +from ad_seller.interfaces.api.main import ( # noqa: E402 + _agentic_match_quality, + _deterministic_score, + _get_optional_api_key_record, + app, +) +from ad_seller.models.audience_capabilities import ( # noqa: E402 + AgenticCapabilityFlag, + CapabilityAudienceBlock, + MaxRefsPerRole, + TaxonomyLockHashes, +) + + +# ============================================================================= +# Fixtures +# ============================================================================= + + +@pytest.fixture +def client(): + """httpx AsyncClient with FastAPI dependency overrides.""" + + app.dependency_overrides[_get_optional_api_key_record] = lambda: None + transport = ASGITransport(app=app) + c = httpx.AsyncClient(transport=transport, base_url="http://test") + yield c + app.dependency_overrides.clear() + + +def _agentic_caps_block(*, supported: bool) -> CapabilityAudienceBlock: + """Build a CapabilityAudienceBlock with deterministic hashes. + + We bypass `build_capability_audience_block()` (which reads the lock file) + by stubbing the function the endpoint imports lazily. + """ + + return CapabilityAudienceBlock( + schema_version="1", + standard_taxonomy_versions=["1.1"], + contextual_taxonomy_versions=["3.1"], + agentic=AgenticCapabilityFlag(supported=supported), + supports_constraints=True, + supports_extensions=False, + supports_exclusions=False, + max_refs_per_role=MaxRefsPerRole(), + taxonomy_lock_hashes=TaxonomyLockHashes( + audience="sha256:" + "a" * 64, + content="sha256:" + "b" * 64, + ), + ) + + +@pytest.fixture +def agentic_supported_seller(): + """Patch the endpoint's lazy import of build_capability_audience_block.""" + + with patch( + "ad_seller.models.audience_capabilities.build_capability_audience_block", + return_value=_agentic_caps_block(supported=True), + ): + yield + + +@pytest.fixture +def legacy_seller(): + with patch( + "ad_seller.models.audience_capabilities.build_capability_audience_block", + return_value=_agentic_caps_block(supported=False), + ): + yield + + +# ============================================================================= +# Pure helpers +# ============================================================================= + + +class TestQualityBuckets: + """Score -> quality label mapping.""" + + @pytest.mark.parametrize( + "score,expected", + [ + (0.95, "STRONG"), + (0.85, "STRONG"), + (0.84, "MODERATE"), + (0.65, "MODERATE"), + (0.5, "WEAK"), + (0.4, "WEAK"), + (0.39, "POOR"), + (0.0, "POOR"), + ], + ) + def test_buckets(self, score, expected): + assert _agentic_match_quality(score) == expected + + +class TestDeterministicScore: + """Sha256-derived score is deterministic and in [0, 1].""" + + def test_in_range(self): + score = _deterministic_score("emb://test/x") + assert 0.0 <= score <= 1.0 + + def test_deterministic(self): + a = _deterministic_score("emb://test/y") + b = _deterministic_score("emb://test/y") + assert a == b + + def test_distinct_inputs_distinct_scores(self): + # Cheap sanity: two different identifiers produce different scores. + a = _deterministic_score("emb://buyer.x/foo") + b = _deterministic_score("emb://buyer.x/bar") + assert a != b + + +# ============================================================================= +# POST /agentic-audience/match endpoint +# ============================================================================= + + +class TestAgenticMatchEndpoint: + """End-to-end behavior of the new endpoint.""" + + @pytest.mark.asyncio + async def test_happy_path_agentic_supported( + self, client, agentic_supported_seller + ): + body = { + "audience_ref": { + "type": "agentic", + "identifier": "emb://buyer.example.com/audiences/auto-q1", + "taxonomy": "agentic-audiences", + "version": "draft-2026-01", + "source": "explicit", + "compliance_context": { + "jurisdiction": "US", + "consent_framework": "IAB-TCFv2", + }, + } + } + async with client as c: + resp = await c.post("/agentic-audience/match", json=body) + + assert resp.status_code == 200 + data = resp.json() + assert data["agentic_supported_by_seller"] is True + assert 0.0 <= data["match_confidence"] <= 1.0 + assert data["match_quality"] in {"STRONG", "MODERATE", "WEAK", "POOR"} + assert data["audience_ref"] == body["audience_ref"] + assert "rationale" in data and isinstance(data["rationale"], str) + + @pytest.mark.asyncio + async def test_deterministic_response_per_identifier( + self, client, agentic_supported_seller + ): + body = { + "audience_ref": { + "type": "agentic", + "identifier": "emb://stable/test", + "taxonomy": "agentic-audiences", + "version": "draft-2026-01", + "source": "explicit", + "compliance_context": { + "jurisdiction": "US", + "consent_framework": "IAB-TCFv2", + }, + } + } + async with client as c: + r1 = await c.post("/agentic-audience/match", json=body) + r2 = await c.post("/agentic-audience/match", json=body) + assert r1.json()["match_confidence"] == r2.json()["match_confidence"] + assert r1.json()["match_quality"] == r2.json()["match_quality"] + + @pytest.mark.asyncio + async def test_legacy_seller_returns_poor(self, client, legacy_seller): + body = { + "audience_ref": { + "type": "agentic", + "identifier": "emb://anything", + "taxonomy": "agentic-audiences", + "version": "draft-2026-01", + "source": "explicit", + "compliance_context": { + "jurisdiction": "US", + "consent_framework": "IAB-TCFv2", + }, + } + } + async with client as c: + resp = await c.post("/agentic-audience/match", json=body) + + assert resp.status_code == 200 + data = resp.json() + assert data["agentic_supported_by_seller"] is False + assert data["match_confidence"] == 0.0 + assert data["match_quality"] == "POOR" + assert data["matched_capabilities"] == [] + + @pytest.mark.asyncio + async def test_non_agentic_ref_rejected_with_400( + self, client, agentic_supported_seller + ): + body = { + "audience_ref": { + "type": "standard", + "identifier": "3-7", + "taxonomy": "iab-audience", + "version": "1.1", + "source": "explicit", + } + } + async with client as c: + resp = await c.post("/agentic-audience/match", json=body) + + assert resp.status_code == 400 + # FastAPI wraps the dict body in `detail`. + body_json = resp.json() + assert body_json["detail"]["error"] == "invalid_audience_ref" + + @pytest.mark.asyncio + async def test_missing_identifier_returns_400( + self, client, agentic_supported_seller + ): + body = { + "audience_ref": { + "type": "agentic", + "identifier": "", + "taxonomy": "agentic-audiences", + "version": "draft-2026-01", + "source": "explicit", + "compliance_context": { + "jurisdiction": "US", + "consent_framework": "IAB-TCFv2", + }, + } + } + async with client as c: + resp = await c.post("/agentic-audience/match", json=body) + + assert resp.status_code == 400 + assert resp.json()["detail"]["error"] == "invalid_audience_ref" diff --git a/tests/unit/test_audience_capabilities.py b/tests/unit/test_audience_capabilities.py new file mode 100644 index 0000000..d235179 --- /dev/null +++ b/tests/unit/test_audience_capabilities.py @@ -0,0 +1,415 @@ +# Author: Green Mountain Systems AI Inc. +# Donated to IAB Tech Lab + +"""Unit tests for typed `AudienceCapabilities` on `Package` (proposal §5.7). + +Covers: +- AudienceCapabilities/AgenticCapabilities default construction +- AudienceRef + ComplianceContext validators (agentic requires + compliance_context; explicit must not carry confidence) +- Legacy `Package(audience_segment_ids=[...])` migration to typed shape +- New typed-shape input passthrough +- PublicPackageView excludes segment lists (capability metadata only) +- AuthenticatedPackageView includes the full capability object +- Round-trip via model_dump -> Package(**data) + +Bead: ar-roi5. +""" + +from __future__ import annotations + +import logging +from unittest.mock import AsyncMock + +import pytest + +from ad_seller.engines.media_kit_service import MediaKitService +from ad_seller.engines.pricing_rules_engine import PricingRulesEngine +from ad_seller.models.audience_capabilities import ( + AgenticCapabilities, + AudienceCapabilities, +) +from ad_seller.models.audience_ref import AudienceRef, ComplianceContext +from ad_seller.models.buyer_identity import BuyerContext, BuyerIdentity +from ad_seller.models.media_kit import ( + AudienceCapabilityPublicSummary, + AuthenticatedPackageView, + Package, + PackageLayer, + PackageStatus, + PublicPackageView, +) +from ad_seller.models.pricing_tiers import TieredPricingConfig + + +# ============================================================================= +# AudienceCapabilities / AgenticCapabilities construction +# ============================================================================= + + +class TestAudienceCapabilitiesConstruction: + """Default construction and field defaults.""" + + def test_default_construction(self): + """AudienceCapabilities() builds cleanly with field defaults.""" + caps = AudienceCapabilities() + assert caps.standard_segment_ids == [] + assert caps.standard_taxonomy_version == "1.1" + assert caps.contextual_segment_ids == [] + assert caps.contextual_taxonomy_version == "3.1" + assert caps.agentic_capabilities is None + + def test_with_standard_segments(self): + caps = AudienceCapabilities(standard_segment_ids=["3", "4"]) + assert caps.standard_segment_ids == ["3", "4"] + assert caps.standard_taxonomy_version == "1.1" + + def test_with_contextual_segments(self): + caps = AudienceCapabilities(contextual_segment_ids=["IAB1-2"]) + assert caps.contextual_segment_ids == ["IAB1-2"] + assert caps.contextual_taxonomy_version == "3.1" + + def test_with_agentic(self): + caps = AudienceCapabilities( + agentic_capabilities=AgenticCapabilities( + supported_signal_types=["identity", "contextual"], + consent_modes=["IAB-TCFv2"], + ) + ) + assert caps.agentic_capabilities is not None + assert caps.agentic_capabilities.spec_version == "draft-2026-01" + assert caps.agentic_capabilities.embedding_dim_range == (256, 1024) + + def test_agentic_defaults(self): + """AgenticCapabilities() agentic-only fields default sensibly.""" + a = AgenticCapabilities() + assert a.supported_signal_types == [] + assert a.embedding_dim_range == (256, 1024) + assert a.spec_version == "draft-2026-01" + assert a.consent_modes == [] + + +# ============================================================================= +# AudienceRef + ComplianceContext validators +# ============================================================================= + + +class TestAudienceRef: + """Wire-format validators on AudienceRef.""" + + def test_standard_explicit(self): + ref = AudienceRef( + type="standard", + identifier="3-7", + taxonomy="iab-audience", + version="1.1", + source="explicit", + ) + assert ref.confidence is None + assert ref.compliance_context is None + + def test_resolved_with_confidence(self): + ref = AudienceRef( + type="contextual", + identifier="IAB1-2", + taxonomy="iab-content", + version="3.1", + source="resolved", + confidence=0.92, + ) + assert ref.confidence == 0.92 + + def test_explicit_with_confidence_rejected(self): + """confidence MUST be None when source='explicit' (wire spec rule).""" + with pytest.raises(ValueError, match="confidence must be None"): + AudienceRef( + type="standard", + identifier="3-7", + taxonomy="iab-audience", + version="1.1", + source="explicit", + confidence=0.9, + ) + + def test_agentic_requires_compliance_context(self): + """type='agentic' MUST carry a compliance_context (wire spec rule).""" + with pytest.raises(ValueError, match="compliance_context is required"): + AudienceRef( + type="agentic", + identifier="emb://buyer.example.com/auds/x", + taxonomy="agentic-audiences", + version="draft-2026-01", + source="explicit", + ) + + def test_agentic_with_compliance_context(self): + ref = AudienceRef( + type="agentic", + identifier="emb://buyer.example.com/auds/x", + taxonomy="agentic-audiences", + version="draft-2026-01", + source="explicit", + compliance_context=ComplianceContext( + jurisdiction="US", + consent_framework="IAB-TCFv2", + ), + ) + assert ref.compliance_context is not None + assert ref.compliance_context.jurisdiction == "US" + + def test_compliance_context_required_fields(self): + cc = ComplianceContext(jurisdiction="EU", consent_framework="GPP") + assert cc.consent_string_ref is None + assert cc.attestation is None + + +# ============================================================================= +# Package legacy migration shim +# ============================================================================= + + +def _basic_package_kwargs(**overrides): + """Minimum-required kwargs for a Package, with overrides.""" + base = { + "package_id": "pkg-test", + "name": "Test Pkg", + "layer": PackageLayer.CURATED, + "base_price": 20.0, + "floor_price": 10.0, + } + base.update(overrides) + return base + + +class TestLegacyMigrationShim: + """`audience_segment_ids: list[str]` -> typed AudienceCapabilities.""" + + def test_legacy_input_migrates(self, caplog): + """Legacy flat field rewrites to capabilities, AT 1.1, INFO log.""" + with caplog.at_level(logging.INFO, logger="ad_seller.audience.migration"): + pkg = Package( + **_basic_package_kwargs(audience_segment_ids=["3", "4"]) + ) + + assert isinstance(pkg.audience_capabilities, AudienceCapabilities) + assert pkg.audience_capabilities.standard_segment_ids == ["3", "4"] + assert pkg.audience_capabilities.standard_taxonomy_version == "1.1" + # Legacy field is gone -- no shadow attribute survives migration. + assert not hasattr(pkg, "audience_segment_ids") + # Migration was logged at INFO on the migration logger. + assert any( + "Migrated legacy audience_segment_ids" in r.message + for r in caplog.records + ) + + def test_legacy_empty_list_migrates(self): + """Empty legacy list still migrates -- preserves emptiness.""" + pkg = Package(**_basic_package_kwargs(audience_segment_ids=[])) + assert pkg.audience_capabilities.standard_segment_ids == [] + assert pkg.audience_capabilities.standard_taxonomy_version == "1.1" + + def test_new_shape_passthrough(self): + """New typed input is preserved byte-for-byte.""" + caps = AudienceCapabilities( + standard_segment_ids=["5", "6"], + contextual_segment_ids=["IAB19"], + agentic_capabilities=AgenticCapabilities( + supported_signal_types=["identity"] + ), + ) + pkg = Package(**_basic_package_kwargs(audience_capabilities=caps)) + assert pkg.audience_capabilities.standard_segment_ids == ["5", "6"] + assert pkg.audience_capabilities.contextual_segment_ids == ["IAB19"] + assert pkg.audience_capabilities.agentic_capabilities is not None + assert ( + pkg.audience_capabilities.agentic_capabilities.supported_signal_types + == ["identity"] + ) + + def test_both_fields_caps_wins(self, caplog): + """If caller sends both, audience_capabilities wins; legacy dropped.""" + explicit = AudienceCapabilities(standard_segment_ids=["99"]) + with caplog.at_level(logging.INFO, logger="ad_seller.audience.migration"): + pkg = Package( + **_basic_package_kwargs( + audience_segment_ids=["3", "4"], + audience_capabilities=explicit, + ) + ) + assert pkg.audience_capabilities.standard_segment_ids == ["99"] + # Drop is logged. + assert any("Dropping legacy" in r.message for r in caplog.records) + + def test_default_no_audience_input(self): + """Package with no audience input gets default AudienceCapabilities().""" + pkg = Package(**_basic_package_kwargs()) + assert isinstance(pkg.audience_capabilities, AudienceCapabilities) + assert pkg.audience_capabilities.standard_segment_ids == [] + + def test_dict_input_legacy_migrates(self): + """Dict input (e.g., from SQLite row) with legacy field migrates.""" + data = { + "package_id": "pkg-from-db", + "name": "Old Row", + "layer": "curated", + "status": "active", + "base_price": 15.0, + "floor_price": 8.0, + "audience_segment_ids": ["3", "4", "5"], + } + pkg = Package(**data) + assert pkg.audience_capabilities.standard_segment_ids == ["3", "4", "5"] + + +# ============================================================================= +# Round-trip via model_dump +# ============================================================================= + + +class TestRoundTrip: + """`Package` survives serialize/deserialize cycle.""" + + def test_roundtrip_legacy_then_dump(self): + """Legacy in -> typed out -> dump -> reconstruct -> still typed.""" + pkg = Package( + **_basic_package_kwargs(audience_segment_ids=["3", "4"]) + ) + data = pkg.model_dump(mode="json") + # Dumped form is the new typed shape, not the legacy field. + assert "audience_capabilities" in data + assert "audience_segment_ids" not in data + assert data["audience_capabilities"]["standard_segment_ids"] == ["3", "4"] + # Reconstruct. + pkg2 = Package(**data) + assert pkg2.audience_capabilities.standard_segment_ids == ["3", "4"] + + def test_roundtrip_with_agentic(self): + caps = AudienceCapabilities( + standard_segment_ids=["10"], + agentic_capabilities=AgenticCapabilities( + supported_signal_types=["identity", "reinforcement"], + consent_modes=["IAB-TCFv2", "advertiser-1p"], + ), + ) + pkg = Package(**_basic_package_kwargs(audience_capabilities=caps)) + data = pkg.model_dump(mode="json") + pkg2 = Package(**data) + assert pkg2.audience_capabilities.agentic_capabilities is not None + assert pkg2.audience_capabilities.agentic_capabilities.supported_signal_types == [ + "identity", + "reinforcement", + ] + + +# ============================================================================= +# Public vs Authenticated views +# ============================================================================= + + +@pytest.fixture +def pricing_engine(): + config = TieredPricingConfig(seller_organization_id="test-seller") + return PricingRulesEngine(config=config) + + +@pytest.fixture +def mock_storage(): + return AsyncMock() + + +@pytest.fixture +def service(mock_storage, pricing_engine): + return MediaKitService(storage=mock_storage, pricing_engine=pricing_engine) + + +def _authenticated_buyer_context() -> BuyerContext: + """Build a buyer context whose effective_tier resolves to authenticated.""" + return BuyerContext( + identity=BuyerIdentity(api_key="test", agency_id="agcy-1"), + ) + + +class TestPublicViewExcludesSegmentLists: + """`PublicPackageView` is capability metadata only -- no segment lists.""" + + def test_public_view_no_segment_lists(self, service): + caps = AudienceCapabilities( + standard_segment_ids=["3", "4"], + contextual_segment_ids=["IAB19"], + agentic_capabilities=AgenticCapabilities( + supported_signal_types=["identity"] + ), + ) + pkg = Package( + **_basic_package_kwargs( + status=PackageStatus.ACTIVE, + audience_capabilities=caps, + ) + ) + view = service._to_public_view(pkg) + assert isinstance(view, PublicPackageView) + # The audience_capabilities on the public view is the public summary + # type -- versions + supports flags only. + assert isinstance(view.audience_capabilities, AudienceCapabilityPublicSummary) + # Most importantly: no segment lists exposed at this tier. + dumped = view.model_dump() + assert "standard_segment_ids" not in dumped["audience_capabilities"] + assert "contextual_segment_ids" not in dumped["audience_capabilities"] + # Capability metadata is exposed. + assert dumped["audience_capabilities"]["supports_standard"] is True + assert dumped["audience_capabilities"]["supports_contextual"] is True + assert dumped["audience_capabilities"]["supports_agentic"] is True + assert dumped["audience_capabilities"]["standard_taxonomy_version"] == "1.1" + assert dumped["audience_capabilities"]["contextual_taxonomy_version"] == "3.1" + assert ( + dumped["audience_capabilities"]["agentic_spec_version"] == "draft-2026-01" + ) + + def test_public_view_empty_capabilities(self, service): + """A package with no audience config still produces a clean summary.""" + pkg = Package(**_basic_package_kwargs(status=PackageStatus.ACTIVE)) + view = service._to_public_view(pkg) + assert view.audience_capabilities.supports_standard is False + assert view.audience_capabilities.supports_contextual is False + assert view.audience_capabilities.supports_agentic is False + assert view.audience_capabilities.agentic_spec_version is None + + +class TestAuthenticatedViewIncludesCapabilities: + """`AuthenticatedPackageView` exposes the full typed capability object.""" + + def test_authenticated_view_includes_full_capability(self, service): + caps = AudienceCapabilities( + standard_segment_ids=["3", "4", "5"], + contextual_segment_ids=["IAB1-2"], + agentic_capabilities=AgenticCapabilities( + supported_signal_types=["identity"] + ), + ) + pkg = Package( + **_basic_package_kwargs( + status=PackageStatus.ACTIVE, + audience_capabilities=caps, + ) + ) + view = service._to_authenticated_view(pkg, _authenticated_buyer_context()) + + assert isinstance(view, AuthenticatedPackageView) + # The authenticated view carries the full AudienceCapabilities object. + assert isinstance(view.audience_capabilities, AudienceCapabilities) + assert view.audience_capabilities.standard_segment_ids == ["3", "4", "5"] + assert view.audience_capabilities.contextual_segment_ids == ["IAB1-2"] + assert view.audience_capabilities.agentic_capabilities is not None + + def test_authenticated_view_dump_has_segment_lists(self, service): + caps = AudienceCapabilities(standard_segment_ids=["7", "8"]) + pkg = Package( + **_basic_package_kwargs( + status=PackageStatus.ACTIVE, + audience_capabilities=caps, + ) + ) + view = service._to_authenticated_view(pkg, _authenticated_buyer_context()) + dumped = view.model_dump() + # Segment lists ARE exposed at this tier. + assert dumped["audience_capabilities"]["standard_segment_ids"] == ["7", "8"] diff --git a/tests/unit/test_audience_plan_validation.py b/tests/unit/test_audience_plan_validation.py new file mode 100644 index 0000000..19fc1e7 --- /dev/null +++ b/tests/unit/test_audience_plan_validation.py @@ -0,0 +1,586 @@ +# Author: Green Mountain Systems AI Inc. +# Donated to IAB Tech Lab + +"""Unit tests for audience-plan validation pieces (bead ar-sn8f). + +Covers proposal §5.7 layers 2-3 + §5.1 Step 2: + +- B: structured `audience_plan_unsupported` error from + `validate_audience_plan()` and its surface on `POST /api/v1/deals`. +- C: `validate_audience` hard-rejects on zero standard / contextual overlap + and remains a soft-warn on low agentic match scores. +- D: `honor_audience_plan_snapshot()` returns the frozen snapshot and + emits a structured warning when current capabilities have shrunk. +""" + +from __future__ import annotations + +import asyncio +import logging +import sys +from types import ModuleType + +import pytest + +# Stub broken flow modules before importing other ad_seller bits. +_broken_flows = [ + "ad_seller.flows.discovery_inquiry_flow", + "ad_seller.flows.execution_activation_flow", +] +for _mod_name in _broken_flows: + if _mod_name not in sys.modules: + _stub = ModuleType(_mod_name) + _cls_name = ( + _mod_name.rsplit(".", 1)[-1].replace("_", " ").title().replace(" ", "") + ) + setattr(_stub, _cls_name, type(_cls_name, (), {})) + sys.modules[_mod_name] = _stub + +import httpx # noqa: E402 +from httpx import ASGITransport # noqa: E402 + +from ad_seller.flows.proposal_handling_flow import ( # noqa: E402 + ProposalHandlingFlow, + ProposalState, +) +from ad_seller.interfaces.api.main import ( # noqa: E402 + _get_optional_api_key_record, + app, +) +from ad_seller.models.audience_capabilities import ( # noqa: E402 + AgenticCapabilities, + AgenticCapabilityFlag, + AudienceCapabilities, + CapabilityAudienceBlock, + MaxRefsPerRole, + TaxonomyLockHashes, +) +from ad_seller.models.flow_state import ExecutionStatus # noqa: E402 +from ad_seller.models.media_kit import ( # noqa: E402 + Package, + PackageLayer, + PackageStatus, +) +from ad_seller.services.audience_plan_validator import ( # noqa: E402 + validate_audience_plan, +) +from ad_seller.services.fulfillment import ( # noqa: E402 + honor_audience_plan_snapshot, +) + + +# ============================================================================= +# Helpers +# ============================================================================= + + +def _caps_block( + *, + agentic_supported: bool = False, + supports_extensions: bool = False, + supports_constraints: bool = True, + supports_exclusions: bool = False, + standard_versions: list[str] | None = None, + contextual_versions: list[str] | None = None, + max_refs: MaxRefsPerRole | None = None, +) -> CapabilityAudienceBlock: + """Build a CapabilityAudienceBlock without touching the lock file.""" + + return CapabilityAudienceBlock( + schema_version="1", + standard_taxonomy_versions=standard_versions or ["1.1"], + contextual_taxonomy_versions=contextual_versions or ["3.1"], + agentic=AgenticCapabilityFlag(supported=agentic_supported), + supports_constraints=supports_constraints, + supports_extensions=supports_extensions, + supports_exclusions=supports_exclusions, + max_refs_per_role=max_refs or MaxRefsPerRole(), + taxonomy_lock_hashes=TaxonomyLockHashes( + audience="sha256:" + "a" * 64, + content="sha256:" + "b" * 64, + ), + ) + + +def _ref( + type_: str, + ident: str, + *, + version: str | None = None, + source: str = "explicit", + confidence: float | None = None, + compliance: dict | None = None, +) -> dict: + """Build a wire-shape AudienceRef dict.""" + + taxonomy = { + "standard": "iab-audience", + "contextual": "iab-content", + "agentic": "agentic-audiences", + }[type_] + default_version = { + "standard": "1.1", + "contextual": "3.1", + "agentic": "draft-2026-01", + }[type_] + ref: dict = { + "type": type_, + "identifier": ident, + "taxonomy": taxonomy, + "version": version or default_version, + "source": source, + "confidence": confidence, + } + if type_ == "agentic": + ref["compliance_context"] = compliance or { + "jurisdiction": "US", + "consent_framework": "IAB-TCFv2", + } + else: + ref["compliance_context"] = None + return ref + + +def _make_package( + *, + package_id: str, + standard_segment_ids: list[str] | None = None, + contextual_segment_ids: list[str] | None = None, +) -> Package: + return Package( + package_id=package_id, + name=f"Pkg {package_id}", + description="test", + layer=PackageLayer.CURATED, + status=PackageStatus.ACTIVE, + base_price=20.0, + floor_price=10.0, + audience_capabilities=AudienceCapabilities( + standard_segment_ids=standard_segment_ids or [], + contextual_segment_ids=contextual_segment_ids or [], + ), + ) + + +# ============================================================================= +# B: validate_audience_plan() against capability block +# ============================================================================= + + +class TestValidateAudiencePlan: + """`audience_plan_unsupported` structured error production.""" + + def test_supported_plan_returns_empty(self): + plan = { + "primary": _ref("standard", "3-7"), + "constraints": [_ref("contextual", "IAB1-2")], + } + caps = _caps_block(supports_constraints=True) + assert validate_audience_plan(plan, caps) == [] + + def test_extensions_role_unsupported(self): + plan = { + "primary": _ref("standard", "3-7"), + "extensions": [_ref("standard", "3-9")], + } + caps = _caps_block(supports_extensions=False) + result = validate_audience_plan(plan, caps) + assert any( + r["path"] == "extensions[0]" + and "extensions not supported" in r["reason"] + for r in result + ) + + def test_primary_taxonomy_version_unsupported(self): + plan = {"primary": _ref("standard", "3-7", version="3.2")} + caps = _caps_block(standard_versions=["1.1"]) + result = validate_audience_plan(plan, caps) + assert len(result) >= 1 + assert result[0]["path"] == "primary.taxonomy" + assert "version" in result[0]["reason"] + + def test_unsupported_constraint_taxonomy_version(self): + plan = { + "primary": _ref("standard", "3-7"), + "constraints": [_ref("contextual", "IAB1-2", version="2.0")], + } + caps = _caps_block( + supports_constraints=True, contextual_versions=["3.1"] + ) + result = validate_audience_plan(plan, caps) + assert any( + r["path"] == "constraints[0].taxonomy" for r in result + ) + + def test_agentic_in_primary_when_unsupported(self): + plan = {"primary": _ref("agentic", "emb://x")} + caps = _caps_block(agentic_supported=False) + result = validate_audience_plan(plan, caps) + assert any( + r["path"] == "primary.taxonomy" + and "agentic" in r["reason"] + for r in result + ) + + def test_empty_plan_is_supported(self): + assert validate_audience_plan(None, _caps_block()) == [] + assert validate_audience_plan({}, _caps_block()) == [] + + def test_exclusions_role_unsupported(self): + plan = { + "primary": _ref("standard", "3-7"), + "exclusions": [_ref("standard", "3-12")], + } + caps = _caps_block(supports_exclusions=False) + result = validate_audience_plan(plan, caps) + assert any(r["path"] == "exclusions[0]" for r in result) + + def test_cardinality_cap_exceeded(self): + # Default max_refs_per_role.constraints = 3 + plan = { + "primary": _ref("standard", "3-7"), + "constraints": [ + _ref("contextual", f"IAB1-{i}") for i in range(5) + ], + } + caps = _caps_block(supports_constraints=True) + result = validate_audience_plan(plan, caps) + assert any( + "exceeds max_refs_per_role" in r["reason"] for r in result + ) + + +# ============================================================================= +# C: ProposalHandlingFlow.validate_audience hard rejects +# ============================================================================= + + +def _run_validate(flow: ProposalHandlingFlow): + """Run the validate_audience coroutine directly.""" + + asyncio.get_event_loop().run_until_complete(flow.validate_audience()) + + +def _build_flow(packages: dict | None = None) -> ProposalHandlingFlow: + """Build a minimally-initialized ProposalHandlingFlow for direct method calls. + + CrewAI's Flow.__init__ wires required-field state via Pydantic validation + and SellerFlowState has required fields. Rather than fight that, we + bypass `Flow.__init__` and seed only the attributes `validate_audience` + actually touches. + """ + + import threading + + flow = ProposalHandlingFlow.__new__(ProposalHandlingFlow) + flow._settings = None + flow._audience_validation = {} + flow._packages_for_audience_validation = packages or {} + flow._state_lock = threading.Lock() + state = ProposalState( + flow_id="test-flow", + flow_type="proposal_handling", + seller_organization_id="test", + seller_name="Test", + ) + state.proposal_id = "p1" + flow._state = state + return flow + + +@pytest.fixture +def flow_with_pkgs(): + """Build a flow with a known package list and standard product.""" + + return _build_flow( + packages={ + "pkg-a": _make_package( + package_id="pkg-a", + standard_segment_ids=["3-7", "3-12"], + contextual_segment_ids=["IAB1-2"], + ), + } + ) + + +class TestValidateAudienceHardRejects: + """Proposal §5.7 layer 3: standard / contextual zero-overlap = hard reject.""" + + def test_standard_zero_overlap_hard_rejects(self, flow_with_pkgs): + flow = flow_with_pkgs + flow.state.proposal_data = { + "product_id": "prod-x", + "audience_plan": { + "primary": _ref("standard", "9-99"), + }, + } + _run_validate(flow) + assert flow.state.status == ExecutionStatus.FAILED + assert any( + "zero overlap" in e and "standard" in e for e in flow.state.errors + ) + + def test_contextual_zero_overlap_hard_rejects(self, flow_with_pkgs): + flow = flow_with_pkgs + flow.state.proposal_data = { + "product_id": "prod-x", + "audience_plan": { + "primary": _ref("standard", "3-7"), # standard ok + "constraints": [_ref("contextual", "IAB99-99")], + }, + } + _run_validate(flow) + assert flow.state.status == ExecutionStatus.FAILED + assert any( + "zero overlap" in e and "contextual" in e for e in flow.state.errors + ) + + def test_standard_overlap_does_not_hard_reject(self, flow_with_pkgs): + flow = flow_with_pkgs + flow.state.proposal_data = { + "product_id": "prod-x", + "audience_plan": {"primary": _ref("standard", "3-7")}, + } + _run_validate(flow) + assert flow.state.status != ExecutionStatus.FAILED + + def test_low_agentic_match_does_not_hard_reject(self, flow_with_pkgs): + # Per bead ar-sn8f: agentic refs are SOFT WARN, not hard reject, + # because the score is opinion (mock-quality in Epic 1). + flow = flow_with_pkgs + flow.state.proposal_data = { + "product_id": "prod-x", + "audience_plan": { + "primary": _ref("standard", "3-7"), # standard ok -> no hard reject + "extensions": [_ref("agentic", "emb://low-score-anything")], + }, + } + _run_validate(flow) + # Low agentic doesn't fail the flow on the hard-reject path. + assert flow.state.status != ExecutionStatus.FAILED + + def test_no_packages_falls_back_to_soft_warn(self): + # When the seller has no packages registered, hard reject defers + # to the existing soft-warn UCP path. + flow = _build_flow(packages=None) + flow.state.proposal_id = "p2" + flow.state.proposal_data = { + "product_id": "prod-x", + "audience_plan": {"primary": _ref("standard", "9-99")}, + } + _run_validate(flow) + # Should NOT fail -- fallback to existing path. + assert flow.state.status != ExecutionStatus.FAILED + + +# ============================================================================= +# D: honor_audience_plan_snapshot helper +# ============================================================================= + + +class TestHonorAudienceSnapshot: + """Snapshot is honored even when current capabilities have shrunk.""" + + def test_returns_frozen_snapshot(self): + snapshot = { + "schema_version": "1", + "audience_plan_id": "sha256:" + "f" * 64, + "primary": _ref("standard", "3-7"), + "extensions": [_ref("agentic", "emb://x")], + } + deal_record = {"audience_plan_snapshot": snapshot} + # Current caps are weaker (no extensions, no agentic). + current = _caps_block( + agentic_supported=False, supports_extensions=False + ) + result = honor_audience_plan_snapshot( + "DEAL-X", deal_record, current + ) + # Snapshot returned exactly -- not rewritten. + assert result == snapshot + + def test_logs_warning_on_capability_degradation(self, caplog): + snapshot = { + "audience_plan_id": "sha256:" + "1" * 64, + "primary": _ref("standard", "3-7"), + "extensions": [_ref("agentic", "emb://x")], + } + deal_record = {"audience_plan_snapshot": snapshot} + current = _caps_block( + agentic_supported=False, supports_extensions=False + ) + + with caplog.at_level(logging.WARNING, logger="ad_seller.services.fulfillment"): + honor_audience_plan_snapshot("DEAL-Y", deal_record, current) + + assert any( + "capabilities degraded" in record.message + and "DEAL-Y" in record.message + for record in caplog.records + ) + + def test_no_warning_when_caps_still_match(self, caplog): + snapshot = { + "audience_plan_id": "sha256:" + "2" * 64, + "primary": _ref("standard", "3-7"), + } + deal_record = {"audience_plan_snapshot": snapshot} + current = _caps_block() + + with caplog.at_level(logging.WARNING, logger="ad_seller.services.fulfillment"): + honor_audience_plan_snapshot("DEAL-Z", deal_record, current) + + assert not any( + "capabilities degraded" in record.message for record in caplog.records + ) + + def test_returns_none_when_no_snapshot(self): + result = honor_audience_plan_snapshot( + "DEAL-NONE", {"other": "data"}, _caps_block() + ) + assert result is None + + def test_returns_none_when_no_deal_record(self): + assert ( + honor_audience_plan_snapshot("DEAL-MISSING", None, _caps_block()) + is None + ) + + +# ============================================================================= +# Endpoint surface: POST /api/v1/deals with audience_plan +# ============================================================================= + + +@pytest.fixture +def http_client(): + """httpx AsyncClient with FastAPI dependency overrides.""" + + from datetime import datetime, timedelta + + app.dependency_overrides[_get_optional_api_key_record] = lambda: None + transport = ASGITransport(app=app) + c = httpx.AsyncClient(transport=transport, base_url="http://test") + # Build a minimal in-memory storage for the deal-booking happy path. + store: dict = {} + storage = type( + "_FakeStorage", + (), + { + "get_quote": staticmethod( + lambda qid: _async_return(store.get(f"quote:{qid}")) + ), + "set_quote": staticmethod( + lambda qid, data, ttl=None: _async_set(store, f"quote:{qid}", data) + ), + "get_deal": staticmethod( + lambda did: _async_return(store.get(f"deal:{did}")) + ), + "set_deal": staticmethod( + lambda did, data: _async_set(store, f"deal:{did}", data) + ), + "_store": store, + }, + )() + quote_id = "qt-test-audplan" + store[f"quote:{quote_id}"] = { + "quote_id": quote_id, + "status": "available", + "deal_type": "PD", + "product": { + "product_id": "ctv-premium", + "name": "Premium CTV", + "inventory_type": "ctv", + }, + "pricing": { + "base_cpm": 30.0, + "tier_discount_pct": 0.0, + "volume_discount_pct": 0.0, + "final_cpm": 30.0, + "currency": "USD", + "pricing_model": "cpm", + "rationale": "test", + }, + "terms": { + "impressions": 1000000, + "flight_start": "2026-04-01", + "flight_end": "2026-04-30", + "guaranteed": False, + }, + "buyer_tier": "public", + "expires_at": (datetime.utcnow() + timedelta(hours=24)).isoformat() + "Z", + "created_at": datetime.utcnow().isoformat() + "Z", + } + yield c, storage, quote_id + app.dependency_overrides.clear() + + +def _async_return(value): + """Helper to wrap a value in an awaitable for AsyncMock-like behavior.""" + + async def _coro(): + return value + + return _coro() + + +def _async_set(store, key, value): + async def _coro(): + store[key] = value + + return _coro() + + +class TestBookDealAudiencePlanRejection: + """Endpoint-level: POST /api/v1/deals with unsupported audience_plan.""" + + @pytest.mark.asyncio + async def test_extensions_unsupported_returns_structured_400( + self, http_client + ): + from unittest.mock import patch + + client, storage, quote_id = http_client + with patch( + "ad_seller.storage.factory.get_storage", return_value=storage + ): + async with client as c: + resp = await c.post( + "/api/v1/deals", + json={ + "quote_id": quote_id, + "audience_plan": { + "primary": _ref("standard", "3-7"), + "extensions": [_ref("standard", "3-9")], + }, + }, + ) + assert resp.status_code == 400 + body = resp.json() + assert body["detail"]["error"] == "audience_plan_unsupported" + assert any( + r["path"] == "extensions[0]" for r in body["detail"]["unsupported"] + ) + + @pytest.mark.asyncio + async def test_supported_plan_books_successfully(self, http_client): + from unittest.mock import patch + + client, storage, quote_id = http_client + with patch( + "ad_seller.storage.factory.get_storage", return_value=storage + ): + async with client as c: + resp = await c.post( + "/api/v1/deals", + json={ + "quote_id": quote_id, + "audience_plan": { + "primary": _ref("standard", "3-7"), + "constraints": [_ref("contextual", "IAB1-2")], + }, + }, + ) + assert resp.status_code == 200 + data = resp.json() + assert data["deal_id"].startswith("DEMO-") diff --git a/tests/unit/test_capability_audience_block.py b/tests/unit/test_capability_audience_block.py new file mode 100644 index 0000000..1236d9e --- /dev/null +++ b/tests/unit/test_capability_audience_block.py @@ -0,0 +1,449 @@ +# Author: Green Mountain Systems AI Inc. +# Donated to IAB Tech Lab + +"""Unit tests for the `audience_capabilities` block on capability discovery. + +Covers proposal §5.7 layer 1 + §6 row 9 (bead ar-2sip): + +- The capability discovery response (`GET /.well-known/agent.json`) carries + the new `audience_capabilities` block. +- `taxonomy_lock_hashes` is loaded DYNAMICALLY from + `data/taxonomies/taxonomies.lock.json` -- never hard-coded. Verified by + pointing the loader at a fixture file with patched hashes. +- Demo defaults match the locked-in decisions: `schema_version="1"`, + `agentic.supported=False`, `supports_extensions=False`, + `supports_exclusions=False`, `supports_constraints=True`, + `max_refs_per_role=(1/3/0/0)`. +- The block is itself a valid `CapabilityAudienceBlock` (Pydantic-roundtrips). +- Existing `AgentCard` callers that don't ship the block still validate + (backward-compatible additive field). + +Tests use the loader's `lock_path=` injection rather than the cached default +path so they don't conflict with concurrent tests touching the same cache. +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path +from types import ModuleType +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +# Stub broken flow modules (pre-existing @listen() bugs with CrewAI version +# mismatch) before importing main, mirroring the pattern in +# test_quote_endpoints.py. +_broken_flows = [ + "ad_seller.flows.discovery_inquiry_flow", + "ad_seller.flows.execution_activation_flow", +] +for _mod_name in _broken_flows: + if _mod_name not in sys.modules: + _stub = ModuleType(_mod_name) + _cls_name = ( + _mod_name.rsplit(".", 1)[-1].replace("_", " ").title().replace(" ", "") + ) + setattr(_stub, _cls_name, type(_cls_name, (), {})) + sys.modules[_mod_name] = _stub + +import httpx # noqa: E402 +from httpx import ASGITransport # noqa: E402 + +from ad_seller.interfaces.api.main import ( # noqa: E402 + _get_optional_api_key_record, + app, +) +from ad_seller.models.agent_registry import AgentCard # noqa: E402 +from ad_seller.models.audience_capabilities import ( # noqa: E402 + AgenticCapabilityFlag, + CapabilityAudienceBlock, + MaxRefsPerRole, + TaxonomyLockHashes, + build_capability_audience_block, + load_taxonomy_lock_hashes, +) + + +# ============================================================================= +# Fixtures +# ============================================================================= + + +@pytest.fixture +def real_lock_path() -> Path: + """Absolute path to the vendored taxonomy lock file.""" + + # tests/unit/test_capability_audience_block.py -> project root + return ( + Path(__file__).resolve().parents[2] + / "data" + / "taxonomies" + / "taxonomies.lock.json" + ) + + +@pytest.fixture +def real_lock_data(real_lock_path: Path) -> dict: + """Parsed contents of the real lock file (read once per test).""" + + return json.loads(real_lock_path.read_text()) + + +@pytest.fixture +def fake_lock_path(tmp_path: Path) -> Path: + """Build a synthetic lock file with known hashes for drift-detection tests. + + Uses distinctive marker hashes so we can prove the block was sourced from + THIS file and not the real lock file (or any hard-coded constant). + """ + + payload = { + "schema_version": "1", + "audience": { + "version": "1.1", + "source": "test://audience", + "path": "audience-1.1/x.tsv", + "sha256": "deadbeef" * 8, # 64-char marker + "fetched_at": "2026-04-25T00:00:00Z", + "license": "CC-BY-3.0", + "license_url": "https://creativecommons.org/licenses/by/3.0/", + "format": "tsv", + }, + "content": { + "version": "3.1", + "source": "test://content", + "path": "content-3.1/y.tsv", + "sha256": "cafef00d" * 8, + "fetched_at": "2026-04-25T00:00:00Z", + "license": "CC-BY-3.0", + "license_url": "https://creativecommons.org/licenses/by/3.0/", + "format": "tsv", + }, + "agentic": { + "version": "draft-2026-01", + "spec_url": "test://agentic", + "source": "test://agentic", + "path": "agentic-audiences-draft-2026-01/spec", + "sha256": "0" * 64, + "fetched_at": "2026-04-25T00:00:00Z", + "license": "CC-BY-4.0", + "license_url_spec": "https://creativecommons.org/licenses/by/4.0/", + "license_url_impl": "https://www.apache.org/licenses/LICENSE-2.0", + "format": "directory", + "files": {}, + }, + } + path = tmp_path / "taxonomies.lock.json" + path.write_text(json.dumps(payload)) + return path + + +@pytest.fixture +def client(): + """httpx AsyncClient with FastAPI dependency overrides.""" + + app.dependency_overrides[_get_optional_api_key_record] = lambda: None + transport = ASGITransport(app=app) + c = httpx.AsyncClient(transport=transport, base_url="http://test") + yield c + app.dependency_overrides.clear() + + +# ============================================================================= +# CapabilityAudienceBlock model construction & defaults +# ============================================================================= + + +class TestCapabilityAudienceBlockDefaults: + """Demo / MVP defaults match the locked-in decisions in the proposal.""" + + def test_schema_version_is_one(self, real_lock_path): + block = build_capability_audience_block(lock_path=real_lock_path) + assert block.schema_version == "1" + + def test_agentic_unsupported_by_default(self, real_lock_path): + """The seller hasn't gained match support yet -- that's §11.""" + block = build_capability_audience_block(lock_path=real_lock_path) + assert block.agentic.supported is False + + def test_supports_constraints_true(self, real_lock_path): + """Seller can filter (filter implementation lands in §10).""" + block = build_capability_audience_block(lock_path=real_lock_path) + assert block.supports_constraints is True + + def test_supports_extensions_false(self, real_lock_path): + block = build_capability_audience_block(lock_path=real_lock_path) + assert block.supports_extensions is False + + def test_supports_exclusions_false(self, real_lock_path): + block = build_capability_audience_block(lock_path=real_lock_path) + assert block.supports_exclusions is False + + def test_max_refs_per_role_demo_defaults(self, real_lock_path): + block = build_capability_audience_block(lock_path=real_lock_path) + assert block.max_refs_per_role.primary == 1 + assert block.max_refs_per_role.constraints == 3 + assert block.max_refs_per_role.extensions == 0 + assert block.max_refs_per_role.exclusions == 0 + + def test_taxonomy_versions_from_lock_file( + self, real_lock_path, real_lock_data + ): + """Taxonomy versions in the block match the lock file -- not hard-coded.""" + block = build_capability_audience_block(lock_path=real_lock_path) + assert block.standard_taxonomy_versions == [ + real_lock_data["audience"]["version"] + ] + assert block.contextual_taxonomy_versions == [ + real_lock_data["content"]["version"] + ] + + +# ============================================================================= +# taxonomy_lock_hashes are loaded dynamically (NOT hard-coded) +# ============================================================================= + + +class TestTaxonomyLockHashesAreDynamic: + """taxonomy_lock_hashes must be sourced from the lock file at call time.""" + + def test_real_lock_file_hashes_match(self, real_lock_path, real_lock_data): + """Hashes in the block match what's in the real lock file.""" + block = build_capability_audience_block(lock_path=real_lock_path) + expected_audience = f"sha256:{real_lock_data['audience']['sha256']}" + expected_content = f"sha256:{real_lock_data['content']['sha256']}" + assert block.taxonomy_lock_hashes.audience == expected_audience + assert block.taxonomy_lock_hashes.content == expected_content + + def test_fake_lock_file_hashes_propagate(self, fake_lock_path): + """Pointing the loader at a different lock file changes the hashes. + + This is the proof that hashes are NOT hard-coded -- if they were, + the block would always emit the real-file hashes regardless of + which file we pointed the loader at. + """ + block = build_capability_audience_block(lock_path=fake_lock_path) + assert block.taxonomy_lock_hashes.audience == f"sha256:{'deadbeef' * 8}" + assert block.taxonomy_lock_hashes.content == f"sha256:{'cafef00d' * 8}" + + def test_load_taxonomy_lock_hashes_standalone(self, fake_lock_path): + """`load_taxonomy_lock_hashes()` is usable on its own and dynamic.""" + hashes = load_taxonomy_lock_hashes(fake_lock_path) + assert isinstance(hashes, TaxonomyLockHashes) + assert hashes.audience == f"sha256:{'deadbeef' * 8}" + assert hashes.content == f"sha256:{'cafef00d' * 8}" + + def test_lock_file_rewrite_picked_up(self, fake_lock_path): + """Rewriting the lock file invalidates the mtime-keyed cache.""" + first = load_taxonomy_lock_hashes(fake_lock_path) + assert first.audience == f"sha256:{'deadbeef' * 8}" + + # Rewrite with new hashes; bump mtime to force cache invalidation. + new_payload = json.loads(fake_lock_path.read_text()) + new_payload["audience"]["sha256"] = "ab" * 32 + new_payload["content"]["sha256"] = "cd" * 32 + fake_lock_path.write_text(json.dumps(new_payload)) + # On some filesystems mtime granularity is coarse -- bump explicitly. + import os + import time + + future = time.time() + 5 + os.utime(fake_lock_path, (future, future)) + + second = load_taxonomy_lock_hashes(fake_lock_path) + assert second.audience == f"sha256:{'ab' * 32}" + assert second.content == f"sha256:{'cd' * 32}" + + +# ============================================================================= +# Pydantic round-trip (model_dump -> model_validate) +# ============================================================================= + + +class TestCapabilityAudienceBlockRoundTrip: + """The block survives serialize/deserialize via Pydantic.""" + + def test_roundtrip_preserves_all_fields(self, real_lock_path): + block = build_capability_audience_block(lock_path=real_lock_path) + dumped = block.model_dump() + rehydrated = CapabilityAudienceBlock.model_validate(dumped) + assert rehydrated.schema_version == block.schema_version + assert ( + rehydrated.standard_taxonomy_versions == block.standard_taxonomy_versions + ) + assert ( + rehydrated.contextual_taxonomy_versions + == block.contextual_taxonomy_versions + ) + assert rehydrated.agentic.supported == block.agentic.supported + assert rehydrated.supports_constraints == block.supports_constraints + assert rehydrated.supports_extensions == block.supports_extensions + assert rehydrated.supports_exclusions == block.supports_exclusions + assert rehydrated.max_refs_per_role == block.max_refs_per_role + assert ( + rehydrated.taxonomy_lock_hashes.audience + == block.taxonomy_lock_hashes.audience + ) + assert ( + rehydrated.taxonomy_lock_hashes.content + == block.taxonomy_lock_hashes.content + ) + + def test_block_validates_from_wire_shape(self, real_lock_path, real_lock_data): + """Construct from the JSON wire shape -- proves wire-format conformance.""" + wire = { + "schema_version": "1", + "standard_taxonomy_versions": [real_lock_data["audience"]["version"]], + "contextual_taxonomy_versions": [real_lock_data["content"]["version"]], + "agentic": {"supported": False}, + "supports_constraints": True, + "supports_extensions": False, + "supports_exclusions": False, + "max_refs_per_role": { + "primary": 1, + "constraints": 3, + "extensions": 0, + "exclusions": 0, + }, + "taxonomy_lock_hashes": { + "audience": f"sha256:{real_lock_data['audience']['sha256']}", + "content": f"sha256:{real_lock_data['content']['sha256']}", + }, + } + block = CapabilityAudienceBlock.model_validate(wire) + assert block.schema_version == "1" + assert block.agentic.supported is False + assert isinstance(block.max_refs_per_role, MaxRefsPerRole) + assert isinstance(block.agentic, AgenticCapabilityFlag) + + +# ============================================================================= +# AgentCard backward-compatibility (block is optional) +# ============================================================================= + + +class TestAgentCardBackwardCompatibility: + """A missing `audience_capabilities` block is treated as legacy. + + The wire spec says the block is optional; older sellers that don't ship + it should still validate as AgentCards (the buyer treats them as legacy + when the field is missing). This test pins that contract. + """ + + def test_agent_card_without_block_validates(self): + from ad_seller.models.agent_registry import ( + AgentAuthentication, + AgentCard, + AgentProvider, + ) + + card = AgentCard( + name="Legacy Seller", + description="Pre-§9 seller", + url="https://legacy.example.com", + provider=AgentProvider(name="Legacy Co", url="https://legacy.example.com"), + authentication=AgentAuthentication(schemes=["api_key"]), + ) + # Block is None on a legacy card. + assert card.audience_capabilities is None + # And the dump survives a round-trip. + rehydrated = AgentCard.model_validate(card.model_dump()) + assert rehydrated.audience_capabilities is None + + +# ============================================================================= +# Live capability discovery endpoint (GET /.well-known/agent.json) +# ============================================================================= + + +def _mock_product_setup_flow(products_dict): + """Mock ProductSetupFlow whose state has the given products.""" + + mock_flow = MagicMock() + mock_flow.state = MagicMock() + mock_flow.state.products = products_dict + mock_flow.kickoff = AsyncMock() + return mock_flow + + +class TestAgentCardEndpointEmitsBlock: + """`GET /.well-known/agent.json` carries the new audience_capabilities block.""" + + async def test_endpoint_includes_audience_capabilities(self, client): + with patch( + "ad_seller.flows.ProductSetupFlow", + return_value=_mock_product_setup_flow({}), + ): + resp = await client.get("/.well-known/agent.json") + assert resp.status_code == 200 + body = resp.json() + assert "audience_capabilities" in body + ac = body["audience_capabilities"] + # Demo defaults end-to-end through the live endpoint. + assert ac["schema_version"] == "1" + assert ac["agentic"]["supported"] is False + assert ac["supports_constraints"] is True + assert ac["supports_extensions"] is False + assert ac["supports_exclusions"] is False + assert ac["max_refs_per_role"] == { + "primary": 1, + "constraints": 3, + "extensions": 0, + "exclusions": 0, + } + # Taxonomy_lock_hashes are present and shaped correctly. + assert ac["taxonomy_lock_hashes"]["audience"].startswith("sha256:") + assert ac["taxonomy_lock_hashes"]["content"].startswith("sha256:") + # And they match what's on disk in the real lock file (i.e., not + # hard-coded somewhere upstream of the endpoint). + from pathlib import Path as _Path + + lock_path = ( + _Path(__file__).resolve().parents[2] + / "data" + / "taxonomies" + / "taxonomies.lock.json" + ) + lock = json.loads(lock_path.read_text()) + assert ( + ac["taxonomy_lock_hashes"]["audience"] + == f"sha256:{lock['audience']['sha256']}" + ) + assert ( + ac["taxonomy_lock_hashes"]["content"] + == f"sha256:{lock['content']['sha256']}" + ) + + async def test_endpoint_response_validates_as_agent_card(self, client): + """The full response still validates as an AgentCard -- no breakage.""" + with patch( + "ad_seller.flows.ProductSetupFlow", + return_value=_mock_product_setup_flow({}), + ): + resp = await client.get("/.well-known/agent.json") + assert resp.status_code == 200 + # Round-trip: response JSON -> AgentCard -> dump -> matches. + card = AgentCard.model_validate(resp.json()) + assert card.audience_capabilities is not None + assert isinstance(card.audience_capabilities, CapabilityAudienceBlock) + + async def test_endpoint_existing_fields_preserved(self, client): + """Pre-existing AgentCard fields are still emitted (no regression).""" + with patch( + "ad_seller.flows.ProductSetupFlow", + return_value=_mock_product_setup_flow({}), + ): + resp = await client.get("/.well-known/agent.json") + body = resp.json() + # Shape sanity -- these existed before this bead. + assert "name" in body + assert "url" in body + assert "version" in body + assert "provider" in body + assert "skills" in body + assert "authentication" in body + assert "capabilities" in body # the unrelated AgentCapabilities (protocols/streaming) + assert "inventory_types" in body + assert "supported_deal_types" in body diff --git a/tests/unit/test_deal_booking_endpoints.py b/tests/unit/test_deal_booking_endpoints.py index 7843091..478e199 100644 --- a/tests/unit/test_deal_booking_endpoints.py +++ b/tests/unit/test_deal_booking_endpoints.py @@ -281,8 +281,19 @@ async def test_full_quote_to_deal_flow(self, client, mock_storage): mock_flow.kickoff = AsyncMock() mock_flow.kickoff_async = AsyncMock() + # ar-0vtg: POST /api/v1/quotes now reads from `_get_static_product_catalog` + # rather than running ProductSetupFlow per request. The deal-booking + # endpoint still uses ProductSetupFlow, so we patch both. + catalog = { + "products": products, + "inventory_types": sorted({p.inventory_type for p in products.values()}), + } with ( patch("ad_seller.flows.ProductSetupFlow", return_value=mock_flow), + patch( + "ad_seller.interfaces.api.main._get_static_product_catalog", + return_value=catalog, + ), patch("ad_seller.storage.factory.get_storage", return_value=mock_storage), ): # Step 1: Create quote diff --git a/tests/unit/test_deal_booking_snapshot.py b/tests/unit/test_deal_booking_snapshot.py new file mode 100644 index 0000000..6691001 --- /dev/null +++ b/tests/unit/test_deal_booking_snapshot.py @@ -0,0 +1,404 @@ +# Author: Green Mountain Systems AI Inc. +# Donated to IAB Tech Lab + +"""Seller-side dual content-type acceptance + frozen plan snapshot at booking. + +Implements bead ar-y6ki (proposal §5.1 Step 2 + §5.6 + §6 row 14b). + +Coverage: +- POST /api/v1/deals accepts the legacy UCP content-type + (`application/vnd.ucp.embedding+json; v=1`) on requests carrying an + `audience_plan`. +- Same endpoint accepts the new IAB Agentic Audiences alias + (`application/vnd.iab.agentic-audiences+json; v=1`) on the same shape. +- Successful bookings carrying an `audience_plan` return a body with + `audience_plan_snapshot` + `audience_match_summary` per wire-format §6.5. +- The seller logs `audience_plan_id` at INFO via + `ad_seller.audience.booking` so the buyer-side log can be correlated. +- The minted deal_id corresponds to a stored deal record carrying the + frozen snapshot (proposal §5.1 Step 2 honor policy). +- Bookings without an `audience_plan` keep the legacy response shape + (no regression on the non-audience path). +""" + +import logging +import sys +from types import ModuleType +from unittest.mock import AsyncMock, patch + +import pytest + +# Stub broken flow modules (pre-existing @listen() bugs with CrewAI version +# mismatch). Mirrors the pattern in tests/unit/test_deal_booking_endpoints.py. +_broken_flows = [ + "ad_seller.flows.discovery_inquiry_flow", + "ad_seller.flows.execution_activation_flow", +] +for _mod_name in _broken_flows: + if _mod_name not in sys.modules: + _stub = ModuleType(_mod_name) + _cls_name = ( + _mod_name.rsplit(".", 1)[-1].replace("_", " ").title().replace(" ", "") + ) + setattr(_stub, _cls_name, type(_cls_name, (), {})) + sys.modules[_mod_name] = _stub + +from datetime import datetime, timedelta # noqa: E402 + +import httpx # noqa: E402 +from httpx import ASGITransport # noqa: E402 + +from ad_seller.interfaces.api.main import _get_optional_api_key_record, app # noqa: E402 + +# Wire-format media types per docs/api/audience_plan_wire_format.md §8. +_UCP = "application/vnd.ucp.embedding+json; v=1" +_AGENTIC = "application/vnd.iab.agentic-audiences+json; v=1" + +# Hash matching the buyer's `AudiencePlan.compute_id()` output for the +# fixture below. The seller's snapshot persists whatever hash the buyer +# sends -- the hash content is treated as opaque on the seller side, so the +# specific value here does not need to match the canonical algorithm. +_FIXTURE_PLAN_ID = "sha256:fixture-plan-id-for-snapshot-test" + + +def _make_available_quote(**overrides): + defaults = { + "quote_id": "qt-snap-1", + "status": "available", + "deal_type": "PD", + "product": { + "product_id": "ctv-premium-sports", + "name": "Premium CTV - Sports", + "inventory_type": "ctv", + }, + "pricing": { + "base_cpm": 35.0, + "tier_discount_pct": 15.0, + "volume_discount_pct": 5.0, + "final_cpm": 28.26, + "currency": "USD", + "pricing_model": "cpm", + "rationale": "Base $35.00 | -15% | $28.26", + }, + "terms": { + "impressions": 5000000, + "flight_start": "2026-04-01", + "flight_end": "2026-04-30", + "guaranteed": False, + }, + "buyer_tier": "advertiser", + "expires_at": (datetime.utcnow() + timedelta(hours=23)).isoformat() + "Z", + "created_at": datetime.utcnow().isoformat() + "Z", + } + defaults.update(overrides) + return defaults + + +def _make_audience_plan(plan_id: str = _FIXTURE_PLAN_ID) -> dict: + """A spec-shaped AudiencePlan with refs the default seller supports. + + Standard primary + contextual constraint + (empty) extensions / + exclusions. Default seller capabilities (`build_capability_audience_block()` + with no overrides) are `agentic_supported=False, supports_extensions=False, + supports_exclusions=False`, so a plan that includes extensions / agentic + refs would be rejected with `audience_plan_unsupported` before it reaches + the snapshot logic. The Path-A demo (proposal §5.3) targets the basic + standard+contextual mix; agentic-extension paths are exercised against + explicitly-overridden seller fixtures in §11's tests. + + Versions are pinned to the seller's taxonomy lock defaults (audience + 1.1, content 3.1) -- changing the lock would require updating these. + """ + + return { + "schema_version": "1", + "audience_plan_id": plan_id, + "primary": { + "type": "standard", + "identifier": "3-7", + "taxonomy": "iab-audience", + "version": "1.1", + "source": "explicit", + "confidence": None, + "compliance_context": None, + }, + "constraints": [ + { + "type": "contextual", + "identifier": "IAB1-2", + "taxonomy": "iab-content", + "version": "3.1", + "source": "resolved", + "confidence": 0.92, + "compliance_context": None, + } + ], + "extensions": [], + "exclusions": [], + "rationale": "Snapshot fixture", + } + + +@pytest.fixture +def mock_storage(): + store: dict = {} + storage = AsyncMock() + storage.get_quote = AsyncMock(side_effect=lambda qid: store.get(f"quote:{qid}")) + storage.set_quote = AsyncMock( + side_effect=lambda qid, data, ttl=86400: store.__setitem__( + f"quote:{qid}", data + ) + ) + storage.get_deal = AsyncMock(side_effect=lambda did: store.get(f"deal:{did}")) + storage.set_deal = AsyncMock( + side_effect=lambda did, data: store.__setitem__(f"deal:{did}", data) + ) + storage._store = store + return storage + + +@pytest.fixture +def client(mock_storage): + app.dependency_overrides[_get_optional_api_key_record] = lambda: None + transport = ASGITransport(app=app) + c = httpx.AsyncClient(transport=transport, base_url="http://test") + yield c + app.dependency_overrides.clear() + + +# --------------------------------------------------------------------------- +# Dual content-type acceptance (proposal §5.6) +# --------------------------------------------------------------------------- + + +class TestDualContentTypeAcceptance: + """Seller MUST accept both wire-format media types on /api/v1/deals.""" + + async def test_accepts_legacy_ucp_content_type(self, client, mock_storage): + quote = _make_available_quote(quote_id="qt-ucp") + mock_storage._store[f"quote:{quote['quote_id']}"] = quote + + with patch( + "ad_seller.storage.factory.get_storage", return_value=mock_storage + ): + resp = await client.post( + "/api/v1/deals", + json={ + "quote_id": quote["quote_id"], + "audience_plan": _make_audience_plan(), + }, + headers={"Content-Type": _UCP}, + ) + + assert resp.status_code == 200 + assert resp.json()["deal_id"].startswith("DEMO-") + + async def test_accepts_new_agentic_audiences_alias(self, client, mock_storage): + quote = _make_available_quote(quote_id="qt-agentic") + mock_storage._store[f"quote:{quote['quote_id']}"] = quote + + with patch( + "ad_seller.storage.factory.get_storage", return_value=mock_storage + ): + resp = await client.post( + "/api/v1/deals", + json={ + "quote_id": quote["quote_id"], + "audience_plan": _make_audience_plan(), + }, + headers={"Content-Type": _AGENTIC}, + ) + + assert resp.status_code == 200 + assert resp.json()["deal_id"].startswith("DEMO-") + + +# --------------------------------------------------------------------------- +# Frozen snapshot + audience_match_summary in the response (wire-format §6.5) +# --------------------------------------------------------------------------- + + +class TestSnapshotResponseShape: + """Successful audience-bearing booking returns the §6.5 shape.""" + + async def test_response_contains_audience_plan_snapshot( + self, client, mock_storage + ): + quote = _make_available_quote(quote_id="qt-snap-2") + mock_storage._store[f"quote:{quote['quote_id']}"] = quote + plan = _make_audience_plan() + + with patch( + "ad_seller.storage.factory.get_storage", return_value=mock_storage + ): + resp = await client.post( + "/api/v1/deals", + json={"quote_id": quote["quote_id"], "audience_plan": plan}, + headers={"Content-Type": _UCP}, + ) + + assert resp.status_code == 200 + body = resp.json() + # Snapshot is the buyer-supplied plan verbatim (proposal §5.1 Step 2). + assert body["audience_plan_snapshot"] == plan + # Hash echoed back so the buyer can verify cross-side parity. + assert body["audience_plan_snapshot"]["audience_plan_id"] == plan[ + "audience_plan_id" + ] + + async def test_response_contains_audience_match_summary( + self, client, mock_storage + ): + quote = _make_available_quote(quote_id="qt-snap-3") + mock_storage._store[f"quote:{quote['quote_id']}"] = quote + + with patch( + "ad_seller.storage.factory.get_storage", return_value=mock_storage + ): + resp = await client.post( + "/api/v1/deals", + json={ + "quote_id": quote["quote_id"], + "audience_plan": _make_audience_plan(), + }, + headers={"Content-Type": _UCP}, + ) + + body = resp.json() + summary = body["audience_match_summary"] + # Per-role keys all present, even when a list is empty (wire-format + # §6.5 receivers MUST treat absent arrays as empty; emitting them + # keeps the buyer's typed parser stable). + assert "primary" in summary + assert summary["primary"]["match"] in {"STRONG", "MODERATE", "WEAK", "NONE"} + assert 0.0 <= summary["primary"]["score"] <= 1.0 + assert isinstance(summary["constraints"], list) + assert isinstance(summary["extensions"], list) + assert isinstance(summary["exclusions"], list) + # Single constraint -> single MatchEntry. + assert len(summary["constraints"]) == 1 + assert summary["constraints"][0]["match"] in { + "STRONG", + "MODERATE", + "WEAK", + "NONE", + } + # Empty extensions / exclusions still emit empty arrays. + assert summary["extensions"] == [] + assert summary["exclusions"] == [] + + async def test_no_audience_plan_keeps_legacy_response_shape( + self, client, mock_storage + ): + """Bookings without an audience_plan still parse, no snapshot fields.""" + + quote = _make_available_quote(quote_id="qt-no-audience") + mock_storage._store[f"quote:{quote['quote_id']}"] = quote + + with patch( + "ad_seller.storage.factory.get_storage", return_value=mock_storage + ): + resp = await client.post( + "/api/v1/deals", + json={"quote_id": quote["quote_id"]}, + ) + + assert resp.status_code == 200 + body = resp.json() + # No audience plan -> legacy shape (no snapshot fields land on the + # response or on the persisted record). + assert "audience_plan_snapshot" not in body + assert "audience_match_summary" not in body + + +# --------------------------------------------------------------------------- +# Forensic logging (proposal §5.1 Step 2) +# --------------------------------------------------------------------------- + + +class TestPlanIdLogging: + """Seller logs audience_plan_id at booking for cross-side correlation.""" + + async def test_logs_audience_plan_id_at_info(self, client, mock_storage, caplog): + quote = _make_available_quote(quote_id="qt-log-1") + mock_storage._store[f"quote:{quote['quote_id']}"] = quote + plan = _make_audience_plan(plan_id="sha256:test-fixture-hash-abc") + + with patch( + "ad_seller.storage.factory.get_storage", return_value=mock_storage + ): + with caplog.at_level( + logging.INFO, logger="ad_seller.audience.booking" + ): + resp = await client.post( + "/api/v1/deals", + json={"quote_id": quote["quote_id"], "audience_plan": plan}, + ) + + assert resp.status_code == 200 + records = [ + r for r in caplog.records if r.name == "ad_seller.audience.booking" + ] + assert len(records) == 1 + msg = records[0].getMessage() + # Plan id, deal id, and quote id all surface for end-to-end correlation. + assert plan["audience_plan_id"] in msg + assert resp.json()["deal_id"] in msg + assert quote["quote_id"] in msg + + async def test_no_audience_plan_does_not_log( + self, client, mock_storage, caplog + ): + quote = _make_available_quote(quote_id="qt-log-2") + mock_storage._store[f"quote:{quote['quote_id']}"] = quote + + with patch( + "ad_seller.storage.factory.get_storage", return_value=mock_storage + ): + with caplog.at_level( + logging.INFO, logger="ad_seller.audience.booking" + ): + await client.post( + "/api/v1/deals", json={"quote_id": quote["quote_id"]} + ) + + assert [ + r for r in caplog.records if r.name == "ad_seller.audience.booking" + ] == [] + + +# --------------------------------------------------------------------------- +# Persisted snapshot is honored at fulfillment time +# --------------------------------------------------------------------------- + + +class TestSnapshotPersistence: + """The minted deal_id must point to a deal record carrying the snapshot.""" + + async def test_minted_deal_record_carries_snapshot( + self, client, mock_storage + ): + """Snapshot lands on the persisted deal record so + `honor_audience_plan_snapshot()` can read it post-booking.""" + + quote = _make_available_quote(quote_id="qt-persist") + mock_storage._store[f"quote:{quote['quote_id']}"] = quote + plan = _make_audience_plan() + + with patch( + "ad_seller.storage.factory.get_storage", return_value=mock_storage + ): + resp = await client.post( + "/api/v1/deals", + json={"quote_id": quote["quote_id"], "audience_plan": plan}, + ) + + deal_id = resp.json()["deal_id"] + stored = mock_storage._store[f"deal:{deal_id}"] + # Frozen snapshot is verbatim on the persisted record. + assert stored["audience_plan_snapshot"] == plan + # Match summary is also persisted alongside (single source of truth + # for fulfillment-time inspection). + assert "audience_match_summary" in stored + # Deal id is the cheap handle (per §5.1 Step 2). + assert stored["deal_id"] == deal_id diff --git a/tests/unit/test_endpoint_no_flow_kickoff.py b/tests/unit/test_endpoint_no_flow_kickoff.py new file mode 100644 index 0000000..cd5eee1 --- /dev/null +++ b/tests/unit/test_endpoint_no_flow_kickoff.py @@ -0,0 +1,250 @@ +# Author: Green Mountain Systems AI Inc. +# Donated to IAB Tech Lab + +"""Regression tests for ar-yet5: endpoints must not call sync ProductSetupFlow.kickoff(). + +In CrewAI 1.10.1, `Flow.kickoff()` is synchronous and returns `None`. +`await flow.kickoff()` raises `TypeError: object NoneType can't be used in +'await' expression`. The fix (mirroring origin's 3d8b69c for /products) is +`await flow.kickoff_async()`. + +These tests guard the invariant that no production endpoint reaches the +sync `.kickoff()` method via the autouse fixture below — if any endpoint +regresses to the broken pattern, the AssertionError trips immediately. +The hermetic POST /packages/sync test additionally exercises one of the +six previously-broken endpoints end-to-end with a mocked flow. + +Read endpoints (`GET /products`, `GET /products/{id}`, `GET /.well-known/agent.json`) +were separately fixed by ar-uwad to read from a static catalog instead of +running the flow at all; the same kickoff-call guard applies to them. +""" + +import sys +from types import ModuleType +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +# Stub broken flow modules (pre-existing @listen() bugs with CrewAI version mismatch). +# Same pattern used in test_deal_booking_endpoints.py. +_broken_flows = [ + "ad_seller.flows.discovery_inquiry_flow", + "ad_seller.flows.execution_activation_flow", +] +for _mod_name in _broken_flows: + if _mod_name not in sys.modules: + _stub = ModuleType(_mod_name) + _cls_name = _mod_name.rsplit(".", 1)[-1].replace("_", " ").title().replace(" ", "") + setattr(_stub, _cls_name, type(_cls_name, (), {})) + sys.modules[_mod_name] = _stub + +import httpx # noqa: E402 +from httpx import ASGITransport # noqa: E402 + +from ad_seller.interfaces.api import main as api_main # noqa: E402 +from ad_seller.interfaces.api.main import app # noqa: E402 + + +@pytest.fixture +def client(): + transport = ASGITransport(app=app) + return httpx.AsyncClient(transport=transport, base_url="http://test") + + +@pytest.fixture(autouse=True) +def _reset_catalog_cache(): + """Reset the static catalog cache between tests so each test sees a fresh state.""" + api_main._STATIC_PRODUCT_CATALOG = None + yield + api_main._STATIC_PRODUCT_CATALOG = None + + +@pytest.fixture(autouse=True) +def _fail_if_flow_kickoff_called(monkeypatch): + """Hard-fail the test if any code path calls ProductSetupFlow().kickoff().""" + + def _boom(*args, **kwargs): + raise AssertionError( + "ProductSetupFlow.kickoff() was called from a read endpoint. " + "Read endpoints must use the cached static catalog (see ar-uwad)." + ) + + # Patch on the class so any code path that constructs ProductSetupFlow + # and calls kickoff() trips the assertion. + from ad_seller.flows.product_setup_flow import ProductSetupFlow + + monkeypatch.setattr(ProductSetupFlow, "kickoff", _boom) + + +async def test_health_returns_200(client): + """Sanity: /health is unaffected by the change.""" + async with client as c: + resp = await c.get("/health") + assert resp.status_code == 200 + assert resp.json() == {"status": "healthy"} + + +async def test_agent_card_returns_200_with_audience_capabilities(client): + """`/.well-known/agent.json` returns 200 with `audience_capabilities` block. + + Previously hung in OpenDirect MCP session.initialize() because of the + per-request flow.kickoff(). + """ + async with client as c: + resp = await c.get("/.well-known/agent.json") + assert resp.status_code == 200 + body = resp.json() + assert "audience_capabilities" in body + assert body["audience_capabilities"] is not None + # Inventory types still populated from the static catalog. + assert "inventory_types" in body + assert len(body["inventory_types"]) > 0 + + +async def test_list_products_returns_200_with_products_key(client): + """`GET /products` returns 200 with a `products` list.""" + async with client as c: + resp = await c.get("/products") + assert resp.status_code == 200 + body = resp.json() + assert "products" in body + assert isinstance(body["products"], list) + # Default catalog is non-empty. + assert len(body["products"]) > 0 + # Shape check on first product. + p = body["products"][0] + assert "product_id" in p + assert "name" in p + assert "inventory_type" in p + assert "base_cpm" in p + assert "deal_types" in p + + +async def test_get_product_returns_200_for_existing_404_for_missing(client): + """`GET /products/{id}` returns 200 for an existing product, 404 for missing.""" + # First list products to get a valid id. + async with client as c: + list_resp = await c.get("/products") + assert list_resp.status_code == 200 + existing_id = list_resp.json()["products"][0]["product_id"] + + # Existing product → 200 + ok_resp = await c.get(f"/products/{existing_id}") + assert ok_resp.status_code == 200 + assert ok_resp.json()["product_id"] == existing_id + + # Missing product → 404 + miss_resp = await c.get("/products/prod-doesnotexist") + assert miss_resp.status_code == 404 + + +async def test_endpoints_do_not_invoke_flow_kickoff(client): + """Hits all four read endpoints; the autouse fixture asserts no flow.kickoff().""" + async with client as c: + for path in ( + "/health", + "/.well-known/agent.json", + "/products", + ): + resp = await c.get(path) + assert resp.status_code == 200, f"{path} returned {resp.status_code}" + + +async def test_create_quote_returns_200_without_flow_kickoff(client): + """`POST /api/v1/quotes` returns 200 and does NOT invoke ProductSetupFlow. + + Regression for ar-0vtg: this endpoint used to call + `await ProductSetupFlow().kickoff()` per request to load the product + catalog, which hangs in OpenDirect MCP session.initialize(). The + autouse `_fail_if_flow_kickoff_called` fixture trips an AssertionError + if any code path under this test calls Flow.kickoff(). + """ + async with client as c: + # Find a real product_id from the cached catalog. + list_resp = await c.get("/products") + assert list_resp.status_code == 200 + product_id = list_resp.json()["products"][0]["product_id"] + + # PG deal requires impressions; pick one comfortably above min (default 10000). + body = { + "product_id": product_id, + "deal_type": "PG", + "impressions": 1_000_000, + } + resp = await c.post("/api/v1/quotes", json=body) + assert resp.status_code == 200, f"got {resp.status_code}: {resp.text}" + payload = resp.json() + assert payload["status"] == "available" + assert payload["product"]["product_id"] == product_id + assert payload["deal_type"] == "PG" + assert payload["pricing"]["final_cpm"] > 0 + assert "quote_id" in payload and payload["quote_id"].startswith("qt-") + + +async def test_create_quote_returns_404_for_unknown_product(client): + """Unknown product → 404, also without flow.kickoff().""" + async with client as c: + body = { + "product_id": "prod-doesnotexist", + "deal_type": "PD", + "impressions": 100_000, + } + resp = await c.post("/api/v1/quotes", json=body) + assert resp.status_code == 404 + + +async def test_create_quote_validates_deal_type(client): + """Bad deal_type → 400, also without flow.kickoff().""" + async with client as c: + list_resp = await c.get("/products") + product_id = list_resp.json()["products"][0]["product_id"] + body = { + "product_id": product_id, + "deal_type": "ZZ", + "impressions": 100_000, + } + resp = await c.post("/api/v1/quotes", json=body) + assert resp.status_code == 400 + + +# ============================================================================= +# ar-yet5: Flow-kickoff write endpoints must use kickoff_async(), not kickoff() +# +# In CrewAI 1.10.1 Flow.kickoff() is synchronous and returns None. +# Awaiting None raises TypeError: object NoneType can't be used in 'await'. +# These tests verify /packages/sync (the simplest of the six affected endpoints) +# does NOT return a 500 with that TypeError, proving kickoff_async() is called. +# The autouse `_fail_if_flow_kickoff_called` fixture from this module guarantees +# the old (broken) kickoff() path is never taken. +# ============================================================================= + + +async def test_packages_sync_does_not_return_typeerror_500(): + """`POST /packages/sync` must not crash with TypeError from awaiting kickoff(). + + Regression for ar-yet5: this endpoint called `await flow.kickoff()` which + returns None in CrewAI 1.10.1 and crashes. Fix: `await flow.kickoff_async()`. + + We mock ProductSetupFlow so the test is hermetic (no real flow execution). + The `_fail_if_flow_kickoff_called` autouse fixture ensures the old `.kickoff()` + method is never reached — if it were, it would raise AssertionError, not pass. + """ + mock_flow = MagicMock() + mock_flow.kickoff_async = AsyncMock() + mock_flow.state.synced_segments = [] + mock_flow.state.warnings = [] + + with patch("ad_seller.flows.ProductSetupFlow", return_value=mock_flow): + with patch("ad_seller.events.helpers.emit_event", new_callable=AsyncMock): + transport = ASGITransport(app=app) + async with httpx.AsyncClient(transport=transport, base_url="http://test") as c: + resp = await c.post("/packages/sync") + + # Any status other than 500 (with TypeError) means the fix is working. + # 200 = fully successful; other 2xx/4xx/5xx domain errors are also acceptable + # as long as they are NOT from the TypeError crash. + assert resp.status_code != 500 or "NoneType" not in resp.text, ( + f"POST /packages/sync returned 500 with TypeError body: {resp.text}" + ) + # Confirm kickoff_async was actually awaited (not the old kickoff()). + mock_flow.kickoff_async.assert_awaited_once() diff --git a/tests/unit/test_modern_agentic_capabilities.py b/tests/unit/test_modern_agentic_capabilities.py new file mode 100644 index 0000000..22f9f20 --- /dev/null +++ b/tests/unit/test_modern_agentic_capabilities.py @@ -0,0 +1,44 @@ +"""E2-6: modern_default() agentic capability declaration tests.""" + +import os + +os.environ.setdefault("ANTHROPIC_API_KEY", "test-key-for-unit-tests") + +from ad_seller.models.audience_capabilities import ( + AgenticCapabilities, + AudienceCapabilities, +) + + +class TestModernDefault: + def test_modern_default_returns_expected_shape(self): + caps = AgenticCapabilities.modern_default() + assert caps.supported_signal_types == [ + "identity", + "contextual", + "reinforcement", + ] + assert caps.embedding_dim_range == (256, 1024) + assert caps.spec_version == "draft-2026-01" + assert "IAB-TCFv2" in caps.consent_modes + + def test_modern_default_covers_buyer_local_dim(self): + # Buyer's local sentence-transformers model is 384-dim. Seller's + # modern_default should accept it. + caps = AgenticCapabilities.modern_default() + lo, hi = caps.embedding_dim_range + assert lo <= 384 <= hi + + def test_default_audience_capabilities_has_no_agentic(self): + # Backward compat: AudienceCapabilities() default leaves agentic null. + ac = AudienceCapabilities() + assert ac.agentic_capabilities is None + + def test_audience_capabilities_with_modern_agentic(self): + # Sellers opt in by setting modern_default on the package. + ac = AudienceCapabilities( + agentic_capabilities=AgenticCapabilities.modern_default(), + ) + assert ac.agentic_capabilities is not None + assert ac.agentic_capabilities.spec_version == "draft-2026-01" + assert "identity" in ac.agentic_capabilities.supported_signal_types diff --git a/tests/unit/test_openrtb_parser.py b/tests/unit/test_openrtb_parser.py new file mode 100644 index 0000000..510048d --- /dev/null +++ b/tests/unit/test_openrtb_parser.py @@ -0,0 +1,411 @@ +# Author: Green Mountain Systems AI Inc. +# Donated to IAB Tech Lab + +"""Tests for the seller's OpenRTB BidRequest -> AudienceRef parser. + +Mirror image of the buyer's `test_openrtb_builder.py`. Together they form a +round-trip parity check for the carrier mapping defined in +``docs/api/audience_plan_wire_format.md`` §9 and proposal §5.1 Step 4. + +Bead: ar-8vzg. +""" + +from __future__ import annotations + +from ad_seller.services.openrtb_parser import ( + AGENTIC_USER_EXT_KEY, + parse_openrtb_audience, +) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _bidrequest_with( + *, + user: dict | None = None, + site: dict | None = None, +) -> dict: + """Minimal BidRequest fragment for parser tests.""" + out: dict = {} + if user is not None: + out["user"] = user + if site is not None: + out["site"] = site + return out + + +# --------------------------------------------------------------------------- +# 1. Standard segments -> standard AudienceRefs +# --------------------------------------------------------------------------- + + +def test_parses_standard_segments_from_user_data() -> None: + bidrequest = _bidrequest_with( + user={ + "data": [ + { + "name": "IAB_Taxonomy", + "ext": {"taxonomy_version": "1.1"}, + "segment": [{"id": "3-7"}, {"id": "4-2"}], + } + ] + } + ) + result = parse_openrtb_audience(bidrequest) + refs = result["refs"] + + assert len(refs) == 2 + assert all(r.type == "standard" for r in refs) + assert all(r.taxonomy == "iab-audience" for r in refs) + assert all(r.version == "1.1" for r in refs) + assert all(r.source == "explicit" for r in refs) + assert {r.identifier for r in refs} == {"3-7", "4-2"} + assert result["warnings"] == [] + + +def test_ignores_non_iab_taxonomy_user_data_entries() -> None: + bidrequest = _bidrequest_with( + user={ + "data": [ + { + "name": "ThirdPartyDataProvider", + "segment": [{"id": "tpdp-99"}], + }, + { + "name": "IAB_Taxonomy", + "ext": {"taxonomy_version": "1.1"}, + "segment": [{"id": "3-7"}], + }, + ] + } + ) + result = parse_openrtb_audience(bidrequest) + # Only the IAB_Taxonomy entry contributes. + assert [r.identifier for r in result["refs"]] == ["3-7"] + + +def test_standard_default_version_when_ext_absent() -> None: + bidrequest = _bidrequest_with( + user={ + "data": [ + { + "name": "IAB_Taxonomy", + "segment": [{"id": "3-7"}], + } + ] + } + ) + result = parse_openrtb_audience(bidrequest) + refs = result["refs"] + assert len(refs) == 1 + # Default to 1.1 when the buyer omits ext.taxonomy_version (defensive). + assert refs[0].version == "1.1" + + +# --------------------------------------------------------------------------- +# 2. Contextual: site.cat + cattax=7 -> contextual refs +# --------------------------------------------------------------------------- + + +def test_parses_contextual_when_cattax_is_7() -> None: + bidrequest = _bidrequest_with(site={"cat": ["IAB1-2", "IAB1-7"], "cattax": 7}) + result = parse_openrtb_audience(bidrequest) + refs = result["refs"] + assert len(refs) == 2 + assert all(r.type == "contextual" for r in refs) + assert all(r.taxonomy == "iab-content" for r in refs) + assert all(r.version == "3.1" for r in refs) + assert {r.identifier for r in refs} == {"IAB1-2", "IAB1-7"} + assert result["warnings"] == [] + + +# --------------------------------------------------------------------------- +# 3. Unknown cattax -> ignore + warning +# --------------------------------------------------------------------------- + + +def test_unknown_cattax_logs_warning_and_drops_cats() -> None: + bidrequest = _bidrequest_with(site={"cat": ["IAB1-2"], "cattax": 6}) + result = parse_openrtb_audience(bidrequest) + assert result["refs"] == [] + assert any("cattax" in w for w in result["warnings"]), result["warnings"] + + +def test_missing_cattax_logs_warning_and_drops_cats() -> None: + bidrequest = _bidrequest_with(site={"cat": ["IAB1-2"]}) # no cattax + result = parse_openrtb_audience(bidrequest) + assert result["refs"] == [] + assert any("cattax" in w for w in result["warnings"]) + + +# --------------------------------------------------------------------------- +# 4. Agentic refs from user.ext.iab_agentic_audiences.refs[] +# --------------------------------------------------------------------------- + + +def test_parses_agentic_refs_from_namespaced_user_ext() -> None: + bidrequest = _bidrequest_with( + user={ + "ext": { + AGENTIC_USER_EXT_KEY: { + "refs": [ + { + "identifier": "emb://buyer.example.com/q1", + "version": "draft-2026-01", + "source": "explicit", + "compliance_context": { + "jurisdiction": "US", + "consent_framework": "IAB-TCFv2", + "consent_string_ref": "tcf:CPxxxx", + "attestation": None, + }, + } + ] + } + } + } + ) + result = parse_openrtb_audience(bidrequest) + refs = result["refs"] + assert len(refs) == 1 + ref = refs[0] + assert ref.type == "agentic" + assert ref.identifier == "emb://buyer.example.com/q1" + assert ref.version == "draft-2026-01" + assert ref.taxonomy == "agentic-audiences" + assert ref.compliance_context is not None + assert ref.compliance_context.jurisdiction == "US" + assert ref.compliance_context.consent_framework == "IAB-TCFv2" + assert result["warnings"] == [] + + +def test_agentic_ref_without_compliance_uses_fallback_with_warning() -> None: + """Spec mandates compliance_context for agentic refs, but a malformed + request MAY omit it. The parser substitutes a clearly-marked fallback + (``jurisdiction='UNKNOWN'``) so downstream code does not crash, and + surfaces a warning so the audit trail can flag the entry.""" + bidrequest = _bidrequest_with( + user={ + "ext": { + AGENTIC_USER_EXT_KEY: { + "refs": [ + { + "identifier": "emb://buyer.example.com/q1", + "version": "draft-2026-01", + "source": "explicit", + # NOTE: compliance_context omitted. + } + ] + } + } + } + ) + result = parse_openrtb_audience(bidrequest) + refs = result["refs"] + assert len(refs) == 1 + ref = refs[0] + assert ref.type == "agentic" + assert ref.compliance_context is not None + assert ref.compliance_context.jurisdiction == "UNKNOWN" + assert ref.compliance_context.consent_framework == "none" + assert any("compliance_context" in w for w in result["warnings"]) + + +# --------------------------------------------------------------------------- +# 5. Round-trip: builder -> parser parity +# --------------------------------------------------------------------------- + + +def test_round_trip_builder_then_parser_recovers_refs() -> None: + """Build a BidRequest from a multi-role plan and parse it back. + + Confirms the carrier mapping is symmetric for the per-ref content + (identifier / version / type / taxonomy). Roles are expected NOT to + survive the trip -- OpenRTB's lossy mapping does not preserve them + (see parser docstring). + """ + # Lazy import the buyer-side builder (test executes in seller venv; + # adjust import path so the buyer source is on PYTHONPATH). + import os + import sys + from pathlib import Path + + # Path resolution (per ar-e2rj): tests can override via the + # `AD_BUYER_SRC_PATH` env var. Otherwise, walk up from this file to + # find the seller repo root (`ad_seller_system`); the buyer repo + # lives at `/ad_buyer_system`. If we're inside a seller + # worktree (`/.worktrees//...`), prefer the matching + # buyer worktree; otherwise fall back to the buyer repo's + # canonical `src/`. + buyer_src = os.environ.get("AD_BUYER_SRC_PATH") + if not buyer_src: + here = Path(__file__).resolve() + seller_repo_root = next( + (p for p in here.parents if p.name == "ad_seller_system"), + None, + ) + if seller_repo_root is None: + raise RuntimeError( + "Could not locate ad_seller_system in path ancestry " + f"of {here}; set AD_BUYER_SRC_PATH to override." + ) + agent_range_root = seller_repo_root.parent + buyer_main = agent_range_root / "ad_buyer_system" / "src" + worktree_name = None + for parent, grandparent in zip(here.parents, here.parents[1:]): + if ( + grandparent.name == ".worktrees" + and grandparent.parent.name == "ad_seller_system" + ): + worktree_name = parent.name + break + if worktree_name is not None: + sibling_worktree = ( + agent_range_root + / "ad_buyer_system" + / ".worktrees" + / worktree_name + / "src" + ) + buyer_src = str( + sibling_worktree if sibling_worktree.is_dir() else buyer_main + ) + else: + buyer_src = str(buyer_main) + sys.path.insert(0, buyer_src) + try: + from ad_buyer.clients.openrtb_builder import ( # type: ignore[import-not-found] + build_openrtb_audience_targeting, + ) + from ad_buyer.models.audience_plan import ( # type: ignore[import-not-found] + AudiencePlan, + AudienceRef as BuyerAudienceRef, + ComplianceContext as BuyerComplianceContext, + ) + finally: + sys.path.remove(buyer_src) + + plan = AudiencePlan( + primary=BuyerAudienceRef( + type="standard", identifier="3-7", taxonomy="iab-audience", + version="1.1", source="explicit", + ), + constraints=[ + BuyerAudienceRef( + type="contextual", identifier="IAB1-2", + taxonomy="iab-content", version="3.1", source="explicit", + ) + ], + extensions=[ + BuyerAudienceRef( + type="agentic", + identifier="emb://buyer.example.com/q1-converters", + taxonomy="agentic-audiences", version="draft-2026-01", + source="explicit", + compliance_context=BuyerComplianceContext( + jurisdiction="US", + consent_framework="IAB-TCFv2", + consent_string_ref="tcf:CPxxxx", + ), + ) + ], + ) + + fragment = build_openrtb_audience_targeting(plan, enable_agentic_ext=True) + parsed = parse_openrtb_audience(fragment) + + # 3 refs out (standard + contextual + agentic). + assert len(parsed["refs"]) == 3, parsed["refs"] + by_type = {r.type: r for r in parsed["refs"]} + + assert by_type["standard"].identifier == "3-7" + assert by_type["standard"].taxonomy == "iab-audience" + assert by_type["standard"].version == "1.1" + + assert by_type["contextual"].identifier == "IAB1-2" + assert by_type["contextual"].taxonomy == "iab-content" + assert by_type["contextual"].version == "3.1" + + assert by_type["agentic"].identifier == "emb://buyer.example.com/q1-converters" + assert by_type["agentic"].taxonomy == "agentic-audiences" + assert by_type["agentic"].version == "draft-2026-01" + assert by_type["agentic"].compliance_context is not None + assert by_type["agentic"].compliance_context.jurisdiction == "US" + assert parsed["warnings"] == [] + + +# --------------------------------------------------------------------------- +# Edge cases / defensive parsing +# --------------------------------------------------------------------------- + + +def test_empty_bidrequest_returns_empty_refs() -> None: + result = parse_openrtb_audience({}) + assert result == {"refs": [], "warnings": []} + + +def test_non_dict_bidrequest_handled_safely() -> None: + result = parse_openrtb_audience("not a dict") # type: ignore[arg-type] + assert result["refs"] == [] + assert any("not an object" in w for w in result["warnings"]) + + +def test_malformed_user_data_entries_skipped_with_warnings() -> None: + bidrequest = _bidrequest_with( + user={ + "data": [ + "not an object", # entry-level malformation + { + "name": "IAB_Taxonomy", + "segment": "not an array", + }, + { + "name": "IAB_Taxonomy", + "segment": [ + {"id": "3-7"}, + {"id": ""}, # empty id + "not an object", # segment-level malformation + ], + }, + ] + } + ) + result = parse_openrtb_audience(bidrequest) + # Only the well-formed segment with id="3-7" survives. + assert [r.identifier for r in result["refs"]] == ["3-7"] + # Multiple warnings recorded. + assert len(result["warnings"]) >= 3 + + +def test_malformed_agentic_refs_skipped_with_warnings() -> None: + bidrequest = _bidrequest_with( + user={ + "ext": { + AGENTIC_USER_EXT_KEY: { + "refs": [ + "not an object", + {"identifier": "", "version": "draft-2026-01"}, + {"identifier": "emb://x", "version": ""}, + { + "identifier": "emb://valid", + "version": "draft-2026-01", + "source": "explicit", + "compliance_context": { + "jurisdiction": "US", + "consent_framework": "IAB-TCFv2", + }, + }, + ] + } + } + } + ) + result = parse_openrtb_audience(bidrequest) + # Only the valid one survives. + valid = [r for r in result["refs"] if r.type == "agentic"] + assert len(valid) == 1 + assert valid[0].identifier == "emb://valid" + assert len(result["warnings"]) >= 3 diff --git a/tests/unit/test_packages_audience_filter.py b/tests/unit/test_packages_audience_filter.py new file mode 100644 index 0000000..bff53f8 --- /dev/null +++ b/tests/unit/test_packages_audience_filter.py @@ -0,0 +1,452 @@ +# Author: Green Mountain Systems AI Inc. +# Donated to IAB Tech Lab + +"""Unit tests for the audience filter on `/packages` and the audience corpus +in `/media-kit/search` scoring (proposal §5.7 + §6 row 10, bead ar-2wxa). + +Service-level coverage (no HTTP): + +- `MediaKitService.list_packages_public/authenticated` accepts an optional + `AudienceFilter` and returns only matching packages. +- `MediaKitService.search_packages` ranks audience-matching packages higher + via the expanded corpus. +- Backward compat: callers that don't pass `audience_filter` see the same + behavior they did before this bead. + +HTTP-level coverage for the endpoint surface lives in +`test_packages_audience_filter_endpoint.py` (integration) -- this file +exercises the service directly so service regressions are caught even if +endpoint plumbing breaks. +""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +import pytest + +from ad_seller.engines.media_kit_service import ( + AudienceFilter, + MediaKitService, +) +from ad_seller.engines.pricing_rules_engine import PricingRulesEngine +from ad_seller.models.audience_capabilities import ( + AgenticCapabilities, + AudienceCapabilities, +) +from ad_seller.models.media_kit import ( + Package, + PackageLayer, + PackageStatus, +) +from ad_seller.models.pricing_tiers import TieredPricingConfig + +# ============================================================================= +# Fixtures +# ============================================================================= + + +def _make_package( + *, + package_id: str, + name: str = "Test Package", + standard_segment_ids: list[str] | None = None, + contextual_segment_ids: list[str] | None = None, + standard_taxonomy_version: str = "1.1", + contextual_taxonomy_version: str = "3.1", + agentic_capabilities: AgenticCapabilities | None = None, + tags: list[str] | None = None, + cat: list[str] | None = None, + is_featured: bool = False, + base_price: float = 20.0, + floor_price: float = 10.0, +) -> dict: + """Build a Package as a storage-shape dict (matches what list_packages returns).""" + pkg = Package( + package_id=package_id, + name=name, + description=f"Description for {name}", + layer=PackageLayer.CURATED, + status=PackageStatus.ACTIVE, + base_price=base_price, + floor_price=floor_price, + is_featured=is_featured, + tags=tags or [], + cat=cat or [], + audience_capabilities=AudienceCapabilities( + standard_segment_ids=standard_segment_ids or [], + standard_taxonomy_version=standard_taxonomy_version, + contextual_segment_ids=contextual_segment_ids or [], + contextual_taxonomy_version=contextual_taxonomy_version, + agentic_capabilities=agentic_capabilities, + ), + ) + return pkg.model_dump(mode="json") + + +@pytest.fixture +def pricing_engine(): + config = TieredPricingConfig(seller_organization_id="test-seller") + return PricingRulesEngine(config=config) + + +@pytest.fixture +def mock_storage(): + return AsyncMock() + + +@pytest.fixture +def service(mock_storage, pricing_engine): + return MediaKitService(storage=mock_storage, pricing_engine=pricing_engine) + + +@pytest.fixture +def mixed_packages(): + """Three packages spanning the three audience types. + + - pkg-std: standard segments only ("3-7", "3-12") + - pkg-ctx: contextual segments only ("IAB1-2", "IAB1-3") + - pkg-agt: agentic capabilities populated + - pkg-none: no audience capabilities (legacy / direct response, etc.) + """ + return [ + _make_package( + package_id="pkg-std", + name="Standard Auto Intenders", + standard_segment_ids=["3-7", "3-12"], + ), + _make_package( + package_id="pkg-ctx", + name="Contextual Automotive", + contextual_segment_ids=["IAB1-2", "IAB1-3"], + ), + _make_package( + package_id="pkg-agt", + name="Agentic Premium", + agentic_capabilities=AgenticCapabilities( + supported_signal_types=["identity", "contextual"], + spec_version="draft-2026-01", + ), + ), + _make_package( + package_id="pkg-none", + name="Legacy Direct Response", + tags=["direct response"], + ), + ] + + +# ============================================================================= +# AudienceFilter.matches() — predicate semantics +# ============================================================================= + + +class TestAudienceFilterPredicate: + """Cover the matching predicate without going through storage.""" + + def test_empty_filter_matches_everything(self): + f = AudienceFilter() + assert f.is_empty() + # Build a Package via dict round-trip (we only care about the predicate) + pkg = Package(**_make_package(package_id="pkg-x")) + assert f.matches(pkg) is True + + def test_standard_id_match(self): + f = AudienceFilter(audience_type="standard", audience_id="3-7") + pkg_match = Package( + **_make_package(package_id="p1", standard_segment_ids=["3-7"]) + ) + pkg_no_match = Package( + **_make_package(package_id="p2", standard_segment_ids=["3-12"]) + ) + assert f.matches(pkg_match) is True + assert f.matches(pkg_no_match) is False + + def test_standard_type_only_requires_any_segment(self): + """Type-only filter passes any package with non-empty standard list.""" + f = AudienceFilter(audience_type="standard") + pkg_with = Package( + **_make_package(package_id="p1", standard_segment_ids=["3-7"]) + ) + pkg_without = Package(**_make_package(package_id="p2")) + assert f.matches(pkg_with) is True + assert f.matches(pkg_without) is False + + def test_contextual_id_match(self): + f = AudienceFilter(audience_type="contextual", audience_id="IAB1-2") + pkg_match = Package( + **_make_package(package_id="p1", contextual_segment_ids=["IAB1-2"]) + ) + pkg_no_match = Package( + **_make_package(package_id="p2", contextual_segment_ids=["IAB2-3"]) + ) + assert f.matches(pkg_match) is True + assert f.matches(pkg_no_match) is False + + def test_agentic_supported_predicate(self): + """For agentic, presence of agentic_capabilities is the gate.""" + f = AudienceFilter(audience_type="agentic") + pkg_with = Package( + **_make_package( + package_id="p1", + agentic_capabilities=AgenticCapabilities( + supported_signal_types=["identity"] + ), + ) + ) + pkg_without = Package(**_make_package(package_id="p2")) + assert f.matches(pkg_with) is True + assert f.matches(pkg_without) is False + + def test_agentic_with_id_still_supported_predicate(self): + """Per ar-2wxa: agentic per-segment matching is §11; type-only gate.""" + f = AudienceFilter( + audience_type="agentic", audience_id="emb://example.com/x" + ) + pkg_with = Package( + **_make_package( + package_id="p1", + agentic_capabilities=AgenticCapabilities(), + ) + ) + assert f.matches(pkg_with) is True + + def test_taxonomy_version_constraint_standard(self): + f = AudienceFilter( + audience_type="standard", + audience_id="3-7", + taxonomy_version="1.1", + ) + pkg_match = Package( + **_make_package( + package_id="p1", + standard_segment_ids=["3-7"], + standard_taxonomy_version="1.1", + ) + ) + pkg_wrong_version = Package( + **_make_package( + package_id="p2", + standard_segment_ids=["3-7"], + standard_taxonomy_version="2.0", + ) + ) + assert f.matches(pkg_match) is True + assert f.matches(pkg_wrong_version) is False + + def test_id_without_type_returns_false(self): + """Defense-in-depth: ID without type can't disambiguate corpus.""" + f = AudienceFilter(audience_id="3-7") + pkg = Package(**_make_package(package_id="p1", standard_segment_ids=["3-7"])) + assert f.matches(pkg) is False + + +# ============================================================================= +# Service-level: list_packages_public/authenticated with audience_filter +# ============================================================================= + + +class TestListPackagesAudienceFilter: + """Exercise the service path that the endpoints call.""" + + @pytest.mark.asyncio + async def test_filter_by_standard_id( + self, service, mock_storage, mixed_packages + ): + mock_storage.list_packages.return_value = mixed_packages + filt = AudienceFilter(audience_type="standard", audience_id="3-7") + results = await service.list_packages_public(audience_filter=filt) + ids = [r.package_id for r in results] + assert ids == ["pkg-std"] + + @pytest.mark.asyncio + async def test_filter_by_contextual_id( + self, service, mock_storage, mixed_packages + ): + mock_storage.list_packages.return_value = mixed_packages + filt = AudienceFilter(audience_type="contextual", audience_id="IAB1-2") + results = await service.list_packages_public(audience_filter=filt) + ids = [r.package_id for r in results] + assert ids == ["pkg-ctx"] + + @pytest.mark.asyncio + async def test_filter_by_agentic_type_only( + self, service, mock_storage, mixed_packages + ): + mock_storage.list_packages.return_value = mixed_packages + filt = AudienceFilter(audience_type="agentic") + results = await service.list_packages_public(audience_filter=filt) + ids = [r.package_id for r in results] + assert ids == ["pkg-agt"] + + @pytest.mark.asyncio + async def test_filter_with_no_match_returns_empty_list( + self, service, mock_storage, mixed_packages + ): + mock_storage.list_packages.return_value = mixed_packages + filt = AudienceFilter( + audience_type="standard", audience_id="never-exists" + ) + results = await service.list_packages_public(audience_filter=filt) + assert results == [] + + @pytest.mark.asyncio + async def test_no_filter_returns_all_active_packages( + self, service, mock_storage, mixed_packages + ): + """Backward compat: no filter -> every package is returned.""" + mock_storage.list_packages.return_value = mixed_packages + results = await service.list_packages_public() + assert {r.package_id for r in results} == { + "pkg-std", + "pkg-ctx", + "pkg-agt", + "pkg-none", + } + + @pytest.mark.asyncio + async def test_filter_combined_with_layer( + self, service, mock_storage, mixed_packages + ): + """Audience filter composes with the existing layer filter.""" + mock_storage.list_packages.return_value = mixed_packages + filt = AudienceFilter(audience_type="standard") + results = await service.list_packages_public( + layer=PackageLayer.CURATED, audience_filter=filt + ) + assert [r.package_id for r in results] == ["pkg-std"] + + @pytest.mark.asyncio + async def test_authenticated_view_respects_filter( + self, service, mock_storage, mixed_packages + ): + from ad_seller.models.buyer_identity import ( + BuyerContext, + BuyerIdentity, + ) + + mock_storage.list_packages.return_value = mixed_packages + ctx = BuyerContext( + identity=BuyerIdentity(agency_id="a1", agency_name="A"), + is_authenticated=True, + ) + filt = AudienceFilter(audience_type="contextual", audience_id="IAB1-2") + results = await service.list_packages_authenticated( + ctx, audience_filter=filt + ) + assert [r.package_id for r in results] == ["pkg-ctx"] + + +# ============================================================================= +# Service-level: search_packages corpus + audience_filter +# ============================================================================= + + +class TestSearchAudienceCorpus: + """The audience corpus is part of `_score_package`'s text bag.""" + + @pytest.mark.asyncio + async def test_search_ranks_audience_match_higher( + self, service, mock_storage + ): + """A query mentioning a segment ID prefers packages declaring that ID.""" + # pkg-with declares "3-7" in its standard segments; pkg-without doesn't. + # Both share the keyword "premium" so neither scores zero. + mock_storage.list_packages.return_value = [ + _make_package( + package_id="pkg-with", + name="Premium Auto", + standard_segment_ids=["3-7"], + tags=["premium"], + ), + _make_package( + package_id="pkg-without", + name="Premium News", + tags=["premium"], + ), + ] + # Query that mentions both the keyword and the segment ID. + results = await service.search_packages("premium 3-7") + ids = [r.package_id for r in results] + # pkg-with should rank first because it matches both tokens + # ("premium" via tag, "3-7" via standard_segment_ids). + assert ids[0] == "pkg-with" + assert "pkg-without" in ids + + @pytest.mark.asyncio + async def test_search_finds_by_contextual_segment_id( + self, service, mock_storage + ): + """A query that's ONLY a contextual ID still matches a package that declares it.""" + mock_storage.list_packages.return_value = [ + _make_package( + package_id="pkg-ctx", + name="Ctx Pkg", + contextual_segment_ids=["IAB1-2"], + ), + _make_package( + package_id="pkg-other", + name="Other Pkg", + tags=["sports"], + ), + ] + results = await service.search_packages("iab1-2") + ids = [r.package_id for r in results] + assert ids == ["pkg-ctx"] + + @pytest.mark.asyncio + async def test_keyword_only_search_still_works(self, service, mock_storage): + """Backward compat: a keyword-only query against keyword-only data + returns the right package.""" + mock_storage.list_packages.return_value = [ + _make_package( + package_id="pkg-sports", + name="Sports Bundle", + tags=["sports", "live events"], + ), + _make_package( + package_id="pkg-news", + name="News Bundle", + tags=["news"], + ), + ] + results = await service.search_packages("sports") + ids = [r.package_id for r in results] + assert ids == ["pkg-sports"] + + @pytest.mark.asyncio + async def test_audience_filter_restricts_search_results( + self, service, mock_storage + ): + """Optional audience_filter narrows search even if keyword would match.""" + mock_storage.list_packages.return_value = [ + _make_package( + package_id="pkg-std", + name="Premium Auto", + standard_segment_ids=["3-7"], + tags=["premium"], + ), + _make_package( + package_id="pkg-ctx", + name="Premium Auto Content", + contextual_segment_ids=["IAB1-2"], + tags=["premium"], + ), + ] + # Without filter: both packages match "premium" + all_results = await service.search_packages("premium") + assert {r.package_id for r in all_results} == {"pkg-std", "pkg-ctx"} + + # With filter restricted to contextual: only pkg-ctx returns + filt = AudienceFilter(audience_type="contextual") + filtered = await service.search_packages("premium", audience_filter=filt) + assert [r.package_id for r in filtered] == ["pkg-ctx"] + + @pytest.mark.asyncio + async def test_search_without_filter_is_unchanged(self, service, mock_storage): + """Backward compat: search() accepts no audience_filter param.""" + mock_storage.list_packages.return_value = [ + _make_package(package_id="pkg-a", name="Alpha", tags=["alpha"]), + ] + results = await service.search_packages("alpha") + assert [r.package_id for r in results] == ["pkg-a"] diff --git a/tests/unit/test_quote_endpoints.py b/tests/unit/test_quote_endpoints.py index 9d7f818..79d1824 100644 --- a/tests/unit/test_quote_endpoints.py +++ b/tests/unit/test_quote_endpoints.py @@ -39,7 +39,10 @@ def _mock_product_setup_flow(products_dict): - """Return a mock ProductSetupFlow whose state has the given products.""" + """Return a mock ProductSetupFlow whose state has the given products. + + Kept for backward compatibility with TestGetQuote and other call sites. + """ mock_flow = MagicMock() mock_flow.state = MagicMock() mock_flow.state.products = products_dict @@ -48,6 +51,17 @@ def _mock_product_setup_flow(products_dict): return mock_flow +def _mock_catalog(products_dict): + """Build the dict shape returned by `_get_static_product_catalog`. + + Quote endpoint switched from `ProductSetupFlow.kickoff()` to the cached + static catalog (ar-uwad / ar-0vtg). Tests patch the catalog accessor + rather than the flow class. + """ + inventory_types = sorted({p.inventory_type for p in products_dict.values()}) + return {"products": products_dict, "inventory_types": inventory_types} + + def _make_product(**overrides): from ad_seller.models.core import DealType, PricingModel from ad_seller.models.flow_state import ProductDefinition @@ -105,8 +119,8 @@ class TestCreateQuote: async def test_happy_path_pd_quote(self, client, mock_storage): with ( patch( - "ad_seller.flows.ProductSetupFlow", - return_value=_mock_product_setup_flow(_products()), + "ad_seller.interfaces.api.main._get_static_product_catalog", + return_value=_mock_catalog(_products()), ), patch("ad_seller.storage.factory.get_storage", return_value=mock_storage), ): @@ -137,8 +151,8 @@ async def test_happy_path_pd_quote(self, client, mock_storage): async def test_pg_quote_sets_guaranteed_true(self, client, mock_storage): with ( patch( - "ad_seller.flows.ProductSetupFlow", - return_value=_mock_product_setup_flow(_products()), + "ad_seller.interfaces.api.main._get_static_product_catalog", + return_value=_mock_catalog(_products()), ), patch("ad_seller.storage.factory.get_storage", return_value=mock_storage), ): @@ -157,8 +171,8 @@ async def test_pg_quote_sets_guaranteed_true(self, client, mock_storage): async def test_target_cpm_accepted_when_above_floor(self, client, mock_storage): with ( patch( - "ad_seller.flows.ProductSetupFlow", - return_value=_mock_product_setup_flow(_products()), + "ad_seller.interfaces.api.main._get_static_product_catalog", + return_value=_mock_catalog(_products()), ), patch("ad_seller.storage.factory.get_storage", return_value=mock_storage), ): @@ -178,8 +192,8 @@ async def test_target_cpm_accepted_when_above_floor(self, client, mock_storage): async def test_target_cpm_rejected_below_floor(self, client, mock_storage): with ( patch( - "ad_seller.flows.ProductSetupFlow", - return_value=_mock_product_setup_flow(_products()), + "ad_seller.interfaces.api.main._get_static_product_catalog", + return_value=_mock_catalog(_products()), ), patch("ad_seller.storage.factory.get_storage", return_value=mock_storage), ): @@ -201,8 +215,8 @@ async def test_target_cpm_rejected_below_floor(self, client, mock_storage): async def test_buyer_identity_affects_tier(self, client, mock_storage): with ( patch( - "ad_seller.flows.ProductSetupFlow", - return_value=_mock_product_setup_flow(_products()), + "ad_seller.interfaces.api.main._get_static_product_catalog", + return_value=_mock_catalog(_products()), ), patch("ad_seller.storage.factory.get_storage", return_value=mock_storage), ): @@ -229,7 +243,8 @@ async def test_buyer_identity_affects_tier(self, client, mock_storage): async def test_product_not_found(self, client, mock_storage): with patch( - "ad_seller.flows.ProductSetupFlow", return_value=_mock_product_setup_flow(_products()) + "ad_seller.interfaces.api.main._get_static_product_catalog", + return_value=_mock_catalog(_products()), ): resp = await client.post( "/api/v1/quotes", @@ -243,7 +258,8 @@ async def test_product_not_found(self, client, mock_storage): async def test_invalid_deal_type(self, client, mock_storage): with patch( - "ad_seller.flows.ProductSetupFlow", return_value=_mock_product_setup_flow(_products()) + "ad_seller.interfaces.api.main._get_static_product_catalog", + return_value=_mock_catalog(_products()), ): resp = await client.post( "/api/v1/quotes", @@ -257,7 +273,8 @@ async def test_invalid_deal_type(self, client, mock_storage): async def test_pg_without_impressions(self, client, mock_storage): with patch( - "ad_seller.flows.ProductSetupFlow", return_value=_mock_product_setup_flow(_products()) + "ad_seller.interfaces.api.main._get_static_product_catalog", + return_value=_mock_catalog(_products()), ): resp = await client.post( "/api/v1/quotes", @@ -271,7 +288,8 @@ async def test_pg_without_impressions(self, client, mock_storage): async def test_below_minimum_impressions(self, client, mock_storage): with patch( - "ad_seller.flows.ProductSetupFlow", return_value=_mock_product_setup_flow(_products()) + "ad_seller.interfaces.api.main._get_static_product_catalog", + return_value=_mock_catalog(_products()), ): resp = await client.post( "/api/v1/quotes",