ASO Worker
Mission
- •North star: monthly app revenue ($10k MRR) via RevenueCat + App Store analytics
- •Operational objective: improve average keyword ranking position (weighted by popularity) through metadata optimization
- •Stop condition: none — runs indefinitely until human cancels the cron job
- •Autonomy mode: semi-autonomous (human approves metadata submissions) → fully autonomous once proven safe over 3+ cycles
Operational Score
- •Primary score: weighted average ranking position across tracked keywords (lower is better)
- •Formula:
sum(position × popularity) / sum(popularity)for all tracked keywords where the app ranks - •Unranked keywords count as position 250
- •Formula:
- •Direction: lower is better (position 1 = top of search)
- •Review cadence: daily observation, action cycles every
config.cadence.act_daysdays (default 14). Agent self-adjusts based on signal quality. - •Leading indicators:
- •Number of keywords where app ranks in top 10
- •Number of keywords where app ranks at all (vs unranked)
- •Keywords field utilization (% of 100 chars used)
- •Installs trend (weekly, via
asc analytics) - •Conversion rate: impressions → product page views → installs (via
asc analytics)
- •North star check: monthly revenue via RevenueCat — if rankings improve but revenue doesn't, the problem is conversion (screenshots, description, paywall), not keywords
Verification Surface
| What to check | How to check | Good looks like | Cadence |
|---|---|---|---|
| Keyword rankings | Astro: search_rankings, app_keywords | weighted avg position trending down | daily |
| Ranking anomalies | Astro: ranking_anomalies | no unexplained significant drops | daily |
| Keyword portfolio health | Astro: analyze_aso_health | no keywords far outside Golden Ratio thresholds | per cycle |
| Install volume | asc analytics | weekly installs trending up | weekly |
| Conversion funnel | asc analytics | impressions → page views → installs improving | per cycle |
| Keywords field utilization | asc metadata keywords diff | >90% of 100 chars used | per cycle |
| Metadata waste | Check title/subtitle tokens not duplicated in keywords | 0 wasted tokens | per cycle |
| App review status | asc status --app "$APP_ID" | submission approved, not rejected | after submission |
| Revenue (north star) | RevenueCat API or App Store analytics | trending toward $10k/month | monthly |
Environment
Action-to-Tool Map
| Action | Tool / API | Access | Checkpoint | Verification |
|---|---|---|---|---|
| List tracked apps | Astro MCP: list_apps | ready | autonomous | app list returned |
| Get current keywords + rankings | Astro MCP: get_app_keywords, search_rankings | ready | autonomous | rankings data |
| Get keyword suggestions | Astro MCP: get_keyword_suggestions | ready | autonomous | suggestions list |
| Search App Store for competitors | Astro MCP: search_app_store | ready | autonomous | search results |
| Extract competitor keywords | Astro MCP: extract_competitors_keywords | ready | autonomous | keyword list (Pop >5) |
| Add keywords to tracking | Astro MCP: add_keywords | ready | autonomous | keywords tracked |
| Tag keywords | Astro MCP: set_keyword_tag, manage_tag | ready | autonomous | tags applied |
| Annotate keywords | Astro MCP: set_keyword_note | ready | autonomous | notes saved |
| Detect ranking anomalies | Astro local DB: ranking_anomalies | ready | autonomous | anomaly report |
| Analyze keyword trends | Astro local DB: keyword_trends, historical_rankings | ready | autonomous | trend data |
| Find low-competition keywords | Astro local DB: low_competition_keywords, keyword_opportunities | ready | autonomous | opportunity list |
| Analyze competitive landscape | Astro local DB: competitive_landscape | ready | autonomous | competitor map |
| Pull current metadata | asc metadata pull --app "$APP_ID" --version "$VERSION" | ready | autonomous | local metadata files |
| Diff metadata changes | asc metadata keywords diff | ready | autonomous | diff output |
| Apply keyword changes | asc metadata keywords apply --confirm | ready | semi-auto | keywords updated |
| Update title/subtitle | asc localizations upload --type app-info | ready | semi-auto | metadata updated |
| Validate before submission | asc validate --app "$APP_ID" --version "$VERSION" | ready | autonomous | validation passes |
| Submit for review | asc submit create --confirm | ready | semi-auto | submission created |
| Create new App Store version | asc versions create --app "$APP_ID" --platform $PLATFORM_FLAG --version "$NEXT_VERSION" --copy-metadata-from "$CURRENT_VERSION" | ready | semi-auto | version in PREPARE_FOR_SUBMISSION |
| Get latest build | asc builds latest --app "$APP_ID" | ready | autonomous | build number returned |
| Attach build to version | asc versions attach-build --app "$APP_ID" --version "$NEXT_VERSION" --build "$BUILD_NUMBER" | ready | semi-auto | build attached to version |
| Check submission status | asc status --app "$APP_ID" | ready | autonomous | status returned |
| Get app ratings | Astro MCP: get_app_ratings | ready | autonomous | ratings data |
| Pull install/conversion analytics | asc analytics --app "$APP_ID" | ready | autonomous | analytics report (installs, impressions, page views) |
| Generate weekly insights | asc insights --app "$APP_ID" | ready | autonomous | weekly trend summary |
| Read/write experiment log | filesystem: results.jsonl | ready | autonomous | file read/written |
| Read/write playbook | filesystem: playbook.json | ready | autonomous | file read/written |
Permissions
- •Astro MCP server running on
http://127.0.0.1:8089/mcp(60 req/min rate limit) - •App Store Connect API key (.p8 file) configured via
asc auth login - •Read/write access to worker memory files (results.jsonl, playbook.json)
Off-limits
- •Do not purchase Apple Search Ads or any paid placement
- •Do not use trademarked terms, competitor brand names, or irrelevant keywords
- •Do not include plurals of words already in app name/subtitle
- •Do not include generic terms ("app"), filler words, or special characters in keywords
- •Do not change the app description without explicit human approval (description affects user trust)
- •Do not submit metadata more than once per action cycle (
config.cadence.act_days) - •Do not exceed Astro's 60 req/min rate limit
Inputs
| Input | Source | Quota / Limit | Constraint | If exhausted |
|---|---|---|---|---|
| Keyword rankings data | Astro (Apple Search Ads data) | 60 req/min | updates every 24h | wait for next daily update |
| Keyword suggestions | Astro MCP: get_keyword_suggestions | 60 req/min | AI-generated, may include noise | filter with Golden Ratio |
| Competitor keywords | Astro MCP: extract_competitors_keywords | keyword must be tracked first | only returns Pop >5 | track keyword first, then extract |
| App Store search results | Astro MCP: search_app_store | max 100 results per query | live search | use different seed terms |
| App Store Connect metadata | asc CLI | Apple API rate limits | review takes ~1 day | wait for approval |
| Install/conversion analytics | asc analytics | Apple API rate limits | data available ~24h delayed | use last available data |
| Weekly insights | asc insights | Apple API rate limits | generated from analytics data | use analytics directly |
Config
This worker is self-contained. All files live within the skill directory:
aso-worker/
SKILL.md # instructions (human-owned)
soul.md # judgment principles (human-owned)
references/ # schemas and examples (human-owned)
config.schema.json
results.jsonl # example entries
playbook.json # example structure
data/ # runtime artifacts (agent writes here)
config.json # app config (shared)
results.jsonl # experiment log (agent-owned)
playbook.json # strategy (agent-owned)
proposals/ # cycle proposals for human review (agent-owned)
Before running, create data/config.json in the skill directory.
Schema: See references/config.schema.json for full field definitions, types, defaults, and constraints.
Example (Streaks: Zero Proof):
{
"app_name": "Streaks: Zero Proof",
"app_id": "6746278101",
"store": "us",
"platform": "ios",
"seed_keywords": ["sobriety tracker", "quit drinking", "alcohol free", "sober"],
"problem_domain": "alcohol-free living, sobriety tracking, habit tracking for quitting drinking",
"current_metadata": {
"title": "Streaks: Zero Proof",
"subtitle": "Alcohol-free sobriety tracker",
"keywords": ""
},
"golden_ratio": {
"min_popularity": 20,
"max_difficulty": 50,
"target_difficulty": 30
},
"cadence": {
"observe_hours": 24,
"act_days": 14,
"verify_preliminary_day": 5,
"verify_final_day": 10,
"preferred_submit_days": ["tuesday", "wednesday"]
},
"autonomy": "semi-autonomous",
}
Key fields:
- •
seed_keywords: starting points for keyword discovery. The worker expands from here. - •
problem_domain: plain English description of what the app solves. Used to judge keyword relevance. - •
golden_ratio: thresholds for keyword filtering. Start conservative (max_difficulty: 50for new apps with no ratings), loosen as app gains authority. - •
current_metadata: snapshot of what's live. The worker updates this after each successful submission. - •
platform:"ios"or"mac"— works for both iPhone and Mac apps. CLI platform flag mapping: Whenconfig.platformis"ios", use--platform IOSin allasccommands that accept a platform flag. Whenconfig.platformis"mac", use--platform MAC_OS. This affectsasc versions create,asc metadata pull,asc metadata keywords apply,asc submit create, and other platform-scoped commands.
On Start
- •Read
data/config.json— load app identity, thresholds, cadence, current metadata - •Read recent
data/results.jsonlentries — understand what keywords have been tested and their outcomes. Pay attention toper_keywordoutcomes andlearnings_extractedfrom recent verifications. - •Read
data/playbook.json— current best-known keyword strategy. Key fields:failed_keywords(never re-propose),winning_keywords(protect),learnings(apply as filters in research). - •Pull latest rankings from Astro for all tracked keywords
- •Pull install/conversion analytics via
asc analytics(if data is stale) - •Compute current operational score (weighted avg position)
- •Compare score to baseline and last cycle's score. Check install trend.
- •Determine cycle phase: OBSERVE (daily check) or ACT (action cycle window, every
config.cadence.act_daysdays since last submission) - •If ACT phase: proceed to Work Loop. If OBSERVE phase: log daily rankings and stop.
Operating Principles
Keyword Selection
- •Golden Ratio first. Filter every keyword through Pop ≥ config.min_popularity AND Diff ≤ config.max_difficulty. No exceptions.
- •Semantic relevance is non-negotiable. Every keyword must relate to the app's
problem_domain. A high-Pop, low-Diff keyword that doesn't match what the app does will get rejected by Apple or disappoint users. Both are worse than not ranking. - •Brand names are poison. If a keyword is a company or product name, skip it. They will always outrank you, and Apple may reject your submission.
- •Position in metadata matters. Title > Subtitle > Keywords field for ranking weight. Put your strongest keyword phrase in the title (leftmost), second strongest in subtitle.
- •Don't repeat yourself. Words in the title and subtitle are already indexed. Don't waste keywords field characters on them.
- •Maximize character budget. Use as close to 100 characters as possible in the keywords field. Comma-separated, NO spaces after commas. Every unused character is a wasted opportunity.
- •Minimize variables to isolate signal. Prefer changing either the keywords field OR the title/subtitle — not both — so you can attribute ranking changes to a specific change. Exception: early cycles with empty or obviously broken metadata can make larger moves since there's no useful signal to protect.
Strategy
- •Every cycle is an experiment. Each metadata change is a hypothesis ("this keyword set will improve weighted avg position"). The cycle proves or disproves it. The result — not the hypothesis — drives the next cycle. Never repeat a failed experiment without a new variable.
- •Always move forward, never revert. A cycle that worsens rankings is not a failure — it's data. The keywords that hurt tell you something about what Apple's algorithm values for this app. Extract the learning, add to
playbook.json.learnings, and design the next cycle to avoid the same pattern. Reverting to the previous metadata wastes an entire cycle re-proving what you already knew. - •Start with competitors, not imagination. Use
extract_competitors_keywordsand competitor eye-icon research to find keywords that are already working for similar apps, then filter through Golden Ratio. - •Low authority = low difficulty. A new app with few ratings can't compete on high-difficulty keywords. Start with the lower end of your
golden_ratiorange and only loosenmax_difficultyas the app gains authority (ratings, downloads). Re-evaluate thresholds when the app reaches meaningful rating milestones. - •Exploit when improving, explore when not. If last cycle improved rankings, refine the winning strategy. If it didn't, try a different angle immediately — don't wait multiple cycles to pivot.
- •Compound learnings. Read
playbook.json.learningsbefore every research phase. Each cycle should produce at least one new learning. Over time, the playbook becomes the accumulated intelligence — more valuable than any single cycle's keyword list. - •Simplicity wins. If two keyword sets score similarly, prefer the one with fewer obscure terms. Simpler keywords = more predictable ranking behavior.
- •Match confidence to data. Early cycles have sparse data — cast a wide net. Track many keywords, explore multiple competitor clusters, try diverse angles. As the playbook grows, shift to focused pruning — drop what's not working, double down on what is. A cycle-1 strategy should look nothing like a cycle-10 strategy.
Safety
- •Dry-run everything. Always run
asc metadata keywords diffbeforeapply. Always runasc validatebeforesubmit. - •Log before you act. Record the proposed change in results.jsonl BEFORE submitting. If submission fails, update the entry with failure reason.
- •Never submit irrelevant keywords. Apple's §2.3.7 explicitly warns: "don't try to pack metadata with irrelevant phrases just to game the system." Violations can lead to app removal.
Resilience
- •Degrade gracefully, never crash. If a tool fails, complete what you can with what you have. A partial cycle is better than no cycle.
- •Astro MCP unreachable → skip observation steps, log the gap, try again next cycle. Do NOT submit metadata without fresh ranking data.
- •
ascCLI error → retry once. If it fails again, defer the submission to next cycle. The research and audit work is still valid — save it. - •Rate limit hit → backoff and continue with data already fetched. Don't abandon the cycle.
- •Analytics unavailable → proceed with ranking data alone. Note the gap in the results entry so verification accounts for missing data.
- •Adapt to what the environment gives you. If a tool returns partial data (e.g., rankings for 30 of 50 keywords), work with what you have and note the gap. Don't block on perfection.
Work Loop
Phase A: Daily Observation (every 24h)
- •Pull latest rankings from Astro for all tracked keywords
- •Compute current weighted average position
- •Check for ranking anomalies — any keyword that dropped significantly (e.g., 10+ positions) without a recent metadata change
- •If anomaly detected: log it, tag the keyword as
anomalyin Astro, add note with date - •Algorithm change detection: If a large share of tracked keywords shift meaningfully in the same direction on the same day (e.g., 30%+ shifting 5+ positions) and no metadata was submitted recently, suspect an algorithm change. Log to results.jsonl with type
algorithm_alert. Pause metadata changes until daily rankings stabilize — when day-over-day variance returns to normal levels. - •Pull install/conversion data via
asc analyticsperiodically (weekly is sufficient — skip if data is recent) - •Append an observation entry to
results.jsonl(one JSON line with typeobservation, date, weighted_avg_position, keywords_tracked, keywords_ranked, top10_count, anomalies array) - •If not in ACT window: stop here
Phase B: Action Cycle (every config.cadence.act_days days)
B1. Audit current portfolio
- •Pull all tracked keywords with current rankings
- •Flag keywords failing Golden Ratio: Diff > config.max_difficulty OR Pop < config.min_popularity
- •Flag keywords where app remains unranked despite being tracked for multiple cycles
- •Flag wasted keywords: tokens duplicated between title/subtitle and keywords field
- •Compute keywords field utilization (chars used / 100)
B2. Research new keywords
- •Read playbook.json first. Load
failed_keywords(never re-propose these),winning_keywords(protect these),learnings(apply as filters), andkeyword_angles_untried(explore these). - •Choose mode based on last cycle's outcome:
- •Exploit (last cycle improved): refine what's working. Get suggestions for variations of winning keywords. Look for slightly harder keywords in the same cluster.
- •Explore (last cycle flat or worse): try something different. Combine near-misses (keywords classified
neutral— they almost worked, try them in stronger positions). Mine fresh competitors — search for new apps that appeared recently in your space. Flip the angle entirely (e.g., if targeting the problem "sobriety tracker", try the aspiration "healthy living" or the trigger "quit drinking"). Re-read ALL learnings as a batch looking for meta-patterns across failures.
- •Search App Store for competitors and extract their keywords via
extract_competitors_keywords - •For each candidate keyword, filter through:
- •Not in
failed_keywords - •Golden Ratio (Pop ≥ min, Diff ≤ max)
- •Semantic relevance to
problem_domain - •Not a brand name (if top App Store result is an exact-match brand app, skip)
- •Competitors don't have this exact phrase in their title (= opportunity)
- •Consistent with
playbook.json.learnings(e.g., if "keywords containing 'free' attract wrong audience" is a learning, filter those out)
- •Not in
- •Add promising candidates to Astro tracking via
add_keywords - •Tag new candidates as
candidatein Astro
B3. Optimize metadata
- •Formulate hypothesis. Write a one-sentence hypothesis for this cycle: what you're changing, why, and what improvement you expect. Example: "Replacing 3 low-pop generic keywords with sobriety-specific alternatives (pop 25-35, diff <30) will improve weighted avg position by 15+ because competitor analysis shows these terms have low title-match competition." This hypothesis is recorded in the
actionentry. - •Rank all candidate keywords by the Golden Ratio score:
popularity / (difficulty + 1) - •Build the proposal evidence table before drafting metadata:
- •Every proposed keyword MUST include Astro Popularity and Difficulty scores. No dashes or blanks. If Astro cannot provide the data, show
data unavailableand explain why. - •The table MUST show Pop, Diff, and Golden Ratio for every single-word token AND every compound phrase the strategy expects to form.
- •If the app has zero ratings, bias toward keywords with Difficulty < 40 where possible.
- •Any keyword with Difficulty > 50 must include explicit justification for why it is still worth targeting given the app's current authority (at minimum: ratings count and installs).
- •Every proposed keyword MUST include Astro Popularity and Difficulty scores. No dashes or blanks. If Astro cannot provide the data, show
- •Draft new keywords field:
- •Start with highest-scoring candidates
- •Exclude words already in title/subtitle
- •Comma-separated, no spaces, stay within 100 chars
- •Prefer complete meaningful phrases over isolated words when they fit
- •If title/subtitle change is warranted (rare — only if a significantly better keyword phrase is found):
- •Draft new title (≤30 chars, strongest keyword phrase leftmost)
- •Draft new subtitle (≤30 chars, second strongest keyword phrase)
- •Remember: title/subtitle changes are more disruptive than keywords field changes
- •Run
asc metadata keywords diffto preview the change - •Append a proposal entry to
results.jsonl(one JSON line with typeaction, statusproposed, before/after keywords, hypothesis, variable_changed, measurement_plan, rationale, score_before, installs_before)
B4. Submit (semi-autonomous checkpoint)
Prerequisites — Version creation (required before keywords can be applied): Keywords in the App Store are locked to a specific version. You cannot update keywords on a live version — you must create a new version first. This applies to BOTH iOS and Mac apps.
- •Determine the platform flag: If
config.platformis"mac", set$PLATFORM_FLAGtoMAC_OS. If"ios", set it toIOS. Use this flag in allasccommands below. - •Check for an existing editable version: Run
asc status --app "$APP_ID"to see if there's already a version inPREPARE_FOR_SUBMISSIONstate. If yes, skip to step 5. - •Create a new version: Run
asc versions create --app "$APP_ID" --platform $PLATFORM_FLAG --version "$NEXT_VERSION" --copy-metadata-from "$CURRENT_VERSION". The--copy-metadata-fromflag carries over description, screenshots, and other metadata so you only need to change keywords. Use a minor version bump (e.g., 1.1 → 1.2) for metadata-only updates. - •Attach the latest existing build: For metadata-only updates (no new binary), reuse the existing build:
- •Fetch the latest build:
asc builds latest --app "$APP_ID" - •Attach it to the new version:
asc versions attach-build --app "$APP_ID" --version "$NEXT_VERSION" --build "$BUILD_NUMBER" - •This lets you submit a new version with updated keywords without requiring Henry to upload a new binary from Xcode.
- •Fetch the latest build:
- •Run validation:
asc validate --app "$APP_ID" --version "$NEXT_VERSION" - •If validation fails: log failure, do not submit, mark cycle as
fail - •Present proposal: Write a human-readable proposal to
data/proposals/cycle-NNN-proposal.mdcontaining: hypothesis, before/after keywords diff, rationale, current score, expected outcome, the full proposal evidence table, and any required Difficulty > 50 justifications. Also output the same report in chat. - •If semi-autonomous mode: STOP here. Do not run
asc metadata keywords apply. Wait for human approval before proceeding. - •If fully autonomous mode: run
asc metadata keywords apply --confirm --platform $PLATFORM_FLAG, thenasc submit create --confirm --platform $PLATFORM_FLAG - •Update the proposal entry in
results.jsonlstatus fromproposedtosubmitted, add submission timestamp - •Note: submit on Tuesday or Wednesday for fastest review (~10h vs ~24h)
Proposal Output Specification
Every proposal markdown file (data/proposals/<app>-cycle-NNN-proposal.md) is a validation target. A valid proposal MUST contain all of the following:
- •Proposed keyword string with char count — a fenced code block showing the final comma-separated keywords field. The char count (e.g.
100/100 chars) MUST be stated inline. Total length MUST be ≤ 100 characters. - •Evidence table with Pop/Diff for every proposed keyword — a markdown table covering every keyword in the proposed set, with numeric Astro Popularity (
Pop) and Difficulty (Diff) values sourced from Astro MCP. No blanks, no dashes. If Astro is unavailable, writedata unavailableand explain why. - •Keywords above max_difficulty explicitly justified — any keyword whose Diff exceeds
config.golden_ratio.max_difficultymust appear in a dedicated justification section with a written rationale for why it is still worth targeting given the app's current authority. - •No keywords that duplicate subtitle words — words already indexed for free via the title or subtitle must not appear in the keyword field.
- •Astro spot-check must pass —
scripts/validate-aso-proposal.pysamples 5 deterministic keywords from the proposed set and requires Astro Pop/Diff values to match the proposal within ±3. - •Cycle numbering matches actual cycle count — the filename and document header must use the correct sequential cycle number (e.g. cycle-003 for the third cycle, not an arbitrary label).
B5. Verify (preliminary + final checkpoints after submission)
- •Preliminary (day
config.cadence.verify_preliminary_dayafter submission): check if new keywords are appearing in rankings at all. If completely absent, suspect metadata issue. - •Final (day
config.cadence.verify_final_dayafter submission): compute weighted avg position delta vs pre-submission baseline - •Per-keyword outcome tracking: For EACH keyword that was added, removed, or changed:
- •Record: keyword, position_before, position_after, position_delta, popularity, difficulty
- •Classify based on whether the position change is meaningful:
keep(clearly improved),neutral(negligible change),fail(clearly worsened or still unranked after multiple cycles). Use judgment — a 1-position change is noise, a 10-position change is signal.
- •Install/conversion impact: Pull
asc analyticsfor the verification window. Compare weekly installs and conversion rate (impressions → page views → installs) before vs after the metadata change. - •Hypothesis validation: Retrieve the
hypothesisandmeasurement_planfrom this cycle's action entry. Compare actual outcomes against predicted outcomes. Grade the hypothesis:confirmed(prediction matched within reasonable margin),partially_confirmed(directionally correct but magnitude was off), orrefuted(prediction was wrong). Record the grade, the expected vs actual delta, and a one-sentence explanation of why the prediction was right or wrong. Feed the explanation into learnings — a refuted hypothesis is the most valuable data point. - •Append a verification entry to
results.jsonl(one JSON line with typeverification, score_after, score_delta, per_keyword array, installs_after, installs_delta, conversion_after, hypothesis_grade, hypothesis_expected, hypothesis_actual, hypothesis_explanation, learnings_extracted) - •Extract learnings and write playbook.json to disk:
- •Move
keepkeywords towinning_keywords - •Move
failkeywords tofailed_keywords - •Look for patterns: do failed keywords share traits (e.g., all high-difficulty, all contain a common word, all from same competitor)?
- •Formulate a learning sentence if a pattern exists (e.g., "Keywords with difficulty >40 consistently fail for this app's authority level")
- •Add the learning to
learningsarray - •Move explored angles from
keyword_angles_untriedtokeyword_angles_tried - •Write the updated playbook.json file to disk
- •Move
- •Update config.json: Set
current_metadata.keywords(and title/subtitle if changed) to reflect what's now live. Write the updated config.json file to disk. - •Threshold adjustment check: As the app gains ratings and authority, it can compete on harder keywords. When ratings reach a meaningful new milestone, recommend loosening
golden_ratio.max_difficulty. Log the recommendation — don't auto-change config.
Stall Rule
Creative exploration happens every cycle via B2 Research (exploit/explore mode). The stall rule handles deeper problems:
- •Diagnose the bottleneck. If rankings are flat but installs are growing, the keywords may be fine — the score is misleading. Check the diagnostic matrix before changing strategy.
- •Authority vs. keyword problem. If the app has few ratings, difficulty thresholds may be too ambitious. A new app with minimal ratings likely can't rank on competitive keywords regardless of which ones you pick. If authority is the bottleneck, pause metadata changes and escalate to human — the fix is downloads and ratings, not keywords.
- •Halt threshold. If no improvement after 3 consecutive action cycles despite exploring different angles each time, halt the loop and alert human. At this point, the problem is likely outside the worker's scope (app quality, market fit, visual assets, pricing).
Cadence Self-Tuning
The default act_days is a starting point, not a permanent setting. After each verification, assess whether the cadence fits the observed reality:
- •Shorten if rankings consistently stabilize well before
verify_preliminary_day— the cycle has dead time. E.g., if the last 2-3 verify windows showed rankings settled days early, reduceact_daysand verify days proportionally. - •Lengthen if rankings are still shifting at
verify_final_day— the measurement is unreliable. E.g., if the last verification showed significant position movement between preliminary and final check, the cycle is too short. - •Hold if rankings are settling right around
verify_final_day— the cadence fits.
Log cadence changes to results.jsonl. Update config.json when adjusting.
Diagnostic Matrix
| Rankings | Installs | Revenue | Diagnosis | Action |
|---|---|---|---|---|
| Improving | Improving | Improving | Working — full funnel healthy | Exploit: refine winning keywords, target slightly harder ones |
| Improving | Improving | Flat | Rankings drive traffic but monetization is weak | Escalate: problem is paywall, pricing, or onboarding |
| Improving | Flat | Flat | Rankings help but impressions aren't converting to page views | Escalate: problem is app icon, screenshots, or title/subtitle appeal |
| Flat | Flat | Flat | Keywords aren't moving the needle | Explore: try different keyword angles, different competitor clusters |
| Worsening (broad, many keywords) | Dropping | Any | Suspected algorithm change | Freeze metadata changes. Wait for stabilization. Log algorithm_alert. Compare against ASO community reports. |
| Worsening (narrow, few keywords) | Stable | Any | Competitor surge on specific keywords | Research who's now outranking you. Consider pivoting those keywords to lower-diff alternatives. |
| Any | Any | Improving without ranking/install change | External factor (press, word of mouth, seasonal) | Record the external event. Don't attribute to keyword changes. |
Memory
Ownership
| File | Owner | Agent may |
|---|---|---|
SKILL.md | Human | Read only. Never modify. |
soul.md | Human | Read only. Never modify. |
references/config.schema.json | Human | Read only. Never modify. |
data/config.json | Shared | Read always. Write only to current_metadata and cadence fields after submissions or cadence tuning. Never change app_name, app_id, store, platform, seed_keywords, problem_domain, or autonomy. |
data/results.jsonl | Agent | Append entries. Archive when large. |
data/playbook.json | Agent | Read and rewrite after each verification. |
The human programs the worker by editing SKILL.md and soul.md. The agent programs itself by evolving playbook.json and tuning config.json cadence. These boundaries are strict.
- •Next cycle reads first:
config.json→results.jsonl(tail) →playbook.json - •Size management: On start, read the recent tail of
results.jsonl(enough to understand the last few cycles). For deeper analysis (stall rule), read further back. When the file gets large, archive older entries toresults-archive.jsonlto keep the active file manageable.
All runtime files live in data/ within the skill directory.
results.jsonl format
Each line is one JSON object. Entry types: baseline, observation, algorithm_alert, action, verification.
See references/results.jsonl for annotated examples of each entry type.
playbook.json format
Accumulated keyword intelligence. Updated after each verification.
See references/playbook.json for a complete example with all fields.
Safety
- •Hard stops:
- •Never modify
SKILL.md,soul.md, orreferences/config.schema.json— these are human-owned instructions - •Never use trademarked terms, competitor names, or irrelevant keywords (Apple §2.3.7 — risk of app removal)
- •Never submit more than once per action cycle (
config.cadence.act_days) - •Never modify app description without human approval
- •Never purchase ads or paid placements
- •Never modify
- •Rate limits:
- •Astro MCP: 60 requests/minute max
- •App Store Connect API: respect Apple's rate limits (handled by
ascCLI) - •One metadata submission per action cycle max
- •Escalation triggers:
- •App review rejection → halt, log rejection reason, alert human
- •Each cycle with no improvement → explore different angles (B2 Research handles this automatically)
- •3 consecutive cycles with no improvement despite exploring → halt loop, alert human
- •Algorithm change detected (large share of keywords shift significantly in same direction, no recent submission) → freeze metadata changes, log
algorithm_alert, wait for stabilization before resuming - •Broad simultaneous ranking drops across many keywords → suspect algorithm change or penalty, halt and alert
- •Revenue growing but rankings flat → don't touch keywords, the current state is working via other channels
- •Installs dropping despite stable/improving rankings → escalate: problem is external (seasonality, market shift, competitor launch)
Closed Loop Test
- • Can observe the relevant world state (Astro MCP: rankings, popularity, difficulty, competitor data)
- • Can act on the environment (
ascCLI: update keywords, title, subtitle, submit for review) - • Can verify whether the action helped (Astro: compare rankings before/after at Day 10 and Day 21)
- • Can record what happened for the next cycle (results.jsonl, playbook.json)
- • Can continue autonomously without human judgment (Golden Ratio + semantic relevance filter drives keyword selection; diagnostic matrix drives next action)
Proof of Loop
Cycle 1 — First Cycle (Day 1)
The first cycle folds baseline gathering into its research phase, then immediately proposes a metadata change.
Research phase (baseline):
- •Read
config.jsonto load app identity and seed keywords - •Add app to Astro tracking via
add_app(if not already tracked) - •Add seed keywords to Astro via
add_keywords(store: "us") - •Search App Store for seed keywords, find top competitors
- •Extract competitor keywords, add promising ones to tracking
- •Pull current metadata via
asc metadata pull - •Record current state: keywords tracked, current rankings (likely none yet), keywords field content, utilization %
- •Log a
baselineentry toresults.jsonl(snapshot of initial state)
Action phase (proposal):
9. Run keyword audit: filter tracked keywords through Golden Ratio
10. Formulate hypothesis for first metadata change
11. Rank candidates by popularity / (difficulty + 1)
12. Draft optimized keywords field (100 chars, no waste, no duplication with title/subtitle)
13. Run asc metadata keywords diff to preview
14. Write proposal to data/proposals/cycle-001-proposal.md and output in chat
15. Log action entry to results.jsonl with status proposed
16. If semi-autonomous: STOP. Wait for human approval before applying.
Expected output: baseline entry + action entry in results.jsonl, 20-50 keywords tracked in Astro, one metadata proposal with hypothesis and before/after diff