Scenario
Your team maintains competitive intelligence in a Notion wiki — feature comparisons, pricing snapshots, win/loss notes, and market positioning. This data is goldmine for sales but trapped in static pages. This job pulls competitive pages, enriches them through Mave Agent’s research capabilities, then generates structured battle cards with the Generate endpoint. The result is always-fresh, AI-enriched battle cards that combine your internal intel with public information. Flow: NotionPOST /databases/{id}/query (competitive wiki) → Extract pages → Mavera POST /api/v1/mave/chat (enrich each competitor) → POST /api/v1/generations → Battle card documents
Code
import os, requests, time
NOTION = os.environ["NOTION_API_KEY"]
MV = os.environ["MAVERA_API_KEY"]
NB = "https://api.notion.com/v1"
MB = "https://app.mavera.io/api/v1"
NH = {
"Authorization": f"Bearer {NOTION}",
"Notion-Version": "2022-06-28",
"Content-Type": "application/json",
}
MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
COMPETITIVE_DB_ID = "your-competitive-wiki-database-id"
# 1. Query competitive wiki pages
competitors = requests.post(f"{NB}/databases/{COMPETITIVE_DB_ID}/query", headers=NH, json={
"sorts": [{"property": "Last edited time", "direction": "descending"}],
"page_size": 10,
}).json().get("results", [])
print(f"Found {len(competitors)} competitor pages")
# 2. Extract text from each page
def get_blocks_text(block_id):
texts = []
cursor = None
while True:
params = {"page_size": 100}
if cursor:
params["start_cursor"] = cursor
r = requests.get(f"{NB}/blocks/{block_id}/children", headers=NH, params=params)
if r.status_code == 429:
time.sleep(1); continue
data = r.json()
for block in data.get("results", []):
btype = block.get("type", "")
rt = block.get(btype, {}).get("rich_text", [])
text = "".join(t.get("plain_text", "") for t in rt)
if text.strip():
texts.append(text)
cursor = data.get("next_cursor")
if not cursor:
break
time.sleep(0.4)
return "\n".join(texts)
battle_cards = []
for page in competitors:
props = page.get("properties", {})
title_parts = props.get("Name", props.get("Title", props.get("Competitor", {}))).get("title", [])
comp_name = "".join(t.get("plain_text", "") for t in title_parts) or "Unknown Competitor"
category = (props.get("Category", {}).get("select", {}) or {}).get("name", "")
website = (props.get("Website", {}).get("url", None)) or ""
internal_intel = get_blocks_text(page["id"])
print(f" Extracting: {comp_name} ({len(internal_intel)} chars)")
# 3. Enrich with Mave Agent (public research)
enrichment = requests.post(f"{MB}/mave/chat", headers=MH, json={
"message": (
f"Research the company '{comp_name}'"
f"{' (' + website + ')' if website else ''}. "
f"Focus on: recent product launches, pricing changes, funding rounds, "
f"key hires, market positioning, and notable customer wins or losses. "
f"Provide cited facts with dates where possible."
),
}).json()
mave_research = enrichment.get("content", "")
sources = enrichment.get("sources", [])
# 4. Generate battle card combining internal intel + Mave research
gen = requests.post(f"{MB}/generations", headers=MH, json={
"prompt": (
f"Create a sales battle card for competing against '{comp_name}'.\n\n"
f"INTERNAL INTELLIGENCE (from our wiki):\n{internal_intel[:4000]}\n\n"
f"MARKET RESEARCH:\n{mave_research[:4000]}\n\n"
"Structure the battle card with these sections:\n"
"1. **Overview** — Company summary, funding, employee count\n"
"2. **Their Positioning** — How they describe themselves\n"
"3. **Where They Win** — Their genuine strengths\n"
"4. **Where We Win** — Our advantages backed by evidence\n"
"5. **Common Objections** — What prospects say, with rebuttals\n"
"6. **Killer Questions** — Questions reps should ask that expose weaknesses\n"
"7. **Landmine Questions** — Questions to watch for that favor them\n"
"8. **Quick Pricing Comparison** — If pricing data is available\n"
"9. **Recent Customer Movements** — Wins/losses from either side\n\n"
"Be specific and actionable. This goes directly to sales reps."
),
}).json()
card_content = gen.get("output", gen.get("content", ""))
battle_cards.append({
"competitor": comp_name,
"card": card_content,
"sources": len(sources),
})
# 5. Optionally write back to Notion as a child page or block
blocks = []
for paragraph in card_content.split("\n\n")[:50]:
p = paragraph.strip()
if not p:
continue
if p.startswith("## ") or p.startswith("**") and p.endswith("**"):
blocks.append({"object": "block", "type": "heading_2",
"heading_2": {"rich_text": [{"type": "text", "text": {"content": p.strip("#* ")}}]}})
else:
blocks.append({"object": "block", "type": "paragraph",
"paragraph": {"rich_text": [{"type": "text", "text": {"content": p[:2000]}}]}})
if blocks:
bc_page = requests.post(f"{NB}/pages", headers=NH, json={
"parent": {"database_id": COMPETITIVE_DB_ID},
"properties": {
"Name": {"title": [{"text": {"content": f"Battle Card: {comp_name}"}}]},
},
"children": blocks[:100],
})
if bc_page.ok:
print(f" → Battle card page created in Notion")
print(f" ✓ {comp_name} — {len(card_content)} chars, {len(sources)} sources")
time.sleep(1)
print(f"\nGenerated {len(battle_cards)} battle cards")
const NOTION = process.env.NOTION_API_KEY;
const MV = process.env.MAVERA_API_KEY;
const NB = "https://api.notion.com/v1";
const MB = "https://app.mavera.io/api/v1";
const NH = {
Authorization: `Bearer ${NOTION}`,
"Notion-Version": "2022-06-28",
"Content-Type": "application/json",
};
const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
const COMPETITIVE_DB_ID = "your-competitive-wiki-database-id";
// 1. Query competitor pages
const competitors = await fetch(`${NB}/databases/${COMPETITIVE_DB_ID}/query`, {
method: "POST", headers: NH,
body: JSON.stringify({
sorts: [{ property: "Last edited time", direction: "descending" }],
page_size: 10,
}),
}).then(r => r.json()).then(d => d.results || []);
console.log(`Found ${competitors.length} competitor pages`);
// 2. Extract text
async function getBlocksText(blockId) {
const texts = [];
let cursor = null;
do {
const params = new URLSearchParams({ page_size: "100" });
if (cursor) params.set("start_cursor", cursor);
const res = await fetch(`${NB}/blocks/${blockId}/children?${params}`, { headers: NH });
if (res.status === 429) { await new Promise(r => setTimeout(r, 1000)); continue; }
const data = await res.json();
for (const block of data.results || []) {
const text = (block[block.type]?.rich_text || []).map(t => t.plain_text).join("");
if (text.trim()) texts.push(text);
}
cursor = data.next_cursor;
await new Promise(r => setTimeout(r, 400));
} while (cursor);
return texts.join("\n");
}
const battleCards = [];
for (const page of competitors) {
const props = page.properties || {};
const titleProp = props.Name || props.Title || props.Competitor || {};
const compName = (titleProp.title || []).map(t => t.plain_text).join("") || "Unknown";
const website = props.Website?.url || "";
const internalIntel = await getBlocksText(page.id);
console.log(` Extracting: ${compName} (${internalIntel.length} chars)`);
// 3. Enrich with Mave
const enrichment = await fetch(`${MB}/mave/chat`, { method: "POST", headers: MH,
body: JSON.stringify({
message: `Research '${compName}'${website ? ` (${website})` : ""}. Recent launches, pricing, funding, hires, positioning, customer wins/losses. Cited facts.`,
}),
}).then(r => r.json());
const maveResearch = enrichment.content || "";
const sources = enrichment.sources || [];
// 4. Generate battle card
const gen = await fetch(`${MB}/generations`, { method: "POST", headers: MH,
body: JSON.stringify({
prompt: `Battle card for '${compName}'.\n\nINTERNAL:\n${internalIntel.slice(0, 4000)}\n\nRESEARCH:\n${maveResearch.slice(0, 4000)}\n\nSections: Overview, Their Positioning, Where They Win, Where We Win, Objections + Rebuttals, Killer Questions, Landmine Questions, Pricing, Customer Movements. Specific and actionable.`,
}),
}).then(r => r.json());
const cardContent = gen.output || gen.content || "";
battleCards.push({ competitor: compName, card: cardContent, sources: sources.length });
// 5. Write back to Notion
const blocks = [];
for (const paragraph of cardContent.split("\n\n").slice(0, 50)) {
const p = paragraph.trim();
if (!p) continue;
if (p.startsWith("## ") || (p.startsWith("**") && p.endsWith("**"))) {
blocks.push({ object: "block", type: "heading_2",
heading_2: { rich_text: [{ type: "text", text: { content: p.replace(/^[#* ]+|[*]+$/g, "") } }] } });
} else {
blocks.push({ object: "block", type: "paragraph",
paragraph: { rich_text: [{ type: "text", text: { content: p.slice(0, 2000) } }] } });
}
}
if (blocks.length) {
await fetch(`${NB}/pages`, { method: "POST", headers: NH,
body: JSON.stringify({
parent: { database_id: COMPETITIVE_DB_ID },
properties: { Name: { title: [{ text: { content: `Battle Card: ${compName}` } }] } },
children: blocks.slice(0, 100),
}),
});
console.log(` → Battle card page created`);
}
console.log(` ✓ ${compName} — ${cardContent.length} chars, ${sources.length} sources`);
await new Promise(r => setTimeout(r, 1000));
}
console.log(`\nGenerated ${battleCards.length} battle cards`);
Example Output
Found 4 competitor pages
Extracting: CompetitorX (2847 chars)
→ Battle card page created in Notion
✓ CompetitorX — 3421 chars, 6 sources
BATTLE CARD: CompetitorX
============================================================
## Overview
Series C startup ($45M raised, Jan 2026). ~180 employees.
Positioning: "AI-powered revenue intelligence."
## Where They Win
- Lower price point ($79/seat vs our $149)
- Strong Slack community (12K members)
- Faster self-serve onboarding (< 10 minutes)
## Where We Win
- No SSO/SCIM below Enterprise tier (dealbreaker for security teams)
- Our API supports webhooks; theirs is polling-only
- Dedicated CSM at $25K+ ACV (theirs: $75K+)
- SOC 2 Type II + HIPAA (they only have SOC 2 Type I)
## Killer Questions
1. "How important is SSO to your InfoSec team?"
2. "Do you need real-time data or is polling every 15 min acceptable?"
3. "What's your ACV threshold for getting a dedicated CSM?"
## Landmine Questions (Watch For)
1. "What's your per-seat price?" (they're cheaper — reframe to total cost of ownership)
2. "How quickly can we get started?" (their self-serve is faster)
## Recent Customer Movements
- We won 3 accounts from CompetitorX in Q1 (all cited SSO gaps)
- Lost 1 SMB deal to CompetitorX on price (sub-$15K ACV)
Error Handling
Competitor page schema varies
Competitor page schema varies
Competitive wikis have inconsistent schemas. The code tries
Name, Title, and Competitor properties. If your title property has a different name, adjust the extraction logic.Mave research freshness
Mave research freshness
Mave Agent’s research reflects its latest available data. For time-sensitive competitive intelligence (e.g., a pricing change announced today), supplement with the Notion wiki’s internal observations.
Creating pages in the database
Creating pages in the database
Writing battle card pages back to Notion requires the database schema to support the properties you’re setting. If your competitive wiki has required properties beyond
Name, the create call will fail with a 400 error.Rate limit budget
Rate limit budget
This job makes 3 API calls per competitor (Notion read + Mave + Generate + optional Notion write). For 10 competitors, that’s ~30+ calls. The 1-second delay between competitors keeps you within Notion’s 3 req/sec limit.