Scenario
You’ve written 4 headline variants for a new campaign and need to know which one drives clicks before spending budget on live A/B tests. This job pulls existing ad copy variants (or you provide new ones), creates a Focus Group with your target personas, and asks them to rate each headline 1–10 and answer “Would you click?” The result is a pre-launch signal on which copy to promote.Architecture
Code
import os, requests, time
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 ad copy variants from the same campaign
CAMPAIGN_ID = os.environ.get("META_CAMPAIGN_ID", "")
if CAMPAIGN_ID:
ads = requests.get(
f"{GRAPH}/{CAMPAIGN_ID}/ads",
params={
"access_token": META,
"fields": "id,name,creative{title,body,link_description,call_to_action_type}",
"limit": 20,
},
).json().get("data", [])
else:
ads = requests.get(
f"{GRAPH}/{ACCT}/ads",
params={
"access_token": META,
"fields": "id,name,creative{title,body,link_description,call_to_action_type}",
"effective_status": '["ACTIVE","PAUSED"]',
"limit": 20,
},
).json().get("data", [])
variants = []
for ad in ads:
c = ad.get("creative", {})
if c.get("title") or c.get("body"):
variants.append({
"ad_id": ad["id"],
"ad_name": ad.get("name", "Untitled"),
"headline": c.get("title", ""),
"body": c.get("body", ""),
"cta": c.get("call_to_action_type", "LEARN_MORE"),
})
variants = variants[:6]
print(f"Testing {len(variants)} ad copy variants")
# 2. Build Focus Group questions
variant_block = "\n\n".join(
f"**Variant {i+1}: \"{v['headline']}\"**\n{v['body'][:200]}\nCTA: {v['cta']}"
for i, v in enumerate(variants)
)
PERSONA_IDS = os.environ.get("PERSONA_IDS", "").split(",")
if not PERSONA_IDS[0]:
personas = requests.get(f"{MB}/personas", headers=MH).json()
PERSONA_IDS = [p["id"] for p in (personas if isinstance(personas, list) else [])[:4]]
questions = []
for i, v in enumerate(variants):
questions.append(f"Rate this headline 1-10 for how much it makes you want to learn more: \"{v['headline']}\"")
questions.append(f"Would you click on this ad? (Yes/No): \"{v['headline']}\" — {v['body'][:100]}")
questions.extend([
"Which variant is the most compelling overall? Give the variant number and explain why.",
"What would make you scroll past all of these? What's missing?",
])
# 3. Run Focus Group
fg = requests.post(f"{MB}/focus-groups", headers=MH, json={
"name": "Meta Ad Copy A/B Test",
"persona_ids": PERSONA_IDS,
"questions": questions,
"context": f"You are evaluating ad copy variants for a social media campaign. Here are the variants:\n\n{variant_block}",
"responses_per_persona": 2,
}).json()
# 4. Poll for results
for _ in range(24):
time.sleep(5)
data = requests.get(f"{MB}/focus-groups/{fg['id']}", headers=MH).json()
if data.get("status") == "completed":
break
# 5. Tabulate scores
scores = {i: {"ratings": [], "clicks": []} for i in range(len(variants))}
for resp in data.get("responses", []):
q = resp.get("question", "")
a = resp.get("answer", "")
for i in range(len(variants)):
if variants[i]["headline"] in q:
if "Rate" in q:
try:
score = int("".join(c for c in a if c.isdigit())[:2])
if 1 <= score <= 10:
scores[i]["ratings"].append(score)
except ValueError:
pass
elif "click" in q.lower():
scores[i]["clicks"].append(1 if a.lower().startswith("yes") else 0)
print("\n=== Ad Copy A/B Results ===")
for i, v in enumerate(variants):
s = scores[i]
avg_rating = sum(s["ratings"]) / max(len(s["ratings"]), 1)
click_rate = sum(s["clicks"]) / max(len(s["clicks"]), 1)
print(f"Variant {i+1}: \"{v['headline']}\"")
print(f" Rating: {avg_rating:.1f}/10 | Click intent: {click_rate:.0%}")
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 ad copy variants
const CAMPAIGN_ID = process.env.META_CAMPAIGN_ID || "";
const adsUrl = CAMPAIGN_ID
? `${GRAPH}/${CAMPAIGN_ID}/ads?access_token=${META}&fields=id,name,creative{title,body,link_description,call_to_action_type}&limit=20`
: `${GRAPH}/${ACCT}/ads?access_token=${META}&fields=id,name,creative{title,body,link_description,call_to_action_type}&effective_status=["ACTIVE","PAUSED"]&limit=20`;
const ads = await fetch(adsUrl).then(r => r.json()).then(d => d.data || []);
const variants = ads
.filter(a => a.creative?.title || a.creative?.body)
.map(a => ({
ad_id: a.id, ad_name: a.name || "Untitled",
headline: a.creative.title || "", body: a.creative.body || "",
cta: a.creative.call_to_action_type || "LEARN_MORE",
})).slice(0, 6);
console.log(`Testing ${variants.length} ad copy variants`);
// 2. Build questions
const variantBlock = variants.map((v, i) =>
`**Variant ${i+1}: "${v.headline}"**\n${v.body.slice(0, 200)}\nCTA: ${v.cta}`
).join("\n\n");
let personaIds = (process.env.PERSONA_IDS || "").split(",").filter(Boolean);
if (!personaIds.length) {
const personas = await fetch(`${MB}/personas`, { headers: MH }).then(r => r.json());
personaIds = (Array.isArray(personas) ? personas : []).slice(0, 4).map(p => p.id);
}
const questions = [];
for (const v of variants) {
questions.push(`Rate this headline 1-10: "${v.headline}"`);
questions.push(`Would you click? (Yes/No): "${v.headline}" — ${v.body.slice(0, 100)}`);
}
questions.push("Which variant is most compelling? Give the number and why.");
questions.push("What would make you scroll past all of these?");
// 3. Run Focus Group
const fg = await fetch(`${MB}/focus-groups`, {
method: "POST", headers: MH,
body: JSON.stringify({
name: "Meta Ad Copy A/B Test", persona_ids: personaIds,
questions, context: `Evaluating ad copy:\n\n${variantBlock}`,
responses_per_persona: 2,
}),
}).then(r => r.json());
// 4. Poll
let data;
for (let i = 0; i < 24; i++) {
await new Promise(r => setTimeout(r, 5000));
data = await fetch(`${MB}/focus-groups/${fg.id}`, { headers: MH }).then(r => r.json());
if (data.status === "completed") break;
}
// 5. Tabulate
const scores = variants.map(() => ({ ratings: [], clicks: [] }));
for (const resp of data.responses || []) {
variants.forEach((v, i) => {
if (resp.question?.includes(v.headline)) {
if (resp.question.includes("Rate")) {
const num = parseInt((resp.answer || "").match(/\d+/)?.[0] || "0");
if (num >= 1 && num <= 10) scores[i].ratings.push(num);
} else if (resp.question.toLowerCase().includes("click")) {
scores[i].clicks.push(resp.answer?.toLowerCase().startsWith("yes") ? 1 : 0);
}
}
});
}
console.log("\n=== Ad Copy A/B Results ===");
variants.forEach((v, i) => {
const avg = scores[i].ratings.reduce((a, b) => a + b, 0) / Math.max(scores[i].ratings.length, 1);
const clickRate = scores[i].clicks.reduce((a, b) => a + b, 0) / Math.max(scores[i].clicks.length, 1);
console.log(`Variant ${i+1}: "${v.headline}"\n Rating: ${avg.toFixed(1)}/10 | Click intent: ${(clickRate * 100).toFixed(0)}%`);
});
Example Output
=== Ad Copy A/B Results ===
Variant 1: "Stop Guessing. Start Growing."
Rating: 7.8/10 | Click intent: 75%
Variant 2: "Your Competitors Already Know This"
Rating: 8.4/10 | Click intent: 88%
Variant 3: "Save 10 Hours a Week on Reporting"
Rating: 6.9/10 | Click intent: 63%
Variant 4: "Built for Teams That Ship Fast"
Rating: 7.2/10 | Click intent: 50%
Winner: Variant 2 — curiosity gap + competitive framing
"Makes me wonder what I'm missing. I'd click to find out." — Meta Female 25-34
"The competitive angle creates urgency." — Meta Male 35-44
Error Handling
No creative title field
No creative title field
Some ad formats (Dynamic Creative, Catalog) don’t have a top-level
title. Check object_story_spec.link_data.message for the primary text instead.Focus Group timeout on many variants
Focus Group timeout on many variants
6 variants × 2 questions × 4 personas = 48 responses. Allow 2+ minutes for polling. Reduce
responses_per_persona if speed matters more than depth.Score parsing from open-ended answers
Score parsing from open-ended answers
Personas respond naturally (“I’d give it an 8”). The regex extraction handles most formats but may miss edge cases like “eight out of ten”.
Meta Ads Integration
All Meta Ads jobs
Focus Groups
Full reference for synthetic focus groups