TikHub API Helper
liangdabiao/tikhub_api_skill
Searches TikHub's social media API endpoints and calls them from the command line for TikTok, Douyin, Xiaohongshu, Instagram, YouTube, Reddit and more.
Reddit Ads API - campaigns, targeting, conversions, agentic optimization
$ npx skills add alinaqi/maggy --skill reddit-ads -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install alinaqi/maggy reddit-ads --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/alinaqi/maggy.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/reddit-ads .claude/skills/reddit-ads && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "reddit-ads" agent skill from https://github.com/alinaqi/maggy/tree/main/skills/reddit-ads into .claude/skills/reddit-ads/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "reddit-ads", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/alinaqi/maggy/tree/main/skills/reddit-adsType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add alinaqi/maggy --skill reddit-ads -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install alinaqi/maggy reddit-ads --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/alinaqi/maggy.git skills-src && mkdir -p .agents/skills && cp -r skills-src/skills/reddit-ads .agents/skills/reddit-ads && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "reddit-ads" agent skill from https://github.com/alinaqi/maggy/tree/main/skills/reddit-ads into .agents/skills/reddit-ads/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "reddit-ads", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add alinaqi/maggy --skill reddit-ads -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install alinaqi/maggy reddit-ads --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/alinaqi/maggy.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/skills/reddit-ads .cursor/skills/reddit-ads && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "reddit-ads" agent skill from https://github.com/alinaqi/maggy/tree/main/skills/reddit-ads into .cursor/skills/reddit-ads/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "reddit-ads", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/alinaqi/maggy.git --path skills/reddit-ads--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add alinaqi/maggy --skill reddit-ads -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install alinaqi/maggy reddit-ads --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/alinaqi/maggy.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/skills/reddit-ads .gemini/skills/reddit-ads && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "reddit-ads" agent skill from https://github.com/alinaqi/maggy/tree/main/skills/reddit-ads into .gemini/skills/reddit-ads/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "reddit-ads", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install alinaqi/maggy reddit-adsInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add alinaqi/maggy --skill reddit-ads -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/alinaqi/maggy.git skills-src && mkdir -p .github/skills && cp -r skills-src/skills/reddit-ads .github/skills/reddit-ads && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "reddit-ads" agent skill from https://github.com/alinaqi/maggy/tree/main/skills/reddit-ads into .github/skills/reddit-ads/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "reddit-ads", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add alinaqi/maggy --skill reddit-ads -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install alinaqi/maggy reddit-ads --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/alinaqi/maggy.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/skills/reddit-ads .opencode/skills/reddit-ads && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "reddit-ads" agent skill from https://github.com/alinaqi/maggy/tree/main/skills/reddit-ads into .opencode/skills/reddit-ads/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "reddit-ads", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
reddit-adsReddit Ads API - campaigns, targeting, conversions, agentic optimization
Reddit Ads is an agent skill from alinaqi/maggy. Reddit Ads API - campaigns, targeting, conversions, agentic optimization
Its SKILL.md is about 15k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Backend & APIs. It works with Reddit. The repository describes itself as: What started as an opinionated Claude Code setup kit is now an autonomous AI engineering command center. The licence is MIT.
2 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit 72a456e. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
No scripts in the folder and no shell commands in SKILL.md (its code samples are typescript, python, javascript, bash, dockerfile and yaml).
From the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
reddit.comads-api.reddit.comAlso links to:
business.reddithelp.comFrom URLs in SKILL.md, links to its own repository left out.
Names these keys or tokens, usually read from environment variables:
REDDIT_CLIENT_SECRETREDDIT_ADS_CLIENT_SECRETREDDIT_ADS_ACCESS_TOKENREDDIT_ADS_REFRESH_TOKENANTHROPIC_API_KEYFrom names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Reddit Ads loads about 15k tokens when it runs. Until then it costs about 21 tokens; SKILL.md has 775 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check noted patterns worth knowing about, such as sudo or a known installer.
# .envAutomated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from alinaqi/maggy at commit 72a456e, republished under its MIT licence (© alinaqi). 775 words, ~15,157 tokens.
.claude/skills/reddit-ads/SKILL.md (or your agent's skills folder).Purpose: Automate Reddit advertising campaigns using the Reddit Ads API. Create, manage, and optimize campaigns, ad groups, and ads programmatically.
┌─────────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────────┘script for server-side automationhttps://yourapp.com/callback)// 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();
}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()| Scope | Access Level |
|---|---|
adsread | Read campaigns, ad groups, ads, reports |
adsedit | Create/update campaigns, ad groups, ads |
history | Access account history |
// 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;# 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)| Method | Endpoint | Description |
|---|---|---|
| GET | /accounts/{account_id} | Get account details |
| GET | /accounts/{account_id}/funding | Get funding information |
| 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 |
| 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 |
| 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 |
| 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 |
| Method | Endpoint | Description |
|---|---|---|
| POST | /accounts/{account_id}/reports | Generate report |
| 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 |
| Type | Description |
|---|---|
DAILY | Average daily spend (may vary slightly) |
LIFETIME | Total spend over campaign duration |
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)| 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 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 |
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)| Type | Description |
|---|---|
LINK | Link ad with image/video |
TEXT | Text-only promoted post |
VIDEO | Video ad |
CAROUSEL | Multiple images/cards |
PRODUCT | Product catalog ad |
| 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 |
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)| 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 |
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
}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])click_id when available for better attributiontest_mode: true for testing without affecting campaigns| Type | Description |
|---|---|
CUSTOMER_LIST | Upload hashed emails/phone/MAIDs |
WEBSITE_VISITORS | Pixel-based retargeting |
LOOKALIKE | Similar to source 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);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']
});| 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 |
# .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┌─────────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────────┘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-Aasync 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');
}// 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'
});// 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
}));
})
];| 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 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 │
└─────────────────────────────────────────────────────────────────┘// services/reddit-ads-optimizer.ts
import Anthropic from '@anthropic-ai/sdk';
import { CronJob } from 'cron';
import RedditAdsClient from '../lib/reddit-ads-client';
interface OptimizationConfig {
accountId: string;
accessToken: string;
refreshToken: string;
// Thresholds
minCTR: number; // Pause ads below this CTR (e.g., 0.005 = 0.5%)
maxCPA: number; // Pause ads above this CPA
minImpressions: number; // Min impressions before decisions (e.g., 1000)
budgetScaleFactor: number; // Scale winning ad groups by this factor (e.g., 1.5)
// Optimization settings
optimizationGoal: 'CLICKS' | 'CONVERSIONS' | 'ROAS';
checkIntervalHours: number;
}
interface PerformanceData {
campaignId: string;
adGroupId: string;
adId: string;
impressions: number;
clicks: number;
spend: number;
conversions: number;
ctr: number;
cpc: number;
cpa: number;
roas: number;
}
class RedditAdsOptimizerService {
private client: RedditAdsClient;
private anthropic: Anthropic;
private config: OptimizationConfig;
private cronJob: CronJob | null = null;
constructor(config: OptimizationConfig) {
this.config = config;
this.client = new RedditAdsClient({
accessToken: config.accessToken,
accountId: config.accountId
});
this.anthropic = new Anthropic();
}
// Start the background optimization service
start() {
const cronSchedule = `0 */${this.config.checkIntervalHours} * * *`;
this.cronJob = new CronJob(cronSchedule, async () => {
console.log(`[${new Date().toISOString()}] Running optimization cycle...`);
await this.runOptimizationCycle();
});
this.cronJob.start();
console.log(`Reddit Ads Optimizer started. Running every ${this.config.checkIntervalHours} hours.`);
}
stop() {
if (this.cronJob) {
this.cronJob.stop();
console.log('Reddit Ads Optimizer stopped.');
}
}
// Main optimization cycle
async runOptimizationCycle() {
try {
// 1. Fetch performance data
const performanceData = await this.fetchPerformanceData();
// 2. Analyze with AI agent
const recommendations = await this.analyzeWithAgent(performanceData);
// 3. Execute optimizations
await this.executeOptimizations(recommendations);
// 4. Log results
await this.logOptimizationResults(recommendations);
} catch (error) {
console.error('Optimization cycle failed:', error);
await this.sendAlert('Optimization cycle failed', error);
}
}
// Fetch last 24h performance data
private async fetchPerformanceData(): Promise<PerformanceData[]> {
const endDate = new Date();
const startDate = new Date(endDate.getTime() - 24 * 60 * 60 * 1000);
const report = await this.client.getReport({
start_date: startDate.toISOString().split('T')[0],
end_date: endDate.toISOString().split('T')[0],
level: 'AD',
metrics: [
'impressions', 'clicks', 'spend', 'conversions',
'ctr', 'cpc', 'cpa', 'conversion_value'
]
});
return report.data.map((row: any) => ({
campaignId: row.campaign_id,
adGroupId: row.ad_group_id,
adId: row.ad_id,
impressions: row.impressions,
clicks: row.clicks,
spend: row.spend,
conversions: row.conversions || 0,
ctr: row.ctr,
cpc: row.cpc,
cpa: row.cpa || 0,
roas: row.conversion_value ? row.conversion_value / row.spend : 0
}));
}
// AI-powered analysis and decision making
private async analyzeWithAgent(data: PerformanceData[]): Promise<OptimizationRecommendation[]> {
const prompt = `You are a Reddit Ads optimization agent. Analyze the following campaign performance data and recommend specific actions.
## Performance Data (Last 24 Hours)
${JSON.stringify(data, null, 2)}
## Optimization Configuration
- Goal: ${this.config.optimizationGoal}
- Min CTR threshold: ${this.config.minCTR * 100}%
- Max CPA threshold: $${this.config.maxCPA}
- Min impressions for decisions: ${this.config.minImpressions}
- Budget scale factor for winners: ${this.config.budgetScaleFactor}x
## Your Task
Analyze each ad/ad group and recommend ONE action per item:
1. PAUSE - Poor performers (low CTR, high CPA, no conversions after sufficient impressions)
2. SCALE - Winners (high CTR, low CPA, good ROAS) - increase budget
3. ADJUST_BID - Moderate performers - suggest bid adjustment
4. KEEP - Insufficient data or acceptable performance
5. ROTATE_CREATIVE - Good targeting but ad fatigue (declining CTR over time)
Return a JSON array of recommendations:
[
{
"adId": "string",
"adGroupId": "string",
"action": "PAUSE|SCALE|ADJUST_BID|KEEP|ROTATE_CREATIVE",
"reason": "Brief explanation",
"newBidMicros": number (optional, for ADJUST_BID),
"budgetMultiplier": number (optional, for SCALE)
}
]
Be aggressive with pausing poor performers to protect budget. Be conservative with scaling (only clear winners).`;
const response = await this.anthropic.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 4096,
messages: [{ role: 'user', content: prompt }]
});
const content = response.content[0];
if (content.type !== 'text') throw new Error('Unexpected response type');
// Extract JSON from response
const jsonMatch = content.text.match(/\[[\s\S]*\]/);
if (!jsonMatch) throw new Error('No JSON found in response');
return JSON.parse(jsonMatch[0]);
}
// Execute the AI recommendations
private async executeOptimizations(recommendations: OptimizationRecommendation[]) {
for (const rec of recommendations) {
try {
switch (rec.action) {
case 'PAUSE':
await this.client.updateAd(rec.adId, { is_enabled: false });
console.log(`Paused ad ${rec.adId}: ${rec.reason}`);
break;
case 'SCALE':
const adGroup = await this.client.getAdGroup(rec.adGroupId);
const currentBudget = adGroup.budget_total_amount_micros;
const newBudget = Math.round(currentBudget * (rec.budgetMultiplier || this.config.budgetScaleFactor));
await this.client.updateAdGroup(rec.adGroupId, {
budget_total_amount_micros: newBudget
});
console.log(`Scaled ad group ${rec.adGroupId} budget to ${newBudget / 1_000_000}: ${rec.reason}`);
break;
case 'ADJUST_BID':
if (rec.newBidMicros) {
await this.client.updateAdGroup(rec.adGroupId, {
bid_amount_micros: rec.newBidMicros
});
console.log(`Adjusted bid for ${rec.adGroupId} to ${rec.newBidMicros / 1_000_000}: ${rec.reason}`);
}
break;
case 'ROTATE_CREATIVE':
// Flag for creative refresh (implement your creative rotation logic)
console.log(`Creative rotation needed for ${rec.adId}: ${rec.reason}`);
await this.flagForCreativeRefresh(rec.adId);
break;
case 'KEEP':
// No action needed
break;
}
} catch (error) {
console.error(`Failed to execute ${rec.action} for ${rec.adId}:`, error);
}
}
}
private async flagForCreativeRefresh(adId: string) {
// Implement: Add to queue, notify team, or auto-generate new creative
}
private async logOptimizationResults(recommendations: OptimizationRecommendation[]) {
const summary = {
timestamp: new Date().toISOString(),
totalRecommendations: recommendations.length,
actions: {
paused: recommendations.filter(r => r.action === 'PAUSE').length,
scaled: recommendations.filter(r => r.action === 'SCALE').length,
bidAdjusted: recommendations.filter(r => r.action === 'ADJUST_BID').length,
creativeRotation: recommendations.filter(r => r.action === 'ROTATE_CREATIVE').length,
kept: recommendations.filter(r => r.action === 'KEEP').length
}
};
console.log('Optimization Summary:', JSON.stringify(summary, null, 2));
// Store in database for historical analysis
}
private async sendAlert(subject: string, error: any) {
// Implement: Send email/Slack notification
}
}
interface OptimizationRecommendation {
adId: string;
adGroupId: string;
action: 'PAUSE' | 'SCALE' | 'ADJUST_BID' | 'KEEP' | 'ROTATE_CREATIVE';
reason: string;
newBidMicros?: number;
budgetMultiplier?: number;
}
export default RedditAdsOptimizerService;# services/reddit_ads_optimizer.py
import anthropic
import schedule
import time
import json
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
from dataclasses import dataclass
from enum import Enum
from lib.reddit_ads_client import RedditAdsClient, RedditAdsConfig
class OptimizationAction(Enum):
PAUSE = "PAUSE"
SCALE = "SCALE"
ADJUST_BID = "ADJUST_BID"
KEEP = "KEEP"
ROTATE_CREATIVE = "ROTATE_CREATIVE"
@dataclass
class OptimizationConfig:
account_id: str
access_token: str
refresh_token: str
min_ctr: float = 0.005 # 0.5%
max_cpa: float = 50.0
min_impressions: int = 1000
budget_scale_factor: float = 1.5
optimization_goal: str = "CONVERSIONS"
check_interval_hours: int = 4
@dataclass
class PerformanceData:
campaign_id: str
ad_group_id: str
ad_id: str
impressions: int
clicks: int
spend: float
conversions: int
ctr: float
cpc: float
cpa: float
roas: float
@dataclass
class OptimizationRecommendation:
ad_id: str
ad_group_id: str
action: OptimizationAction
reason: str
new_bid_micros: Optional[int] = None
budget_multiplier: Optional[float] = None
class RedditAdsOptimizerService:
def __init__(self, config: OptimizationConfig):
self.config = config
self.client = RedditAdsClient(RedditAdsConfig(
access_token=config.access_token,
account_id=config.account_id
))
self.anthropic = anthropic.Anthropic()
self._running = False
def start(self):
"""Start the background optimization service."""
self._running = True
# Schedule optimization runs
schedule.every(self.config.check_interval_hours).hours.do(
self.run_optimization_cycle
)
print(f"Reddit Ads Optimizer started. Running every {self.config.check_interval_hours} hours.")
# Run immediately on start
self.run_optimization_cycle()
# Keep running
while self._running:
schedule.run_pending()
time.sleep(60)
def stop(self):
"""Stop the optimization service."""
self._running = False
print("Reddit Ads Optimizer stopped.")
def run_optimization_cycle(self):
"""Main optimization cycle."""
print(f"[{datetime.now().isoformat()}] Running optimization cycle...")
try:
# 1. Fetch performance data
performance_data = self._fetch_performance_data()
# 2. Analyze with AI agent
recommendations = self._analyze_with_agent(performance_data)
# 3. Execute optimizations
self._execute_optimizations(recommendations)
# 4. Log results
self._log_optimization_results(recommendations)
except Exception as e:
print(f"Optimization cycle failed: {e}")
self._send_alert("Optimization cycle failed", str(e))
def _fetch_performance_data(self) -> List[PerformanceData]:
"""Fetch last 24h performance data."""
end_date = datetime.now()
start_date = end_date - timedelta(days=1)
report = self.client.get_report({
'start_date': start_date.strftime('%Y-%m-%d'),
'end_date': end_date.strftime('%Y-%m-%d'),
'level': 'AD',
'metrics': [
'impressions', 'clicks', 'spend', 'conversions',
'ctr', 'cpc', 'cpa', 'conversion_value'
]
})
return [
PerformanceData(
campaign_id=row['campaign_id'],
ad_group_id=row['ad_group_id'],
ad_id=row['ad_id'],
impressions=row['impressions'],
clicks=row['clicks'],
spend=row['spend'],
conversions=row.get('conversions', 0),
ctr=row['ctr'],
cpc=row['cpc'],
cpa=row.get('cpa', 0),
roas=row.get('conversion_value', 0) / row['spend'] if row['spend'] > 0 else 0
)
for row in report.get('data', [])
]
def _analyze_with_agent(self, data: List[PerformanceData]) -> List[OptimizationRecommendation]:
"""AI-powered analysis and decision making."""
prompt = f"""You are a Reddit Ads optimization agent. Analyze the following campaign performance data and recommend specific actions.
## Performance Data (Last 24 Hours)
{json.dumps([vars(d) for d in data], indent=2)}
## Optimization Configuration
- Goal: {self.config.optimization_goal}
- Min CTR threshold: {self.config.min_ctr * 100}%
- Max CPA threshold: ${self.config.max_cpa}
- Min impressions for decisions: {self.config.min_impressions}
- Budget scale factor for winners: {self.config.budget_scale_factor}x
## Your Task
Analyze each ad/ad group and recommend ONE action per item:
1. PAUSE - Poor performers (low CTR, high CPA, no conversions after sufficient impressions)
2. SCALE - Winners (high CTR, low CPA, good ROAS) - increase budget
3. ADJUST_BID - Moderate performers - suggest bid adjustment
4. KEEP - Insufficient data or acceptable performance
5. ROTATE_CREATIVE - Good targeting but ad fatigue (declining CTR over time)
Return a JSON array of recommendations:
[
{{
"ad_id": "string",
"ad_group_id": "string",
"action": "PAUSE|SCALE|ADJUST_BID|KEEP|ROTATE_CREATIVE",
"reason": "Brief explanation",
"new_bid_micros": number (optional, for ADJUST_BID),
"budget_multiplier": number (optional, for SCALE)
}}
]
Be aggressive with pausing poor performers to protect budget. Be conservative with scaling (only clear winners)."""
response = self.anthropic.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
messages=[{"role": "user", "content": prompt}]
)
content = response.content[0].text
# Extract JSON from response
import re
json_match = re.search(r'\[[\s\S]*\]', content)
if not json_match:
raise ValueError("No JSON found in response")
recommendations_data = json.loads(json_match.group())
return [
OptimizationRecommendation(
ad_id=r['ad_id'],
ad_group_id=r['ad_group_id'],
action=OptimizationAction(r['action']),
reason=r['reason'],
new_bid_micros=r.get('new_bid_micros'),
budget_multiplier=r.get('budget_multiplier')
)
for r in recommendations_data
]
def _execute_optimizations(self, recommendations: List[OptimizationRecommendation]):
"""Execute the AI recommendations."""
for rec in recommendations:
try:
if rec.action == OptimizationAction.PAUSE:
self.client.update_ad(rec.ad_id, {'is_enabled': False})
print(f"Paused ad {rec.ad_id}: {rec.reason}")
elif rec.action == OptimizationAction.SCALE:
ad_group = self.client.get_ad_group(rec.ad_group_id)
current_budget = ad_group['budget_total_amount_micros']
multiplier = rec.budget_multiplier or self.config.budget_scale_factor
new_budget = int(current_budget * multiplier)
self.client.update_ad_group(rec.ad_group_id, {
'budget_total_amount_micros': new_budget
})
print(f"Scaled ad group {rec.ad_group_id} budget to ${new_budget / 1_000_000}: {rec.reason}")
elif rec.action == OptimizationAction.ADJUST_BID:
if rec.new_bid_micros:
self.client.update_ad_group(rec.ad_group_id, {
'bid_amount_micros': rec.new_bid_micros
})
print(f"Adjusted bid for {rec.ad_group_id}: {rec.reason}")
elif rec.action == OptimizationAction.ROTATE_CREATIVE:
print(f"Creative rotation needed for {rec.ad_id}: {rec.reason}")
self._flag_for_creative_refresh(rec.ad_id)
except Exception as e:
print(f"Failed to execute {rec.action} for {rec.ad_id}: {e}")
def _flag_for_creative_refresh(self, ad_id: str):
"""Flag ad for creative refresh."""
# Implement: Add to queue, notify team, or auto-generate new creative
pass
def _log_optimization_results(self, recommendations: List[OptimizationRecommendation]):
"""Log optimization results."""
summary = {
'timestamp': datetime.now().isoformat(),
'total_recommendations': len(recommendations),
'actions': {
'paused': len([r for r in recommendations if r.action == OptimizationAction.PAUSE]),
'scaled': len([r for r in recommendations if r.action == OptimizationAction.SCALE]),
'bid_adjusted': len([r for r in recommendations if r.action == OptimizationAction.ADJUST_BID]),
'creative_rotation': len([r for r in recommendations if r.action == OptimizationAction.ROTATE_CREATIVE]),
'kept': len([r for r in recommendations if r.action == OptimizationAction.KEEP]),
}
}
print(f"Optimization Summary: {json.dumps(summary, indent=2)}")
def _send_alert(self, subject: str, error: str):
"""Send alert notification."""
# Implement: Send email/Slack notification
pass
# Entry point for running as background service
if __name__ == "__main__":
import os
config = OptimizationConfig(
account_id=os.environ['REDDIT_ADS_ACCOUNT_ID'],
access_token=os.environ['REDDIT_ADS_ACCESS_TOKEN'],
refresh_token=os.environ['REDDIT_ADS_REFRESH_TOKEN'],
min_ctr=0.005,
max_cpa=50.0,
min_impressions=1000,
budget_scale_factor=1.5,
optimization_goal="CONVERSIONS",
check_interval_hours=4
)
optimizer = RedditAdsOptimizerService(config)
optimizer.start()# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "services/reddit_ads_optimizer.py"]# docker-compose.yml
version: '3.8'
services:
reddit-ads-optimizer:
build: .
container_name: reddit-ads-optimizer
restart: unless-stopped
environment:
- REDDIT_ADS_CLIENT_ID=${REDDIT_ADS_CLIENT_ID}
- REDDIT_ADS_CLIENT_SECRET=${REDDIT_ADS_CLIENT_SECRET}
- REDDIT_ADS_ACCOUNT_ID=${REDDIT_ADS_ACCOUNT_ID}
- REDDIT_ADS_ACCESS_TOKEN=${REDDIT_ADS_ACCESS_TOKEN}
- REDDIT_ADS_REFRESH_TOKEN=${REDDIT_ADS_REFRESH_TOKEN}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
volumes:
- ./logs:/app/logs
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"┌─────────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────────┘// services/multi-agent-optimizer.ts
import Anthropic from '@anthropic-ai/sdk';
interface AgentRole {
name: string;
systemPrompt: string;
}
const AGENTS: AgentRole[] = [
{
name: 'Performance Analyst',
systemPrompt: `You analyze Reddit Ads performance data. Identify:
- Top performers (high CTR, low CPA, good ROAS)
- Poor performers (low CTR, high CPA, no conversions)
- Trends (improving, declining, stable)
Output structured analysis with confidence scores.`
},
{
name: 'Budget Strategist',
systemPrompt: `You optimize budget allocation across campaigns.
Given performance analysis, recommend:
- Budget increases for winners (max 50% increase)
- Budget decreases for losers
- Reallocation between ad groups
Protect total budget while maximizing ROI.`
},
{
name: 'Creative Director',
systemPrompt: `You evaluate ad creative performance.
Identify ads with:
- Creative fatigue (declining engagement)
- High potential but poor execution
- A/B test winners
Recommend creative refreshes and new variations.`
},
{
name: 'Risk Manager',
systemPrompt: `You ensure optimization safety.
Review recommendations and flag:
- Overly aggressive scaling
- Insufficient data for decisions
- Budget concentration risk
- Compliance concerns
Approve, modify, or reject recommendations.`
}
];
class MultiAgentOptimizer {
private anthropic: Anthropic;
constructor() {
this.anthropic = new Anthropic();
}
async runAgentPipeline(performanceData: any) {
let context = { performanceData };
// Run agents in sequence, each building on previous output
for (const agent of AGENTS) {
const response = await this.anthropic.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 4096,
system: agent.systemPrompt,
messages: [{
role: 'user',
content: `Previous context:\n${JSON.stringify(context, null, 2)}\n\nProvide your analysis and recommendations.`
}]
});
context = {
...context,
[agent.name.toLowerCase().replace(' ', '_')]: response.content[0]
};
}
return context;
}
}// api/optimization-stats.ts
interface OptimizationStats {
period: string;
totalOptimizations: number;
actionBreakdown: {
paused: number;
scaled: number;
bidAdjusted: number;
creativeRotated: number;
};
performanceImpact: {
ctrChange: number;
cpaChange: number;
roasChange: number;
spendEfficiency: number;
};
budgetSaved: number;
revenueIncreased: number;
}
async function getOptimizationStats(
startDate: Date,
endDate: Date
): Promise<OptimizationStats> {
// Query optimization logs and performance data
// Calculate before/after metrics
// Return aggregated stats
}© alinaqi, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in skills/reddit-ads of alinaqi/maggy.
Open the folder on GitHubat commit 72a456e
Reddit Ads next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Reddit Ads this skillalinaqi/maggy | 707 | — | ~15k | Automated safety check: Notes | MIT | |
| TikHub API Helperliangdabiao/tikhub_api_skill | 129 | — | ~1.5k | Automated safety check: Pass | None | |
| Scrapecreators APIScrapeCreators/social-media-research-skills | 3.4k | — | ~4k | Automated safety check: Notes | MIT | |
| reddapi.dev API Referencelignertys/reddit-research-skills | 14 | — | ~2.3k | Automated safety check: Pass | MIT | |
| Reddit Skillartwist-polyakov/polyakov-claude-skills | 208 | — | ~3.1k | Automated safety check: Notes | MIT | |
| Sentiment Analysisagiprolabs/claude-trading-skills | 410 | — | ~2.4k | Automated safety check: Pass | MIT |
liangdabiao/tikhub_api_skill
Searches TikHub's social media API endpoints and calls them from the command line for TikTok, Douyin, Xiaohongshu, Instagram, YouTube, Reddit and more.
ScrapeCreators/social-media-research-skills
Scrape and extract public data from 27+ social media platforms using the ScrapeCreators REST API.
lignertys/reddit-research-skills
Raw reference for the reddapi.dev API: authentication, search, trends and subreddit endpoints, request parameters, response schemas and error codes.
artwist-polyakov/polyakov-claude-skills
Reddit: чтение постов и комментариев без ключей через веб-инструмент агента, RSS и публичный JSON; API для пользователей, сабреддитов и поиска.
agiprolabs/claude-trading-skills
Market sentiment extraction from social media, news, and on-chain data including mention velocity, fear and greed indices, and influencer tracking
sundial-org/awesome-openclaw-skills
Browse, search, post, and moderate Reddit. An agent skill from sundial-org/awesome-openclaw-skills.
alinaqi/maggy
AI Engine Optimization - semantic triples, page templates, content clusters for AI citations
alinaqi/maggy
Claude Code Agent Teams - default team-based development with strict TDD pipeline enforcement
alinaqi/maggy
Latest AI models reference - Claude, OpenAI, Gemini, Eleven Labs, Replicate
alinaqi/maggy
Android Java development with MVVM, ViewBinding, and Espresso testing
alinaqi/maggy
Android Kotlin development with Coroutines, Jetpack Compose, Hilt, and MockK testing
alinaqi/maggy
AI-driven testing agent that auto-discovers, generates, executes, evaluates, and fixes tests for any project type
Works with
Categories
Reddit Ads API - campaigns, targeting, conversions, agentic optimization. Reddit Ads is an agent skill from alinaqi/maggy.
Reddit Ads fits situations like: backend & APIs work in your project.
Run `npx skills add alinaqi/maggy --skill reddit-ads -a claude-code`. Or copy the skill folder (skills/reddit-ads in alinaqi/maggy) into .claude/skills/reddit-ads in your project. Claude Code loads it when a task matches its description.
Run `npx skills add alinaqi/maggy --skill reddit-ads -a codex`. Or copy the skill folder (skills/reddit-ads in alinaqi/maggy) into .agents/skills/reddit-ads in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add alinaqi/maggy --skill reddit-ads -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/reddit-ads, .gemini/skills/reddit-ads, .github/skills/reddit-ads and .opencode/skills/reddit-ads in your project.
Going by SKILL.md and its folder, Reddit Ads needs credentials named REDDIT_CLIENT_SECRET, REDDIT_ADS_CLIENT_SECRET, REDDIT_ADS_ACCESS_TOKEN and REDDIT_ADS_REFRESH_TOKEN. Our summary lists: Python 3; Node.js; A credential in REDDIT_CLIENT_SECRET; A credential in REDDIT_ADS_CLIENT_SECRET.
SKILL.md names 3 domains. In commands or code: reddit.com and ads-api.reddit.com; the agent is likely to contact these when it follows the instructions. As links in the text: business.reddithelp.com. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found notes only (mentions a .env file), nothing it rates as a warning. It is not a guarantee. Review the folder before installing.
Reddit Ads is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 15k tokens (SKILL.md is roughly 61k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Reddit Ads: TikHub API Helper (liangdabiao/tikhub_api_skill, 129 stars), Scrapecreators API (ScrapeCreators/social-media-research-skills, 3.4k stars), reddapi.dev API Reference (lignertys/reddit-research-skills, 14 stars) and Reddit Skill (artwist-polyakov/polyakov-claude-skills, 208 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
alinaqi (a GitHub user) maintains it in alinaqi/maggy, which has 707 GitHub stars. The repository holds 71 skills in this directory. The repository was last updated on September 24, 2026.
Source: alinaqi/maggy on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.