Cartographie Ton Marché
Prends une description de produit, des mots-clés de catégorie ou des noms de concurrents. Cherche sur Reddit, HN, GitHub Issues, G2 et Google Trends de vrais signaux de douleur. Note-les et regroupe-les. Construis un framework de positionnement complet : définition de l'ICP, thèmes de douleur classés avec citations textuelles, signaux de taille de marché et angles de message dérivés du langage réel utilisé par les gens.
Règle cruciale : Chaque citation de douleur dans la sortie doit exister textuellement dans les données brutes collectées par le script. Chaque nom de fournisseur dans la carte du marché doit provenir des résultats de scraping G2 ou des résultats de recherche GitHub. La taille du marché doit dire « les signaux suggèrent » — n'estime jamais un montant en dollars à partir de proxies légers. Si une source renvoie 0 résultats, signale 0 — ne complète pas avec des exemples inventés.
Erreurs Courantes
| L'agent voudra... | Pourquoi c'est faux |
|---|---|
| Inventer des points douloureux ou des chiffres de taille de marché | Chaque citation de douleur doit être textuelle, issue des données brutes. La taille du marché doit citer les signaux trouvés. N'estime jamais la taille « typique » d'un marché. |
| Noter par nombre de posts au lieu du pain_score | Un post avec 2 000 upvotes sur les prix est plus fort que 50 posts avec 10 upvotes chacun. Utilise la formule pain_score de references/pain-scoring.md. |
| Utiliser les mêmes subreddits pour chaque catégorie | r/politics ajoute du bruit à une recherche devops. Détecte automatiquement les subreddits pertinents à partir de la catégorie et des noms de concurrents avant de chercher. |
| Envoyer tous les signaux bruts à l'IA sans les noter | Note d'abord localement. Envoie seulement les 60 signaux avec le pain_score le plus élevé au clustering IA. Économise des tokens et améliore la qualité du regroupement. |
| Sauter l'extraction de l'ICP à partir des métadonnées des posts | Le subreddit, le flair, la bio de l'auteur (HN) et le type d'organisation GitHub sont des signaux ICP plus riches que le contenu des posts. Capture-les et signale-les toujours. |
| Confondre le nombre de fournisseurs avec la taille du marché | « 47 fournisseurs sur G2 » signifie concurrentiel, pas grand. Présente tous les signaux comme des indicateurs directionnels, pas des chiffres durs. |
Étape 1 : Vérification de l'Installation
echo "GITHUB_TOKEN: ${GITHUB_TOKEN:-non défini -- la recherche sur GitHub Issues s'exécute à 60 requêtes/heure sans authentification}"
echo "Aucune autre clé API requise."
echo ""
echo "Sources de données utilisées lors de cette exécution :"
echo " Reddit public JSON (sans auth, 10 req/min)"
echo " HN Algolia API (sans auth, gratuit)"
echo " GitHub Issues API (${GITHUB_TOKEN:+authentifié, }60-5000 req/hr)"
echo " G2 category scrape (sans auth, analyse HTML)"
echo " Google Trends (sans auth, endpoint non officiel)"
Si GITHUB_TOKEN n'est pas défini : continue. La recherche non authentifiée sur GitHub est limitée à 60 req/heure — suffisant pour une exécution standard. Pour une utilisation répétée, ajoute un token sur github.com/settings/tokens (aucun scope nécessaire pour les dépôts publics).
Étape 2 : Analyse de l'Entrée
Collecte à partir de la conversation :
category— mot(s) clé(s) décrivant l'espace du marché (par ex. « observabilité développeur », « analytics B2B », « outils devops »)competitors— liste optionnelle de noms de produits concurrents ou de domaines (par ex. « Datadog, New Relic, Grafana »)product_context— optionnel : ce que fait le produit de l'utilisateur (aide à adapter les angles de message)
Si l'utilisateur fournit seulement une description de produit sans mot-clé de catégorie : extrais-en toi-même 2-3 mots-clés de catégorie.
Si l'utilisateur fournit seulement des noms de concurrents sans catégorie : déduis la catégorie en recherchant les concurrents.
Écris l'entrée analysée :
python3 << 'PYEOF'
import json, os
data = {
"category": "CATEGORY_ICI",
"competitors": ["CONC_1", "CONC_2"],
"product_context": "CONTEXTE_PRODUIT_ICI"
}
with open("/tmp/mym-input.json", "w") as f:
json.dump(data, f, indent=2)
print("Entrée écrite dans /tmp/mym-input.json")
print(f"Catégorie : {data['category']}")
print(f"Concurrents : {', '.join(data['competitors']) if data['competitors'] else 'aucun fourni'}")
PYEOF
Étape 3 : Exécute le Script Autonome de Collecte de Données
Le script gère toute la collecte de données. Vérifie d'abord s'il existe :
ls scripts/fetch.py 2>/dev/null && echo "script disponible" || echo "non trouvé"
S'il est disponible, exécute-le :
GITHUB_TOKEN="${GITHUB_TOKEN:-}" python3 scripts/fetch.py \
"$(python3 -c "import json; d=json.load(open('/tmp/mym-input.json')); print(d['category'])")" \
--competitors "$(python3 -c "import json; d=json.load(open('/tmp/mym-input.json')); print(','.join(d['competitors']))")" \
--context "$(python3 -c "import json; d=json.load(open('/tmp/mym-input.json')); print(d['product_context'])")" \
--output /tmp/mym-raw.json
Attends la fin (autorise jusqu'à 4 minutes — les recherches Reddit + HN prennent environ 90 secondes au total).
Vérifie la sortie :
python3 -c "
import json
with open('/tmp/mym-raw.json') as f:
d = json.load(f)
print(f'Signaux Reddit : {d[\"market_signals\"][\"reddit_signals_found\"]}')
print(f'Signaux HN : {d[\"market_signals\"][\"hn_signals_found\"]}')
print(f'Signaux GitHub : {d[\"market_signals\"][\"github_issue_signals\"]}')
print(f'Fournisseurs G2 : {d[\"market_signals\"][\"vendor_count_g2\"]}')
print(f'Tendances : {d[\"market_signals\"][\"trends_direction\"]}')
print(f'Signaux totaux : {d[\"summary\"][\"total_pain_signals\"]}')
"
Si les signaux totaux < 10 : arrête. Dis à l'utilisateur : « Moins de 10 signaux de douleur trouvés pour cette catégorie. Le marché est peut-être trop niche pour une couverture Reddit/HN, ou les mots-clés de catégorie doivent être ajustés. Essaie des mots-clés plus larges ou ajoute des noms de concurrents. »
Étape 4 : Regroupement IA des Douleurs
Imprime les 60 meilleurs signaux de douleur pour l'analyse IA :
python3 -c "
import json
with open('/tmp/mym-raw.json') as f:
d = json.load(f)
top60 = sorted(d['raw_pains'], key=lambda x: x['pain_score'], reverse=True)[:60]
print(json.dumps(top60, indent=2))
"
Tu as maintenant les 60 meilleurs signaux de douleur. Analyse-les et produis des groupes de douleurs.
Instructions pour l'analyse IA :
- Identifie 5-7 thèmes de douleur récurrents à travers toutes les sources
- Pour chaque thème : choisis un nom qui utilise le langage du marché (pas tes propres mots)
- Agrège le pain_score de tous les signaux dans chaque groupe
- Sélectionne les 3-5 meilleures citations textuelles pour chaque thème (score le plus élevé, langage le plus spécifique)
- Note dans quelles sources et subreddits chaque thème se concentre
- Signale tout thème qui n'apparaît que dans une seule source (confiance moindre)
Écris les groupes dans /tmp/mym-clusters.json :
{
"clusters": [
{
"theme": "langage exact issu des données",
"total_score": 847,
"signal_count": 34,
"sources": {"reddit": 18, "hn": 12, "github_issue": 4},
"top_subreddits": ["devops", "sysadmin"],
"verbatim_quotes": [
{"text": "citation exacte", "source": "reddit", "score": 234, "url": "..."},
{"text": "citation exacte", "source": "hn", "score": 87, "url": "..."}
],
"who_has_this_pain": "description de qui publie à ce sujet"
}
]
}
python3 -c "
import json, os
# Confirme que le fichier de clusters a été écrit
with open('/tmp/mym-clusters.json') as f:
d = json.load(f)
print(f'Groupes écrits : {len(d[\"clusters\"])}')
for c in d['clusters']:
print(f' {c[\"theme\"]} -- score : {c[\"total_score\"]}, signaux : {c[\"signal_count\"]}')
"
Étape 5 : Profilage ICP
Imprime les signaux ICP provenant des données brutes :
python3 -c "
import json
with open('/tmp/mym-raw.json') as f:
d = json.load(f)
print('Signaux ICP :')
print(json.dumps(d['icp_signals'], indent=2))
print()
print('Distribution des subreddits :')
sub_counts = {}
for p in d['raw_pains']:
s = p.get('subreddit', '')
if s:
sub_counts[s] = sub_counts.get(s, 0) + 1
for sub, count in sorted(sub_counts.items(), key=lambda x: -x[1])[:10]:
print(f' r/{sub} : {count} signaux')
"
En utilisant les signaux ICP et la distribution des subreddits ci-dessus, synthétise le profil ICP. Écris-le dans /tmp/mym-clusters.json en ajoutant une clé icp :
{
"icp": {
"who_they_are": "Profil de 2-3 phrases utilisant le langage des données",
"where_they_live": ["r/devops (89 posts)", "r/sysadmin (67 posts)", "HN ask-hn (34 fils)"],
"what_they_say": ["citation textuelle 1", "citation textuelle 2", "citation textuelle 3"],
"what_they_have_tried": ["outils ou approches alternatives mentionnés dans les données"],
"confidence": "high|medium|low -- basé sur le volume de signaux et la diversité des sources"
}
}
Étape 6 : Synthèse de la Taille du Marché
Imprime les signaux du marché :
python3 -c "
import json
with open('/tmp/mym-raw.json') as f:
d = json.load(f)
ms = d['market_signals']
print('Signaux du marché :')
print(f' Fournisseurs G2 : {ms[\"vendor_count_g2\"]}')
print(f' Direction des tendances : {ms[\"trends_direction\"]}')
print(f' Signaux HN (12 mois) : {ms[\"hn_signals_found\"]}')
print(f' Signaux Reddit : {ms[\"reddit_signals_found\"]}')
print(f' Top fournisseurs G2 : {json.dumps(ms.get(\"top_vendors\", []), indent=4)}')
"
Synthétise une évaluation directionnelle de la taille du marché en utilisant uniquement ces signaux. N'estime pas un montant en dollars. Utilise un langage comme :
- « Les signaux suggèrent un marché concurrentiel et en croissance » (nombreux fournisseurs + tendances à la hausse)
- « Les signaux suggèrent un marché émergent » (peu de fournisseurs + faible volume de signaux)
- « Les signaux suggèrent un marché saturé » (nombreux fournisseurs + tendances plates ou à la baisse)
Ajoute l'évaluation à /tmp/mym-clusters.json comme une clé market_size.
Étape 7 : Framework de Positionnement
En utilisant les groupes (Étape 4), l'ICP (Étape 5) et la taille du marché (Étape 6), génère le framework de positionnement.
Instructions :
- Choisis les 3 principaux groupes de douleur comme angles de positionnement primaires
- Pour chaque angle : écris une déclaration de positionnement en utilisant le langage textuel des données (pas paraphrasé)
- Génère 3 titres de page d'accueil qui utilisent les phrases exactes que les gens utilisent dans les données de douleur
- Génère 3 lignes d'objet d'email froid basées sur le langage de la douleur
- N'utilise PAS de mots interdits : puissant, robuste, transparent, innovant, révolutionnaire, rationaliser, exploiter, transformer, révolutionner
Écris le framework de positionnement complet dans /tmp/mym-output.json :
{
"positioning_angles": [
{
"pain": "nom du thème",
"statement": "positionnement en une ligne utilisant le langage du marché",
"headline": "titre de page d'accueil utilisant le langage de douleur textuel",
"cold_email_subject": "ligne d'objet"
}
],
"icp_card": {
"one_liner": "une phrase : qui ils sont + ce qui les intéresse",
"where_to_find_them": [...],
"how_to_talk_to_them": "notes sur le ton et le vocabulaire issues des données"
},
"market_map": [...top fournisseurs de G2 avec notes de positionnement...]
}
Étape 8 : Auto-Qualité et Sauvegarde de la Sortie
Exécute les contrôles d'auto-qualité :
python3 -c "
import json
# Charge toutes les sorties
with open('/tmp/mym-raw.json') as f:
raw = json.load(f)
with open('/tmp/mym-clusters.json') as f:
clusters = json.load(f)
with open('/tmp/mym-output.json') as f:
output = json.load(f)
raw_texts = set()
for p in raw['raw_pains']:
raw_texts.add(p.get('title', ''))
raw_texts.add(p.get('body_excerpt', ''))
# Contrôle 1 : Pas de tirets cadratins
import json as j
full_text = j.dumps(output)
if '—' in full_text:
print('ÉCHEC : tiret cadratin trouvé dans la sortie')
else:
print('RÉUSSI : pas de tirets cadratins')
# Contrôle 2 : Pas de mots interdits
banned = ['puissant', 'robuste', 'transparent', 'innovant', 'révolutionnaire',
'rationaliser', 'exploiter', 'transformer', 'révolutionner']
found = [w for w in banned if w.lower() in full_text.lower()]
if found:
print(f'ÉCHEC : mots interdits trouvés : {found}')
else:
print('RÉUSSI : pas de mots interdits')
# Contrôle 3 : Vérification du langage de taille de marché
if 'billion' in full_text.lower() or 'trillion' in full_text.lower() or 'worth \$' in full_text.lower():
print('ÉCHEC : estimation dure de taille de marché trouvée -- utilise un langage directionnel uniquement')
else:
print('RÉUSSI : pas d\'estimations dures de taille de marché')
# Contrôle 4 : Correspondance des comptes de signaux
total = raw['summary']['total_pain_signals']
print(f'RÉUSSI : {total} signaux de douleur totaux dans les données brutes')
print('Auto-qualité terminée.')
"
Corrige tout échec avant de sauvegarder.
Sauvegarde le rapport final :
python3 << 'PYEOF'
import json, re
from datetime import datetime
with open('/tmp/mym-input.json') as f:
inp = json.load(f)
with open('/tmp/mym-raw.json') as f:
raw = json.load(f)
with open('/tmp/mym-clusters.json') as f:
clusters = json.load(f)
with open('/tmp/mym-output.json') as f:
output = json.load(f)
slug = re.sub(r'[^a-z0-9]+', '-', inp['category'].lower()).strip('-')
date = datetime.now().strftime('%Y-%m-%d')
outpath_md = f"docs/market-maps/{slug}-{date}.md"
outpath_json = f"docs/market-maps/{slug}-{date}.json"
# Construire le rapport markdown
ms = raw['market_signals']
icp = clusters.get('icp', {})
market_assessment = clusters.get('market_size', {})
angles = output.get('positioning_angles', [])
icp_card = output.get('icp_card', {})
market_map = output.get('market_map', [])
lines = [
f"# Carte du Marché : {inp['category'].title()}",
f"Date : {date} | Signaux analysés : {raw['summary']['total_pain_signals']} | Sources : Reddit ({ms['reddit_signals_found']}) + HN ({ms['hn_signals_found']}) + GitHub Issues ({ms['github_issue_signals']})",
"",
"---",
"",
"## Signaux de Taille du Marché",
f"Fournisseurs sur G2 : {ms['vendor_count_g2']} | Google Trends : {ms['trends_direction'].upper()} | Stade du marché : {market_assessment.get('stage', 'voir signaux ci-dessous')}",
"",
market_assessment.get('summary', ''),
"",
"---",
"",
"## Votre ICP",
"",
f"**Qui ils sont :** {icp.get('who_they_are', '')}",
"",
f"**Où ils se trouvent :** {', '.join(icp.get('where_they_live', []))}",
"",
"**Ce qu'ils disent :**",
]
for q in icp.get('what_they_say', []):
lines.append(f'> "{q}"')
lines += ["", "---", "", "## Principales Douleurs (classées par force du signal)", ""]
for i, c in enumerate(clusters.get('clusters', []), 1):
lines.append(f"### Douleur {i} : {c['theme']} [score : {c['total_score']}]")
sources = c.get('sources', {})
source_str = " + ".join(f"{src} ({cnt})" for src, cnt in sources.items())
lines.append(f"{c['signal_count']} signaux | Sources : {source_str}")
lines.append(f"Qui a cette douleur : {c.get('who_has_this_pain', '')}")
lines.append("")
lines.append("Textuel :")
for q in c.get('verbatim_quotes', [])[:4]:
lines.append(f"- \"{q['text']}\" (de {q['source']}, score {q['score']})")
lines.append("")
lines += ["---", "", "## Angles de Positionnement", ""]
for a in angles:
lines.append(f"### Douleur ciblée : {a['pain']}")
lines.append(f"**Déclaration :** {a['statement']}")
lines.append(f"**Titre page d'accueil :** {a['headline']}")
lines.append(f"**Ligne d'objet email froid :** {a['cold_email_subject']}")
lines.append("")
lines += ["---", "", "## Carte des Concurrents", ""]
lines.append("| Fournisseur | Note de positionnement |")
lines.append("|---|---|")
for v in market_map:
name = v.get('name', v.get('vendor', ''))
note = v.get('positioning_note', v.get('note', ''))
lines.append(f"| {name} | {note} |")
report_md = '\n'.join(lines)
os.makedirs(os.path.dirname(outpath_md), exist_ok=True)
with open(outpath_md, 'w') as f:
f.write(report_md)
with open(outpath_json, 'w') as f:
json.dump(output, f, indent=2, ensure_ascii=False)
print(f"Rapport sauvegardé dans : {outpath_md}")
print(f"Données JSON sauvegardées dans : {outpath_json}")
PYEOF
Terminé.