Help us improve
Share bugs, ideas, or general feedback.
From appmate
Identifies top 5-10 rivals outranking an app on its core keywords using iTunes SERP analysis. Invoke when user asks for competitor research or via /competitor-research.
npx claudepluginhub fengyiqicoder/appmate --plugin appmateHow this skill is triggered — by the user, by Claude, or both
Slash command
/appmate:competitor-researchThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Single authoritative reference. Re-read before every run. Pure-SERP approach: zero RAG, zero AppMate semantic search. The script holds all the data-layer logic; Claude does keyword tokenization and a single batched LLM relevance pass.
Researches keywords, analyzes competitors, optimizes metadata, and tracks performance for Apple App Store and Google Play Store listings.
Generates a stage-diagnosed growth strategy for an app, including phase diagnosis and 3-5 actionable strategies. Auto-chains competitor-research when competitor data is missing.
Provides ASO toolkit for keyword research, competitor analysis, metadata optimization, review sentiment, and performance tracking on Apple App Store and Google Play.
Share bugs, ideas, or general feedback.
Single authoritative reference. Re-read before every run. Pure-SERP approach: zero RAG, zero AppMate semantic search. The script holds all the data-layer logic; Claude does keyword tokenization and a single batched LLM relevance pass.
Run before anything else:
python3 scripts/appmate_config.py check
If exit code ≠ 0, STOP. Tell the user AppMate credentials are not configured, show the precheck output verbatim, and direct them to the appmate-setup skill. Do not invoke any other step of this skill.
Single app → script writes phase_a (raw metadata + primary_genre_id) → LLM tokenizes keywords → script fetches iTunes Search top-200 per token, collects rivals outranking self, aggregates, scores, hard-filters by genre+density, writes phase_b → LLM does batched relevance pass on name + description[:200] → script writes final JSON, Claude renders the markdown in the user's conversation language, pastes back into conversation.
competitor-research (this skill) | feature-ideation / growth-strategy | |
|---|---|---|
| Role | produces the competitive signal | consumes it |
| Signal | iTunes Search SERP overlap, strict outrank | reuses this skill's data/competitors_<slug>.json (auto-chains this skill when the file is missing) |
| Output role | the deliverable itself | transient input evidence in their phase_a JSONs |
| Persistence | data/competitors_<slug>.json (cached, reused) | not cached |
| Item | Content |
|---|---|
| Trigger | user says "find competitors for <app>" / "跑 <app> 的竞品" / invokes this skill for <app> |
| Input | data/apps_full.json + data/sales_cache.json |
| Output | data/phase_a_competitors_<slug>.json, data/phase_b_competitors_<slug>.json, data/competitors_<slug>.json, data/competitors_<slug>.md + Claude pastes the full markdown back into the conversation |
| User intervention | 2 (trigger + LLM tokenize&filter, both in the same Claude turn) |
python3 scripts/competitor_research.py analyze "<app>"
App argument: App Store ID / bundle ID / SKU / fuzzy name. Resolves via the same find_app used by other skills.
Writes data/phase_a_competitors_<slug>.json. If credentials are missing or app is not found, exits 2 with a clear message — do not proceed.
Read data/phase_a_competitors_<slug>.json. Look at raw.title, raw.subtitle, raw.keywords for the main-market locale.
Tokenization rules (identical to aso-daily-report Step 2):
桌面便签, 云便签).Output a comma-separated token list and pass it to the script:
python3 scripts/competitor_research.py rank "Sticky Note Pro" --tokens "便签,桌面便签,sticky note,memo"
This writes data/phase_b_competitors_<slug>.json with up to 25 candidates that survived the genre + density hard filters.
Read data/phase_b_competitors_<slug>.json. For each candidate, look at name, description_short, and outranked_keywords[:3].
One batched judgement call. For ALL candidates in one pass, decide for each:
keep: true + a one-sentence reason (in the user's conversation language) explaining why the rival's target users overlap with the app'skeep: false + a one-sentence reason (in the user's conversation language) explaining why they do notExample reasons (when the user is writing Chinese):
「XX便签」描述同样主打桌面快速记事,目标用户重叠描述显示是情绪打卡 app,跟便签场景不重叠Compose the final JSON data/competitors_<slug>.json:
{
"app": "...",
"app_id": "...",
"bundle_id": "...",
"market": "CN",
"primary_genre_id": 6007,
"generated_at": "...",
"tokens": ["..."],
"self_ranks": {"...": ...},
"filtered": [
{... full candidate fields from phase_b ...,
"relevance_keep": true,
"relevance_reason": "..."}
],
"dropped_by_relevance": [
{"itunes_id": "...", "name": "...", "threat_score": ...,
"drop_reason": "..."}
]
}
filtered is sorted by threat_score desc, truncated to 10. dropped_by_relevance is diagnostic only — never rendered in markdown.
If fewer than 3 candidates pass relevance, the markdown shows an evidence-thin warning at the top.
Rendered in the same language the user has been using in this conversation. Default to English; if the user has been writing in Chinese / Japanese / Spanish / etc., translate the template headers, labels and prose accordingly. App Store metadata strings (title / subtitle / keywords / competitor app names) must remain in the App Store's source locale (e.g. zh-Hans names stay zh-Hans) — only the surrounding explanation follows the user's conversation language.
The template below is written with English placeholders. Substitute the equivalent words in the user's conversation language when rendering.
# 🎯 <App name> · Top competitors worth studying
> ⚠️ <evidence-thin warning — only when kept < 3>
**Main market**: <flag> <country> · **30-day downloads**: <N> · **Core keywords searched**: <X>
---
## 1. <Rival name> · ★<rating> (<review_count> reviews)
Outranks you on **<outrank_count> keywords**, on average **<round avg_rank_diff> places** higher
| Keyword | You | Them | Lead | Popularity |
|---|:-:|:-:|:-:|:-:|
| `<kw1>` | <#N or unranked> | **#<n>** | <diff> | <pop> <🔥 if ≥50> |
| `<kw2>` | ... | ... | ... | ... |
| `<kw3>` | ... | ... | ... | ... |
> **Why this one**: <relevance_reason — one sentence in the user's conversation language>
---
## 2. <Rival name> · ...
... (5–10 rivals total, top 3 keywords each) ...
---
**Top <N>**: #X / #Y / #Z — <one-sentence summary of each top rival's core threat>
Want a deeper look at one rival's keyword layout? Tell me the number and I can pull their metadata via the `aso-optimize` skill for side-by-side comparison.
len(filtered) == 0)When the LLM relevance pass keeps zero candidates, do NOT render the per-rival ## blocks. Instead, render exactly this (translate headers/labels into the user's conversation language):
# 🎯 <App name> · Top competitors worth studying
> ⚠️ No qualifying rivals · this app has no same-category rivals on its own keyword SERPs that meet the `MIN_OUTRANK_COUNT = 3` threshold.
**Main market**: <flag> <country> · **30-day downloads**: <N> · **Core keywords searched**: <X>
Likely causes (ordered by likelihood):
1. **Too few keywords**: at least 3 valid tokens are required for any rival to pass the density threshold. Check whether `raw.keywords` in `phase_a_competitors_<slug>.json` is empty or has only 1-2 tokens.
2. **The app is the leader of this niche**: no rival outranks you ≥ 3 times across your own SERP.
3. **Category mismatch**: your `primary_genre_id` does not match any candidate rival — common for niche keywords with cross-category mixing.
To dig deeper, run `python3 scripts/competitor_research.py show-b "<app>"` to inspect the phase_b candidate pool (the pre-filter detail).
H2 (##) block — low-density layout.`桌面便签`.T/S/K/X.> blockquote (translate the label to the user's conversation language).dropped_by_relevance never appears in markdown. JSON only.threat_score descending.len(filtered) == 0, render the Empty-state template above (no ## rival blocks); never invent placeholder rivals.| Dimension | Source |
|---|---|
| Pick app | data/apps_full.json via aso_optimize_v2.find_app |
| Main market | the country with the largest 30-day downloads in sales_cache.json |
| primary_genre_id | iTunes Lookup, cached in data/itunes_lookup_cache.json (no TTL) |
| SERP top-200 per token | iTunes Search API (https://itunes.apple.com/search), cached in data/serp_details_cache.json |
| Keyword popularity | keyword_local.lookup_popularity (static keyword_reference_<region>.json) |
| Tokenization | LLM semantic split (not regex / jieba) |
| Relevance filter | LLM batched call over name + description[:200] |
| Parameter | Value | Note |
|---|---|---|
SERP_LIMIT | 200 | top-N per iTunes Search call |
MIN_OUTRANK_COUNT | 3 | candidate must outrank on ≥ this many tokens |
MAX_CANDIDATES_BEFORE_LLM | 25 | phase_b truncates to this |
DESCRIPTION_TRUNCATE | 200 | chars shown to LLM per candidate |
TOP_N_RIVALS | 10 | upper bound on filtered |
MIN_RIVALS_FOR_REPORT | 3 | below this, ⚠️ evidence-thin warning |
TOP_K_KEYWORDS_PER_CARD | 3 | per-card keyword table size in markdown |
python3 scripts/competitor_research.py analyze "Sticky Note Pro"
python3 scripts/competitor_research.py rank "Sticky Note Pro" --tokens "便签,桌面便签,sticky note,memo"
python3 scripts/competitor_research.py show-a "Sticky Note Pro"
python3 scripts/competitor_research.py show-b "Sticky Note Pro"
aso-daily-report run that finds an app dropping out of top 20 on its own keyword → trigger this skill to see who took the slot.aso-optimize skill run for <app> for keyword reshuffling.competitors_<slug>.json yet — wiring feature-ideation / growth-strategy is a separate task (see spec §15).MIN_OUTRANK_COUNT = 3 → empty result.dropped_by_relevance vary across runs on borderline cases (audit via the JSON).threat_score descendingT/S/K/X## N. <name>> blockquotedropped_by_relevance NOT rendereddata/competitors_<slug>.json writtendata/competitors_<slug>.md written