Claude Code Plugins

Community-maintained marketplace

Feedback

Reddit Ads API - campaigns, targeting, conversions, agentic optimization

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name reddit-ads
description Reddit Ads API - campaigns, targeting, conversions, agentic optimization

Reddit Ads API Skill

Load with: base.md

Purpose: Automate Reddit advertising campaigns using the Reddit Ads API. Create, manage, and optimize campaigns, ad groups, and ads programmatically.


API Overview

┌─────────────────────────────────────────────────────────────────┐
│  REDDIT ADS API HIERARCHY                                        │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  Account                                                        │
│    └── Campaign (objective, budget, schedule)                   │
│         └── Ad Group (targeting, bidding, placement)            │
│              └── Ad (creative, headline, CTA)                   │
│                                                                 │
│  + Custom Audiences (customer lists, lookalikes)                │
│  + Conversions API (track events server-side)                   │
├─────────────────────────────────────────────────────────────────┤
│  BASE URL: https://ads-api.reddit.com/api/v2.0                  │
│  DOCS: https://ads-api.reddit.com/docs/                         │
│  RATE LIMIT: 1 request per second                               │
│  AUTH: OAuth 2.0 with Bearer token                              │
└─────────────────────────────────────────────────────────────────┘

Authentication

Step 1: Create Reddit Developer App

  1. Go to https://www.reddit.com/prefs/apps/
  2. Click "Create App" or "Create Another App"
  3. Fill in:
    • Name: Your app name
    • Type: Select script for server-side automation
    • Redirect URI: Your callback URL (e.g., https://yourapp.com/callback)
  4. Note your Client ID (under app name) and Client Secret

Step 2: Authorization Flow

// Node.js OAuth2 flow
const REDDIT_CLIENT_ID = process.env.REDDIT_ADS_CLIENT_ID;
const REDDIT_CLIENT_SECRET = process.env.REDDIT_ADS_CLIENT_SECRET;
const REDIRECT_URI = 'https://yourapp.com/callback';

// Step 1: Generate authorization URL
function getAuthorizationUrl(state) {
  const scopes = 'adsread,adsedit,history';
  return `https://www.reddit.com/api/v1/authorize?` +
    `client_id=${REDDIT_CLIENT_ID}` +
    `&response_type=code` +
    `&state=${state}` +
    `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
    `&duration=permanent` +
    `&scope=${scopes}`;
}

// Step 2: Exchange code for tokens
async function getAccessToken(authorizationCode) {
  const credentials = Buffer.from(
    `${REDDIT_CLIENT_ID}:${REDDIT_CLIENT_SECRET}`
  ).toString('base64');

  const response = await fetch('https://www.reddit.com/api/v1/access_token', {
    method: 'POST',
    headers: {
      'Authorization': `Basic ${credentials}`,
      'Content-Type': 'application/x-www-form-urlencoded',
      'User-Agent': 'YourApp/1.0.0'
    },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code: authorizationCode,
      redirect_uri: REDIRECT_URI
    })
  });

  return response.json();
  // Returns: { access_token, refresh_token, expires_in, scope }
}

// Step 3: Refresh token when expired
async function refreshAccessToken(refreshToken) {
  const credentials = Buffer.from(
    `${REDDIT_CLIENT_ID}:${REDDIT_CLIENT_SECRET}`
  ).toString('base64');

  const response = await fetch('https://www.reddit.com/api/v1/access_token', {
    method: 'POST',
    headers: {
      'Authorization': `Basic ${credentials}`,
      'Content-Type': 'application/x-www-form-urlencoded',
      'User-Agent': 'YourApp/1.0.0'
    },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken
    })
  });

  return response.json();
}

Python OAuth2 Flow

import requests
import base64
import os

REDDIT_CLIENT_ID = os.environ['REDDIT_ADS_CLIENT_ID']
REDDIT_CLIENT_SECRET = os.environ['REDDIT_ADS_CLIENT_SECRET']
REDIRECT_URI = 'https://yourapp.com/callback'
USER_AGENT = 'YourApp/1.0.0'

def get_authorization_url(state: str) -> str:
    """Generate OAuth authorization URL."""
    scopes = 'adsread,adsedit,history'
    return (
        f"https://www.reddit.com/api/v1/authorize?"
        f"client_id={REDDIT_CLIENT_ID}"
        f"&response_type=code"
        f"&state={state}"
        f"&redirect_uri={REDIRECT_URI}"
        f"&duration=permanent"
        f"&scope={scopes}"
    )

def get_access_token(authorization_code: str) -> dict:
    """Exchange authorization code for access token."""
    credentials = base64.b64encode(
        f"{REDDIT_CLIENT_ID}:{REDDIT_CLIENT_SECRET}".encode()
    ).decode()

    response = requests.post(
        'https://www.reddit.com/api/v1/access_token',
        headers={
            'Authorization': f'Basic {credentials}',
            'User-Agent': USER_AGENT
        },
        data={
            'grant_type': 'authorization_code',
            'code': authorization_code,
            'redirect_uri': REDIRECT_URI
        }
    )
    return response.json()

def refresh_access_token(refresh_token: str) -> dict:
    """Refresh expired access token."""
    credentials = base64.b64encode(
        f"{REDDIT_CLIENT_ID}:{REDDIT_CLIENT_SECRET}".encode()
    ).decode()

    response = requests.post(
        'https://www.reddit.com/api/v1/access_token',
        headers={
            'Authorization': f'Basic {credentials}',
            'User-Agent': USER_AGENT
        },
        data={
            'grant_type': 'refresh_token',
            'refresh_token': refresh_token
        }
    )
    return response.json()

Required Scopes

Scope Access Level
adsread Read campaigns, ad groups, ads, reports
adsedit Create/update campaigns, ad groups, ads
history Access account history

Reddit Ads Client

Node.js Client

// lib/reddit-ads-client.ts
interface RedditAdsConfig {
  accessToken: string;
  accountId: string;
}

class RedditAdsClient {
  private baseUrl = 'https://ads-api.reddit.com/api/v2.0';
  private accessToken: string;
  private accountId: string;

  constructor(config: RedditAdsConfig) {
    this.accessToken = config.accessToken;
    this.accountId = config.accountId;
  }

  private async request<T>(
    method: string,
    endpoint: string,
    body?: object
  ): Promise<T> {
    const url = `${this.baseUrl}${endpoint}`;

    const response = await fetch(url, {
      method,
      headers: {
        'Authorization': `Bearer ${this.accessToken}`,
        'Content-Type': 'application/json',
        'User-Agent': 'YourApp/1.0.0'
      },
      body: body ? JSON.stringify(body) : undefined
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(`Reddit Ads API Error: ${JSON.stringify(error)}`);
    }

    return response.json();
  }

  // Account
  async getAccount() {
    return this.request('GET', `/accounts/${this.accountId}`);
  }

  // Campaigns
  async getCampaigns() {
    return this.request('GET', `/accounts/${this.accountId}/campaigns`);
  }

  async getCampaign(campaignId: string) {
    return this.request('GET', `/accounts/${this.accountId}/campaigns/${campaignId}`);
  }

  async createCampaign(campaign: CampaignCreate) {
    return this.request('POST', `/accounts/${this.accountId}/campaigns`, campaign);
  }

  async updateCampaign(campaignId: string, updates: Partial<CampaignCreate>) {
    return this.request('PUT', `/accounts/${this.accountId}/campaigns/${campaignId}`, updates);
  }

  // Ad Groups
  async getAdGroups(campaignId?: string) {
    const endpoint = campaignId
      ? `/accounts/${this.accountId}/campaigns/${campaignId}/ad_groups`
      : `/accounts/${this.accountId}/ad_groups`;
    return this.request('GET', endpoint);
  }

  async getAdGroup(adGroupId: string) {
    return this.request('GET', `/accounts/${this.accountId}/ad_groups/${adGroupId}`);
  }

  async createAdGroup(adGroup: AdGroupCreate) {
    return this.request('POST', `/accounts/${this.accountId}/ad_groups`, adGroup);
  }

  async updateAdGroup(adGroupId: string, updates: Partial<AdGroupCreate>) {
    return this.request('PUT', `/accounts/${this.accountId}/ad_groups/${adGroupId}`, updates);
  }

  // Ads
  async getAds(adGroupId?: string) {
    const endpoint = adGroupId
      ? `/accounts/${this.accountId}/ad_groups/${adGroupId}/ads`
      : `/accounts/${this.accountId}/ads`;
    return this.request('GET', endpoint);
  }

  async createAd(ad: AdCreate) {
    return this.request('POST', `/accounts/${this.accountId}/ads`, ad);
  }

  async updateAd(adId: string, updates: Partial<AdCreate>) {
    return this.request('PUT', `/accounts/${this.accountId}/ads/${adId}`, updates);
  }

  // Reports
  async getReport(reportRequest: ReportRequest) {
    return this.request('POST', `/accounts/${this.accountId}/reports`, reportRequest);
  }

  // Custom Audiences
  async getCustomAudiences() {
    return this.request('GET', `/accounts/${this.accountId}/custom_audiences`);
  }

  async createCustomAudience(audience: CustomAudienceCreate) {
    return this.request('POST', `/accounts/${this.accountId}/custom_audiences`, audience);
  }
}

export default RedditAdsClient;

Python Client

# lib/reddit_ads_client.py
import requests
from typing import Optional, Dict, Any, List
from dataclasses import dataclass

@dataclass
class RedditAdsConfig:
    access_token: str
    account_id: str

class RedditAdsClient:
    BASE_URL = 'https://ads-api.reddit.com/api/v2.0'

    def __init__(self, config: RedditAdsConfig):
        self.access_token = config.access_token
        self.account_id = config.account_id
        self.session = requests.Session()
        self.session.headers.update({
            'Authorization': f'Bearer {self.access_token}',
            'Content-Type': 'application/json',
            'User-Agent': 'YourApp/1.0.0'
        })

    def _request(
        self,
        method: str,
        endpoint: str,
        json: Optional[Dict] = None
    ) -> Dict[str, Any]:
        url = f"{self.BASE_URL}{endpoint}"
        response = self.session.request(method, url, json=json)
        response.raise_for_status()
        return response.json()

    # Account
    def get_account(self) -> Dict:
        return self._request('GET', f'/accounts/{self.account_id}')

    # Campaigns
    def get_campaigns(self) -> List[Dict]:
        return self._request('GET', f'/accounts/{self.account_id}/campaigns')

    def get_campaign(self, campaign_id: str) -> Dict:
        return self._request('GET', f'/accounts/{self.account_id}/campaigns/{campaign_id}')

    def create_campaign(self, campaign: Dict) -> Dict:
        return self._request('POST', f'/accounts/{self.account_id}/campaigns', json=campaign)

    def update_campaign(self, campaign_id: str, updates: Dict) -> Dict:
        return self._request('PUT', f'/accounts/{self.account_id}/campaigns/{campaign_id}', json=updates)

    # Ad Groups
    def get_ad_groups(self, campaign_id: Optional[str] = None) -> List[Dict]:
        endpoint = (
            f'/accounts/{self.account_id}/campaigns/{campaign_id}/ad_groups'
            if campaign_id
            else f'/accounts/{self.account_id}/ad_groups'
        )
        return self._request('GET', endpoint)

    def create_ad_group(self, ad_group: Dict) -> Dict:
        return self._request('POST', f'/accounts/{self.account_id}/ad_groups', json=ad_group)

    def update_ad_group(self, ad_group_id: str, updates: Dict) -> Dict:
        return self._request('PUT', f'/accounts/{self.account_id}/ad_groups/{ad_group_id}', json=updates)

    # Ads
    def get_ads(self, ad_group_id: Optional[str] = None) -> List[Dict]:
        endpoint = (
            f'/accounts/{self.account_id}/ad_groups/{ad_group_id}/ads'
            if ad_group_id
            else f'/accounts/{self.account_id}/ads'
        )
        return self._request('GET', endpoint)

    def create_ad(self, ad: Dict) -> Dict:
        return self._request('POST', f'/accounts/{self.account_id}/ads', json=ad)

    # Reports
    def get_report(self, report_request: Dict) -> Dict:
        return self._request('POST', f'/accounts/{self.account_id}/reports', json=report_request)

    # Custom Audiences
    def get_custom_audiences(self) -> List[Dict]:
        return self._request('GET', f'/accounts/{self.account_id}/custom_audiences')

    def create_custom_audience(self, audience: Dict) -> Dict:
        return self._request('POST', f'/accounts/{self.account_id}/custom_audiences', json=audience)

API Endpoints Reference

Account Endpoints

Method Endpoint Description
GET /accounts/{account_id} Get account details
GET /accounts/{account_id}/funding Get funding information

Campaign Endpoints

Method Endpoint Description
GET /accounts/{account_id}/campaigns List all campaigns
GET /accounts/{account_id}/campaigns/{campaign_id} Get campaign by ID
POST /accounts/{account_id}/campaigns Create campaign
PUT /accounts/{account_id}/campaigns/{campaign_id} Update campaign
DELETE /accounts/{account_id}/campaigns/{campaign_id} Delete campaign

Ad Group Endpoints

Method Endpoint Description
GET /accounts/{account_id}/ad_groups List all ad groups
GET /accounts/{account_id}/ad_groups/{ad_group_id} Get ad group by ID
POST /accounts/{account_id}/ad_groups Create ad group
PUT /accounts/{account_id}/ad_groups/{ad_group_id} Update ad group
DELETE /accounts/{account_id}/ad_groups/{ad_group_id} Delete ad group

Ad Endpoints

Method Endpoint Description
GET /accounts/{account_id}/ads List all ads
GET /accounts/{account_id}/ads/{ad_id} Get ad by ID
POST /accounts/{account_id}/ads Create ad
PUT /accounts/{account_id}/ads/{ad_id} Update ad
DELETE /accounts/{account_id}/ads/{ad_id} Delete ad

Custom Audience Endpoints

Method Endpoint Description
GET /accounts/{account_id}/custom_audiences List custom audiences
POST /accounts/{account_id}/custom_audiences Create custom audience
PUT /accounts/{account_id}/custom_audiences/{audience_id} Update audience
DELETE /accounts/{account_id}/custom_audiences/{audience_id} Delete audience

Report Endpoints

Method Endpoint Description
POST /accounts/{account_id}/reports Generate report

Campaign Creation

Campaign Objectives

Objective Use Case
BRAND_AWARENESS Build brand recognition and reach
TRAFFIC Drive clicks to website/landing page
CONVERSIONS Track and optimize for conversions
VIDEO_VIEWS Maximize video view engagement
APP_INSTALLS Drive mobile app installations
CATALOG_SALES Promote product catalog items

Budget Types

Type Description
DAILY Average daily spend (may vary slightly)
LIFETIME Total spend over campaign duration

Campaign Create Example

interface CampaignCreate {
  name: string;
  objective: 'BRAND_AWARENESS' | 'TRAFFIC' | 'CONVERSIONS' | 'VIDEO_VIEWS' | 'APP_INSTALLS';
  is_enabled: boolean;
  budget_type: 'DAILY' | 'LIFETIME';
  budget_total_amount_micros: number; // Amount in micros (1 USD = 1,000,000 micros)
  start_time: string; // ISO 8601 format
  end_time?: string; // ISO 8601 format (optional)
}

// Create a traffic campaign with $50/day budget
const campaign: CampaignCreate = {
  name: 'Q1 2025 Traffic Campaign',
  objective: 'TRAFFIC',
  is_enabled: true,
  budget_type: 'DAILY',
  budget_total_amount_micros: 50_000_000, // $50
  start_time: '2025-01-15T00:00:00Z',
  end_time: '2025-03-31T23:59:59Z'
};

const result = await client.createCampaign(campaign);
# Python example
campaign = {
    'name': 'Q1 2025 Traffic Campaign',
    'objective': 'TRAFFIC',
    'is_enabled': True,
    'budget_type': 'DAILY',
    'budget_total_amount_micros': 50_000_000,  # $50
    'start_time': '2025-01-15T00:00:00Z',
    'end_time': '2025-03-31T23:59:59Z'
}

result = client.create_campaign(campaign)

Ad Group Creation

Bidding Strategies

Strategy Description Use Case
LOWEST_COST Maximize conversions within budget Best for most campaigns
COST_CAP Set average CPC cap Control cost per result
MANUAL Set strict CPC/CPM bid Maximum control

Targeting Options

Targeting Type Description
communities Target specific subreddits
interests Target by interest categories
keywords Target by keyword engagement
devices Target by device type
locations Target by geography
custom_audiences Target uploaded customer lists

Ad Group Create Example

interface AdGroupCreate {
  name: string;
  campaign_id: string;
  is_enabled: boolean;
  bid_strategy: 'LOWEST_COST' | 'COST_CAP' | 'MANUAL';
  bid_amount_micros?: number; // For COST_CAP or MANUAL
  goal_type: 'CLICKS' | 'IMPRESSIONS' | 'CONVERSIONS';
  goal_value_micros?: number;
  targeting: {
    communities?: string[]; // Subreddit names without r/
    interests?: string[];
    keywords?: string[];
    geo_locations?: {
      countries?: string[];
      regions?: string[];
      cities?: string[];
    };
    devices?: ('DESKTOP' | 'MOBILE' | 'TABLET')[];
    custom_audience_ids?: string[];
  };
  start_time?: string;
  end_time?: string;
}

// Create ad group targeting specific subreddits
const adGroup: AdGroupCreate = {
  name: 'Tech Enthusiasts - Subreddit Targeting',
  campaign_id: 'campaign_123',
  is_enabled: true,
  bid_strategy: 'LOWEST_COST',
  goal_type: 'CLICKS',
  targeting: {
    communities: [
      'technology',
      'gadgets',
      'programming',
      'webdev',
      'startups'
    ],
    geo_locations: {
      countries: ['US', 'CA', 'GB']
    },
    devices: ['DESKTOP', 'MOBILE']
  },
  start_time: '2025-01-15T00:00:00Z'
};

const result = await client.createAdGroup(adGroup);
# Python example
ad_group = {
    'name': 'Tech Enthusiasts - Subreddit Targeting',
    'campaign_id': 'campaign_123',
    'is_enabled': True,
    'bid_strategy': 'LOWEST_COST',
    'goal_type': 'CLICKS',
    'targeting': {
        'communities': [
            'technology',
            'gadgets',
            'programming',
            'webdev',
            'startups'
        ],
        'geo_locations': {
            'countries': ['US', 'CA', 'GB']
        },
        'devices': ['DESKTOP', 'MOBILE']
    },
    'start_time': '2025-01-15T00:00:00Z'
}

result = client.create_ad_group(ad_group)

Ad Creation

Ad Types

Type Description
LINK Link ad with image/video
TEXT Text-only promoted post
VIDEO Video ad
CAROUSEL Multiple images/cards
PRODUCT Product catalog ad

Call-to-Action Options

CTA Use Case
SHOP_NOW E-commerce
SIGN_UP Lead generation
LEARN_MORE Information
DOWNLOAD App/content download
INSTALL App install
GET_QUOTE Services
CONTACT_US B2B/Services
APPLY_NOW Jobs/Finance
BOOK_NOW Travel/Services
WATCH_NOW Video content
SUBSCRIBE Newsletters/SaaS
GET_OFFER Promotions
SEE_MENU Restaurants

Ad Create Example

interface AdCreate {
  name: string;
  ad_group_id: string;
  is_enabled: boolean;
  type: 'LINK' | 'TEXT' | 'VIDEO' | 'CAROUSEL';
  headline: string; // Max 300 characters
  body?: string;
  url: string;
  display_url?: string;
  call_to_action: string;
  thumbnail_url?: string; // For image/video ads
  video_url?: string; // For video ads
}

// Create a link ad
const ad: AdCreate = {
  name: 'Product Launch Ad - v1',
  ad_group_id: 'ad_group_456',
  is_enabled: true,
  type: 'LINK',
  headline: 'Introducing Our Revolutionary New Product',
  body: 'Discover how our latest innovation can transform your workflow. Join 10,000+ satisfied customers.',
  url: 'https://yoursite.com/product?utm_source=reddit&utm_medium=paid',
  display_url: 'yoursite.com/product',
  call_to_action: 'LEARN_MORE',
  thumbnail_url: 'https://yoursite.com/images/ad-creative.jpg'
};

const result = await client.createAd(ad);
# Python example
ad = {
    'name': 'Product Launch Ad - v1',
    'ad_group_id': 'ad_group_456',
    'is_enabled': True,
    'type': 'LINK',
    'headline': 'Introducing Our Revolutionary New Product',
    'body': 'Discover how our latest innovation can transform your workflow. Join 10,000+ satisfied customers.',
    'url': 'https://yoursite.com/product?utm_source=reddit&utm_medium=paid',
    'display_url': 'yoursite.com/product',
    'call_to_action': 'LEARN_MORE',
    'thumbnail_url': 'https://yoursite.com/images/ad-creative.jpg'
}

result = client.create_ad(ad)

Conversions API

Event Types

Event Type Description
PAGE_VISIT Page view
VIEW_CONTENT Product/content view
SEARCH Search action
ADD_TO_CART Add to cart
ADD_TO_WISHLIST Add to wishlist
PURCHASE Completed purchase
LEAD Lead submission
SIGN_UP Account creation
CUSTOM Custom event

Conversion Event Structure

interface ConversionEvent {
  event_at: number; // Unix timestamp in milliseconds
  event_type: {
    tracking_type: string;
    custom_event_name?: string; // For CUSTOM type
  };
  user: {
    email?: string; // SHA256 hashed, lowercase
    phone_number?: string; // SHA256 hashed, E.164 format
    external_id?: string;
    ip_address?: string;
    user_agent?: string;
    aaid?: string; // Android Advertising ID
    idfa?: string; // iOS IDFA
  };
  event_metadata?: {
    item_count?: number;
    value_decimal?: number;
    currency?: string;
    conversion_id: string; // Unique event ID
    products?: Array<{
      id: string;
      name?: string;
      category?: string;
    }>;
  };
  click_id?: string; // Reddit click ID for attribution
}

Send Conversion Events

import crypto from 'crypto';

function hashPII(value: string): string {
  return crypto
    .createHash('sha256')
    .update(value.toLowerCase().trim())
    .digest('hex');
}

async function sendConversionEvent(
  accessToken: string,
  pixelId: string,
  event: ConversionEvent
) {
  const response = await fetch(
    `https://ads-api.reddit.com/api/v2.0/conversions/events/${pixelId}`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        events: [event],
        test_mode: false // Set true for testing
      })
    }
  );

  return response.json();
}

