@the_ro_show/agent-ads-sdk 0.11.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -418,6 +418,255 @@ curl -X POST https://peruwnbrqkvmrldhpoom.supabase.co/functions/v1/feedback \
418
418
 
419
419
  ---
420
420
 
421
+ ## 🎛️ Developer Controls (v0.12.0+)
422
+
423
+ Take control of what ads appear in your app with quality, category, and advertiser filters.
424
+
425
+ ### Quality Score Filter
426
+
427
+ Only show ads that meet your quality standards:
428
+
429
+ ```typescript
430
+ const ad = await client.decideFromContext({
431
+ userMessage: "I need car insurance",
432
+ minQualityScore: 0.8 // Only ads with 0.8+ quality
433
+ });
434
+ ```
435
+
436
+ **Use cases:**
437
+ - **Premium apps**: Set 0.9+ for best-in-class ads only
438
+ - **General apps**: Set 0.7+ for good balance
439
+ - **High-volume apps**: Set 0.5+ to maximize fill rate
440
+
441
+ ### Category Controls (IAB Taxonomy 3.0)
442
+
443
+ Filter ads using **IAB Content Taxonomy 3.0** - the industry-standard category system used by Google Ads, Microsoft Ads, and all major ad platforms.
444
+
445
+ **704 categories** across 38 top-level categories with 4-tier hierarchy.
446
+
447
+ #### Discover Available Categories
448
+
449
+ ```typescript
450
+ // Get all 38 top-level categories
451
+ const tier1 = await client.getCategories({ tier: 1 });
452
+ tier1.categories.forEach(cat => {
453
+ console.log(`${cat.id}: ${cat.name}`);
454
+ });
455
+ // Output: 1: Automotive, 31: Insurance, 150: Attractions, etc.
456
+
457
+ // Get all subcategories of "Automotive" (ID: 1)
458
+ const automotive = await client.getCategories({ parent_id: 1 });
459
+ // Returns: Auto Insurance (31), Auto Repair (34), Auto Buying (30), etc.
460
+
461
+ // Search for insurance-related categories
462
+ const insurance = await client.getCategories({ search: 'insurance' });
463
+ insurance.categories.forEach(cat => {
464
+ console.log(cat.full_path);
465
+ });
466
+ // Output: "Automotive > Auto Insurance", "Personal Finance > Insurance", etc.
467
+ ```
468
+
469
+ #### Filter by Category
470
+
471
+ Use IAB category IDs for precise control:
472
+
473
+ ```typescript
474
+ // Insurance app: Only show insurance ads (by IAB ID)
475
+ const ad = await client.decideFromContext({
476
+ userMessage: "I need car insurance",
477
+ allowedCategories: [31] // 31 = Auto Insurance
478
+ });
479
+
480
+ // Block entire "Sensitive Topics" tree (IAB ID: 601)
481
+ // This blocks all gambling, adult content, controversial topics, etc.
482
+ const ad = await client.decideFromContext({
483
+ userMessage: "Help me with something",
484
+ blockedCategories: [601] // Blocks parent + all children
485
+ });
486
+
487
+ // Wedding planner: Allow wedding + photography + food
488
+ const ad = await client.decideFromContext({
489
+ userMessage: "Help me plan my wedding",
490
+ allowedCategories: [
491
+ 603, // Weddings (Personal Celebrations)
492
+ 162, // Photography (Hobbies & Interests)
493
+ 190 // Restaurants (Food & Drink)
494
+ ]
495
+ });
496
+ ```
497
+
498
+ **Parent-Child Relationships:**
499
+ - Blocking a parent category blocks ALL child categories automatically
500
+ - Example: Blocking `1` (Automotive) blocks Auto Insurance, Auto Repair, etc.
501
+ - Great for compliance: Block `601` (Sensitive Topics) to block gambling, adult, controversial in one go
502
+
503
+ **Important:**
504
+ - If `allowedCategories` is set, `blockedCategories` is ignored
505
+ - IAB category IDs (numbers) are **recommended** - legacy string categories are deprecated (support ends 2026-06-01)
506
+ - Use `getCategories()` API to discover category IDs
507
+ - Empty `allowedCategories: []` is rejected (would block all ads - use `blockedCategories` instead)
508
+
509
+ **Validation rules:**
510
+ - `minQualityScore`: Must be 0.0-1.0
511
+ - `allowedCategories`: Must be non-empty array of strings or numbers
512
+ - `blockedCategories`: Must be array of strings or numbers
513
+ - `blockedAdvertisers`: Must be array of non-empty strings (advertiser IDs)
514
+
515
+ ### Advertiser Blocklist
516
+
517
+ Block specific advertisers based on user feedback:
518
+
519
+ ```typescript
520
+ const ad = await client.decideFromContext({
521
+ userMessage: "I need legal help",
522
+ blockedAdvertisers: ['adv_abc123', 'adv_xyz789']
523
+ });
524
+ ```
525
+
526
+ Get advertiser IDs from previous ad responses.
527
+
528
+ ## 💰 Revenue Optimization (v0.13.1+)
529
+
530
+ Control revenue vs. relevance tradeoffs with bid and relevance filters.
531
+
532
+ ### Minimum CPC Filter
533
+
534
+ Only show ads above a minimum bid threshold:
535
+
536
+ ```typescript
537
+ const ad = await client.decideFromContext({
538
+ userMessage: "I need car insurance",
539
+ minCPC: 100 // Only ads bidding ≥$1.00 per click
540
+ });
541
+ ```
542
+
543
+ **Use cases:**
544
+ - **Premium apps**: `minCPC: 200` for $2+ ads only
545
+ - **High-value verticals**: Filter out low-budget advertisers
546
+ - **Revenue targets**: Ensure minimum revenue per impression
547
+
548
+ ### Minimum Relevance Score
549
+
550
+ Only show ads that are semantically relevant:
551
+
552
+ ```typescript
553
+ const ad = await client.decideFromContext({
554
+ userMessage: "Help me plan my wedding",
555
+ minRelevanceScore: 0.8 // Only highly relevant ads (0.0-1.0)
556
+ });
557
+ ```
558
+
559
+ **Use cases:**
560
+ - **Niche apps**: Legal assistant = `0.8+` for specialized content only
561
+ - **User experience**: Filter out loosely related ads
562
+ - **Context matching**: Ensure ads match conversation topic
563
+ - **Default threshold**: Backend uses 0.25 minimum
564
+
565
+ ### Ranking Strategy
566
+
567
+ Choose how ads are ranked:
568
+
569
+ ```typescript
570
+ // Default: Revenue-optimized (highest bid wins)
571
+ const ad = await client.decideFromContext({
572
+ userMessage: "I need legal help",
573
+ optimizeFor: 'revenue' // Rank by bid × quality × relevance
574
+ });
575
+
576
+ // Alternative: Relevance-optimized (best match wins)
577
+ const ad = await client.decideFromContext({
578
+ userMessage: "I need legal help",
579
+ optimizeFor: 'relevance' // Rank by semantic similarity only
580
+ });
581
+ ```
582
+
583
+ **Use cases:**
584
+ - **General apps**: `'revenue'` to maximize earnings
585
+ - **Niche apps**: `'relevance'` to prioritize perfect matches over high bids
586
+ - **Premium experiences**: Combine with high `minRelevanceScore` + `'relevance'` ranking
587
+
588
+ **How it works (second-price auction):**
589
+ - **Revenue mode**: Winner ranked by composite score (bid × quality × relevance), pays just enough to beat next ad's composite score + $0.01
590
+ - **Relevance mode**: Winner ranked by semantic similarity, pays just enough to beat next ad in the composite score space + $0.01
591
+ - **Always capped**: Winner never pays more than their max bid (auction integrity guaranteed)
592
+ - **Floor price**: Minimum $0.25 clearing price ensures platform revenue
593
+
594
+ **Important notes:**
595
+ - `minRelevanceScore` only applies to campaigns with semantic targeting
596
+ - For keyword/automatic campaigns, relevance filter has no effect
597
+ - Validation errors return HTTP 400 with clear messages
598
+
599
+ ### Combined Revenue Controls
600
+
601
+ ```typescript
602
+ // Premium legal assistant: High relevance + high bids
603
+ const ad = await client.decideFromContext({
604
+ userMessage: "I need estate planning help",
605
+ minRelevanceScore: 0.8, // Only highly relevant
606
+ minCPC: 200, // Only $2+ bids
607
+ optimizeFor: 'relevance', // Best match wins
608
+ allowedCategories: [318] // Legal services only (IAB)
609
+ });
610
+
611
+ // General chatbot: Maximize revenue
612
+ const ad = await client.decideFromContext({
613
+ userMessage: "Help me with something",
614
+ optimizeFor: 'revenue', // Highest bid wins
615
+ minQualityScore: 0.7 // Decent quality threshold
616
+ });
617
+ ```
618
+
619
+ ### Combined Controls (All Phases)
620
+
621
+ Mix and match for precise control:
622
+
623
+ ```typescript
624
+ const ad = await client.decideFromContext({
625
+ userMessage: "I need car insurance",
626
+ // Phase 1: Quality & Brand Safety
627
+ minQualityScore: 0.8, // High quality only
628
+ allowedCategories: [31, 398], // Auto + Personal Finance Insurance (IAB)
629
+ // Phase 2: Revenue Optimization
630
+ minCPC: 50, // $0.50+ bids only
631
+ minRelevanceScore: 0.7, // Highly relevant only
632
+ optimizeFor: 'revenue' // Highest bid wins
633
+ });
634
+ ```
635
+
636
+ ### HTTP Example
637
+
638
+ ```bash
639
+ curl -X POST https://peruwnbrqkvmrldhpoom.supabase.co/functions/v1/decide \
640
+ -H "X-AM-API-Key: am_live_YOUR_KEY" \
641
+ -H "Content-Type: application/json" \
642
+ -d '{
643
+ "context": "I need car insurance",
644
+ "minQualityScore": 0.8,
645
+ "blockedCategories": ["crypto", "gambling"]
646
+ }'
647
+ ```
648
+
649
+ ### Best Practices
650
+
651
+ **Quality Scores:**
652
+ - Start with no filter, monitor what you get
653
+ - Gradually increase threshold if quality is lacking
654
+ - Higher thresholds = fewer ads but better quality
655
+
656
+ **Categories (IAB Taxonomy):**
657
+ - Use `getCategories()` to discover the full 704-category taxonomy
658
+ - Use `allowedCategories` for specialized apps (wedding planner = `[603]`, legal assistant = `[318]`)
659
+ - Use `blockedCategories` for compliance (kids apps = block `[601]` Sensitive Topics)
660
+ - Parent categories block all children automatically (block `[1]` Automotive = blocks all 40+ auto subcategories)
661
+ - Full taxonomy reference: https://github.com/InteractiveAdvertisingBureau/Taxonomies
662
+
663
+ **Advertiser Blocking:**
664
+ - Collect user feedback on specific ads
665
+ - Block advertisers with multiple complaints
666
+ - Use sparingly - too many blocks reduce fill rate
667
+
668
+ ---
669
+
421
670
  ## API Reference
