Skip to main content

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.

{
"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

FieldTypeRequiredDescriptionConstraints
min_pricefloatNoMinimum product price filterMust be non-negative; if both min/max set, min ≤ max
max_pricefloatNoMaximum product price filterMust be non-negative; if both min/max set, min ≤ max
price_currencystringNoCurrency code for price validationISO 4217 code (e.g., "EUR", "USD"); used for validation only, not sent to API

Validation Rules

  1. Price Range:

    • Both min_price and max_price are optional
    • If set, must be non-negative (≥ 0)
    • If both set, min_price must be ≤ max_price
    • Invalid values are ignored gracefully (not rejected)
  2. Currency:

    • price_currency is optional
    • If provided, must be valid ISO 4217 currency code
    • Used for validation/logging only, not passed to Affillizz API
  3. 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 800and800 and 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 800and800 and 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:

  1. Extract Price Values: Identify numeric price values in query
  2. Detect Range Type: Determine if min, max, or both
  3. Identify Currency: Extract currency from query or use default
  4. Validate Values: Ensure min ≤ max if both present
  5. 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

Use Cases

Reference

Next Steps

  1. Understand the Extension: Review field definitions and validation rules
  2. Implement Query Parsing: Add price constraint detection to your platform
  3. Build Bid Requests: Include extension in OpenRTB requests
  4. Test Scenarios: Verify all usage scenarios work correctly
  5. Go Live: Deploy to production

Support

Questions?

Feedback

  • Submit via GitHub issues
  • Join community discussions
  • Participate in working groups