Build AppGrowKit

KINDAreplaces $19/mosaves $228/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort weekendthe files for this build are in the project pack

RuntimeNode 22 and TypeScript with a small local interface, one machineStorageSQLite, insert only, with raw responses kept on diskCollectionA scheduler with a rate limit read from the current published figureImagesAn image model for panel composition, real text composited afterwardsReviewA vision model checking every render against an explicit list

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. 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

  2. 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

  3. 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

  4. 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

  5. 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.

VariableNeededExampleWhere the value comes from
DATABASE_PATHrequired./data/aso.dbApps, polls and every observation. This is the asset; back it up daily.
RAW_RESPONSE_DIRrequired./data/rawRaw bodies keyed by hash, so any observation can be re-derived.
POLL_INTERVAL_MSrequired3000Derived from the currently published rate limit with headroom. Recheck it.
USER_AGENTrequiredmy-aso-tracker/1.0 (contact@example.com)Identify yourself with a real contact address on every request.
IMAGE_API_KEYsecretrequiredsk-...redactedPanel generation. Also set a provider-side spend limit.
VISION_API_KEYsecretrequiredsk-...redactedThe critic pass. May be the same provider as generation.
MONTHLY_COST_CEILINGrequired20.00Generation stops here for the month; collection keeps running.
MIN_TREND_DAYSrequired14Below this, the interface shows points and refuses to draw a trend.

The build, in order

  1. 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.

    1. Do not copy a number from a blog post, including this one.

      Files src/collect/schedule.tsdocs/rate-limit.md

    2. The failures are what make a gap explainable.

      Files src/collect/poll.tstests/poll.test.ts

    3. Silence is what throttling looks like from here.

      Files src/collect/backoff.ts

    done when · tick each as it passes
    watch 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.
  2. 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.

    1. The chart is the product; the current value is a by-product.

      Files src/store/observations.tstests/insert-only.test.ts

    2. A tidy chart with invented points is worse than a gappy one.

      Files src/ui/charts.tsx

    3. Two points is not a trend however tempting the slope is.

      Files src/analysis/trend.ts

    done when · tick each as it passes
    watch 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.
  3. New reviews since you last looked

    The review feed is diffed by review identity so nothing is double counted and nothing is missed.

    1. The feed reorders and pages without telling you.

      Files src/collect/reviews.tstests/reviews.test.ts

    2. One is your observation and one is theirs.

      Files src/store/reviews.ts

    3. Disappearing is information, not a reason to delete.

      Files src/collect/reviews.ts

    done when · tick each as it passes
    watch 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.
  4. Panels, and a critic that rejects them

    Screenshot sets are generated from your real screens and every candidate is checked before you see it.

    1. Otherwise the phone in the panel runs an app you do not have.

      Files src/screens/generate.ts

    2. Model-drawn type is the thing reviewers notice first.

      Files src/screens/compose.ts

    3. The failures are how you improve the prompt.

      Files src/screens/critic.tstests/critic.test.ts

    done when · tick each as it passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more seo & marketing.