Skip to content

Engagement Forecaster

The Engagement Forecaster predicts the perfect moment to send notifications, maximizing welcome probability while minimizing disruption. It's fully integrated into the MARL system.


Overview

Mission: Intelligent notification timing for maximum welcome probability

Key Innovation: 200+ context signals + MARL integration (not a siloed service)

Expected Lift: 23%+ CTR improvement (based on Meta research)

Phase: Introduced in Phase 3 (Week 10)


Context Signals

200+ Dimensional Context

interface NotificationContext {
  // Temporal (15 features)
  timeOfDay: number;              // 0-23
  dayOfWeek: number;              // 0-6
  isWeekend: boolean;
  isHoliday: boolean;
  daysUntilWedding: number;
  weeksSinceEngagement: number;
  planningPhase: string;
  seasonalContext: string;
  hourOfWeek: number;             // 0-167
  timeZone: string;
  localTime: number;
  businessHours: boolean;
  dinnerTime: boolean;
  bedTime: boolean;
  morningCommute: boolean;

  // Behavioral (30 features)
  avgSessionDuration: number;
  sessionsPerWeek: number;
  lastActiveHours: number;
  lastActiveDay: string;
  typicalActiveHours: number[];
  notificationOpenRate: number;
  notificationOptOutRate: number;
  preferredContentTypes: string[];
  engagementPatterns: TimeSeries;
  browsingVelocity: number;
  saveRate: number;
  shareRate: number;
  inquiryRate: number;
  returnUserProbability: number;
  sessionStartTimes: number[];
  sessionEndTimes: number[];
  averageEngagementDelay: number;
  contentPreferences: Map<string, number>;
  interactionDepth: number;
  multiDayEngagement: boolean;
  // ... 10 more behavioral features

  // Device/Environment (20 features)
  deviceType: string;
  osVersion: string;
  appVersion: string;
  batteryLevel: number;
  batteryCharging: boolean;
  networkQuality: string;
  networkType: string;            // wifi, cellular, etc.
  screenBrightness: number;
  doNotDisturb: boolean;
  silentMode: boolean;
  locationContext: string;        // home, work, commute
  motion: string;                 // stationary, walking, driving
  headphonesConnected: boolean;
  bluetoothDevices: string[];
  recentAppUsage: string[];
  backgroundAppsCount: number;
  availableStorage: number;
  deviceOrientation: string;
  screenOnTime: number;
  lockScreenFrequency: number;

  // Content (25 features)
  contentType: string;
  contentCategory: string;
  qualityScore: number;
  personalizationScore: number;
  relevanceScore: number;
  urgency: number;
  vendorAvailability: boolean;
  vendorResponseTime: number;
  bookingDeadline: Date;
  contentRecency: number;
  contentPopularity: number;
  similarContentEngaged: boolean;
  vendorType: string;
  priceRange: string;
  regionMatch: boolean;
  styleMatch: number;
  noveltyScore: number;
  diversityContribution: number;
  collaborativeSignal: number;
  contentSentimentScore: number;
  visualQualityScore: number;
  textComplexity: number;
  mediaType: string;
  contentLength: number;
  ageOfContent: number;

  // Social (15 features)
  friendActivity: number;
  friendsOnline: number;
  trendingInRegion: boolean;
  trendingGlobally: boolean;
  vendorResponsePending: boolean;
  partnerActivity: boolean;
  partnerLastActive: number;
  communityEngagement: number;
  socialProof: number;
  viralityScore: number;
  influencerMention: boolean;
  peerRecommendations: number;
  groupPlanningActive: boolean;
  weddingPartyEngagement: number;
  vendorRecommendations: number;

  // Historical (20 features)
  previousNotificationTimes: number[];
  previousOpenTimes: number[];
  previousDismissalTimes: number[];
  timeSinceLastNotification: number;
  notificationFrequency: number;
  historicalEngagementByHour: Map<number, number>;
  historicalEngagementByDay: Map<number, number>;
  bestPerformingTimeSlot: number;
  worstPerformingTimeSlot: number;
  weekdayVsWeekendPreference: number;
  morningVsEveningPreference: number;
  immediateVsDelayedEngagement: number;
  batchVsInstantPreference: number;
  notificationStackingTolerance: number;
  quietHoursPreference: TimeRange[];
  preferredNotificationTypes: string[];
  dismissalPatterns: string[];
  engagementSequence: string[];
  recoveryTimeAfterDismissal: number;
  fatigueLevel: number;

