reddit-ads
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReddit Ads API Skill
Reddit Ads API 技能文档
Load with: base.md
Purpose: Automate Reddit advertising campaigns using the Reddit Ads API. Create, manage, and optimize campaigns, ad groups, and ads programmatically.
加载依赖:base.md
用途:使用Reddit Ads API自动化Reddit广告系列,以编程方式创建、管理和优化广告系列、广告组及广告。
API Overview
API 概述
┌─────────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────┐
│ REDDIT ADS API 层级结构 │
│ ───────────────────────────────────────────────────────────── │
│ │
│ 账户(Account) │
│ └── 广告系列(Campaign)(目标、预算、排期) │
│ └── 广告组(Ad Group)(定向、出价、投放位置) │
│ └── 广告(Ad)(创意素材、标题、行动号召) │
│ │
│ + 自定义受众(Custom Audiences)(客户列表、相似受众) │
│ + 转化API(Conversions API)(服务器端事件追踪) │
├─────────────────────────────────────────────────────────────────┤
│ 基础URL: https://ads-api.reddit.com/api/v2.0 │
│ 文档: https://ads-api.reddit.com/docs/ │
│ 请求限制: 每秒1次请求 │
│ 认证方式: OAuth 2.0 + Bearer token │
└─────────────────────────────────────────────────────────────────┘Authentication
认证流程
Step 1: Create Reddit Developer App
步骤1:创建Reddit开发者应用
- Go to https://www.reddit.com/prefs/apps/
- Click "Create App" or "Create Another App"
- Fill in:
- Name: Your app name
- Type: Select for server-side automation
script - Redirect URI: Your callback URL (e.g., )
https://yourapp.com/callback
- Note your Client ID (under app name) and Client Secret
- 访问https://www.reddit.com/prefs/apps/
- 点击“Create App”或“Create Another App”
- 填写信息:
- 名称(Name): 你的应用名称
- 类型(Type): 选择用于服务器端自动化
script - 重定向URI(Redirect URI): 你的回调URL(例如:)
https://yourapp.com/callback
- 记录你的Client ID(应用名称下方)和Client Secret
Step 2: Authorization Flow
步骤2:授权流程
javascript
// 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();
}javascript
// Node.js OAuth2 实现
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';
// 步骤1:生成授权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}`;
}
// 步骤2:用授权码交换令牌
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();
// 返回结果: { access_token, refresh_token, expires_in, scope }
}
// 步骤3:令牌过期时刷新
async function refreshAccessToken(refreshToken) {
const credentials = Buffer.from(
`${REDDIT_CLIENT_ID}:${REDDIT_CLIENT_SECRET}`
).toString('base64');
const response = await fetch('https://www.reddit.com/api/v1/access_token', {
method: 'POST',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'YourApp/1.0.0'
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken
})
});
return response.json();
}Python OAuth2 Flow
Python OAuth2 实现
python
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()python
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:
"""生成OAuth授权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:
"""用授权码交换访问令牌。"""
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:
"""刷新过期的访问令牌。"""
credentials = base64.b64encode(
f"{REDDIT_CLIENT_ID}:{REDDIT_CLIENT_SECRET}".encode()
).decode()
response = requests.post(
'https://www.reddit.com/api/v1/access_token',
headers={
'Authorization': f'Basic {credentials}',
'User-Agent': USER_AGENT
},
data={
'grant_type': 'refresh_token',
'refresh_token': refresh_token
}
)
return response.json()Required Scopes
必要权限范围
| Scope | Access Level |
|---|---|
| Read campaigns, ad groups, ads, reports |
| Create/update campaigns, ad groups, ads |
| Access account history |
| 权限范围 | 访问级别 |
|---|---|
| 读取广告系列、广告组、广告、报表 |
| 创建/更新广告系列、广告组、广告 |
| 访问账户历史记录 |
Reddit Ads Client
Reddit Ads 客户端
Node.js Client
Node.js 客户端
typescript
// 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;typescript
// 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 错误: ${JSON.stringify(error)}`);
}
return response.json();
}
// 账户相关
async getAccount() {
return this.request('GET', `/accounts/${this.accountId}`);
}
// 广告系列相关
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);
}
// 广告组相关
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);
}
// 广告相关
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);
}
// 报表相关
async getReport(reportRequest: ReportRequest) {
return this.request('POST', `/accounts/${this.accountId}/reports`, reportRequest);
}
// 自定义受众相关
async getCustomAudiences() {
return this.request('GET', `/accounts/${this.accountId}/custom_audiences`);
}
async createCustomAudience(audience: CustomAudienceCreate) {
return this.request('POST', `/accounts/${this.accountId}/custom_audiences`, audience);
}
}
export default RedditAdsClient;Python Client
Python 客户端
python
undefinedpython
undefinedlib/reddit_ads_client.py
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)
---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()
# 账户相关
def get_account(self) -> Dict:
return self._request('GET', f'/accounts/{self.account_id}')
# 广告系列相关
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)
# 广告组相关
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)
# 广告相关
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)
# 报表相关
def get_report(self, report_request: Dict) -> Dict:
return self._request('POST', f'/accounts/{self.account_id}/reports', json=report_request)
# 自定义受众相关
def get_custom_audiences(self) -> List[Dict]:
return self._request('GET', f'/accounts/{self.account_id}/custom_audiences')
def create_custom_audience(self, audience: Dict) -> Dict:
return self._request('POST', f'/accounts/{self.account_id}/custom_audiences', json=audience)
---API Endpoints Reference
API 端点参考
Account Endpoints
账户端点
| Method | Endpoint | Description |
|---|---|---|
| GET | | Get account details |
| GET | | Get funding information |
| 请求方法 | 端点 | 描述 |
|---|---|---|
| GET | | 获取账户详情 |
| GET | | 获取资金信息 |
Campaign Endpoints
广告系列端点
| Method | Endpoint | Description |
|---|---|---|
| GET | | List all campaigns |
| GET | | Get campaign by ID |
| POST | | Create campaign |
| PUT | | Update campaign |
| DELETE | | Delete campaign |
| 请求方法 | 端点 | 描述 |
|---|---|---|
| GET | | 列出所有广告系列 |
| GET | | 根据ID获取广告系列 |
| POST | | 创建广告系列 |
| PUT | | 更新广告系列 |
| DELETE | | 删除广告系列 |
Ad Group Endpoints
广告组端点
| Method | Endpoint | Description |
|---|---|---|
| GET | | List all ad groups |
| GET | | Get ad group by ID |
| POST | | Create ad group |
| PUT | | Update ad group |
| DELETE | | Delete ad group |
| 请求方法 | 端点 | 描述 |
|---|---|---|
| GET | | 列出所有广告组 |
| GET | | 根据ID获取广告组 |
| POST | | 创建广告组 |
| PUT | | 更新广告组 |
| DELETE | | 删除广告组 |
Ad Endpoints
广告端点
| Method | Endpoint | Description |
|---|---|---|
| GET | | List all ads |
| GET | | Get ad by ID |
| POST | | Create ad |
| PUT | | Update ad |
| DELETE | | Delete ad |
| 请求方法 | 端点 | 描述 |
|---|---|---|
| GET | | 列出所有广告 |
| GET | | 根据ID获取广告 |
| POST | | 创建广告 |
| PUT | | 更新广告 |
| DELETE | | 删除广告 |
Custom Audience Endpoints
自定义受众端点
| Method | Endpoint | Description |
|---|---|---|
| GET | | List custom audiences |
| POST | | Create custom audience |
| PUT | | Update audience |
| DELETE | | Delete audience |
| 请求方法 | 端点 | 描述 |
|---|---|---|
| GET | | 列出自定义受众 |
| POST | | 创建自定义受众 |
| PUT | | 更新受众 |
| DELETE | | 删除受众 |
Report Endpoints
报表端点
| Method | Endpoint | Description |
|---|---|---|
| POST | | Generate report |
| 请求方法 | 端点 | 描述 |
|---|---|---|
| POST | | 生成报表 |
Campaign Creation
广告系列创建
Campaign Objectives
广告系列目标
| Objective | Use Case |
|---|---|
| Build brand recognition and reach |
| Drive clicks to website/landing page |
| Track and optimize for conversions |
| Maximize video view engagement |
| Drive mobile app installations |
| Promote product catalog items |
| 目标 | 使用场景 |
|---|---|
| 提升品牌知名度与触达量 |
| 引导流量至网站/落地页 |
| 追踪并优化转化效果 |
| 最大化视频观看互动量 |
| 推动移动应用安装 |
| 推广商品目录中的产品 |
Budget Types
预算类型
| Type | Description |
|---|---|
| Average daily spend (may vary slightly) |
| Total spend over campaign duration |
| 类型 | 描述 |
|---|---|
| 日均花费(可能略有浮动) |
| 广告系列周期内的总花费 |
Campaign Create Example
广告系列创建示例
typescript
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
undefinedtypescript
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; // 金额单位为微单位(1美元 = 1,000,000微单位)
start_time: string; // ISO 8601 格式
end_time?: string; // ISO 8601 格式(可选)
}
// 创建每日预算50美元的流量型广告系列
const campaign: CampaignCreate = {
name: '2025年Q1流量广告系列',
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
undefinedPython example
Python 示例
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)
---campaign = {
'name': '2025年Q1流量广告系列',
'objective': 'TRAFFIC',
'is_enabled': True,
'budget_type': 'DAILY',
'budget_total_amount_micros': 50_000_000, # 50美元
'start_time': '2025-01-15T00:00:00Z',
'end_time': '2025-03-31T23:59:59Z'
}
result = client.create_campaign(campaign)
---Ad Group Creation
广告组创建
Bidding Strategies
出价策略
| Strategy | Description | Use Case |
|---|---|---|
| Maximize conversions within budget | Best for most campaigns |
| Set average CPC cap | Control cost per result |
| Set strict CPC/CPM bid | Maximum control |
| 策略 | 描述 | 使用场景 |
|---|---|---|
| 在预算内最大化转化量 | 适用于大多数广告系列 |
| 设置平均CPC上限 | 控制单次结果成本 |
| 设置严格的CPC/CPM出价 | 获得最大控制权 |
Targeting Options
定向选项
| Targeting Type | Description |
|---|---|
| Target specific subreddits |
| Target by interest categories |
| Target by keyword engagement |
| Target by device type |
| Target by geography |
| Target uploaded customer lists |
| 定向类型 | 描述 |
|---|---|
| 定向特定子版块 |
| 按兴趣类别定向 |
| 按关键词互动行为定向 |
| 按设备类型定向 |
| 按地理位置定向 |
| 定向上传的客户列表 |
Ad Group Create Example
广告组创建示例
typescript
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
undefinedtypescript
interface AdGroupCreate {
name: string;
campaign_id: string;
is_enabled: boolean;
bid_strategy: 'LOWEST_COST' | 'COST_CAP' | 'MANUAL';
bid_amount_micros?: number; // 适用于COST_CAP或MANUAL策略
goal_type: 'CLICKS' | 'IMPRESSIONS' | 'CONVERSIONS';
goal_value_micros?: number;
targeting: {
communities?: string[]; // 子版块名称,无需加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;
}
// 创建定向特定子版块的广告组
const adGroup: AdGroupCreate = {
name: '科技爱好者 - 子版块定向',
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
undefinedPython example
Python 示例
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_group = {
'name': '科技爱好者 - 子版块定向',
'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 ad with image/video |
| Text-only promoted post |
| Video ad |
| Multiple images/cards |
| Product catalog ad |
| 类型 | 描述 |
|---|---|
| 带图片/视频的链接广告 |
| 纯文字推广帖 |
| 视频广告 |
| 多图/卡片轮播广告 |
| 商品目录广告 |
Call-to-Action Options
行动号召选项
| CTA | Use Case |
|---|---|
| E-commerce |
| Lead generation |
| Information |
| App/content download |
| App install |
| Services |
| B2B/Services |
| Jobs/Finance |
| Travel/Services |
| Video content |
| Newsletters/SaaS |
| Promotions |
| Restaurants |
| 行动号召 | 使用场景 |
|---|---|
| 电商场景 |
| 线索收集 |
| 信息推广 |
| 应用/内容下载 |
| 应用安装 |
| 服务类推广 |
| B2B/服务类 |
| 招聘/金融场景 |
| 旅游/服务类 |
| 视频内容推广 |
| 新闻通讯/SaaS产品 |
| 促销活动 |
| 餐饮行业 |
Ad Create Example
广告创建示例
typescript
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
undefinedtypescript
interface AdCreate {
name: string;
ad_group_id: string;
is_enabled: boolean;
type: 'LINK' | 'TEXT' | 'VIDEO' | 'CAROUSEL';
headline: string; // 最多300字符
body?: string;
url: string;
display_url?: string;
call_to_action: string;
thumbnail_url?: string; // 适用于图片/视频广告
video_url?: string; // 适用于视频广告
}
// 创建链接广告
const ad: AdCreate = {
name: '产品发布广告 - v1',
ad_group_id: 'ad_group_456',
is_enabled: true,
type: 'LINK',
headline: '全新革命性产品重磅推出',
body: '了解我们的最新创新如何改变你的工作流程。加入10000+满意客户的行列。',
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
undefinedPython example
Python 示例
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)
---ad = {
'name': '产品发布广告 - v1',
'ad_group_id': 'ad_group_456',
'is_enabled': True,
'type': 'LINK',
'headline': '全新革命性产品重磅推出',
'body': '了解我们的最新创新如何改变你的工作流程。加入10000+满意客户的行列。',
'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
转化API
Event Types
事件类型
| Event Type | Description |
|---|---|
| Page view |
| Product/content view |
| Search action |
| Add to cart |
| Add to wishlist |
| Completed purchase |
| Lead submission |
| Account creation |
| Custom event |
| 事件类型 | 描述 |
|---|---|
| 页面浏览 |
| 商品/内容浏览 |
| 搜索行为 |
| 加入购物车 |
| 加入心愿单 |
| 完成购买 |
| 线索提交 |
| 账户创建 |
| 自定义事件 |
Conversion Event Structure
转化事件结构
typescript
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
}typescript
interface ConversionEvent {
event_at: number; // Unix时间戳(毫秒)
event_type: {
tracking_type: string;
custom_event_name?: string; // 适用于CUSTOM类型
};
user: {
email?: string; // SHA256哈希,小写
phone_number?: string; // SHA256哈希,E.164格式
external_id?: string;
ip_address?: string;
user_agent?: string;
aaid?: string; // Android广告ID
idfa?: string; // iOS IDFA
};
event_metadata?: {
item_count?: number;
value_decimal?: number;
currency?: string;
conversion_id: string; // 唯一事件ID
products?: Array<{
id: string;
name?: string;
category?: string;
}>;
};
click_id?: string; // 用于归因的Reddit点击ID
}Send Conversion Events
发送转化事件
typescript
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);python
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()typescript
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 // 测试时设为true
})
}
);
return response.json();
}
// 示例:追踪购买事件
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' // 来自rdt_cid参数
};
await sendConversionEvent(accessToken, 'pixel_123', purchaseEvent);python
import hashlib
import time
import requests
def hash_pii(value: str) -> str:
"""SHA256哈希处理隐私数据。"""
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:
"""向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...'
},
'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])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])
undefined- 事件需在过去7天内发生才会被处理
- 单次批量请求最多支持500个事件
- 尽可能包含以提升归因准确性
click_id - 测试时使用,不会影响实际广告系列
test_mode: true
Important Notes
自定义受众
—
受众类型
- Events must occur within last 7 days to be processed
- Maximum 500 events per batch request
- Include when available for better attribution
click_id - Use for testing without affecting campaigns
test_mode: true
| 类型 | 描述 |
|---|---|
| 上传哈希后的邮箱/手机号/移动广告ID |
| 基于像素的重定向受众 |
| 与源受众相似的受众 |
Custom Audiences
创建客户列表受众
Audience Types
—
| Type | Description |
|---|---|
| Upload hashed emails/phone/MAIDs |
| Pixel-based retargeting |
| Similar to source audience |
typescript
interface CustomAudienceCreate {
name: string;
type: 'CUSTOMER_LIST';
description?: string;
users: Array<{
email_sha256?: string;
maid_sha256?: string; // 移动广告ID
}>;
}
// 从客户邮箱创建受众
const audience: CustomAudienceCreate = {
name: '2024年Q4高价值客户',
type: 'CUSTOMER_LIST',
description: '客户终身价值>500美元的用户',
users: customerEmails.map(email => ({
email_sha256: hashPII(email)
}))
};
const result = await client.createCustomAudience(audience);Create Customer List Audience
最小受众规模
typescript
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);- 用于定向的受众需至少有1000个匹配用户
- 匹配率会以范围形式显示以保护隐私
Minimum Audience Size
报表功能
—
报表请求
- 1,000 matched users minimum to be usable for targeting
- Match rates displayed as ranges for privacy
typescript
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[];
};
}
// 获取广告系列效果报表
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']
});Reporting
可用指标
Report Request
—
typescript
interface ReportRequest {
start_date: string; // YYYY-MM-DD
end_date: string; // YYYY-MM-DD
level: 'ACCOUNT' | 'CAMPAIGN' | 'AD_GROUP' | 'AD';
metrics: string[];
dimensions?: string[];
filters?: {
campaign_ids?: string[];
ad_group_ids?: string[];
};
}
// Get campaign performance report
const report = await client.getReport({
start_date: '2025-01-01',
end_date: '2025-01-31',
level: 'CAMPAIGN',
metrics: [
'impressions',
'clicks',
'spend',
'ctr',
'cpc',
'conversions',
'conversion_rate',
'cpa'
],
dimensions: ['date']
});| 指标 | 描述 |
|---|---|
| 总曝光量 |
| 总点击量 |
| 总花费(账户货币单位) |
| 点击率 |
| 单次点击成本 |
| 千次曝光成本 |
| 总转化量 |
| 转化量/点击量 |
| 单次转化成本 |
| 视频播放量 |
| 视频完整播放量 |
Available Metrics
环境变量
| Metric | Description |
|---|---|
| Total impressions |
| Total clicks |
| Total spend (in account currency) |
| Click-through rate |
| Cost per click |
| Cost per 1,000 impressions |
| Total conversions |
| Conversions / Clicks |
| Cost per acquisition |
| Video view count |
| Videos watched to completion |
bash
undefinedEnvironment Variables
.env
bash
undefinedREDDIT_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
---.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
---┌─────────────────────────────────────────────────────────────────┐
│ 推荐结构 │
│ ───────────────────────────────────────────────────────────── │
│ │
│ 广告系列(按目标/产品线划分) │
│ ├── 广告组:子版块定向 - 科技领域 │
│ │ ├── 广告:标题A + 图片1 │
│ │ └── 广告:标题B + 图片1 │
│ ├── 广告组:子版块定向 - 商业领域 │
│ │ ├── 广告:标题A + 图片1 │
│ │ └── 广告:标题B + 图片1 │
│ └── 广告组:兴趣定向 - 创业者群体 │
│ ├── 广告:标题A + 图片2 │
│ └── 广告:标题B + 图片2 │
│ │
│ • 按定向类型拆分广告组 │
│ • 每个广告组测试2-3个广告变体 │
│ • 使用清晰的命名规则 │
└─────────────────────────────────────────────────────────────────┘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 │
└─────────────────────────────────────────────────────────────────┘广告系列: [目标] - [产品/品牌] - [时间范围]
示例: TRAFFIC - ProductX - Q1-2025
广告组: [定向类型] - [受众描述]
示例: 子版块 - 科技爱好者
广告: [标题类型] - [创意版本]
示例: 问题解决型 - 图片ANaming 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- 每秒1次请求限制
- 实现指数退避重试机制
- 尽可能批量操作
typescript
async function rateLimitedRequest<T>(
fn: () => Promise<T>,
retries = 3
): Promise<T> {
for (let i = 0; i < retries; i++) {
try {
await new Promise(resolve => setTimeout(resolve, 1000)); // 1秒延迟
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('超出最大重试次数');
}Rate Limiting
完整工作流示例
- 1 request per second limit
- Implement exponential backoff for retries
- Batch operations where possible
typescript
async function rateLimitedRequest<T>(
fn: () => Promise<T>,
retries = 3
): Promise<T> {
for (let i = 0; i < retries; i++) {
try {
await new Promise(resolve => setTimeout(resolve, 1000)); // 1 second delay
return await fn();
} catch (error: any) {
if (error.status === 429 && i < retries - 1) {
const delay = Math.pow(2, i) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
throw new Error('Max retries exceeded');
}typescript
// 完整广告系列创建工作流
async function createRedditAdCampaign(
client: RedditAdsClient,
config: {
campaignName: string;
dailyBudget: number;
targetSubreddits: string[];
headline: string;
body: string;
landingUrl: string;
imageUrl: string;
}
) {
// 1. 创建广告系列
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(`创建广告系列: ${campaign.id}`);
// 2. 创建带定向的广告组
const adGroup = await client.createAdGroup({
name: `${config.campaignName} - 子版块定向`,
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(`创建广告组: ${adGroup.id}`);
// 3. 创建广告
const ad = await client.createAd({
name: `${config.campaignName} - 广告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(`创建广告: ${ad.id}`);
return { campaign, adGroup, ad };
}
// 使用示例
const result = await createRedditAdCampaign(client, {
campaignName: '2025年1月产品发布',
dailyBudget: 50, // 每日50美元
targetSubreddits: ['technology', 'gadgets', 'programming'],
headline: '开发工具的未来已来',
body: '加入50000+开发者行列,使用我们的工具加速交付。',
landingUrl: 'https://yoursite.com?utm_source=reddit',
imageUrl: 'https://yoursite.com/ad-image.jpg'
});Complete Workflow Example
测试
—
测试清单
typescript
// 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'
});- OAuth流程执行成功
- 令牌过期前刷新功能正常
- 广告系列按预算创建成功
- 广告组定向设置正确应用
- 广告创意正常展示
- 转化事件被正确追踪(使用test_mode)
- 报表返回预期指标
- 请求限制被妥善处理
- 错误响应被正确处理
Testing
开发用Mock API
Test Checklist
—
- OAuth flow completes successfully
- Token refresh works before expiry
- Campaign creates with correct budget
- Ad group targeting is applied correctly
- Ad creative displays properly
- Conversion events tracked (use test_mode)
- Reports return expected metrics
- Rate limiting handled gracefully
- Error responses handled properly
typescript
// 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: '测试账户',
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
}));
})
];Mock API for Development
故障排除
typescript
// 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
}));
})
];| 错误 | 原因 | 解决方法 |
|---|---|---|
| 令牌无效/过期 | 刷新访问令牌 |
| 账户未被白名单授权 | 联系Reddit Ads支持团队 |
| 超出请求限制 | 实现退避机制,降低请求频率 |
| 请求参数无效 | 检查必填字段和数据类型 |
| 匹配用户不足1000 | 向受众添加更多用户 |
Troubleshooting
智能优化服务
—
架构概述
| Error | Cause | Fix |
|---|---|---|
| Invalid/expired token | Refresh access token |
| Account not whitelisted | Contact Reddit Ads support |
| Rate limit exceeded | Implement backoff, slow down |
| Invalid payload | Check required fields, data types |
| < 1,000 matched users | Add more users to audience |
┌─────────────────────────────────────────────────────────────────┐
│ 智能Reddit广告优化器 │
│ ───────────────────────────────────────────────────────────── │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 调度器 │───▶│ 分析器 │───▶│ 优化器 │ │
│ │ (Cron) │ │ (AI/大语言模型) │ │ (执行动作) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 获取报表 │ │ 制定策略 │ │ 执行变更 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ 循环周期:每4-6小时 │
│ 执行动作:暂停低效广告、扩量优质广告、调整出价、轮换创意 │
└─────────────────────────────────────────────────────────────────┘Agentic Optimization Service
后台服务(Node.js)
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 │
└─────────────────────────────────────────────────────────────────┘typescript
// 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;
// 阈值
minCTR: number; // 低于该点击率的广告将被暂停(例如:0.005 = 0.5%)
maxCPA: number; // 高于该转化成本的广告将被暂停
minImpressions: number; // 做出决策前的最小曝光量(例如:1000)
budgetScaleFactor: number; // 优质广告组的预算扩量系数(例如:1.5)
// 优化设置
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() {
const cronSchedule = `0 */${this.config.checkIntervalHours} * * *`;
this.cronJob = new CronJob(cronSchedule, async () => {
console.log(`[${new Date().toISOString()}] 开始优化周期...`);
await this.runOptimizationCycle();
});
this.cronJob.start();
console.log(`Reddit Ads优化器已启动。每${this.config.checkIntervalHours}小时运行一次。`);
}
stop() {
if (this.cronJob) {
this.cronJob.stop();
console.log('Reddit Ads优化器已停止。');
}
}
// 主优化周期
async runOptimizationCycle() {
try {
// 1. 获取性能数据
const performanceData = await this.fetchPerformanceData();
// 2. AI智能分析
const recommendations = await this.analyzeWithAgent(performanceData);
// 3. 执行优化动作
await this.executeOptimizations(recommendations);
// 4. 记录结果
await this.logOptimizationResults(recommendations);
} catch (error) {
console.error('优化周期失败:', error);
await this.sendAlert('优化周期失败', error);
}
}
// 获取过去24小时的性能数据
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驱动的分析与决策
private async analyzeWithAgent(data: PerformanceData[]): Promise<OptimizationRecommendation[]> {
const prompt = `你是一名Reddit广告优化专家。分析以下广告系列性能数据并给出具体的优化建议。Background Service (Node.js)
性能数据(过去24小时)
typescript
// 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.${JSON.stringify(data, null, 2)}
Performance Data (Last 24 Hours)
优化配置
${JSON.stringify(data, null, 2)}
- 目标: ${this.config.optimizationGoal}
- 最低点击率阈值: ${this.config.minCTR * 100}%
- 最高转化成本阈值: $${this.config.maxCPA}
- 决策所需最小曝光量: ${this.config.minImpressions}
- 优质广告组预算扩量系数: ${this.config.budgetScaleFactor}倍
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
分析每个广告/广告组并为每个条目推荐一个动作:
- PAUSE - 低效广告(低点击率、高转化成本、达到足够曝光量但无转化)
- SCALE - 优质广告(高点击率、低转化成本、良好的投资回报率)- 增加预算
- ADJUST_BID - 中等表现广告 - 建议调整出价
- KEEP - 数据不足或表现合格
- ROTATE_CREATIVE - 定向良好但创意疲劳(点击率持续下降)
返回JSON格式的建议数组:
[
{
"adId": "string",
"adGroupId": "string",
"action": "PAUSE|SCALE|ADJUST_BID|KEEP|ROTATE_CREATIVE",
"reason": "简要说明",
"newBidMicros": number (可选,仅ADJUST_BID动作需要),
"budgetMultiplier": number (可选,仅SCALE动作需要)
}
]
对于低效广告要果断暂停以保护预算,对于优质广告扩量要保守(仅针对明确的优质广告)。`;
const response = await this.anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 4096,
messages: [{ role: 'user', content: prompt }]
});
const content = response.content[0];
if (content.type !== 'text') throw new Error('响应类型不符合预期');
// 从响应中提取JSON
const jsonMatch = content.text.match(/\[[\s\S]*\]/);
if (!jsonMatch) throw new Error('响应中未找到JSON数据');
return JSON.parse(jsonMatch[0]);}
// 执行AI推荐的优化动作
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();
break;
暂停广告 ${rec.adId}: ${rec.reason} 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(`将广告组 ${rec.adGroupId} 的预算调整为 ${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(`调整广告组 ${rec.adGroupId} 的出价为 ${rec.newBidMicros / 1_000_000}美元: ${rec.reason}`);
}
break;
case 'ROTATE_CREATIVE':
// 标记需要轮换创意(实现自定义的创意轮换逻辑)
console.log(`广告 ${rec.adId} 需要轮换创意: ${rec.reason}`);
await this.flagForCreativeRefresh(rec.adId);
break;
case 'KEEP':
// 无需执行动作
break;
}
} catch (error) {
console.error(`执行 ${rec.action} 动作失败(广告ID: ${rec.adId}):`, error);
}
}}
private async flagForCreativeRefresh(adId: string) {
// 实现逻辑:添加到队列、通知团队或自动生成新创意
}
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('优化总结:', JSON.stringify(summary, null, 2));
// 存储到数据库用于历史分析
}
private async sendAlert(subject: string, error: any) {
// 实现逻辑:发送邮件/Slack通知
}
}
interface OptimizationRecommendation {
adId: string;
adGroupId: string;
action: 'PAUSE' | 'SCALE' | 'ADJUST_BID' | 'KEEP' | 'ROTATE_CREATIVE';
reason: string;
newBidMicros?: number;
budgetMultiplier?: number;
}
export default RedditAdsOptimizerService;
undefinedYour Task
后台服务(Python)
Analyze each ad/ad group and recommend ONE action per item:
- PAUSE - Poor performers (low CTR, high CPA, no conversions after sufficient impressions)
- SCALE - Winners (high CTR, low CPA, good ROAS) - increase budget
- ADJUST_BID - Moderate performers - suggest bid adjustment
- KEEP - Insufficient data or acceptable performance
- ROTATE_CREATIVE - Good targeting but ad fatigue (declining CTR over time)
Return a JSON array of recommendations:
[
{
"adId": "string",
"adGroupId": "string",
"action": "PAUSE|SCALE|ADJUST_BID|KEEP|ROTATE_CREATIVE",
"reason": "Brief explanation",
"newBidMicros": number (optional, for ADJUST_BID),
"budgetMultiplier": number (optional, for SCALE)
}
]
Be aggressive with pausing poor performers to protect budget. Be conservative with scaling (only clear winners).`;
const response = await this.anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 4096,
messages: [{ role: 'user', content: prompt }]
});
const content = response.content[0];
if (content.type !== 'text') throw new Error('Unexpected response type');
// Extract JSON from response
const jsonMatch = content.text.match(/\[[\s\S]*\]/);
if (!jsonMatch) throw new Error('No JSON found in response');
return JSON.parse(jsonMatch[0]);}
// Execute the AI recommendations
private async executeOptimizations(recommendations: OptimizationRecommendation[]) {
for (const rec of recommendations) {
try {
switch (rec.action) {
case 'PAUSE':
await this.client.updateAd(rec.adId, { is_enabled: false });
console.log();
break;
Paused ad ${rec.adId}: ${rec.reason} 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;
undefinedpython
undefinedBackground Service (Python)
services/reddit_ads_optimizer.py
python
undefinedimport 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):
"""启动后台优化服务。"""
self._running = True
# 调度优化任务
schedule.every(self.config.check_interval_hours).hours.do(
self.run_optimization_cycle
)
print(f"Reddit Ads优化器已启动。每{self.config.check_interval_hours}小时运行一次。")
# 启动时立即运行一次
self.run_optimization_cycle()
# 保持服务运行
while self._running:
schedule.run_pending()
time.sleep(60)
def stop(self):
"""停止优化服务。"""
self._running = False
print("Reddit Ads优化器已停止。")
def run_optimization_cycle(self):
"""主优化周期。"""
print(f"[{datetime.now().isoformat()}] 开始优化周期...")
try:
# 1. 获取性能数据
performance_data = self._fetch_performance_data()
# 2. AI智能分析
recommendations = self._analyze_with_agent(performance_data)
# 3. 执行优化动作
self._execute_optimizations(recommendations)
# 4. 记录结果
self._log_optimization_results(recommendations)
except Exception as e:
print(f"优化周期失败: {e}")
self._send_alert("优化周期失败", str(e))
def _fetch_performance_data(self) -> List[PerformanceData]:
"""获取过去24小时的性能数据。"""
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驱动的分析与决策。"""
prompt = f"""你是一名Reddit广告优化专家。分析以下广告系列性能数据并给出具体的优化建议。services/reddit_ads_optimizer.py
性能数据(过去24小时)
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.{json.dumps([vars(d) for d in data], indent=2)}
Performance Data (Last 24 Hours)
优化配置
{json.dumps([vars(d) for d in data], indent=2)}
- 目标: {self.config.optimization_goal}
- 最低点击率阈值: {self.config.min_ctr * 100}%
- 最高转化成本阈值: ${self.config.max_cpa}
- 决策所需最小曝光量: {self.config.min_impressions}
- 优质广告组预算扩量系数: {self.config.budget_scale_factor}倍
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
分析每个广告/广告组并为每个条目推荐一个动作:
- PAUSE - 低效广告(低点击率、高转化成本、达到足够曝光量但无转化)
- SCALE - 优质广告(高点击率、低转化成本、良好的投资回报率)- 增加预算
- ADJUST_BID - 中等表现广告 - 建议调整出价
- KEEP - 数据不足或表现合格
- ROTATE_CREATIVE - 定向良好但创意疲劳(点击率持续下降)
返回JSON格式的建议数组:
[
{{
"ad_id": "string",
"ad_group_id": "string",
"action": "PAUSE|SCALE|ADJUST_BID|KEEP|ROTATE_CREATIVE",
"reason": "简要说明",
"new_bid_micros": number (可选,仅ADJUST_BID动作需要),
"budget_multiplier": number (可选,仅SCALE动作需要)
}}
]
对于低效广告要果断暂停以保护预算,对于优质广告扩量要保守(仅针对明确的优质广告)。"""
response = self.anthropic.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
messages=[{"role": "user", "content": prompt}]
)
content = response.content[0].text
# 从响应中提取JSON
import re
json_match = re.search(r'\[[\s\S]*\]', content)
if not json_match:
raise ValueError("响应中未找到JSON数据")
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]):
"""执行AI推荐的优化动作。"""
for rec in recommendations:
try:
if rec.action == OptimizationAction.PAUSE:
self.client.update_ad(rec.ad_id, {'is_enabled': False})
print(f"暂停广告 {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"将广告组 {rec.ad_group_id} 的预算调整为 ${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"调整广告组 {rec.ad_group_id} 的出价: {rec.reason}")
elif rec.action == OptimizationAction.ROTATE_CREATIVE:
print(f"广告 {rec.ad_id} 需要轮换创意: {rec.reason}")
self._flag_for_creative_refresh(rec.ad_id)
except Exception as e:
print(f"执行 {rec.action} 动作失败(广告ID: {rec.ad_id}): {e}")
def _flag_for_creative_refresh(self, ad_id: str):
"""标记广告需要轮换创意。"""
# 实现逻辑:添加到队列、通知团队或自动生成新创意
pass
def _log_optimization_results(self, recommendations: List[OptimizationRecommendation]):
"""记录优化结果。"""
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"优化总结: {json.dumps(summary, indent=2)}")
def _send_alert(self, subject: str, error: str):
"""发送告警通知。"""
# 实现逻辑:发送邮件/Slack通知
passYour Task
作为后台服务运行的入口
Analyze each ad/ad group and recommend ONE action per item:
- PAUSE - Poor performers (low CTR, high CPA, no conversions after sufficient impressions)
- SCALE - Winners (high CTR, low CPA, good ROAS) - increase budget
- ADJUST_BID - Moderate performers - suggest bid adjustment
- KEEP - Insufficient data or acceptable performance
- ROTATE_CREATIVE - Good targeting but ad fatigue (declining CTR over time)
Return a JSON array of recommendations:
[
{{
"ad_id": "string",
"ad_group_id": "string",
"action": "PAUSE|SCALE|ADJUST_BID|KEEP|ROTATE_CREATIVE",
"reason": "Brief explanation",
"new_bid_micros": number (optional, for ADJUST_BID),
"budget_multiplier": number (optional, for SCALE)
}}
]
Be aggressive with pausing poor performers to protect budget. Be conservative with scaling (only clear winners)."""
response = self.anthropic.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
messages=[{"role": "user", "content": prompt}]
)
content = response.content[0].text
# Extract JSON from response
import re
json_match = re.search(r'\[[\s\S]*\]', content)
if not json_match:
raise ValueError("No JSON found in response")
recommendations_data = json.loads(json_match.group())
return [
OptimizationRecommendation(
ad_id=r['ad_id'],
ad_group_id=r['ad_group_id'],
action=OptimizationAction(r['action']),
reason=r['reason'],
new_bid_micros=r.get('new_bid_micros'),
budget_multiplier=r.get('budget_multiplier')
)
for r in recommendations_data
]
def _execute_optimizations(self, recommendations: List[OptimizationRecommendation]):
"""Execute the AI recommendations."""
for rec in recommendations:
try:
if rec.action == OptimizationAction.PAUSE:
self.client.update_ad(rec.ad_id, {'is_enabled': False})
print(f"Paused ad {rec.ad_id}: {rec.reason}")
elif rec.action == OptimizationAction.SCALE:
ad_group = self.client.get_ad_group(rec.ad_group_id)
current_budget = ad_group['budget_total_amount_micros']
multiplier = rec.budget_multiplier or self.config.budget_scale_factor
new_budget = int(current_budget * multiplier)
self.client.update_ad_group(rec.ad_group_id, {
'budget_total_amount_micros': new_budget
})
print(f"Scaled ad group {rec.ad_group_id} budget to ${new_budget / 1_000_000}: {rec.reason}")
elif rec.action == OptimizationAction.ADJUST_BID:
if rec.new_bid_micros:
self.client.update_ad_group(rec.ad_group_id, {
'bid_amount_micros': rec.new_bid_micros
})
print(f"Adjusted bid for {rec.ad_group_id}: {rec.reason}")
elif rec.action == OptimizationAction.ROTATE_CREATIVE:
print(f"Creative rotation needed for {rec.ad_id}: {rec.reason}")
self._flag_for_creative_refresh(rec.ad_id)
except Exception as e:
print(f"Failed to execute {rec.action} for {rec.ad_id}: {e}")
def _flag_for_creative_refresh(self, ad_id: str):
"""Flag ad for creative refresh."""
# Implement: Add to queue, notify team, or auto-generate new creative
pass
def _log_optimization_results(self, recommendations: List[OptimizationRecommendation]):
"""Log optimization results."""
summary = {
'timestamp': datetime.now().isoformat(),
'total_recommendations': len(recommendations),
'actions': {
'paused': len([r for r in recommendations if r.action == OptimizationAction.PAUSE]),
'scaled': len([r for r in recommendations if r.action == OptimizationAction.SCALE]),
'bid_adjusted': len([r for r in recommendations if r.action == OptimizationAction.ADJUST_BID]),
'creative_rotation': len([r for r in recommendations if r.action == OptimizationAction.ROTATE_CREATIVE]),
'kept': len([r for r in recommendations if r.action == OptimizationAction.KEEP]),
}
}
print(f"Optimization Summary: {json.dumps(summary, indent=2)}")
def _send_alert(self, subject: str, error: str):
"""Send alert notification."""
# Implement: Send email/Slack notification
passif 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()undefinedEntry point for running as background service
Docker部署
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()undefineddockerfile
undefinedDocker Deployment
Dockerfile
dockerfile
undefinedFROM 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"]
```yamlDockerfile
docker-compose.yml
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"]
```yamlversion: '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"
undefineddocker-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"
undefined┌─────────────────────────────────────────────────────────────────┐
│ 智能优化策略 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 基于表现的暂停策略 │
│ ───────────────────────────────────────────────────────── │
│ 若曝光量>1000且点击率<0.3% → 暂停 │
│ 若曝光量>500且转化量=0 → 暂停 │
│ 若转化成本>目标值的2倍 → 暂停 │
│ │
│ 2. 优质广告扩量策略 │
│ ───────────────────────────────────────────────────────── │
│ 若点击率>1%且转化成本<目标值且转化量>5 │
│ → 预算扩量1.5倍 │
│ 最高扩量至原预算的3倍以控制风险 │
│ │
│ 3. 出价优化策略 │
│ ───────────────────────────────────────────────────────── │
│ 若排名低但点击率良好 → 提高出价10-20% │
│ 若转化成本高但仍有转化 → 降低出价10-15% │
│ │
│ 4. 创意疲劳检测策略 │
│ ───────────────────────────────────────────────────────── │
│ 若点击率连续3天下降 → 轮换创意 │
│ 若曝光频率>3 → 轮换创意 │
│ │
│ 5. 预算重新分配策略 │
│ ───────────────────────────────────────────────────────── │
│ 将暂停广告的预算转移至优质广告 │
│ 保持每日总预算上限 │
└─────────────────────────────────────────────────────────────────┘Optimization Strategies
进阶:多Agent优化
┌─────────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────────┘typescript
// services/multi-agent-optimizer.ts
import Anthropic from '@anthropic-ai/sdk';
interface AgentRole {
name: string;
systemPrompt: string;
}
const AGENTS: AgentRole[] = [
{
name: '性能分析师',
systemPrompt: `你负责分析Reddit广告性能数据。识别:
- 优质广告(高点击率、低转化成本、良好投资回报率)
- 低效广告(低点击率、高转化成本、无转化)
- 趋势(提升、下降、稳定)
输出带置信度的结构化分析结果。`
},
{
name: '预算策略师',
systemPrompt: `你负责优化广告系列间的预算分配。
根据性能分析结果,推荐:
- 为优质广告增加预算(最多增加50%)
- 为低效广告减少预算
- 在广告组间重新分配预算
在保护总预算的同时最大化投资回报率。`
},
{
name: '创意总监',
systemPrompt: `你负责评估广告创意表现。
识别以下广告:
- 创意疲劳(互动率下降)
- 潜力高但执行不佳
- A/B测试获胜者
推荐创意更新和新变体。`
},
{
name: '风险经理',
systemPrompt: `你负责确保优化的安全性。
审核建议并标记:
- 过于激进的扩量
- 数据不足的决策
- 预算集中风险
- 合规问题
批准、修改或拒绝建议。`
}
];
class MultiAgentOptimizer {
private anthropic: Anthropic;
constructor() {
this.anthropic = new Anthropic();
}
async runAgentPipeline(performanceData: any) {
let context = { performanceData };
// 按顺序运行每个Agent,基于前一个的输出继续
for (const agent of AGENTS) {
const response = await this.anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 4096,
system: agent.systemPrompt,
messages: [{
role: 'user',
content: `之前的分析结果:\n${JSON.stringify(context, null, 2)}\n\n请给出你的分析和建议。`
}]
});
context = {
...context,
[agent.name.toLowerCase().replace(' ', '_')]: response.content[0]
};
}
return context;
}
}Advanced: Multi-Agent Optimization
监控仪表盘数据
typescript
// services/multi-agent-optimizer.ts
import Anthropic from '@anthropic-ai/sdk';
interface AgentRole {
name: string;
systemPrompt: string;
}
const AGENTS: AgentRole[] = [
{
name: 'Performance Analyst',
systemPrompt: `You analyze Reddit Ads performance data. Identify:
- Top performers (high CTR, low CPA, good ROAS)
- Poor performers (low CTR, high CPA, no conversions)
- Trends (improving, declining, stable)
Output structured analysis with confidence scores.`
},
{
name: 'Budget Strategist',
systemPrompt: `You optimize budget allocation across campaigns.
Given performance analysis, recommend:
- Budget increases for winners (max 50% increase)
- Budget decreases for losers
- Reallocation between ad groups
Protect total budget while maximizing ROI.`
},
{
name: 'Creative Director',
systemPrompt: `You evaluate ad creative performance.
Identify ads with:
- Creative fatigue (declining engagement)
- High potential but poor execution
- A/B test winners
Recommend creative refreshes and new variations.`
},
{
name: 'Risk Manager',
systemPrompt: `You ensure optimization safety.
Review recommendations and flag:
- Overly aggressive scaling
- Insufficient data for decisions
- Budget concentration risk
- Compliance concerns
Approve, modify, or reject recommendations.`
}
];
class MultiAgentOptimizer {
private anthropic: Anthropic;
constructor() {
this.anthropic = new Anthropic();
}
async runAgentPipeline(performanceData: any) {
let context = { performanceData };
// Run agents in sequence, each building on previous output
for (const agent of AGENTS) {
const response = await this.anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 4096,
system: agent.systemPrompt,
messages: [{
role: 'user',
content: `Previous context:\n${JSON.stringify(context, null, 2)}\n\nProvide your analysis and recommendations.`
}]
});
context = {
...context,
[agent.name.toLowerCase().replace(' ', '_')]: response.content[0]
};
}
return context;
}
}typescript
// 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> {
// 查询优化日志和性能数据
// 计算优化前后的指标变化
// 返回聚合统计数据
}Monitoring Dashboard Data
资源
typescript
// api/optimization-stats.ts
interface OptimizationStats {
period: string;
totalOptimizations: number;
actionBreakdown: {
paused: number;
scaled: number;
bidAdjusted: number;
creativeRotated: number;
};
performanceImpact: {
ctrChange: number;
cpaChange: number;
roasChange: number;
spendEfficiency: number;
};
budgetSaved: number;
revenueIncreased: number;
}
async function getOptimizationStats(
startDate: Date,
endDate: Date
): Promise<OptimizationStats> {
// Query optimization logs and performance data
// Calculate before/after metrics
// Return aggregated stats
}Resources
—