# Visual Verification Agent (`techforce.global/visual-verification-agent`) Actor

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.

- **URL**: https://apify.com/techforce.global/visual-verification-agent.md
- **Developed by:** [Techforce Global](https://apify.com/techforce.global) (community)
- **Categories:** AI, Developer tools, Automation
- **Stats:** 8 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $0.20 / actor start

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Visual Verification Agent — AI Website QA Across Desktop, Mobile & Tablet, Graded A+ to C

[![Apify Actor](https://img.shields.io/badge/Apify-Actor-orange?logo=apify)](https://apify.com/techforce.global/visual-verification-agent)
[![MCP Ready](https://img.shields.io/badge/MCP-Compatible-blue?logo=anthropic)](https://mcp.apify.com?tools=techforce.global/visual-verification-agent)
[![Devices](https://img.shields.io/badge/Devices-desktop%20%C2%B7%20mobile%20%C2%B7%20tablet-blueviolet)](#devices)
[![Countries](https://img.shields.io/badge/Countries-15-darkgreen)](#countries)
[![BYO key](https://img.shields.io/badge/AI-bring%20your%20own%20key-red)](#-input-contract)

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

```yaml
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 that
      provider 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 bare
      letter "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 (HTTP
      error, 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 is
      saved to the key-value store or returned in the dataset.
  limits:
    urls_per_run: 1
    countries_per_run: 1
    devices_per_run: 3
    rows_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 the
      same 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 a
    consistent graded verdict rather than a subjective eyeball check.
  when_not_to_use: >
    You need performance metrics, accessibility audits, link or SEO checks, visual
    regression against a baseline, multi-page crawling, authenticated pages, or you
    have 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"
```

***

```json
{
    "@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/A` grade — 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)

1. **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.
2. **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.
3. **You get one dataset row per selected device.** Three devices selected means three rows, each graded independently.
4. **`grade` is 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.
5. **`C` has two very different meanings.** Either the page genuinely rendered badly, *or* it couldn't be checked at all (HTTP error, timeout). **Read `reason`** — it says which.
6. **`N/A` means 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.
7. **An AI provider problem does not fail the run.** The run completes as `SUCCEEDED` with 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.
8. **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.
9. **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.
10. **`reason` is free text from a vision model**, not a fixed vocabulary. Don't pattern-match it for control flow — use `grade` for that.
11. **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.
12. **Your AI provider bills you directly**, on top of Apify usage. A run makes one vision call per device checked.
13. **`country` changes what the page serves you**, not just a label — expect genuinely different content, language or redirects per country. That's the feature.
14. **No login support.** Anything behind authentication can't be checked.
15. **`deliveryMode` defaults to `summary`** — one connector call per run. Use `perResult` when you want an issue opened per failing device.

```yaml
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**

```json
{
  "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**

```json
{
  "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?**

```json
{
  "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**

```json
{
  "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:

```json
{
  "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:

```json
{
  "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:

```json
{
  "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 `grade` string 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")`, not `grade == "C"`. The MCP delivery exposes a normalised `{gradeLetter}` for exactly this reason.
>
> **2. `C` is ambiguous by design.** A dead site and an ugly site both land there. `reason` is what separates "HTTP Error 503" from "hero image failed to load".

#### Example — a healthy page

```json
{
  "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

```json
{
  "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)

```json
{
  "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

```json
{
  "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

```python
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 candidate
    return grade or "?"          # "N/A" or empty

assessed = [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

```json
{
  "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.

> ⚠️ **`SUCCEEDED` does 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)

```bash
claude mcp add --transport http apify \
  "https://mcp.apify.com/?tools=techforce.global/visual-verification-agent"
```

#### Python (apify-client)

```python
import os
from apify_client import ApifyClient

client = 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 candidate
    return 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:

```python
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().items
        worst = 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)

```typescript
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)

```bash
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)

```bash
## 1. Start execution with a hard spend 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":["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 status
curl -H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' \
  'https://api.apify.com/v2/actor-runs/%3CrunId%3E'

## 3. Retrieve when SUCCEEDED
curl -H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' \
  'https://api.apify.com/v2/datasets/%3CDATASET_ID%3E/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:** `SUCCEEDED` with an empty dataset is a valid outcome here, not a failure — only a `FAILED` status 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; the `401`/`408`/`429` rows 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

```text
[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](https://apify.com/techforce.global/visual-verification-agent)** |
| **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](https://apify.com/techforce.global/visual-verification-agent) 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 `maxTotalChargeUsd` on 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

1. **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".
2. **Baseline run.** A URL you know renders correctly, `devices: ["desktop"]` — confirms your key and the pipeline in one cheap check.
3. **Grade A+ or A?** Then the pipeline works, and the earlier result was about the page, not the Actor.
4. **Read `reason` on any C.** An HTTP error or timeout means the page couldn't be checked — a very different problem from a bad layout.
5. **Open the URL yourself** at the same device profile and country. If you see what `reason` describes, 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](https://apify.com/techforce.global/advanced-website-crawling-actor) | "What **content** is on this site?" | Crawls many pages and extracts their content |
| [Website Contact Scraper](https://apify.com/techforce.global/website-contact-scraper-emails-phone-numbers-social-links) | "How do I **contact** this business?" | Emails, phones and social links from a site |
| [Google Maps Business Leads & Sales Intelligence](https://apify.com/techforce.global/google-maps-leads-sales-intelligence-tool) | "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](https://apify.com/techforce.global/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](https://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](https://calendly.com/techforce-global/intro-meeting)
- **Maintained by**: [Techforce Global](https://www.techforceglobal.com) — Specialists in High-Performance Web Scrapers and Agentic Workflows.

***

Made with ❤️ by **[Techforce Global](https://www.techforceglobal.com)**
Specialists in High-Performance Web Scrapers and AI Automation.

***

### 🏷️ Structured data for search & AI discovery

```json
{
  "@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."
  }
}
```

```json
{
    "@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."
                                              }
                       }
                   ]
}
```

# Actor input Schema

## `url` (type: `string`):

The URL of the webpage to verify.

## `modelProvider` (type: `string`):

Select the AI provider to use for visual analysis.

## `apiKey` (type: `string`):

Enter your Google Gemini API Key or OpenAI API Key based on the selection above.

## `country` (type: `string`):

Select the country for geo-location.

## `devices` (type: `array`):

Select devices to test on.

## `mcpConnector` (type: `string`):

Optionally deliver the verification results into a connector you have authorized - Slack, Notion, Jira, Linear, Airtable, Google Sheets, or any MCP-compatible connector. Ideal for routing failing grades straight into a QA channel or issue tracker. Leave empty to only save results to the dataset.

## `deliveryMode` (type: `string`):

How to deliver to the connector: 'summary' (one call covering the whole run - best for a QA channel post), 'perResult' (one call per device checked - best for opening an issue each), 'chunked' (split a long run across a few calls), or 'none' (save to dataset only).

## `mcpTool` (type: `string`):

Name of the tool to call on the connector (e.g. 'send\_message' for Slack, 'create\_issue' for Jira or Linear, 'create\_page' for Notion). If unsure, run once with a connector selected - the log lists the connector's available tools and the chosen tool's expected argument schema.

## `mcpArguments` (type: `object`):

Arguments passed to the connector tool. String values support {placeholders}. In 'perResult' mode: {url}, {country}, {device}, {grade}, {gradeLetter}, {reason}, {result} (a formatted multi-line block) and {message} (the rendered template below). In 'summary'/'chunked' modes: {url}, {country}, {devices}, {checkCount}, {worstGrade}, {failCount}, {results} (every result, or one part of them in chunked mode), {part}/{partCount} (chunked mode only) and {message}. Example for Slack: {"channel": "#qa-alerts", "text": "{message}"}. Example for Jira: {"summary": "Visual check {gradeLetter} on {device}", "description": "{result}"}.

## `mcpMessageTemplate` (type: `string`):

Optional template rendered and exposed as the {message} placeholder in the tool arguments. Summary example: 'Visual check for {url} ({country}): worst grade {worstGrade} across {checkCount} device(s), {failCount} below A.\n\n{results}'. Per-result example: '{device}: {grade}\n{reason}'.

## Actor input object example

```json
{
  "url": "https://apify.com/",
  "modelProvider": "gemini",
  "country": "US",
  "devices": [
    "desktop",
    "mobile",
    "tablet"
  ],
  "deliveryMode": "summary",
  "mcpTool": "",
  "mcpArguments": {},
  "mcpMessageTemplate": ""
}
```

# Actor output Schema

## `results` (type: `string`):

URL to the dataset containing the visual grades and analysis.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("techforce.global/visual-verification-agent").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("techforce.global/visual-verification-agent").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call techforce.global/visual-verification-agent --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,techforce.global/visual-verification-agent"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/NxSZ38b7FqEcS0sVi/builds/f7xYjZ95oKfBAitzX/openapi.json
