关于 map-your-market
面向产品团队和营销人员的市场研究技能,从 Reddit、HN、GitHub Issues、G2 和 Google Trends 收集痛点信号。它对证据进行评分和聚类,构建 ICP 与定位框架,并要求引用和市场判断均依据实际采集数据。
主要功能
- 01从五个指定来源收集信号
- 02为痛点信号评分并聚类
- 03依据元数据构建 ICP 画像
- 04生成基于证据的定位角度
- 05报告方向性市场信号,不估算金额
适用场景
- 01研究某个产品类别的客户痛点
- 02比较竞争对手与市场信号
- 03依据客户用语制定产品定位
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ varnan-tech/ opendirectory --skill map-your-market安装后在 Claude Code 中输入/map-your-market确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.env.exampleREADME.mdevals/evals.jsonreferences/icp-signals.mdpain-scoring.mdsubreddit-map.mdscripts/fetch.py详细介绍
绘制你的市场
输入产品描述、类别关键词或竞争对手名称。搜索 Reddit、HN、GitHub Issues、G2 和 Google Trends,寻找真实的痛点信号。为其评分并聚类。构建完整的定位框架:ICP 定义、按排名排列的痛点主题及原文引述、市场规模信号,以及根据人们实际使用的语言提炼的宣传角度。
**关键规则:**输出中的每条痛点引述都必须与脚本收集的原始数据完全一致。市场地图中的每个供应商名称都必须来自 G2 抓取结果或 GitHub 搜索结果。描述市场规模时必须说“信号表明”——绝不能根据薄弱的代理指标估算美元金额。如果某个来源返回 0 条结果,就报告 0——不要补充虚构的示例。
常见错误
| Agent 会想要…… | 为什么这是错的 |
|---|---|
| 编造痛点或市场规模数字 | 每条痛点引述都必须与原始数据完全一致。市场规模必须引用发现的信号。绝不要估算“典型”市场规模。 |
| 按帖子数量而不是 pain_score 评分 | 一条获得 2,000 个赞、讨论定价的帖子,比 50 条各获得 10 个赞的帖子更有说服力。使用 references/pain-scoring.md 中的 pain_score 公式。 |
| 对每个类别都使用相同的 subreddit | 在 devops 搜索中,r/politics 会带来噪声。搜索前,根据类别和竞争对手名称自动检测相关 subreddit。 |
| 不评分就把所有原始信号都发给 AI | 先在本地评分。只将 pain_score 最高的 60 条信号发送给 AI 聚类。这样可以节省令牌并提升聚类质量。 |
| 跳过从帖子元数据中提取 ICP | Subreddit、flair、作者简介(HN)和 GitHub 组织类型,比帖子内容能提供更丰富的 ICP 信号。务必采集并报告这些信息。 |
| 混淆供应商数量与市场规模 | “G2 上有 47 家供应商”意味着竞争激烈,而不是市场规模大。所有信号都应作为方向性指标呈现,而非精确数字。 |
第 1 步:检查设置
echo "GITHUB_TOKEN: ${GITHUB_TOKEN:-not set -- GitHub Issues search runs at 60 req/hr unauthenticated}"
echo "No other API keys required."
echo ""
echo "Data sources this run will use:"
echo " Reddit public JSON (no auth, 10 req/min)"
echo " HN Algolia API (no auth, free)"
echo " GitHub Issues API (${GITHUB_TOKEN:+authenticated, }60-5000 req/hr)"
echo " G2 category scrape (no auth, HTML parse)"
echo " Google Trends (no auth, unofficial endpoint)"
如果未设置 GITHUB_TOKEN:继续执行。未经身份验证的 GitHub 搜索每小时可请求 60 次,足以应付常规运行。若要重复使用,请在 github.com/settings/tokens 添加令牌(公共仓库无需任何权限范围)。
第 2 步:解析输入
从对话中收集:
category-- 描述市场领域的关键词(例如“开发者可观测性”“B2B 分析”“devops 工具”)competitors-- 可选的竞争对手产品名称或域名列表(例如“Datadog、New Relic、Grafana”)product_context-- 可选:用户的产品用途(有助于定制宣传角度)
如果用户只提供了产品描述,没有类别关键词:自行从描述中提取 2-3 个类别关键词。
如果用户只提供了竞争对手名称,没有类别:查找竞争对手并推断类别。
写入解析后的输入:
python3 << 'PYEOF'
import json, os
data = {
"category": "CATEGORY_HERE",
"competitors": ["COMP_1", "COMP_2"],
"product_context": "PRODUCT_CONTEXT_HERE"
}
with open("/tmp/mym-input.json", "w") as f:
json.dump(data, f, indent=2)
print("Input written to /tmp/mym-input.json")
print(f"Category: {data['category']}")
print(f"Competitors: {', '.join(data['competitors']) if data['competitors'] else 'none provided'}")
PYEOF
第 3 步:运行独立数据采集脚本
该脚本负责采集所有数据。首先检查脚本是否存在:
ls scripts/fetch.py 2>/dev/null && echo "script available" || echo "not found"
如果可用,则运行:
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
等待运行完成(最多等待 4 分钟——Reddit + HN 搜索总共需要约 90 秒)。
验证输出:
python3 -c "
import json
with open('/tmp/mym-raw.json') as f:
d = json.load(f)
print(f'Reddit signals: {d[\"market_signals\"][\"reddit_signals_found\"]}')
print(f'HN signals: {d[\"market_signals\"][\"hn_signals_found\"]}')
print(f'GitHub signals: {d[\"market_signals\"][\"github_issue_signals\"]}')
print(f'G2 vendors: {d[\"market_signals\"][\"vendor_count_g2\"]}')
print(f'Trends: {d[\"market_signals\"][\"trends_direction\"]}')
print(f'Total signals: {d[\"summary\"][\"total_pain_signals\"]}')
"
如果信号总数少于 10:停止。告知用户:“为此类别找到的痛点信号少于 10 条。该市场可能过于小众,Reddit/HN 上的相关内容不足,也可能需要调整类别关键词。请尝试更宽泛的关键词,或添加竞争对手名称。”
步骤 4:AI 痛点聚类
打印用于 AI 分析的前 60 条痛点信号:
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))
"
现在你已有前 60 条痛点信号。请分析这些信号并生成痛点聚类。
AI 分析说明:
- 在所有来源中识别 5-7 个反复出现的痛点主题
- 为每个主题选取一个符合市场自身用语的名称(而非你自己的措辞)
- 汇总每个聚类中所有信号的 pain_score
- 为每个主题选出 3-5 条最合适的原文引述(分数最高、措辞最具体)
- 标明每个主题主要集中在哪些来源和 subreddit
- 标记仅出现在单一来源中的主题(置信度较低)
将聚类写入 /tmp/mym-clusters.json:
{
"clusters": [
{
"theme": "exact language from the data",
"total_score": 847,
"signal_count": 34,
"sources": {"reddit": 18, "hn": 12, "github_issue": 4},
"top_subreddits": ["devops", "sysadmin"],
"verbatim_quotes": [
{"text": "exact quote", "source": "reddit", "score": 234, "url": "..."},
{"text": "exact quote", "source": "hn", "score": 87, "url": "..."}
],
"who_has_this_pain": "description of who is posting about this"
}
]
}
python3 -c "
import json, os
# Confirm clusters file was written
with open('/tmp/mym-clusters.json') as f:
d = json.load(f)
print(f'Clusters written: {len(d[\"clusters\"])}')
for c in d['clusters']:
print(f' {c[\"theme\"]} -- score: {c[\"total_score\"]}, signals: {c[\"signal_count\"]}')
"
步骤 5:ICP 画像
打印原始数据中的 ICP 信号:
python3 -c "
import json
with open('/tmp/mym-raw.json') as f:
d = json.load(f)
print('ICP signals:')
print(json.dumps(d['icp_signals'], indent=2))
print()
print('Subreddit distribution:')
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} signals')
"
根据上述 ICP 信号和 subreddit 分布,综合生成 ICP 画像。通过添加 icp 键,将其写入 /tmp/mym-clusters.json:
{
"icp": {
"who_they_are": "2-3 sentence profile using language from the data",
"where_they_live": ["r/devops (89 posts)", "r/sysadmin (67 posts)", "HN ask-hn (34 threads)"],
"what_they_say": ["verbatim quote 1", "verbatim quote 2", "verbatim quote 3"],
"what_they_have_tried": ["alternative tools or approaches mentioned in the data"],
"confidence": "high|medium|low -- based on signal volume and source diversity"
}
}
步骤 6:市场规模综合评估
打印市场信号:
python3 -c "
import json
with open('/tmp/mym-raw.json') as f:
d = json.load(f)
ms = d['market_signals']
print('Market signals:')
print(f' G2 vendors: {ms[\"vendor_count_g2\"]}')
print(f' Trends direction: {ms[\"trends_direction\"]}')
print(f' HN signals (12mo): {ms[\"hn_signals_found\"]}')
print(f' Reddit signals: {ms[\"reddit_signals_found\"]}')
print(f' G2 top vendors: {json.dumps(ms.get(\"top_vendors\", []), indent=4)}')
"
仅使用这些信号,对市场规模进行方向性评估。不要估算美元金额。使用类似以下的表述:
- “信号显示市场竞争激烈且正在增长”(供应商众多 + 趋势向上)
- “信号显示市场处于早期阶段”(供应商较少 + 信号量较低)
- “信号显示市场已趋于饱和”(供应商众多 + 趋势持平或向下)
将评估结果作为 market_size 键添加到 /tmp/mym-clusters.json。
步骤 7:定位框架
根据聚类(步骤 4)、ICP(步骤 5)和市场规模(步骤 6),生成定位框架。
说明:
- 选出排名前三的痛点聚类,作为主要定位角度
- 为每个角度撰写一条定位陈述,使用数据中的原文措辞(而非改写)
- 生成 3 条落地页标题,使用人们在痛点数据中使用的确切表述
- 根据痛点措辞生成 3 条冷邮件主题行
- 不要使用禁用词:powerful、robust、seamless、innovative、game-changing、streamline、leverage、transform、revolutionize
将完整的定位框架写入 /tmp/mym-output.json:
{
"positioning_angles": [
{
"pain": "theme name",
"statement": "one-line positioning using market language",
"headline": "landing page headline using verbatim pain language",
"cold_email_subject": "subject line"
}
],
"icp_card": {
"one_liner": "one sentence: who they are + what they care about",
"where_to_find_them": [...],
"how_to_talk_to_them": "tone + vocabulary notes from the data"
},
"market_map": [...top vendors from G2 with positioning notes...]
}
步骤 8:自检并保存输出
运行自检:
python3 -c "
import json
# Load all outputs
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', ''))
# Check 1: No em dashes
import json as j
full_text = j.dumps(output)
if '—' in full_text:
print('FAIL: em dash found in output')
else:
print('PASS: no em dashes')
# Check 2: No banned words
banned = ['powerful', 'robust', 'seamless', 'innovative', 'game-changing',
'streamline', 'leverage', 'transform', 'revolutionize']
found = [w for w in banned if w.lower() in full_text.lower()]
if found:
print(f'FAIL: banned words found: {found}')
else:
print('PASS: no banned words')
# Check 3: Market size language check
if 'billion' in full_text.lower() or 'trillion' in full_text.lower() or 'worth \$' in full_text.lower():
print('FAIL: hard market size estimate found -- use directional language only')
else:
print('PASS: no hard market size estimates')
# Check 4: Signal counts match
total = raw['summary']['total_pain_signals']
print(f'PASS: {total} total pain signals in raw data')
print('Self-QA complete.')
"
修复所有未通过的检查后再保存。
保存最终报告:
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"
# Build markdown report
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"# Market Map: {inp['category'].title()}",
f"Date: {date} | Signals analyzed: {raw['summary']['total_pain_signals']} | Sources: Reddit ({ms['reddit_signals_found']}) + HN ({ms['hn_signals_found']}) + GitHub Issues ({ms['github_issue_signals']})",
"",
"---",
"",
"## Market Size Signals",
f"Vendors on G2: {ms['vendor_count_g2']} | Google Trends: {ms['trends_direction'].upper()} | Market stage: {market_assessment.get('stage', 'see signals below')}",
"",
market_assessment.get('summary', ''),
"",
"---",
"",
"## Your ICP",
"",
f"**Who they are:** {icp.get('who_they_are', '')}",
"",
f"**Where they live:** {', '.join(icp.get('where_they_live', []))}",
"",
"**What they say:**",
]
for q in icp.get('what_they_say', []):
lines.append(f'> "{q}"')
lines += ["", "---", "", "## Top Pains (ranked by signal strength)", ""]
for i, c in enumerate(clusters.get('clusters', []), 1):
lines.append(f"### Pain {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']} signals | Sources: {source_str}")
lines.append(f"Who has this pain: {c.get('who_has_this_pain', '')}")
lines.append("")
lines.append("Verbatim:")
for q in c.get('verbatim_quotes', [])[:4]:
lines.append(f'> "{q[\"text\"]}" ({q["source"]}, score: {q["score"]})')
lines.append("")
lines += ["---", "", "## Market Map (Key Players)", ""]
if market_map:
lines.append("| Vendor | Positioning |")
lines.append("|---|---|")
for v in market_map:
lines.append(f"| {v.get('name','')} | {v.get('positioning','')} |")
else:
top = ms.get('top_vendors', [])
if top:
lines.append("| Vendor | G2 Reviews | Rating |")
lines.append("|---|---|---|")
for v in top:
lines.append(f"| {v.get('name','')} | {v.get('review_count','')} | {v.get('rating','')} |")
lines += ["", "---", "", "## Messaging Framework", ""]
for a in angles:
lines.append(f"**{a['pain']}:** {a['statement']}")
lines.append(f"Headline: \"{a['headline']}\"")
lines.append(f"Cold email subject: \"{a['cold_email_subject']}\"")
lines.append("")
lines += ["---", "", "## ICP Card", "",
f"**One liner:** {icp_card.get('one_liner', '')}",
"",
f"**Find them at:** {', '.join(icp_card.get('where_to_find_them', []))}",
"",
f"**How to talk to them:** {icp_card.get('how_to_talk_to_them', '')}",
"",
"---",
"",
"## Data Quality Notes",
f"- All pain quotes are verbatim from raw signals",
f"- All vendor names from G2 scrape",
f"- Market size is directional only (no dollar estimates)",
f"- Sources: Reddit ({ms['reddit_signals_found']}), HN ({ms['hn_signals_found']}), GitHub Issues ({ms['github_issue_signals']}), G2 ({ms['vendor_count_g2']} vendors)",
"",
f"Saved to: {outpath_md}",
f"JSON snapshot: {outpath_json}",
]
with open(outpath_md, 'w') as f:
f.write('\n'.join(lines))
# Save JSON snapshot
snapshot = {"input": inp, "market_signals": ms, "clusters": clusters.get('clusters', []),
"icp": icp, "market_size": market_assessment, "positioning": output, "date": date}
with open(outpath_json, 'w') as f:
json.dump(snapshot, f, indent=2)
print(f"Report saved: {outpath_md}")
print(f"JSON snapshot: {outpath_json}")
PYEOF
清理临时文件:
rm -f /tmp/mym-input.json /tmp/mym-raw.json /tmp/mym-clusters.json /tmp/mym-output.json
echo "Done. Market map saved to docs/market-maps/"
向用户展示已保存的 .md 文件的完整内容。