Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.mavera.io/llms.txt

Use this file to discover all available pages before exploring further.

Scenario

You’ve built a 1% Lookalike from your best customers, but you’re missing adjacent segments that could convert. This job takes your Lookalike source data, creates seed audience personas in Mavera, then uses Mave to identify adjacent demographic and psychographic segments you haven’t targeted. The output is new expansion personas you can test with incremental budget.

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. Find Lookalike Audiences and their seeds
audiences = requests.get(
    f"{GRAPH}/{ACCT}/customaudiences",
    params={
        "access_token": META,
        "fields": "id,name,subtype,approximate_count,lookalike_spec,description",
        "limit": 50,
    },
).json().get("data", [])

lookalikes = [a for a in audiences if a.get("subtype") == "LOOKALIKE"]
print(f"Lookalike audiences: {len(lookalikes)}")

# 2. Get seed audience details
seeds = {}
for la in lookalikes[:5]:
    spec = la.get("lookalike_spec", {})
    origin_id = spec.get("origin", [{}])[0].get("id") if isinstance(spec.get("origin"), list) else None
    if not origin_id:
        continue
    seed_info = requests.get(
        f"{GRAPH}/{origin_id}",
        params={"access_token": META, "fields": "id,name,approximate_count,description,subtype"},
    ).json()
    seeds[la["id"]] = {
        "lookalike_name": la["name"],
        "seed_name": seed_info.get("name", "Unknown"),
        "seed_size": seed_info.get("approximate_count", 0),
        "seed_description": seed_info.get("description", ""),
        "ratio": spec.get("ratio", 0.01),
        "country": spec.get("country", "US"),
    }

# 3. Get demographic insights on ads using these lookalikes
for la_id, seed in seeds.items():
    insights = requests.get(
        f"{GRAPH}/{ACCT}/insights",
        params={
            "access_token": META,
            "fields": "impressions,clicks,ctr,actions",
            "breakdowns": "age,gender",
            "filtering": f'[{{"field":"audience","operator":"CONTAIN","value":"{la_id}"}}]',
            "date_preset": "last_90d",
            "limit": 50,
        },
    ).json().get("data", [])

    top_demos = sorted(insights, key=lambda x: int(x.get("clicks", 0)), reverse=True)[:5]
    seed["top_demographics"] = [
        {"age": d.get("age"), "gender": d.get("gender"), "clicks": d.get("clicks")}
        for d in top_demos
    ]
    time.sleep(0.5)

# 4. Create seed personas
seed_personas = []
for la_id, seed in seeds.items():
    demo_desc = ", ".join(
        f"{d['gender']} {d['age']} ({d['clicks']} clicks)"
        for d in seed.get("top_demographics", [])[:3]
    )

    persona = requests.post(f"{MB}/personas", headers=MH, json={
        "name": f"Seed: {seed['seed_name'][:40]}",
        "description": (
            f"Seed audience for lookalike '{seed['lookalike_name']}'. "
            f"Size: {seed['seed_size']:,}. Country: {seed['country']}. "
            f"Top converting demographics: {demo_desc}. "
            f"{seed['seed_description']}"
        ),
        "demographic": {
            "top_segments": seed.get("top_demographics", []),
            "country": seed["country"],
        },
    }).json()
    seed_personas.append({
        "persona_id": persona["id"],
        "seed_name": seed["seed_name"],
        "lookalike_name": seed["lookalike_name"],
    })
    time.sleep(0.3)

# 5. Mave: identify adjacent expansion segments
seed_summary = "\n".join(
    f"- Seed \"{sp['seed_name']}\" → Lookalike \"{sp['lookalike_name']}\""
    for sp in seed_personas
)
demo_summary = "\n".join(
    f"  {s['seed_name']}: {', '.join(f\"{d['gender']} {d['age']}\" for d in s.get('top_demographics',[])[:3])}"
    for s in seeds.values()
)

