# Metacritic Scores Scraper (`scrapyx/metacritic-scores-scraper`) Actor

Critic and user scores for games, movies and TV from Metacritic's own JSON API. Paginates by offset because Metacritic's `page` parameter is inert — every value returns the same 24 rows — and keeps the 0-100 critic score and the 0-10 user score clearly apart.

- **URL**: https://apify.com/scrapyx/metacritic-scores-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** Videos, Marketing, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.26 / 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.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Metacritic Scores Scraper

Critic and user scores for **games, movies and TV** from Metacritic's own
internal JSON API, plus full title detail from JSON-LD. HTTP-only, no API key,
no login, no browser.

### Modes

| Mode | What you get |
| --- | --- |
| `browse` | The catalogue, sorted and filtered — Metascore, user score, sentiment breakdown, genres, release date. Games (14,335), movies (17,317) and TV (3,444). |
| `titles` | Full detail for titles you name — cast, director/creator, publisher, platforms, images, trailer, description. |

### Five upstream quirks it corrects

#### 1. The parameter called `page` does **nothing**

Holding everything else constant:

```
page=1, 2, 3, 100, 600, 5000  ->  the IDENTICAL 24 items, every time
                                  24/24 overlapping page 1, same first title,
                                  HTTP 200, plausible data
offset=0, 24, 48, 240         ->  24 items each, ZERO overlap
```

This is worse than a clamp. A clamp eventually stops; `page` looks like it is
working *forever*. A walker built on it collects the same 24 rows over and over
and reports thousands of results — all duplicates — with no error and no empty
page to stop on.

The API tells on itself if you read `links.next.href`: its own "next" link
keeps `page=1` and appends `&offset=24`. This actor paginates by `offset`,
never sends `page`, and publishes `offsetsWalked` instead of page numbers so
the walk is auditable.

#### 2. The two scores are on **different scales**, and only one says so

```
criticScoreSummary: {"score": 99, "max": 100, "reviewCount": 22, ...}
userScore:          {"score": 9.1}          <- no `max` field anywhere
```

The critic score is 0–100 and declares its maximum. The user score is 0–10 and
declares nothing. Publishing both under a field called "score" invites a
tenfold comparison error, so they are named apart (`criticScore` /
`criticScoreMax`, `userScore` / `userScoreMax`) and every row carries
`scoresUseDifferentScales: true`.

#### 3. The sentiment buckets are **not a partition** of the review count

Measured across 48 games, 7 disagreed — and every one **undercounted**:

| Title | positive + neutral + negative | `reviewCount` | shortfall |
| --- | ---: | ---: | ---: |
| Zelda: Breath of the Wild | 109 | 117 | **8** |
| Red Dead Redemption 2 | 99 | 109 | **10** |
| Super Mario Odyssey | 114 | 124 | **10** |
| Elden Ring | 86 | 93 | **7** |

Never the other direction, and it clusters on the most-reviewed titles — some
reviews are counted in the total but assigned to no sentiment bucket. Both
figures ship, plus `criticSentimentBucketSum`, `criticReviewsUnbucketed` and
`criticSentimentBucketsCoverAllReviews`, rather than implying the buckets add
up.

#### 4. An unknown `productType` **silently returns games**

| Sent | Answer |
| --- | --- |
| `productType=nosuchtype` | **200 with the games catalogue, 14,335 results** |
| `sortBy=-nosuchfield` | HTTP 400 — honest |
| `genres=nosuchgenre-xyz` | 200 with `totalResults: 0` — honest |
| `nosuchparam=x` | 200, ignored |

So a typo in the product type answers a different question convincingly. It is
validated locally and refused before a request is spent. `sortBy` is validated
too — upstream is honest there, but failing early costs nothing.

#### 5. `totalResults` is **true**, and the walk ends honestly

Rare enough in this portfolio to state plainly. Games claims 14,335:

```
offset 14,300 -> 24 rows      offset 14,328 -> exactly 7 rows   (14,328+7 = 14,335)
offset 14,400 -> 0 rows       offset 100,000 -> 0 rows
```

No wrap back to the start, no clamp to the last page, no phantom results.

#### Bonus: the `apiKey` is decorative

