Case study
ASO Audit Agent
Scores any App Store listing against a 10-dimension rubric, with cited evidence and a prioritized fix plan.
- Role
- Solo - architecture and implementation
- Year
- 2026
- Status
- Live

At a glance
- ASO dimensions
- 10
- workflow steps
- 4
- model & scrape providers
- 3
- evals
- 2
Overview
ASO Audit Agent takes an Apple App Store URL, confirms the right app, and scores the listing against a 10-dimension App Store Optimization rubric - an overall score, a prioritized fix plan, rewrites, and a competitor comparison.
Every score cites evidence quoted from the listing, so a grade is checkable. A dimension with no usable signal is excluded and the score renormalized over what was actually evaluated - never a missing source posing as a zero. Built on Mastra: one conversational agent owns the chat, one deterministic workflow owns the audit.
Why I Built It
ASO advice is easy to generate and hard to check - ask a model to review a listing and it returns ten confident paragraphs traceable to nothing on the page.
The interesting problem is the inverse of generation: constraining a model until its output is falsifiable. A score must attach to a quote; a dimension with no data must leave the denominator; the pipeline must run the same way on an app it has never seen. Apple makes it harder - the keyword field is private and promotional surfaces are not exposed - so deciding the honest ceiling, and reporting it instead of filling it, is most of the work.
The Approach
Split along the axis that matters: conversation is variable, the audit is not. A Mastra agent owns the chat - URL parsing, the confirmation card, progress, follow-ups. A deterministic workflow owns the audit: scrape, a parallel fan-out (reviews, competitors, one vision call per screenshot), a single structured scoring call, then a pure assembly step. Named step events stream into the chat, so progress is a real signal.
Scope boundaries, chosen before the code and stated in the report:
- Keyword field is Apple-private - inferred from title, subtitle and description, and flagged as inferred.
- Preview-video presence is detected; its content is not evaluated - that needs decoding the asset for a 5%-weight dimension.
- Competitors come from a keyword search - framed as directional, not exact.
- No paid ASO data service for one dimension in ten.
Architecture
A single Next.js app on the Node runtime with Mastra's chat route mounted as a handler - one process, one dev command. All network code lives in tools and nothing else does I/O; the rubric is a versioned prompt imported only by the scoring step, so swapping it never touches the agent.
Three providers, chosen per task: Gemini Flash-Lite for chat and vision, Groq's 70B llama for structured scoring, Firecrawl for the listing fields Apple's APIs do not expose. Model ids are env-only and the agent provider is selectable, so a swap is config, not infrastructure.
Run state is two keys in thread metadata - the confirmed app and the last report - written deterministically; follow-ups answer from conversation history. Testing is two evals rather than a unit suite: evidence faithfulness (every quote a literal substring of the collected data) and schema-validity variance across repeated runs. They run locally, since they hit live providers.
Decisions & Tradeoffs
A deterministic workflow for the audit; an agent only for the conversation
Tradeoff accepted
Two orchestration primitives instead of one. In return the audit runs identically on an unseen app, the fan-out gets concurrency for free, and progress events exist without inventing a protocol. Suspend and resume is wrong for chat - the user's next message already is the resume event.
Alternatives considered (2)
- One agent calling the data tools in sequence, then a scoring tool
- A workflow at the top, using suspend and resume for the confirmation gate
Exclude dimensions with no usable signal and renormalize; never score a missing source as zero
Tradeoff accepted
The overall score is not comparable across audits that evaluated different dimensions, so every report carries a data-quality block and a "scored on N of 10" line. A missing source posing as a zero is the one failure that makes the number worthless; the rule is enforced after parsing, in code, because a model that agrees with it in the prompt still breaks it.
Alternatives considered (3)
- Score an unavailable dimension zero
- Assign a neutral default
- Drop the dimension without saying so
Score with a dedicated structured-output model, not with the conversational agent
Tradeoff accepted
Two providers means two rate-limit regimes. It buys isolation - the agent cannot drift from numbers it did not produce, and emits no prose summary on success - and lets each side be chosen on merit; the vision model was swapped after an A/B on the real pipeline.
Alternatives considered (2)
- Let the chat agent produce the scores
- One frontier model for chat, vision and scoring
Keep run state in thread metadata, not the framework's working memory
Tradeoff accepted
Framework working memory cannot be enabled without folding its JSON into context and exposing a model-facing mutate tool. Thread metadata keeps the same two-key deterministic contract with no model-visible blob. The cost: state is in-process and does not survive a restart.
Alternatives considered (2)
- Mastra working memory
- A database for thread state
Gate the audit behind an explicit confirmation of the matched app
Tradeoff accepted
One extra round trip and a small input state machine. It stops a nine-call, multi-provider pipeline from running against the wrong app - the expensive failure, and the one the user notices last.
Alternatives considered (2)
- Run the audit immediately on a valid URL
- Confirm only when the URL is ambiguous
Engineering Challenges
Getting schema-valid output from a model that does not have to give you one.
How it was solved
Scoring is one call returning all ten dimensions in a Zod shape. A parse failure retries once with the errors appended; a second failure emits a partial report and lists the rest under data quality. A post-parse guard catches the model excluding a dimension it may not, so nothing disappears silently - the workflow always reaches assembly.
Rate limits as a design input, not an operational detail.
How it was solved
Groq reserves the declared output budget against its per-minute window, so an oversized cap trips "request too large" before a token is produced. That one fact set the output cap, shrank the review corpus, and shaped the retry: 429s and the matched rate-limit error back off; timeouts and 400s fail fast.
A wrong assumption that survived into the pipeline and hid there.
How it was solved
An early spike judged preview-video presence undetectable, so the code read it from an Apple field that does not exist - and every app's video dimension excluded itself while working exactly as written. The fix counts player-widget markers in the scraped HTML, keyed on the CSS class not the localized label; absence is still never asserted.
Features
- Ten-dimension scorecard with one overall score, renormalized over the dimensions actually evaluated
- Verbatim cited evidence behind every score and recommendation
- Recommendations grouped into quick wins, high-impact and strategic
- Confirmation gate before any audit runs
- Live progress streamed from the workflow into the chat
- Inferred and excluded dimensions reported explicitly, never silently
Tech Stack
- Next.js 16 (App Router, Node runtime)
- React 19
- TypeScript
- Tailwind CSS 4
- Mastra (agent + workflow)
- Vercel AI SDK
- Google Gemini (chat + vision)
- Groq llama-3.3-70b (scoring)
- Firecrawl
- Apple iTunes Lookup / Search / RSS
- Zod
- Vercel
Lessons Learned
- The cheapest guard against hallucinated evidence is a substring check - a mechanical eval caught more than any prompt tightening.
- Where correctness matters, enforce in code what the prompt asks for; a model that agrees with a rule still breaks it.
- A degraded report is a product; an exception is not. Every failure mode got a defined output before it got a retry.
Screenshots


Contact
If you need an engineer who can own it end to end, from the interface to the infrastructure, let's talk.
Email me