// Example: Track a purchase
const purchaseEvent: ConversionEvent = {
  event_at: Date.now(),
  event_type: {
    tracking_type: 'PURCHASE'
  },
  user: {
    email: hashPII('customer@example.com'),
    ip_address: '192.168.1.1',
    user_agent: 'Mozilla/5.0...'
  },
  event_metadata: {
    conversion_id: 'order_12345',
    value_decimal: 99.99,
    currency: 'USD',
    item_count: 2,
    products: [
      { id: 'SKU001', name: 'Product A', category: 'Electronics' },
      { id: 'SKU002', name: 'Product B', category: 'Electronics' }
    ]
  },
  click_id: 'reddit_click_id_from_url' // From rdt_cid parameter
};

await sendConversionEvent(accessToken, 'pixel_123', purchaseEvent);
import hashlib
import time
import requests

def hash_pii(value: str) -> str:
    """SHA256 hash PII data."""
    return hashlib.sha256(value.lower().strip().encode()).hexdigest()

def send_conversion_event(
    access_token: str,
    pixel_id: str,
    events: list[dict],
    test_mode: bool = False
) -> dict:
    """Send conversion events to Reddit."""
    response = requests.post(
        f'https://ads-api.reddit.com/api/v2.0/conversions/events/{pixel_id}',
        headers={
            'Authorization': f'Bearer {access_token}',
            'Content-Type': 'application/json'
        },
        json={
            'events': events,
            'test_mode': test_mode
        }
    )
    response.raise_for_status()
    return response.json()