422
671
 
423
672
  ### `POST /v1/decide`
package/dist/index.d.mts CHANGED
@@ -63,6 +63,68 @@ interface DecideFromContextRequest {
63
63
  language?: string;
64
64
  /** User's platform. Default: 'web' */
65
65
  platform?: 'web' | 'ios' | 'android' | 'desktop' | 'voice' | 'other';
66
+ /**
67
+ * Minimum quality score threshold (0.0 - 1.0).
68
+ * Only return ads with quality scores at or above this value.
69
+ * Higher values = better quality but lower fill rate.
70
+ *
71
+ * @example
72
+ * minQualityScore: 0.8 // Only show ads with 0.8+ quality
73
+ */
74
+ minQualityScore?: number;
75
+ /**
76
+ * Only show ads from these categories.
77
+ * If set, blockedCategories is ignored.
78
+ *
79
+ * @example
80
+ * allowedCategories: ['wedding', 'photography', 'venues']
81
+ */
82
+ allowedCategories?: string[];
83
+ /**
84
+ * Never show ads from these categories.
85
+ * Ignored if allowedCategories is set.
86
+ *
87
+ * @example
88
+ * blockedCategories: ['gambling', 'crypto', 'dating']
89
+ */
90
+ blockedCategories?: string[];
91
+ /**
92
+ * Never show ads from these advertisers.
93
+ * Use advertiser IDs from previous ad responses.
94
+ *
95
+ * @example
96
+ * blockedAdvertisers: ['adv_abc123', 'adv_xyz789']
97
+ */
98
+ blockedAdvertisers?: string[];
99
+ /**
100
+ * Minimum cost-per-click threshold in cents.
101
+ * Only return ads with bids at or above this value.
102
+ * Higher values = more revenue per ad but lower fill rate.
103
+ *
104
+ * @example
105
+ * minCPC: 50 // Only show ads bidding $0.50 or more per click
106
+ */
107
+ minCPC?: number;
108
+ /**
109
+ * Minimum semantic relevance score threshold (0.0 - 1.0).
110
+ * Only return ads with relevance scores at or above this value.
111
+ * Higher values = more relevant ads but lower fill rate.
112
+ * Default backend threshold: 0.25
113
+ *
114
+ * @example
115
+ * minRelevanceScore: 0.8 // Only show highly relevant ads
116
+ */
117
+ minRelevanceScore?: number;
118
+ /**
119
+ * Ad ranking strategy.
120
+ * - 'revenue': Rank by bid amount (highest bid wins)
121
+ * - 'relevance': Rank by semantic similarity (best match wins)
122
+ * Default: 'revenue'
123
+ *
124
+ * @example
125
+ * optimizeFor: 'relevance' // Prioritize best semantic match over highest bid
126
+ */
127
+ optimizeFor?: 'revenue' | 'relevance';
66
128
  }
