Build a Douyin Competitor Monitor Agent
Step-by-step tutorial: build an agent that tracks competitor Douyin accounts, detects new videos, and generates daily briefings for under $1/month.
TL;DR — A complete Python agent that monitors 10 competitor Douyin accounts daily: detects new videos, tracks engagement changes, and produces an LLM-generated briefing. Total cost: ~$0.90/month for data + ~$0.30/month for LLM analysis. Full code included, runs via cron or any scheduler.
You want to know what your competitors are doing on Douyin without checking 10 accounts manually every morning. Here is an agent that does it for $1.20/month.
What the agent does
Every day at 7 AM:
- Fetches the profile of each competitor (follower count, bio changes)
- Pulls their recent video list (last 20 videos)
- Compares with yesterday’s snapshot to detect new uploads
- For new videos, fetches detailed engagement metrics
- Sends everything to an LLM for analysis
- Outputs a structured briefing (Slack message, email, or file)
Prerequisites
- A SandBase API key (used for both Douyin data and LLM calls)
- Python 3.10+
openaipackage (pip install openai)
The complete agent
#!/usr/bin/env python3
"""Douyin competitor monitoring agent.
Tracks competitor accounts, detects new videos, analyzes trends.
Run daily via cron: 0 7 * * * python3 /path/to/monitor.py
"""
import json
import os
from datetime import datetime, timezone
from pathlib import Path
from openai import OpenAI
# --- Configuration ---
COMPETITORS = [
{"uid": "competitor_1_uid", "name": "Brand A"},
{"uid": "competitor_2_uid", "name": "Brand B"},
{"uid": "competitor_3_uid", "name": "Brand C"},
{"uid": "competitor_4_uid", "name": "Brand D"},
{"uid": "competitor_5_uid", "name": "Brand E"},
{"uid": "competitor_6_uid", "name": "Brand F"},
{"uid": "competitor_7_uid", "name": "Brand G"},
{"uid": "competitor_8_uid", "name": "Brand H"},
{"uid": "competitor_9_uid", "name": "Brand I"},
{"uid": "competitor_10_uid", "name": "Brand J"},
]
SNAPSHOT_DIR = Path("./snapshots")
SNAPSHOT_DIR.mkdir(exist_ok=True)
client = OpenAI(
base_url="https://api.sandbase.ai/v1",
api_key=os.environ["SANDBASE_API_KEY"],
)
# --- Data fetching ---
def fetch_profile(uid: str) -> dict:
"""Fetch a Douyin user's profile data."""
response = client.chat.completions.create(
model="douyin/app-v3/user-profile",
messages=[],
extra_body={"uid": uid},
)
return json.loads(response.choices[0].message.content)
def fetch_video_list(uid: str, count: int = 20) -> list:
"""Fetch a user's recent videos."""
response = client.chat.completions.create(
model="douyin/creator/item-list",
messages=[],
extra_body={"uid": uid, "count": count},
)
return json.loads(response.choices[0].message.content)
def fetch_video_detail(aweme_id: str) -> dict:
"""Fetch detailed metrics for a specific video."""
response = client.chat.completions.create(
model="douyin/app-v3/video-detail",
messages=[],
extra_body={"aweme_id": aweme_id},
)
return json.loads(response.choices[0].message.content)
# --- Snapshot comparison ---
def load_previous_snapshot(competitor_name: str) -> dict:
"""Load yesterday's snapshot for comparison."""
path = SNAPSHOT_DIR / f"{competitor_name}.json"
if path.exists():
return json.loads(path.read_text())
return {"videos": [], "follower_count": 0}
def save_snapshot(competitor_name: str, data: dict):
"""Save today's data as the new snapshot."""
path = SNAPSHOT_DIR / f"{competitor_name}.json"
path.write_text(json.dumps(data, ensure_ascii=False, indent=2))
def detect_new_videos(current_videos: list, previous_videos: list) -> list:
"""Find videos that exist now but didn't exist yesterday."""
previous_ids = {v.get("aweme_id") for v in previous_videos}
return [v for v in current_videos if v.get("aweme_id") not in previous_ids]
# --- Analysis ---
def generate_briefing(all_data: list[dict]) -> str:
"""Use LLM to generate a human-readable competitive briefing."""
prompt = f"""You are a competitive intelligence analyst for a Douyin-focused brand.
Analyze the following competitor activity data and produce a concise daily briefing.
Focus on:
1. Who posted new content and what topics/formats they used
2. Significant follower changes (>1% growth or decline)
3. Videos with unusually high engagement (vs their average)
4. Content strategy shifts or patterns across competitors
Data:
{json.dumps(all_data, ensure_ascii=False, indent=2)}
Output a structured briefing with:
- Executive summary (3 sentences max)
- New content highlights (by competitor)
- Engagement anomalies
- Recommended actions for our brand
"""
response = client.chat.completions.create(
model="anthropic/claude-sonnet-4",
messages=[{"role": "user", "content": prompt}],
max_tokens=2000,
)
return response.choices[0].message.content
# --- Main loop ---
def run_daily_monitor():
"""Execute the full monitoring cycle."""
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
print(f"[{today}] Starting competitor monitoring cycle...")
all_competitor_data = []
total_api_calls = 0
for competitor in COMPETITORS:
uid = competitor["uid"]
name = competitor["name"]
print(f" Checking {name}...")
# Fetch current state
profile = fetch_profile(uid)
videos = fetch_video_list(uid)
total_api_calls += 2
# Compare with previous
previous = load_previous_snapshot(name)
new_videos = detect_new_videos(videos, previous.get("videos", []))
# Fetch details for new videos
new_video_details = []
for video in new_videos[:5]: # Cap at 5 new videos per competitor
detail = fetch_video_detail(video["aweme_id"])
new_video_details.append(detail)
total_api_calls += 1
# Build competitor summary
competitor_data = {
"name": name,
"follower_count": profile.get("follower_count", 0),
"follower_change": profile.get("follower_count", 0) - previous.get("follower_count", 0),
"new_videos_count": len(new_videos),
"new_videos": new_video_details,
"total_videos_checked": len(videos),
}
all_competitor_data.append(competitor_data)
# Save today's snapshot
save_snapshot(name, {
"videos": videos,
"follower_count": profile.get("follower_count", 0),
"date": today,
})
# Generate LLM briefing
print(f" Generating briefing ({total_api_calls} data calls made)...")
briefing = generate_briefing(all_competitor_data)
total_api_calls += 1
# Output
output_path = SNAPSHOT_DIR / f"briefing-{today}.md"
output_path.write_text(f"# Competitor Briefing: {today}\n\n{briefing}")
print(f" Briefing saved to {output_path}")
print(f" Total API calls: {total_api_calls}")
print(f" Estimated cost: ${total_api_calls * 0.001 + 0.01:.4f}")
return briefing
if __name__ == "__main__":
run_daily_monitor()
How it works, step by step
Step 1: Profile check (10 calls, $0.01)
profile = fetch_profile(uid)
# Returns: {"follower_count": 890000, "nickname": "...", "signature": "..."}
The agent compares today’s follower count with yesterday’s snapshot. A 5% overnight drop or 10% spike gets flagged.
Step 2: Video list (10 calls, $0.01)
videos = fetch_video_list(uid, count=20)
# Returns: [{"aweme_id": "...", "desc": "...", "create_time": 1722412800, ...}, ...]
By comparing with yesterday’s list, the agent identifies new uploads. Most competitors post 0-3 videos/day, so the delta is small.
Step 3: New video details (0-15 calls, $0-$0.015)
Only fetched for videos that weren’t in yesterday’s snapshot:
detail = fetch_video_detail(aweme_id)
# Returns: {"statistics": {"play_count": 1230000, "digg_count": 45200, ...}, ...}
Capped at 5 new videos per competitor to control costs. If a competitor posts 10 videos in a day (unusual), you still catch the first 5.
Step 4: LLM analysis (1 call, ~$0.01)
The entire batch of competitor data goes to Claude Sonnet 4 in one prompt. Output is a structured briefing with actionable recommendations.
Cost breakdown
| Component | Calls/day | Unit cost | Daily cost |
|---|---|---|---|
| Profile checks | 10 | $0.001 | $0.010 |
| Video lists | 10 | $0.001 | $0.010 |
| New video details | ~10 avg | $0.001 | $0.010 |
| LLM briefing | 1 | ~$0.01 | $0.010 |
| Total | ~31 | $0.040/day |
Monthly: $1.20/month for daily monitoring of 10 competitor accounts.
Compare this to a junior analyst spending 30 minutes each morning doing the same check: at $25/hour, that is $375/month. The agent does it better (never misses a day, checks all 10 accounts completely) for 0.3% of the cost.
Running it
Option 1: Cron (simplest)
# Run every day at 7 AM
0 7 * * * SANDBASE_API_KEY=sk-... python3 /path/to/monitor.py >> /var/log/competitor-monitor.log 2>&1
Option 2: GitHub Actions (free for private repos)
name: Daily Competitor Monitor
on:
schedule:
- cron: '0 23 * * *' # 7 AM Beijing time = 23:00 UTC previous day
jobs:
monitor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install openai
- run: python monitor.py
env:
SANDBASE_API_KEY: ${{ secrets.SANDBASE_API_KEY }}
Option 3: Any agent framework
The run_daily_monitor() function can be wrapped as a tool in LangGraph, CrewAI, or any framework that supports periodic execution.
Extending the agent
Add alerting: Check if follower_change exceeds a threshold and send a Slack webhook immediately instead of waiting for the daily briefing.
Add Weibo cross-reference: When a competitor posts a viral Douyin video, check if the same content appears on their Weibo (using weibo/app/user-timeline). Cross-platform content strategy reveals their distribution playbook.
Add historical trending: Store snapshots for 30 days and ask the LLM to identify weekly trends: “Brand A is posting 40% more product demos this month compared to last.”
Limitations
- UIDs must be known: You need the Douyin user IDs of your competitors. These can be found from their public profile URLs.
- Rate considerations: 30 calls at once is fine. If you scale to 100 competitors (300 calls), space them out over a few minutes.
- Data lag: Video engagement metrics reflect the state at query time. A video posted 1 hour ago may show low engagement that is not yet representative.
- No private data: This monitors public content only. DMs, private videos, and restricted accounts are not accessible.
FAQ
Can I monitor more than 10 accounts?
Yes. The cost scales linearly: 20 accounts ≈ $2.40/month, 50 accounts ≈ $6/month. At 50+ accounts, consider batching calls with 2-second delays between competitors to avoid rate pressure.
What if a competitor deletes a video?
The snapshot comparison detects this too. A video present yesterday but absent today is a deletion. Add a check for deleted_videos = previous_ids - current_ids if you want to track removals.
Can I use GPT-4o instead of Claude Sonnet 4 for analysis?
Yes. Replace model="anthropic/claude-sonnet-4" with model="openai/gpt-4o". Both are available through the same SandBase endpoint. Cost difference is marginal for a single daily analysis call.
How do I get competitor UIDs?
From their public Douyin profile URL. The format is https://www.douyin.com/user/{uid}. You can also search by name using douyin/search/user-search and extract the UID from results.
For more Douyin API details, see 310 Douyin APIs on SandBase. For context on social data approaches, see our social media data API guide.


