const SG = process.env.SENDGRID_API_KEY;
const MV = process.env.MAVERA_API_KEY;
const SG_BASE = "https://api.sendgrid.com/v3";
const MB = "https://app.mavera.io/api/v1";
const sgH = { Authorization: `Bearer ${SG}`, "Content-Type": "application/json" };
const mvH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
// 1. Trigger export
const exportRes = await fetch(`${SG_BASE}/marketing/contacts/exports`, {
method: "POST", headers: sgH,
body: JSON.stringify({ list_ids: [], segment_ids: [], file_type: "csv", max_file_size: 5000 }),
}).then(r => r.json());
const exportId = exportRes.id;
console.log(`Export triggered: ${exportId}`);
// 2. Poll until ready
let downloadUrl = null;
for (let i = 0; i < 30; i++) {
await new Promise(r => setTimeout(r, 10000));
const status = await fetch(`${SG_BASE}/marketing/contacts/exports/${exportId}`,
{ headers: sgH }).then(r => r.json());
console.log(` Status: ${status.status} (attempt ${i + 1})`);
if (status.status === "ready" && status.urls?.length) {
downloadUrl = status.urls[0]; break;
}
if (status.status === "failure") { console.log("Export failed"); process.exit(1); }
}
if (!downloadUrl) { console.log("Timed out"); process.exit(1); }
// 3. Download and parse CSV
const csvText = await fetch(downloadUrl).then(r => r.text());
const lines = csvText.split("\n");
const headers = lines[0].split(",").map(h => h.trim().replace(/"/g, ""));
const contacts = lines.slice(1).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;
});
console.log(`Downloaded ${contacts.length} contacts`);
// 4. Cluster
const clusters = {};
for (const c of contacts) {
const industry = (c.industry || c.custom_industry || "unknown").toLowerCase();
const role = (c.job_title || c.custom_role || "unknown").toLowerCase();
const score = parseInt(c.engagement_score || "0");
const engagement = score > 70 ? "high" : score > 30 ? "medium" : "low";
const key = `${industry}|${role}|${engagement}`;
(clusters[key] ??= []).push(c);
}
// 5. Create personas for clusters with 10+
const significant = Object.entries(clusters)
.filter(([, v]) => v.length >= 10)
.sort(([, a], [, b]) => b.length - a.length)
.slice(0, 20);
console.log(`Significant clusters: ${significant.length}`);
const personas = [];
for (const [key, members] of significant) {
const [industry, role, engagement] = key.split("|");
const domains = [...new Set(
members.map(m => (m.email || "").split("@")[1]).filter(Boolean)
)].slice(0, 5);
const sources = [...new Set(
members.map(m => m.signup_source).filter(Boolean)
)].slice(0, 3);
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
const res = await fetch(`${MB}/personas`, {
method: "POST", headers: mvH,
body: JSON.stringify({
name: `SG: ${cap(role)} / ${cap(industry)} / ${cap(engagement)}`,
description: `SendGrid segment. Role: ${role}. Industry: ${industry}. Engagement: ${engagement}. N=${members.length}. Domains: ${domains.slice(0, 3).join(", ")}. Sources: ${sources.join(", ")}.`,
demographic: { job_titles: [role], industries: [industry] },
psychographic: { engagement_level: engagement, signup_sources: sources },
}),
});
const persona = await res.json();
personas.push({ cluster: key, id: persona.id, n: members.length });
console.log(` ${cap(role)} / ${cap(industry)} / ${cap(engagement)}: ${persona.id} (${members.length})`);
await new Promise(r => setTimeout(r, 300));
}
console.log(`\nCreated ${personas.length} personas from ${contacts.length} contacts`);