Product Search Extension
Introduction
The Product Search Extension (affinity_ai_product_search) enables publishers
to filter product search results by price range, improving ad relevance for user
queries. This extension is particularly useful when users specify budget
constraints in their search queries (e.g., "show me iPhones under $900").
Extension Name: affinity_ai_product_search
Location: imp[].ext.affinity_ai_product_search (impression-level)
Applies To: Affillizz Search Product Ad adapter
Keyword Search Behavior
The site.keywords field drives the product search query. It accepts a
comma-separated list of keywords — when multiple keywords are provided, the
search returns products matching any of the terms (broad OR matching).
Single Keyword
{
"site": {
"keywords": "iphone 13"
}
}
Result: Returns products matching iphone 13.
Multiple Keywords (OR Search)
{
"site": {
"keywords": "iphone 13, smartphone, apple"
}
}
Result: Returns products matching any of the provided terms — i.e.,
products relevant to iphone 13, smartphone, or apple. This is useful when
you want to broaden the search to cover related terms or product categories.
Rules & Limits
- Deduplication: Duplicate keywords are automatically removed
- Case normalization: Keywords are lowercased before search
- Cap: A maximum of 10 keywords are used; any beyond the 10th are ignored
- Whitespace: Leading and trailing whitespace around each keyword is trimmed
Why Product Search Filtering?
Better Ad Relevance
- Match User Intent: Filter products to match user budget constraints
- Improved CTR: More relevant products lead to higher engagement
- Better Conversions: Price-appropriate products convert better
- Enhanced UX: Users see products they can actually afford
Privacy-First Design
- No PII Required: Price filters are contextual only
- GDPR/CCPA Compliant: No personal data involved
- Transparent: Clear filtering based on query intent
- Optional: Completely backward compatible
Simple Integration
- Flat JSON Structure: Easy to parse and use
- Optional Fields: Both min and max are optional
- Graceful Handling: Invalid values ignored, not rejected
- Standard Protocol: Uses OpenRTB extension mechanism
Extension Structure
The extension uses a simple, flat JSON structure:
{
"affinity_ai_product_search": {
"min_price": 800.0,
"max_price": 900.0,
"price_currency": "EUR"
}
}
Field Definitions
| Field | Type | Required | Description | Constraints |
|---|---|---|---|---|
min_price | float | No | Minimum product price filter | Must be non-negative; if both min/max set, min ≤ max |
max_price | float | No | Maximum product price filter | Must be non-negative; if both min/max set, min ≤ max |
price_currency | string | No | Currency code for price validation | ISO 4217 code (e.g., "EUR", "USD"); used for validation only, not sent to API |
Validation Rules
-
Price Range:
- Both
min_priceandmax_priceare optional - If set, must be non-negative (≥ 0)
- If both set,
min_pricemust be ≤max_price - Invalid values are ignored gracefully (not rejected)
- Both
-
Currency:
price_currencyis optional- If provided, must be valid ISO 4217 currency code
- Used for validation/logging only, not passed to Affillizz API
-
Backward Compatibility:
- Extension is completely optional
- Requests without extension work as before
- Invalid extension values are ignored gracefully
How It Works
Request Structure
The extension is placed in imp[].ext alongside other impression-level
extensions:
{
"id": "bid-request-123",
"imp": [
{
"id": "imp-1",
"native": {
"request": "{\"native\":{\"ver\":\"1.2\",\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":80}}]}}"
},
"ext": {
"affinity_ai_product_search": {
"min_price": 800.0,
"max_price": 900.0,
"price_currency": "EUR"
},
"aura": {
"adcpFormats": [
{
"agent_url": "https://creative.adcontextprotocol.org",
"id": "native_product_listing_v1"
}
]
}
}
}
],
"site": {
"id": "ai-search-sem-text",
"keywords": "iphone 13"
},
"device": {
"language": "de",
"geo": { "country": "DE" }
}
}
Usage Scenarios
Scenario 1: Price Range Filter (Both Min and Max)
User Query: "Show me iPhones between 900"
{
"imp": [
{
"id": "imp-1",
"native": {
"request": "{\"native\":{\"ver\":\"1.2\",\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":80}}]}}"
},
"ext": {
"affinity_ai_product_search": {
"min_price": 800.0,
"max_price": 900.0,
"price_currency": "USD"
}
}
}
],
"site": {
"keywords": "iphone"
}
}
Result: Returns products priced between 900
API Call:
GET /v1/ads/search?query=iphone&minOfferPrice=800&maxOfferPrice=900
Scenario 2: Minimum Price Only
User Query: "Show me premium headphones $50 and above"
{
"imp": [
{
"id": "imp-1",
"native": {
"request": "{\"native\":{\"ver\":\"1.2\",\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":80}}]}}"
},
"ext": {
"affinity_ai_product_search": {
"min_price": 50.0,
"price_currency": "USD"
}
}
}
],
"site": {
"keywords": "headphones"
}
}
Result: Returns products priced $50 and above
API Call:
GET /v1/ads/search?query=headphones&minOfferPrice=50
Scenario 3: Maximum Price Only
User Query: "Show me budget laptops under $500"
{
"imp": [
{
"id": "imp-1",
"native": { "request": "..." },
"ext": {
"affinity_ai_product_search": {
"max_price": 500.0,
"price_currency": "USD"
}
}
}
],
"site": {
"keywords": "laptop"
}
}
Result: Returns products priced under $500
API Call:
GET /v1/ads/search?query=laptop&maxOfferPrice=500
Scenario 4: No Filter (Default Behavior)
User Query: "Show me smartphones"
{
"imp": [
{
"id": "imp-1",
"native": {
"request": "{\"native\":{\"ver\":\"1.2\",\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":80}}]}}"
}
}
],
"site": {
"keywords": "smartphone"
}
}
Result: Returns products without price filtering (all price ranges)
API Call:
GET /v1/ads/search?query=smartphone
Publisher Implementation
1. Parse User Query
Extract price constraints from user queries:
class PriceParser {
parseQuery(query) {
const patterns = {
range: /between \$?(\d+(?:\.\d+)?)\s*(?:and|to|-)\s*\$?(\d+(?:\.\d+)?)/i,
under: /under|below|less than\s*\$?(\d+(?:\.\d+)?)/i,
over: /over|above|more than|at least\s*\$?(\d+(?:\.\d+)?)/i,
}
// Check for range
const rangeMatch = query.match(patterns.range)
if (rangeMatch) {
return {
min_price: parseFloat(rangeMatch[1]),
max_price: parseFloat(rangeMatch[2]),
price_currency: this.detectCurrency(query),
}
}
// Check for maximum
const underMatch = query.match(patterns.under)
if (underMatch) {
return {
max_price: parseFloat(underMatch[1]),
price_currency: this.detectCurrency(query),
}
}
// Check for minimum
const overMatch = query.match(patterns.over)
if (overMatch) {
return {
min_price: parseFloat(overMatch[1]),
price_currency: this.detectCurrency(query),
}
}
return null
}
detectCurrency(query) {
if (query.includes('$') || query.includes('USD')) return 'USD'
if (query.includes('€') || query.includes('EUR')) return 'EUR'
if (query.includes('£') || query.includes('GBP')) return 'GBP'
return 'USD' // default
}
}
2. Build Bid Request
Add the extension to your bid request:
function buildBidRequest(query, priceFilter) {
const bidRequest = {
id: generateId(),
imp: [
{
id: 'imp-1',
banner: { w: 300, h: 250 },
ext: {},
},
],
site: {
keywords: extractKeywords(query),
},
}
// Add price filter if present
if (priceFilter) {
bidRequest.imp[0].ext.affinity_ai_product_search = priceFilter
}
return bidRequest
}
3. Send Request
const parser = new PriceParser()
const priceFilter = parser.parseQuery(userQuery)
const bidRequest = buildBidRequest(userQuery, priceFilter)
const bidResponse = await sendBidRequest(bidRequest)
Adapter Implementation
1. Extract Price Filter
func extractPriceFilter(imp *openrtb2.Imp) *PriceFilter {
if imp.Ext == nil {
return nil
}
var impExt ImpExt
if err := json.Unmarshal(imp.Ext, &impExt); err != nil {
return nil
}
return impExt.AffinityAIProductSearch
}
2. Validate Price Filter
func validatePriceFilter(filter *PriceFilter) error {
if filter == nil {
return nil
}
// Check non-negative
if filter.MinPrice != nil && *filter.MinPrice < 0 {
return fmt.Errorf("min_price must be non-negative")
}
if filter.MaxPrice != nil && *filter.MaxPrice < 0 {
return fmt.Errorf("max_price must be non-negative")
}
// Check min <= max
if filter.MinPrice != nil && filter.MaxPrice != nil {
if *filter.MinPrice > *filter.MaxPrice {
return fmt.Errorf("min_price must be <= max_price")
}
}
return nil
}
3. Build API Request
func buildAPIRequest(filter *PriceFilter, baseParams url.Values) url.Values {
params := baseParams
if filter != nil {
if filter.MinPrice != nil {
params.Set("minOfferPrice", fmt.Sprintf("%.0f", *filter.MinPrice))
}
if filter.MaxPrice != nil {
params.Set("maxOfferPrice", fmt.Sprintf("%.0f", *filter.MaxPrice))
}
// Note: price_currency is NOT sent to API (validation only)
}
return params
}
Best Practices
Extension Usage
- Optional: Extension is completely optional and backward compatible
- Graceful: Invalid values are ignored, not rejected
- Validation: Currency is for validation only (not sent to API)
- Flexible: Both min and max are optional (use either or both)
Query Parsing
When parsing user queries for price constraints:
- Extract Price Values: Identify numeric price values in query
- Detect Range Type: Determine if min, max, or both
- Identify Currency: Extract currency from query or use default
- Validate Values: Ensure min ≤ max if both present
- Add Extension: Include in bid request if price filter detected
Error Handling
- Missing Extension: Request works normally without extension
- Invalid Values: Ignored gracefully (e.g., negative prices, min > max)
- Unknown Currency: Logged but not rejected
- Partial Data: Min or max alone is valid
Integration
- Compatible: Works with existing Affinity AI extensions (AdCP, context enhancement)
- Combinable: Can be combined with other impression-level extensions
- Per-Impression: Applies per-impression (different filters per ad slot)
- Adapter-Specific: Only affects Affillizz Search Product Ad adapter
Integration with Other Extensions
Works With AdCP
The product search extension integrates seamlessly with AdCP formats:
{
"imp": [
{
"id": "imp-1",
"native": {
"request": "{\"native\":{\"ver\":\"1.2\",\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":80}}]}}"
},
"ext": {
"affinity_ai_product_search": {
"min_price": 800.0,
"max_price": 900.0,
"price_currency": "EUR"
},
"aura": {
"adcpFormats": [
{
"agent_url": "https://creative.adcontextprotocol.org",
"id": "native_product_listing_v1"
}
]
}
}
}
]
}
Works With Context Enhancement
Combine price filtering with intent and sentiment signals:
{
"imp": [{
"id": "imp-1",
"native": { "request": "{\"native\":{\"ver\":\"1.2\",\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":80}}]}}" },
"ext": {
"affinity_ai_product_search": {
"max_price": 500.0,
"price_currency": "USD"
},
"aura": {
"adcpFormats": [...]
}
}
}],
"ext": {
"aura": {
"intent": {
"value": "purchase_inquiry",
"confidence": 0.85
},
"sentiment": {
"value": "positive",
"score": 0.72
}
}
}
}
Per-Impression Filtering
Different ad slots can have different price filters:
{
"imp": [
{
"id": "imp-1",
"native": {
"request": "{\"native\":{\"ver\":\"1.2\",\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":80}}]}}"
},
"ext": {
"affinity_ai_product_search": {
"max_price": 100.0
}
}
},
{
"id": "imp-2",
"native": {
"request": "{\"native\":{\"ver\":\"1.2\",\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":80}}]}}"
},
"ext": {
"affinity_ai_product_search": {
"min_price": 500.0
}
}
}
]
}
Performance Considerations
Latency
Price filtering adds minimal overhead:
- Parsing: ~1-2ms to extract extension
- Validation: ~1ms to validate values
- API Impact: No additional latency (same API call)
- Total Overhead: less than 5ms
Accuracy
Price filtering is deterministic:
- Exact Matching: Products filtered by exact price range
- No Approximation: No fuzzy matching or estimation
- Consistent Results: Same filter always returns same products
Scalability
Designed for high-volume traffic:
- Stateless: No session state required
- Cacheable: Filter logic can be cached
- Efficient: Simple numeric comparison
Privacy & Compliance
No PII
Product Search Extension never includes:
- User names or identifiers
- Email addresses or phone numbers
- Precise location data
- Browsing history
GDPR Compliant
- Contextual signals only (price range from query)
- No user tracking
- Consent not required for price filtering
- Right to object supported
CCPA Compliant
- No sale of personal information
- Opt-out mechanisms available
- Privacy string support
Documentation
Related Extensions
- Affinity AI Extensions Overview - All Affinity AI extensions
- Context Enhancement - Intent and sentiment signals
- AdCP Protocol - Creative assembly
Use Cases
- AI Search + LLM Weaving - Product search filtering in AI search
- AI Search Product Ads - Visual product listing ads
Reference
- OpenRTB 2.6 - Base protocol
- Use Cases - Implementation examples
Next Steps
- Understand the Extension: Review field definitions and validation rules
- Implement Query Parsing: Add price constraint detection to your platform
- Build Bid Requests: Include extension in OpenRTB requests
- Test Scenarios: Verify all usage scenarios work correctly
- Go Live: Deploy to production
Support
Questions?
- Check Use Cases
- Contact support
Feedback
- Submit via GitHub issues
- Join community discussions
- Participate in working groups