# Example: Track a purchase
purchase_event = {
    'event_at': int(time.time() * 1000),
    'event_type': {
        'tracking_type': 'PURCHASE'
    },
    'user': {
        'email': hash_pii('customer@example.com'),
        'ip_address': '192.168.1.1',
        'user_agent': 'Mozilla/5.0...'
    },
    'event_metadata': {
        'conversion_id': 'order_12345',
        'value_decimal': 99.99,
        'currency': 'USD',
        'item_count': 2,
        'products': [
            {'id': 'SKU001', 'name': 'Product A', 'category': 'Electronics'},
            {'id': 'SKU002', 'name': 'Product B', 'category': 'Electronics'}
        ]
    },
    'click_id': 'reddit_click_id_from_url'
}

result = send_conversion_event(access_token, 'pixel_123', [purchase_event])

Important Notes

  • Events must occur within last 7 days to be processed
  • Maximum 500 events per batch request
  • Include click_id when available for better attribution
  • Use test_mode: true for testing without affecting campaigns

Custom Audiences

Audience Types

Type Description
CUSTOMER_LIST Upload hashed emails/phone/MAIDs
WEBSITE_VISITORS Pixel-based retargeting
LOOKALIKE Similar to source audience

Create Customer List Audience

interface CustomAudienceCreate {
  name: string;
  type: 'CUSTOMER_LIST';
  description?: string;
  users: Array<{
    email_sha256?: string;
    maid_sha256?: string; // Mobile Advertising ID
  }>;
}

