openaso

Changelog

Every tool contract change, new capability, and hosted-service update. Result shapes are public API here — when one changes, it is in this file.

Unreleased

already live on the hosted endpoint
added
  • Agent-first operating model. The contracts were already built for agents; the surroundings assumed a human. This release removes the human from every step an agent should be able to take alone: - Keyless trial door — POST https://mcp.openaso.ai/trial serves the stateless tools with no credentials behind a per-IP daily budget (OPENASO_TRIAL_DAILY, default 25; hashed IPs in the new trial_calls table). The budget error names the signup path. The 401 on /mcp now lists every way in. - OAuth sign-in — the API serves RFC 9728 protected-resource metadata at /.well-known/oauth-protected-resource and its 401 carries resource_metadata, so OAuth-capable MCP clients (Claude.ai/ChatGPT connectors, VS Code) run the standard flow against the dashboard site, which is now an OAuth 2.1 authorization server (Better Auth mcp plugin: dynamic client registration, PKCE, consent page at /oauth/consent, tables in 20260925030000_oauth_mcp.sql). Access tokens are verified server-side against the shared database and map to the same accounts row an API key would, by verified email. Advertised only when OPENASO_OAUTH_ISSUER is set. - `find_app` tool + openaso find command: an app name or bundle id to trackIds, per storefront (matchedBy bundleId/search). - `account` (hosted): plan, remaining daily calls and reset time, tracked count vs cap, webhook/annotation state, deployment features. The quota error now points at it and at the limits page instead of dead-ending. - `webhook` (hosted): one https endpoint per account; the daily managed run POSTs the run's rank drops, HMAC-SHA256 signed (x-openaso-signature), test action for a sample delivery. Public hostnames only (no IP literals / internal names — the fleet's egress must not be aimable). New webhooks table; TrackRunSummary.webhooks. - `annotate` (hosted): timeline notes with optional app/storefront scope and backdating; history now returns them as events in the same scope so movement reads as before/after a change. New events table (cap 1000 per account). - App Store Connect, bring your own key (hosted): asc_connect stores an account's ASC API key encrypted at rest (OPENASO_SECRETS_KEY, AES-256-GCM, new asc_credentials table); asc_listing reads the real title, subtitle and hidden keyword field per locale for the live and the editable version — the field no public API exposes, straight into coverage; asc_update_listing writes validated fields onto the editable version. The research → draft → ship → measure loop closes. - App Store Connect analytics reports (hosted): asc_analytics_enable creates the app's ONGOING analyticsReportRequest once (Admin role; idempotent, replaces a request Apple stopped for inactivity), and asc_analytics reads the real acquisition funnel from the "App Store Discovery and Engagement" and "App Store Downloads" reports — impressions → product page views → taps → downloads (first-time + redownloads) per day, per source type and per storefront, with per-impression and per-page-view rates and a completeThrough date (Apple finalizes within three days). Later instances restate earlier dates, so the newest instance wins per (report, date); Unique Counts are not summed. Daily instances are reduced to funnel facts and cached per account in the new asc_analytics_instances table (migration 20260926000000), purged by asc_connect action=remove; nothing is ever joined across accounts, as Apple's Developer Program License Agreement §6.7 requires. The client gained the request → reports → instances → segments flow and a gzip download that never sends the bearer token to the file host. - Fair-queued Apple throttle. The shared politeness throttle now serves lanes round-robin (hosted: one lane per account; scheduled jobs on their own lanes). One tenant's forty parallel volume calls used to queue 160 Apple requests ahead of everyone else's next rank; the p90 of the most-used tool was over two minutes. - Metering learns why. usage rows gain client (MCP client User-Agent), error (tool error text) and storefronts (fan-out), migration 20260925000000_usage_observability.sql. OAuth calls are metered with a null api_key_id. - Hosted track rows carry previous (rank in the prior snapshot).
  • revenue command + MCP tool: order-of-magnitude revenue estimates modeled from the app's public top-grossing chart rank — the one monetization signal Apple publishes for arbitrary apps. Daily/monthly gross-consumer-spend bands in USD (low/mid/high; developer proceeds ~70-85% of gross), from a power-law fit ($4.0M x rank^-0.95 on the US overall chart, calibrated 2026-07 against published rank-revenue datapoints: ChatGPT/ TikTok/MONOPOLY GO! press figures, Cal AI founder-public numbers, Distimo 2013 thresholds, the DazeEnd/Overcast 2015 curve), scaled per storefront (iOS spend share vs US) and per genre chart (spend share through the power law). Falls back from the overall chart to the app's genre grossing chart; apps on no consulted top 100 get upperBoundDaily only. Bands carry a 2.5x multiplier and a note stating the model's limits — estimates, never Apple data.
  • charts now reports topGrossing alongside top-free/top-paid, served by the legacy itunes.apple.com RSS feed (ITunesClient.topGrossing, per- genre capable) — the only public source of grossing ranks; the modern marketing-tools feed deliberately omits grossing. Additive JSON change: per-storefront results gain a topGrossing field.
  • Hosted: the daily managed run now archives the overall top-grossing top-100 per storefront into the new chart_snapshots table (migration 20260724000000_chart_snapshots.sql) for every tracked storefront plus US plus OPENASO_GROSSING_COUNTRIES — insurance against the legacy feed being retired, and the future calibration series for the revenue model. The run summary gains a charts section with its own failure list.
  • audit command + MCP tool: one-call ASO research bundle per storefront — the app's current listing (title + web-scraped subtitle), its visible word pool, and scored keyword candidates (supplied phrases plus autocomplete discoveries seeded from the pool). Each candidate is scored from a single shared search: demand (popularity, the volume tool's model — real Apple Search Ads popularity where available, else the estimate), competition, incumbent strength (medianTopRatings) and the app's current rank — where separate volume + rank calls would search twice. Emits progress notifications; supplied phrases never compete with discoveries for the limit slots.
  • optimize_metadata MCP prompt (the server now declares the prompts capability, served over stdio and hosted HTTP alike): a packaged metadata-optimization playbook the client's model executes with the tools — audit, deepen with reviews/competitors, choose targets, draft against coverage with {phrase, value: popularity} weights, regression-check that no currently-ranking phrase is silently dropped, then verify with rank and set up daily tracking. Prompt arguments: appId (required), countries, phrases (comma-separated).
  • coverage word-value aggregation: target phrases accept {phrase, value} weights (CLI: "photo editor=87"), and wordValues sums each word's search value across every phrase it can combine into ("photo" serves "photo editor", "photo maker" and "take photo" at once). A phrase's value is credited to each of its words — totals are not additive across words — so unlockValue (absent words whose addition alone covers new phrases) and atRiskValue (a true drop-counterfactual: covered phrases that would actually break, respecting surviving plural variants) are the marginal numbers to budget the 30/30/100 characters by.
  • coverage keyword-field waste accounting (keywordFieldWaste): commas count toward the 100-character limit; spaces around commas, duplicates of title/subtitle words, in-field repeats, stopwords and the separators their removal frees are reported as reclaimable — never more than the field contains. The comma/space convention is now also taught in the server instructions and tool description.
  • coverage command + MCP tool: metadata keyword coverage, pure local computation. Apple indexes the title, subtitle and hidden keyword field per locale as one combined word pool, so an app can rank for phrases assembled from words across fields — a model agents consuming the server did not reliably know. coverage operationalizes it: combined pool with strongest-field attribution, cross-field duplicates (reclaimable characters), per-field character budgets (30/30/100), and per-phrase coverage (exact-in-field / cross-field / missing words, with plural and stopword handling). The server instructions, the rank/app tool descriptions and a note in every coverage result now teach the combination model explicitly, so it survives hosts that drop server-level instructions.