expansion = requests.post(f"{MB}/mave/chat", headers=MH, json={
    "message": f"""Analyze these Meta Ads seed audiences and identify adjacent segments for expansion.

SEED AUDIENCES:
{seed_summary}

CONVERTING DEMOGRAPHICS:
{demo_summary}

For each seed audience:
1. What adjacent demographic segments are likely to convert but aren't in the current seed?
2. What psychographic traits unite the converting demographics?
3. Suggest 3-4 expansion personas (new segments to test) with:
   - Name, age range, gender, interests
   - Why they're adjacent to the seed audience
   - Estimated overlap with current targeting (low/medium/high)
   - Recommended ad angle for this segment
4. Which expansion segment should be tested first and why?"""
}).json()

print("\n=== Expansion Analysis ===")
print(expansion.get("content", "")[:2000])

# 6. Create expansion personas from Mave's recommendations
expansion_prompt = requests.post(f"{MB}/mave/chat", headers=MH, json={
    "message": f"""Based on your expansion analysis, output exactly 4 expansion personas in this format:
    
Name: [persona name]
Age: [range]
Gender: [any/male/female]  
Interests: [comma-separated]
Description: [2 sentences]

Only output the 4 personas, no other text."""
}).json()

expansion_text = expansion_prompt.get("content", "")
personas_blocks = [b.strip() for b in expansion_text.split("\n\n") if b.strip().startswith("Name:")]

expansion_personas = []
for block in personas_blocks[:4]:
    lines = {l.split(":")[0].strip(): ":".join(l.split(":")[1:]).strip()
             for l in block.split("\n") if ":" in l}
    if not lines.get("Name"):
        continue
    p = requests.post(f"{MB}/personas", headers=MH, json={
        "name": f"Expansion: {lines['Name']}",
        "description": lines.get("Description", "Expansion segment from Mave analysis."),
        "demographic": {
            "age_range": lines.get("Age", "25-54"),
            "gender": lines.get("Gender", "any"),
            "interests": [i.strip() for i in lines.get("Interests", "").split(",")],
        },
    }).json()
    expansion_personas.append({"name": lines["Name"], "id": p["id"]})
    time.sleep(0.3)

print(f"\nCreated {len(expansion_personas)} expansion personas:")
for ep in expansion_personas:
    print(f"  {ep['name']}{ep['id']}")

Example Output

=== Expansion Analysis ===

## Seed: "High-Value Purchasers" → 1% Lookalike

**Converting demographics:** Female 25-34 (42%), Male 35-44 (28%), Female 35-44 (18%)
**Unifying psychographics:** Career-driven, value efficiency, early adopters, active on Instagram

### Expansion Personas

1. **"Career-Switching Millennials"** (M/F, 28-36)
   Adjacent because: Share career-driven traits but are in transition — higher urgency
   Overlap: Low — different life stage signals
   Ad angle: "Made for people building what's next"

2. **"Gen X Decision Makers"** (M/F, 45-54)
   Adjacent because: Have budget authority your 35-44 segment aspires to
   Overlap: Medium — some already in 2% lookalike
   Ad angle: "Your team will thank you" (delegation framing)

3. **"Side-Hustle Creators"** (M/F, 22-30)
   Adjacent because: Early adopter behavior matches, but lower purchase power
   Overlap: Low — different income tier
   Ad angle: "Start free. Scale when you're ready."

4. **"Remote-First Team Leads"** (M/F, 30-42)
   Adjacent because: Efficiency-oriented, digital-native workflow preferences
   Overlap: Medium — behavioral overlap, different job title signals
   Ad angle: "Built for teams that never meet in person"

**Test first:** "Career-Switching Millennials" — lowest overlap means most incremental
reach. High urgency drives faster conversion cycles.

Created 4 expansion personas:
  Career-Switching Millennials → per_exp_csm_1
  Gen X Decision Makers → per_exp_gx_2
  Side-Hustle Creators → per_exp_shc_3
  Remote-First Team Leads → per_exp_rft_4

Error Handling

The lookalike_spec.origin field can be an array of objects or a single ID depending on API version. Always check if it’s an array first.
The filtering parameter uses a JSON array string. Encoding issues cause silent failures — validate the JSON structure before sending.
The structured output from Mave’s second call may not perfectly match the expected format. Add fallback parsing for variations in line formatting.
These are hypothetical segments. Test with small budgets ($50-100/day) before scaling. Track CPA against your seed audience as the benchmark.

All Meta Ads Jobs

Browse all Meta Ads integration jobs

Personas

Creating and managing personas