// Create audience from customer emails
const audience: CustomAudienceCreate = {
  name: 'High Value Customers Q4 2024',
  type: 'CUSTOMER_LIST',
  description: 'Customers with LTV > $500',
  users: customerEmails.map(email => ({
    email_sha256: hashPII(email)
  }))
};

const result = await client.createCustomAudience(audience);

Minimum Audience Size

  • 1,000 matched users minimum to be usable for targeting
  • Match rates displayed as ranges for privacy

Reporting

Report Request

interface ReportRequest {
  start_date: string; // YYYY-MM-DD
  end_date: string; // YYYY-MM-DD
  level: 'ACCOUNT' | 'CAMPAIGN' | 'AD_GROUP' | 'AD';
  metrics: string[];
  dimensions?: string[];
  filters?: {
    campaign_ids?: string[];
    ad_group_ids?: string[];
  };
}

// Get campaign performance report
const report = await client.getReport({
  start_date: '2025-01-01',
  end_date: '2025-01-31',
  level: 'CAMPAIGN',
  metrics: [
    'impressions',
    'clicks',
    'spend',
    'ctr',
    'cpc',
    'conversions',
    'conversion_rate',
    'cpa'
  ],
  dimensions: ['date']
});

Available Metrics

Metric Description
impressions Total impressions
clicks Total clicks
spend Total spend (in account currency)
ctr Click-through rate
cpc Cost per click
cpm Cost per 1,000 impressions
conversions Total conversions
conversion_rate Conversions / Clicks
cpa Cost per acquisition
video_views Video view count
video_completions Videos watched to completion

Environment Variables

# .env
REDDIT_ADS_CLIENT_ID=your_client_id
REDDIT_ADS_CLIENT_SECRET=your_client_secret
REDDIT_ADS_ACCOUNT_ID=t2_xxxxx
REDDIT_ADS_ACCESS_TOKEN=your_access_token
REDDIT_ADS_REFRESH_TOKEN=your_refresh_token
REDDIT_ADS_PIXEL_ID=your_pixel_id

Best Practices

Campaign Structure

┌─────────────────────────────────────────────────────────────────┐
│  RECOMMENDED STRUCTURE                                          │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  Campaign (by objective/product line)                           │
│  ├── Ad Group: Subreddit Targeting - Tech                      │
│  │   ├── Ad: Headline A + Image 1                              │
│  │   └── Ad: Headline B + Image 1                              │
│  ├── Ad Group: Subreddit Targeting - Business                  │
│  │   ├── Ad: Headline A + Image 1                              │
│  │   └── Ad: Headline B + Image 1                              │
│  └── Ad Group: Interest Targeting - Entrepreneurs              │
│      ├── Ad: Headline A + Image 2                              │
│      └── Ad: Headline B + Image 2                              │
│                                                                 │
│  • Separate ad groups by targeting type                         │
│  • Test 2-3 ad variations per ad group                          │
│  • Use clear naming conventions                                 │
└─────────────────────────────────────────────────────────────────┘

Naming Conventions

Campaign:  [Objective] - [Product/Brand] - [Date Range]
           Example: TRAFFIC - ProductX - Q1-2025

