| name | reddit-ads |
| description | Reddit Ads API - campaigns, targeting, conversions, agentic optimization |
| disable-model-invocation | false |
Reddit Ads API Skill
Load with: base.md
Purpose: Automate Reddit advertising campaigns using the Reddit Ads API. Create, manage, and optimize campaigns, ad groups, and ads programmatically.
API Overview
┌─────────────────────────────────────────────────────────────────┐
│ REDDIT ADS API HIERARCHY │
│ ───────────────────────────────────────────────────────────── │
│ │
│ Account │
│ └── Campaign (objective, budget, schedule) │
│ └── Ad Group (targeting, bidding, placement) │
│ └── Ad (creative, headline, CTA) │
│ │
│ + Custom Audiences (customer lists, lookalikes) │
│ + Conversions API (track events server-side) │
├─────────────────────────────────────────────────────────────────┤
│ BASE URL: https://ads-api.reddit.com/api/v2.0 │
│ DOCS: https://ads-api.reddit.com/docs/ │
│ RATE LIMIT: 1 request per second │
│ AUTH: OAuth 2.0 with Bearer token │
└─────────────────────────────────────────────────────────────────┘
Authentication
Step 1: Create Reddit Developer App
- 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
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';
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}`;
}
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', {
: ,
: {
: ,
: ,
:
},
: ({
: ,
: authorizationCode,
:
})
});
response.();
}
() {
credentials = .(
).();
response = (, {
: ,
: {
: ,
: ,
:
},
: ({
: ,
: refreshToken
})
});
response.();
}
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
}
)
response.json()
() -> :
credentials = base64.b64encode(
.encode()
).decode()
response = requests.post(
,
headers={
: ,
: USER_AGENT
},
data={
: ,
: refresh_token
}
)
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
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',
:
},
: body ? .(body) :
});
(!response.) {
error = response.();
();
}
response.();
}
() {
.(, );
}
() {
.(, );
}
() {
.(, );
}
() {
.(, , campaign);
}
() {
.(, , updates);
}
() {
endpoint = campaignId
?
: ;
.(, endpoint);
}
() {
.(, );
}
() {
.(, , adGroup);
}
() {
.(, , updates);
}
() {
endpoint = adGroupId
?
: ;
.(, endpoint);
}
() {
.(, , ad);
}
() {
.(, , updates);
}
() {
.(, , reportRequest);
}
() {
.(, );
}
() {
.(, , audience);
}
}
;
Python Client
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()
response.json()
() -> :
._request(, )
() -> []:
._request(, )
() -> :
._request(, )
() -> :
._request(, , json=campaign)
() -> :
._request(, , json=updates)
() -> []:
endpoint = (
campaign_id
)
._request(, endpoint)
() -> :
._request(, , json=ad_group)
() -> :
._request(, , json=updates)
() -> []:
endpoint = (
ad_group_id
)
._request(, endpoint)
() -> :
._request(, , json=ad)
() -> :
._request(, , json=report_request)
() -> []:
._request(, )
() -> :
._request(, , 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;
start_time: string;
end_time?: string;
}
const campaign: CampaignCreate = {
name: 'Q1 2025 Traffic Campaign',
objective: 'TRAFFIC',
is_enabled: true,
budget_type: 'DAILY',
budget_total_amount_micros: 50_000_000,
start_time: '2025-01-15T00:00:00Z',
end_time: '2025-03-31T23:59:59Z'
};
const result = await client.createCampaign(campaign);
campaign = {
'name': 'Q1 2025 Traffic Campaign',
'objective': 'TRAFFIC',
'is_enabled': True,
'budget_type': 'DAILY',
'budget_total_amount_micros': 50_000_000,
'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;
goal_type: 'CLICKS' | 'IMPRESSIONS' | 'CONVERSIONS';
goal_value_micros?: number;
targeting: {
communities?: string[];
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;
}
const adGroup: AdGroupCreate = {
name: ,
: ,
: ,
: ,
: ,
: {
: [
,
,
,
,
],
: {
: [, , ]
},
: [, ]
},
:
};
result = client.(adGroup);
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;
body?: string;
url: string;
display_url?: string;
call_to_action: string;
thumbnail_url?: string;
video_url?: string;
}
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: ,
:
};
result = client.(ad);
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;
event_type: {
tracking_type: string;
custom_event_name?: string;
};
user: {
email?: string;
phone_number?: string;
external_id?: string;
ip_address?: string;
user_agent?: string;
aaid?: string;
idfa?: string;
};
event_metadata?: {
item_count?: number;
value_decimal?: number;
currency?: string;
conversion_id: string;
products?: Array<{
id: string;
name?: string;
category?: string;
}>;
};
click_id?: string;
}
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
})
}
);
return response.json();
}
: = {
: .(),
: {
:
},
: {
: (),
: ,
:
},
: {
: ,
: ,
: ,
: ,
: [
{ : , : , : },
{ : , : , : }
]
},
:
};
(accessToken, , 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()
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...'
},
: {
: ,
: ,
: ,
: ,
: [
{: , : , : },
{: , : , : }
]
},
:
}
result = send_conversion_event(access_token, , [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;
}>;
}
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;
end_date: string;
level: 'ACCOUNT' | 'CAMPAIGN' | 'AD_GROUP' | 'AD';
metrics: string[];
dimensions?: string[];
filters?: {
campaign_ids?: string[];
ad_group_ids?: string[];
};
}
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
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));
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
async function createRedditAdCampaign(
client: RedditAdsClient,
config: {
campaignName: string;
dailyBudget: number;
targetSubreddits: string[];
headline: string;
body: string;
landingUrl: string;
imageUrl: string;
}
) {
const campaign = await client.createCampaign({
name: config.campaignName,
objective: 'TRAFFIC',
is_enabled: false,
budget_type: 'DAILY',
budget_total_amount_micros: config.dailyBudget * 1_000_000,
start_time: new Date().toISOString()
});
console.log(`Created campaign: ${campaign.id}`);
const adGroup = await client.createAdGroup({
name: `${config.campaignName} - Subreddit Targeting`,
campaign_id: campaign.id,
: ,
: ,
: ,
: {
: config.,
: { : [] },
: [, ]
}
});
.();
ad = client.({
: ,
: adGroup.,
: ,
: ,
: config.,
: config.,
: config.,
: ,
: config.
});
.();
{ campaign, adGroup, ad };
}
result = (client, {
: ,
: ,
: [, , ],
: ,
: ,
: ,
:
});
Testing
Test Checklist
Mock API for Development
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)
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;
minCTR: number;
maxCPA: number;
minImpressions: number;
budgetScaleFactor: number;
optimizationGoal: 'CLICKS' | 'CONVERSIONS' | 'ROAS';
checkIntervalHours: number;
}
interface PerformanceData {
campaignId: string;
adGroupId: string;
adId: string;
impressions: ;
: ;
: ;
: ;
: ;
: ;
: ;
: ;
}
{
: ;
: ;
: ;
: | = ;
() {
. = config;
. = ({
: config.,
: config.
});
. = ();
}
() {
cronSchedule = ;
. = (cronSchedule, () => {
.();
.();
});
..();
.();
}
() {
(.) {
..();
.();
}
}
() {
{
performanceData = .();
recommendations = .(performanceData);
.(recommendations);
.(recommendations);
} (error) {
.(, error);
.(, error);
}
}
(): <[]> {
endDate = ();
startDate = (endDate.() - * * * );
report = ..({
: startDate.().()[],
: endDate.().()[],
: ,
: [
, , , ,
, , ,
]
});
report..( ({
: row.,
: row.,
: row.,
: row.,
: row.,
: row.,
: row. || ,
: row.,
: row.,
: row. || ,
: row. ? row. / row. :
}));
}
(: []): <[]> {
prompt = ;
response = ...({
: ,
: ,
: [{ : , : prompt }]
});
content = response.[];
(content. !== ) ();
jsonMatch = content..();
(!jsonMatch) ();
.(jsonMatch[]);
}
() {
( rec recommendations) {
{
(rec.) {
:
..(rec., { : });
.();
;
:
adGroup = ..(rec.);
currentBudget = adGroup.;
newBudget = .(currentBudget * (rec. || ..));
..(rec., {
: newBudget
});
.();
;
:
(rec.) {
..(rec., {
: rec.
});
.();
}
;
:
.();
.(rec.);
;
:
;
}
} (error) {
.(, error);
}
}
}
() {
}
() {
summary = {
: ().(),
: recommendations.,
: {
: recommendations.( r. === ).,
: recommendations.( r. === ).,
: recommendations.( r. === ).,
: recommendations.( r. === ).,
: recommendations.( r. === ).
}
};
.(, .(summary, , ));
}
() {
}
}
{
: ;
: ;
: | | | | ;
: ;
?: ;
?: ;
}
;
Background Service (Python)
import anthropic
import schedule
import time
import json
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
from dataclasses import dataclass
from enum import Enum
from lib.reddit_ads_client import RedditAdsClient, RedditAdsConfig
class OptimizationAction(Enum):
PAUSE = "PAUSE"
SCALE = "SCALE"
ADJUST_BID = "ADJUST_BID"
KEEP = "KEEP"
ROTATE_CREATIVE = "ROTATE_CREATIVE"
@dataclass
class OptimizationConfig:
account_id: str
access_token: str
refresh_token: str
min_ctr: float = 0.005
max_cpa: float = 50.0
min_impressions: int = 1000
budget_scale_factor: float = 1.5
optimization_goal: str = "CONVERSIONS"
check_interval_hours: int = 4
@dataclass
class PerformanceData:
campaign_id:
ad_group_id:
ad_id:
impressions:
clicks:
spend:
conversions:
ctr:
cpc:
cpa:
roas:
:
ad_id:
ad_group_id:
action: OptimizationAction
reason:
new_bid_micros: [] =
budget_multiplier: [] =
:
():
.config = config
.client = RedditAdsClient(RedditAdsConfig(
access_token=config.access_token,
account_id=config.account_id
))
.anthropic = anthropic.Anthropic()
._running =
():
._running =
schedule.every(.config.check_interval_hours).hours.do(
.run_optimization_cycle
)
()
.run_optimization_cycle()
._running:
schedule.run_pending()
time.sleep()
():
._running =
()
():
()
:
performance_data = ._fetch_performance_data()
recommendations = ._analyze_with_agent(performance_data)
._execute_optimizations(recommendations)
._log_optimization_results(recommendations)
Exception e:
()
._send_alert(, (e))
() -> [PerformanceData]:
end_date = datetime.now()
start_date = end_date - timedelta(days=)
report = .client.get_report({
: start_date.strftime(),
: end_date.strftime(),
: ,
: [
, , , ,
, , ,
]
})
[
PerformanceData(
campaign_id=row[],
ad_group_id=row[],
ad_id=row[],
impressions=row[],
clicks=row[],
spend=row[],
conversions=row.get(, ),
ctr=row[],
cpc=row[],
cpa=row.get(, ),
roas=row.get(, ) / row[] row[] >
)
row report.get(, [])
]
() -> [OptimizationRecommendation]:
prompt =
response = .anthropic.messages.create(
model=,
max_tokens=,
messages=[{: , : prompt}]
)
content = response.content[].text
re
json_match = re.search(, content)
json_match:
ValueError()
recommendations_data = json.loads(json_match.group())
[
OptimizationRecommendation(
ad_id=r[],
ad_group_id=r[],
action=OptimizationAction(r[]),
reason=r[],
new_bid_micros=r.get(),
budget_multiplier=r.get()
)
r recommendations_data
]
():
rec recommendations:
:
rec.action == OptimizationAction.PAUSE:
.client.update_ad(rec.ad_id, {: })
()
rec.action == OptimizationAction.SCALE:
ad_group = .client.get_ad_group(rec.ad_group_id)
current_budget = ad_group[]
multiplier = rec.budget_multiplier .config.budget_scale_factor
new_budget = (current_budget * multiplier)
.client.update_ad_group(rec.ad_group_id, {
: new_budget
})
()
rec.action == OptimizationAction.ADJUST_BID:
rec.new_bid_micros:
.client.update_ad_group(rec.ad_group_id, {
: rec.new_bid_micros
})
()
rec.action == OptimizationAction.ROTATE_CREATIVE:
()
._flag_for_creative_refresh(rec.ad_id)
Exception e:
()
():
():
summary = {
: datetime.now().isoformat(),
: (recommendations),
: {
: ([r r recommendations r.action == OptimizationAction.PAUSE]),
: ([r r recommendations r.action == OptimizationAction.SCALE]),
: ([r r recommendations r.action == OptimizationAction.ADJUST_BID]),
: ([r r recommendations r.action == OptimizationAction.ROTATE_CREATIVE]),
: ([r r recommendations r.action == OptimizationAction.KEEP]),
}
}
()
():
__name__ == :
os
config = OptimizationConfig(
account_id=os.environ[],
access_token=os.environ[],
refresh_token=os.environ[],
min_ctr=,
max_cpa=,
min_impressions=,
budget_scale_factor=,
optimization_goal=,
check_interval_hours=
)
optimizer = RedditAdsOptimizerService(config)
optimizer.start()
Docker Deployment
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "services/reddit_ads_optimizer.py"]
version: '3.8'
services:
reddit-ads-optimizer:
build: .
container_name: reddit-ads-optimizer
restart: unless-stopped
environment:
- REDDIT_ADS_CLIENT_ID=${REDDIT_ADS_CLIENT_ID}
- REDDIT_ADS_CLIENT_SECRET=${REDDIT_ADS_CLIENT_SECRET}
- REDDIT_ADS_ACCOUNT_ID=${REDDIT_ADS_ACCOUNT_ID}
- REDDIT_ADS_ACCESS_TOKEN=${REDDIT_ADS_ACCESS_TOKEN}
- REDDIT_ADS_REFRESH_TOKEN=${REDDIT_ADS_REFRESH_TOKEN}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
volumes:
- ./logs:/app/logs
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Optimization Strategies
┌─────────────────────────────────────────────────────────────────┐
│ AGENTIC OPTIMIZATION STRATEGIES │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. PERFORMANCE-BASED PAUSING │
│ ───────────────────────────────────────────────────────── │
│ IF impressions > 1000 AND ctr < 0.3% → PAUSE │
│ IF impressions > 500 AND conversions = 0 → PAUSE │
│ IF cpa > 2x target → PAUSE │
│ │
│ 2. WINNER SCALING │
│ ───────────────────────────────────────────────────────── │
│ IF ctr > 1% AND cpa < target AND conversions > 5 │
│ → SCALE budget by 1.5x │
│ Cap at 3x original budget to manage risk │
│ │
│ 3. BID OPTIMIZATION │
│ ───────────────────────────────────────────────────────── │
│ IF position low AND ctr good → INCREASE bid 10-20% │
│ IF cpa high but converting → DECREASE bid 10-15% │
│ │
│ 4. CREATIVE FATIGUE DETECTION │
│ ───────────────────────────────────────────────────────── │
│ IF ctr declining 3 consecutive days → ROTATE_CREATIVE │
│ IF frequency > 3 → ROTATE_CREATIVE │
│ │
│ 5. BUDGET REALLOCATION │
│ ───────────────────────────────────────────────────────── │
│ Move budget from paused ads to scaled winners │
│ Maintain total daily budget cap │
└─────────────────────────────────────────────────────────────────┘
Advanced: Multi-Agent Optimization
import Anthropic from '@anthropic-ai/sdk';
interface AgentRole {
name: string;
systemPrompt: string;
}
const AGENTS: AgentRole[] = [
{
name: 'Performance Analyst',
systemPrompt: `You analyze Reddit Ads performance data. Identify:
- Top performers (high CTR, low CPA, good ROAS)
- Poor performers (low CTR, high CPA, no conversions)
- Trends (improving, declining, stable)
Output structured analysis with confidence scores.`
},
{
name: 'Budget Strategist',
systemPrompt: `You optimize budget allocation across campaigns.
Given performance analysis, recommend:
- Budget increases for winners (max 50% increase)
- Budget decreases for losers
- Reallocation between ad groups
Protect total budget while maximizing ROI.`
},
{
name: 'Creative Director',
systemPrompt: `You evaluate ad creative performance.
Identify ads with:
- Creative fatigue (declining engagement)
- High potential but poor execution
- A/B test winners
Recommend creative refreshes and new variations.`
},
{
name: 'Risk Manager',
systemPrompt: `You ensure optimization safety.
Review recommendations and flag:
- Overly aggressive scaling
- Insufficient data for decisions
- Budget concentration risk
- Compliance concerns
Approve, modify, or reject recommendations.`
}
];
class MultiAgentOptimizer {
private anthropic: Anthropic;
constructor() {
. = ();
}
() {
context = { performanceData };
( agent ) {
response = ...({
: ,
: ,
: agent.,
: [{
: ,
:
}]
});
context = {
...context,
[agent..().(, )]: response.[]
};
}
context;
}
}
Monitoring Dashboard Data
interface OptimizationStats {
period: string;
totalOptimizations: number;
actionBreakdown: {
paused: number;
scaled: number;
bidAdjusted: number;
creativeRotated: number;
};
performanceImpact: {
ctrChange: number;
cpaChange: number;
roasChange: number;
spendEfficiency: number;
};
budgetSaved: number;
revenueIncreased: number;
}
async function getOptimizationStats(
startDate: Date,
endDate: Date
): Promise<OptimizationStats> {
}
Resources