reddit-ads

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Reddit 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开发者应用

  1. Go to https://www.reddit.com/prefs/apps/
  2. Click "Create App" or "Create Another App"
  3. Fill in:
    • Name: Your app name
    • Type: Select
      script
      for server-side automation
    • Redirect URI: Your callback URL (e.g.,
      https://yourapp.com/callback
      )
  4. Note your Client ID (under app name) and Client Secret
  1. 访问https://www.reddit.com/prefs/apps/
  2. 点击“Create App”或“Create Another App”
  3. 填写信息:
    • 名称(Name): 你的应用名称
    • 类型(Type): 选择
      script
      用于服务器端自动化
    • 重定向URI(Redirect URI): 你的回调URL(例如:
      https://yourapp.com/callback
  4. 记录你的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

必要权限范围

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

权限范围访问级别
adsread
读取广告系列、广告组、广告、报表
adsedit
创建/更新广告系列、广告组、广告
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
undefined
python
undefined

lib/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

账户端点

MethodEndpointDescription
GET
/accounts/{account_id}
Get account details
GET
/accounts/{account_id}/funding
Get funding information
请求方法端点描述
GET
/accounts/{account_id}
获取账户详情
GET
/accounts/{account_id}/funding
获取资金信息

Campaign Endpoints

广告系列端点

MethodEndpointDescription
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
请求方法端点描述
GET
/accounts/{account_id}/campaigns
列出所有广告系列
GET
/accounts/{account_id}/campaigns/{campaign_id}
根据ID获取广告系列
POST
/accounts/{account_id}/campaigns
创建广告系列
PUT
/accounts/{account_id}/campaigns/{campaign_id}
更新广告系列
DELETE
/accounts/{account_id}/campaigns/{campaign_id}
删除广告系列

Ad Group Endpoints

广告组端点

MethodEndpointDescription
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
请求方法端点描述
GET
/accounts/{account_id}/ad_groups
列出所有广告组
GET
/accounts/{account_id}/ad_groups/{ad_group_id}
根据ID获取广告组
POST
/accounts/{account_id}/ad_groups
创建广告组
PUT
/accounts/{account_id}/ad_groups/{ad_group_id}
更新广告组
DELETE
/accounts/{account_id}/ad_groups/{ad_group_id}
删除广告组

Ad Endpoints

广告端点

MethodEndpointDescription
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
请求方法端点描述
GET
/accounts/{account_id}/ads
列出所有广告
GET
/accounts/{account_id}/ads/{ad_id}
根据ID获取广告
POST
/accounts/{account_id}/ads
创建广告
PUT
/accounts/{account_id}/ads/{ad_id}
更新广告
DELETE
/accounts/{account_id}/ads/{ad_id}
删除广告

Custom Audience Endpoints

自定义受众端点

MethodEndpointDescription
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
请求方法端点描述
GET
/accounts/{account_id}/custom_audiences
列出自定义受众
POST
/accounts/{account_id}/custom_audiences
创建自定义受众
PUT
/accounts/{account_id}/custom_audiences/{audience_id}
更新受众
DELETE
/accounts/{account_id}/custom_audiences/{audience_id}
删除受众

Report Endpoints

报表端点

MethodEndpointDescription
POST
/accounts/{account_id}/reports
Generate report

请求方法端点描述
POST
/accounts/{account_id}/reports
生成报表

Campaign Creation

广告系列创建

Campaign Objectives

广告系列目标

ObjectiveUse 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
目标使用场景
BRAND_AWARENESS
提升品牌知名度与触达量
TRAFFIC
引导流量至网站/落地页
CONVERSIONS
追踪并优化转化效果
VIDEO_VIEWS
最大化视频观看互动量
APP_INSTALLS
推动移动应用安装
CATALOG_SALES
推广商品目录中的产品

Budget Types

预算类型

TypeDescription
DAILY
Average daily spend (may vary slightly)
LIFETIME
Total spend over campaign duration
类型描述
DAILY
日均花费(可能略有浮动)
LIFETIME
广告系列周期内的总花费

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
undefined
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; // 金额单位为微单位(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
undefined

Python 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

出价策略

StrategyDescriptionUse Case
LOWEST_COST
Maximize conversions within budgetBest for most campaigns
COST_CAP
Set average CPC capControl cost per result
MANUAL
Set strict CPC/CPM bidMaximum control
策略描述使用场景
LOWEST_COST
在预算内最大化转化量适用于大多数广告系列
COST_CAP
设置平均CPC上限控制单次结果成本
MANUAL
设置严格的CPC/CPM出价获得最大控制权

Targeting Options

定向选项

Targeting TypeDescription
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
定向类型描述
communities
定向特定子版块
interests
按兴趣类别定向
keywords
按关键词互动行为定向
devices
按设备类型定向
locations
按地理位置定向
custom_audiences
定向上传的客户列表

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
undefined
typescript
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
undefined

Python 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

广告类型

TypeDescription
LINK
Link ad with image/video
TEXT
Text-only promoted post
VIDEO
Video ad
CAROUSEL
Multiple images/cards
PRODUCT
Product catalog ad
类型描述
LINK
带图片/视频的链接广告
TEXT
纯文字推广帖
VIDEO
视频广告
CAROUSEL
多图/卡片轮播广告
PRODUCT
商品目录广告

Call-to-Action Options

行动号召选项

CTAUse 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
行动号召使用场景
SHOP_NOW
电商场景
SIGN_UP
线索收集
LEARN_MORE
信息推广
DOWNLOAD
应用/内容下载
INSTALL
应用安装
GET_QUOTE
服务类推广
CONTACT_US
B2B/服务类
APPLY_NOW
招聘/金融场景
BOOK_NOW
旅游/服务类
WATCH_NOW
视频内容推广
SUBSCRIBE
新闻通讯/SaaS产品
GET_OFFER
促销活动
SEE_MENU
餐饮行业

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
undefined
typescript
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
undefined

Python 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 TypeDescription
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
事件类型描述
PAGE_VISIT
页面浏览
VIEW_CONTENT
商品/内容浏览
SEARCH
搜索行为
ADD_TO_CART
加入购物车
ADD_TO_WISHLIST
加入心愿单
PURCHASE
完成购买
LEAD
线索提交
SIGN_UP
账户创建
CUSTOM
自定义事件

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
    click_id
    when available for better attribution
  • Use
    test_mode: true
    for testing without affecting campaigns

类型描述
CUSTOMER_LIST
上传哈希后的邮箱/手机号/移动广告ID
WEBSITE_VISITORS
基于像素的重定向受众
LOOKALIKE
与源受众相似的受众

Custom Audiences

创建客户列表受众

Audience Types

TypeDescription
CUSTOMER_LIST
Upload hashed emails/phone/MAIDs
WEBSITE_VISITORS
Pixel-based retargeting
LOOKALIKE
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']
});
指标描述
impressions
总曝光量
clicks
总点击量
spend
总花费(账户货币单位)
ctr
点击率
cpc
单次点击成本
cpm
千次曝光成本
conversions
总转化量
conversion_rate
转化量/点击量
cpa
单次转化成本
video_views
视频播放量
video_completions
视频完整播放量