Ad Group:  [Targeting Type] - [Audience Description]
           Example: Subreddits - Tech Enthusiasts

Ad:        [Headline Type] - [Creative Version]
           Example: Problem-Solution - Image-A

Rate Limiting

  • 1 request per second limit
  • Implement exponential backoff for retries
  • Batch operations where possible
async function rateLimitedRequest<T>(
  fn: () => Promise<T>,
  retries = 3
): Promise<T> {
  for (let i = 0; i < retries; i++) {
    try {
      await new Promise(resolve => setTimeout(resolve, 1000)); // 1 second delay
      return await fn();
    } catch (error: any) {
      if (error.status === 429 && i < retries - 1) {
        const delay = Math.pow(2, i) * 1000;
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }
      throw error;
    }
  }
  throw new Error('Max retries exceeded');
}

Complete Workflow Example

// Full campaign creation workflow
async function createRedditAdCampaign(
  client: RedditAdsClient,
  config: {
    campaignName: string;
    dailyBudget: number;
    targetSubreddits: string[];
    headline: string;
    body: string;
    landingUrl: string;
    imageUrl: string;
  }
) {
  // 1. Create Campaign
  const campaign = await client.createCampaign({
    name: config.campaignName,
    objective: 'TRAFFIC',
    is_enabled: false, // Start paused for review
    budget_type: 'DAILY',
    budget_total_amount_micros: config.dailyBudget * 1_000_000,
    start_time: new Date().toISOString()
  });

  console.log(`Created campaign: ${campaign.id}`);

  // 2. Create Ad Group with targeting
  const adGroup = await client.createAdGroup({
    name: `${config.campaignName} - Subreddit Targeting`,
    campaign_id: campaign.id,
    is_enabled: true,
    bid_strategy: 'LOWEST_COST',
    goal_type: 'CLICKS',
    targeting: {
      communities: config.targetSubreddits,
      geo_locations: { countries: ['US'] },
      devices: ['DESKTOP', 'MOBILE']
    }
  });

  console.log(`Created ad group: ${adGroup.id}`);

  // 3. Create Ad
  const ad = await client.createAd({
    name: `${config.campaignName} - Ad v1`,
    ad_group_id: adGroup.id,
    is_enabled: true,
    type: 'LINK',
    headline: config.headline,
    body: config.body,
    url: config.landingUrl,
    call_to_action: 'LEARN_MORE',
    thumbnail_url: config.imageUrl
  });

  console.log(`Created ad: ${ad.id}`);

  return { campaign, adGroup, ad };
}

// Usage
const result = await createRedditAdCampaign(client, {
  campaignName: 'Product Launch - Jan 2025',
  dailyBudget: 50, // $50/day
  targetSubreddits: ['technology', 'gadgets', 'programming'],
  headline: 'Introducing the Future of Development',
  body: 'Join 50,000+ developers using our tool to ship faster.',
  landingUrl: 'https://yoursite.com?utm_source=reddit',
  imageUrl: 'https://yoursite.com/ad-image.jpg'
});

Testing

Test Checklist

  • OAuth flow completes successfully
  • Token refresh works before expiry
  • Campaign creates with correct budget
  • Ad group targeting is applied correctly
  • Ad creative displays properly
  • Conversion events tracked (use test_mode)
  • Reports return expected metrics
  • Rate limiting handled gracefully
  • Error responses handled properly

Mock API for Development

// test/mocks/reddit-ads-mock.ts
import { rest } from 'msw';

export const redditAdsMocks = [
  rest.post('https://www.reddit.com/api/v1/access_token', (req, res, ctx) => {
    return res(ctx.json({
      access_token: 'mock_access_token',
      refresh_token: 'mock_refresh_token',
      expires_in: 3600,
      scope: 'adsread adsedit history'
    }));
  }),

  rest.get('https://ads-api.reddit.com/api/v2.0/accounts/:accountId', (req, res, ctx) => {
    return res(ctx.json({
      id: req.params.accountId,
      name: 'Test Account',
      currency: 'USD'
    }));
  }),

  rest.post('https://ads-api.reddit.com/api/v2.0/accounts/:accountId/campaigns', (req, res, ctx) => {
    return res(ctx.json({
      id: 'campaign_mock_123',
      ...req.body
    }));
  })
];

Troubleshooting

Error Cause Fix
401 Unauthorized Invalid/expired token Refresh access token
403 Forbidden Account not whitelisted Contact Reddit Ads support
429 Too Many Requests Rate limit exceeded Implement backoff, slow down
400 Bad Request Invalid payload Check required fields, data types
Audience too small < 1,000 matched users Add more users to audience


Agentic Optimization Service

Architecture Overview

┌─────────────────────────────────────────────────────────────────┐
│  AGENTIC REDDIT ADS OPTIMIZER                                   │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐         │
│  │  Scheduler  │───▶│  Analyzer   │───▶│  Optimizer  │         │
│  │  (Cron)     │    │  (AI/LLM)   │    │  (Actions)  │         │
│  └─────────────┘    └─────────────┘    └─────────────┘         │
│         │                  │                  │                 │
│         ▼                  ▼                  ▼                 │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐         │
│  │  Fetch      │    │  Decide     │    │  Execute    │         │
│  │  Reports    │    │  Strategy   │    │  Changes    │         │
│  └─────────────┘    └─────────────┘    └─────────────┘         │
│                                                                 │
│  Loop: Every 4-6 hours                                          │
│  Actions: Pause losers, scale winners, adjust bids, rotate ads  │
└─────────────────────────────────────────────────────────────────┘

Background Service (Node.js)

// services/reddit-ads-optimizer.ts
import Anthropic from '@anthropic-ai/sdk';
import { CronJob } from 'cron';
import RedditAdsClient from '../lib/reddit-ads-client';

interface OptimizationConfig {
  accountId: string;
  accessToken: string;
  refreshToken: string;
  // Thresholds
  minCTR: number;           // Pause ads below this CTR (e.g., 0.005 = 0.5%)
  maxCPA: number;           // Pause ads above this CPA
  minImpressions: number;   // Min impressions before decisions (e.g., 1000)
  budgetScaleFactor: number; // Scale winning ad groups by this factor (e.g., 1.5)
  // Optimization settings
  optimizationGoal: 'CLICKS' | 'CONVERSIONS' | 'ROAS';
  checkIntervalHours: number;
}

interface PerformanceData {
  campaignId: string;
  adGroupId: string;
  adId: string;
  impressions: number;
  clicks: number;
  spend: number;
  conversions: number;
  ctr: number;
  cpc: number;
  cpa: number;
  roas: number;
}

class RedditAdsOptimizerService {
  private client: RedditAdsClient;
  private anthropic: Anthropic;
  private config: OptimizationConfig;
  private cronJob: CronJob | null = null;

  constructor(config: OptimizationConfig) {
    this.config = config;
    this.client = new RedditAdsClient({
      accessToken: config.accessToken,
      accountId: config.accountId
    });
    this.anthropic = new Anthropic();
  }

  // Start the background optimization service
  start() {
    const cronSchedule = `0 */${this.config.checkIntervalHours} * * *`;

    this.cronJob = new CronJob(cronSchedule, async () => {
      console.log(`[${new Date().toISOString()}] Running optimization cycle...`);
      await this.runOptimizationCycle();
    });

    this.cronJob.start();
    console.log(`Reddit Ads Optimizer started. Running every ${this.config.checkIntervalHours} hours.`);
  }

