Scenario
You have 15 active video creatives across campaigns and need to know which ones have the strongest messaging, emotional pull, and CTA effectiveness. This job pulls all active creatives, runs Video Analysis on each, then feeds the scores into Mave to produce a ranked comparison matrix with actionable recommendations on which to scale and which to kill.Architecture
Code
import os, requests, time, tempfile
META = os.environ["META_ACCESS_TOKEN"]
ACCT = os.environ["META_AD_ACCOUNT_ID"]
MV = os.environ["MAVERA_API_KEY"]
GRAPH = "https://graph.facebook.com/v24.0"
MB = "https://app.mavera.io/api/v1"
MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
# 1. Pull active ads with creative details
ads = requests.get(
f"{GRAPH}/{ACCT}/ads",
params={
"access_token": META,
"effective_status": '["ACTIVE"]',
"fields": "id,name,creative{id,name,video_id,title,body}",
"limit": 50,
},
).json().get("data", [])
video_ads = [a for a in ads if a.get("creative", {}).get("video_id")]
print(f"Active video ads: {len(video_ads)}")
# 2. Analyze each (reusing upload+analysis pattern)
scored = []
for ad in video_ads[:15]:
creative = ad["creative"]
vid = creative["video_id"]
video_info = requests.get(
f"{GRAPH}/{vid}",
params={"access_token": META, "fields": "source,length"},
).json()
if not video_info.get("source"):
continue
vid_resp = requests.get(video_info["source"], stream=True)
tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
for chunk in vid_resp.iter_content(8192):
tmp.write(chunk)
tmp.close()
with open(tmp.name, "rb") as f:
asset = requests.post(f"{MB}/assets",
headers={"Authorization": f"Bearer {MV}"},
files={"file": (f"{vid}.mp4", f, "video/mp4")},
).json()
os.unlink(tmp.name)
analysis = requests.post(f"{MB}/video-analyses", headers=MH,
json={"asset_id": asset["id"], "name": creative.get("name", vid)},
).json()
for _ in range(30):
time.sleep(10)
status = requests.get(f"{MB}/video-analyses/{analysis['id']}",
headers={"Authorization": f"Bearer {MV}"}).json()
if status.get("status") in ("completed", "failed"):
break
if status.get("status") == "completed":
scored.append({
"ad_name": ad.get("name", "Untitled"),
"creative_name": creative.get("name", ""),
"title": creative.get("title", ""),
"body": creative.get("body", "")[:100],
"duration": video_info.get("length"),
"scores": status.get("scores", {}),
})
time.sleep(1)
# 3. Build comparison table for Mave
table_rows = []
for i, s in enumerate(scored, 1):
sc = s["scores"]
table_rows.append(
f"{i}. \"{s['ad_name']}\" — emotional: {sc.get('emotional','?')}, "
f"cognitive: {sc.get('cognitive','?')}, behavioral: {sc.get('behavioral','?')}, "
f"duration: {s['duration']}s, copy: \"{s['body']}\""
)
table = "\n".join(table_rows)
# 4. Mave comparison
comparison = requests.post(f"{MB}/mave/chat", headers=MH, json={
"message": f"""Compare these {len(scored)} video ad creatives based on their Mavera analysis scores.
CREATIVE SCORES:
{table}
Produce:
1. Ranked matrix (best to worst) with rationale
2. Message clarity ranking
3. Emotional intensity ranking
4. CTA strength ranking
5. Which creatives to SCALE (top 3) and why
6. Which creatives to PAUSE or REWORK and what to fix
7. Patterns in top performers vs bottom performers
8. Specific recommendations for each creative"""
}).json()
print("=== Creative Comparison Matrix ===")
print(comparison.get("content", ""))
const META = process.env.META_ACCESS_TOKEN;
const ACCT = process.env.META_AD_ACCOUNT_ID;
const MV = process.env.MAVERA_API_KEY;
const GRAPH = "https://graph.facebook.com/v24.0";
const MB = "https://app.mavera.io/api/v1";
const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
// 1. Pull active video ads
const ads = await fetch(
`${GRAPH}/${ACCT}/ads?access_token=${META}&effective_status=["ACTIVE"]&fields=id,name,creative{id,name,video_id,title,body}&limit=50`
).then(r => r.json()).then(d => d.data || []);
const videoAds = ads.filter(a => a.creative?.video_id);
console.log(`Active video ads: ${videoAds.length}`);
// 2. Analyze each creative
const scored = [];
for (const ad of videoAds.slice(0, 15)) {
const { creative } = ad;
const vid = creative.video_id;
const videoInfo = await fetch(
`${GRAPH}/${vid}?access_token=${META}&fields=source,length`
).then(r => r.json());
if (!videoInfo.source) continue;
const vidBuffer = Buffer.from(await fetch(videoInfo.source).then(r => r.arrayBuffer()));
const form = new FormData();
form.append("file", new Blob([vidBuffer], { type: "video/mp4" }), `${vid}.mp4`);
const asset = await fetch(`${MB}/assets`, {
method: "POST", headers: { Authorization: `Bearer ${MV}` }, body: form,
}).then(r => r.json());
const analysis = await fetch(`${MB}/video-analyses`, {
method: "POST", headers: MH,
body: JSON.stringify({ asset_id: asset.id, name: creative.name || vid }),
}).then(r => r.json());
let status;
for (let i = 0; i < 30; i++) {
await new Promise(r => setTimeout(r, 10000));
status = await fetch(`${MB}/video-analyses/${analysis.id}`,
{ headers: { Authorization: `Bearer ${MV}` } }).then(r => r.json());
if (status.status === "completed" || status.status === "failed") break;
}
if (status?.status === "completed") {
scored.push({
ad_name: ad.name || "Untitled", creative_name: creative.name || "",
title: creative.title || "", body: (creative.body || "").slice(0, 100),
duration: videoInfo.length, scores: status.scores || {},
});
}
await new Promise(r => setTimeout(r, 1000));
}
// 3. Build comparison table
const table = scored.map((s, i) => {
const sc = s.scores;
return `${i+1}. "${s.ad_name}" — emotional: ${sc.emotional ?? "?"}, cognitive: ${sc.cognitive ?? "?"}, behavioral: ${sc.behavioral ?? "?"}, duration: ${s.duration}s, copy: "${s.body}"`;
}).join("\n");
// 4. Mave comparison
const comparison = await fetch(`${MB}/mave/chat`, {
method: "POST", headers: MH,
body: JSON.stringify({
message: `Compare these ${scored.length} video ad creatives.\n\nSCORES:\n${table}\n\nProduce: 1) Ranked matrix 2) Message clarity ranking 3) Emotional intensity 4) CTA strength 5) SCALE top 3 6) PAUSE/REWORK 7) Patterns 8) Per-creative recommendations`,
}),
}).then(r => r.json());
console.log("=== Creative Comparison Matrix ===");
console.log(comparison.content || "");
Example Output
=== Creative Comparison Matrix ===
## Overall Ranking
| Rank | Creative | Emotional | Cognitive | Behavioral | Verdict |
|------|----------|-----------|-----------|------------|---------|
| 1 | "Testimonial — Real Users" | 9.0 | 7.1 | 8.4 | SCALE |
| 2 | "Summer Sale Hero — 30s" | 8.2 | 6.5 | 7.8 | SCALE |
| 3 | "Product Demo — Features" | 5.8 | 8.9 | 7.2 | KEEP |
| 4 | "Brand Story — Our Mission" | 7.5 | 4.2 | 3.1 | REWORK |
| 5 | "UGC Compilation" | 6.1 | 3.8 | 4.5 | PAUSE |
## Key Patterns
- Top performers combine emotional hooks (>8.0) with strong CTAs (behavioral >7.5)
- "Brand Story" has emotional pull but no conversion path — add CTA overlay at 0:22
- UGC compilation lacks narrative structure; re-edit with clear problem→solution arc
## Recommendations
1. Scale "Testimonial" with 3 new audience segments
2. Test "Summer Sale" with shorter 15s cut — first 3s hook scores 9.1
3. Rework "Brand Story" — add product demo at midpoint and end-card CTA
Error Handling
Effective status filter syntax
Effective status filter syntax
The
effective_status param requires a JSON array as a string: '["ACTIVE"]'. Common mistake: passing ACTIVE without brackets.Creative nested fields
Creative nested fields
Use
creative{id,name,video_id} syntax for nested field expansion. Without braces, you get only the creative ID.Large creative libraries
Large creative libraries
For accounts with 100+ creatives, paginate with
after cursor from the response’s paging.cursors.after field.Meta Ads Integration
All Meta Ads jobs
Mave Agent
AI research agent reference