@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-adsQuick 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 variationPerformance 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