Scenario
Your quarterly brand tracking survey measures awareness, consideration, preference, and NPS against competitors. The numbers tell you where you stand, but not why. This job exports brand tracker data, sends it to Mave Agent with web search enabled, and asks: “Research the underlying market dynamics driving these brand perception shifts.” The result is AI research that connects your tracking data to real market events — competitor launches, industry shifts, PR events — giving your brand team actionable context. Flow: Qualtrics brand tracker export → Extract brand metrics over time → MavePOST /api/v1/mave/chat (with web search): “Research underlying market dynamics” → Cited competitive intelligence report
Architecture
Code
import os, csv, io, zipfile, requests, time
from collections import defaultdict
QT = os.environ["QUALTRICS_TOKEN"]
DC = os.environ["QUALTRICS_DC"]
MV = os.environ["MAVERA_API_KEY"]
Q_BASE = f"https://{DC}.qualtrics.com/API/v3"
MB = "https://app.mavera.io/api/v1"
Q_H = {"X-API-TOKEN": QT, "Content-Type": "application/json"}
MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
SURVEY_ID = os.environ.get("BRAND_TRACKER_ID", "SV_xxxxx")
# 1. Export responses (async)
export = requests.post(f"{Q_BASE}/surveys/{SURVEY_ID}/export-responses",
headers=Q_H, json={"format": "csv"}).json()
progress_id = export["result"]["progressId"]
file_id = None
for _ in range(60):
time.sleep(5)
status = requests.get(
f"{Q_BASE}/surveys/{SURVEY_ID}/export-responses/{progress_id}",
headers=Q_H).json()
if status["result"].get("percentComplete") == 100:
file_id = status["result"]["fileId"]
break
zip_data = requests.get(
f"{Q_BASE}/surveys/{SURVEY_ID}/export-responses/{file_id}/file",
headers=Q_H).content
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
csv_name = [n for n in zf.namelist() if n.endswith(".csv")][0]
with zf.open(csv_name) as f:
reader = csv.DictReader(io.TextIOWrapper(f, encoding="utf-8-sig"))
rows = [r for r in reader if r.get("Finished") == "1"]
print(f"Brand tracker: {len(rows)} responses")
# 2. Extract brand metrics
BRANDS = os.environ.get("TRACKED_BRANDS", "OurBrand,CompetitorA,CompetitorB,CompetitorC").split(",")
METRICS = ["Awareness", "Consideration", "Preference", "NPS"]
brand_data = defaultdict(lambda: defaultdict(list))
for row in rows:
quarter = row.get("RecordedDate", "")[:7]
for brand in BRANDS:
for metric in METRICS:
col_variants = [
f"{brand}_{metric}", f"{metric}_{brand}",
f"Q_{brand}_{metric}", f"{brand} - {metric}",
]
for col in col_variants:
val = row.get(col, "").strip()
if val:
try:
brand_data[brand][f"{metric}_{quarter}"].append(float(val))
except ValueError:
brand_data[brand][f"{metric}_{quarter}"].append(val)
break
# 3. Build brand comparison summary
summary_parts = []
for brand in BRANDS:
brand_summary = f"### {brand}\n"
metrics_by_type = defaultdict(dict)
for key, values in sorted(brand_data[brand].items()):
metric, quarter = key.rsplit("_", 1)
nums = [v for v in values if isinstance(v, (int, float))]
if nums:
metrics_by_type[metric][quarter] = sum(nums) / len(nums)
for metric, quarters in metrics_by_type.items():
trend = " → ".join(f"{q}: {v:.1f}" for q, v in sorted(quarters.items()))
brand_summary += f" {metric}: {trend}\n"
summary_parts.append(brand_summary)
brand_summary = "\n".join(summary_parts)
if not brand_summary.strip():
brand_summary = f"Raw column sample: {', '.join(list(rows[0].keys())[:20])}"
print("Warning: Could not auto-detect brand metric columns. Check column naming.")
# 4. Mave competitive analysis with web search
analysis = requests.post(f"{MB}/mave/chat", headers=MV_H, json={
"message": f"""Analyze this brand tracking data and research the underlying market dynamics.
BRAND TRACKING DATA ({len(rows)} respondents):
{brand_summary[:4000]}
Tasks:
1) Identify significant shifts in brand perception — which brands gained/lost and by how much
2) Research recent market events that explain these shifts (product launches, funding, PR, partnerships)
3) Competitive positioning analysis — where does each brand sit in the market map?
4) Identify emerging threats not yet reflected in tracking data
5) Recommend strategic responses to competitive movements
6) Predict next quarter's brand perception based on current trajectories
Use web search to ground your analysis in real market events."""
}).json()
print(f"\n{'='*60}")
print(f"COMPETITIVE INTELLIGENCE REPORT")
print(f"{'='*60}")
print(analysis.get("content", "")[:3000])
print(f"\nSources: {len(analysis.get('sources', []))}")
for src in analysis.get("sources", [])[:5]:
print(f" - {src.get('title', src.get('url', ''))}")
const QT = process.env.QUALTRICS_TOKEN;
const DC = process.env.QUALTRICS_DC;
const MV = process.env.MAVERA_API_KEY;
const Q_BASE = `https://${DC}.qualtrics.com/API/v3`;
const MB = "https://app.mavera.io/api/v1";
const qH = { "X-API-TOKEN": QT, "Content-Type": "application/json" };
const mvH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
const SURVEY_ID = process.env.BRAND_TRACKER_ID || "SV_xxxxx";
const BRANDS = (process.env.TRACKED_BRANDS || "OurBrand,CompetitorA,CompetitorB").split(",");
// 1. Export (async)
const exp = await fetch(`${Q_BASE}/surveys/${SURVEY_ID}/export-responses`,
{ method: "POST", headers: qH, body: JSON.stringify({ format: "csv" }) }).then(r => r.json());
let fileId = null;
for (let i = 0; i < 60; i++) {
await new Promise(r => setTimeout(r, 5000));
const s = await fetch(`${Q_BASE}/surveys/${SURVEY_ID}/export-responses/${exp.result.progressId}`,
{ headers: qH }).then(r => r.json());
if (s.result?.percentComplete === 100) { fileId = s.result.fileId; break; }
}
// 2. Download + parse
const JSZip = (await import("jszip")).default;
const zipBuf = await fetch(`${Q_BASE}/surveys/${SURVEY_ID}/export-responses/${fileId}/file`,
{ headers: qH }).then(r => r.arrayBuffer());
const zip = await JSZip.loadAsync(zipBuf);
const csvFile = Object.keys(zip.files).find(n => n.endsWith(".csv"));
const csvText = await zip.files[csvFile].async("string");
const lines = csvText.split("\n");
const headers = lines[0].split(",").map(h => h.replace(/"/g, "").trim());
const rows = lines.slice(3).filter(l => l.trim()).map(line => {
const vals = line.match(/(".*?"|[^,]*)/g) || [];
const obj = {};
headers.forEach((h, i) => { obj[h] = (vals[i] || "").replace(/"/g, "").trim(); });
return obj;
}).filter(r => r.Finished === "1");
console.log(`Brand tracker: ${rows.length} responses`);
// 3. Extract metrics + build summary
const brandData = {};
for (const brand of BRANDS) {
brandData[brand] = {};
for (const row of rows) {
const quarter = (row.RecordedDate || "").slice(0, 7);
for (const metric of ["Awareness", "Consideration", "Preference", "NPS"]) {
for (const col of [`${brand}_${metric}`, `${metric}_${brand}`, `Q_${brand}_${metric}`]) {
const val = (row[col] || "").trim();
if (val && !isNaN(Number(val))) {
const key = `${metric}_${quarter}`;
(brandData[brand][key] ??= []).push(Number(val));
break;
}
}
}
}
}
const summaryParts = BRANDS.map(brand => {
let s = `### ${brand}\n`;
const byMetric = {};
for (const [key, vals] of Object.entries(brandData[brand])) {
const [metric, quarter] = key.split(/_(?=[^_]+$)/);
(byMetric[metric] ??= {})[quarter] = vals.reduce((a, b) => a + b, 0) / vals.length;
}
for (const [metric, quarters] of Object.entries(byMetric)) {
s += ` ${metric}: ${Object.entries(quarters).sort().map(([q, v]) => `${q}: ${v.toFixed(1)}`).join(" → ")}\n`;
}
return s;
}).join("\n");
// 4. Mave analysis
const analysis = await fetch(`${MB}/mave/chat`, { method: "POST", headers: mvH,
body: JSON.stringify({
message: `Analyze brand tracking data. Research market dynamics.
DATA (${rows.length} respondents):
${summaryParts.slice(0, 4000)}
1) Brand perception shifts 2) Market events explaining shifts
3) Competitive positioning 4) Emerging threats 5) Strategic responses 6) Next-quarter prediction
Use web search for real market events.`,
}),
}).then(r => r.json());
console.log(`\n${"=".repeat(60)}\nCOMPETITIVE INTELLIGENCE REPORT\n${"=".repeat(60)}`);
console.log((analysis.content || "").slice(0, 3000));
console.log(`\nSources: ${(analysis.sources || []).length}`);
Example Output
============================================================
COMPETITIVE INTELLIGENCE REPORT
============================================================
## Brand Perception Shifts (Q3 2025 → Q1 2026)
### OurBrand
- Awareness: 42% → 48% (+6pp) — Driven by podcast sponsorship campaign
- Consideration: 31% → 35% (+4pp) — Product Hunt re-launch helped
- NPS: 38 → 41 (+3) — Onboarding improvements showing results
### CompetitorA
- Awareness: 67% → 71% (+4pp) — $50M Series C press coverage
- Consideration: 45% → 39% (-6pp) — Pricing backlash on social media
- NPS: 52 → 44 (-8) — API deprecation angered developer community
### CompetitorB
- Awareness: 23% → 31% (+8pp) — Aggressive content marketing
- Consideration: 12% → 19% (+7pp) — Free tier launch in January
## Market Dynamics
1. CompetitorA's pricing change created a switching window — 3 Reddit threads
with 200+ upvotes on "CompetitorA alternatives"
2. Category analyst report (Forrester, Jan 2026) elevated 2 new entrants
3. EU AI Act compliance requirements creating new evaluation criteria
## Strategic Recommendations
1. Target CompetitorA's disaffected developers with migration guides
2. Pursue the Forrester evaluation for inclusion in next wave
3. Launch competitive pricing page addressing CompetitorA's backlash
Sources: 7
- TechCrunch: "CompetitorA raises $50M Series C"
- Reddit: "r/SaaS - Best CompetitorA alternatives 2026"
- Forrester: "The AI Platform Landscape, Q1 2026"
Error Handling
Column naming varies
Column naming varies
Brand tracker surveys use custom column names. The code tries multiple patterns (
Brand_Metric, Metric_Brand, Q_Brand_Metric). If no matches found, it prints the raw column names for manual mapping.Web search availability
Web search availability
Mave’s web search requires specific permissions. If web search is unavailable, the analysis will rely on the survey data alone without external market context.
Quarterly data alignment
Quarterly data alignment
Responses are grouped by
RecordedDate month. Ensure your brand tracker runs on a consistent schedule for meaningful trend analysis.