Available Metrics

环境变量

MetricDescription
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

bash
undefined

Environment Variables

.env

bash
undefined
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

---

.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

广告组:  [定向类型] - [受众描述]
           示例: 子版块 - 科技爱好者

广告:        [标题类型] - [创意版本]
           示例: 问题解决型 - 图片A

Naming Conventions

请求限制处理

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

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

Ad:        [Headline Type] - [Creative Version]
           Example: Problem-Solution - Image-A
  • 每秒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
    }));
  })
];

错误原因解决方法
401 Unauthorized
令牌无效/过期刷新访问令牌
403 Forbidden
账户未被白名单授权联系Reddit Ads支持团队
429 Too Many Requests
超出请求限制实现退避机制,降低请求频率
400 Bad Request
请求参数无效检查必填字段和数据类型
Audience too small
匹配用户不足1000向受众添加更多用户


Troubleshooting

智能优化服务

架构概述

ErrorCauseFix
401 Unauthorized
Invalid/expired tokenRefresh access token
403 Forbidden
Account not whitelistedContact Reddit Ads support
429 Too Many Requests
Rate limit exceededImplement backoff, slow down
400 Bad Request
Invalid payloadCheck required fields, data types
Audience too small
< 1,000 matched usersAdd 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
分析每个广告/广告组并为每个条目推荐一个动作:
  1. PAUSE - 低效广告(低点击率、高转化成本、达到足够曝光量但无转化)
  2. SCALE - 优质广告(高点击率、低转化成本、良好的投资回报率)- 增加预算
  3. ADJUST_BID - 中等表现广告 - 建议调整出价
  4. KEEP - 数据不足或表现合格
  5. 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(
暂停广告 ${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(`将广告组 ${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;
undefined

Your Task

后台服务(Python)

Analyze each ad/ad group and recommend ONE action per item:
  1. PAUSE - Poor performers (low CTR, high CPA, no conversions after sufficient impressions)
  2. SCALE - Winners (high CTR, low CPA, good ROAS) - increase budget
  3. ADJUST_BID - Moderate performers - suggest bid adjustment
  4. KEEP - Insufficient data or acceptable performance
  5. ROTATE_CREATIVE - Good targeting but ad fatigue (declining CTR over time)
Return a JSON array of recommendations: [ { "adId": "string", "adGroupId": "string", "action": "PAUSE|SCALE|ADJUST_BID|KEEP|ROTATE_CREATIVE", "reason": "Brief explanation", "newBidMicros": number (optional, for ADJUST_BID), "budgetMultiplier": number (optional, for SCALE) } ]
Be aggressive with pausing poor performers to protect budget. Be conservative with scaling (only clear winners).`;
const response = await this.anthropic.messages.create({
  model: 'claude-sonnet-4-20250514',
  max_tokens: 4096,
  messages: [{ role: 'user', content: prompt }]
});

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

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

return JSON.parse(jsonMatch[0]);
}
// Execute the AI recommendations private async executeOptimizations(recommendations: OptimizationRecommendation[]) { for (const rec of recommendations) { try { switch (rec.action) { case 'PAUSE': await this.client.updateAd(rec.adId, { is_enabled: false }); console.log(
Paused ad ${rec.adId}: ${rec.reason}
); break;
      case 'SCALE':
        const adGroup = await this.client.getAdGroup(rec.adGroupId);
        const currentBudget = adGroup.budget_total_amount_micros;
        const newBudget = Math.round(currentBudget * (rec.budgetMultiplier || this.config.budgetScaleFactor));
        await this.client.updateAdGroup(rec.adGroupId, {
          budget_total_amount_micros: newBudget
        });
        console.log(`Scaled ad group ${rec.adGroupId} budget to ${newBudget / 1_000_000}: ${rec.reason}`);
        break;

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

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

      case 'KEEP':
        // No action needed
        break;
    }
  } catch (error) {
    console.error(`Failed to execute ${rec.action} for ${rec.adId}:`, error);
  }
}
}
private async flagForCreativeRefresh(adId: string) { // Implement: Add to queue, notify team, or auto-generate new creative }
private async logOptimizationResults(recommendations: OptimizationRecommendation[]) { const summary = { timestamp: new Date().toISOString(), totalRecommendations: recommendations.length, actions: { paused: recommendations.filter(r => r.action === 'PAUSE').length, scaled: recommendations.filter(r => r.action === 'SCALE').length, bidAdjusted: recommendations.filter(r => r.action === 'ADJUST_BID').length, creativeRotation: recommendations.filter(r => r.action === 'ROTATE_CREATIVE').length, kept: recommendations.filter(r => r.action === 'KEEP').length } }; console.log('Optimization Summary:', JSON.stringify(summary, null, 2)); // Store in database for historical analysis }
private async sendAlert(subject: string, error: any) { // Implement: Send email/Slack notification } }
interface OptimizationRecommendation { adId: string; adGroupId: string; action: 'PAUSE' | 'SCALE' | 'ADJUST_BID' | 'KEEP' | 'ROTATE_CREATIVE'; reason: string; newBidMicros?: number; budgetMultiplier?: number; }
export default RedditAdsOptimizerService;
undefined
python
undefined

Background Service (Python)

services/reddit_ads_optimizer.py

python
undefined
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):
    """启动后台优化服务。"""
    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
分析每个广告/广告组并为每个条目推荐一个动作:
  1. PAUSE - 低效广告(低点击率、高转化成本、达到足够曝光量但无转化)
  2. SCALE - 优质广告(高点击率、低转化成本、良好的投资回报率)- 增加预算
  3. ADJUST_BID - 中等表现广告 - 建议调整出价
  4. KEEP - 数据不足或表现合格
  5. 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通知
    pass

Your Task

作为后台服务运行的入口

Analyze each ad/ad group and recommend ONE action per item:
  1. PAUSE - Poor performers (low CTR, high CPA, no conversions after sufficient impressions)
  2. SCALE - Winners (high CTR, low CPA, good ROAS) - increase budget
  3. ADJUST_BID - Moderate performers - suggest bid adjustment
  4. KEEP - Insufficient data or acceptable performance
  5. ROTATE_CREATIVE - Good targeting but ad fatigue (declining CTR over time)
Return a JSON array of recommendations: [ {{ "ad_id": "string", "ad_group_id": "string", "action": "PAUSE|SCALE|ADJUST_BID|KEEP|ROTATE_CREATIVE", "reason": "Brief explanation", "new_bid_micros": number (optional, for ADJUST_BID), "budget_multiplier": number (optional, for SCALE) }} ]
Be aggressive with pausing poor performers to protect budget. Be conservative with scaling (only clear winners)."""
    response = self.anthropic.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=4096,
        messages=[{"role": "user", "content": prompt}]
    )

    content = response.content[0].text

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

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

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

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

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

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

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

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

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

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

def _send_alert(self, subject: str, error: str):
    """Send alert notification."""
    # Implement: Send email/Slack notification
    pass
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()
undefined

Entry 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()
undefined
dockerfile
undefined

Docker Deployment

Dockerfile

dockerfile
undefined
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"]

```yaml

Dockerfile

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"]

```yaml
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

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"
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