Build AppGrowKit
KINDAreplaces $19/mosaves $228/yrback to the verdict
A personal App Store tracker and screenshot generator whose hard problems are polling an unauthenticated public endpoint politely, keeping an insert-only time series that is worth something in six months, and catching what an image model invents before it reaches a store listing.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the collector, the interface and the screenshot pipeline.
Get it nodejs.org, or your version manager. open ↗
Verify
node --version prints v22 or higher - installfree
Why A rank tracker that only polls when you happen to have a terminal open produces a history full of holes, which is the one thing it cannot recover from.
Get it A system timer or service on an always-on machine, not a terminal loop.
Verify
A poll row exists for every scheduled slot over three consecutive days - API keypay per image; measure your own cost per set on the first run
Why Panel composition is the only part of the build that costs money per use, which is why it has a ceiling.
Get it Create a key with your chosen provider and set a spend limit in their console as well as in this app.
Verify
One panel renders and a cost row is written - API keypay per image, smaller than generation
Why The critic pass is what stops an invented award badge reaching a store listing, and it needs a model that can read a rendered image.
Get it A multimodal model from any provider, which may be the same key as the image model or a different one.
Verify
A deliberately garbled render fails at least one check - have readyfree
Why Required dimensions and device classes change, and a set built to last year's numbers is rejected at submission.
Get it Open the store's published specification on the day you build, record the URL, the date and the figures in the repository.
Verify
The recorded figures match what the exporter produces
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- apps(track_id, storefront, name, bundle_id, added_at) · the apps you own, listed by hand - polls(id, at, endpoint, url, http_status, request_ms, response_sha256, raw_path) · one row per request, failures included - ratings(poll_id, track_id, storefront, average, count, app_version) · insert only, never reconciled - ranks(poll_id, track_id, storefront, chart, genre_id, position) · a missing position is a row with a null and a reason, not an absent row - keyword_positions(poll_id, keyword, storefront, track_id, position, results_returned) - reviews(id, track_id, storefront, review_id, review_at, first_seen_at, rating, title, body, app_version) - screenshot_runs(id, at, device, prompt_version, model_id, cost_minor, candidate_count, chosen_candidate) - screenshot_checks(candidate_id, check_name, passed, notes) · the critic's findings kept whether it passed or failed
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_PATH | required | ./data/aso.db | Apps, polls and every observation. This is the asset; back it up daily. |
RAW_RESPONSE_DIR | required | ./data/raw | Raw bodies keyed by hash, so any observation can be re-derived. |
POLL_INTERVAL_MS | required | 3000 | Derived from the currently published rate limit with headroom. Recheck it. |
USER_AGENT | required | my-aso-tracker/1.0 (contact@example.com) | Identify yourself with a real contact address on every request. |
IMAGE_API_KEYsecret | required | sk-...redacted | Panel generation. Also set a provider-side spend limit. |
VISION_API_KEYsecret | required | sk-...redacted | The critic pass. May be the same provider as generation. |
MONTHLY_COST_CEILING | required | 20.00 | Generation stops here for the month; collection keeps running. |
MIN_TREND_DAYS | required | 14 | Below this, the interface shows points and refuses to draw a trend. |
The build, in order
A poller that behaves and keeps its receipts
Collection runs on a schedule, stays under the published rate, and records every request including the failures.
Do not copy a number from a blog post, including this one.
Files
src/collect/schedule.tsdocs/rate-limit.mdThe failures are what make a gap explainable.
Files
src/collect/poll.tstests/poll.test.tsSilence is what throttling looks like from here.
Files
src/collect/backoff.ts
done when · tick each as it passeswatch out- Retrying a failed request immediately against an unauthenticated public endpoint gets your address throttled, and because there is no key and no dashboard you find out by receiving nothing rather than by receiving an error you can read. Apple's Search API page publishes a figure of approximately twenty calls per minute and says it is subject to change, so read that page on the day you build, set your interval from it with room to spare, back off exponentially on any non-success, and cache lookups instead of repeating them.
- Not recording the failures makes a gap in the chart indistinguishable from a day your app had no rank. Write a row for every attempt, and make the interface draw a gap where there is no successful poll instead of joining the line across it.
A time series nothing is allowed to overwrite
Ratings, ranks and keyword positions accumulate as insert-only observations tied to the poll that saw them.
The chart is the product; the current value is a by-product.
Files
src/store/observations.tstests/insert-only.test.tsA tidy chart with invented points is worse than a gappy one.
Files
src/ui/charts.tsxTwo points is not a trend however tempting the slope is.
Files
src/analysis/trend.ts
done when · tick each as it passeswatch out- Keying today's rating on the app id so tomorrow's poll updates the row makes the app look correct and destroys the only thing you were building, and because the current value is always right you will not notice for months. Use an autoincrementing observation id, add no unique constraint on the app and storefront, and put a test in the suite that inserts twice for the same app on the same day and asserts two rows.
- Interpolating across a missing day makes a chart look tidy and quietly invents a measurement. Break the line, and label the gap with how many polls failed in that window.
New reviews since you last looked
The review feed is diffed by review identity so nothing is double counted and nothing is missed.
The feed reorders and pages without telling you.
Files
src/collect/reviews.tstests/reviews.test.tsOne is your observation and one is theirs.
Files
src/store/reviews.tsDisappearing is information, not a reason to delete.
Files
src/collect/reviews.ts
done when · tick each as it passeswatch out- Keying reviews on their position in the feed gives you duplicates whenever the ordering shifts and silent misses whenever a page boundary moves, which both happen routinely on a paged public feed. Key on the review identifier, keep first_seen_at as your own observation, and never assume the feed is stable between polls.
Panels, and a critic that rejects them
Screenshot sets are generated from your real screens and every candidate is checked before you see it.
Otherwise the phone in the panel runs an app you do not have.
Files
src/screens/generate.tsModel-drawn type is the thing reviewers notice first.
Files
src/screens/compose.tsThe failures are how you improve the prompt.
Files
src/screens/critic.tstests/critic.test.ts
done when · tick each as it passeswatch out- An image model given a written brief will invent star ratings, award badges, download counts and text that is nearly-but-not-quite words, and at thumbnail size all of it looks fine to you and none of it is true about your app. Check every render against a named list covering garbled text, elements sliced by a panel edge, invented ratings or badges, and hardware that contradicts the style, and store the check results whether they passed or not.
- Letting the model render your headline means the type is subtly wrong in a different way in every panel and the set stops looking like one set. Composite headlines as real text with a real font after generation, which also makes changing the wording free.
Spending, exports and honest empty statesproduct builder
Image generation has a ceiling, the history exports, and the panels you cannot fill say why.
Stopping halfway through a set wastes what you spent.
Files
src/cost/ceiling.tstests/cost.test.tsThe history is the asset, so it has to be portable.
Files
src/export/csv.tsAn honest blank beats a plausible invention.
Files
src/ui/empty-states.tsx
done when · tick each as it passeswatch out- A screenshot pipeline with two model calls per candidate and a re-render on failure costs more per iteration than you expect, and an experiment left running overnight is a bill. Count every call, hold the total against a monthly ceiling, and refuse to start a run that would cross it rather than stopping halfway through a set.
- Filling an empty competitor panel with something derived from your own single day of data is how a placeholder becomes a number somebody plans around. Ship the empty state with a sentence about what a catalogue of that size actually requires, and leave it empty until you have it.
That is the whole plan for AppGrowKit. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Competitor intelligence and keyword difficulty.
- Revenue and download estimates.
- Tracking apps you do not own.
- Automated store submission.
- months of rank, rating, and keyword history you cannot backfill
- the 1.7M-app, 36-storefront catalog behind competitor lookups and keyword difficulty
- revenue and download estimates, which are calibrated against that catalog
- keyword volume and competition scores, which need a corpus to be relative to
- the MCP server that answers ASO questions from Claude, Cursor, or ChatGPT
- an accuracy pass on generated screenshots: the QA critic, best-of-2 scoring, and exact-size output
- A weekly digest of what moved, built from the same observations rather than a second pipeline
- Annotating the timeline with your own release dates, which is the only causal information you actually have
- A second storefront for an app you already ship there, which tests the storefront dimension before it matters
Need the files? The project pack on the verdict page hands your agent the whole brief · more seo & marketing.