67
129
  interface DecideResponse {
68
130
  request_id: string;
@@ -557,6 +619,44 @@ interface GetServiceRequest {
557
619
  region?: string;
558
620
  };
559
621
  }
622
+ interface IABCategory {
623
+ /** Unique IAB category ID */
624
+ id: number;
625
+ /** Parent category ID (null for tier 1 categories) */
626
+ parent_id: number | null;
627
+ /** Category name */
628
+ name: string;
629
+ /** Tier 1 category (top level) */
630
+ tier_1: string | null;
631
+ /** Tier 2 category (sub-category) */
632
+ tier_2: string | null;
633
+ /** Tier 3 category (sub-sub-category) */
634
+ tier_3: string | null;
635
+ /** Tier 4 category (deepest level) */
636
+ tier_4: string | null;
637
+ /** Full hierarchical path (e.g., "Automotive > Auto Insurance") */
638
+ full_path: string;
639
+ }
640
+ interface CategoryTaxonomyResponse {
641
+ /** Taxonomy version */
642
+ version: string;
643
+ /** Source organization */
644
+ source: string;
645
+ /** Reference URL */
646
+ url: string;
647
+ /** Total number of categories returned (after filtering) */
648
+ total: number;
649
+ /** Array of categories */
650
+ categories: IABCategory[];
651
+ }
652
+ interface GetCategoriesParams {
653
+ /** Filter by tier level (1-4) */
654
+ tier?: 1 | 2 | 3 | 4;
655
+ /** Filter by parent category ID */
656
+ parent_id?: number;
657
+ /** Search by name or path (case-insensitive) */
658
+ search?: string;
659
+ }
560
660
 
