Mavera Surfaces
| Surface | Role |
|---|---|
Files (POST /files/upload-url, POST /files) | Upload competitor video ads |
Video Analysis (POST /video-analyses) | Frame-level scoring of each competitor creative |
Mave (POST /mave/chat) | Synthesize cross-competitor findings into a competitive intelligence report |
What Value Does Mavera Add?
| Value | How |
|---|---|
| Insurance | Know what competitors are doing before you finalize your own creative. Avoid launching something they already dominate. |
| Opening new doors | Frame-level analysis reveals tactics invisible from just watching: pacing patterns, CTA timing, emotional arcs. |
| Saving time | A human competitive review is subjective and takes days. This pipeline produces a structured report in minutes. |
When to Use This
- Quarterly competitive review: what creative strategies are competitors using?
- Pre-campaign: before building your next ad, see what’s already in market.
- Client pitch: show up with data on the competitive landscape.
- Creative team onboarding: give new hires a data-driven view of the competitive environment.
Where to find competitor ads: Meta Ad Library, TikTok Creative Center, YouTube channels, or screen-recording public content. This playbook assumes you already have the video files locally.
What You Need
| Requirement | Details |
|---|---|
| Mavera API key | Starts with mvra_live_. Get one at Developer Settings. |
| Workspace ID | From your dashboard URL (ws_...). |
| 3–8 competitor video ads | MP4 or MOV, 15–60 s each. At least 2 competitors for meaningful comparison. |
| Your own ad (optional) | Include one of yours for direct benchmarking. |
| Credits | ~100–250 per video + ~15–30 for Mave. See Credits Estimate. |
| Python 3.8+ or Node.js 18+ | requests for Python; native fetch for Node. |
MAVERA_API_KEY=mvra_live_your_key_here
MAVERA_WORKSPACE_ID=ws_your_workspace_id
The Flow
1
Collect competitor ads
Download 3–8 ads from public sources. Organize by competitor:
nike_hero_30s.mp4, adidas_brand_45s.mp4.2
Upload all ads to Mavera
Files API uploads with competitor name auto-extracted from filenames.
3
Run Video Analysis on each
Batch create + poll with a competitive-analysis-oriented goal prompt.
4
Build comparison matrix and ask Mave
Group by competitor, compute averages, feed into Mave: “What are our competitors doing better in their video creative?”
Stage 1 — Upload Competitor Ads
import os, re, time, json, glob, requests
from collections import defaultdict
API_KEY = os.environ["MAVERA_API_KEY"]
WORKSPACE_ID = os.environ["MAVERA_WORKSPACE_ID"]
BASE = "https://app.mavera.io/api/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
def upload_video(path: str) -> dict:
with open(path, "rb") as f:
content = f.read()
name = os.path.basename(path)
mime = "video/mp4" if path.lower().endswith(".mp4") else "video/quicktime"
url_resp = requests.post(f"{BASE}/files/upload-url", headers=HEADERS, json={
"file_name": name, "file_type": mime, "file_size": len(content), "workspace_id": WORKSPACE_ID,
}).json()
if "error" in url_resp:
raise Exception(url_resp["error"]["message"])
requests.put(url_resp["upload_url"], data=content, headers={"Content-Type": mime}).raise_for_status()
file_rec = requests.post(f"{BASE}/files", headers=HEADERS, json={
"name": name, "type": mime, "url": url_resp["public_url"],
"workspace_id": WORKSPACE_ID, "file_size": len(content),
}).json()
if "error" in file_rec:
raise Exception(file_rec["error"]["message"])
return {"id": file_rec["id"], "name": name}
def extract_competitor(filename: str) -> str:
"""Extract competitor name from filename prefix before first _ or -."""
base = os.path.splitext(filename)[0]
return re.split(r"[_\-]", base)[0]
def upload_competitor_ads(directory: str) -> list[dict]:
paths = sorted(glob.glob(os.path.join(directory, "*.mp4"))
+ glob.glob(os.path.join(directory, "*.mov")))
if not paths:
raise FileNotFoundError(f"No video files in {directory}")
assets = []
for p in paths:
asset = upload_video(p)
asset["competitor"] = extract_competitor(asset["name"])
print(f" {asset['name']} ({asset['competitor']}) → {asset['id']}")
assets.append(asset)
return assets
const fs = require("fs");
const path = require("path");
const API_KEY = process.env.MAVERA_API_KEY;
const WORKSPACE_ID = process.env.MAVERA_WORKSPACE_ID;
const BASE = "https://app.mavera.io/api/v1";
const HEADERS = { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" };
async function uploadVideo(videoPath) {
const content = fs.readFileSync(videoPath);
const name = path.basename(videoPath);
const mime = videoPath.toLowerCase().endsWith(".mp4") ? "video/mp4" : "video/quicktime";
const urlResp = await fetch(`${BASE}/files/upload-url`, {
method: "POST", headers: HEADERS,
body: JSON.stringify({ file_name: name, file_type: mime, file_size: content.length, workspace_id: WORKSPACE_ID }),
}).then((r) => r.json());
if (urlResp.error) throw new Error(urlResp.error.message);
await fetch(urlResp.upload_url, { method: "PUT", body: content, headers: { "Content-Type": mime } });
const fileRec = await fetch(`${BASE}/files`, {
method: "POST", headers: HEADERS,
body: JSON.stringify({ name, type: mime, url: urlResp.public_url, workspace_id: WORKSPACE_ID, file_size: content.length }),
}).then((r) => r.json());
if (fileRec.error) throw new Error(fileRec.error.message);
return { id: fileRec.id, name };
}
function extractCompetitor(filename) {
return path.basename(filename, path.extname(filename)).split(/[_-]/)[0] || "unknown";
}
async function uploadCompetitorAds(directory) {
const files = fs.readdirSync(directory).filter((f) => /\.(mp4|mov)$/i.test(f)).sort().map((f) => path.join(directory, f));
if (!files.length) throw new Error(`No video files in ${directory}`);
const assets = [];
for (const fp of files) {
const asset = await uploadVideo(fp);
asset.competitor = extractCompetitor(asset.name);
console.log(` ${asset.name} (${asset.competitor}) → ${asset.id}`);
assets.push(asset);
}
return assets;
}
Use consistent naming:
competitorname_adtype.mp4 (e.g., nike_hero_30s.mp4). The pipeline auto-extracts the competitor name from the prefix before the first _ or -.Stage 2 — Video Analysis (Batch)
def create_analysis(asset_id: str, label: str, competitor: str) -> dict:
resp = requests.post(f"{BASE}/video-analyses", headers=HEADERS, json={
"title": f"Competitor Reel: {competitor} — {label}", "asset_id": asset_id,
"goal": "Analyze competitor ad: hook effectiveness, emotional arc, pacing, CTA, brand integration",
"brand": competitor, "product": "Competitor product",
"primary_intent": "Understand competitor creative strategy",
"chunk_duration": 5, "frames_per_chunk": 3, "workspace_id": WORKSPACE_ID,
}).json()
if "error" in resp:
raise Exception(resp["error"]["message"])
return resp
def poll_analysis(analysis_id: str, timeout_min: int = 20) -> dict:
for _ in range(timeout_min * 4):
resp = requests.get(f"{BASE}/video-analyses/{analysis_id}", headers=HEADERS).json()
if "error" in resp:
raise Exception(resp["error"]["message"])
if resp["status"] == "COMPLETED":
return resp
if resp["status"] == "FAILED":
raise Exception(f"Analysis {analysis_id} failed")
time.sleep(15)
raise TimeoutError(f"Analysis {analysis_id} timed out")
def analyze_all_competitors(assets: list[dict]) -> list[dict]:
jobs = []
for asset in assets:
job = create_analysis(asset["id"], asset["name"], asset["competitor"])
print(f" Created analysis {job['id']} for {asset['competitor']}: {asset['name']}")
jobs.append({"analysis_id": job["id"], "name": asset["name"],
"competitor": asset["competitor"], "asset_id": asset["id"]})
results = []
for job in jobs:
result = poll_analysis(job["analysis_id"])
metrics = result.get("results", {}).get("full_video_metrics", {})
results.append({"name": job["name"], "competitor": job["competitor"],
"asset_id": job["asset_id"], "metrics": metrics,
"chunks": metrics.get("chunks", [])})
print(f" Completed {job['competitor']}: {job['name']} (overall={metrics.get('overall_score')})")
return results
async function createAnalysis(assetId, label, competitor) {
const resp = await fetch(`${BASE}/video-analyses`, {
method: "POST", headers: HEADERS,
body: JSON.stringify({
title: `Competitor Reel: ${competitor} — ${label}`, asset_id: assetId,
goal: "Analyze competitor ad: hook effectiveness, emotional arc, pacing, CTA, brand integration",
brand: competitor, product: "Competitor product",
primary_intent: "Understand competitor creative strategy",
chunk_duration: 5, frames_per_chunk: 3, workspace_id: WORKSPACE_ID,
}),
}).then((r) => r.json());
if (resp.error) throw new Error(resp.error.message);
return resp;
}
async function pollAnalysis(analysisId, timeoutMin = 20) {
for (let i = 0; i < timeoutMin * 4; i++) {
const resp = await fetch(`${BASE}/video-analyses/${analysisId}`, { headers: HEADERS }).then((r) => r.json());
if (resp.error) throw new Error(resp.error.message);
if (resp.status === "COMPLETED") return resp;
if (resp.status === "FAILED") throw new Error(`Analysis ${analysisId} failed`);
await new Promise((r) => setTimeout(r, 15000));
}
throw new Error(`Analysis ${analysisId} timed out`);
}
async function analyzeAllCompetitors(assets) {
const jobs = [];
for (const asset of assets) {
const job = await createAnalysis(asset.id, asset.name, asset.competitor);
console.log(` Created analysis ${job.id} for ${asset.competitor}: ${asset.name}`);
jobs.push({ analysisId: job.id, name: asset.name, competitor: asset.competitor, assetId: asset.id });
}
const results = [];
for (const job of jobs) {
const result = await pollAnalysis(job.analysisId);
const metrics = result.results?.full_video_metrics || {};
results.push({ name: job.name, competitor: job.competitor, assetId: job.assetId,
metrics, chunks: metrics.chunks || [] });
console.log(` Completed ${job.competitor}: ${job.name} (overall=${metrics.overall_score})`);
}
return results;
}
Stage 3 — Build the Comparison Matrix
Group by competitor. For competitors with multiple ads, compute averages.def build_comparison_matrix(results: list[dict]) -> dict:
grouped = defaultdict(list)
for r in results:
grouped[r["competitor"]].append(r)
matrix = {}
for comp, ads in grouped.items():
all_m = [a["metrics"] for a in ads]
avg = lambda key: round(sum(m.get(key, 0) for m in all_m) / len(all_m), 1)
matrix[comp] = {
"ad_count": len(ads), "ads": [a["name"] for a in ads],
"avg_overall": avg("overall_score"), "avg_emotion": avg("emotional_impact"),
"avg_attention": avg("attention_score"), "avg_cta": avg("cta_effectiveness"),
"best_ad": max(ads, key=lambda a: a["metrics"].get("overall_score", 0))["name"],
}
return matrix
def format_competitor_table(matrix: dict) -> str:
lines = ["| Competitor | Ads | Avg Overall | Avg Emotion | Avg Attention | Avg CTA | Best Ad |",
"|------------|-----|-------------|-------------|---------------|---------|---------|"]
for comp, d in sorted(matrix.items(), key=lambda x: x[1]["avg_overall"], reverse=True):
lines.append(f"| {comp} | {d['ad_count']} | {d['avg_overall']}/100 | {d['avg_emotion']}/10 "
f"| {d['avg_attention']}/10 | {d['avg_cta']}/10 | {d['best_ad']} |")
return "\n".join(lines)
def format_individual_table(results: list[dict]) -> str:
ranked = sorted(results, key=lambda r: r["metrics"].get("overall_score", 0), reverse=True)
lines = ["| Competitor | Ad | Overall | Emotion | Attention | Brand Recall | CTA |",
"|------------|----|---------|---------|-----------|--------------|----|"]
for r in ranked:
m = r["metrics"]
lines.append(f"| {r['competitor']} | {r['name']} | {m.get('overall_score', '—')}/100 "
f"| {m.get('emotional_impact', '—')}/10 | {m.get('attention_score', '—')}/10 "
f"| {m.get('brand_recall_likelihood', '—')} | {m.get('cta_effectiveness', '—')}/10 |")
return "\n".join(lines)
def format_hook_comparison(results: list[dict]) -> str:
lines = ["| Competitor | Ad | Hook Engagement | Hook Emotion | Hook Attention |",
"|------------|----|-----------------|--------------:|----------------|"]
for r in results:
c = r["chunks"][0] if r["chunks"] else {}
lines.append(f"| {r['competitor']} | {r['name']} | {c.get('engagement', '—')}/100 "
f"| {c.get('emotional_intensity', '—')}/10 | {c.get('attention', '—')}/10 |")
return "\n".join(lines)
function buildComparisonMatrix(results) {
const grouped = {};
for (const r of results) { (grouped[r.competitor] ||= []).push(r); }
const matrix = {};
for (const [comp, ads] of Object.entries(grouped)) {
const metrics = ads.map((a) => a.metrics);
const avg = (k) => +(metrics.reduce((s, m) => s + (m[k] || 0), 0) / metrics.length).toFixed(1);
matrix[comp] = {
adCount: ads.length, ads: ads.map((a) => a.name),
avgOverall: avg("overall_score"), avgEmotion: avg("emotional_impact"),
avgAttention: avg("attention_score"), avgCta: avg("cta_effectiveness"),
bestAd: ads.reduce((a, b) => (b.metrics.overall_score || 0) > (a.metrics.overall_score || 0) ? b : a).name,
};
}
return matrix;
}
function formatCompetitorTable(matrix) {
const lines = ["| Competitor | Ads | Avg Overall | Avg Emotion | Avg Attention | Avg CTA | Best Ad |",
"|------------|-----|-------------|-------------|---------------|---------|---------|"];
for (const [comp, d] of Object.entries(matrix).sort((a, b) => b[1].avgOverall - a[1].avgOverall))
lines.push(`| ${comp} | ${d.adCount} | ${d.avgOverall}/100 | ${d.avgEmotion}/10 | ${d.avgAttention}/10 | ${d.avgCta}/10 | ${d.bestAd} |`);
return lines.join("\n");
}
function formatIndividualTable(results) {
const ranked = [...results].sort((a, b) => (b.metrics.overall_score || 0) - (a.metrics.overall_score || 0));
const lines = ["| Competitor | Ad | Overall | Emotion | CTA |", "|------------|----|---------|---------|----|"];
for (const r of ranked) { const m = r.metrics;
lines.push(`| ${r.competitor} | ${r.name} | ${m.overall_score ?? "—"}/100 | ${m.emotional_impact ?? "—"}/10 | ${m.cta_effectiveness ?? "—"}/10 |`);
}
return lines.join("\n");
}
Stage 4 — Mave Competitive Intelligence
def generate_competitive_report(results: list[dict], matrix: dict) -> str:
comp_table = format_competitor_table(matrix)
ind_table = format_individual_table(results)
hook_table = format_hook_comparison(results)
competitors = list(matrix.keys())
prompt = f"""You are a senior creative strategist conducting competitive creative intelligence.
## Competitor Summary (Averages)
{comp_table}
## Individual Ad Scores
{ind_table}
## Hook Comparison (First 5 Seconds)
{hook_table}
## Competitors: {', '.join(competitors)}
## Your Task
Produce a competitive creative intelligence report:
1. **Executive Summary** — Who's winning the creative war and why? 3 sentences.
2. **Competitor Rankings** — Rank by creative quality. What separates #1 from last?
3. **What Competitors Do Better** — Specific elements where competitors outperform. Cite scores.
4. **Where Competitors Are Weak** — Weaknesses to exploit. Low emotion? Poor hooks? Weak CTAs?
5. **Hook Strategies** — Compare openings across competitors. Who stops the scroll best?
6. **Emotional Arcs** — Who builds emotional journeys vs flat-line ads?
7. **Brand Integration** — How do competitors integrate branding? Early logo? End card?
8. **CTA Strategies** — When and how do competitors deliver their CTA?
9. **Opportunities** — 5 creative strategies to steal, adapt, or counter.
10. **Threats** — 3 competitor trends that could hurt your position if ignored.
Reference competitor names and ad filenames. Cite scores."""
resp = requests.post(f"{BASE}/mave/chat", headers=HEADERS,
json={"message": prompt}, timeout=180).json()
if "error" in resp:
raise Exception(resp["error"]["message"])
return resp["content"]
async function generateCompetitiveReport(results, matrix) {
const compTable = formatCompetitorTable(matrix);
const indTable = formatIndividualTable(results);
const competitors = Object.keys(matrix);
const prompt = `You are a senior creative strategist conducting competitive creative intelligence.
## Competitor Summary
${compTable}
## Individual Ad Scores
${indTable}
## Competitors: ${competitors.join(", ")}
## Your Task
Produce a competitive creative intelligence report:
1. **Executive Summary** — Who's winning and why? 3 sentences.
2. **Competitor Rankings** — Rank by creative quality. What separates #1 from last?
3. **What Competitors Do Better** — Elements where they outperform. Cite scores.
4. **Where Competitors Are Weak** — Weaknesses to exploit.
5. **Hook Strategies** — Compare openings. Who stops the scroll best?
6. **Emotional Arcs** — Who builds emotional journeys?
7. **Brand Integration** — How do competitors brand? Early logo? End card?
8. **CTA Strategies** — When and how is the CTA delivered?
9. **Opportunities** — 5 strategies to steal, adapt, or counter.
10. **Threats** — 3 trends that could hurt if ignored.
Reference names and filenames. Cite scores.`;
const resp = await fetch(`${BASE}/mave/chat`, {
method: "POST", headers: HEADERS,
body: JSON.stringify({ message: prompt }), signal: AbortSignal.timeout(180000),
}).then((r) => r.json());
if (resp.error) throw new Error(resp.error.message);
return resp.content;
}
Running the Full Pipeline
def run_competitor_reel(ad_directory: str = "./competitor_ads"):
assets = upload_competitor_ads(ad_directory)
results = analyze_all_competitors(assets)
matrix = build_comparison_matrix(results)
print(format_competitor_table(matrix))
report = generate_competitive_report(results, matrix)
competitors = set(a["competitor"] for a in assets)
with open("competitor_reel_report.md", "w") as f:
f.write(f"# Competitive Intelligence — {time.strftime('%B %Y')}\n\n")
f.write(format_competitor_table(matrix) + "\n\n" + format_individual_table(results))
f.write(f"\n\n---\n\n{report}")
print("Saved to competitor_reel_report.md")
return matrix, report
if __name__ == "__main__":
import sys
run_competitor_reel(sys.argv[1] if len(sys.argv) > 1 else "./competitor_ads")
async function runCompetitorReel(dir = "./competitor_ads") {
const assets = await uploadCompetitorAds(dir);
const results = await analyzeAllCompetitors(assets);
const matrix = buildComparisonMatrix(results);
console.log(formatCompetitorTable(matrix));
const report = await generateCompetitiveReport(results, matrix);
const month = new Date().toLocaleString("default", { month: "long", year: "numeric" });
fs.writeFileSync("competitor_reel_report.md",
`# Competitive Creative Intelligence — ${month}\n\n${formatCompetitorTable(matrix)}\n\n` +
`${formatIndividualTable(results)}\n\n---\n\n${report}`);
return { matrix, report };
}
runCompetitorReel(process.argv[2] || "./competitor_ads");
Example Output
# Competitive Creative Intelligence — March 2026
**Competitors:** adidas, nike, ours, puma | **Total ads:** 7
| Competitor | Ads | Avg Overall | Avg Emotion | Avg CTA | Best Ad |
|------------|-----|-------------|-------------|---------|-----------------|
| nike | 2 | 86.5/100 | 9.0/10 | 7.5/10 | nike_hero_30s |
| ours | 1 | 78.0/100 | 7.0/10 | 8.0/10 | ours_hero_30s |
| adidas | 2 | 74.0/100 | 7.5/10 | 6.5/10 | adidas_brand_45s|
| puma | 2 | 65.0/100 | 6.0/10 | 5.0/10 | puma_launch_30s |
## Executive Summary
Nike dominates (86.5/100) via emotional intensity (9/10) and strong hooks.
Our CTA (8/10) is competitive but overall trails by 8.5 points. Puma's weak
hooks and low emotion are exploitable...
Variations
Include your own ads for direct benchmarking
Include your own ads for direct benchmarking
Name your ads with an
ours_ prefix. They’ll appear in the matrix for direct comparison.Quarterly tracking
Quarterly tracking
Save each matrix to JSON for trend analysis over time.
Add Focus Group for audience reaction
Add Focus Group for audience reaction
After Video Analysis, run a Focus Group showing the top competitor ad vs yours for simulated audience preference.
Platform-specific analysis
Platform-specific analysis
Tag files by platform (
nike_tiktok_hero.mp4) and add platform as a grouping dimension in the matrix.Override competitor name extraction
Override competitor name extraction
Use a manual
COMPETITOR_MAP dict if filenames don’t follow the competitor_type.mp4 convention.Credits Estimate
| Stage | Typical Cost | Notes |
|---|---|---|
| File uploads (×N) | 0 | Free |
| Video Analysis (×N) | 100–250 each | Depends on video length |
| Mave synthesis | 15–30 | Single research query |
| 6-ad reel | ~615–1,530 | 5 competitors + 1 own |
| 10-ad reel | ~1,015–2,530 | Large competitive landscape |
Prioritize quality over quantity. 2 ads from each of 3 key competitors gives more actionable intel than 1 ad from 8 competitors.
See Also
Ad Creative Audit
Audit your own ads instead of competitors’
Hook Analysis Sprint
Deep-dive into hook strategies across variants
Video + Focus Group Double
Layer competitor analysis with synthetic audience reactions
Competitive Ad Analysis Pipeline
Head-to-head pipeline with Focus Group and Mave report
Video Analysis
Metrics reference and chunk configuration
Mave Agent
Research agent for synthesis and recommendations