Visual Verification Agent
Pricing
from $0.20 / actor start
Visual Verification Agent
An AI-powered Virtual QA Engineer that verifies your website's visual integrity across Desktop, Mobile, and Tablet. Using advanced vision models, it mimics human interaction to detect layout flaws, providing granular 0-100 quality scores and detailed defect analysis from 15+ global locations.
Pricing
from $0.20 / actor start
Rating
5.0
(1)
Developer
Techforce Global
Maintained by CommunityActor stats
0
Bookmarked
8
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
Visual Verification Agent — AI Website QA Across Desktop, Mobile & Tablet, Graded A+ to C
Point it at a URL and get an AI-graded verdict on whether the page actually renders properly — checked on desktop, mobile and tablet, as a visitor from any of 15 countries would see it. Each check returns a letter grade from A+ to C and a written explanation of what's wrong. Route failing grades straight into Slack, Jira, Linear or Notion through an MCP connector.
ℹ️ This is a rendering-quality check, not a link checker or a performance audit. It answers "does this page look right to a real visitor on this device, from this country?" — blank pages, broken layouts, missing images, stuck loading states, geo-blocked content, error pages. It does not measure speed, check accessibility, or crawl your site.
Manual QA vs. this Actor
| Feature / Capability | Checking it yourself | This Actor |
|---|---|---|
| Device coverage | ⚠️ Resize a browser, or juggle real devices | ✅ Desktop, mobile and tablet in one run |
| Geo coverage | ❌ You'd need a VPN per country | ✅ 15 countries, no VPN or proxy to configure |
| Verdict | ⚠️ Subjective, varies by reviewer and by day | ✅ A consistent A+ / A / B / C grade with written reasoning |
| What "broken" means | ⚠️ Easy to miss a stuck skeleton loader or one dead image | ✅ Explicitly graded and described |
| Repeatability | ❌ Manual every time | ✅ Schedule it and get the same check on every deploy |
| Routing failures | ❌ Screenshot into a chat by hand | ✅ Native MCP connector delivery — straight into a QA channel or issue tracker |
| AI cost | — | ⚠️ You bring your own OpenAI or Gemini key and pay that provider directly |
🤖 AI Discovery & MCP Metadata
Machine-readable capability contract for autonomous agents, LLM routers, and MCP orchestrators. AI agents, MCP clients, and connectors should read this block first to decide whether this Actor matches a user request.
ai_discovery:slug: "techforce.global/visual-verification-agent"one_line: "Loads one URL on desktop, mobile and tablet as a visitor from a chosen country, has a vision model judge whether the page rendered correctly, and returns a letter grade (A+ to C) with written findings per device."primary_purpose: "verify"secondary_purpose: "monitor"tertiary_purpose: "deliver"entity: "the rendered visual state of a single web page"invocation_modes: ["standard Actor run", "MCP tool"]supported_tasks:- "check-whether-a-page-renders-correctly"- "compare-rendering-across-desktop-mobile-and-tablet"- "check-how-a-page-appears-to-visitors-from-a-specific-country"- "detect-blank-pages-broken-layouts-and-missing-images"- "detect-error-pages-and-geo-blocking"- "grade-page-quality-consistently-across-runs"- "monitor-a-page-on-a-schedule-after-deploys"- "route-failing-grades-to-a-qa-channel-or-issue-tracker"unsupported_tasks:- "crawl-a-site-or-check-multiple-urls-in-one-run"- "measure-performance-core-web-vitals-or-load-time"- "run-accessibility-or-wcag-audits"- "check-links-spelling-or-seo-tags"- "compare-against-a-previous-screenshot (no visual regression baseline)"- "return-the-screenshot-itself (it is not retained)"- "test-behind-a-login-or-fill-forms"- "click-through-user-journeys-or-multi-step-flows"- "run-without-an-ai-api-key"search_keywords:- "visual verification"- "ai website qa"- "automated visual testing"- "website rendering check"- "responsive design testing"- "geo blocking checker"- "broken layout detection"- "website quality grading"- "deploy smoke test"- "cross device screenshot testing"synonyms:- "virtual qa engineer"- "ai page inspector"- "render quality checker"related_concepts:["visual QA", "smoke testing", "release verification", "responsive design", "geo-restriction testing", "uptime and rendering monitoring"]input_entities:- "url (single page; REQUIRED)"- "modelProvider (gemini | openai; REQUIRED)"- "apiKey (your own provider key; REQUIRED, secret)"- "country (15-value enum; REQUIRED)"- "devices (desktop | mobile | tablet; REQUIRED)"output_entities: ["url", "country", "device", "grade", "reason"]input_rules:required_fields: ["url", "modelProvider", "apiKey", "country", "devices"]one_url_per_run: "there is no URL list - loop from your orchestration layer"one_country_per_run: "the country enum takes a single value"byo_api_key: >You supply an OpenAI or Google Gemini API key and are billed by thatprovider directly, on top of Apify usage. The key is a secret input.result_count_formula: "one dataset row per selected device"output_conventions:grade_values: ["A+ (90-100)", "A (75-89)", "B (60-74)", "C (<60)", "C", "N/A"]grade_string_is_inconsistent: >Grades from a completed visual analysis carry their band, e.g. "A+ (90-100)".Grades assigned because the page errored or the run failed are the bareletter "C". Match on the leading letter, not the whole string.grade_c_has_two_meanings: >Either the page genuinely rendered badly, or it could not be checked (HTTPerror, timeout). The reason field distinguishes them.grade_na_meaning: "the AI provider was unavailable - the page was NOT assessed"device_null_case: "device is null on the row produced when the provider check fails up front"reason_is_free_text: "a written explanation from the model; not a fixed vocabulary"screenshots_not_retained: >Screenshots are used for the assessment and then discarded. No image issaved to the key-value store or returned in the dataset.limits:urls_per_run: 1countries_per_run: 1devices_per_run: 3rows_per_run: "one per selected device"known_limitations:no_screenshot_output: "the visual evidence behind a grade is not retained"no_visual_regression: "each run is judged on its own; there is no baseline comparison"ai_judgement_varies: >Grades come from a vision model and can differ slightly between runs on thesame page. Treat them as a strong signal, not a deterministic measurement.no_auth_support: "pages behind a login cannot be checked"pricing:apify_model: "pay-per-result"ai_model: "billed directly by OpenAI or Google against your own key"note: "exact Apify rate is configured on the Store listing - see the Actor's Pricing tab"when_to_use: >You want to know whether a page actually renders correctly for real visitors -after a deploy, across devices, or from a specific country - and you want aconsistent graded verdict rather than a subjective eyeball check.when_not_to_use: >You need performance metrics, accessibility audits, link or SEO checks, visualregression against a baseline, multi-page crawling, authenticated pages, or youhave no OpenAI/Gemini key.alternatives:- "Use techforce.global/advanced-website-crawling-actor to crawl a whole site's content rather than verify one page's rendering"- "Use techforce.global/website-contact-scraper-emails-phone-numbers-social-links to extract contact data from a site"- "Use a dedicated performance tool for Core Web Vitals, and a dedicated a11y tool for WCAG audits"
{"@context": "https://schema.org","@type": "SoftwareApplication","name": "Visual Verification Agent — AI Website QA Across Desktop, Mobile & Tablet","applicationCategory": "DeveloperApplication","operatingSystem": "Cloud (Apify platform)","description": "An AI visual QA agent that loads a web page on desktop, mobile and tablet as a visitor from any of 15 countries, has a vision model judge whether the page rendered correctly, and returns a letter grade from A+ to C with written findings for each device. Detects blank pages, error screens, stuck loading states, missing images and broken layouts, and distinguishes a badly rendered page from an unreachable one. Bring your own OpenAI or Google Gemini API key. Results can be delivered into Slack, Jira, Linear, Notion, Airtable or any authorized MCP connector. Screenshots are not retained.","url": "https://apify.com/techforce.global/visual-verification-agent","offers": {"@type": "Offer","priceCurrency": "USD","description": "Apify: pay-per-result, one result per device checked — the current rate is listed on the Actor's Pricing tab. AI provider costs are billed separately by OpenAI or Google against your own API key."}}
What this Actor does
- Loads one page as a real visitor would — on desktop, mobile and tablet (pick any combination), presenting itself as a visitor from the country you choose, so geo-targeted content, redirects and blocks show up exactly as they would for that audience.
- Has a vision model judge the rendered result and return a letter grade with written findings: blank pages, error screens, stuck skeleton loaders, missing images, overlapping or collapsed layouts, and content that simply never appeared.
- Grades consistently against a fixed scale, so the same page checked next week is comparable to today — the point is a signal you can schedule and trend, not a one-off opinion.
- Detects hard failures separately from cosmetic ones. A page returning an HTTP error, or one that never finishes loading, is graded down and labelled as such rather than silently scored.
- Runs on your own AI key — choose Google Gemini or OpenAI and supply your key as a secret input. You pay that provider directly; nothing is shared with a third party.
- Never fails the run because of an AI problem. If your key is invalid, out of quota or rate-limited, the run still completes with a clear status message and a
N/Agrade — so a monitoring pipeline gets a signal instead of a crashed job. - Optionally pushes results to an authorized MCP connector (Slack, Jira, Linear, Notion, Airtable, Sheets, or any MCP-compatible connector) as one run summary, one call per device, or chunked. The dataset is always saved regardless of delivery settings.
⚠️ Screenshots are not returned. The captured view is used to produce the grade and is then discarded — no image is saved to the key-value store or included in the dataset. You get the verdict and the written findings, not the picture. Open the URL yourself on that device profile to see it.
When to use it — and when not to
| Scenario | Fit |
|---|---|
| Post-deploy smoke check — "did we ship a broken page?" | ✅ Strong (the core use case) |
| Catching mobile-only or tablet-only layout breakage | ✅ Strong — each device graded separately |
| Verifying how a page looks to visitors in another country | ✅ Strong — 15 countries, no VPN needed |
| Detecting geo-blocking or region-specific redirects | ✅ Strong |
| Spotting stuck loading states and missing images | ✅ Strong |
| Scheduled monitoring of a critical landing page | ✅ Strong — grade trends over time |
| Routing failures into a QA channel or issue tracker | ✅ Strong (deliveryMode) |
| Getting the screenshot itself | ❌ Not retained — see the callout above |
| Visual regression against a previous baseline | ❌ Each run is judged on its own |
| Checking many URLs or crawling a site | ❌ One URL per run — loop from your side |
| Performance, load time, Core Web Vitals | ❌ Different tool entirely |
| Accessibility / WCAG audits | ❌ Not assessed |
| Broken links, spelling, SEO tags | ❌ Not assessed |
| Pages behind a login | ❌ No authentication support |
| Multi-step flows (add to cart, checkout) | ❌ Single page load only |
| Running without an AI API key | ❌ Required — the grade comes from a vision model |
⚡ Execution Rules (Avoid Common Agent Failures)
- All five inputs are required —
url,modelProvider,apiKey,country,devices. There are no optional shortcuts; omitting the key fails the run with a clear message. - One URL and one country per run. Both are single values. Checking three pages across two countries is six runs — loop from your orchestration layer.
- You get one dataset row per selected device. Three devices selected means three rows, each graded independently.
gradeis not a clean enum — match on the leading letter. A completed analysis returns the band string ("A+ (90-100)","B (60-74)"); a page that errored or timed out returns the bare letter"C".startswith("C")is safe;== "C"is not.Chas two very different meanings. Either the page genuinely rendered badly, or it couldn't be checked at all (HTTP error, timeout). Readreason— it says which.N/Ameans the page was never assessed. It's produced when your AI provider is unavailable (invalid key, quota exhausted, rate-limited). It is not a passing or failing grade — treat it as "no result" and re-run.- An AI provider problem does not fail the run. The run completes as
SUCCEEDEDwith a status message explaining the issue. Check the status message and grades, not just the run status, or a monitoring pipeline will read a skipped check as a healthy one. - If the provider fails partway through, remaining devices are not checked. Once calls start failing they'll keep failing, so the run stops early rather than burning your quota. Expect fewer rows than devices selected.
- Screenshots are not saved. There is no image in the dataset or key-value store. If you need visual evidence attached to a ticket, capture it separately.
reasonis free text from a vision model, not a fixed vocabulary. Don't pattern-match it for control flow — usegradefor that.- Grades can vary slightly between runs on the same page. That's inherent to AI judgement. For monitoring, alert on a grade dropping a band, not on every point of movement.
- Your AI provider bills you directly, on top of Apify usage. A run makes one vision call per device checked.
countrychanges what the page serves you, not just a label — expect genuinely different content, language or redirects per country. That's the feature.- No login support. Anything behind authentication can't be checked.
deliveryModedefaults tosummary— one connector call per run. UseperResultwhen you want an issue opened per failing device.
dependencies:- rule: "url, modelProvider, apiKey, country and devices are all required"on_violation: "a missing API key fails the run with an explicit status message"- rule: "one URL and one country per run"on_violation: "agents passing arrays get only a single check"- rule: "grade strings are inconsistent - match the leading letter"on_violation: "equality checks against 'C' miss the banded 'C (<60)' form and vice versa"- rule: "grade C means either a bad render OR an unreachable page"on_violation: "a site outage misreported as a design defect - read `reason`"- rule: "grade N/A means the page was not assessed at all"on_violation: "a skipped check counted as a pass"- rule: "AI provider failures complete the run as SUCCEEDED"on_violation: "monitoring reads run status only and never notices the check was skipped"- rule: "a mid-run provider failure stops the remaining devices"on_violation: "fewer rows than devices selected, misread as a crash"- rule: "screenshots are not retained anywhere"on_violation: "a workflow expecting an image attachment finds none"- rule: "the AI provider bills you directly per device checked"on_violation: "cost estimated from the Apify rate alone"- rule: "delivery requires mcpConnector AND mcpTool"on_violation: "delivery step is skipped with a warning; the dataset is still written in full"- rule: "mcpArguments must reference {message} for the rendered template to appear anywhere"on_violation: "message body is empty; the tool is still called"- rule: "mcpConnector only resolves when the Actor runs on the Apify platform"on_violation: "local runs log 'APIFY_MCP_PROXY_URL is not set' and skip delivery; the dataset is unaffected"authentication:actor_input: "your own OpenAI or Google Gemini API key (secret input, REQUIRED)"platform: "Apify API token required for API / SDK / MCP invocation"target_site: "no credentials - public pages only"
📥 Input Contract
.actor/input_schema.json is authoritative — if any table on this page disagrees with the schema files, the schema files win.
Parameters
| Field | Type | Required | Default | Allowed Values / Format | Example |
|---|---|---|---|---|---|
url | string | ✅ Yes | https://apify.com | One page URL | "https://example.com/pricing" |
modelProvider | enum | ✅ Yes | "gemini" | gemini, openai | "openai" |
apiKey | string (secret) | ✅ Yes | — | Your key for the selected provider | "sk-…" / "AIza…" |
country | enum | ✅ Yes | "US" | 15 countries — see below | "DE" |
devices | array | ✅ Yes | all three | desktop, mobile, tablet | ["mobile"] |
Devices
Each selected device is checked independently and produces its own graded row.
| Value | Profile |
|---|---|
desktop | Desktop viewport (1366×768) |
mobile | Phone viewport (390×844) |
tablet | Tablet viewport (768×1024) |
Countries
The page is requested as a visitor from the selected country would see it — so region-specific content, language, redirects and blocks appear as they do for that audience.
🇺🇸 US · 🇬🇧 GB · 🇩🇪 DE · 🇮🇳 IN · 🇸🇬 SG · 🇨🇦 CA · 🇦🇺 AU · 🇧🇷 BR · 🇫🇷 FR · 🇪🇸 ES · 🇮🇹 IT · 🇳🇱 NL · 🇷🇺 RU · 🇯🇵 JP · 🇨🇳 CN
AI provider
You bring your own key and are billed by that provider directly.
| Provider | Value | You need |
|---|---|---|
| Google Gemini | gemini | A Google AI Studio API key with vision access and available quota |
| OpenAI | openai | An OpenAI API key with vision access and available credit |
The key is validated before any page is loaded, so an invalid or exhausted key is reported immediately rather than after a full run.
Examples
Post-deploy smoke check across all three devices
{"url": "https://example.com/","modelProvider": "gemini","apiKey": "<your-gemini-api-key>","country": "US","devices": ["desktop", "mobile", "tablet"],"deliveryMode": "none"}
Mobile-only check — the cheapest useful run
{"url": "https://example.com/pricing","modelProvider": "gemini","apiKey": "<your-gemini-api-key>","country": "GB","devices": ["mobile"],"deliveryMode": "none"}
Geo check — is the page reaching visitors in Germany?
{"url": "https://example.com/de","modelProvider": "openai","apiKey": "<your-openai-api-key>","country": "DE","devices": ["desktop", "mobile"],"deliveryMode": "none"}
Scheduled monitor with a Slack alert
{"url": "https://example.com/","modelProvider": "gemini","apiKey": "<your-gemini-api-key>","country": "US","devices": ["desktop", "mobile"],"mcpConnector": "<your-authorized-slack-connector>","deliveryMode": "summary","mcpTool": "send_message","mcpArguments": { "channel": "#qa-alerts", "text": "{message}" },"mcpMessageTemplate": "🔎 {url} ({country}) — worst grade {worstGrade} across {checkCount} device(s), {failCount} below A.\n\n{results}"}
MCP Delivery Configuration (Optional)
Delivery pushes verification results into your own tools.
| Parameter | Type | Default | Description |
|---|---|---|---|
mcpConnector | string | "" | Authorized MCP connector on your Apify account (Slack, Jira, Linear, Notion, Airtable, Sheets, …). Leave empty for dataset-only runs. |
deliveryMode | enum | summary | summary (one call for the run) · perResult (one call per device) · chunked · none. |
mcpTool | string | "" | Tool name on the connector: send_message, create_issue, create_page, create_record. Run once with a connector selected — the log lists the available tools and the chosen tool's expected argument schema. |
mcpArguments | object | {} | Arguments passed to the tool. String leaves support {placeholders}. |
mcpMessageTemplate | string | "" | Template rendered and exposed to mcpArguments as {message}. |
Per-result placeholders: {url}, {country}, {device}, {grade}, {gradeLetter} (the bare letter — use this for routing), {reason}, {result} (a formatted block), {message}.
Summary / chunked placeholders: {url}, {country}, {devices}, {checkCount}, {worstGrade}, {failCount}, {results}, {part}, {partCount}, {message}.
Open a Jira issue per device checked:
{"url": "https://example.com/","modelProvider": "gemini","apiKey": "<your-gemini-api-key>","country": "US","devices": ["desktop", "mobile", "tablet"],"mcpConnector": "<your-authorized-jira-connector>","deliveryMode": "perResult","mcpTool": "create_issue","mcpArguments": {"summary": "Visual check {gradeLetter} — {device} — {url}","description": "{result}"}}
Log every run to a Notion QA page:
{"url": "https://example.com/","modelProvider": "gemini","apiKey": "<your-gemini-api-key>","country": "US","devices": ["desktop", "mobile"],"mcpConnector": "<your-authorized-notion-connector>","deliveryMode": "summary","mcpTool": "notion-create-pages","mcpArguments": {"parent": { "page_id": "<your-page-id>" },"pages": [{"properties": { "title": "Visual check — {url} — {worstGrade}" },"content": "{results}"}]}}
One Airtable row per device:
{"url": "https://example.com/","modelProvider": "gemini","apiKey": "<your-gemini-api-key>","country": "US","devices": ["desktop", "mobile", "tablet"],"mcpConnector": "<your-authorized-airtable-connector>","deliveryMode": "perResult","mcpTool": "create_record","mcpArguments": {"fields": {"URL": "{url}","Country": "{country}","Device": "{device}","Grade": "{gradeLetter}","Findings": "{reason}"}}}
📤 Output Contract & Data Structure
Storage: Apify Dataset — one row per device checked, written at the end of the run.
Pagination: limit & offset on the dataset items endpoint.
Duplicates: none within a run — each device appears once.
Result record — 5 fields
| Field | Type | Nullable | Meaning |
|---|---|---|---|
url | string | no | The URL you submitted, echoed back |
country | string | no | The country profile used for the check |
device | string | ⚠️ null on the row produced when the AI provider fails before any check runs | desktop, mobile or tablet |
grade | string | no | A+ (90-100) · A (75-89) · B (60-74) · C (<60) · C · N/A — see the warning below |
reason | string | no | Written findings explaining the grade, or the error that prevented a check |
Grade semantics
| Grade | Meaning | What to do |
|---|---|---|
| A+ | Fully rendered, all content visible, looks professional | Nothing |
| A | Good — minor cosmetic imperfection at most | Nothing, or note it |
| B | Noticeable defects: missing images, small layout problems | Investigate |
| C | Either badly broken or not checkable (HTTP error, timeout) | Read reason to tell which — then act |
| N/A | The page was not assessed — your AI provider was unavailable | Fix the key/quota and re-run |
⚠️ Two things to handle in code.
1. The
gradestring is not a clean enum. A completed analysis carries its band ("B (60-74)"); a grade assigned because the page errored or the run failed is the bare letter ("C"). Match on the leading letter, e.g.grade.startswith("C"), notgrade == "C". The MCP delivery exposes a normalised{gradeLetter}for exactly this reason.2.
Cis ambiguous by design. A dead site and an ugly site both land there.reasonis what separates "HTTP Error 503" from "hero image failed to load".
Example — a healthy page
{"url": "https://example.com/","country": "US","device": "desktop","grade": "A+ (90-100)","reason": "Page is fully loaded with all content visible. Navigation, hero section and footer render correctly with no missing assets or layout defects."}
Example — a real defect, mobile only
{"url": "https://example.com/","country": "US","device": "mobile","grade": "B (60-74)","reason": "The hero image failed to load, leaving a blank region at the top of the page. The navigation menu overlaps the headline text at this viewport width."}
Example — page unreachable (also grade C)
{"url": "https://example.com/","country": "US","device": "tablet","grade": "C","reason": "HTTP Error 503: The page returned an error status."}
Example — provider unavailable, nothing assessed
{"url": "https://example.com/","country": "US","device": null,"grade": "N/A","reason": "AI analysis skipped: ❌ Quota Exceeded: Your Google Gemini API key has insufficient credits (Free Tier limit reached)."}
Consuming the results safely
def letter(row):"""Normalise the inconsistent grade string to its bare letter."""grade = (row.get("grade") or "").strip()for candidate in ("A+", "A", "B", "C"):if grade.startswith(candidate):return candidatereturn grade or "?" # "N/A" or emptyassessed = [r for r in items if r["grade"] != "N/A"]if not assessed:raise SystemExit("Nothing was assessed — check the run's status message.")failing = [r for r in assessed if letter(r) in ("B", "C")]for row in failing:# A "C" can mean a dead page rather than a bad design — reason says which.print(f'{row["device"]}: {letter(row)} — {row["reason"]}')
What a successful run looks like
{"status": "SUCCEEDED","defaultDatasetId": "<datasetId>","stats": { "itemCount": 3 }}
itemCount equals the number of devices selected — unless the AI provider failed, in which case you get fewer rows (the run stops early) or a single N/A row.
⚠️
SUCCEEDEDdoes not mean the page passed. It means the Actor ran. Read the grades — and read the run's status message, which is set whenever an AI provider issue prevented a real assessment.
▶️ Invocation & Integration
Lifecycle: Discover → Understand → Configure → Execute → Retrieve → Validate. Authenticate every call with Authorization: Bearer <APIFY_TOKEN>.
Apify Console
Open the Actor → Input tab → enter the Target URL → pick an AI Model Provider and paste your API Key → choose a Country and one or more Devices → (optional) pick an MCP connector, tool and template → Start → read the grades in the Dataset tab.
Model Context Protocol (MCP)
claude mcp add --transport http apify \"https://mcp.apify.com?tools=techforce.global/visual-verification-agent"
Python (apify-client)
import osfrom apify_client import ApifyClientclient = ApifyClient(os.getenv("APIFY_TOKEN"))ACTOR = "techforce.global/visual-verification-agent"run = client.actor(ACTOR).call(run_input={"url": "https://example.com/","modelProvider": "gemini","apiKey": os.environ["GEMINI_API_KEY"], # your key, billed by the provider"country": "US","devices": ["desktop", "mobile", "tablet"],"deliveryMode": "none",})items = client.dataset(run["defaultDatasetId"]).list_items().items# SUCCEEDED means the Actor ran - not that the page passed.print(f'status: {run["status"]} · message: {run.get("statusMessage")}')def letter(row):grade = (row.get("grade") or "").strip()for candidate in ("A+", "A", "B", "C"):if grade.startswith(candidate):return candidatereturn grade or "?"for row in items:print(f'{str(row["device"]):<8} {letter(row):<3} {row["reason"][:90]}')failing = [r for r in items if letter(r) in ("B", "C")]skipped = [r for r in items if r["grade"] == "N/A"]if skipped:print("⚠️ Some checks were skipped — the AI provider was unavailable.")elif failing:raise SystemExit(f"{len(failing)} device(s) below grade A")
Multi-page or multi-country sweep — one run per combination:
PAGES = ["https://example.com/", "https://example.com/pricing"]COUNTRIES = ["US", "DE"]for page in PAGES:for country in COUNTRIES:run = client.actor(ACTOR).call(run_input={"url": page, "country": country,"modelProvider": "gemini", "apiKey": os.environ["GEMINI_API_KEY"],"devices": ["desktop", "mobile"], "deliveryMode": "none",})rows = client.dataset(run["defaultDatasetId"]).list_items().itemsworst = min((letter(r) for r in rows), key="A+ A B C".split().index, default="?")print(f"{page} [{country}] → worst grade {worst}")
JavaScript / TypeScript (apify-client)
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const ACTOR = 'techforce.global/visual-verification-agent';const run = await client.actor(ACTOR).call({url: 'https://example.com/',modelProvider: 'gemini',apiKey: process.env.GEMINI_API_KEY,country: 'US',devices: ['desktop', 'mobile'],deliveryMode: 'none',});const { items } = await client.dataset(run.defaultDatasetId).listItems();// Grade strings are inconsistent — match the leading letter.const letter = (row: any) =>['A+', 'A', 'B', 'C'].find((c) => (row.grade ?? '').startsWith(c)) ?? row.grade ?? '?';const failing = items.filter((r) => ['B', 'C'].includes(letter(r)));const skipped = items.filter((r) => r.grade === 'N/A');console.log(`${items.length} checked · ${failing.length} failing · ${skipped.length} skipped`);if (skipped.length) console.warn('AI provider was unavailable — results incomplete.');
cURL — synchronous run (small runs; 300s limit)
curl -X POST \-H 'Content-Type: application/json' \-H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' \-d '{"url":"https://example.com/","modelProvider":"gemini","apiKey":"<your-key>","country":"US","devices":["mobile"],"deliveryMode":"none"}' \'https://api.apify.com/v2/acts/techforce.global~visual-verification-agent/run-sync-get-dataset-items'
cURL — asynchronous with a spend cap (recommended)
# 1. Start execution with a hard spend limitcurl -X POST -H 'Content-Type: application/json' \-H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' \-d '{"url":"https://example.com/","modelProvider":"gemini","apiKey":"<your-key>","country":"US","devices":["desktop","mobile","tablet"],"deliveryMode":"none"}' \'https://api.apify.com/v2/acts/techforce.global~visual-verification-agent/runs?maxTotalChargeUsd=0.50'# 2. Poll — read statusMessage, not just statuscurl -H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' \'https://api.apify.com/v2/actor-runs/<runId>'# 3. Retrieve when SUCCEEDEDcurl -H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' \'https://api.apify.com/v2/datasets/<DATASET_ID>/items?clean=true&format=json'
CI / scheduling
The natural pattern is an Apify schedule (or a post-deploy webhook) per critical page, with deliveryMode: "summary" posting into a QA channel. In CI, fail the pipeline when any device comes back below A — but treat N/A as inconclusive, not as a pass or a failure.
⚠️ Autonomous Agent Error Handling & Resolution Matrix
| Error Code | Detection Condition | Underlying Cause | Deterministic Agent Action |
|---|---|---|---|
AUTH_INVALID | HTTP 401 from Apify | Missing or malformed APIFY_TOKEN | Abort & Prompt User: request a valid Apify API token. |
RATE_LIMITED | HTTP 429 from Apify | Apify API rate / concurrency limits | Retry: exponential backoff (2s, 4s, 8s). |
SYNC_TIMEOUT | HTTP 408 on the sync endpoint | Multi-device runs can exceed the 300s synchronous ceiling | Reconfigure: async POST /runs + polling. |
MISSING_API_KEY | Run FAILED: API Key is required for <provider> provider. | apiKey empty | Modify Input: supply a key for the selected provider. |
UNKNOWN_PROVIDER | Run FAILED: Unknown provider: <x> | modelProvider not gemini or openai | Modify Input: use a valid provider value. |
AI_KEY_INVALID | SUCCEEDED, one row, grade: "N/A", reason mentions an invalid key | The key was rejected by the provider | Prompt User: supply a valid key, then re-run. |
AI_QUOTA_EXCEEDED | SUCCEEDED, grade: "N/A", reason mentions quota or credits | Provider quota or credit exhausted | Prompt User: top up or wait, then re-run. |
AI_PROVIDER_MIDRUN | SUCCEEDED, fewer rows than devices, one row N/A | The provider failed partway; remaining devices were skipped to protect your quota | Retry once the provider is healthy. |
CHECK_SKIPPED_NOT_PASSED | Consumer treats N/A as a pass | N/A means the page was not assessed | Fix Logic: treat as inconclusive; re-run. |
SUCCESS_NOT_PASS | Consumer treats run status as the verdict | SUCCEEDED only means the Actor ran | Fix Logic: read grades and the run's statusMessage. |
GRADE_STRING_MISMATCH | grade == "C" matches some rows but not others | Grades are banded ("C (<60)") or bare ("C") | Fix Code: match the leading letter. |
GRADE_C_AMBIGUOUS | A site outage reported as a design defect | C covers both bad rendering and unreachable pages | Fix Logic: branch on reason. |
PAGE_HTTP_ERROR | grade: "C", reason starts HTTP Error <code> | The page returned an error status | Escalate: this is an availability incident, not a QA finding. |
PAGE_TIMEOUT | grade: "C", reason mentions a timeout | The page took too long to become checkable (often infinite scroll) | Retry; if persistent, the page is too heavy to verify this way. |
NO_SCREENSHOT | Workflow expects an image | Screenshots are not retained | Fix Expectation: capture separately if you need evidence. |
NO_BASELINE | User wants visual regression | Each run is judged on its own | Fix Expectation: compare grades across runs instead. |
MULTI_URL_UNSUPPORTED | Agent passes a URL list | One URL per run | Loop: one run per URL. |
MULTI_COUNTRY_UNSUPPORTED | Agent passes a country list | One country per run | Loop: one run per country. |
AUTH_PAGE_REQUESTED | Target needs a login | No authentication support | Abort & Prompt User: public pages only. |
WRONG_TOOL_PERF | User wants load time / Core Web Vitals | Not measured | Re-route: a performance tool. |
WRONG_TOOL_A11Y | User wants a WCAG audit | Not assessed | Re-route: an accessibility tool. |
GRADE_VARIANCE | The same page grades differently between runs | AI judgement is not deterministic | Fix Logic: alert on a band drop, not on small movement. |
DELIVERY_SKIPPED | Log no tool name was provided | mcpConnector set but mcpTool empty | Modify Input: set mcpTool; the dataset is already saved. |
DELIVERY_TOOL_UNKNOWN | Log Tool 'x' is not available on this connector | Wrong tool name for that connector | Modify Input: pick a name from the Available tools: list in the same log line. |
DELIVERY_ARGS_INVALID | Connector rejects the arguments | mcpArguments doesn't match the tool's schema | Read the log: the line Connector [<tool>] expects arguments matching this schema: prints the exact shape. |
DELIVERY_EMPTY | Connector called, body empty | mcpArguments omitted the {message} placeholder | Modify Input: map {message} inside mcpArguments. |
MCP_PROXY_MISSING | Log APIFY_MCP_PROXY_URL is not set | Running locally instead of on the platform | Reconfigure: apify push and run on the platform. |
CHARGE_LIMIT | Run aborted before all devices | maxTotalChargeUsd spend threshold reached | Prompt User: confirm a higher spend cap. |
Reading run status:
SUCCEEDEDwith an empty dataset is a valid outcome here, not a failure — only aFAILEDstatus is a platform-level problem. Every Actor-side row, quoted message and field condition in the table above comes from this Actor’s own behaviour; the401/408/429rows are standard Apify API responses.
🗣️ Natural Language → Actor Mapping
| User says | Intent | Constructed Actor input |
|---|---|---|
| "Is our homepage broken?" | Smoke check | {"url":"<home>","country":"US","devices":["desktop","mobile","tablet"]} |
| "Check the new landing page before we launch" | Release gate | all three devices, fail the pipeline below grade A |
| "Does the site look right on mobile?" | Responsive check | {"devices":["mobile"]} |
| "Our German visitors say the page is blank" | Geo check | {"country":"DE","devices":["desktop","mobile"]} |
| "Are we geo-blocked in China?" | Geo-restriction | {"country":"CN","devices":["desktop"]} |
| "Did the deploy break anything visually?" | Post-deploy | schedule/webhook run, alert on a grade drop |
| "Monitor our pricing page daily" | Monitoring | Apify schedule + deliveryMode: "summary" to Slack |
| "Open a ticket for anything broken" | Issue tracking | {"mcpConnector":"jira","deliveryMode":"perResult","mcpTool":"create_issue"} |
| "Which device has the problem?" | Diagnosis | run all three; each is graded separately |
| "Why did it get a C?" | Explanation | read reason — it distinguishes a bad render from an unreachable page |
| "Send me the screenshot" | Evidence | ❌ Not retained — the grade and findings are the output |
| "Compare it to how it looked last week" | Visual regression | ❌ No baseline — compare grades across runs instead |
| "Check all 40 pages on our site" | Crawl | ❌ One URL per run — loop, or use a crawler for content |
| "How fast does it load?" | Performance | ❌ Not measured |
| "Is it accessible?" | A11y audit | ❌ Not assessed |
| "Check the page after logging in" | Authenticated | ❌ No login support |
| "Add to cart and check the checkout" | User journey | ❌ Single page load only |
Should NOT route here: performance, load time or Core Web Vitals · accessibility / WCAG audits · broken-link, spelling or SEO checks · visual regression against a stored baseline · retrieving the screenshot itself · crawling or checking many URLs in one run · authenticated pages and multi-step user journeys · any run without an OpenAI or Gemini key.
🧭 Agent Execution & Routing Logic
[Input User Query]│▼1. Is the question "does this page RENDER correctly?" NO → Route elsewhere│ YES (performance / a11y / SEO / crawling▼ are all different tools)2. Does the user need the SCREENSHOT, or a comparison to a baseline?│ YES → Abort: images are not retained and there is no visual regression baseline▼3. Is the page public? NO → Abort: no login support▼4. ── PRECONDITION ── Is an OpenAI or Gemini API key available?│ NO → Abort and say why: the grade comes from a vision model, and the│ key is a required input billed to the user's own provider account│ YES → set modelProvider + apiKey▼5. One URL and one country per run.Several pages or markets → LOOP, one run per combination▼6. Pick devices — each one is a separate graded row and a separate AI call:├── quick/cheap check → ["mobile"] (where breakage usually shows first)└── release gate → ["desktop","mobile","tablet"]▼7. Deliver to a connector? YES → mcpConnector + mcpTool + {message}│ summary (1 call) / perResult (1 per device) / chunked│ NO → set deliveryMode "none" (it defaults to summary)▼[Execute]│▼8. INTERPRET CAREFULLY — run status is NOT the verdict:│├── any row grade "N/A" → the page was NOT ASSESSED (AI provider issue).│ Read the run's statusMessage. Treat as INCONCLUSIVE:│ never count it as a pass. Fix the key/quota, re-run.│├── grade starts "C" → read `reason`:│ "HTTP Error..." or timeout → AVAILABILITY incident, escalate as an outage│ anything else → genuine rendering failure, raise a QA ticket│├── grade starts "B" → real defect worth investigating│└── grade starts "A" → healthy│▼9. Matching grades: use the LEADING LETTER, never string equality —a graded result is "B (60-74)" but an errored one is bare "C".
💰 Cost Control & Pricing Transparency
Two separate bills — budget for both.
| Cost | Who charges you | Driver |
|---|---|---|
| Apify: pay-per-result | Apify | One result per device checked. Rate on the Actor's Pricing tab |
| AI provider | OpenAI or Google, directly against your key | One vision call per device checked |
| Apify platform usage | Apify | Browser time — one page load and capture per device |
ℹ️ Always confirm the live rate on the Actor’s Apify Store page before committing to a large run. The Store listing is what bills you; the figures above are a worked example at the advertised rate. Apify platform usage is billed separately.
The multiplier is devices × runs
| Configuration | Rows | AI calls |
|---|---|---|
| 1 device, 1 run | 1 | 1 |
| 3 devices, 1 run | 3 | 3 |
| 3 devices, daily for a month | ~90 | ~90 |
| 3 devices × 2 countries × 5 pages | 30 per sweep | 30 per sweep |
Cost levers:
- Check one device for routine monitoring. Mobile is where breakage usually shows first; save all three for release gates.
- Don't over-schedule. A page that changes on deploy needs checking on deploy, not hourly.
- A failed key costs almost nothing. The key is validated before any page is loaded, so a bad key ends the run early instead of burning browser time.
- Choose your provider on price. Both are supported; the cheaper one is whichever your account already has favourable rates on.
- Cap the Apify spend with
maxTotalChargeUsdon any agent-driven run — note this does not cap your AI provider spend, which is billed outside Apify.
🔍 Companion machine-readable files
| File | Purpose |
|---|---|
.actor/actor.json | Identity, title, description, version, build tag, and the input / output / dataset schema references |
.actor/input_schema.json | Authoritative typed input contract — the five required fields, the 15-country and 3-device enums, and the 5 delivery fields |
.actor/dataset_schema.json | Console Output view — device, grade, findings, country, URL |
.actor/output_schema.json | Declares where results are stored |
If any table on this page disagrees with the schema files, the schema files win.
🛠️ Troubleshooting
| # | Symptom you see | Most likely cause | Fix |
|---|---|---|---|
| 1 | Run FAILED: API Key is required… | apiKey empty | Supply a key for the selected provider. |
| 2 | One row, grade: "N/A", reason mentions an invalid key | The provider rejected the key | Check you pasted the key for the provider you selected. |
| 3 | grade: "N/A", reason mentions quota or credits | Provider quota or credit exhausted | Top up or wait, then re-run. |
| 4 | Fewer rows than devices selected | The provider failed partway; remaining devices were skipped to protect your quota | Re-run once the provider is healthy. |
| 5 | Run says SUCCEEDED but nothing was checked | AI issues complete the run rather than failing it | Read the run's status message and the grades. |
| 6 | grade == "C" matches some rows but not others | Grades are banded ("C (<60)") or bare ("C") | Match the leading letter, not the whole string. |
| 7 | A site outage looks like a design defect | C covers both | Read reason — an HTTP error says so explicitly. |
| 8 | grade: "C", reason mentions a timeout | The page took too long to become checkable — often infinite scroll | Retry; very heavy pages may not be verifiable this way. |
| 9 | No screenshot anywhere | Screenshots are not retained | Capture separately if you need visual evidence. |
| 10 | Grades move slightly between identical runs | AI judgement isn't deterministic | Alert on a band drop, not on small movement. |
| 11 | A grade seems too harsh or too kind | Same cause — it's a model's judgement | Read reason; if it describes something real, trust it. |
| 12 | device is null on a row | That's the row produced when the provider fails before any check | Expected — nothing was assessed. |
| 13 | Only one country came back | One country per run | Loop, one run per country. |
| 14 | Only one URL was checked | One URL per run | Loop, one run per URL. |
| 15 | Page behind a login can't be checked | No authentication support | Public pages only. |
| 16 | Content differs from what you expected | country genuinely changes what the site serves | That's the feature — check the country you meant. |
| 17 | HTTP 408 on run-sync-get-dataset-items | The synchronous endpoint has a hard 300-second ceiling | Use async POST /runs → poll → fetch. |
| 18 | Connector rejected the arguments | mcpArguments doesn't match the tool's schema | Read the Connector [<tool>] expects arguments matching this schema: log line. |
| 19 | Slack / Jira received nothing | Delivery needs both mcpConnector and mcpTool | Set both. The dataset is still written in full. |
| 20 | Connector fired but the body is empty | mcpArguments did not reference {message} | Map {message} and populate mcpMessageTemplate. |
| 21 | Log APIFY_MCP_PROXY_URL is not set | You ran locally; connectors only resolve on the platform | Deploy with apify push and run on the platform. |
| 22 | HTTP 401 / 403 from Apify | Missing, expired or malformed APIFY_TOKEN | Regenerate in Apify Console → Settings → API & Integrations. |
| 23 | HTTP 429 from Apify | Apify account concurrency / rate limits | Retry with exponential backoff (2s → 4s → 8s). |
Known limitations
- The screenshot behind a grade is not retained — you get the verdict and the written findings, not the image.
- No visual regression baseline — each run is judged on its own merits, so compare grades across runs rather than pixels.
- AI judgement is not deterministic. Grades can shift slightly between identical runs; alert on band changes.
- One URL and one country per run.
- No authenticated pages, no multi-step journeys, no performance or accessibility measurement.
- An AI key is mandatory, and its cost is yours.
Diagnostic checklist before opening an issue
- Check the run's status message. Any AI provider issue is reported there, and it's the difference between "the page is fine" and "the page was never checked".
- Baseline run. A URL you know renders correctly,
devices: ["desktop"]— confirms your key and the pipeline in one cheap check. - Grade A+ or A? Then the pipeline works, and the earlier result was about the page, not the Actor.
- Read
reasonon any C. An HTTP error or timeout means the page couldn't be checked — a very different problem from a bad layout. - Open the URL yourself at the same device profile and country. If you see what
reasondescribes, the grade is doing its job.
If the issue survives all five steps, open an Issues ticket on the Actor page (or email support) with the run ID, the exact input JSON (without your API key), and what you expected the grade to be.
❓ FAQ
Setup
Do I need my own AI API key?
Yes — it's required. Choose Google Gemini or OpenAI, paste your key as a secret input, and that provider bills you directly for one vision call per device checked. The key is validated before any page is loaded, so mistakes are cheap.
Which provider should I pick?
Whichever you already have credit and favourable rates on. Both are supported and both produce a grade and written findings in the same output shape.
Is my API key safe?
It's a secret input, stored encrypted by Apify and used only to call the provider you selected. It never appears in the dataset or the run log.
Can I plug it into Claude, Cursor, or a LangChain agent?
Yes — it is a native MCP tool:
claude mcp add --transport http apify "https://mcp.apify.com?tools=techforce.global/visual-verification-agent"
What it checks
What counts as "broken"?
Blank pages, error screens, stuck loading placeholders, missing images, overlapping or collapsed layouts, and content that never appeared. The grade tells you how bad it is; reason tells you what it saw.
Does the country setting really change anything?
Yes — the page is requested as a visitor from that country, so geo-targeted content, languages, redirects and regional blocks show up as they would for that audience. Checking CN or DE can produce genuinely different results from US.
Can it check pages behind a login?
No — public pages only.
Does it measure speed or accessibility?
No. This is a rendering-quality check. Use a dedicated performance tool for Core Web Vitals and a dedicated accessibility tool for WCAG.
Can it check my whole site?
Not in one run — it takes a single URL. Loop it over your key pages from your own orchestration layer.
Interpreting results
What does grade C mean exactly?
Either the page rendered badly, or it couldn't be checked at all — an HTTP error or a timeout. reason tells you which, and the difference matters: one is a QA ticket, the other is an outage.
What does N/A mean?
The page was not assessed — your AI provider was unavailable. It's neither a pass nor a fail. Fix the key or quota and re-run.
Why does grade == "C" miss some rows?
Because a graded result carries its band ("C (<60)") while an errored one is the bare letter ("C"). Match the leading letter instead — the MCP delivery exposes a normalised {gradeLetter} for this.
Why did the run succeed when the page is broken?
SUCCEEDED means the Actor ran, not that the page passed. The verdict is in the grades.
Can I see the screenshot it graded?
No — it isn't retained. You get the grade and the written findings.
Why did the same page get a different grade today?
The grade comes from a vision model, so small variation is normal. For monitoring, alert when a grade drops a band rather than on every point of movement.
Pricing
How is this billed?
Two bills: Apify charges pay-per-result (one result per device), and your AI provider charges you directly for one vision call per device. Apify platform usage (browser time) is billed separately again.
How do I keep it cheap?
Check one device for routine monitoring, all three only for release gates, and don't schedule more often than the page actually changes.
Does maxTotalChargeUsd cap everything?
No — it caps the Apify spend only. Your AI provider spend is outside Apify's control; manage it with quotas on the provider side.
🔗 Related Actors
Different questions about the same website
| Actor | Answers | Why pick it over this one |
|---|---|---|
| This Actor | "Does this page render correctly for real visitors?" | Graded visual verdict per device and country |
| Advanced Website Crawling Actor | "What content is on this site?" | Crawls many pages and extracts their content |
| Website Contact Scraper | "How do I contact this business?" | Emails, phones and social links from a site |
| Google Maps Business Leads & Sales Intelligence | "Which local businesses exist, and how good are their sites?" | Finds businesses and grades their web presence at scale |
ℹ️ Complementary, not overlapping. This Actor verifies one page deeply across devices and geographies; the crawler covers many pages shallowly. Use the crawler to find your key URLs, then verify each one here.
Suggested pipeline patterns
🚦 Release gate
Post-deploy webhook → check your top 3 pages on all devices → fail the pipeline if any grade drops below A, and treat N/A as inconclusive rather than a pass.
📉 Rendering monitor
Daily schedule per critical page, mobile only → deliveryMode: "summary" into a QA channel → alert when a grade drops a band.
🌍 Geo rollout check Before launching in a new market, run the same URL across the countries you serve → confirm nothing is blocked, mistranslated or redirected wrongly.
🎫 Auto-triaged QA tickets
deliveryMode: "perResult" into Jira or Linear with {gradeLetter} in the summary → each failing device becomes its own ticket with the findings attached.
🔎 Crawl, then verify Advanced Website Crawling Actor to enumerate your important URLs → loop this Actor over them → a rendering health report for the whole site.
Browse all Actors by Techforce Global at scraper.techforce.global.
🔐 Compliance & Data Privacy
This Actor loads a single public web page that you specify and assesses how it rendered. It does not log in, does not submit forms, does not crawl beyond the page you give it, and does not extract content from the page into the dataset — the output is a grade and a written assessment.
Four things worth being deliberate about:
- Point it at pages you own or are authorised to test. It's built for verifying your own properties and those of clients who've engaged you. Running repeated automated checks against a third party's site without permission is a different activity with different risks.
- Your API key goes to your chosen AI provider. The page's rendered view is sent to OpenAI or Google for assessment under their terms and data-handling policies — review those if the page contains anything sensitive. Nothing is sent to any other third party, and the captured view is discarded after the assessment.
- Don't verify pages showing personal data. A page rendered while logged in, or one displaying customer records, would transmit that content to the AI provider. This Actor has no login support precisely because it's meant for public pages; keep it that way.
- A grade is an AI's judgement, not a certification. It's a strong operational signal, and it can be wrong. Don't present it as a compliance attestation, an accessibility conformance claim, or a guarantee about someone else's website.
You remain responsible for your AI provider's terms, for having authorisation to test the target page, and for applicable data-protection law where the page or its content involves personal data.
🆘 Support & Custom Pipeline Engineering
Need multi-URL batch verification, retained screenshots, visual regression against a baseline, custom grading criteria for your design system, or CI integration that gates deploys on rendering health?
- Email: bhavin.shah@techforceglobal.com
- Custom Enterprise Integrations: Book a 15-Minute Technical Consultation
- Maintained by: Techforce Global — Specialists in High-Performance Web Scrapers and Agentic Workflows.
Made with ❤️ by Techforce Global Specialists in High-Performance Web Scrapers and AI Automation.
🏷️ Structured data for search & AI discovery
{"@context": "https://schema.org","@type": "SoftwareApplication","name": "Visual Verification Agent — AI Website QA Across Desktop, Mobile & Tablet","applicationCategory": "DeveloperApplication","operatingSystem": "Cloud (Apify platform)","description": "An AI visual QA agent that loads a web page on desktop, mobile and tablet as a visitor from any of 15 countries, has a vision model judge whether the page rendered correctly, and returns a letter grade from A+ to C with written findings for each device. Detects blank pages, error screens, stuck loading states, missing images and broken layouts, and distinguishes a badly rendered page from an unreachable one. Bring your own OpenAI or Google Gemini API key. Results can be delivered into Slack, Jira, Linear, Notion, Airtable or any authorized MCP connector. Screenshots are not retained.","url": "https://apify.com/techforce.global/visual-verification-agent","offers": {"@type": "Offer","priceCurrency": "USD","description": "Apify: pay-per-result, one result per device checked — the current rate is listed on the Actor's Pricing tab. AI provider costs are billed separately by OpenAI or Google against your own API key."}}
{"@context": "https://schema.org","@type": "FAQPage","mainEntity": [{"@type": "Question","name": "Do I need my own AI API key?","acceptedAnswer": {"@type": "Answer","text": "Yes - it's required. Choose Google Gemini or OpenAI, paste your key as a secret input, and that provider bills you directly for one vision call per device checked. The key is validated before any page is loaded, so mistakes are cheap."}},{"@type": "Question","name": "Which provider should I pick?","acceptedAnswer": {"@type": "Answer","text": "Whichever you already have credit and favourable rates on. Both are supported and both produce a grade and written findings in the same output shape."}},{"@type": "Question","name": "Is my API key safe?","acceptedAnswer": {"@type": "Answer","text": "It's a secret input, stored encrypted by Apify and used only to call the provider you selected. It never appears in the dataset or the run log."}},{"@type": "Question","name": "Can I plug it into Claude, Cursor, or a LangChain agent?","acceptedAnswer": {"@type": "Answer","text": "Yes - it is a native MCP tool:"}},{"@type": "Question","name": "What counts as \"broken\"?","acceptedAnswer": {"@type": "Answer","text": "Blank pages, error screens, stuck loading placeholders, missing images, overlapping or collapsed layouts, and content that never appeared. The grade tells you how bad it is; reason tells you what it saw."}},{"@type": "Question","name": "Does the country setting really change anything?","acceptedAnswer": {"@type": "Answer","text": "Yes - the page is requested as a visitor from that country, so geo-targeted content, languages, redirects and regional blocks show up as they would for that audience. Checking CN or DE can produce genuinely different results from US."}},{"@type": "Question","name": "Can it check pages behind a login?","acceptedAnswer": {"@type": "Answer","text": "No - public pages only."}},{"@type": "Question","name": "Does it measure speed or accessibility?","acceptedAnswer": {"@type": "Answer","text": "No. This is a rendering-quality check. Use a dedicated performance tool for Core Web Vitals and a dedicated accessibility tool for WCAG."}},{"@type": "Question","name": "Can it check my whole site?","acceptedAnswer": {"@type": "Answer","text": "Not in one run - it takes a single URL. Loop it over your key pages from your own orchestration layer."}},{"@type": "Question","name": "What does grade C mean exactly?","acceptedAnswer": {"@type": "Answer","text": "Either the page rendered badly, or it couldn't be checked at all - an HTTP error or a timeout. reason tells you which, and the difference matters: one is a QA ticket, the other is an outage."}},{"@type": "Question","name": "What does N/A mean?","acceptedAnswer": {"@type": "Answer","text": "The page was not assessed - your AI provider was unavailable. It's neither a pass nor a fail. Fix the key or quota and re-run."}},{"@type": "Question","name": "Why does grade == \"C\" miss some rows?","acceptedAnswer": {"@type": "Answer","text": "Because a graded result carries its band (\"C (<60)\") while an errored one is the bare letter (\"C\"). Match the leading letter instead - the MCP delivery exposes a normalised {gradeLetter} for this."}},{"@type": "Question","name": "Why did the run succeed when the page is broken?","acceptedAnswer": {"@type": "Answer","text": "SUCCEEDED means the Actor ran, not that the page passed. The verdict is in the grades."}},{"@type": "Question","name": "Can I see the screenshot it graded?","acceptedAnswer": {"@type": "Answer","text": "No - it isn't retained. You get the grade and the written findings."}},{"@type": "Question","name": "Why did the same page get a different grade today?","acceptedAnswer": {"@type": "Answer","text": "The grade comes from a vision model, so small variation is normal. For monitoring, alert when a grade drops a band rather than on every point of movement."}},{"@type": "Question","name": "How is this billed?","acceptedAnswer": {"@type": "Answer","text": "Two bills: Apify charges pay-per-result (one result per device), and your AI provider charges you directly for one vision call per device. Apify platform usage (browser time) is billed separately again."}},{"@type": "Question","name": "How do I keep it cheap?","acceptedAnswer": {"@type": "Answer","text": "Check one device for routine monitoring, all three only for release gates, and don't schedule more often than the page actually changes."}},{"@type": "Question","name": "Does maxTotalChargeUsd cap everything?","acceptedAnswer": {"@type": "Answer","text": "No - it caps the Apify spend only. Your AI provider spend is outside Apify's control; manage it with quotas on the provider side."}}]}