561
661
  /**
562
662
  * Utility functions for the AttentionMarket SDK.
@@ -989,6 +1089,39 @@ declare class AttentionMarketClient {
989
1089
  * ```
990
1090
  */
991
1091
  logServiceResult(params: ServiceResultRequest): Promise<ServiceResultResponse>;
1092
+ /**
1093
+ * Get IAB Content Taxonomy categories.
1094
+ *
1095
+ * Returns the complete IAB Content Taxonomy 3.0 (704 categories across 38 top-level categories).
1096
+ * Supports filtering by tier level, parent category, or search term.
1097
+ *
1098
+ * Use this to discover available categories for `allowedCategories` and `blockedCategories` parameters.
1099
+ *
1100
+ * @param params - Optional filters (tier, parent_id, search)
1101
+ * @returns The IAB Content Taxonomy with categories
1102
+ *
1103
+ * @example Get all Tier 1 categories (38 top-level categories)
1104
+ * ```typescript
1105
+ * const tier1 = await client.getCategories({ tier: 1 });
1106
+ * console.log(`${tier1.total} top-level categories`);
1107
+ * tier1.categories.forEach(cat => console.log(`${cat.id}: ${cat.name}`));
1108
+ * ```
1109
+ *
1110
+ * @example Get all subcategories of "Automotive" (ID: 1)
1111
+ * ```typescript
1112
+ * const automotiveCategories = await client.getCategories({ parent_id: 1 });
1113
+ * console.log(`Found ${automotiveCategories.total} automotive subcategories`);
1114
+ * ```
1115
+ *
1116
+ * @example Search for insurance-related categories
1117
+ * ```typescript
1118
+ * const insuranceCategories = await client.getCategories({ search: 'insurance' });
1119
+ * insuranceCategories.categories.forEach(cat => {
1120
+ * console.log(`${cat.id}: ${cat.full_path}`);
1121
+ * });
1122
+ * ```
1123
+ */
1124
+ getCategories(params?: GetCategoriesParams): Promise<CategoryTaxonomyResponse>;
992
1125
  }