  stop() {
    if (this.cronJob) {
      this.cronJob.stop();
      console.log('Reddit Ads Optimizer stopped.');
    }
  }

  // Main optimization cycle
  async runOptimizationCycle() {
    try {
      // 1. Fetch performance data
      const performanceData = await this.fetchPerformanceData();

      // 2. Analyze with AI agent
      const recommendations = await this.analyzeWithAgent(performanceData);

      // 3. Execute optimizations
      await this.executeOptimizations(recommendations);

      // 4. Log results
      await this.logOptimizationResults(recommendations);

    } catch (error) {
      console.error('Optimization cycle failed:', error);
      await this.sendAlert('Optimization cycle failed', error);
    }
  }

  // Fetch last 24h performance data
  private async fetchPerformanceData(): Promise<PerformanceData[]> {
    const endDate = new Date();
    const startDate = new Date(endDate.getTime() - 24 * 60 * 60 * 1000);

    const report = await this.client.getReport({
      start_date: startDate.toISOString().split('T')[0],
      end_date: endDate.toISOString().split('T')[0],
      level: 'AD',
      metrics: [
        'impressions', 'clicks', 'spend', 'conversions',
        'ctr', 'cpc', 'cpa', 'conversion_value'
      ]
    });

    return report.data.map((row: any) => ({
      campaignId: row.campaign_id,
      adGroupId: row.ad_group_id,
      adId: row.ad_id,
      impressions: row.impressions,
      clicks: row.clicks,
      spend: row.spend,
      conversions: row.conversions || 0,
      ctr: row.ctr,
      cpc: row.cpc,
      cpa: row.cpa || 0,
      roas: row.conversion_value ? row.conversion_value / row.spend : 0
    }));
  }

  // AI-powered analysis and decision making
  private async analyzeWithAgent(data: PerformanceData[]): Promise<OptimizationRecommendation[]> {
    const prompt = `You are a Reddit Ads optimization agent. Analyze the following campaign performance data and recommend specific actions.

## Performance Data (Last 24 Hours)
${JSON.stringify(data, null, 2)}

## Optimization Configuration
- Goal: ${this.config.optimizationGoal}
- Min CTR threshold: ${this.config.minCTR * 100}%
- Max CPA threshold: $${this.config.maxCPA}
- Min impressions for decisions: ${this.config.minImpressions}
- Budget scale factor for winners: ${this.config.budgetScaleFactor}x

## Your Task
Analyze each ad/ad group and recommend ONE action per item:
1. PAUSE - Poor performers (low CTR, high CPA, no conversions after sufficient impressions)
2. SCALE - Winners (high CTR, low CPA, good ROAS) - increase budget
3. ADJUST_BID - Moderate performers - suggest bid adjustment
4. KEEP - Insufficient data or acceptable performance
5. ROTATE_CREATIVE - Good targeting but ad fatigue (declining CTR over time)

Return a JSON array of recommendations:
[
  {
    "adId": "string",
    "adGroupId": "string",
    "action": "PAUSE|SCALE|ADJUST_BID|KEEP|ROTATE_CREATIVE",
    "reason": "Brief explanation",
    "newBidMicros": number (optional, for ADJUST_BID),
    "budgetMultiplier": number (optional, for SCALE)
  }
]

Be aggressive with pausing poor performers to protect budget. Be conservative with scaling (only clear winners).`;

    const response = await this.anthropic.messages.create({
      model: 'claude-sonnet-4-20250514',
      max_tokens: 4096,
      messages: [{ role: 'user', content: prompt }]
    });

    const content = response.content[0];
    if (content.type !== 'text') throw new Error('Unexpected response type');

    // Extract JSON from response
    const jsonMatch = content.text.match(/\[[\s\S]*\]/);
    if (!jsonMatch) throw new Error('No JSON found in response');

    return JSON.parse(jsonMatch[0]);
  }

  // Execute the AI recommendations
  private async executeOptimizations(recommendations: OptimizationRecommendation[]) {
    for (const rec of recommendations) {
      try {
        switch (rec.action) {
          case 'PAUSE':
            await this.client.updateAd(rec.adId, { is_enabled: false });
            console.log(`Paused ad ${rec.adId}: ${rec.reason}`);
            break;

          case 'SCALE':
            const adGroup = await this.client.getAdGroup(rec.adGroupId);
            const currentBudget = adGroup.budget_total_amount_micros;
            const newBudget = Math.round(currentBudget * (rec.budgetMultiplier || this.config.budgetScaleFactor));
            await this.client.updateAdGroup(rec.adGroupId, {
              budget_total_amount_micros: newBudget
            });
            console.log(`Scaled ad group ${rec.adGroupId} budget to ${newBudget / 1_000_000}: ${rec.reason}`);
            break;

          case 'ADJUST_BID':
            if (rec.newBidMicros) {
              await this.client.updateAdGroup(rec.adGroupId, {
                bid_amount_micros: rec.newBidMicros
              });
              console.log(`Adjusted bid for ${rec.adGroupId} to ${rec.newBidMicros / 1_000_000}: ${rec.reason}`);
            }
            break;

          case 'ROTATE_CREATIVE':
            // Flag for creative refresh (implement your creative rotation logic)
            console.log(`Creative rotation needed for ${rec.adId}: ${rec.reason}`);
            await this.flagForCreativeRefresh(rec.adId);
            break;

          case 'KEEP':
            // No action needed
            break;
        }
      } catch (error) {
        console.error(`Failed to execute ${rec.action} for ${rec.adId}:`, error);
      }
    }
  }

  private async flagForCreativeRefresh(adId: string) {
    // Implement: Add to queue, notify team, or auto-generate new creative
  }

  private async logOptimizationResults(recommendations: OptimizationRecommendation[]) {
    const summary = {
      timestamp: new Date().toISOString(),
      totalRecommendations: recommendations.length,
      actions: {
        paused: recommendations.filter(r => r.action === 'PAUSE').length,
        scaled: recommendations.filter(r => r.action === 'SCALE').length,
        bidAdjusted: recommendations.filter(r => r.action === 'ADJUST_BID').length,
        creativeRotation: recommendations.filter(r => r.action === 'ROTATE_CREATIVE').length,
        kept: recommendations.filter(r => r.action === 'KEEP').length
      }
    };
    console.log('Optimization Summary:', JSON.stringify(summary, null, 2));
    // Store in database for historical analysis
  }

  private async sendAlert(subject: string, error: any) {
    // Implement: Send email/Slack notification
  }
}

interface OptimizationRecommendation {
  adId: string;
  adGroupId: string;
  action: 'PAUSE' | 'SCALE' | 'ADJUST_BID' | 'KEEP' | 'ROTATE_CREATIVE';
  reason: string;
  newBidMicros?: number;
  budgetMultiplier?: number;
}

export default RedditAdsOptimizerService;

Background Service (Python)

# services/reddit_ads_optimizer.py
import anthropic
import schedule
import time
import json
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
from dataclasses import dataclass
from enum import Enum

from lib.reddit_ads_client import RedditAdsClient, RedditAdsConfig

