Scenario
People who reply, quote-tweet, and mention your brand reveal interests and engagement patterns. This job collects that data, analyzes behavioral patterns via Mave, and creates data-grounded persona profiles — built from real interactions, not assumptions.Architecture
Code
import os, requests, time
X = os.environ["X_BEARER_TOKEN"]; MV = os.environ["MAVERA_API_KEY"]
X_BASE = "https://api.x.com/2"; MV_BASE = "https://app.mavera.io/api/v1"
X_H = {"Authorization": f"Bearer {X}"}
MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
BRAND = "yourbrand"
queries = [f"@{BRAND} -is:retweet", f"url:x.com/{BRAND} is:quote"]
# 1. Collect interactions
interactions = []
for q in queries:
nt = None
for _ in range(3):
params = {"query": q, "max_results": 100, "tweet.fields": "public_metrics,author_id",
"expansions": "author_id", "user.fields": "username,description,public_metrics"}
if nt: params["next_token"] = nt
r = requests.get(f"{X_BASE}/tweets/search/recent", headers=X_H, params=params)
if r.status_code == 429:
time.sleep(int(r.headers.get("x-rate-limit-reset", time.time()+60)) - int(time.time()))
r = requests.get(f"{X_BASE}/tweets/search/recent", headers=X_H, params=params)
r.raise_for_status(); data = r.json()
users = {u["id"]: u for u in data.get("includes",{}).get("users",[])}
for t in data.get("data",[]):
a = users.get(t.get("author_id"),{})
interactions.append({"text": t["text"], "type": "quote" if "is:quote" in q else "reply",
"username": a.get("username",""), "bio": a.get("description",""),
"followers": a.get("public_metrics",{}).get("followers_count",0)})
nt = data.get("meta",{}).get("next_token")
if not nt: break
time.sleep(1)
# 2. Pattern analysis
block = "\n\n".join(f"@{i['username']} ({i['followers']:,} fol) [{i['type']}]\nBio: {i['bio'][:120]}\n{i['text'][:200]}"
for i in sorted(interactions, key=lambda x: -x["followers"])[:40])
analysis = requests.post(f"{MV_BASE}/mave/chat", headers=MV_H, json={
"message": f"Analyze reply/quote patterns around @{BRAND}. {len(interactions)} interactions.\n\n{block}\n\n"
"Identify 4-6 persona archetypes. For each: name, interaction pattern, topics, language, estimated %, value to brand."
}).json()
archetypes = analysis.get("content","")
print(archetypes[:1200])
# 3. Create personas
blocks = archetypes.split("##")[1:] if "##" in archetypes else [archetypes]
for b in blocks[:6]:
lines = b.strip().split("\n")
name = lines[0].strip().strip("#").strip() if lines else "X Persona"
p = requests.post(f"{MV_BASE}/personas", headers=MV_H, json={
"name": f"X Audience: {name}", "description": "\n".join(lines[1:])[:500],
"psychographic": {"source": "x_reply_mining", "brand": BRAND},
}).json()
print(f" Created: {name} — {p['id']}")
time.sleep(0.3)
// --- Same X_BASE, MV_BASE, X_H, MV_H setup as Job 1 ---
const BRAND = "yourbrand";
const queries = [`@${BRAND} -is:retweet`, `url:x.com/${BRAND} is:quote`];
const interactions = [];
for (const q of queries) {
let nt = null;
for (let i = 0; i < 3; i++) {
const params = new URLSearchParams({ query: q, max_results: "100",
"tweet.fields": "public_metrics,author_id", expansions: "author_id",
"user.fields": "username,description,public_metrics" });
if (nt) params.set("next_token", nt);
let r = await fetch(`${X_BASE}/tweets/search/recent?${params}`, { headers: X_H });
if (r.status === 429) { await new Promise(res => setTimeout(res, 60000));
r = await fetch(`${X_BASE}/tweets/search/recent?${params}`, { headers: X_H }); }
if (!r.ok) break; const data = await r.json();
const users = Object.fromEntries((data.includes?.users||[]).map(u => [u.id, u]));
for (const t of data.data||[]) { const a = users[t.author_id]||{};
interactions.push({ text: t.text, type: q.includes("is:quote")?"quote":"reply",
username: a.username||"", bio: a.description||"", followers: a.public_metrics?.followers_count||0 }); }
nt = data.meta?.next_token; if (!nt) break;
await new Promise(r => setTimeout(r, 1000));
}
}
const block = interactions.sort((a,b) => b.followers-a.followers).slice(0,40)
.map(i => `@${i.username} (${i.followers.toLocaleString()}) [${i.type}]\nBio: ${i.bio.slice(0,120)}\n${i.text.slice(0,200)}`).join("\n\n");
const analysis = await fetch(`${MV_BASE}/mave/chat`, { method: "POST", headers: MV_H,
body: JSON.stringify({ message: `Analyze @${BRAND} patterns. ${interactions.length} interactions.\n\n${block}\n\nIdentify 4-6 archetypes.` }),
}).then(r => r.json());
for (const b of ((analysis.content||"").split("##").slice(1)||[analysis.content]).slice(0,6)) {
const lines = b.trim().split("\n");
const name = (lines[0]||"Persona").replace(/#/g,"").trim();
const p = await fetch(`${MV_BASE}/personas`, { method: "POST", headers: MV_H,
body: JSON.stringify({ name: `X Audience: ${name}`, description: lines.slice(1).join("\n").slice(0,500),
psychographic: { source: "x_reply_mining", brand: BRAND } }),
}).then(r => r.json());
console.log(` Created: ${name} — ${p.id}`);
}
Example Output
## Industry Thought Leader (12%) — Quote-tweets with commentary. Influencer.
## Help-Seeker (35%) — "How do I..." replies. Active customer, upsell target.
## Lurker-Turned-Engager (20%) — Silent liker, replies to pain-point tweets. Prospect.
## Competitive Comparer (15%) — Mentions competitors alongside you. Evaluating.
Created: Industry Thought Leader — per_x_tl_01
Created: Help-Seeker — per_x_hs_02
Created: Lurker-Turned-Engager — per_x_le_03
Created: Competitive Comparer — per_x_cc_04
Error Handling
Quote tweet syntax
Quote tweet syntax
Use
url:x.com/{username} is:quote to find quotes. If sparse, broaden to @{username} is:quote.Archetype parsing
Archetype parsing
Code splits Mave output on
## headers. If Mave uses a different format, fewer personas are created. Review raw analysis.