993
1126
 
994
1127
  /**
package/dist/index.d.ts CHANGED
@@ -63,6 +63,68 @@ interface DecideFromContextRequest {
63
63
  language?: string;
64
64
  /** User's platform. Default: 'web' */
65
65
  platform?: 'web' | 'ios' | 'android' | 'desktop' | 'voice' | 'other';
66
+ /**
67
+ * Minimum quality score threshold (0.0 - 1.0).
68
+ * Only return ads with quality scores at or above this value.
69
+ * Higher values = better quality but lower fill rate.
70
+ *
71
+ * @example
72
+ * minQualityScore: 0.8 // Only show ads with 0.8+ quality
73
+ */
74
+ minQualityScore?: number;
75
+ /**
76
+ * Only show ads from these categories.
77
+ * If set, blockedCategories is ignored.
78
+ *
79
+ * @example
80
+ * allowedCategories: ['wedding', 'photography', 'venues']
81
+ */
82
+ allowedCategories?: string[];
83
+ /**
84
+ * Never show ads from these categories.
85
+ * Ignored if allowedCategories is set.
86
+ *
87
+ * @example
88
+ * blockedCategories: ['gambling', 'crypto', 'dating']
89
+ */
90
+ blockedCategories?: string[];
91
+ /**
92
+ * Never show ads from these advertisers.
93
+ * Use advertiser IDs from previous ad responses.
94
+ *
95
+ * @example
96
+ * blockedAdvertisers: ['adv_abc123', 'adv_xyz789']
97
+ */
98
+ blockedAdvertisers?: string[];
99
+ /**
100
+ * Minimum cost-per-click threshold in cents.
101
+ * Only return ads with bids at or above this value.
102
+ * Higher values = more revenue per ad but lower fill rate.
103
+ *
104
+ * @example
105
+ * minCPC: 50 // Only show ads bidding $0.50 or more per click
106
+ */
107
+ minCPC?: number;
108
+ /**
109
+ * Minimum semantic relevance score threshold (0.0 - 1.0).
110
+ * Only return ads with relevance scores at or above this value.
111
+ * Higher values = more relevant ads but lower fill rate.
112
+ * Default backend threshold: 0.25
113
+ *
114
+ * @example
115
+ * minRelevanceScore: 0.8 // Only show highly relevant ads
116
+ */
117
+ minRelevanceScore?: number;
118
+ /**
119
+ * Ad ranking strategy.
120
+ * - 'revenue': Rank by bid amount (highest bid wins)
121
+ * - 'relevance': Rank by semantic similarity (best match wins)
122
+ * Default: 'revenue'
123
+ *
124
+ * @example
125
+ * optimizeFor: 'relevance' // Prioritize best semantic match over highest bid
126
+ */
127
+ optimizeFor?: 'revenue' | 'relevance';
66
128
  }
