s-m-r-t

@happyvertical/smrt-ads

Ad management system with waterfall priority, zone targeting, weighted A/B testing, and immutable event tracking.

v0.19.0WaterfallA/B TestingIAB Formats

Overview

smrt-ads provides ad campaign management with waterfall priority delivery, zone-based targeting, weighted variation selection, and immutable event tracking.

Installation

bash
npm install @happyvertical/smrt-ads

Quick Start

typescript
import {
  AdDeliveryTierCollection, AdGroupCollection,
  AdVariationCollection, AdEventCollection
} from '@happyvertical/smrt-ads';

// Create delivery tier
const tier1 = await tiers.create({
  name: 'Premium',
  priority: 1,  // Highest priority
  pricingModel: 'CPM'
});
await tier1.save();

// Create ad group
const group = await groups.create({
  contractId: contract.id,
  tierName: tier1.name,
  verticalSlug: 'automotive',
  zoneIds: ['zone-1', 'zone-2'],  // smrt-properties zones
  targeting: { device: 'mobile', geo: 'US' },
  dailyBudget: 500,
  startDate: new Date('2025-01-20'),
  endDate: new Date('2025-02-20'),
  status: 'ACTIVE'
});
await group.save();

// Create variations with weights
const varA = await variations.create({
  groupId: group.id,
  formatName: 'leaderboard-728x90',
  assetId: asset1.id,
  clickUrl: 'https://example.com/promo',
  weight: 70  // 70% traffic
});
await varA.save();

const varB = await variations.create({
  groupId: group.id,
  formatName: 'leaderboard-728x90',
  assetId: asset2.id,
  clickUrl: 'https://example.com/promo2',
  weight: 30  // 30% traffic
});
await varB.save();

// Select variation (weighted random)
const selected = await variations.selectByWeight(group.id);

// Track impression
await events.create({
  eventType: 'IMPRESSION',
  variationId: selected.id,
  zoneId: 'zone-1',
  siteId: property.id
});

Core Models

AdDeliveryTier (Priority)

typescript
class AdDeliveryTier extends SmrtObject {
  name: string
  priority: number          // 1=highest, 2, 3...
  pricingModel: 'FIXED' | 'CPM' | 'CPC' | 'CPA'
  description?: string

  isHigherPriorityThan(other: AdDeliveryTier): boolean
  isFixedPricing(): boolean
  isPerformanceBased(): boolean
}

AdGroup (Campaign)

typescript
class AdGroup extends SmrtObject {
  contractId: string        // FK to Contract (smrt-commerce)
  tierName: string          // Delivery tier
  verticalSlug?: string     // FK to Tag (smrt-tags)
  zoneIds: string[]         // FK to Zone[] (smrt-properties)
  targeting: Record<string, any>
  dailyBudget?: number
  totalBudget?: number
  startDate: Date
  endDate: Date
  status: 'DRAFT' | 'ACTIVE' | 'PAUSED' | 'COMPLETED'

  isActive(): boolean
  hasZoneId(zoneId: string): boolean
  addZoneId(zoneId: string): void
  removeZoneId(zoneId: string): void
}

AdVariation (Creative)

typescript
class AdVariation extends SmrtObject {
  groupId: string
  formatName: string        // FK to AdFormat
  assetId: string           // FK to Asset (smrt-assets)
  clickUrl: string
  altText?: string
  weight: number            // For A/B testing (default: 100)
  impressions: number       // Denormalized
  clicks: number            // Denormalized
  status: 'DRAFT' | 'ACTIVE' | 'PAUSED'

  getCTR(): number
  recordImpression(): void
  recordClick(): void
}

Waterfall Priority & Selection

typescript
// Ad selection algorithm
async function selectAd(zoneId: string) {
  // 1. Find eligible groups for zone
  const eligibleGroups = await groups.findEligibleForZone(zoneId);
  // Filters: ACTIVE, in date range, has zoneId

  // 2. Sort by tier priority (1 first)
  eligibleGroups.sort((a, b) =>
    tierMap[a.tierName].priority - tierMap[b.tierName].priority
  );

  // 3. Select first group with available variations
  for (const group of eligibleGroups) {
    const variation = await variations.selectByWeight(group.id);
    if (variation) return variation;
  }

  return null; // No ads available
}

// Weighted selection (A/B testing)
const selected = await variations.selectByWeight(groupId);
// Weight 70 vs 30 = 70% chance of first variation

Performance Tracking

typescript
// Track events (immutable)
await events.create({
  eventType: 'IMPRESSION',
  variationId: variation.id,
  zoneId: zoneId,
  siteId: propertyId,
  metadata: { userAgent: req.headers['user-agent'] }
});

await events.create({
  eventType: 'CLICK',
  variationId: variation.id,
  zoneId: zoneId,
  siteId: propertyId
});

// Get stats
const stats = await events.getVariationStats(variation.id);
console.log(`CTR: ${stats.ctr}%, Conversions: ${stats.conversions}`);

// Top performers
const topAds = await events.findTopPerformers(10);

// Date range analysis
const weekEvents = await events.findByDateRange(
  new Date('2025-01-20'),
  new Date('2025-01-27')
);

Best Practices

DOs

  • Use lower priority numbers for premium tiers (1=highest)
  • Set reasonable weights for A/B tests (sum doesn't need to be 100)
  • Track impressions before serving to prevent double-counting
  • Use targeting JSON for flexible audience rules
  • Link to smrt-commerce contracts for billing

DON'Ts

  • Don't modify AdEvent records (immutable by design)
  • Don't set weight to 0 (makes variation unselectable)
  • Don't forget to filter by date range and status
  • Don't serve ads without zone eligibility check
  • Don't ignore tier priority ordering

Related Modules