# Chrome Extension Scraper: Listings, Users & Dead Links (`aiqlabs/chrome-extension-audit`) Actor

Chrome quietly stops distributing extensions your browsers keep running. Give this any list of extension IDs and it reports listings Google has pulled, extensions nobody has updated in years, and privacy policy, website and contact links that are dead - including domains anyone can now register.

- **URL**: https://apify.com/aiqlabs/chrome-extension-audit.md
- **Developed by:** [Ai-Q Labs](https://apify.com/aiqlabs) (community)
- **Categories:** Developer tools, Automation, Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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

## Chrome Extension Audit: Removed Listings & Dead Links

Give this a list of Chrome extension IDs. It tells you which of them Google has
quietly stopped distributing, which nobody has updated in years, and which point
their privacy policy, developer website or contact address at somewhere that no
longer exists — including addresses **anyone can now register**.

An extension that has been pulled from the Chrome Web Store does not uninstall
itself. It keeps running, it stops getting updates, and the store page it came
from goes blank without telling anyone. Nothing in Chrome surfaces that, and no
other Actor on this store checks for it.

Free. No API key. Nothing is scraped that the store's own `robots.txt` does not
permit.

***

### What it is for

The input this is built around is a list you already have:

- the extension IDs in a Chrome Enterprise `ExtensionInstallAllowlist` or
  `ExtensionSettings` policy
- an export of what is installed across a fleet
- the dependencies of your own extension, or a competitor set you track
- a shortlist you are about to approve

Paste the IDs, get one row per extension.

It is **not** a way to sweep the store. Google answers a rapid crawl of its own
listings with an "unusual traffic" interstitial, and this Actor stops when it
sees one rather than pretending it did not happen. See *Rate limits* below.

### What it reports

| Finding | Severity | What it means |
|---|---|---|
| `listing_unavailable` | critical | The store serves no listing at this ID. If the ID came from an installed extension, it is no longer distributed — copies already installed keep running and stop getting updates. See the note below on what this cannot tell you. |
| `privacy_policy_domain_unregistered` | critical | The policy URL's domain is not registered. Anyone can take it and publish a "privacy policy" under this extension's name. |
| `developer_site_domain_unregistered` | critical | The same, for the developer's own website. |
| `developer_email_domain_unregistered` | critical | Nobody owns the domain behind the published contact address. Whoever registers it receives mail sent to this extension's developer. |
| `listing_unknown` | high | The store redirects the ID to its home page instead of to a listing — which points at the ID itself being malformed. |
| `privacy_policy_dead` | high | The policy link does not lead to a working page. |
| `developer_site_dead` | high | The developer website does not lead to a working page. |
| `developer_email_undeliverable` | high | Mail to the published address bounces — the domain is held by someone, but publishes no MX. |
| `no_update_in_years` | medium / low | Nobody has released a new version in five years (medium) or three (low). |
| `privacy_policy_missing` | medium | The listing describes what it does with your data and links to no policy. |
| `link_blocked` | low | The link's server answered 401/403/429. It is up and declining to talk to a program — reported as such, never as broken. |
| `link_insecure` | low | Declared as `http://` and it stays there. |
| `not_checked` | low | This one was not examined. A statement about the run, never about the extension. |

Plus a flag that is the reason this Actor exists:

**`abandonedWithDeadContact`** — at least two years without an update, at least
ten thousand users, and a privacy policy, website or contact address that no
longer works. Code still running in thousands of browsers with no working way to
reach whoever wrote it. Sorted by reach in `ABANDONED_HIGH_REACH`.

### How this differs from the other Chrome Web Store Actors

Fourteen Actors on this store work on Chrome Web Store listings. Their
descriptions were read before this one was written. Thirteen of them **extract**:
metadata, reviews, install counts, manifests, developer emails for lead lists.
The fourteenth audits the packaged code's permissions.

None of them checks whether what the listing claims is still true. Several list
"privacy links" among the fields they return — as a string, copied out and handed
over, with nobody asking whether it leads anywhere.

That is the entire job here. This Actor returns fewer fields than any of them,
and follows every one it returns.

One other difference worth stating: several take a **search query** as input. The
store's `robots.txt` disallows `/search`. This Actor does not use it, and refuses
a search URL in its input rather than rewriting it.

### Output

One dataset row per extension, plus five key-value records:

- `SUMMARY` — counts, the query, what was and was not read, every caveat
- `ACTION_LIST` — findings ranked by severity, then by how many people are affected
- `ABANDONED_HIGH_REACH` — the flag above, biggest first
- `PRIVACY_REPORT` — policy URL, its status, the data-handling declaration, trader status
- `DEVELOPER_PROFILES` — totals per developer, so a bad publisher shows up as a pattern

Each row carries `users`, `lastUpdated`, `yearsSinceUpdate` and `reachBand`
whether or not anything is wrong with it, so the output works as a straight
inventory too.

### What it will not do

- **It does not use the store search.** `robots.txt` disallows `/search` and any
  listing URL carrying `q=`. A search URL in the input is refused with that
  reason, not quietly rewritten.
- **It does not open the per-listing sub-pages** (`/reviews`, `/privacy`,
  `/support`, `/related`, `/preview`, `/report`). `robots.txt` disallows all of
  them. The support address it reports is the one the listing itself prints.
- **It does not solve the interstitial.** No CAPTCHA solving, no identity
  rotation, no browser user agent. When Google says slow down, the run stops and
  says how far it got.
- **It does not retry a refused link behind a browser user agent.** A server that
  answers 403 to an automated request is alive and declining. That is an answer.
- **It does not connect to a mail server.** Deliverability is not tested and no
  address is verified. It asks DNS whether the domain can receive mail at all.
- **It does not judge what a live page says.** A parked page and a real one both
  answer 200. Whether an address is free comes from the registry, not from
  reading the page.

### How common is each of these? (measured, not guessed)

Severity is a claim about rarity, so it was measured before it was assigned. In
July 2026 this Actor's own code — not a simplified copy — read a random sample of
**90 listings** drawn from the store's own sitemap. Four had been removed; 86
were read in full.

**What the store publishes**

| | share of the 86 |
|---|---|
| Last-updated date | **100%** |
| Trader / non-trader status | **100%** |
| Developer contact email | 93% |
| Install count | 76% (86% of extensions; themes never carry one) |
| "Offered by" | 55% |
| **Privacy policy link** | **37%** |
| **Developer website** | **34%** |

Two thirds of the store publishes no privacy policy and two thirds no website.
That is why **their absence is not a finding here** — only a dead one is. The one
exception is narrow and rare: a listing that declares how it handles your data
and still links to no policy, which was **2.3%** of the sample.

**Age.** Median listing was updated 0.8 years ago. 17% have not been touched in
three years, 11% in five. So age alone never rises above medium.

**Links.** Of 59 distinct declared links, 46 answered normally, 10 did not, and 3
refused an automated request (recorded as `blocked`, not broken). Of the nine
domains behind dead links, six were registered, two sat on TLDs the IANA RDAP
bootstrap does not cover — reported `unknown` — and **one was not registered at
all**.

**Mail.** 49 developers used free webmail (never checked, never flagged) and 29
used a domain of their own. Two of those 29 could not receive mail, and both sat
on domains **nobody had registered**.

**What the Actor said about them**

| | |
|---|---|
| **Nothing to report** | **65 of 86 — 76%** |
| low / medium / high / critical | 5 / 5 / 9 / 2 |

The two criticals were both `developer_email_domain_unregistered`, verified
independently against Verisign's RDAP service with a known-registered control.
Each is a live listing whose published contact address sits on a domain anyone
can register today.

### Rate limits

Google applies a per-IP limit to its own store and enforces it with an "unusual
traffic" interstitial, served as a **redirect away from the listing**. That
matters more than it sounds: to any tool that only asks "did I land on a listing
page?", it is indistinguishable from a deleted extension.

This was found the hard way while building this Actor. A run of 130 listings, one
every 600 ms, came back reporting that **93 of them did not exist** — every one a
healthy extension taken from the store's own sitemap minutes earlier.

So:

- listings are read **one at a time**, with a pause (`requestDelayMs`, default
  2500 ms, floor 1200 ms)
- the interstitial is recognised specifically, and never confused with a removal
- when it appears the run **stops**, and the remaining extensions are reported as
  `not_checked` rather than guessed at
- `SUMMARY.rateLimited` and `listingsReadBeforeRateLimit` say exactly what
  happened

If a run is cut short, raise `requestDelayMs` and run it again. The limit
released after roughly fifteen minutes in testing.

### Categories are a sample, not a census

`categories` accepts paths like `category/extensions/productivity/workflow`. Each
returns about 31 extensions, and that is all you can get: the store **ignores
`?page=N`** — page 2 of a category is byte-for-byte page 1, verified before this
was written. Every category read says how many it contributed, in `SUMMARY.notes`.
Use it to spot-check, not to cover.

### Where the registry cannot answer

Domain registration comes from RDAP, reached through the IANA bootstrap. That
bootstrap lists about 1,200 TLDs, and several common ones — `.io`, `.co`, `.jp`,
`.ro` — are **not among them**.

A dead link on one of those is reported as dead, with the registration recorded
as `unknown`. It is never assumed to be safely held, and it is never called free.
The public RDAP proxies that appear to cover those TLDs return "not found" for
domains that are plainly registered, which is a false accusation, not a
convenience.

### What "unavailable" cannot tell you

An extension that Google has pulled and an extension ID that never existed
produce **exactly the same response**: a 301 to `/detail/empty-title/<id>` and a
blank page. Three invented-but-well-formed IDs were tried; all three were
indistinguishable from a real removal.

So the finding is called `listing_unavailable` and it says both things. For the
input this Actor is built for — IDs that came from extensions actually installed
somewhere — the first reading is the right one, and it is critical. For an ID
typed by hand, check the ID before acting on it. The alternative, printing
"Google pulled this extension" over somebody's typo, is a fabrication with a
severity attached.

### Fact, inference, and the difference

Three kinds of statement appear in these rows, and they are worded differently on
purpose:

- **"This listing has been removed."** Google's doing. Read off the store's own
  redirect.
- **"Nobody has registered this domain."** A registry's answer, quoted.
- **"Nobody has updated this in four years."** Read off the date the store
  publishes. It says nothing about whether the extension still works — a small
  utility that does one thing correctly may not need a release, which is why age
  alone never rises above medium.

When something could not be checked, the answer is `unknown`. Never `ok`.

# Actor input Schema

## `extensions` (type: `array`):

The extensions to audit. Accepts 32-character extension IDs, current store URLs (chromewebstore.google.com/detail/...) and the older chrome.google.com/webstore/detail/... form. This is the input the Actor is built for: paste the ID column from a fleet inventory or a Chrome Enterprise policy file. Store search URLs are refused, because the store's robots.txt disallows them.

## `categories` (type: `array`):

Optional. Chrome Web Store category paths such as category/extensions/productivity/workflow. Each yields about 31 extensions: the store ignores ?page=N, so there is no way to page through a category. Use this to spot-check a category, not to cover it.

## `checkDeveloperWebsite` (type: `boolean`):

Fetch the developer's own website when the listing publishes one. About a third of listings do.

## `checkDeveloperEmail` (type: `boolean`):

Looks up MX records for the developer's contact domain. Nothing connects to a mail server and no address is verified; this only asks whether the domain is capable of receiving at all. Free webmail (Gmail, Outlook and similar) is skipped, because the developer does not run that mail server.

## `checkDomainRegistration` (type: `boolean`):

Only for links that already failed. This is what separates 'the page is gone' from 'the address is free for anyone to take', which is the finding this Actor exists for.

## `onlyIssues` (type: `boolean`):

Leave this off to get a row per extension including the healthy ones, which is what you want when the output feeds a spreadsheet.

## `maxExtensions` (type: `integer`):

A ceiling on the run. Anything dropped is stated in SUMMARY rather than silently truncated.

## `requestDelayMs` (type: `integer`):

Listings are read one at a time with this pause between them. Going faster does not help: Google answers a rapid sweep of its own store with an 'unusual traffic' interstitial, and the Actor then has to stop. Raise this if a run is being cut short.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for any one request - a store page, a developer's website, or a registry - before giving up on it and recording the reason.

## Actor input object example

```json
{
  "extensions": [
    "ddkjiahejlhfcafbddmgiahcphecmpfh",
    "https://chromewebstore.google.com/detail/camera/ofmpffnppnlgkgmbgidhhjcglloeejpg"
  ],
  "categories": [],
  "checkDeveloperWebsite": true,
  "checkDeveloperEmail": true,
  "checkDomainRegistration": true,
  "onlyIssues": false,
  "maxExtensions": 200,
  "requestDelayMs": 2500,
  "requestTimeoutSecs": 30
}
```

# Actor output Schema

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

No description

## `csv` (type: `string`):

No description

## `actionList` (type: `string`):

No description

## `abandonedHighReach` (type: `string`):

No description

## `privacyReport` (type: `string`):

No description

## `developerProfiles` (type: `string`):

No description

## `summary` (type: `string`):

No description

# 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 = {
    "extensions": [
        "ddkjiahejlhfcafbddmgiahcphecmpfh",
        "https://chromewebstore.google.com/detail/camera/ofmpffnppnlgkgmbgidhhjcglloeejpg"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("aiqlabs/chrome-extension-audit").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 = { "extensions": [
        "ddkjiahejlhfcafbddmgiahcphecmpfh",
        "https://chromewebstore.google.com/detail/camera/ofmpffnppnlgkgmbgidhhjcglloeejpg",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("aiqlabs/chrome-extension-audit").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "extensions": [
    "ddkjiahejlhfcafbddmgiahcphecmpfh",
    "https://chromewebstore.google.com/detail/camera/ofmpffnppnlgkgmbgidhhjcglloeejpg"
  ]
}' |
apify call aiqlabs/chrome-extension-audit --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=aiqlabs/chrome-extension-audit",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

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