class OptimizationAction(Enum):
    PAUSE = "PAUSE"
    SCALE = "SCALE"
    ADJUST_BID = "ADJUST_BID"
    KEEP = "KEEP"
    ROTATE_CREATIVE = "ROTATE_CREATIVE"

@dataclass
class OptimizationConfig:
    account_id: str
    access_token: str
    refresh_token: str
    min_ctr: float = 0.005  # 0.5%
    max_cpa: float = 50.0
    min_impressions: int = 1000
    budget_scale_factor: float = 1.5
    optimization_goal: str = "CONVERSIONS"
    check_interval_hours: int = 4

@dataclass
class PerformanceData:
    campaign_id: str
    ad_group_id: str
    ad_id: str
    impressions: int
    clicks: int
    spend: float
    conversions: int
    ctr: float
    cpc: float
    cpa: float
    roas: float

@dataclass
class OptimizationRecommendation:
    ad_id: str
    ad_group_id: str
    action: OptimizationAction
    reason: str
    new_bid_micros: Optional[int] = None
    budget_multiplier: Optional[float] = None

class RedditAdsOptimizerService:
    def __init__(self, config: OptimizationConfig):
        self.config = config
        self.client = RedditAdsClient(RedditAdsConfig(
            access_token=config.access_token,
            account_id=config.account_id
        ))
        self.anthropic = anthropic.Anthropic()
        self._running = False

    def start(self):
        """Start the background optimization service."""
        self._running = True

        # Schedule optimization runs
        schedule.every(self.config.check_interval_hours).hours.do(
            self.run_optimization_cycle
        )

        print(f"Reddit Ads Optimizer started. Running every {self.config.check_interval_hours} hours.")

        # Run immediately on start
        self.run_optimization_cycle()

        # Keep running
        while self._running:
            schedule.run_pending()
            time.sleep(60)

    def stop(self):
        """Stop the optimization service."""
        self._running = False
        print("Reddit Ads Optimizer stopped.")

    def run_optimization_cycle(self):
        """Main optimization cycle."""
        print(f"[{datetime.now().isoformat()}] Running optimization cycle...")

        try:
            # 1. Fetch performance data
            performance_data = self._fetch_performance_data()

            # 2. Analyze with AI agent
            recommendations = self._analyze_with_agent(performance_data)

            # 3. Execute optimizations
            self._execute_optimizations(recommendations)

            # 4. Log results
            self._log_optimization_results(recommendations)

        except Exception as e:
            print(f"Optimization cycle failed: {e}")
            self._send_alert("Optimization cycle failed", str(e))

    def _fetch_performance_data(self) -> List[PerformanceData]:
        """Fetch last 24h performance data."""
        end_date = datetime.now()
        start_date = end_date - timedelta(days=1)

        report = self.client.get_report({
            'start_date': start_date.strftime('%Y-%m-%d'),
            'end_date': end_date.strftime('%Y-%m-%d'),
            'level': 'AD',
            'metrics': [
                'impressions', 'clicks', 'spend', 'conversions',
                'ctr', 'cpc', 'cpa', 'conversion_value'
            ]
        })

        return [
            PerformanceData(
                campaign_id=row['campaign_id'],
                ad_group_id=row['ad_group_id'],
                ad_id=row['ad_id'],
                impressions=row['impressions'],
                clicks=row['clicks'],
                spend=row['spend'],
                conversions=row.get('conversions', 0),
                ctr=row['ctr'],
                cpc=row['cpc'],
                cpa=row.get('cpa', 0),
                roas=row.get('conversion_value', 0) / row['spend'] if row['spend'] > 0 else 0
            )
            for row in report.get('data', [])
        ]

    def _analyze_with_agent(self, data: List[PerformanceData]) -> List[OptimizationRecommendation]:
        """AI-powered analysis and decision making."""

        prompt = f"""You are a Reddit Ads optimization agent. Analyze the following campaign performance data and recommend specific actions.

## Performance Data (Last 24 Hours)
{json.dumps([vars(d) for d in data], indent=2)}

## Optimization Configuration
- Goal: {self.config.optimization_goal}
- Min CTR threshold: {self.config.min_ctr * 100}%
- Max CPA threshold: ${self.config.max_cpa}
- Min impressions for decisions: {self.config.min_impressions}
- Budget scale factor for winners: {self.config.budget_scale_factor}x

## Your Task
Analyze each ad/ad group and recommend ONE action per item:
1. PAUSE - Poor performers (low CTR, high CPA, no conversions after sufficient impressions)
2. SCALE - Winners (high CTR, low CPA, good ROAS) - increase budget
3. ADJUST_BID - Moderate performers - suggest bid adjustment
4. KEEP - Insufficient data or acceptable performance
5. ROTATE_CREATIVE - Good targeting but ad fatigue (declining CTR over time)

Return a JSON array of recommendations:
[
  {{
    "ad_id": "string",
    "ad_group_id": "string",
    "action": "PAUSE|SCALE|ADJUST_BID|KEEP|ROTATE_CREATIVE",
    "reason": "Brief explanation",
    "new_bid_micros": number (optional, for ADJUST_BID),
    "budget_multiplier": number (optional, for SCALE)
  }}
]

Be aggressive with pausing poor performers to protect budget. Be conservative with scaling (only clear winners)."""

        response = self.anthropic.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=4096,
            messages=[{"role": "user", "content": prompt}]
        )

        content = response.content[0].text

        # Extract JSON from response
        import re
        json_match = re.search(r'\[[\s\S]*\]', content)
        if not json_match:
            raise ValueError("No JSON found in response")

        recommendations_data = json.loads(json_match.group())

        return [
            OptimizationRecommendation(
                ad_id=r['ad_id'],
                ad_group_id=r['ad_group_id'],
                action=OptimizationAction(r['action']),
                reason=r['reason'],
                new_bid_micros=r.get('new_bid_micros'),
                budget_multiplier=r.get('budget_multiplier')
            )
            for r in recommendations_data
        ]

    def _execute_optimizations(self, recommendations: List[OptimizationRecommendation]):
        """Execute the AI recommendations."""
        for rec in recommendations:
            try:
                if rec.action == OptimizationAction.PAUSE:
                    self.client.update_ad(rec.ad_id, {'is_enabled': False})
                    print(f"Paused ad {rec.ad_id}: {rec.reason}")

                elif rec.action == OptimizationAction.SCALE:
                    ad_group = self.client.get_ad_group(rec.ad_group_id)
                    current_budget = ad_group['budget_total_amount_micros']
                    multiplier = rec.budget_multiplier or self.config.budget_scale_factor
                    new_budget = int(current_budget * multiplier)
                    self.client.update_ad_group(rec.ad_group_id, {
                        'budget_total_amount_micros': new_budget
                    })
                    print(f"Scaled ad group {rec.ad_group_id} budget to ${new_budget / 1_000_000}: {rec.reason}")

                elif rec.action == OptimizationAction.ADJUST_BID:
                    if rec.new_bid_micros:
                        self.client.update_ad_group(rec.ad_group_id, {
                            'bid_amount_micros': rec.new_bid_micros
                        })
                        print(f"Adjusted bid for {rec.ad_group_id}: {rec.reason}")

                elif rec.action == OptimizationAction.ROTATE_CREATIVE:
                    print(f"Creative rotation needed for {rec.ad_id}: {rec.reason}")
                    self._flag_for_creative_refresh(rec.ad_id)

            except Exception as e:
                print(f"Failed to execute {rec.action} for {rec.ad_id}: {e}")

    def _flag_for_creative_refresh(self, ad_id: str):
        """Flag ad for creative refresh."""
        # Implement: Add to queue, notify team, or auto-generate new creative
        pass

    def _log_optimization_results(self, recommendations: List[OptimizationRecommendation]):
        """Log optimization results."""
        summary = {
            'timestamp': datetime.now().isoformat(),
            'total_recommendations': len(recommendations),
            'actions': {
                'paused': len([r for r in recommendations if r.action == OptimizationAction.PAUSE]),
                'scaled': len([r for r in recommendations if r.action == OptimizationAction.SCALE]),
                'bid_adjusted': len([r for r in recommendations if r.action == OptimizationAction.ADJUST_BID]),
                'creative_rotation': len([r for r in recommendations if r.action == OptimizationAction.ROTATE_CREATIVE]),
                'kept': len([r for r in recommendations if r.action == OptimizationAction.KEEP]),
            }
        }
        print(f"Optimization Summary: {json.dumps(summary, indent=2)}")

    def _send_alert(self, subject: str, error: str):
        """Send alert notification."""
        # Implement: Send email/Slack notification
        pass


