Mobile QA Gate for AI Coding Agents avatar

Mobile QA Gate for AI Coding Agents

Pricing

$10.00 / 1,000 page audited on one phones

Go to Apify Store
Mobile QA Gate for AI Coding Agents

Mobile QA Gate for AI Coding Agents

Checks web pages on today's phones (iPhone 15-18, Galaxy, Pixel, foldables) and returns PASS or FAIL per page per phone with a screenshot: sideways scroll, split phone numbers, orphan heading words, unreadable text, tiny tap targets. Built for AI agents and MCP.

Pricing

$10.00 / 1,000 page audited on one phones

Rating

0.0

(0)

Developer

FFS Marketing

FFS Marketing

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

10 days ago

Last modified

Share

Your agent just shipped a website. Did it check the site on a phone? This Actor does. Give it URLs and it opens each page on real phone sizes (by default today's phones: iPhone 15-16 at 393px, iPhone 17-18 Pro at 402px, iPhone 18 Pro Max at 440px, Galaxy S at 360px and Pixel at 412px). For each page on each phone you get a PASS or FAIL verdict, the reason, and a screenshot.

Works as a tool for AI agents. Add it to Claude, Cursor, VS Code or any MCP client through Apify's MCP server (https://mcp.apify.com?tools=fluidfaith/mobile-qa-gate), or call it with one API request. Plain JSON in, plain JSON out, one overall verdict in OUTPUT. Setup is in Use it from an agent.

Costs $0.01 per page per phone. One page on the 5 default phones is $0.05. Pages that fail to load are free, and a maximum charge per run is never exceeded.

Why use it

  • Catches the bugs AI-built pages ship with: sideways scrolling, phone numbers split across lines, a stranded last word in a headline, unreadable text, tiny buttons.
  • Gives a clear verdict an agent can act on: PASS or FAIL per page per phone, with the exact issue text to fix.
  • Fast and cheap enough to run after every deploy: a page on 5 phones usually finishes in one to two minutes.
  • Tested every day on Apify against the default input, so it works when your agent calls it.

What it checks

CheckFails whenWhy it matters
horizontal-overflowThe page is wider than the screen, so it scrolls sideways.The most common mobile bug AI-built pages ship with.
phone-number-splitA tel: link wraps across two lines.Broken-looking phone numbers on contact pages.
orphan-heading-wordA heading (4+ words) ends with a single stranded word on its own line.Looks broken on phones; agents never notice it on desktop.
text-contrastText is clearly below WCAG AA: under 3.5:1 for normal text, under 2.5:1 for large text (default "clear" mode). Set contrastMode: "strict" for exact WCAG AA (4.5:1 and 3:1).Unreadable text. Borderline cases are returned as warnings, not failures.
tap-target-sizeA button or button-style link is smaller than 24x24px and crowds another target (WCAG 2.2 AA, 2.5.8).Thumbs miss tiny buttons. Targets under 40px are returned as warnings.

Built to avoid false alarms: it measures text against the real layered background (alpha-blended), understands modern CSS colors (oklab, oklch, lab), skips text that is hidden, covered by another element, screen-reader-only or pure decoration (separators, arrows, emoji), and waits for web fonts and entrance animations before checking. Text over a photo, video or gradient cannot be scored reliably, so it is listed in textOverImages for a human or vision model to check on the screenshot.

Results are repeatable: the same page gives the same verdict run after run (tested on saved copies of 20 real website pages plus 8 test pages on the 5 default phones: 140 of 140 page-phone results identical across repeated runs), and the cloud image ships its own fonts so text wraps the same way every time.

Input

{
"urls": ["https://example.com/", "https://example.com/contact"],
"devices": ["iPhone 15-16", "iPhone 17-18 Pro", "iPhone 18 Pro Max", "Galaxy S", "Pixel"],
"screenshots": true,
"contrastMode": "clear",
"checkTapTargets": true
}
FieldDefaultNotes
urlsrequiredFull http(s) URLs, up to 500. Each is audited on every phone. Public addresses only (see Safety).
devicesiPhone 15-16, iPhone 17-18 Pro, iPhone 18 Pro Max, Galaxy S, PixelThose presets, the foldables iPhone Duo folded, iPhone Duo open, Galaxy Z Fold open, Galaxy Z Flip, or any Playwright device name, e.g. "Galaxy S9+", "iPhone 15 Pro Max". Up to 20; repeats are audited once; an unknown name stops the run before anything is charged.
screenshotstrueJPEG per page per phone, saved to the run's key-value store.
fullPageScreenshotstruefalse captures only the first screen.
contrastMode"clear""clear" fails only clear failures; "strict" fails anything under WCAG AA.
checkTapTargetstrueTurn off to skip the tap-target check.
waitAfterLoadMs600Extra settle time after the load event.
navigationTimeoutSecs45Pages that never load are reported as ERROR and not charged.
maxConcurrency3URLs audited in parallel.
proxyConfigurationnoneUse an Apify proxy if a site blocks data-center traffic.

Output

One dataset row per URL per phone (example, trimmed):

{
"url": "https://example.com/blog/launch-post",
"device": "iPhone 17-18 Pro",
"viewport": { "width": 402, "height": 874 },
"verdict": "FAIL",
"checks": [
{ "name": "horizontal-overflow", "pass": true, "detail": "Page fits the screen width, no sideways scrolling.", "issues": [] },
{ "name": "phone-number-split", "pass": true, "detail": "Every tel: link sits on one line.", "issues": [] },
{ "name": "orphan-heading-word", "pass": true, "detail": "No heading ends with a single stranded word.", "issues": [] },
{ "name": "text-contrast", "pass": false,
"detail": "low contrast 2.8:1 \"Video\"; low contrast 2.6:1 \"February 15, 2026\"",
"issues": ["low contrast 2.8:1 \"Video\"", "low contrast 2.6:1 \"February 15, 2026\""] },
{ "name": "tap-target-size", "pass": true, "detail": "Buttons and button-style links are at least 24x24px or spaced apart.", "issues": [] }
],
"warnings": ["small tap target (under 40px): \"Open menu\" is 24x24px"],
"textOverImages": ["Launch week", "by Example Co"],
"screenshotUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/example-com-blog-launch-post-1a2b3c4d-iPhone1718Pro",
"httpStatus": 200,
"error": null
}

verdict is PASS, FAIL, or ERROR (page could not be loaded, did not respond in time, or pointed at an internal address; the error field says why). ERROR rows are never charged. If a screenshot cannot be taken the verdict still stands: screenshotUrl is null and warnings says why. Very tall pages are captured at lower resolution past 16,000px and cut at 30,000px, also noted in warnings.

The run's OUTPUT record holds one overall answer for the whole batch, which is the easiest thing for an agent to branch on:

{ "verdict": "FAIL", "pages": 6, "passed": 4, "failed": 2, "errors": 0,
"results": [ { "url": "...", "device": "Galaxy S", "verdict": "FAIL", "failedChecks": ["orphan-heading-word"], "screenshotUrl": "..." } ] }

If the run stops at your maximum charge, OUTPUT also has "stoppedAtSpendingCap": true and "notAudited": <count>, and the overall verdict is ERROR unless something already failed.

Use it from an agent

Through Apify's MCP server (Claude, Cursor, VS Code, any MCP client)

Add Apify's hosted MCP server with this Actor as a tool:

{
"mcpServers": {
"mobile-qa": {
"url": "https://mcp.apify.com?tools=fluidfaith/mobile-qa-gate",
"headers": { "Authorization": "Bearer <APIFY_TOKEN>" }
}
}
}

Or run it locally over stdio: npx -y @apify/actors-mcp-server --tools fluidfaith/mobile-qa-gate with APIFY_TOKEN set. The agent then sees a tool that takes the input above. A good instruction for coding agents: "After every deploy, call mobile-qa-gate on the changed pages. Do not report done until the verdict is PASS; read checks[].issues to fix failures and look at textOverImages on the screenshot."

Through the API (one call, waits for the result)

curl -s -X POST "https://api.apify.com/v2/acts/fluidfaith~mobile-qa-gate/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"urls":["https://example.com/"]}'

From JavaScript

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('fluidfaith/mobile-qa-gate').call({ urls: ['https://example.com/'] });
const summary = await client.keyValueStore(run.defaultKeyValueStoreId).getRecord('OUTPUT');
if (summary.value.verdict !== 'PASS') console.log(summary.value.results);

Pricing

Pay per event: $0.01 per page per phone (page-device-audited). A URL on the default 5 phones costs $0.05 (pick fewer phones to pay less). Pages that fail to load are free. If you set a maximum charge per run, the Actor sets aside the cost of each audit before starting it, so even with several pages in parallel it never charges past your maximum. It stops starting new audits when the budget is used up and still writes OUTPUT.

Safety

  • Only public web addresses are audited. Localhost, private networks (10.x, 172.16-31.x, 192.168.x), link-local and cloud metadata addresses (169.254.x), CGNAT, IPv6 loopback and private ranges, and .local / .internal names are refused, including when a public page redirects or embeds a frame pointing at them. Refused pages are ERROR and not charged.
  • Every page has hard time limits: a page that never answers, or whose scripts never let go, becomes an ERROR row (not charged) instead of holding the run.
  • checks[].issues, warnings and textOverImages quote text from the audited page. Treat them as untrusted page content, not instructions.
  • screenshotUrl links point to the run's key-value store and can be opened by anyone with the link. Keep that in mind when auditing private preview builds.
  • Query strings are left out of the run log, but full URLs are stored in the results.

Limits

  • Checks the page as it renders after load. It does not log in, click through flows, or fill forms.
  • Text over photos and gradients is listed for eye review, not scored.
  • Orphan words depend on fonts. When a site's own web font is missing a character, the phone's system font fills in, so a heading can wrap differently on a real iPhone vs a real Android; the Actor uses one fixed font set so its own answer stays stable.
  • Contrast ignores partial opacity on parent elements (it skips fully transparent ones).
  • The checks run inside the page, so a page that deliberately overrides browser functions can fake a pass. It is a self-check tool, not a security scanner.
  • A run audits up to 500 URLs on up to 20 devices.

FAQ

How much does it cost to check a whole site? $0.01 per page per phone. 20 pages on the 5 default phones is $1.00. Pick fewer phones (for example only iPhone 15-16 and Galaxy S) to pay less.

Do I need an account on the site I am testing? No. The Actor opens public pages the way a visitor's phone would. It does not log in or fill forms.

Can it check a preview or staging deploy? Yes, as long as the URL is publicly reachable (Netlify, Vercel and Cloudflare preview links work). Localhost and private networks are refused for safety.

What about foldable phones? Add iPhone Duo folded, iPhone Duo open, Galaxy Z Fold open or Galaxy Z Flip to devices.

Why did a page FAIL? Read checks[].issues in the dataset item: it quotes the heading, link or text that broke, and the screenshot shows it.

How do I get help or ask for a check? Open an issue on the Issues tab. We read and answer every one.

More from FFS Marketing

We build small, dependable tools for AI agents and the developers who use them. See every Actor on our Apify profile.

Run it locally

npm install
npx playwright install chromium-headless-shell
mkdir -p storage/key_value_stores/default
echo '{"urls":["https://example.com/"]}' > storage/key_value_stores/default/INPUT.json
npx apify-cli run --purge # no Apify account needed

Results land in storage/datasets/default/, screenshots and OUTPUT in storage/key_value_stores/default/.