67
129
  interface DecideResponse {
68
130
  request_id: string;
@@ -557,6 +619,44 @@ interface GetServiceRequest {
557
619
  region?: string;
558
620
  };
559
621
  }
622
+ interface IABCategory {
623
+ /** Unique IAB category ID */
624
+ id: number;
625
+ /** Parent category ID (null for tier 1 categories) */
626
+ parent_id: number | null;
627
+ /** Category name */
628
+ name: string;
629
+ /** Tier 1 category (top level) */
630
+ tier_1: string | null;
631
+ /** Tier 2 category (sub-category) */
632
+ tier_2: string | null;
633
+ /** Tier 3 category (sub-sub-category) */
634
+ tier_3: string | null;
635
+ /** Tier 4 category (deepest level) */
636
+ tier_4: string | null;
637
+ /** Full hierarchical path (e.g., "Automotive > Auto Insurance") */
638
+ full_path: string;
639
+ }
640
+ interface CategoryTaxonomyResponse {
641
+ /** Taxonomy version */
642
+ version: string;
643
+ /** Source organization */
644
+ source: string;
645
+ /** Reference URL */
646
+ url: string;
647
+ /** Total number of categories returned (after filtering) */
648
+ total: number;
649
+ /** Array of categories */
650
+ categories: IABCategory[];
651
+ }
652
+ interface GetCategoriesParams {
653
+ /** Filter by tier level (1-4) */
654
+ tier?: 1 | 2 | 3 | 4;
655
+ /** Filter by parent category ID */
656
+ parent_id?: number;
657
+ /** Search by name or path (case-insensitive) */
658
+ search?: string;
659
+ }
560
660
 
561
661
  /**
562
662
  * Utility functions for the AttentionMarket SDK.
@@ -989,6 +1089,39 @@ declare class AttentionMarketClient {
989
1089
  * ```
990
1090
  */
991
1091
  logServiceResult(params: ServiceResultRequest): Promise<ServiceResultResponse>;
1092
+ /**
1093
+ * Get IAB Content Taxonomy categories.
1094
+ *
1095
+ * Returns the complete IAB Content Taxonomy 3.0 (704 categories across 38 top-level categories).
1096
+ * Supports filtering by tier level, parent category, or search term.
1097
+ *
1098
+ * Use this to discover available categories for `allowedCategories` and `blockedCategories` parameters.
1099
+ *
1100
+ * @param params - Optional filters (tier, parent_id, search)
1101
+ * @returns The IAB Content Taxonomy with categories
1102
+ *
1103
+ * @example Get all Tier 1 categories (38 top-level categories)
1104
+ * ```typescript
1105
+ * const tier1 = await client.getCategories({ tier: 1 });
1106
+ * console.log(`${tier1.total} top-level categories`);
1107
+ * tier1.categories.forEach(cat => console.log(`${cat.id}: ${cat.name}`));
1108
+ * ```
1109
+ *
1110
+ * @example Get all subcategories of "Automotive" (ID: 1)
1111
+ * ```typescript
1112
+ * const automotiveCategories = await client.getCategories({ parent_id: 1 });
1113
+ * console.log(`Found ${automotiveCategories.total} automotive subcategories`);
1114
+ * ```
1115
+ *
1116
+ * @example Search for insurance-related categories
1117
+ * ```typescript
1118
+ * const insuranceCategories = await client.getCategories({ search: 'insurance' });
1119
+ * insuranceCategories.categories.forEach(cat => {
1120
+ * console.log(`${cat.id}: ${cat.full_path}`);
1121
+ * });
1122
+ * ```
1123
+ */
1124
+ getCategories(params?: GetCategoriesParams): Promise<CategoryTaxonomyResponse>;
992
1125
  }
