@the_ro_show/agent-ads-sdk 0.13.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
@@ -500,7 +500,17 @@ const ad = await client.decideFromContext({
500
500
  - Example: Blocking `1` (Automotive) blocks Auto Insurance, Auto Repair, etc.
501
501
  - Great for compliance: Block `601` (Sensitive Topics) to block gambling, adult, controversial in one go
502
502
 
503
- **Note:** If `allowedCategories` is set, `blockedCategories` is ignored.
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)
504
514
 
505
515
  ### Advertiser Blocklist
506
516
 
@@ -515,16 +525,111 @@ const ad = await client.decideFromContext({
515
525
 
516
526
  Get advertiser IDs from previous ad responses.
517
527
 
518
- ### Combined Controls
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)
519
620
 
520
621
  Mix and match for precise control:
521
622
 
522
623
  ```typescript
523
624
  const ad = await client.decideFromContext({
524
625
  userMessage: "I need car insurance",
626
+ // Phase 1: Quality & Brand Safety
525
627
  minQualityScore: 0.8, // High quality only
526
- blockedCategories: ['crypto'], // No crypto ads
527
- allowedCategories: ['insurance', 'finance'] // Relevant 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
528
633
  });
529
634
  ```
530
635
 
package/dist/index.d.mts CHANGED
@@ -96,6 +96,35 @@ interface DecideFromContextRequest {
96
96
  * blockedAdvertisers: ['adv_abc123', 'adv_xyz789']
97
97
  */
98
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';
99
128
  }
100
129
  interface DecideResponse {
101
130
  request_id: string;
package/dist/index.d.ts CHANGED
@@ -96,6 +96,35 @@ interface DecideFromContextRequest {
96
96
  * blockedAdvertisers: ['adv_abc123', 'adv_xyz789']
97
97
  */
98
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';
99
128
  }
100
129
  interface DecideResponse {
101
130
  request_id: string;
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,
@@ -534,7 +588,11 @@ var AttentionMarketClient = class {
534
588
  ...params.minQualityScore !== void 0 && { minQualityScore: params.minQualityScore },
535
589
  ...params.allowedCategories && { allowedCategories: params.allowedCategories },
536
590
  ...params.blockedCategories && { blockedCategories: params.blockedCategories },
537
- ...params.blockedAdvertisers && { blockedAdvertisers: params.blockedAdvertisers }
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 }
538
596
  };
539
597
  const response = await this.decideRaw(request, options);
540
598
  if (response.status === "no_fill" || response.units.length === 0) {