# Entry point for running as background service
if __name__ == "__main__":
    import os

    config = OptimizationConfig(
        account_id=os.environ['REDDIT_ADS_ACCOUNT_ID'],
        access_token=os.environ['REDDIT_ADS_ACCESS_TOKEN'],
        refresh_token=os.environ['REDDIT_ADS_REFRESH_TOKEN'],
        min_ctr=0.005,
        max_cpa=50.0,
        min_impressions=1000,
        budget_scale_factor=1.5,
        optimization_goal="CONVERSIONS",
        check_interval_hours=4
    )

    optimizer = RedditAdsOptimizerService(config)
    optimizer.start()

Docker Deployment

# Dockerfile
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["python", "services/reddit_ads_optimizer.py"]
# docker-compose.yml
version: '3.8'

services:
  reddit-ads-optimizer:
    build: .
    container_name: reddit-ads-optimizer
    restart: unless-stopped
    environment:
      - REDDIT_ADS_CLIENT_ID=${REDDIT_ADS_CLIENT_ID}
      - REDDIT_ADS_CLIENT_SECRET=${REDDIT_ADS_CLIENT_SECRET}
      - REDDIT_ADS_ACCOUNT_ID=${REDDIT_ADS_ACCOUNT_ID}
      - REDDIT_ADS_ACCESS_TOKEN=${REDDIT_ADS_ACCESS_TOKEN}
      - REDDIT_ADS_REFRESH_TOKEN=${REDDIT_ADS_REFRESH_TOKEN}
      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
    volumes:
      - ./logs:/app/logs
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Optimization Strategies

┌─────────────────────────────────────────────────────────────────┐
│  AGENTIC OPTIMIZATION STRATEGIES                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. PERFORMANCE-BASED PAUSING                                   │
│     ─────────────────────────────────────────────────────────  │
│     IF impressions > 1000 AND ctr < 0.3% → PAUSE               │
│     IF impressions > 500 AND conversions = 0 → PAUSE           │
│     IF cpa > 2x target → PAUSE                                  │
│                                                                 │
│  2. WINNER SCALING                                              │
│     ─────────────────────────────────────────────────────────  │
│     IF ctr > 1% AND cpa < target AND conversions > 5           │
│     → SCALE budget by 1.5x                                      │
│     Cap at 3x original budget to manage risk                    │
│                                                                 │
│  3. BID OPTIMIZATION                                            │
│     ─────────────────────────────────────────────────────────  │
│     IF position low AND ctr good → INCREASE bid 10-20%         │
│     IF cpa high but converting → DECREASE bid 10-15%           │
│                                                                 │
│  4. CREATIVE FATIGUE DETECTION                                  │
│     ─────────────────────────────────────────────────────────  │
│     IF ctr declining 3 consecutive days → ROTATE_CREATIVE      │
│     IF frequency > 3 → ROTATE_CREATIVE                          │
│                                                                 │
│  5. BUDGET REALLOCATION                                         │
│     ─────────────────────────────────────────────────────────  │
│     Move budget from paused ads to scaled winners              │
│     Maintain total daily budget cap                             │
└─────────────────────────────────────────────────────────────────┘

Advanced: Multi-Agent Optimization

// services/multi-agent-optimizer.ts
import Anthropic from '@anthropic-ai/sdk';

interface AgentRole {
  name: string;
  systemPrompt: string;
}

const AGENTS: AgentRole[] = [
  {
    name: 'Performance Analyst',
    systemPrompt: `You analyze Reddit Ads performance data. Identify:
    - Top performers (high CTR, low CPA, good ROAS)
    - Poor performers (low CTR, high CPA, no conversions)
    - Trends (improving, declining, stable)
    Output structured analysis with confidence scores.`
  },
  {
    name: 'Budget Strategist',
    systemPrompt: `You optimize budget allocation across campaigns.
    Given performance analysis, recommend:
    - Budget increases for winners (max 50% increase)
    - Budget decreases for losers
    - Reallocation between ad groups
    Protect total budget while maximizing ROI.`
  },
  {
    name: 'Creative Director',
    systemPrompt: `You evaluate ad creative performance.
    Identify ads with:
    - Creative fatigue (declining engagement)
    - High potential but poor execution
    - A/B test winners
    Recommend creative refreshes and new variations.`
  },
  {
    name: 'Risk Manager',
    systemPrompt: `You ensure optimization safety.
    Review recommendations and flag:
    - Overly aggressive scaling
    - Insufficient data for decisions
    - Budget concentration risk
    - Compliance concerns
    Approve, modify, or reject recommendations.`
  }
];

class MultiAgentOptimizer {
  private anthropic: Anthropic;

  constructor() {
    this.anthropic = new Anthropic();
  }

  async runAgentPipeline(performanceData: any) {
    let context = { performanceData };

    // Run agents in sequence, each building on previous output
    for (const agent of AGENTS) {
      const response = await this.anthropic.messages.create({
        model: 'claude-sonnet-4-20250514',
        max_tokens: 4096,
        system: agent.systemPrompt,
        messages: [{
          role: 'user',
          content: `Previous context:\n${JSON.stringify(context, null, 2)}\n\nProvide your analysis and recommendations.`
        }]
      });

      context = {
        ...context,
        [agent.name.toLowerCase().replace(' ', '_')]: response.content[0]
      };
    }

    return context;
  }
}

Monitoring Dashboard Data

// api/optimization-stats.ts
interface OptimizationStats {
  period: string;
  totalOptimizations: number;
  actionBreakdown: {
    paused: number;
    scaled: number;
    bidAdjusted: number;
    creativeRotated: number;
  };
  performanceImpact: {
    ctrChange: number;
    cpaChange: number;
    roasChange: number;
    spendEfficiency: number;
  };
  budgetSaved: number;
  revenueIncreased: number;
}

async function getOptimizationStats(
  startDate: Date,
  endDate: Date
): Promise<OptimizationStats> {
  // Query optimization logs and performance data
  // Calculate before/after metrics
  // Return aggregated stats
}

Resources