993
1126
 
994
1127
  /**
package/dist/index.js CHANGED
@@ -503,6 +503,60 @@ var AttentionMarketClient = class {
503
503
  const platform = params.platform || "web";
504
504
  const placementType = params.placement || "sponsored_suggestion";
505
505
  const taxonomy = params.suggestedCategory || this.inferTaxonomy(params.userMessage);
506
+ if (params.minQualityScore !== void 0) {
507
+ if (typeof params.minQualityScore !== "number" || params.minQualityScore < 0 || params.minQualityScore > 1) {
508
+ throw new Error("minQualityScore must be a number between 0.0 and 1.0");
509
+ }
510
+ }
511
+ if (params.allowedCategories !== void 0) {
512
+ if (!Array.isArray(params.allowedCategories)) {
513
+ throw new Error("allowedCategories must be an array");
514
+ }
515
+ if (params.allowedCategories.length === 0) {
516
+ throw new Error("allowedCategories cannot be empty (would block all ads). Use blockedCategories to exclude specific categories, or omit to allow all.");
517
+ }
518
+ for (const cat of params.allowedCategories) {
519
+ if (typeof cat !== "string" && typeof cat !== "number") {
520
+ throw new Error(`allowedCategories must contain strings or numbers, found: ${typeof cat}`);
521
+ }
522
+ }
523
+ }
524
+ if (params.blockedCategories !== void 0) {
525
+ if (!Array.isArray(params.blockedCategories)) {
526
+ throw new Error("blockedCategories must be an array");
527
+ }
528
+ for (const cat of params.blockedCategories) {
529
+ if (typeof cat !== "string" && typeof cat !== "number") {
530
+ throw new Error(`blockedCategories must contain strings or numbers, found: ${typeof cat}`);
531
+ }
532
+ }
533
+ if (params.allowedCategories && params.allowedCategories.length > 0) {
534
+ console.warn("[AttentionMarket] Both allowedCategories and blockedCategories specified. blockedCategories will be ignored.");
535
+ }
536
+ }
537
+ if (params.blockedAdvertisers !== void 0) {
538
+ if (!Array.isArray(params.blockedAdvertisers)) {
539
+ throw new Error("blockedAdvertisers must be an array");
540
+ }
541
+ for (const id of params.blockedAdvertisers) {
542
+ if (typeof id !== "string" || id.trim().length === 0) {
543
+ throw new Error("blockedAdvertisers must contain non-empty strings (advertiser IDs)");
544
+ }
545
+ }
546
+ }
547
+ if (params.minCPC !== void 0) {
548
+ if (typeof params.minCPC !== "number" || params.minCPC < 0) {
549
+ throw new Error("minCPC must be a non-negative number (cost-per-click in cents)");
550
+ }
551
+ }
552
+ if (params.minRelevanceScore !== void 0) {
553
+ if (typeof params.minRelevanceScore !== "number" || params.minRelevanceScore < 0 || params.minRelevanceScore > 1) {
554
+ throw new Error("minRelevanceScore must be a number between 0.0 and 1.0");
555
+ }
556
+ }
557
+ if (params.optimizeFor && params.optimizeFor !== "revenue" && params.optimizeFor !== "relevance") {
558
+ throw new Error('optimizeFor must be either "revenue" or "relevance"');
559
+ }
506
560
  const request = {
507
561
  request_id: generateUUID(),
508
562
  agent_id: this.agentId,
@@ -529,7 +583,16 @@ var AttentionMarketClient = class {
529
583
  }
530
584
  },
531
585
  context,
532
- user_intent: params.userMessage
586
+ user_intent: params.userMessage,
587
+ // Developer controls (Phase 1: Quality & Brand Safety)
588
+ ...params.minQualityScore !== void 0 && { minQualityScore: params.minQualityScore },
589
+ ...params.allowedCategories && { allowedCategories: params.allowedCategories },
590
+ ...params.blockedCategories && { blockedCategories: params.blockedCategories },
591
+ ...params.blockedAdvertisers && { blockedAdvertisers: params.blockedAdvertisers },
592
+ // Developer controls (Phase 2: Revenue Optimization)
593
+ ...params.minCPC !== void 0 && { minCPC: params.minCPC },
594
+ ...params.minRelevanceScore !== void 0 && { minRelevanceScore: params.minRelevanceScore },
595
+ ...params.optimizeFor && { optimizeFor: params.optimizeFor }
533
596
  };
534
597
  const response = await this.decideRaw(request, options);
535
598
  if (response.status === "no_fill" || response.units.length === 0) {
@@ -1156,6 +1219,55 @@ var AttentionMarketClient = class {
1156
1219
  { body: params }
1157
1220
  );
1158
1221
  }
1222
+ /**
1223
+ * Get IAB Content Taxonomy categories.
1224
+ *
1225
+ * Returns the complete IAB Content Taxonomy 3.0 (704 categories across 38 top-level categories).
1226
+ * Supports filtering by tier level, parent category, or search term.
1227
+ *
1228
+ * Use this to discover available categories for `allowedCategories` and `blockedCategories` parameters.
1229
+ *
1230
+ * @param params - Optional filters (tier, parent_id, search)
1231
+ * @returns The IAB Content Taxonomy with categories
1232
+ *
1233
+ * @example Get all Tier 1 categories (38 top-level categories)
1234
+ * ```typescript
1235
+ * const tier1 = await client.getCategories({ tier: 1 });
1236
+ * console.log(`${tier1.total} top-level categories`);
1237
+ * tier1.categories.forEach(cat => console.log(`${cat.id}: ${cat.name}`));
1238
+ * ```
1239
+ *
1240
+ * @example Get all subcategories of "Automotive" (ID: 1)
1241
+ * ```typescript
1242
+ * const automotiveCategories = await client.getCategories({ parent_id: 1 });
1243
+ * console.log(`Found ${automotiveCategories.total} automotive subcategories`);
1244
+ * ```
1245
+ *
1246
+ * @example Search for insurance-related categories
1247
+ * ```typescript
1248
+ * const insuranceCategories = await client.getCategories({ search: 'insurance' });
1249
+ * insuranceCategories.categories.forEach(cat => {
1250
+ * console.log(`${cat.id}: ${cat.full_path}`);
1251
+ * });
1252
+ * ```
1253
+ */
1254
+ async getCategories(params) {
1255
+ const queryParams = new URLSearchParams();
1256
+ if (params?.tier) {
1257
+ queryParams.append("tier", params.tier.toString());
1258
+ }
1259
+ if (params?.parent_id) {
1260
+ queryParams.append("parent_id", params.parent_id.toString());
1261
+ }
1262
+ if (params?.search) {
1263
+ queryParams.append("search", params.search);
1264
+ }
1265
+ const url = `/v1/categories${queryParams.toString() ? "?" + queryParams.toString() : ""}`;
1266
+ return await this.http.request(
1267
+ "GET",
1268
+ url
1269
+ );
1270
+ }
1159
1271
  };
1160
1272
 
1161
1273
  // src/mock-client.ts