The same query returns identical data with the correct key, a bogus key, or no
key at all. It is embedded in the site's own HTML anyway. This actor never
sends one and never caches one — there is nothing to rot.

### Output

One `SEARCH_SUMMARY` per run, one `TITLE` per title, one `ERROR` per failure.

`TITLE` rows carry the upstream object verbatim plus `titleSlug`, `titleUrl`,
`titleName`, `metacriticId`, `productType`, `itemType`, `releaseDate`,
`premiereYear`, `contentRating`, `genres`, `description`, `criticScore`,
`criticScoreMax`, `criticReviewCount`, the sentiment breakdown and its
shortfall fields, `userScore`, `userScoreMax`, `resultOffset` and `resultRank`.
With detail: `cast`, `directors`, `creators`, `publishers`,
`productionCompanies`, `gamePlatforms`, `imageUrl`, `trailerUrl`, `schemaType`.

### Limits

- **The URL segment differs from the API's `productType`** — `game` vs `games`,
  `movie` vs `movies`. A wrong segment is an honest 404. Pass a full
  metacritic.com URL in `titles` mode and the segment is taken from it.
- Page size is fixed at 24: `limit` above that is an honest HTTP 400.
- Music is not covered — that section serves no JSON-LD and no `__NUXT__`
  payload.
- There is no WAF on this host; a proxy is offered but was not needed for any
  of the reconnaissance.

# Actor input Schema

## `mode` (type: `string`):

browse = walk the catalogue with scores. titles = full detail for titles you name.

## `productType` (type: `string`):

games (14,335), movies (17,317) or tv (3,444). Validated here rather than passed through: Metacritic does NOT reject an unknown product type — it silently returns the games catalogue, so a typo would answer a different question with a plausible 14,000-row result.

## `sortBy` (type: `string`):

browse mode only. Prefix with '-' for descending.

## `genre` (type: `string`):

browse mode only, e.g. 'action' or 'drama'. An unknown genre honestly returns zero results rather than the unfiltered catalogue.

## `releaseYearMin` (type: `integer`):

browse mode only.

## `releaseYearMax` (type: `integer`):

browse mode only.

## `titleSlugs` (type: `array`):

For mode='titles'. A slug like `parasite`, a `game/slug` pair, or a full metacritic.com URL. NOTE the URL segment differs from the product type — 'game' vs 'games', 'movie' vs 'movies' — so passing a full URL is the reliable way to mix games, movies and shows in one run.

## `fetchTitleDetails` (type: `boolean`):

browse mode only — titles mode always fetches it. Adds cast, director/creator, publisher, platforms, images and trailer from JSON-LD, at one extra request per title.

## `maxResults` (type: `integer`):

Set 0 for unlimited. The page size is fixed at 24 — Metacritic answers HTTP 400 for any larger limit. Upstream's own total is honest, so an unlimited run really can walk the whole catalogue.

## `maxConcurrency` (type: `integer`):

Title-detail fetches in flight at once. The catalogue walk itself is sequential, because each offset depends on the last.

## `minRequestInterval` (type: `integer`):

Politeness pacing shared across all workers. 0 uses the built-in default. Reconnaissance saw no interstitials at all.

## `proxyConfiguration` (type: `object`):

No WAF was found on this host and the whole reconnaissance ran proxy-free. Residential still defaults on for cloud runs, because this repo has repeatedly found datacenter ASNs scored differently from a home IP.

## Actor input object example

```json
{
  "mode": "browse",
  "productType": "games",
  "sortBy": "-metaScore",
  "genre": "action",
  "releaseYearMin": 2020,
  "releaseYearMax": 2026,
  "titleSlugs": [
    "parasite",
    "https://www.metacritic.com/tv/severance/"
  ],
  "fetchTitleDetails": false,
  "maxResults": 120,
  "maxConcurrency": 4,
  "minRequestInterval": 0,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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("scrapyx/metacritic-scores-scraper").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("scrapyx/metacritic-scores-scraper").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 scrapyx/metacritic-scores-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapyx/metacritic-scores-scraper"
        }
    }
}
```

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/w0N82NHPJ4Y3cgLJ2/builds/wbwIeCqIsgNOfjBk2/openapi.json