  // Predictive (15 features)
  predictedActiveWindow: TimeRange;
  predictedEngagementProbability: number;
  predictedSessionLength: number;
  predictedContentInterest: Map<string, number>;
  churnRisk: number;
  conversionProbability: number;
  nextSessionPrediction: Date;
  lifetimeValuePrediction: number;
  attentionCapacity: number;
  cognitiveLoad: number;
  decisionFatigue: number;
  emotionalState: string;
  stressLevel: number;
  planningStagePrediction: string;
  urgencyAwareness: number;

  // Experimental (60+ features)
  // Weather, events, competitive activity, etc.
}

Prediction Model

Welcome Probability Prediction

class EngagementForecaster {
  private model: NeuralNetwork;

  async predictWelcome(
    user: User,
    content: Content,
    context: NotificationContext
  ): Promise<number> {
    // Extract and normalize features
    const features = this.extractFeatures(user, content, context);
    const normalized = this.normalizeFeatures(features);

    // Neural network prediction
    const welcomeProb = await this.model.predict(normalized);

    return welcomeProb;  // 0.0 to 1.0
  }

  private extractFeatures(
    user: User,
    content: Content,
    context: NotificationContext
  ): number[] {
    return [
      // Temporal features
      context.timeOfDay / 24,
      context.dayOfWeek / 7,
      context.isWeekend ? 1 : 0,
      context.daysUntilWedding / 365,

      // Behavioral features
      user.avgSessionDuration / 600,  // Normalize to 10 min
      user.notificationOpenRate,
      context.timeSinceLastNotification / 86400,  // Days

      // Content features
      content.qualityScore,
      content.personalizationScore,
      content.urgency,

      // ... 200+ more features
    ];
  }
}

Send-Time Optimization

async optimizeSendTime(
  user: User,
  content: Content,
  context: NotificationContext
): Promise<Date> {
  // Sample candidate times over next 24 hours
  const candidates = [];
  const now = Date.now();

  for (let i = 0; i < 24; i++) {
    const candidateTime = now + i * 3600000;  // Every hour

    const candidateContext = {
      ...context,
      timeOfDay: new Date(candidateTime).getHours(),
      // Update other temporal features
    };

    const welcomeProb = await this.predictWelcome(
      user,
      content,
      candidateContext
    );

    candidates.push({
      time: candidateTime,
      probability: welcomeProb
    });
  }

  // Return time with highest probability
  const best = candidates.reduce((a, b) =>
    a.probability > b.probability ? a : b
  );

  return new Date(best.time);
}

Decision Logic

Should Send Notification?

async shouldSendNotification(
  user: User,
  content: Content,
  context: NotificationContext
): Promise<NotificationDecision> {
  // 1. Predict welcome probability
  const welcomeProb = await this.predictWelcome(user, content, context);

  // 2. Check threshold
  if (welcomeProb < WELCOME_THRESHOLD) {
    return {
      send: false,
      reason: 'low_welcome_probability',
      probability: welcomeProb
    };
  }

  // 3. Check fatigue
  if (await this.isUserFatigued(user)) {
    return {
      send: false,
      reason: 'user_fatigue',
      fatigueScore: await this.getFatigueScore(user)
    };
  }

  // 4. Check quiet hours
  if (this.isQuietHours(user, context)) {
    return {
      send: false,
      reason: 'quiet_hours',
      quietHoursEnd: this.getQuietHoursEnd(user)
    };
  }

  // 5. Optimize timing
  const optimalTime = await this.optimizeSendTime(user, content, context);

  return {
    send: true,
    timing: optimalTime,
    probability: welcomeProb,
    reason: 'optimal_timing'
  };
}

Fatigue Detection

async isUserFatigued(user: User): Promise<boolean> {
  const recentNotifications = await this.getRecentNotifications(user, days: 1);

  // Too many notifications
  if (recentNotifications.length > MAX_DAILY_NOTIFICATIONS) {
    return true;
  }

  // High dismissal rate
  const dismissalRate = recentNotifications.filter(n => n.dismissed).length /
    recentNotifications.length;

  if (dismissalRate > 0.7) {
    return true;
  }

  // Declining engagement
  const engagementTrend = this.computeEngagementTrend(recentNotifications);
  if (engagementTrend < -0.3) {  // 30% decline
    return true;
  }

  return false;
}

MARL Integration

Agent Participation in MAGRPO