changed
  • Smaller context tax. Every result's note is now a one-line pointer to the relevant openaso.ai/reference page instead of a paragraph repeated on every call (the volume note alone was ~900 characters × 5,650 calls last month). Server instructions cover find_app, the hosted account tools and the App Store Connect path; search_terms lists all fifteen genre keys in its schema instead of four examples.
  • `search_terms` (hosted): Apple's own weekly search-term popularity. A weekly Cloud Run Job mirrors the Apple Ads API's search-term popularity dataset — Apple's top ~500 terms per genre per storefront with their 1-100 scores and genre ranks — into search_term_popularity for the ten pulse storefronts (13 weeks retained). The new read-only tool serves the top terms for a storefront or genre and their movement against an earlier week (rising, falling, new), straight from the archive with no Apple calls. The same archive is the ground truth for calibrating the volume estimate.
  • Web: public App Store ranking trends at /app-store/ranking-trends — market-wide climbers, fallers, breakouts, dropouts, turnover, and volatility across 10 storefronts, every chart category, and 4-hour/24-hour/7-day/30-day windows. A dedicated advisory-locked Cloud Run Job archives all 780 chart series every four hours in idempotent UTC buckets. Free-chart velocity is labeled as a relative download-momentum proxy (never an invented download count), with paid-download and grossing/spend views alongside it. Any public keyword lookup records the complete top 200 in the new service-wide keyword_snapshots archive, then shows every app gaining, falling, entering, or leaving that search result over time. Phrases with at least three views in seven days refresh every six hours; quieter phrases refresh daily. The corpus also grows proactively from Apple autocomplete: a persistent US prefix frontier starts with every one-character and two-letter prefix, scans 1,000 due branches per four-hour discovery run, and expands branches that fill Apple's 10-suggestion response. Discovery and ranking now run as independent Cloud Run Jobs, so a queue of top-200 searches cannot stop the next prefix batch. The ranking lane gives every discovered term a complete top-200 search snapshot. There is no shortlist, promotion stage, or active-series ceiling: autocomplete membership defines the proactive tracked corpus, while best position and repeat appearances remain evidence rather than eligibility. Autocomplete is the only proactive discovery source — no app metadata, reviews, generated terms, or customer watchlists. New series get a bootstrap refresh on the next four-hour pulse before moving to the normal demand-based cadence; every due series remains eligible, with no oldest-series batch cap. Four bounded workers overlap Apple network waits and backoff while the process-wide request-start throttle holds sustained load near 20/minute, and each collector runs as a Cloud Run Job so corpus-dependent duration has no 30-minute HTTP ceiling. Scheduled autocomplete and search calls bypass the shared response cache: neither can be reused before its TTL expires, and avoiding those raw megabyte-scale Postgres writes leaves only normalized snapshots crossing the database boundary. Chart and interactive-call caching is unchanged. The durable frontier resumes instead of restarting from a; a 100,000-prefix ceiling bounds discovery expansion, not the number of tracked keywords.
  • Web: the dashboard now hands a freshly minted key straight into a client — paste-ready connect snippets (Claude Code command, MCP config JSON for Cursor/Windsurf/any client, and a curl that runs a real rank call from a bare terminal) with the key already filled in, shown at mint time and kept on the dashboard with a YOUR_KEY placeholder afterwards. Closes the gap where a key was minted but no call ever followed.
  • Web: per-app ASO snapshot pages at /apps/<id> — today's chart positions, ratings, the visible title + subtitle word pool (tokenized the way coverage tokenizes, subtitle scraped best-effort like the app tool), and the newest reviews, assembled from the same public Apple endpoints the API uses and refreshed daily. Chart rows now lead to the snapshots, and a /apps hub lists each category's leaders. Only charted apps are indexable — a top-100 presence is the curation line against index bloat.
  • Web: /charts grew from one storefront to the ten largest (US, GB, JP, DE, FR, KR, CA, AU, BR, IN — ~780 pages, all rendered on demand) and now shows day-over-day movement. Each chart's daily render archives it into the service-wide chart_snapshots table (same series shape the hosted runner writes for grossing) and diffs against the previous day: ▲/▼/new badges per row, a movers line up top. History is a progressive enhancement — no database, no badges, page still renders.
  • Web: email verification and self-serve password reset, sent through Resend (RESEND_API_KEY + optional MAIL_FROM). Password signup now requires clicking a verification link before sign-in, and the dashboard refuses to attach an account to an unverified session — closing the hole where registering someone else's email adopted their keys and usage. Without a mail transport configured, production is Google-only (local dev keeps unverified passwords for credential-free checkouts). New /reset-password page; "forgot password?" on the login form.
  • Web: account dashboard at openaso.ai — sign in with email + password (Better Auth; sessions on the site, keys still header-auth on the API), see today's tool-call usage against the daily quota, and mint or revoke API keys self-serve (up to 5 active). Accounts link by email, so a key issued via the old POST /signup and a dashboard login with the same address are one account; lost-key recovery is now self-serve instead of email-support. New auth_* tables (migration 20260728000000_web_auth.sql).
  • Apple Search Ads popularity now comes from the official Apple Ads API. volume/audit read Apple's search-term popularity dataset (Apple Ads Platform API, OAuth) instead of the Apple Ads console's internal endpoint, which needed a web-session cookie that expired every few days. Same source: "apple" contract, two semantic shifts: the scale is 1-100 (Apple publishes only its top ~500 terms per genre per storefront per week, so the long tail is *omitted* rather than floored at 5 — applePopularity is now null where it used to be 5, and floored means "Apple was asked and carries no signal"), and an omitted term's popularity is now capped below the lowest score that made Apple's lists for that storefront — the omission is evidence — while estimate stays the uncapped independent number. A failed Apple lookup is no longer cached, so an outage cannot poison 24 hours of results.
  • The `volume` estimate is calibrated against Apple's real scores. With the archived dataset as ground truth (109 US terms stratified over Apple's published 41-100 range), the blend weights moved from autocomplete 0.45 / ratings 0.35 / competition 0.1 / title 0.1 to 0.15 / 0.55 / 0.2 / 0.1 with a 0.93 scale: rating mass of the top results is the strongest single signal (r 0.66), autocomplete position the weakest (r 0.28). Out-of-sample mean error against Apple drops from 13.0 to 9.1 points and the old +8-point overshoot on mid-range terms is gone. components are unchanged; only their weighting is.
  • MCP tools: countries is capped at 25 storefronts per call (schema maxItems + runtime check). Uncapped, one quota-unit call could fan out to every 2-letter code — hundreds of storefront queries, each its own Apple requests and, when configured, a credentialed Apple Ads popularity lookup. Query more markets across multiple calls.
  • The Apple Ads popularity client paces its requests with the same shared politeness throttle as the iTunes client (default 1/s, minIntervalMs) instead of firing batches back-to-back, so a burst of uncached lookups cannot hammer the endpoint through the single shared Apple Ads session.
  • Hosted: the canonical MCP endpoint is now https://mcp.openaso.ai/mcp — a Cloud Run domain mapping in front of the same service (docs/HOSTED.md "Custom domain"). The old https://openaso-806591819206.us-west1.run.app/mcp URL keeps working indefinitely as a legacy alias; nothing breaks, but new configs, docs and registry listings should use the domain.
  • Real Apple Search Ads popularity for `volume` (and `audit`). The headline demand number is now popularity (0-100): Apple's real Search Ads Search Popularity when source is "apple", served server-side by the hosted service through a single Apple Ads account (Apple exposes this only via the Apple Ads console's internal endpoint, not the official API). Apple floors low-demand keywords at 5 and returns nothing below its threshold or in storefronts outside Apple Ads coverage; there — and in local mode, which carries no credentials — source is "proxy" and popularity falls back to the independent estimate (the former blended score), with floored and applePopularity exposed for transparency. Breaking JSON contract: the volume per-storefront volumeScore/popularityScore fields are replaced by popularity/source/applePopularity/floored/estimate; audit candidates gain popularity/popularitySource/applePopularity/floored/ estimate in place of volumeScore. The popularity fetch never fails a tool call — an expired session or uncovered storefront simply degrades to the estimate. Configured via OPENASO_APPLE_ADS_COOKIE + OPENASO_APPLE_ADS_ADAM_ID (see docs/HOSTED.md); cached per (storefront, keyword) for 24h in the shared L2 cache.
