Reddit Ads API Skill
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
- Go to https://www.reddit.com/prefs/apps/
- Click "Create App" or "Create Another App"
- Fill in:
- Name: Your app name
- Type: Select
script for server-side automation
- Redirect URI: Your callback URL (e.g.,
https://yourapp.com/callback)
- 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
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 recommenda
…(truncated)
1---
2name: reddit-ads
3description: Reddit Ads API - campaigns, targeting, conversions, agentic optimization
4when-to-use: When building Reddit ad campaign management or optimization tools
5user-invocable: false
6effort: medium
7---
8
9# Reddit Ads API Skill
10
11
12**Purpose:** Automate Reddit advertising campaigns using the Reddit Ads API. Create, manage, and optimize campaigns, ad groups, and ads programmatically.
13
14---
15
16## API Overview
17
18```
19┌─────────────────────────────────────────────────────────────────┐
20│ REDDIT ADS API HIERARCHY │
21│ ───────────────────────────────────────────────────────────── │
22│ │
23│ Account │
24│ └── Campaign (objective, budget, schedule) │
25│ └── Ad Group (targeting, bidding, placement) │
26│ └── Ad (creative, headline, CTA) │
27│ │
28│ + Custom Audiences (customer lists, lookalikes) │
29│ + Conversions API (track events server-side) │
30├─────────────────────────────────────────────────────────────────┤
31│ BASE URL: https://ads-api.reddit.com/api/v2.0 │
32│ DOCS: https://ads-api.reddit.com/docs/ │
33│ RATE LIMIT: 1 request per second │
34│ AUTH: OAuth 2.0 with Bearer token │
35└─────────────────────────────────────────────────────────────────┘
36```
37
38---
39
40## Authentication
41
42### Step 1: Create Reddit Developer App
43
441. Go to https://www.reddit.com/prefs/apps/
452. Click "Create App" or "Create Another App"
463. Fill in:
47 - **Name:** Your app name
48 - **Type:** Select `script` for server-side automation
49 - **Redirect URI:** Your callback URL (e.g., `https://yourapp.com/callback`)
504. Note your **Client ID** (under app name) and **Client Secret**
51
52### Step 2: Authorization Flow
53
54```javascript
55// Node.js OAuth2 flow
56const REDDIT_CLIENT_ID = process.env.REDDIT_ADS_CLIENT_ID;
57const REDDIT_CLIENT_SECRET = process.env.REDDIT_ADS_CLIENT_SECRET;
58const REDIRECT_URI = 'https://yourapp.com/callback';
59
60// Step 1: Generate authorization URL
61function getAuthorizationUrl(state) {
62 const scopes = 'adsread,adsedit,history';
63 return `https://www.reddit.com/api/v1/authorize?` +
64 `client_id=${REDDIT_CLIENT_ID}` +
65 `&response_type=code` +
66 `&state=${state}` +
67 `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
68 `&duration=permanent` +
69 `&scope=${scopes}`;
70}
71
72// Step 2: Exchange code for tokens
73async function getAccessToken(authorizationCode) {
74 const credentials = Buffer.from(
75 `${REDDIT_CLIENT_ID}:${REDDIT_CLIENT_SECRET}`
76 ).toString('base64');
77
78 const response = await fetch('https://www.reddit.com/api/v1/access_token', {
79 method: 'POST',
80 headers: {
81 'Authorization': `Basic ${credentials}`,
82 'Content-Type': 'application/x-www-form-urlencoded',
83 'User-Agent': 'YourApp/1.0.0'
84 },
85 body: new URLSearchParams({
86 grant_type: 'authorization_code',
87 code: authorizationCode,
88 redirect_uri: REDIRECT_URI
89 })
90 });
91
92 return response.json();
93 // Returns: { access_token, refresh_token, expires_in, scope }
94}
95
96// Step 3: Refresh token when expired
97async function refreshAccessToken(refreshToken) {
98 const credentials = Buffer.from(
99 `${REDDIT_CLIENT_ID}:${REDDIT_CLIENT_SECRET}`
100 ).toString('base64');
101
102 const response = await fetch('https://www.reddit.com/api/v1/access_token', {
103 method: 'POST',
104 headers: {
105 'Authorization': `Basic ${credentials}`,
106 'Content-Type': 'application/x-www-form-urlencoded',
107 'User-Agent': 'YourApp/1.0.0'
108 },
109 body: new URLSearchParams({
110 grant_type: 'refresh_token',
111 refresh_token: refreshToken
112 })
113 });
114
115 return response.json();
116}
117```
118
119### Python OAuth2 Flow
120
121```python
122import requests
123import base64
124import os
125
126REDDIT_CLIENT_ID = os.environ['REDDIT_ADS_CLIENT_ID']
127REDDIT_CLIENT_SECRET = os.environ['REDDIT_ADS_CLIENT_SECRET']
128REDIRECT_URI = 'https://yourapp.com/callback'
129USER_AGENT = 'YourApp/1.0.0'
130
131def get_authorization_url(state: str) -> str:
132 """Generate OAuth authorization URL."""
133 scopes = 'adsread,adsedit,history'
134 return (
135 f"https://www.reddit.com/api/v1/authorize?"
136 f"client_id={REDDIT_CLIENT_ID}"
137 f"&response_type=code"
138 f"&state={state}"
139 f"&redirect_uri={REDIRECT_URI}"
140 f"&duration=permanent"
141 f"&scope={scopes}"
142 )
143
144def get_access_token(authorization_code: str) -> dict:
145 """Exchange authorization code for access token."""
146 credentials = base64.b64encode(
147 f"{REDDIT_CLIENT_ID}:{REDDIT_CLIENT_SECRET}".encode()
148 ).decode()
149
150 response = requests.post(
151 'https://www.reddit.com/api/v1/access_token',
152 headers={
153 'Authorization': f'Basic {credentials}',
154 'User-Agent': USER_AGENT
155 },
156 data={
157 'grant_type': 'authorization_code',
158 'code': authorization_code,
159 'redirect_uri': REDIRECT_URI
160 }
161 )
162 return response.json()
163
164def refresh_access_token(refresh_token: str) -> dict:
165 """Refresh expired access token."""
166 credentials = base64.b64encode(
167 f"{REDDIT_CLIENT_ID}:{REDDIT_CLIENT_SECRET}".encode()
168 ).decode()
169
170 response = requests.post(
171 'https://www.reddit.com/api/v1/access_token',
172 headers={
173 'Authorization': f'Basic {credentials}',
174 'User-Agent': USER_AGENT
175 },
176 data={
177 'grant_type': 'refresh_token',
178 'refresh_token': refresh_token
179 }
180 )
181 return response.json()
182```
183
184### Required Scopes
185
186| Scope | Access Level |
187|-------|--------------|
188| `adsread` | Read campaigns, ad groups, ads, reports |
189| `adsedit` | Create/update campaigns, ad groups, ads |
190| `history` | Access account history |
191
192---
193
194## Reddit Ads Client
195
196### Node.js Client
197
198```typescript
199// lib/reddit-ads-client.ts
200interface RedditAdsConfig {
201 accessToken: string;
202 accountId: string;
203}
204
205class RedditAdsClient {
206 private baseUrl = 'https://ads-api.reddit.com/api/v2.0';
207 private accessToken: string;
208 private accountId: string;
209
210 constructor(config: RedditAdsConfig) {
211 this.accessToken = config.accessToken;
212 this.accountId = config.accountId;
213 }
214
215 private async request<T>(
216 method: string,
217 endpoint: string,
218 body?: object
219 ): Promise<T> {
220 const url = `${this.baseUrl}${endpoint}`;
221
222 const response = await fetch(url, {
223 method,
224 headers: {
225 'Authorization': `Bearer ${this.accessToken}`,
226 'Content-Type': 'application/json',
227 'User-Agent': 'YourApp/1.0.0'
228 },
229 body: body ? JSON.stringify(body) : undefined
230 });
231
232 if (!response.ok) {
233 const error = await response.json();
234 throw new Error(`Reddit Ads API Error: ${JSON.stringify(error)}`);
235 }
236
237 return response.json();
238 }
239
240 // Account
241 async getAccount() {
242 return this.request('GET', `/accounts/${this.accountId}`);
243 }
244
245 // Campaigns
246 async getCampaigns() {
247 return this.request('GET', `/accounts/${this.accountId}/campaigns`);
248 }
249
250 async getCampaign(campaignId: string) {
251 return this.request('GET', `/accounts/${this.accountId}/campaigns/${campaignId}`);
252 }
253
254 async createCampaign(campaign: CampaignCreate) {
255 return this.request('POST', `/accounts/${this.accountId}/campaigns`, campaign);
256 }
257
258 async updateCampaign(campaignId: string, updates: Partial<CampaignCreate>) {
259 return this.request('PUT', `/accounts/${this.accountId}/campaigns/${campaignId}`, updates);
260 }
261
262 // Ad Groups
263 async getAdGroups(campaignId?: string) {
264 const endpoint = campaignId
265 ? `/accounts/${this.accountId}/campaigns/${campaignId}/ad_groups`
266 : `/accounts/${this.accountId}/ad_groups`;
267 return this.request('GET', endpoint);
268 }
269
270 async getAdGroup(adGroupId: string) {
271 return this.request('GET', `/accounts/${this.accountId}/ad_groups/${adGroupId}`);
272 }
273
274 async createAdGroup(adGroup: AdGroupCreate) {
275 return this.request('POST', `/accounts/${this.accountId}/ad_groups`, adGroup);
276 }
277
278 async updateAdGroup(adGroupId: string, updates: Partial<AdGroupCreate>) {
279 return this.request('PUT', `/accounts/${this.accountId}/ad_groups/${adGroupId}`, updates);
280 }
281
282 // Ads
283 async getAds(adGroupId?: string) {
284 const endpoint = adGroupId
285 ? `/accounts/${this.accountId}/ad_groups/${adGroupId}/ads`
286 : `/accounts/${this.accountId}/ads`;
287 return this.request('GET', endpoint);
288 }
289
290 async createAd(ad: AdCreate) {
291 return this.request('POST', `/accounts/${this.accountId}/ads`, ad);
292 }
293
294 async updateAd(adId: string, updates: Partial<AdCreate>) {
295 return this.request('PUT', `/accounts/${this.accountId}/ads/${adId}`, updates);
296 }
297
298 // Reports
299 async getReport(reportRequest: ReportRequest) {
300 return this.request('POST', `/accounts/${this.accountId}/reports`, reportRequest);
301 }
302
303 // Custom Audiences
304 async getCustomAudiences() {
305 return this.request('GET', `/accounts/${this.accountId}/custom_audiences`);
306 }
307
308 async createCustomAudience(audience: CustomAudienceCreate) {
309 return this.request('POST', `/accounts/${this.accountId}/custom_audiences`, audience);
310 }
311}
312
313export default RedditAdsClient;
314```
315
316### Python Client
317
318```python
319# lib/reddit_ads_client.py
320import requests
321from typing import Optional, Dict, Any, List
322from dataclasses import dataclass
323
324@dataclass
325class RedditAdsConfig:
326 access_token: str
327 account_id: str
328
329class RedditAdsClient:
330 BASE_URL = 'https://ads-api.reddit.com/api/v2.0'
331
332 def __init__(self, config: RedditAdsConfig):
333 self.access_token = config.access_token
334 self.account_id = config.account_id
335 self.session = requests.Session()
336 self.session.headers.update({
337 'Authorization': f'Bearer {self.access_token}',
338 'Content-Type': 'application/json',
339 'User-Agent': 'YourApp/1.0.0'
340 })
341
342 def _request(
343 self,
344 method: str,
345 endpoint: str,
346 json: Optional[Dict] = None
347 ) -> Dict[str, Any]:
348 url = f"{self.BASE_URL}{endpoint}"
349 response = self.session.request(method, url, json=json)
350 response.raise_for_status()
351 return response.json()
352
353 # Account
354 def get_account(self) -> Dict:
355 return self._request('GET', f'/accounts/{self.account_id}')
356
357 # Campaigns
358 def get_campaigns(self) -> List[Dict]:
359 return self._request('GET', f'/accounts/{self.account_id}/campaigns')
360
361 def get_campaign(self, campaign_id: str) -> Dict:
362 return self._request('GET', f'/accounts/{self.account_id}/campaigns/{campaign_id}')
363
364 def create_campaign(self, campaign: Dict) -> Dict:
365 return self._request('POST', f'/accounts/{self.account_id}/campaigns', json=campaign)
366
367 def update_campaign(self, campaign_id: str, updates: Dict) -> Dict:
368 return self._request('PUT', f'/accounts/{self.account_id}/campaigns/{campaign_id}', json=updates)
369
370 # Ad Groups
371 def get_ad_groups(self, campaign_id: Optional[str] = None) -> List[Dict]:
372 endpoint = (
373 f'/accounts/{self.account_id}/campaigns/{campaign_id}/ad_groups'
374 if campaign_id
375 else f'/accounts/{self.account_id}/ad_groups'
376 )
377 return self._request('GET', endpoint)
378
379 def create_ad_group(self, ad_group: Dict) -> Dict:
380 return self._request('POST', f'/accounts/{self.account_id}/ad_groups', json=ad_group)
381
382 def update_ad_group(self, ad_group_id: str, updates: Dict) -> Dict:
383 return self._request('PUT', f'/accounts/{self.account_id}/ad_groups/{ad_group_id}', json=updates)
384
385 # Ads
386 def get_ads(self, ad_group_id: Optional[str] = None) -> List[Dict]:
387 endpoint = (
388 f'/accounts/{self.account_id}/ad_groups/{ad_group_id}/ads'
389 if ad_group_id
390 else f'/accounts/{self.account_id}/ads'
391 )
392 return self._request('GET', endpoint)
393
394 def create_ad(self, ad: Dict) -> Dict:
395 return self._request('POST', f'/accounts/{self.account_id}/ads', json=ad)
396
397 # Reports
398 def get_report(self, report_request: Dict) -> Dict:
399 return self._request('POST', f'/accounts/{self.account_id}/reports', json=report_request)
400
401 # Custom Audiences
402 def get_custom_audiences(self) -> List[Dict]:
403 return self._request('GET', f'/accounts/{self.account_id}/custom_audiences')
404
405 def create_custom_audience(self, audience: Dict) -> Dict:
406 return self._request('POST', f'/accounts/{self.account_id}/custom_audiences', json=audience)
407```
408
409---
410
411## API Endpoints Reference
412
413### Account Endpoints
414
415| Method | Endpoint | Description |
416|--------|----------|-------------|
417| GET | `/accounts/{account_id}` | Get account details |
418| GET | `/accounts/{account_id}/funding` | Get funding information |
419
420### Campaign Endpoints
421
422| Method | Endpoint | Description |
423|--------|----------|-------------|
424| GET | `/accounts/{account_id}/campaigns` | List all campaigns |
425| GET | `/accounts/{account_id}/campaigns/{campaign_id}` | Get campaign by ID |
426| POST | `/accounts/{account_id}/campaigns` | Create campaign |
427| PUT | `/accounts/{account_id}/campaigns/{campaign_id}` | Update campaign |
428| DELETE | `/accounts/{account_id}/campaigns/{campaign_id}` | Delete campaign |
429
430### Ad Group Endpoints
431
432| Method | Endpoint | Description |
433|--------|----------|-------------|
434| GET | `/accounts/{account_id}/ad_groups` | List all ad groups |
435| GET | `/accounts/{account_id}/ad_groups/{ad_group_id}` | Get ad group by ID |
436| POST | `/accounts/{account_id}/ad_groups` | Create ad group |
437| PUT | `/accounts/{account_id}/ad_groups/{ad_group_id}` | Update ad group |
438| DELETE | `/accounts/{account_id}/ad_groups/{ad_group_id}` | Delete ad group |
439
440### Ad Endpoints
441
442| Method | Endpoint | Description |
443|--------|----------|-------------|
444| GET | `/accounts/{account_id}/ads` | List all ads |
445| GET | `/accounts/{account_id}/ads/{ad_id}` | Get ad by ID |
446| POST | `/accounts/{account_id}/ads` | Create ad |
447| PUT | `/accounts/{account_id}/ads/{ad_id}` | Update ad |
448| DELETE | `/accounts/{account_id}/ads/{ad_id}` | Delete ad |
449
450### Custom Audience Endpoints
451
452| Method | Endpoint | Description |
453|--------|----------|-------------|
454| GET | `/accounts/{account_id}/custom_audiences` | List custom audiences |
455| POST | `/accounts/{account_id}/custom_audiences` | Create custom audience |
456| PUT | `/accounts/{account_id}/custom_audiences/{audience_id}` | Update audience |
457| DELETE | `/accounts/{account_id}/custom_audiences/{audience_id}` | Delete audience |
458
459### Report Endpoints
460
461| Method | Endpoint | Description |
462|--------|----------|-------------|
463| POST | `/accounts/{account_id}/reports` | Generate report |
464
465---
466
467## Campaign Creation
468
469### Campaign Objectives
470
471| Objective | Use Case |
472|-----------|----------|
473| `BRAND_AWARENESS` | Build brand recognition and reach |
474| `TRAFFIC` | Drive clicks to website/landing page |
475| `CONVERSIONS` | Track and optimize for conversions |
476| `VIDEO_VIEWS` | Maximize video view engagement |
477| `APP_INSTALLS` | Drive mobile app installations |
478| `CATALOG_SALES` | Promote product catalog items |
479
480### Budget Types
481
482| Type | Description |
483|------|-------------|
484| `DAILY` | Average daily spend (may vary slightly) |
485| `LIFETIME` | Total spend over campaign duration |
486
487### Campaign Create Example
488
489```typescript
490interface CampaignCreate {
491 name: string;
492 objective: 'BRAND_AWARENESS' | 'TRAFFIC' | 'CONVERSIONS' | 'VIDEO_VIEWS' | 'APP_INSTALLS';
493 is_enabled: boolean;
494 budget_type: 'DAILY' | 'LIFETIME';
495 budget_total_amount_micros: number; // Amount in micros (1 USD = 1,000,000 micros)
496 start_time: string; // ISO 8601 format
497 end_time?: string; // ISO 8601 format (optional)
498}
499
500// Create a traffic campaign with $50/day budget
501const campaign: CampaignCreate = {
502 name: 'Q1 2025 Traffic Campaign',
503 objective: 'TRAFFIC',
504 is_enabled: true,
505 budget_type: 'DAILY',
506 budget_total_amount_micros: 50_000_000, // $50
507 start_time: '2025-01-15T00:00:00Z',
508 end_time: '2025-03-31T23:59:59Z'
509};
510
511const result = await client.createCampaign(campaign);
512```
513
514```python
515# Python example
516campaign = {
517 'name': 'Q1 2025 Traffic Campaign',
518 'objective': 'TRAFFIC',
519 'is_enabled': True,
520 'budget_type': 'DAILY',
521 'budget_total_amount_micros': 50_000_000, # $50
522 'start_time': '2025-01-15T00:00:00Z',
523 'end_time': '2025-03-31T23:59:59Z'
524}
525
526result = client.create_campaign(campaign)
527```
528
529---
530
531## Ad Group Creation
532
533### Bidding Strategies
534
535| Strategy | Description | Use Case |
536|----------|-------------|----------|
537| `LOWEST_COST` | Maximize conversions within budget | Best for most campaigns |
538| `COST_CAP` | Set average CPC cap | Control cost per result |
539| `MANUAL` | Set strict CPC/CPM bid | Maximum control |
540
541### Targeting Options
542
543| Targeting Type | Description |
544|----------------|-------------|
545| `communities` | Target specific subreddits |
546| `interests` | Target by interest categories |
547| `keywords` | Target by keyword engagement |
548| `devices` | Target by device type |
549| `locations` | Target by geography |
550| `custom_audiences` | Target uploaded customer lists |
551
552### Ad Group Create Example
553
554```typescript
555interface AdGroupCreate {
556 name: string;
557 campaign_id: string;
558 is_enabled: boolean;
559 bid_strategy: 'LOWEST_COST' | 'COST_CAP' | 'MANUAL';
560 bid_amount_micros?: number; // For COST_CAP or MANUAL
561 goal_type: 'CLICKS' | 'IMPRESSIONS' | 'CONVERSIONS';
562 goal_value_micros?: number;
563 targeting: {
564 communities?: string[]; // Subreddit names without r/
565 interests?: string[];
566 keywords?: string[];
567 geo_locations?: {
568 countries?: string[];
569 regions?: string[];
570 cities?: string[];
571 };
572 devices?: ('DESKTOP' | 'MOBILE' | 'TABLET')[];
573 custom_audience_ids?: string[];
574 };
575 start_time?: string;
576 end_time?: string;
577}
578
579// Create ad group targeting specific subreddits
580const adGroup: AdGroupCreate = {
581 name: 'Tech Enthusiasts - Subreddit Targeting',
582 campaign_id: 'campaign_123',
583 is_enabled: true,
584 bid_strategy: 'LOWEST_COST',
585 goal_type: 'CLICKS',
586 targeting: {
587 communities: [
588 'technology',
589 'gadgets',
590 'programming',
591 'webdev',
592 'startups'
593 ],
594 geo_locations: {
595 countries: ['US', 'CA', 'GB']
596 },
597 devices: ['DESKTOP', 'MOBILE']
598 },
599 start_time: '2025-01-15T00:00:00Z'
600};
601
602const result = await client.createAdGroup(adGroup);
603```
604
605```python
606# Python example
607ad_group = {
608 'name': 'Tech Enthusiasts - Subreddit Targeting',
609 'campaign_id': 'campaign_123',
610 'is_enabled': True,
611 'bid_strategy': 'LOWEST_COST',
612 'goal_type': 'CLICKS',
613 'targeting': {
614 'communities': [
615 'technology',
616 'gadgets',
617 'programming',
618 'webdev',
619 'startups'
620 ],
621 'geo_locations': {
622 'countries': ['US', 'CA', 'GB']
623 },
624 'devices': ['DESKTOP', 'MOBILE']
625 },
626 'start_time': '2025-01-15T00:00:00Z'
627}
628
629result = client.create_ad_group(ad_group)
630```
631
632---
633
634## Ad Creation
635
636### Ad Types
637
638| Type | Description |
639|------|-------------|
640| `LINK` | Link ad with image/video |
641| `TEXT` | Text-only promoted post |
642| `VIDEO` | Video ad |
643| `CAROUSEL` | Multiple images/cards |
644| `PRODUCT` | Product catalog ad |
645
646### Call-to-Action Options
647
648| CTA | Use Case |
649|-----|----------|
650| `SHOP_NOW` | E-commerce |
651| `SIGN_UP` | Lead generation |
652| `LEARN_MORE` | Information |
653| `DOWNLOAD` | App/content download |
654| `INSTALL` | App install |
655| `GET_QUOTE` | Services |
656| `CONTACT_US` | B2B/Services |
657| `APPLY_NOW` | Jobs/Finance |
658| `BOOK_NOW` | Travel/Services |
659| `WATCH_NOW` | Video content |
660| `SUBSCRIBE` | Newsletters/SaaS |
661| `GET_OFFER` | Promotions |
662| `SEE_MENU` | Restaurants |
663
664### Ad Create Example
665
666```typescript
667interface AdCreate {
668 name: string;
669 ad_group_id: string;
670 is_enabled: boolean;
671 type: 'LINK' | 'TEXT' | 'VIDEO' | 'CAROUSEL';
672 headline: string; // Max 300 characters
673 body?: string;
674 url: string;
675 display_url?: string;
676 call_to_action: string;
677 thumbnail_url?: string; // For image/video ads
678 video_url?: string; // For video ads
679}
680
681// Create a link ad
682const ad: AdCreate = {
683 name: 'Product Launch Ad - v1',
684 ad_group_id: 'ad_group_456',
685 is_enabled: true,
686 type: 'LINK',
687 headline: 'Introducing Our Revolutionary New Product',
688 body: 'Discover how our latest innovation can transform your workflow. Join 10,000+ satisfied customers.',
689 url: 'https://yoursite.com/product?utm_source=reddit&utm_medium=paid',
690 display_url: 'yoursite.com/product',
691 call_to_action: 'LEARN_MORE',
692 thumbnail_url: 'https://yoursite.com/images/ad-creative.jpg'
693};
694
695const result = await client.createAd(ad);
696```
697
698```python
699# Python example
700ad = {
701 'name': 'Product Launch Ad - v1',
702 'ad_group_id': 'ad_group_456',
703 'is_enabled': True,
704 'type': 'LINK',
705 'headline': 'Introducing Our Revolutionary New Product',
706 'body': 'Discover how our latest innovation can transform your workflow. Join 10,000+ satisfied customers.',
707 'url': 'https://yoursite.com/product?utm_source=reddit&utm_medium=paid',
708 'display_url': 'yoursite.com/product',
709 'call_to_action': 'LEARN_MORE',
710 'thumbnail_url': 'https://yoursite.com/images/ad-creative.jpg'
711}
712
713result = client.create_ad(ad)
714```
715
716---
717
718## Conversions API
719
720### Event Types
721
722| Event Type | Description |
723|------------|-------------|
724| `PAGE_VISIT` | Page view |
725| `VIEW_CONTENT` | Product/content view |
726| `SEARCH` | Search action |
727| `ADD_TO_CART` | Add to cart |
728| `ADD_TO_WISHLIST` | Add to wishlist |
729| `PURCHASE` | Completed purchase |
730| `LEAD` | Lead submission |
731| `SIGN_UP` | Account creation |
732| `CUSTOM` | Custom event |
733
734### Conversion Event Structure
735
736```typescript
737interface ConversionEvent {
738 event_at: number; // Unix timestamp in milliseconds
739 event_type: {
740 tracking_type: string;
741 custom_event_name?: string; // For CUSTOM type
742 };
743 user: {
744 email?: string; // SHA256 hashed, lowercase
745 phone_number?: string; // SHA256 hashed, E.164 format
746 external_id?: string;
747 ip_address?: string;
748 user_agent?: string;
749 aaid?: string; // Android Advertising ID
750 idfa?: string; // iOS IDFA
751 };
752 event_metadata?: {
753 item_count?: number;
754 value_decimal?: number;
755 currency?: string;
756 conversion_id: string; // Unique event ID
757 products?: Array<{
758 id: string;
759 name?: string;
760 category?: string;
761 }>;
762 };
763 click_id?: string; // Reddit click ID for attribution
764}
765```
766
767### Send Conversion Events
768
769```typescript
770import crypto from 'crypto';
771
772function hashPII(value: string): string {
773 return crypto
774 .createHash('sha256')
775 .update(value.toLowerCase().trim())
776 .digest('hex');
777}
778
779async function sendConversionEvent(
780 accessToken: string,
781 pixelId: string,
782 event: ConversionEvent
783) {
784 const response = await fetch(
785 `https://ads-api.reddit.com/api/v2.0/conversions/events/${pixelId}`,
786 {
787 method: 'POST',
788 headers: {
789 'Authorization': `Bearer ${accessToken}`,
790 'Content-Type': 'application/json'
791 },
792 body: JSON.stringify({
793 events: [event],
794 test_mode: false // Set true for testing
795 })
796 }
797 );
798
799 return response.json();
800}
801
802// Example: Track a purchase
803const purchaseEvent: ConversionEvent = {
804 event_at: Date.now(),
805 event_type: {
806 tracking_type: 'PURCHASE'
807 },
808 user: {
809 email: hashPII('customer@example.com'),
810 ip_address: '192.168.1.1',
811 user_agent: 'Mozilla/5.0...'
812 },
813 event_metadata: {
814 conversion_id: 'order_12345',
815 value_decimal: 99.99,
816 currency: 'USD',
817 item_count: 2,
818 products: [
819 { id: 'SKU001', name: 'Product A', category: 'Electronics' },
820 { id: 'SKU002', name: 'Product B', category: 'Electronics' }
821 ]
822 },
823 click_id: 'reddit_click_id_from_url' // From rdt_cid parameter
824};
825
826await sendConversionEvent(accessToken, 'pixel_123', purchaseEvent);
827```
828
829```python
830import hashlib
831import time
832import requests
833
834def hash_pii(value: str) -> str:
835 """SHA256 hash PII data."""
836 return hashlib.sha256(value.lower().strip().encode()).hexdigest()
837
838def send_conversion_event(
839 access_token: str,
840 pixel_id: str,
841 events: list[dict],
842 test_mode: bool = False
843) -> dict:
844 """Send conversion events to Reddit."""
845 response = requests.post(
846 f'https://ads-api.reddit.com/api/v2.0/conversions/events/{pixel_id}',
847 headers={
848 'Authorization': f'Bearer {access_token}',
849 'Content-Type': 'application/json'
850 },
851 json={
852 'events': events,
853 'test_mode': test_mode
854 }
855 )
856 response.raise_for_status()
857 return response.json()
858
859# Example: Track a purchase
860purchase_event = {
861 'event_at': int(time.time() * 1000),
862 'event_type': {
863 'tracking_type': 'PURCHASE'
864 },
865 'user': {
866 'email': hash_pii('customer@example.com'),
867 'ip_address': '192.168.1.1',
868 'user_agent': 'Mozilla/5.0...'
869 },
870 'event_metadata': {
871 'conversion_id': 'order_12345',
872 'value_decimal': 99.99,
873 'currency': 'USD',
874 'item_count': 2,
875 'products': [
876 {'id': 'SKU001', 'name': 'Product A', 'category': 'Electronics'},
877 {'id': 'SKU002', 'name': 'Product B', 'category': 'Electronics'}
878 ]
879 },
880 'click_id': 'reddit_click_id_from_url'
881}
882
883result = send_conversion_event(access_token, 'pixel_123', [purchase_event])
884```
885
886### Important Notes
887
888- Events must occur within **last 7 days** to be processed
889- Maximum **500 events per batch** request
890- Include `click_id` when available for better attribution
891- Use `test_mode: true` for testing without affecting campaigns
892
893---
894
895## Custom Audiences
896
897### Audience Types
898
899| Type | Description |
900|------|-------------|
901| `CUSTOMER_LIST` | Upload hashed emails/phone/MAIDs |
902| `WEBSITE_VISITORS` | Pixel-based retargeting |
903| `LOOKALIKE` | Similar to source audience |
904
905### Create Customer List Audience
906
907```typescript
908interface CustomAudienceCreate {
909 name: string;
910 type: 'CUSTOMER_LIST';
911 description?: string;
912 users: Array<{
913 email_sha256?: string;
914 maid_sha256?: string; // Mobile Advertising ID
915 }>;
916}
917
918// Create audience from customer emails
919const audience: CustomAudienceCreate = {
920 name: 'High Value Customers Q4 2024',
921 type: 'CUSTOMER_LIST',
922 description: 'Customers with LTV > $500',
923 users: customerEmails.map(email => ({
924 email_sha256: hashPII(email)
925 }))
926};
927
928const result = await client.createCustomAudience(audience);
929```
930
931### Minimum Audience Size
932
933- **1,000 matched users** minimum to be usable for targeting
934- Match rates displayed as ranges for privacy
935
936---
937
938## Reporting
939
940### Report Request
941
942```typescript
943interface ReportRequest {
944 start_date: string; // YYYY-MM-DD
945 end_date: string; // YYYY-MM-DD
946 level: 'ACCOUNT' | 'CAMPAIGN' | 'AD_GROUP' | 'AD';
947 metrics: string[];
948 dimensions?: string[];
949 filters?: {
950 campaign_ids?: string[];
951 ad_group_ids?: string[];
952 };
953}
954
955// Get campaign performance report
956const report = await client.getReport({
957 start_date: '2025-01-01',
958 end_date: '2025-01-31',
959 level: 'CAMPAIGN',
960 metrics: [
961 'impressions',
962 'clicks',
963 'spend',
964 'ctr',
965 'cpc',
966 'conversions',
967 'conversion_rate',
968 'cpa'
969 ],
970 dimensions: ['date']
971});
972```
973
974### Available Metrics
975
976| Metric | Description |
977|--------|-------------|
978| `impressions` | Total impressions |
979| `clicks` | Total clicks |
980| `spend` | Total spend (in account currency) |
981| `ctr` | Click-through rate |
982| `cpc` | Cost per click |
983| `cpm` | Cost per 1,000 impressions |
984| `conversions` | Total conversions |
985| `conversion_rate` | Conversions / Clicks |
986| `cpa` | Cost per acquisition |
987| `video_views` | Video view count |
988| `video_completions` | Videos watched to completion |
989
990---
991
992## Environment Variables
993
994```bash
995# .env
996REDDIT_ADS_CLIENT_ID=your_client_id
997REDDIT_ADS_CLIENT_SECRET=your_client_secret
998REDDIT_ADS_ACCOUNT_ID=t2_xxxxx
999REDDIT_ADS_ACCESS_TOKEN=your_access_token
1000REDDIT_ADS_REFRESH_TOKEN=your_refresh_token
1001REDDIT_ADS_PIXEL_ID=your_pixel_id
1002```
1003
1004---
1005
1006## Best Practices
1007
1008### Campaign Structure
1009
1010```
1011┌─────────────────────────────────────────────────────────────────┐
1012│ RECOMMENDED STRUCTURE │
1013│ ───────────────────────────────────────────────────────────── │
1014│ │
1015│ Campaign (by objective/product line) │
1016│ ├── Ad Group: Subreddit Targeting - Tech │
1017│ │ ├── Ad: Headline A + Image 1 │
1018│ │ └── Ad: Headline B + Image 1 │
1019│ ├── Ad Group: Subreddit Targeting - Business │
1020│ │ ├── Ad: Headline A + Image 1 │
1021│ │ └── Ad: Headline B + Image 1 │
1022│ └── Ad Group: Interest Targeting - Entrepreneurs │
1023│ ├── Ad: Headline A + Image 2 │
1024│ └── Ad: Headline B + Image 2 │
1025│ │
1026│ • Separate ad groups by targeting type │
1027│ • Test 2-3 ad variations per ad group │
1028│ • Use clear naming conventions │
1029└─────────────────────────────────────────────────────────────────┘
1030```
1031
1032### Naming Conventions
1033
1034```
1035Campaign: [Objective] - [Product/Brand] - [Date Range]
1036 Example: TRAFFIC - ProductX - Q1-2025
1037
1038Ad Group: [Targeting Type] - [Audience Description]
1039 Example: Subreddits - Tech Enthusiasts
1040
1041Ad: [Headline Type] - [Creative Version]
1042 Example: Problem-Solution - Image-A
1043```
1044
1045### Rate Limiting
1046
1047- **1 request per second** limit
1048- Implement exponential backoff for retries
1049- Batch operations where possible
1050
1051```typescript
1052async function rateLimitedRequest<T>(
1053 fn: () => Promise<T>,
1054 retries = 3
1055): Promise<T> {
1056 for (let i = 0; i < retries; i++) {
1057 try {
1058 await new Promise(resolve => setTimeout(resolve, 1000)); // 1 second delay
1059 return await fn();
1060 } catch (error: any) {
1061 if (error.status === 429 && i < retries - 1) {
1062 const delay = Math.pow(2, i) * 1000;
1063 await new Promise(resolve => setTimeout(resolve, delay));
1064 continue;
1065 }
1066 throw error;
1067 }
1068 }
1069 throw new Error('Max retries exceeded');
1070}
1071```
1072
1073---
1074
1075## Complete Workflow Example
1076
1077```typescript
1078// Full campaign creation workflow
1079async function createRedditAdCampaign(
1080 client: RedditAdsClient,
1081 config: {
1082 campaignName: string;
1083 dailyBudget: number;
1084 targetSubreddits: string[];
1085 headline: string;
1086 body: string;
1087 landingUrl: string;
1088 imageUrl: string;
1089 }
1090) {
1091 // 1. Create Campaign
1092 const campaign = await client.createCampaign({
1093 name: config.campaignName,
1094 objective: 'TRAFFIC',
1095 is_enabled: false, // Start paused for review
1096 budget_type: 'DAILY',
1097 budget_total_amount_micros: config.dailyBudget * 1_000_000,
1098 start_time: new Date().toISOString()
1099 });
1100
1101 console.log(`Created campaign: ${campaign.id}`);
1102
1103 // 2. Create Ad Group with targeting
1104 const adGroup = await client.createAdGroup({
1105 name: `${config.campaignName} - Subreddit Targeting`,
1106 campaign_id: campaign.id,
1107 is_enabled: true,
1108 bid_strategy: 'LOWEST_COST',
1109 goal_type: 'CLICKS',
1110 targeting: {
1111 communities: config.targetSubreddits,
1112 geo_locations: { countries: ['US'] },
1113 devices: ['DESKTOP', 'MOBILE']
1114 }
1115 });
1116
1117 console.log(`Created ad group: ${adGroup.id}`);
1118
1119 // 3. Create Ad
1120 const ad = await client.createAd({
1121 name: `${config.campaignName} - Ad v1`,
1122 ad_group_id: adGroup.id,
1123 is_enabled: true,
1124 type: 'LINK',
1125 headline: config.headline,
1126 body: config.body,
1127 url: config.landingUrl,
1128 call_to_action: 'LEARN_MORE',
1129 thumbnail_url: config.imageUrl
1130 });
1131
1132 console.log(`Created ad: ${ad.id}`);
1133
1134 return { campaign, adGroup, ad };
1135}
1136
1137// Usage
1138const result = await createRedditAdCampaign(client, {
1139 campaignName: 'Product Launch - Jan 2025',
1140 dailyBudget: 50, // $50/day
1141 targetSubreddits: ['technology', 'gadgets', 'programming'],
1142 headline: 'Introducing the Future of Development',
1143 body: 'Join 50,000+ developers using our tool to ship faster.',
1144 landingUrl: 'https://yoursite.com?utm_source=reddit',
1145 imageUrl: 'https://yoursite.com/ad-image.jpg'
1146});
1147```
1148
1149---
1150
1151## Testing
1152
1153### Test Checklist
1154
1155- [ ] OAuth flow completes successfully
1156- [ ] Token refresh works before expiry
1157- [ ] Campaign creates with correct budget
1158- [ ] Ad group targeting is applied correctly
1159- [ ] Ad creative displays properly
1160- [ ] Conversion events tracked (use test_mode)
1161- [ ] Reports return expected metrics
1162- [ ] Rate limiting handled gracefully
1163- [ ] Error responses handled properly
1164
1165### Mock API for Development
1166
1167```typescript
1168// test/mocks/reddit-ads-mock.ts
1169import { rest } from 'msw';
1170
1171export const redditAdsMocks = [
1172 rest.post('https://www.reddit.com/api/v1/access_token', (req, res, ctx) => {
1173 return res(ctx.json({
1174 access_token: 'mock_access_token',
1175 refresh_token: 'mock_refresh_token',
1176 expires_in: 3600,
1177 scope: 'adsread adsedit history'
1178 }));
1179 }),
1180
1181 rest.get('https://ads-api.reddit.com/api/v2.0/accounts/:accountId', (req, res, ctx) => {
1182 return res(ctx.json({
1183 id: req.params.accountId,
1184 name: 'Test Account',
1185 currency: 'USD'
1186 }));
1187 }),
1188
1189 rest.post('https://ads-api.reddit.com/api/v2.0/accounts/:accountId/campaigns', (req, res, ctx) => {
1190 return res(ctx.json({
1191 id: 'campaign_mock_123',
1192 ...req.body
1193 }));
1194 })
1195];
1196```
1197
1198---
1199
1200## Troubleshooting
1201
1202| Error | Cause | Fix |
1203|-------|-------|-----|
1204| `401 Unauthorized` | Invalid/expired token | Refresh access token |
1205| `403 Forbidden` | Account not whitelisted | Contact Reddit Ads support |
1206| `429 Too Many Requests` | Rate limit exceeded | Implement backoff, slow down |
1207| `400 Bad Request` | Invalid payload | Check required fields, data types |
1208| `Audience too small` | < 1,000 matched users | Add more users to audience |
1209
1210---
1211
1212---
1213
1214## Agentic Optimization Service
1215
1216### Architecture Overview
1217
1218```
1219┌─────────────────────────────────────────────────────────────────┐
1220│ AGENTIC REDDIT ADS OPTIMIZER │
1221│ ───────────────────────────────────────────────────────────── │
1222│ │
1223│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
1224│ │ Scheduler │───▶│ Analyzer │───▶│ Optimizer │ │
1225│ │ (Cron) │ │ (AI/LLM) │ │ (Actions) │ │
1226│ └─────────────┘ └─────────────┘ └─────────────┘ │
1227│ │ │ │ │
1228│ ▼ ▼ ▼ │
1229│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
1230│ │ Fetch │ │ Decide │ │ Execute │ │
1231│ │ Reports │ │ Strategy │ │ Changes │ │
1232│ └─────────────┘ └─────────────┘ └─────────────┘ │
1233│ │
1234│ Loop: Every 4-6 hours │
1235│ Actions: Pause losers, scale winners, adjust bids, rotate ads │
1236└─────────────────────────────────────────────────────────────────┘
1237```
1238
1239### Background Service (Node.js)
1240
1241```typescript
1242// services/reddit-ads-optimizer.ts
1243import Anthropic from '@anthropic-ai/sdk';
1244import { CronJob } from 'cron';
1245import RedditAdsClient from '../lib/reddit-ads-client';
1246
1247interface OptimizationConfig {
1248 accountId: string;
1249 accessToken: string;
1250 refreshToken: string;
1251 // Thresholds
1252 minCTR: number; // Pause ads below this CTR (e.g., 0.005 = 0.5%)
1253 maxCPA: number; // Pause ads above this CPA
1254 minImpressions: number; // Min impressions before decisions (e.g., 1000)
1255 budgetScaleFactor: number; // Scale winning ad groups by this factor (e.g., 1.5)
1256 // Optimization settings
1257 optimizationGoal: 'CLICKS' | 'CONVERSIONS' | 'ROAS';
1258 checkIntervalHours: number;
1259}
1260
1261interface PerformanceData {
1262 campaignId: string;
1263 adGroupId: string;
1264 adId: string;
1265 impressions: number;
1266 clicks: number;
1267 spend: number;
1268 conversions: number;
1269 ctr: number;
1270 cpc: number;
1271 cpa: number;
1272 roas: number;
1273}
1274
1275class RedditAdsOptimizerService {
1276 private client: RedditAdsClient;
1277 private anthropic: Anthropic;
1278 private config: OptimizationConfig;
1279 private cronJob: CronJob | null = null;
1280
1281 constructor(config: OptimizationConfig) {
1282 this.config = config;
1283 this.client = new RedditAdsClient({
1284 accessToken: config.accessToken,
1285 accountId: config.accountId
1286 });
1287 this.anthropic = new Anthropic();
1288 }
1289
1290 // Start the background optimization service
1291 start() {
1292 const cronSchedule = `0 */${this.config.checkIntervalHours} * * *`;
1293
1294 this.cronJob = new CronJob(cronSchedule, async () => {
1295 console.log(`[${new Date().toISOString()}] Running optimization cycle...`);
1296 await this.runOptimizationCycle();
1297 });
1298
1299 this.cronJob.start();
1300 console.log(`Reddit Ads Optimizer started. Running every ${this.config.checkIntervalHours} hours.`);
1301 }
1302
1303 stop() {
1304 if (this.cronJob) {
1305 this.cronJob.stop();
1306 console.log('Reddit Ads Optimizer stopped.');
1307 }
1308 }
1309
1310 // Main optimization cycle
1311 async runOptimizationCycle() {
1312 try {
1313 // 1. Fetch performance data
1314 const performanceData = await this.fetchPerformanceData();
1315
1316 // 2. Analyze with AI agent
1317 const recommendations = await this.analyzeWithAgent(performanceData);
1318
1319 // 3. Execute optimizations
1320 await this.executeOptimizations(recommendations);
1321
1322 // 4. Log results
1323 await this.logOptimizationResults(recommendations);
1324
1325 } catch (error) {
1326 console.error('Optimization cycle failed:', error);
1327 await this.sendAlert('Optimization cycle failed', error);
1328 }
1329 }
1330
1331 // Fetch last 24h performance data
1332 private async fetchPerformanceData(): Promise<PerformanceData[]> {
1333 const endDate = new Date();
1334 const startDate = new Date(endDate.getTime() - 24 * 60 * 60 * 1000);
1335
1336 const report = await this.client.getReport({
1337 start_date: startDate.toISOString().split('T')[0],
1338 end_date: endDate.toISOString().split('T')[0],
1339 level: 'AD',
1340 metrics: [
1341 'impressions', 'clicks', 'spend', 'conversions',
1342 'ctr', 'cpc', 'cpa', 'conversion_value'
1343 ]
1344 });
1345
1346 return report.data.map((row: any) => ({
1347 campaignId: row.campaign_id,
1348 adGroupId: row.ad_group_id,
1349 adId: row.ad_id,
1350 impressions: row.impressions,
1351 clicks: row.clicks,
1352 spend: row.spend,
1353 conversions: row.conversions || 0,
1354 ctr: row.ctr,
1355 cpc: row.cpc,
1356 cpa: row.cpa || 0,
1357 roas: row.conversion_value ? row.conversion_value / row.spend : 0
1358 }));
1359 }
1360
1361 // AI-powered analysis and decision making
1362 private async analyzeWithAgent(data: PerformanceData[]): Promise<OptimizationRecommendation[]> {
1363 const prompt = `You are a Reddit Ads optimization agent. Analyze the following campaign performance data and recommend specific actions.
1364
1365## Performance Data (Last 24 Hours)
1366${JSON.stringify(data, null, 2)}
1367
1368## Optimization Configuration
1369- Goal: ${this.config.optimizationGoal}
1370- Min CTR threshold: ${this.config.minCTR * 100}%
1371- Max CPA threshold: $${this.config.maxCPA}
1372- Min impressions for decisions: ${this.config.minImpressions}
1373- Budget scale factor for winners: ${this.config.budgetScaleFactor}x
1374
1375## Your Task
1376Analyze each ad/ad group and recommend ONE action per item:
13771. PAUSE - Poor performers (low CTR, high CPA, no conversions after sufficient impressions)
13782. SCALE - Winners (high CTR, low CPA, good ROAS) - increase budget
13793. ADJUST_BID - Moderate performers - suggest bid adjustment
13804. KEEP - Insufficient data or acceptable performance
13815. ROTATE_CREATIVE - Good targeting but ad fatigue (declining CTR over time)
1382
1383Return a JSON array of recommendations:
1384[
1385 {
1386 "adId": "string",
1387 "adGroupId": "string",
1388 "action": "PAUSE|SCALE|ADJUST_BID|KEEP|ROTATE_CREATIVE",
1389 "reason": "Brief explanation",
1390 "newBidMicros": number (optional, for ADJUST_BID),
1391 "budgetMultiplier": number (optional, for SCALE)
1392 }
1393]
1394
1395Be aggressive with pausing poor performers to protect budget. Be conservative with scaling (only clear winners).`;
1396
1397 const response = await this.anthropic.messages.create({
1398 model: 'claude-sonnet-4-6',
1399 max_tokens: 4096,
1400 messages: [{ role: 'user', content: prompt }]
1401 });
1402
1403 const content = response.content[0];
1404 if (content.type !== 'text') throw new Error('Unexpected response type');
1405
1406 // Extract JSON from response
1407 const jsonMatch = content.text.match(/\[[\s\S]*\]/);
1408 if (!jsonMatch) throw new Error('No JSON found in response');
1409
1410 return JSON.parse(jsonMatch[0]);
1411 }
1412
1413 // Execute the AI recommendations
1414 private async executeOptimizations(recommendations: OptimizationRecommendation[]) {
1415 for (const rec of recommendations) {
1416 try {
1417 switch (rec.action) {
1418 case 'PAUSE':
1419 await this.client.updateAd(rec.adId, { is_enabled: false });
1420 console.log(`Paused ad ${rec.adId}: ${rec.reason}`);
1421 break;
1422
1423 case 'SCALE':
1424 const adGroup = await this.client.getAdGroup(rec.adGroupId);
1425 const currentBudget = adGroup.budget_total_amount_micros;
1426 const newBudget = Math.round(currentBudget * (rec.budgetMultiplier || this.config.budgetScaleFactor));
1427 await this.client.updateAdGroup(rec.adGroupId, {
1428 budget_total_amount_micros: newBudget
1429 });
1430 console.log(`Scaled ad group ${rec.adGroupId} budget to ${newBudget / 1_000_000}: ${rec.reason}`);
1431 break;
1432
1433 case 'ADJUST_BID':
1434 if (rec.newBidMicros) {
1435 await this.client.updateAdGroup(rec.adGroupId, {
1436 bid_amount_micros: rec.newBidMicros
1437 });
1438 console.log(`Adjusted bid for ${rec.adGroupId} to ${rec.newBidMicros / 1_000_000}: ${rec.reason}`);
1439 }
1440 break;
1441
1442 case 'ROTATE_CREATIVE':
1443 // Flag for creative refresh (implement your creative rotation logic)
1444 console.log(`Creative rotation needed for ${rec.adId}: ${rec.reason}`);
1445 await this.flagForCreativeRefresh(rec.adId);
1446 break;
1447
1448 case 'KEEP':
1449 // No action needed
1450 break;
1451 }
1452 } catch (error) {
1453 console.error(`Failed to execute ${rec.action} for ${rec.adId}:`, error);
1454 }
1455 }
1456 }
1457
1458 private async flagForCreativeRefresh(adId: string) {
1459 // Implement: Add to queue, notify team, or auto-generate new creative
1460 }
1461
1462 private async logOptimizationResults(recommendations: OptimizationRecommendation[]) {
1463 const summary = {
1464 timestamp: new Date().toISOString(),
1465 totalRecommendations: recommendations.length,
1466 actions: {
1467 paused: recommendations.filter(r => r.action === 'PAUSE').length,
1468 scaled: recommendations.filter(r => r.action === 'SCALE').length,
1469 bidAdjusted: recommendations.filter(r => r.action === 'ADJUST_BID').length,
1470 creativeRotation: recommendations.filter(r => r.action === 'ROTATE_CREATIVE').length,
1471 kept: recommendations.filter(r => r.action === 'KEEP').length
1472 }
1473 };
1474 console.log('Optimization Summary:', JSON.stringify(summary, null, 2));
1475 // Store in database for historical analysis
1476 }
1477
1478 private async sendAlert(subject: string, error: any) {
1479 // Implement: Send email/Slack notification
1480 }
1481}
1482
1483interface OptimizationRecommendation {
1484 adId: string;
1485 adGroupId: string;
1486 action: 'PAUSE' | 'SCALE' | 'ADJUST_BID' | 'KEEP' | 'ROTATE_CREATIVE';
1487 reason: string;
1488 newBidMicros?: number;
1489 budgetMultiplier?: number;
1490}
1491
1492export default RedditAdsOptimizerService;
1493```
1494
1495### Background Service (Python)
1496
1497```python
1498# services/reddit_ads_optimizer.py
1499import anthropic
1500import schedule
1501import time
1502import json
1503from datetime import datetime, timedelta
1504from typing import List, Dict, Any, Optional
1505from dataclasses import dataclass
1506from enum import Enum
1507
1508from lib.reddit_ads_client import RedditAdsClient, RedditAdsConfig
1509
1510class OptimizationAction(Enum):
1511 PAUSE = "PAUSE"
1512 SCALE = "SCALE"
1513 ADJUST_BID = "ADJUST_BID"
1514 KEEP = "KEEP"
1515 ROTATE_CREATIVE = "ROTATE_CREATIVE"
1516
1517@dataclass
1518class OptimizationConfig:
1519 account_id: str
1520 access_token: str
1521 refresh_token: str
1522 min_ctr: float = 0.005 # 0.5%
1523 max_cpa: float = 50.0
1524 min_impressions: int = 1000
1525 budget_scale_factor: float = 1.5
1526 optimization_goal: str = "CONVERSIONS"
1527 check_interval_hours: int = 4
1528
1529@dataclass
1530class PerformanceData:
1531 campaign_id: str
1532 ad_group_id: str
1533 ad_id: str
1534 impressions: int
1535 clicks: int
1536 spend: float
1537 conversions: int
1538 ctr: float
1539 cpc: float
1540 cpa: float
1541 roas: float
1542
1543@dataclass
1544class OptimizationRecommendation:
1545 ad_id: str
1546 ad_group_id: str
1547 action: OptimizationAction
1548 reason: str
1549 new_bid_micros: Optional[int] = None
1550 budget_multiplier: Optional[float] = None
1551
1552class RedditAdsOptimizerService:
1553 def __init__(self, config: OptimizationConfig):
1554 self.config = config
1555 self.client = RedditAdsClient(RedditAdsConfig(
1556 access_token=config.access_token,
1557 account_id=config.account_id
1558 ))
1559 self.anthropic = anthropic.Anthropic()
1560 self._running = False
1561
1562 def start(self):
1563 """Start the background optimization service."""
1564 self._running = True
1565
1566 # Schedule optimization runs
1567 schedule.every(self.config.check_interval_hours).hours.do(
1568 self.run_optimization_cycle
1569 )
1570
1571 print(f"Reddit Ads Optimizer started. Running every {self.config.check_interval_hours} hours.")
1572
1573 # Run immediately on start
1574 self.run_optimization_cycle()
1575
1576 # Keep running
1577 while self._running:
1578 schedule.run_pending()
1579 time.sleep(60)
1580
1581 def stop(self):
1582 """Stop the optimization service."""
1583 self._running = False
1584 print("Reddit Ads Optimizer stopped.")
1585
1586 def run_optimization_cycle(self):
1587 """Main optimization cycle."""
1588 print(f"[{datetime.now().isoformat()}] Running optimization cycle...")
1589
1590 try:
1591 # 1. Fetch performance data
1592 performance_data = self._fetch_performance_data()
1593
1594 # 2. Analyze with AI agent
1595 recommendations = self._analyze_with_agent(performance_data)
1596
1597 # 3. Execute optimizations
1598 self._execute_optimizations(recommendations)
1599
1600 # 4. Log results
1601 self._log_optimization_results(recommendations)
1602
1603 except Exception as e:
1604 print(f"Optimization cycle failed: {e}")
1605 self._send_alert("Optimization cycle failed", str(e))
1606
1607 def _fetch_performance_data(self) -> List[PerformanceData]:
1608 """Fetch last 24h performance data."""
1609 end_date = datetime.now()
1610 start_date = end_date - timedelta(days=1)
1611
1612 report = self.client.get_report({
1613 'start_date': start_date.strftime('%Y-%m-%d'),
1614 'end_date': end_date.strftime('%Y-%m-%d'),
1615 'level': 'AD',
1616 'metrics': [
1617 'impressions', 'clicks', 'spend', 'conversions',
1618 'ctr', 'cpc', 'cpa', 'conversion_value'
1619 ]
1620 })
1621
1622 return [
1623 PerformanceData(
1624 campaign_id=row['campaign_id'],
1625 ad_group_id=row['ad_group_id'],
1626 ad_id=row['ad_id'],
1627 impressions=row['impressions'],
1628 clicks=row['clicks'],
1629 spend=row['spend'],
1630 conversions=row.get('conversions', 0),
1631 ctr=row['ctr'],
1632 cpc=row['cpc'],
1633 cpa=row.get('cpa', 0),
1634 roas=row.get('conversion_value', 0) / row['spend'] if row['spend'] > 0 else 0
1635 )
1636 for row in report.get('data', [])
1637 ]
1638
1639 def _analyze_with_agent(self, data: List[PerformanceData]) -> List[OptimizationRecommendation]:
1640 """AI-powered analysis and decision making."""
1641
1642 prompt = f"""You are a Reddit Ads optimization agent. Analyze the following campaign performance data and recommend specific actions.
1643
1644## Performance Data (Last 24 Hours)
1645{json.dumps([vars(d) for d in data], indent=2)}
1646
1647## Optimization Configuration
1648- Goal: {self.config.optimization_goal}
1649- Min CTR threshold: {self.config.min_ctr * 100}%
1650- Max CPA threshold: ${self.config.max_cpa}
1651- Min impressions for decisions: {self.config.min_impressions}
1652- Budget scale factor for winners: {self.config.budget_scale_factor}x
1653
1654## Your Task
1655Analyze each ad/ad group and recommend ONE action per item:
16561. PAUSE - Poor performers (low CTR, high CPA, no conversions after sufficient impressions)
16572. SCALE - Winners (high CTR, low CPA, good ROAS) - increase budget
16583. ADJUST_BID - Moderate performers - suggest bid adjustment
16594. KEEP - Insufficient data or acceptable performance
16605. ROTATE_CREATIVE - Good targeting but ad fatigue (declining CTR over time)
1661
1662Return a JSON array of recommendations:
1663[
1664 {{
1665 "ad_id": "string",
1666 "ad_group_id": "string",
1667 "action": "PAUSE|SCALE|ADJUST_BID|KEEP|ROTATE_CREATIVE",
1668 "reason": "Brief explanation",
1669 "new_bid_micros": number (optional, for ADJUST_BID),
1670 "budget_multiplier": number (optional, for SCALE)
1671 }}
1672]
1673
1674Be aggressive with pausing poor performers to protect budget. Be conservative with scaling (only clear winners)."""
1675
1676 response = self.anthropic.messages.create(
1677 model="claude-sonnet-4-6",
1678 max_tokens=4096,
1679 messages=[{"role": "user", "content": prompt}]
1680 )
1681
1682 content = response.content[0].text
1683
1684 # Extract JSON from response
1685 import re
1686 json_match = re.search(r'\[[\s\S]*\]', content)
1687 if not json_match:
1688 raise ValueError("No JSON found in response")
1689
1690 recommendations_data = json.loads(json_match.group())
1691
1692 return [
1693 OptimizationRecommendation(
1694 ad_id=r['ad_id'],
1695 ad_group_id=r['ad_group_id'],
1696 action=OptimizationAction(r['action']),
1697 reason=r['reason'],
1698 new_bid_micros=r.get('new_bid_micros'),
1699 budget_multiplier=r.get('budget_multiplier')
1700 )
1701 for r in recommendations_data
1702 ]
1703
1704 def _execute_optimizations(self, recommendations: List[OptimizationRecommendation]):
1705 """Execute the AI recommendations."""
1706 for rec in recommendations:
1707 try:
1708 if rec.action == OptimizationAction.PAUSE:
1709 self.client.update_ad(rec.ad_id, {'is_enabled': False})
1710 print(f"Paused ad {rec.ad_id}: {rec.reason}")
1711
1712 elif rec.action == OptimizationAction.SCALE:
1713 ad_group = self.client.get_ad_group(rec.ad_group_id)
1714 current_budget = ad_group['budget_total_amount_micros']
1715 multiplier = rec.budget_multiplier or self.config.budget_scale_factor
1716 new_budget = int(current_budget * multiplier)
1717 self.client.update_ad_group(rec.ad_group_id, {
1718 'budget_total_amount_micros': new_budget
1719 })
1720 print(f"Scaled ad group {rec.ad_group_id} budget to ${new_budget / 1_000_000}: {rec.reason}")
1721
1722 elif rec.action == OptimizationAction.ADJUST_BID:
1723 if rec.new_bid_micros:
1724 self.client.update_ad_group(rec.ad_group_id, {
1725 'bid_amount_micros': rec.new_bid_micros
1726 })
1727 print(f"Adjusted bid for {rec.ad_group_id}: {rec.reason}")
1728
1729 elif rec.action == OptimizationAction.ROTATE_CREATIVE:
1730 print(f"Creative rotation needed for {rec.ad_id}: {rec.reason}")
1731 self._flag_for_creative_refresh(rec.ad_id)
1732
1733 except Exception as e:
1734 print(f"Failed to execute {rec.action} for {rec.ad_id}: {e}")
1735
1736 def _flag_for_creative_refresh(self, ad_id: str):
1737 """Flag ad for creative refresh."""
1738 # Implement: Add to queue, notify team, or auto-generate new creative
1739 pass
1740
1741 def _log_optimization_results(self, recommendations: List[OptimizationRecommendation]):
1742 """Log optimization results."""
1743 summary = {
1744 'timestamp': datetime.now().isoformat(),
1745 'total_recommendations': len(recommendations),
1746 'actions': {
1747 'paused': len([r for r in recommendations if r.action == OptimizationAction.PAUSE]),
1748 'scaled': len([r for r in recommendations if r.action == OptimizationAction.SCALE]),
1749 'bid_adjusted': len([r for r in recommendations if r.action == OptimizationAction.ADJUST_BID]),
1750 'creative_rotation': len([r for r in recommendations if r.action == OptimizationAction.ROTATE_CREATIVE]),
1751 'kept': len([r for r in recommendations if r.action == OptimizationAction.KEEP]),
1752 }
1753 }
1754 print(f"Optimization Summary: {json.dumps(summary, indent=2)}")
1755
1756 def _send_alert(self, subject: str, error: str):
1757 """Send alert notification."""
1758 # Implement: Send email/Slack notification
1759 pass
1760
1761
1762# Entry point for running as background service
1763if __name__ == "__main__":
1764 import os
1765
1766 config = OptimizationConfig(
1767 account_id=os.environ['REDDIT_ADS_ACCOUNT_ID'],
1768 access_token=os.environ['REDDIT_ADS_ACCESS_TOKEN'],
1769 refresh_token=os.environ['REDDIT_ADS_REFRESH_TOKEN'],
1770 min_ctr=0.005,
1771 max_cpa=50.0,
1772 min_impressions=1000,
1773 budget_scale_factor=1.5,
1774 optimization_goal="CONVERSIONS",
1775 check_interval_hours=4
1776 )
1777
1778 optimizer = RedditAdsOptimizerService(config)
1779 optimizer.start()
1780```
1781
1782### Docker Deployment
1783
1784```dockerfile
1785# Dockerfile
1786FROM python:3.11-slim
1787
1788WORKDIR /app
1789
1790COPY requirements.txt .
1791RUN pip install --no-cache-dir -r requirements.txt
1792
1793COPY . .
1794
1795CMD ["python", "services/reddit_ads_optimizer.py"]
1796```
1797
1798```yaml
1799# docker-compose.yml
1800version: '3.8'
1801
1802services:
1803 reddit-ads-optimizer:
1804 build: .
1805 container_name: reddit-ads-optimizer
1806 restart: unless-stopped
1807 environment:
1808 - REDDIT_ADS_CLIENT_ID=${REDDIT_ADS_CLIENT_ID}
1809 - REDDIT_ADS_CLIENT_SECRET=${REDDIT_ADS_CLIENT_SECRET}
1810 - REDDIT_ADS_ACCOUNT_ID=${REDDIT_ADS_ACCOUNT_ID}
1811 - REDDIT_ADS_ACCESS_TOKEN=${REDDIT_ADS_ACCESS_TOKEN}
1812 - REDDIT_ADS_REFRESH_TOKEN=${REDDIT_ADS_REFRESH_TOKEN}
1813 - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
1814 volumes:
1815 - ./logs:/app/logs
1816 logging:
1817 driver: "json-file"
1818 options:
1819 max-size: "10m"
1820 max-file: "3"
1821```
1822
1823### Optimization Strategies
1824
1825```
1826┌─────────────────────────────────────────────────────────────────┐
1827│ AGENTIC OPTIMIZATION STRATEGIES │
1828├─────────────────────────────────────────────────────────────────┤
1829│ │
1830│ 1. PERFORMANCE-BASED PAUSING │
1831│ ───────────────────────────────────────────────────────── │
1832│ IF impressions > 1000 AND ctr < 0.3% → PAUSE │
1833│ IF impressions > 500 AND conversions = 0 → PAUSE │
1834│ IF cpa > 2x target → PAUSE │
1835│ │
1836│ 2. WINNER SCALING │
1837│ ───────────────────────────────────────────────────────── │
1838│ IF ctr > 1% AND cpa < target AND conversions > 5 │
1839│ → SCALE budget by 1.5x │
1840│ Cap at 3x original budget to manage risk │
1841│ │
1842│ 3. BID OPTIMIZATION │
1843│ ───────────────────────────────────────────────────────── │
1844│ IF position low AND ctr good → INCREASE bid 10-20% │
1845│ IF cpa high but converting → DECREASE bid 10-15% │
1846│ │
1847│ 4. CREATIVE FATIGUE DETECTION │
1848│ ───────────────────────────────────────────────────────── │
1849│ IF ctr declining 3 consecutive days → ROTATE_CREATIVE │
1850│ IF frequency > 3 → ROTATE_CREATIVE │
1851│ │
1852│ 5. BUDGET REALLOCATION │
1853│ ───────────────────────────────────────────────────────── │
1854│ Move budget from paused ads to scaled winners │
1855│ Maintain total daily budget cap │
1856└─────────────────────────────────────────────────────────────────┘
1857```
1858
1859### Advanced: Multi-Agent Optimization
1860
1861```typescript
1862// services/multi-agent-optimizer.ts
1863import Anthropic from '@anthropic-ai/sdk';
1864
1865interface AgentRole {
1866 name: string;
1867 systemPrompt: string;
1868}
1869
1870const AGENTS: AgentRole[] = [
1871 {
1872 name: 'Performance Analyst',
1873 systemPrompt: `You analyze Reddit Ads performance data. Identify:
1874 - Top performers (high CTR, low CPA, good ROAS)
1875 - Poor performers (low CTR, high CPA, no conversions)
1876 - Trends (improving, declining, stable)
1877 Output structured analysis with confidence scores.`
1878 },
1879 {
1880 name: 'Budget Strategist',
1881 systemPrompt: `You optimize budget allocation across campaigns.
1882 Given performance analysis, recommend:
1883 - Budget increases for winners (max 50% increase)
1884 - Budget decreases for losers
1885 - Reallocation between ad groups
1886 Protect total budget while maximizing ROI.`
1887 },
1888 {
1889 name: 'Creative Director',
1890 systemPrompt: `You evaluate ad creative performance.
1891 Identify ads with:
1892 - Creative fatigue (declining engagement)
1893 - High potential but poor execution
1894 - A/B test winners
1895 Recommend creative refreshes and new variations.`
1896 },
1897 {
1898 name: 'Risk Manager',
1899 systemPrompt: `You ensure optimization safety.
1900 Review recommendations and flag:
1901 - Overly aggressive scaling
1902 - Insufficient data for decisions
1903 - Budget concentration risk
1904 - Compliance concerns
1905 Approve, modify, or reject recommendations.`
1906 }
1907];
1908
1909class MultiAgentOptimizer {
1910 private anthropic: Anthropic;
1911
1912 constructor() {
1913 this.anthropic = new Anthropic();
1914 }
1915
1916 async runAgentPipeline(performanceData: any) {
1917 let context = { performanceData };
1918
1919 // Run agents in sequence, each building on previous output
1920 for (const agent of AGENTS) {
1921 const response = await this.anthropic.messages.create({
1922 model: 'claude-sonnet-4-6',
1923 max_tokens: 4096,
1924 system: agent.systemPrompt,
1925 messages: [{
1926 role: 'user',
1927 content: `Previous context:\n${JSON.stringify(context, null, 2)}\n\nProvide your analysis and recommendations.`
1928 }]
1929 });
1930
1931 context = {
1932 ...context,
1933 [agent.name.toLowerCase().replace(' ', '_')]: response.content[0]
1934 };
1935 }
1936
1937 return context;
1938 }
1939}
1940```
1941
1942### Monitoring Dashboard Data
1943
1944```typescript
1945// api/optimization-stats.ts
1946interface OptimizationStats {
1947 period: string;
1948 totalOptimizations: number;
1949 actionBreakdown: {
1950 paused: number;
1951 scaled: number;
1952 bidAdjusted: number;
1953 creativeRotated: number;
1954 };
1955 performanceImpact: {
1956 ctrChange: number;
1957 cpaChange: number;
1958 roasChange: number;
1959 spendEfficiency: number;
1960 };
1961 budgetSaved: number;
1962 revenueIncreased: number;
1963}
1964
1965async function getOptimizationStats(
1966 startDate: Date,
1967 endDate: Date
1968): Promise<OptimizationStats> {
1969 // Query optimization logs and performance data
1970 // Calculate before/after metrics
1971 // Return aggregated stats
1972}
1973```
1974
1975---
1976
1977## Resources
1978
1979- [Reddit Ads API Docs](https://ads-api.reddit.com/docs/)
1980- [Reddit Developer Portal](https://www.reddit.com/prefs/apps/)
1981- [Reddit Ads Help Center](https://business.reddithelp.com/s/article/Reddit-Ads-API)
1982- [OAuth2 Documentation](https://www.reddit.com/dev/api/oauth/)