class EngagementForecaster extends Agent {
  async proposeNotificationCandidates(
    user: User,
    context: UserContext
  ): Promise<NotificationCandidate[]> {
    // Get relevant content
    const candidates = await this.getNotificationCandidates(user);

    // Score each candidate
    const scored = await Promise.all(
      candidates.map(async (content) => {
        const notifContext = this.buildNotificationContext(user, context);
        const welcomeProb = await this.predictWelcome(user, content, notifContext);
        const timing = await this.optimizeSendTime(user, content, notifContext);

        return {
          content,
          welcomeProb,
          timing,
          score: welcomeProb
        };
      })
    );

    return scored.filter(c => c.welcomeProb > WELCOME_THRESHOLD);
  }

  async updatePolicy(advantage: number): Promise<void> {
    // MAGRPO update based on user response
    // Positive advantage = notification was well-received
    // Negative advantage = notification was poorly received

    if (advantage > 0) {
      // Reinforce this timing/context pattern
      await this.model.train({
        features: this.lastContext,
        target: 1.0,  // High welcome probability
        learningRate: BASE_LR * advantage
      });
    } else {
      // Penalize this pattern
      await this.model.train({
        features: this.lastContext,
        target: 0.0,  // Low welcome probability
        learningRate: BASE_LR * Math.abs(advantage)
      });
    }
  }
}

Reward Signal

computeNotificationReward(interaction: NotificationInteraction): number {
  // Highly engaged = high reward
  if (interaction.opened && interaction.engaged) {
    return 1.0;
  }

  // Opened but didn't engage = medium reward
  if (interaction.opened && !interaction.engaged) {
    return 0.5;
  }

  // Dismissed = negative reward
  if (interaction.dismissed) {
    return -0.5;
  }

  // Opted out = large negative reward
  if (interaction.optedOut) {
    return -2.0;
  }

  // Ignored = small negative reward
  return -0.1;
}

Notification Types

Type-Specific Strategies

const NOTIFICATION_STRATEGIES = {
  vendor_discovery: {
    welcomeThreshold: 0.7,
    urgency: 0.5,
    maxDaily: 2,
    optimalTimes: ['morning', 'evening']
  },

  booking_reminder: {
    welcomeThreshold: 0.6,  // More urgent, lower threshold
    urgency: 0.9,
    maxDaily: 1,
    optimalTimes: ['morning', 'early_afternoon']
  },

  vendor_response: {
    welcomeThreshold: 0.5,  // High urgency
    urgency: 1.0,
    maxDaily: 5,  // Important communication
    optimalTimes: ['immediately']  // Send ASAP
  },

  inspiration: {
    welcomeThreshold: 0.8,  // Only when highly welcome
    urgency: 0.2,
    maxDaily: 1,
    optimalTimes: ['evening', 'weekend']
  },

  planning_milestone: {
    welcomeThreshold: 0.7,
    urgency: 0.8,
    maxDaily: 1,
    optimalTimes: ['morning']
  }
};

API Endpoints

Predict Welcome Probability

Endpoint: POST /api/forecaster/predict

Request:

{
  "userId": "user-123",
  "contentId": "content-456",
  "context": { ... }
}

Response:

{
  "welcomeProbability": 0.82,
  "optimalSendTime": "2025-10-12T19:30:00Z",
  "confidence": 0.9,
  "reasoning": {
    "userTypicallyActiveAt": "19:00-21:00",
    "contentRelevance": "high",
    "recentEngagementPattern": "positive"
  }
}

Get Notification Queue

Endpoint: GET /api/forecaster/queue/:userId

Response:

{
  "scheduledNotifications": [
    {
      "contentId": "content-456",
      "type": "vendor_discovery",
      "scheduledFor": "2025-10-12T19:30:00Z",
      "welcomeProbability": 0.82,
      "canCancel": true
    },
    {
      "contentId": "content-789",
      "type": "booking_reminder",
      "scheduledFor": "2025-10-13T09:00:00Z",
      "welcomeProbability": 0.75,
      "canCancel": false
    }
  ]
}


Performance Targets

Metric Target Notes
CTR lift > 23% vs naive timing
Opt-out rate < 5% User annoyance
Prediction latency < 100ms Real-time
Model accuracy > 75% Welcome prediction
User trust (NPS) Neutral or + No negative impact

Monitoring

Key Metrics

// Track predictions
metrics.histogram('welcome_probability_predicted', welcomeProb);
metrics.histogram('welcome_probability_actual', actualEngagement);

// Track timing optimization
metrics.histogram('send_time_delay_hours', delayHours);
metrics.increment('notification_sent', { type, timing: 'optimal' });

// Track outcomes
metrics.increment('notification_opened', { type });
metrics.increment('notification_dismissed', { type });
metrics.increment('notification_opted_out', { type });

// Track model performance
metrics.gauge('forecaster_accuracy', accuracy);
metrics.gauge('forecaster_ctr_lift', ctrLift);

Resources