The Scenario
You have five different ways to describe your product and five distinct audience segments. Which message works best for which audience? You build a message-testing matrix: every message gets evaluated by every persona, producing a quantitative fit score for each cell. The output is a heat map of message-persona fit that tells you exactly which message to use for which audience.Mavera-only workflow. No survey platforms, no panel vendors, no statistical software. Just Mavera’s Personas and Focus Groups surfaces.
When to Use This
- Pre-launch messaging finalization — pick the winning variant per audience segment.
- Channel-specific copy — different channels reach different personas.
- Website personalization — serve the right headline to the right visitor segment.
- Sales enablement — give each rep the message that resonates with their territory.
Architecture
| Mavera Surface | Role in Pipeline |
|---|---|
Personas (POST /personas) | Create 5 distinct audience segments |
Focus Groups (POST /focus-groups) | Test each message with each persona (25 combinations) |
What You Need
| Requirement | Details |
|---|---|
| Mavera API key | Starts with mvra_live_. Get one at Developer Settings. |
| Python 3.8+ or Node.js 18+ | requests for Python; native fetch for Node. |
| Credits | ~375–650 total. See Credits Estimate. |
MAVERA_API_KEY=mvra_live_your_key_here
Step 1 — Define 5 Message Variants
Each message takes a different angle: value, speed, trust, emotion, or technical.import os, time, json, requests
API_KEY = os.environ["MAVERA_API_KEY"]
BASE = "https://app.mavera.io/api/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
MESSAGES = {
"value": {"label": "Value-First", "headline": "Cut Research Costs by 80%",
"body": "Traditional audience research costs $15K–$50K per study. Mavera delivers the same insights for a fraction, in minutes."},
"speed": {"label": "Speed-First", "headline": "From Question to Insight in 10 Minutes",
"body": "Stop waiting weeks. Mavera's AI personas deliver focus group-quality feedback before your next standup."},
"trust": {"label": "Trust & Credibility", "headline": "Research You Can Bet Your Campaign On",
"body": "Personas built on data from millions of real consumers. 87% predictive accuracy. Trusted by 200+ marketing teams."},
"emotion": {"label": "Emotional", "headline": "Never Launch Blind Again",
"body": "That sinking feeling when a campaign underperforms? It starts with skipping research. Mavera makes it so fast, there's no excuse."},
"technical": {"label": "Technical", "headline": "API-First Audience Intelligence",
"body": "OpenAI-compatible Chat, Focus Groups with 12 question types, Video Analysis, Brand Voice extraction. One API key, unlimited surfaces."},
}
const API_KEY = process.env.MAVERA_API_KEY;
const BASE = "https://app.mavera.io/api/v1";
const HEADERS = { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" };
const MESSAGES = {
value: { label: "Value-First", headline: "Cut Research Costs by 80%",
body: "Traditional research costs $15K–$50K per study. Mavera delivers the same insights for a fraction, in minutes." },
speed: { label: "Speed-First", headline: "From Question to Insight in 10 Minutes",
body: "Stop waiting weeks. AI personas deliver focus group-quality feedback before your next standup." },
trust: { label: "Trust & Credibility", headline: "Research You Can Bet Your Campaign On",
body: "Personas built on data from millions of real consumers. 87% predictive accuracy. 200+ teams." },
emotion: { label: "Emotional", headline: "Never Launch Blind Again",
body: "That sinking feeling when a campaign underperforms? Starts with skipping research. Mavera makes it so fast, there's no excuse." },
technical: { label: "Technical", headline: "API-First Audience Intelligence",
body: "OpenAI-compatible Chat, Focus Groups, Video Analysis, Brand Voice. One API key, unlimited surfaces." },
};
Step 2 — Create 5 Personas
PERSONA_DEFS = [
{"name": "Startup CMO", "role": "CMO, Series B startup (50 people, $8M ARR)",
"description": "Built the team from scratch. Budget-conscious but growth-aggressive. Measures everything by pipeline impact.",
"traits": ["growth-focused", "budget-aware", "hands-on", "speed-oriented"]},
{"name": "Enterprise Brand Mgr", "role": "Senior Brand Manager, Fortune 500 CPG",
"description": "Manages $200M brand. Uses Nielsen, Kantar, Ipsos. Needs bulletproof methodology for internal buy-in.",
"traits": ["process-driven", "methodology-focused", "risk-averse", "data-rigorous"]},
{"name": "Agency Strategist", "role": "VP Strategy, creative agency with 40 clients",
"description": "Strategic frameworks across all accounts. Differentiates in pitches. Needs tools that scale across clients.",
"traits": ["multi-client thinker", "pitch-oriented", "insight-driven", "scalability-focused"]},
{"name": "Product Marketer", "role": "PMM, mid-stage SaaS company",
"description": "Owns positioning and launch for 3 product lines. Validates messaging before launch. Enables sales with battlecards.",
"traits": ["cross-functional", "launch-focused", "sales-enablement-oriented", "messaging-obsessed"]},
{"name": "Solo Consultant", "role": "Independent marketing consultant, 5–8 clients",
"description": "One-person operation. Uses research to justify recs. Every dollar comes from margin. Needs tools that make one person look like a team.",
"traits": ["price-sensitive", "time-constrained", "credibility-seeking", "simplicity-driven"]},
]
def create_persona(p):
resp = requests.post(f"{BASE}/personas", headers=HEADERS, json={
"name": p["name"], "role": p["role"], "description": p["description"], "traits": p["traits"]})
resp.raise_for_status()
data = resp.json()
print(f" Created: {data['name']} → {data['id']}")
return data["id"]
persona_map = {p["name"]: create_persona(p) for p in PERSONA_DEFS}
const PERSONA_DEFS = [
{ name: "Startup CMO", role: "CMO, Series B startup",
description: "Built team from scratch. Budget-conscious but growth-aggressive. Pipeline-focused.",
traits: ["growth-focused", "budget-aware", "hands-on", "speed-oriented"] },
{ name: "Enterprise Brand Mgr", role: "Senior Brand Manager, Fortune 500 CPG",
description: "$200M brand. Uses Nielsen, Kantar. Needs bulletproof methodology.",
traits: ["process-driven", "methodology-focused", "risk-averse", "data-rigorous"] },
{ name: "Agency Strategist", role: "VP Strategy, creative agency",
description: "Frameworks across 40 accounts. Differentiates in pitches. Needs scale.",
traits: ["multi-client thinker", "pitch-oriented", "insight-driven", "scalability-focused"] },
{ name: "Product Marketer", role: "PMM, mid-stage SaaS",
description: "Owns positioning for 3 product lines. Validates before launch. Enables sales.",
traits: ["cross-functional", "launch-focused", "sales-enablement-oriented", "messaging-obsessed"] },
{ name: "Solo Consultant", role: "Independent consultant, 5–8 clients",
description: "One-person op. Research justifies recs. Every dollar from margin.",
traits: ["price-sensitive", "time-constrained", "credibility-seeking", "simplicity-driven"] },
];
async function createPersona(p) {
const resp = await fetch(`${BASE}/personas`, { method: "POST", headers: HEADERS,
body: JSON.stringify({ name: p.name, role: p.role, description: p.description, traits: p.traits }) });
const data = await resp.json();
if (data.error) throw new Error(data.error.message);
return data.id;
}
const personaMap = {};
for (const p of PERSONA_DEFS) personaMap[p.name] = await createPersona(p);
Step 3 — Run the 5×5 Matrix
Create all 25 Focus Groups, then poll for completion. Mavera processes them concurrently.def build_questions(msg):
block = f"**{msg['headline']}**\n\n{msg['body']}"
return [
{"question": f"Rate how compelling this is to you (0-10):\n\n{block}", "type": "NPS", "order": 1},
{"question": "How would you describe this product to a colleague?", "type": "OPEN_ENDED", "order": 2},
{"question": "Strongest element?", "type": "MULTIPLE_CHOICE",
"options": ["Headline", "Value proposition", "Proof points", "Tone/voice", "Nothing stood out"], "order": 3},
{"question": "Biggest gap or weakness?", "type": "OPEN_ENDED", "order": 4},
{"question": "Would this make you want to learn more?", "type": "MULTIPLE_CHOICE",
"options": ["Yes — click immediately", "Probably — with right CTA", "Unlikely — not relevant", "No — off-putting"], "order": 5},
]
def poll_fg(fg_id, timeout_min=10):
for _ in range(timeout_min * 6):
resp = requests.get(f"{BASE}/focus-groups/{fg_id}", headers=HEADERS).json()
if "error" in resp: raise Exception(resp["error"]["message"])
if resp["status"] == "COMPLETED": return resp
time.sleep(10)
raise TimeoutError(f"Focus Group {fg_id} timed out")
print("Creating 25 Focus Groups...")
matrix_jobs = {}
for mk, msg in MESSAGES.items():
for pname, pid in persona_map.items():
key = f"{mk}|{pname}"
resp = requests.post(f"{BASE}/focus-groups", headers=HEADERS, json={
"name": f"Matrix: {msg['label']} × {pname}",
"persona_ids": [pid], "sample_size": 5, "questions": build_questions(msg),
})
resp.raise_for_status()
matrix_jobs[key] = resp.json()["id"]
print(f"{len(matrix_jobs)} Focus Groups created. Polling...")
matrix_results = {k: poll_fg(fid) for k, fid in matrix_jobs.items()}
print("All 25 complete.")
function buildQuestions(msg) {
const block = `**${msg.headline}**\n\n${msg.body}`;
return [
{ question: `Rate how compelling (0-10):\n\n${block}`, type: "NPS", order: 1 },
{ question: "How would you describe this to a colleague?", type: "OPEN_ENDED", order: 2 },
{ question: "Strongest element?", type: "MULTIPLE_CHOICE",
options: ["Headline", "Value proposition", "Proof points", "Tone/voice", "Nothing stood out"], order: 3 },
{ question: "Biggest gap or weakness?", type: "OPEN_ENDED", order: 4 },
{ question: "Would this make you want to learn more?", type: "MULTIPLE_CHOICE",
options: ["Yes — click immediately", "Probably — with right CTA", "Unlikely", "No — off-putting"], order: 5 },
];
}
async function pollFG(fgId, timeoutMin = 10) {
for (let i = 0; i < timeoutMin * 6; i++) {
const resp = await fetch(`${BASE}/focus-groups/${fgId}`, { headers: HEADERS }).then((r) => r.json());
if (resp.error) throw new Error(resp.error.message);
if (resp.status === "COMPLETED") return resp;
await new Promise((r) => setTimeout(r, 10000));
}
throw new Error("Timed out");
}
const matrixJobs = {};
for (const [mk, msg] of Object.entries(MESSAGES)) {
for (const [pname, pid] of Object.entries(personaMap)) {
const key = `${mk}|${pname}`;
const resp = await fetch(`${BASE}/focus-groups`, { method: "POST", headers: HEADERS,
body: JSON.stringify({ name: `Matrix: ${msg.label} × ${pname}`, persona_ids: [pid],
sample_size: 5, questions: buildQuestions(msg) }) });
matrixJobs[key] = (await resp.json()).id;
}
}
const matrixResults = {};
for (const [k, fid] of Object.entries(matrixJobs)) matrixResults[k] = await pollFG(fid);
25 Focus Groups take 5–15 minutes total. They run concurrently — the bottleneck is the slowest individual group.
Step 4 — Build and Display the Fit Matrix
def build_and_print_matrix(results):
msg_keys = list(MESSAGES.keys())
p_names = list(persona_map.keys())
matrix = {}
for mk in msg_keys:
matrix[mk] = {}
for pn in p_names:
cell = results.get(f"{mk}|{pn}", {})
nps = None
for qr in cell.get("results", []):
if qr["type"] == "NPS":
nps = qr.get("nps_score")
break
matrix[mk][pn] = nps
print("\n" + "=" * 90)
print("MESSAGE-PERSONA FIT MATRIX (NPS Scores)")
print("=" * 90)
header = f"{'Message':<18}" + "".join(f" {pn:>14}" for pn in p_names) + f" {'AVG':>8}"
print(header)
print("─" * 90)
best = (None, -100)
for mk in msg_keys:
label = MESSAGES[mk]["label"]
scores = [matrix[mk].get(pn) for pn in p_names if matrix[mk].get(pn) is not None]
avg = sum(scores) / len(scores) if scores else 0
row = f"{label:<18}" + "".join(
f" {matrix[mk].get(pn, 'N/A'):>14}" for pn in p_names) + f" {avg:>7.1f}"
print(row)
if avg > best[1]: best = (mk, avg)
print("─" * 90)
print(f"\n Best overall: {MESSAGES[best[0]]['label']} (avg NPS: {best[1]:.1f})")
for pn in p_names:
bm = max(msg_keys, key=lambda mk: matrix[mk].get(pn, -100) or -100)
print(f" Best for {pn}: {MESSAGES[bm]['label']} (NPS: {matrix[bm].get(pn)})")
return matrix
matrix = build_and_print_matrix(matrix_results)
function buildAndPrintMatrix(results) {
const msgKeys = Object.keys(MESSAGES);
const pNames = Object.keys(personaMap);
const matrix = {};
for (const mk of msgKeys) {
matrix[mk] = {};
for (const pn of pNames) {
const cell = results[`${mk}|${pn}`] || {};
matrix[mk][pn] = (cell.results || []).find((q) => q.type === "NPS")?.nps_score ?? null;
}
}
console.log("\nMESSAGE-PERSONA FIT MATRIX (NPS)");
console.log("─".repeat(90));
for (const mk of msgKeys) {
const scores = pNames.map((pn) => matrix[mk][pn]).filter((s) => s !== null);
const avg = scores.length ? (scores.reduce((a, b) => a + b) / scores.length).toFixed(1) : "N/A";
console.log(` ${MESSAGES[mk].label.padEnd(18)} ${pNames.map((pn) => String(matrix[mk][pn] ?? "N/A").padStart(14)).join("")} ${String(avg).padStart(8)}`);
}
for (const pn of pNames) {
const best = msgKeys.reduce((b, mk) => (matrix[mk][pn] || -100) > (matrix[b][pn] || -100) ? mk : b);
console.log(` Best for ${pn}: ${MESSAGES[best].label} (NPS: ${matrix[best][pn]})`);
}
return matrix;
}
buildAndPrintMatrix(matrixResults);
Example Output
MESSAGE-PERSONA FIT MATRIX (NPS Scores)
══════════════════════════════════════════════════════════════════════════════════════
Message Startup CMO Enterprise BM Agency Strat Product Mktr Solo Conslt AVG
──────────────────────────────────────────────────────────────────────────────────────
Value-First 72 35 58 55 82 60.4
Speed-First 85 28 65 70 75 64.6
Trust & Credibility 40 78 55 48 30 50.2
Emotional 68 22 70 62 55 55.4
Technical 55 45 38 80 20 47.6
──────────────────────────────────────────────────────────────────────────────────────
Best overall: Speed-First (avg NPS: 64.6)
Best for Startup CMO: Speed-First (NPS: 85)
Best for Enterprise Brand Mgr: Trust & Credibility (NPS: 78)
Best for Agency Strategist: Emotional (NPS: 70)
Best for Product Marketer: Technical (NPS: 80)
Best for Solo Consultant: Value-First (NPS: 82)
Variations
Export as CSV
Export as CSV
import csv
with open("matrix.csv", "w", newline="") as f:
w = csv.writer(f)
w.writerow(["Message", "Persona", "NPS"])
for mk in MESSAGES:
for pn in persona_map:
w.writerow([MESSAGES[mk]["label"], pn, matrix[mk].get(pn, "N/A")])
Increase sample size for confidence
Increase sample size for confidence
Bump
sample_size from 5 to 25 for more stable NPS. Credits scale linearly.Add a 6th message mid-study
Add a 6th message mid-study
new_msg = {"label": "Social Proof", "headline": "Join 500+ Teams", "body": "..."}
for pn, pid in persona_map.items():
resp = requests.post(f"{BASE}/focus-groups", headers=HEADERS, json={
"name": f"Matrix: Social Proof × {pn}", "persona_ids": [pid],
"sample_size": 5, "questions": build_questions(new_msg)})
Auto-improve the weakest message with Generate
Auto-improve the weakest message with Generate
worst = min(matrix, key=lambda mk: sum((matrix[mk].get(pn) or 0) for pn in persona_map))
gen_resp = requests.post(f"{BASE}/generations", headers=HEADERS, json={
"app": "ad_copy", "inputs": {"original": json.dumps(MESSAGES[worst]),
"feedback": "Underperformed across all persona segments. Improve."}}).json()
print(f"Improved: {gen_resp.get('content', '')[:200]}")
Visualize as a heat map
Visualize as a heat map
import matplotlib.pyplot as plt
import numpy as np
data = np.array([[matrix[mk].get(pn, 0) or 0 for pn in persona_map] for mk in MESSAGES])
fig, ax = plt.subplots(figsize=(10, 5))
ax.imshow(data, cmap="RdYlGn", aspect="auto", vmin=-100, vmax=100)
ax.set_xticks(range(len(persona_map))); ax.set_xticklabels(persona_map.keys(), rotation=45, ha="right")
ax.set_yticks(range(len(MESSAGES))); ax.set_yticklabels([m["label"] for m in MESSAGES.values()])
plt.colorbar(ax.images[0], label="NPS"); plt.tight_layout(); plt.savefig("heatmap.png", dpi=150)
Credits Estimate
| Operation | Typical Cost | Notes |
|---|---|---|
| Persona creation (×5) | 0–25 | One-time; reuse across runs |
| Focus Groups (×25, 5 respondents, 5 questions) | 375–625 | ~15–25 per group |
| Total | ~375–650 |
Start with
sample_size: 5 for directional signal. Increase to 15–25 only for top-performing cells where you need higher confidence.What’s Next
Industry Panel Simulation
Deep-dive a single message with 10 buying-committee personas
Persona Debate
Pit opposing personas against each other for pricing insights
Generational Content Testing
Test across age demographics instead of role-based segments
A/B Copy Production
Generate production-ready copy from matrix winners
Persona Selection Guide
Choose the right persona types for your research goal
Credits & Budget
Pre-flight checks and usage tracking