removed
  • Hosted: anonymous POST /signup (email in, key out) is retired — API keys now require an account, created at openaso.ai/login and minted in the dashboard. The endpoint answers 410 Gone with that pointer (CORS kept for cached clients). The per-IP abuse cap (3/day) moves to the dashboard path; the fleet-wide daily signup cap is retired outright (OPENASO_SIGNUP_DAILY_CAP is no longer read), and the new OPENASO_SITE_URL (default https://openaso.ai) feeds the landing page and the 410 pointer.

0.3.0

2026-07-22
breaking
  • CLI --json output now always uses the canonical multi-storefront shape — the same shape the MCP tools return. Previously each command emitted a different, flatter JSON shape when a single --country was given, so adding a second country silently changed the schema. Single-storefront *text* output is unchanged.
  • competitors --json renames count to returned and adds total (the API's result count, capped at 200), matching the MCP tool. The search always requests the full 200 results and slices to limit, so total is meaningful regardless of the limit.
  • rank --json with --save emits flat historyFile/prev/change fields (matching the MCP tool) instead of a nested history object.
  • Exit codes are now meaningful: 0 success, 1 usage/validation error, 2 iTunes API/network failure, 3 app not found in any requested storefront (previously not-found exited 1, indistinguishable from usage errors).
  • Relative --history-file/--config paths resolve against $OPENASO_HOME when set (else the current directory, as before) and results echo resolved absolute paths.
  • The default politeness throttle between API requests is now 1000ms (was 250ms) to match the iTunes API's real rate limits.
added
  • suggest command + MCP tool: App Store search-autocomplete suggestions (keyword discovery) via the public MZSearchHints endpoint.
  • reviews command + MCP tool: recent customer reviews per storefront from the public RSS feed — rating distribution plus review texts.
  • charts command + MCP tool: top-free / top-paid chart positions (top 100) from the public marketing-tools feed.
  • history command + MCP tool: read the JSONL history back — per (keyword, app, storefront) series with current/previous/best/worst rank, filterable by keyword/app/country/date. No network.
  • app --full (CLI) / full: true (MCP): return the complete iTunes lookup record, including the description — previously unreachable over MCP.
  • App summaries include languages (localization coverage signal).
  • --version; per-command --help; with --json, errors are emitted as JSON on stdout ({"error": {"message": ...}}).
  • MCP: tool annotations (readOnlyHint etc.) and titles, server instructions, progress notifications for track when the client sends a progressToken, cancellation support, full output schemas for competitors rows and app summaries, JSON-Schema constraints (min/max/default/pattern), unknown argument keys rejected, unknown tools rejected as protocol errors.
  • HTTP client: per-attempt timeout (10s default), authoritative Retry-After support (capped at 60s, shorter or longer than the backoff), retry on malformed 200 bodies, junk chart/review feed bodies rejected instead of read as empty, response-body snippets and cause chaining in errors, race-free shared throttle, and AbortSignal support end to end — cancellation reaches every MCP tool's outbound requests and interrupts retry backoff and throttle waits promptly.
  • track fetches each keyword x storefront search once for all configured apps (was once per app) and indexes history in one read per run.
  • History appends recover from partial trailing lines (crash/disk-full) instead of corrupting the next record.
  • Packaging: files allowlist, prepare/prepublishOnly build hooks, keywords/author/repository metadata, LICENSE file, CI workflow (Node 18/20/22), .openaso.example.json, type-checked tests (tsc -p tsconfig.test.json).

0.2.0

  • Initial public shape: rank, competitors, volume, app, track commands; MCP server over stdio; JSONL rank history with per-storefront change tracking; hermetic test suite.