> ## 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.

# Lookalike Audience Persona Expansion

> Analyze Lookalike seed audiences, identify adjacent demographic and psychographic segments, and create expansion personas for incremental reach testing.

### 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

```mermaid theme={"dark"}
flowchart LR
    A["Meta GET customaudiences (Lookalike)"] --> B[Insights on seed audience] --> C["POST /api/v1/personas (seed)"] --> D["POST /api/v1/mave/chat"] --> E["POST /api/v1/personas (expansion)"] --> F[New targeting opportunities]
```

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  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']}")
  ```

  ```javascript JavaScript theme={"dark"}
  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. Lookalike Audiences
  const audiences = await fetch(
    `${GRAPH}/${ACCT}/customaudiences?access_token=${META}&fields=id,name,subtype,approximate_count,lookalike_spec,description&limit=50`
  ).then(r => r.json()).then(d => d.data || []);

  const lookalikes = audiences.filter(a => a.subtype === "LOOKALIKE").slice(0, 5);
  console.log(`Lookalikes: ${lookalikes.length}`);

  // 2. Seed details
  const seeds = {};
  for (const la of lookalikes) {
    const originId = Array.isArray(la.lookalike_spec?.origin) ? la.lookalike_spec.origin[0]?.id : null;
    if (!originId) continue;
    const seedInfo = await fetch(
      `${GRAPH}/${originId}?access_token=${META}&fields=id,name,approximate_count,description`
    ).then(r => r.json());
    seeds[la.id] = {
      lookalike_name: la.name, seed_name: seedInfo.name || "Unknown",
      seed_size: seedInfo.approximate_count || 0, seed_description: seedInfo.description || "",
      ratio: la.lookalike_spec?.ratio || 0.01, country: la.lookalike_spec?.country || "US",
    };
  }

  // 3. Demographic insights
  for (const [laId, seed] of Object.entries(seeds)) {
    const insights = await fetch(
      `${GRAPH}/${ACCT}/insights?access_token=${META}&fields=impressions,clicks,ctr,actions&breakdowns=age,gender&filtering=[{"field":"audience","operator":"CONTAIN","value":"${laId}"}]&date_preset=last_90d&limit=50`
    ).then(r => r.json()).then(d => d.data || []);
    seed.top_demographics = insights
      .sort((a, b) => parseInt(b.clicks || "0") - parseInt(a.clicks || "0"))
      .slice(0, 5).map(d => ({ age: d.age, gender: d.gender, clicks: d.clicks }));
    await new Promise(r => setTimeout(r, 500));
  }

  // 4. Seed personas
  const seedPersonas = [];
  for (const [laId, seed] of Object.entries(seeds)) {
    const demoDesc = (seed.top_demographics || []).slice(0, 3)
      .map(d => `${d.gender} ${d.age} (${d.clicks} clicks)`).join(", ");
    const persona = await fetch(`${MB}/personas`, {
      method: "POST", headers: MH,
      body: JSON.stringify({
        name: `Seed: ${(seed.seed_name || "").slice(0, 40)}`,
        description: `Seed for '${seed.lookalike_name}'. Size: ${seed.seed_size.toLocaleString()}. Top demos: ${demoDesc}.`,
        demographic: { top_segments: seed.top_demographics, country: seed.country },
      }),
    }).then(r => r.json());
    seedPersonas.push({ persona_id: persona.id, seed_name: seed.seed_name, lookalike_name: seed.lookalike_name });
    await new Promise(r => setTimeout(r, 300));
  }

  // 5. Mave expansion analysis
  const seedSummary = seedPersonas.map(sp => `- "${sp.seed_name}" → "${sp.lookalike_name}"`).join("\n");
  const demoSummary = Object.values(seeds).map(s =>
    `  ${s.seed_name}: ${(s.top_demographics || []).slice(0, 3).map(d => `${d.gender} ${d.age}`).join(", ")}`
  ).join("\n");

  const expansion = await fetch(`${MB}/mave/chat`, {
    method: "POST", headers: MH,
    body: JSON.stringify({
      message: `Analyze seeds and find expansion segments:\n\nSEEDS:\n${seedSummary}\n\nDEMOS:\n${demoSummary}\n\nFor each: 1) Adjacent segments 2) Unifying psychographics 3) 3-4 expansion personas 4) Test priority`,
    }),
  }).then(r => r.json());

  console.log("\n=== Expansion Analysis ===");
  console.log((expansion.content || "").slice(0, 2000));

  // 6. Create expansion personas
  const expPrompt = await fetch(`${MB}/mave/chat`, {
    method: "POST", headers: MH,
    body: JSON.stringify({
      message: "Output exactly 4 expansion personas with Name, Age, Gender, Interests, Description. No other text.",
    }),
  }).then(r => r.json());

  const blocks = (expPrompt.content || "").split("\n\n").filter(b => b.trim().startsWith("Name:"));
  const expansionPersonas = [];
  for (const block of blocks.slice(0, 4)) {
    const lines = Object.fromEntries(
      block.split("\n").filter(l => l.includes(":")).map(l => {
        const [k, ...v] = l.split(":");
        return [k.trim(), v.join(":").trim()];
      })
    );
    if (!lines.Name) continue;
    const p = await fetch(`${MB}/personas`, {
      method: "POST", headers: MH,
      body: JSON.stringify({
        name: `Expansion: ${lines.Name}`, description: lines.Description || "Expansion segment.",
        demographic: {
          age_range: lines.Age || "25-54", gender: lines.Gender || "any",
          interests: (lines.Interests || "").split(",").map(i => i.trim()),
        },
      }),
    }).then(r => r.json());
    expansionPersonas.push({ name: lines.Name, id: p.id });
    await new Promise(r => setTimeout(r, 300));
  }

  console.log(`\nCreated ${expansionPersonas.length} expansion personas:`);
  expansionPersonas.forEach(ep => console.log(`  ${ep.name} → ${ep.id}`));
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
=== 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

<AccordionGroup>
  <Accordion title="Lookalike spec structure varies">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.</Accordion>
  <Accordion title="Filtering by audience ID in Insights">The `filtering` parameter uses a JSON array string. Encoding issues cause silent failures — validate the JSON structure before sending.</Accordion>
  <Accordion title="Mave persona extraction is best-effort">The structured output from Mave's second call may not perfectly match the expected format. Add fallback parsing for variations in line formatting.</Accordion>
  <Accordion title="Expansion personas need validation">These are hypothetical segments. Test with small budgets (\$50-100/day) before scaling. Track CPA against your seed audience as the benchmark.</Accordion>
</AccordionGroup>

<CardGroup cols={2}>
  <Card title="All Meta Ads Jobs" icon="meta" href="/integrations/meta-ads">
    Browse all Meta Ads integration jobs
  </Card>

  <Card title="Personas" icon="user" href="/features/personas">
    Creating and managing personas
  </Card>
</CardGroup>
