# Tinder Subscription Prices by Country and App Store Ratings (`zinin/tinder-app-intel`) Actor

Live Tinder Gold/Plus/Platinum/Boost/Super Like prices by country from the App Store and Google Play, converted to USD, with ratings and change tracking between runs.

- **URL**: https://apify.com/zinin/tinder-app-intel.md
- **Developed by:** [Tim Zinin](https://apify.com/zinin) (community)
- **Categories:** Social media, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 storefront snapshots

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

## Tinder App Intel — Subscription Prices by Country and App Store Ratings

Tinder Gold, Plus, Platinum, Boost and Super Like prices, read straight from Tinder's own
App Store and Google Play listings for any country you name, converted to USD and compared
across storefronts, plus the app's rating, version and release notes — no login, no API
key, no profile data, only what Tinder itself already publishes on its own store page.

Match Group, the company that owns Tinder, sets these prices per storefront, and the gap
between the cheapest and most expensive country is often larger than most people assume —
Gold ran from $13.99 to $24.99 across the US storefront alone at the time this README was
written, and 32.99 € in Germany translates to roughly $37.62 at yesterday's ECB rate, well
above every US price point. Nobody at Match Group publishes that comparison for you; this
Actor builds it from the same page a phone would load, on a schedule, and hands it back as
structured rows you can filter, chart or feed into a spreadsheet.

### What you get

- **Real subscription prices, not an estimate.** Every entry in `inAppPurchases[]` is read
  directly off the App Store's own in-app-purchase list for that country's storefront —
  `Tinder Gold`, `Tinder Plus`, `1 Boost`, `3 Super Likes`, `Tinder Platinum`, and so on — in
  the storefront's own currency and its own language. Nothing here is inferred from a
  marketing page or a press release; it is the same list a paying subscriber sees on their
  phone in that country, fetched fresh at run time.
- **planFamily normalizes the label across languages and punctuation.** Apple's own feed
  spells the same plan differently depending on the storefront: `3 Super-Likes` on the
  German page (hyphenated), `5 Super Likes` on the same German page a few rows later (a
  space, no hyphen), `Super Likes` on the US page. All three collapse to the same stable
  `planFamily: "SuperLike"` so you can compare the same plan across a whole country list
  without writing your own string-matching logic. Anything this Actor's dictionary doesn't
  recognize comes back as `planFamily: "other"` — never a guess dressed up as a fact.
- **USD conversion, honestly null when the source currency has no published rate.** Set
  `convertToUsd` (on by default) and every price in `inAppPurchases[]` gets a `usd` field
  computed from the European Central Bank's daily reference rate — the same rate a bank or
  an accountant would use, not a marked-up card-network rate. A handful of currencies this
  line's storefronts can show — Russian ruble, Kazakhstani tenge, UAE dirham, among others —
  are not part of the ECB's daily feed at all. For those, `usd` stays `null` and `fxNote`
  says exactly why, in the same row, instead of silently guessing a number that would be
  wrong by construction.
- **Track price and rating changes over time, per storefront.** Set `compareWithPreviousRun`
  to `true` and give the run a `watchId`, and every row after the first for that `watchId`
  carries a `changes[]` array: new plans that appeared, plans that disappeared, price moves,
  rating moves of 0.05 or more, and version bumps, each compared against the last time you
  ran that exact `country`/`platform` pair under that `watchId`. The first run for a fresh
  `watchId` reports a clean baseline — `changes: []` — never a false "everything changed"
  because there was nothing to compare against yet.
- **Google Play's own shape, not a forced match to the App Store's.** The two stores do not
  publish the same information. Google Play's public listing exposes one aggregate
  `iapPriceRange` (`{min, max, currency}` across every in-app item the app sells) and never
  names individual plans; the App Store's listing does name them, one row per plan, but
  never states a plan's duration or the app's age rating anywhere in the feed this Actor
  reads. This Actor reports each store's own real shape side by side rather than inventing a
  merged view that would misrepresent what either store actually discloses.
- **Independent storefront reads, not a single global snapshot.** Every `country × platform`
  pair is fetched on its own — a 404 for Russia does not affect the German row, a rating
  drop in the UK does not touch the US price list. You choose exactly which storefronts to
  check, from one country to sixty, and get one row per combination back.
- Runs on Apify: schedule it daily or weekly to watch a price move, call it from the REST
  API or an SDK, wire it to a webhook, or export straight to JSON, CSV or Excel — see
  Integration recipes below for working examples of each.

### Who uses it

- **Equity and credit analysts covering Match Group (NASDAQ: MTCH).** Tinder is Match
  Group's largest single brand by revenue disclosure. A pricing-power read across
  storefronts — is Gold actually priced higher in high-income markets, did a price move
  follow a specific quarter's earnings call — is exactly the kind of primary-source input a
  research note cites, and it doesn't exist anywhere as a maintained dataset outside of
  building it yourself from the same public pages this Actor reads.
- **Competitor product and pricing teams at Bumble, Badoo, Hinge and every other dating app
  that competes with Tinder for the same subscriber.** Before setting a Boost or Super Like
  price in a new country, a product team wants to know what the market leader charges there
  today, not last year. This Actor gives that read without anyone on the team manually
  opening the App Store in sixty different country settings.
- **ASO (App Store Optimization) and mobile-growth agencies** that already track ratings,
  review velocity and keyword rank for a portfolio of dating apps and want subscription
  pricing folded into the same weekly report, from the same kind of public-page read they
  already trust for the rest of their dashboard.
- **Currency and FX researchers** who use consumer subscription pricing as one input into
  purchasing-power-parity or price-discrimination studies — App Store and Google Play
  prices are set deliberately per country and are a cleaner signal for this than most retail
  goods, because there is no shipping, tariff or local-tax variance to strip out first.
- **Anyone tracking their own subscription cost over time or across a trip.** A traveler who
  keeps Tinder Gold active while relocating between countries can watch whether re-signing
  up in a new storefront would be cheaper or more expensive than staying on their current
  billing country, using nothing but this Actor's own dataset history via `watchId`.

This Actor does **not** serve anyone looking for Tinder user profiles, photos, messages,
match data or any other personal information about Tinder's own users — see "What this is
NOT" under Evidence and boundaries. That is a different, and in this line's judgment
indefensible, product; this one only ever reads the store listing every visitor to
apps.apple.com or play.google.com already sees.

### How to run it

1. Click **Try for free** — no card is required on the free plan, and the default input
   below runs without any secret or account of your own.
2. Pick your **Countries** — ISO-3166 alpha-2 codes, lowercase, e.g. `us`, `gb`, `de`. Leave
   the default (`us`, `gb`) for a first look, or list up to 60.
3. Pick your **Platforms** — `appstore`, `googleplay`, or both (the default).
4. Leave **Convert prices to USD** on unless you specifically want each storefront's own
   local currency and nothing else.
5. Press **Start**. A run against the default input finishes in well under a minute; a full
   60-country list against both platforms, at this Actor's own two-request-at-a-time pace
   toward Apple, takes longer — see Operating guide for what to expect on a large run.
6. Pull the results from the **Dataset** tab (table, JSON, CSV, Excel or via the API), or
   read the free run-level summary from the **Key-value store** tab under the `OUTPUT` key.

### Pricing

Pay-per-event, on the platform's standard **PAY_PER_EVENT** model: **$0.005 charged once at
run start, plus $0.005 per storefront that came back with real data** (`status: "ok"`,
`found: true` — the platform calls this a `result-found` event internally; on the page it is
simply "a storefront with data"). Every other outcome is free and is never billed under any
event name: a country the app genuinely is not sold in (`not_available_in_country`), a
source that did not answer after this Actor's own retries (`rate_limited`), a real
parsing/source error (`source_error`), or a country code that was not a valid ISO territory
in the first place (`invalid_country`). You are billed only for a storefront that actually
delivered a usable row — never for one that came back empty, and never twice for the same
storefront in one run.

The default prefill — two countries, two platforms, so up to four storefronts — resolves to
**$0.005 + up to 4 × $0.005 = up to $0.025** for a run where every storefront is genuinely on
sale in that country (Tinder is sold in nearly every country checked in this line's testing,
so in practice this usually lands at the full $0.025). Ten storefronts that all resolve —
five countries across both platforms, say — comes to $0.005 + 10 × $0.005 = **$0.055**.
`maxStorefronts` (default 10, maximum 120) is a hard ceiling on how many `country × platform`
combinations one run will ever attempt; the excess combinations are dropped *before* a single
request is made, with one free explanatory row pushed to the dataset (`status: "truncated"`)
naming exactly which storefronts were skipped — never a silent drop, and never a charge for
work that never happened.

At startup, the Actor checks that result events are configured as paid events and that ordinary dataset writes are free. It reads the event price once at startup. Before each successful storefront row, it uses that price snapshot and reads the remaining run budget from the Apify SDK. It does not compare the configured prices with the numbers printed on this page. The live Pricing tab is authoritative; set a maximum run charge to bound your spend.

### Input contract

| Field | Required | What it does |
|---|---|---|
| `countries` | no | ISO-3166 alpha-2 codes, lowercase, e.g. `["us","gb","de"]`. 1 to 60 codes. Duplicates (including different casing or stray whitespace, like `"US"` or `" us "`) are silently collapsed to one. A code that is not a real ISO-3166 territory at all comes back as a free `invalid_country` row rather than being rejected up front — the run still finishes and every other country in the list is still checked. A code that IS a real country, but one where the App Store or Google Play genuinely does not sell this app, comes back as a free `not_available_in_country` row, distinct from `invalid_country` — see Field dictionary for exactly how those two differ in the row itself. Default: `["us"]`. |
| `platforms` | no | `appstore`, `googleplay`, or both. Up to 2 values. Any value this line does not recognize is dropped rather than failing the run. Default: both. |
| `convertToUsd` | no | When `true` (the default), every priced entry in `inAppPurchases[]` gets a `usd` field computed from yesterday's published ECB daily reference rate. When `false`, `usd` is `null` throughout and no FX lookup happens at all — useful if you only ever want the storefront's own local price and plan to convert currencies yourself, on your own schedule, with your own rate source. |
| `compareWithPreviousRun` | no | When `true`, every row is diffed against the last time this exact `country`/`platform` pair was run under the same `watchId`, and the result carries a populated `changes[]` array. Default: `false`. |
| `watchId` | no | Names which saved snapshot to diff against, so you can track several independent country/platform lists without them interfering with each other's history. Pattern: `[a-zA-Z0-9_-]{1,40}` — no colons or other punctuation (an early version of this line's diff logic used a colon-delimited key-value-store key internally, which broke on a `watchId` containing a colon; the pattern now simply forbids what would collide with that key format, and an invalid `watchId` falls back to `"default"` rather than failing the run). Default: `"default"`. |
| `maxStorefronts` | no | Hard ceiling on `countries × platforms` for one run, enforced *before* any request is made. Range: 1 to 120. Default: 10. |

```json
{
    "countries": [
        "us",
        "gb"
    ],
    "platforms": [
        "appstore",
        "googleplay"
    ],
    "convertToUsd": true
}
```

### Real happy, partial and failure output

One row per `country × platform` pair, plus one free explanatory row if `maxStorefronts`
ever truncates the list. Every example below is real and unedited, copied as-is from an
actual run of this exact code — never rewritten, trimmed of numbers, or invented for this
page. For readability, a few fields that are either very long (`releaseNotes`) or genuinely
empty for a given platform/status (`installs`, `containsAds`, `iapPriceRange`, `fxNote`,
`planSummary`) are left out of some of the blocks below where they add nothing to read — the
real Dataset row always contains every field regardless.

#### Happy path: a storefront that came back with real, priced data

**App Store, United States — the full named plan list, all 10 entries this page carried at
capture time.** This is the block SPEC.md §7's own acceptance golden checks against: Gold
and Plus must both be present, and every entry must carry `amount > 0` with `currency`
`"USD"`.

```json
{
    "recordType": "storefront_snapshot",
    "brand": "tinder",
    "platform": "appstore",
    "country": "us",
    "appId": "547702041",
    "observedAt": "2026-09-26T14:02:59.288Z",
    "status": "ok",
    "found": true,
    "appTitle": "Tinder Dating App: Date & Chat App",
    "rating": 4.2,
    "ratingCount": 1751429,
    "ratingHistogram": {
        "1": 214657,
        "2": 58997,
        "3": 114363,
        "4": 139574,
        "5": 1223838
    },
    "version": "17.36.0",
    "lastUpdated": "2026-09-22T16:46:21.000Z",
    "inAppPurchases": [
        {
            "name": "Tinder Gold",
            "priceText": "$14.99",
            "amount": 14.99,
            "currency": "USD",
            "planFamily": "Gold",
            "durationDays": null,
            "usd": 14.99
        },
        {
            "name": "Tinder Gold",
            "priceText": "$18.99",
            "amount": 18.99,
            "currency": "USD",
            "planFamily": "Gold",
            "durationDays": null,
            "usd": 18.99
        },
        {
            "name": "1 Boost",
            "priceText": "$7.99",
            "amount": 7.99,
            "currency": "USD",
            "planFamily": "Boost",
            "durationDays": null,
            "usd": 7.99
        },
        {
            "name": "Tinder Gold",
            "priceText": "$13.99",
            "amount": 13.99,
            "currency": "USD",
            "planFamily": "Gold",
            "durationDays": null,
            "usd": 13.99
        },
        {
            "name": "1 Boost",
            "priceText": "$6.99",
            "amount": 6.99,
            "currency": "USD",
            "planFamily": "Boost",
            "durationDays": null,
            "usd": 6.99
        },
        {
            "name": "Tinder Gold",
            "priceText": "$24.99",
            "amount": 24.99,
            "currency": "USD",
            "planFamily": "Gold",
            "durationDays": null,
            "usd": 24.99
        },
        {
            "name": "Tinder Plus",
            "priceText": "$9.99",
            "amount": 9.99,
            "currency": "USD",
            "planFamily": "Plus",
            "durationDays": null,
            "usd": 9.99
        },
        {
            "name": "3 Super Likes",
            "priceText": "$9.99",
            "amount": 9.99,
            "currency": "USD",
            "planFamily": "SuperLike",
            "durationDays": null,
            "usd": 9.99
        },
        {
            "name": "1 Boost",
            "priceText": "$3.99",
            "amount": 3.99,
            "currency": "USD",
            "planFamily": "Boost",
            "durationDays": null,
            "usd": 3.99
        },
        {
            "name": "5 Super Likes",
            "priceText": "$4.99",
            "amount": 4.99,
            "currency": "USD",
            "planFamily": "SuperLike",
            "durationDays": null,
            "usd": 4.99
        }
    ],
    "planSummary": {
        "Gold": {
            "minAmount": 13.99,
            "maxAmount": 24.99,
            "currency": "USD",
            "minUsd": 13.99,
            "maxUsd": 24.99
        },
        "Boost": {
            "minAmount": 3.99,
            "maxAmount": 7.99,
            "currency": "USD",
            "minUsd": 3.99,
            "maxUsd": 7.99
        },
        "Plus": {
            "minAmount": 9.99,
            "maxAmount": 9.99,
            "currency": "USD",
            "minUsd": 9.99,
            "maxUsd": 9.99
        },
        "SuperLike": {
            "minAmount": 4.99,
            "maxAmount": 9.99,
            "currency": "USD",
            "minUsd": 4.99,
            "maxUsd": 9.99
        }
    },
    "fx": {
        "source": "ECB",
        "date": "2026-09-25",
        "rate": 1
    },
    "error": "",
    "sourceUrl": "https://apps.apple.com/us/app/id547702041",
    "changes": []
}
```

**Google Play, United States — same app, the same run, this store's genuinely different
shape.** No named plans here at all; Google Play's public listing only ever exposes one
aggregate price range across every in-app item Tinder sells on this platform.

```json
{
    "recordType": "storefront_snapshot",
    "brand": "tinder",
    "platform": "googleplay",
    "country": "us",
    "appId": "com.tinder",
    "observedAt": "2026-09-26T14:02:59.291Z",
    "status": "ok",
    "found": true,
    "appTitle": "Tinder Dating App: Chat & Date",
    "rating": 3.9014534950256348,
    "ratingCount": 9204414,
    "installs": "500M+",
    "lastUpdated": "2026-09-21T00:00:00.000Z",
    "containsAds": true,
    "iapPriceRange": {
        "min": 0.49,
        "max": 299.99,
        "currency": "USD"
    },
    "inAppPurchases": [],
    "fx": {
        "source": "ECB",
        "date": "2026-09-25",
        "rate": 1
    },
    "error": "",
    "sourceUrl": "https://play.google.com/store/apps/details?id=com.tinder&hl=en&gl=us",
    "changes": []
}
```

**App Store, Germany — a second country, a second currency, and both real Super Like
spellings from the same live page.** Captured from the same fixture this Actor's own test
suite uses to guard the parser, then replayed through the real `runDatingAppIntel` entry
point exactly the way `tests/injected-fetch-paths.test.js` replays a captured page through
the same code path — the enrichment (currency, `planFamily`, USD conversion, `planSummary`)
below is the real pipeline's own output, not hand-written. Notice `"3 Super-Likes"` (hyphen)
and `"5 Super Likes"` (space, no hyphen) both landing on `planFamily: "SuperLike"`, and every
EUR amount carrying a real `usd` figure from that day's ECB rate (`1.1403`):

```json
{
    "recordType": "storefront_snapshot",
    "brand": "tinder",
    "platform": "appstore",
    "country": "de",
    "appId": "547702041",
    "observedAt": "2026-09-26T15:50:13.855Z",
    "status": "ok",
    "found": true,
    "appTitle": "Tinder Dating App: Chat & Date‑App",
    "rating": 4.1,
    "ratingCount": 235720,
    "ratingHistogram": {
        "1": 29076,
        "2": 10790,
        "3": 23341,
        "4": 27875,
        "5": 144638
    },
    "installs": null,
    "version": "17.36.0",
    "lastUpdated": "2026-09-22T16:46:21.000Z",
    "releaseNotes": "Wir haben die Art und Weise verbessert, wie du Gleichgesinnte finden und kennenlernen kannst, die auch etwas Langfristiges, Aufregendes oder eine besondere Story suchen.\n\nFinde, was du wirklich suchst – mit Tinder.",
    "containsAds": null,
    "iapPriceRange": null,
    "inAppPurchases": [
        {
            "name": "Tinder Gold",
            "priceText": "13,99 €",
            "amount": 13.99,
            "currency": "EUR",
            "planFamily": "Gold",
            "durationDays": null,
            "usd": 15.952797
        },
        {
            "name": "Tinder Gold",
            "priceText": "27,49 €",
            "amount": 27.49,
            "currency": "EUR",
            "planFamily": "Gold",
            "durationDays": null,
            "usd": 31.346847
        },
        {
            "name": "1 Boost",
            "priceText": "9,99 €",
            "amount": 9.99,
            "currency": "EUR",
            "planFamily": "Boost",
            "durationDays": null,
            "usd": 11.391597
        },
        {
            "name": "1 Boost",
            "priceText": "7,99 €",
            "amount": 7.99,
            "currency": "EUR",
            "planFamily": "Boost",
            "durationDays": null,
            "usd": 9.110997
        },
        {
            "name": "Tinder Gold",
            "priceText": "13,99 €",
            "amount": 13.99,
            "currency": "EUR",
            "planFamily": "Gold",
            "durationDays": null,
            "usd": 15.952797
        },
        {
            "name": "Tinder Platinum",
            "priceText": "32,99 €",
            "amount": 32.99,
            "currency": "EUR",
            "planFamily": "Platinum",
            "durationDays": null,
            "usd": 37.618497
        },
        {
            "name": "3 Super-Likes",
            "priceText": "11,99 €",
            "amount": 11.99,
            "currency": "EUR",
            "planFamily": "SuperLike",
            "durationDays": null,
            "usd": 13.672197
        },
        {
            "name": "5 Super Likes",
            "priceText": "5,99 €",
            "amount": 5.99,
            "currency": "EUR",
            "planFamily": "SuperLike",
            "durationDays": null,
            "usd": 6.830397
        },
        {
            "name": "Tinder Gold",
            "priceText": "16,49 €",
            "amount": 16.49,
            "currency": "EUR",
            "planFamily": "Gold",
            "durationDays": null,
            "usd": 18.803547
        },
        {
            "name": "5 Super Likes",
            "priceText": "9,99 €",
            "amount": 9.99,
            "currency": "EUR",
            "planFamily": "SuperLike",
            "durationDays": null,
            "usd": 11.391597
        }
    ],
    "planSummary": {
        "Gold": {
            "minAmount": 13.99,
            "maxAmount": 27.49,
            "currency": "EUR",
            "minUsd": 15.952797,
            "maxUsd": 31.346847
        },
        "Boost": {
            "minAmount": 7.99,
            "maxAmount": 9.99,
            "currency": "EUR",
            "minUsd": 9.110997,
            "maxUsd": 11.391597
        },
        "Platinum": {
            "minAmount": 32.99,
            "maxAmount": 32.99,
            "currency": "EUR",
            "minUsd": 37.618497,
            "maxUsd": 37.618497
        },
        "SuperLike": {
            "minAmount": 5.99,
            "maxAmount": 11.99,
            "currency": "EUR",
            "minUsd": 6.830397,
            "maxUsd": 13.672197
        }
    },
    "fx": {
        "source": "ECB",
        "date": "2026-09-25",
        "rate": 1.1403
    },
    "fxNote": null,
    "error": "",
    "sourceUrl": "https://apps.apple.com/de/app/id547702041",
    "changes": []
}
```

#### Partial / "source said no": a country where Tinder genuinely is not sold

**App Store, Russia — a real HTTP 404, not a guess.** Apple's own App Store returned a
genuine "this app is not available in this country's storefront" response for `ru` at
capture time. `error` is an empty string here on purpose — the source answered cleanly, it
is simply "no", not a technical failure, and the row is free, never billed:

```json
{
    "recordType": "storefront_snapshot",
    "brand": "tinder",
    "platform": "appstore",
    "country": "ru",
    "appId": "547702041",
    "observedAt": "2026-09-26T13:21:46.294Z",
    "appTitle": null,
    "rating": null,
    "ratingCount": null,
    "ratingHistogram": null,
    "installs": null,
    "version": null,
    "lastUpdated": null,
    "releaseNotes": null,
    "containsAds": null,
    "iapPriceRange": null,
    "inAppPurchases": [],
    "planSummary": {},
    "fx": null,
    "fxNote": null,
    "status": "not_available_in_country",
    "found": false,
    "error": "",
    "sourceUrl": "https://apps.apple.com/ru/app/id547702041"
}
```

#### Failure path 1: a malformed country code, rejected before any request

**A code that is not a real ISO-3166 territory at all.** `zz` never reaches Apple or Google —
this Actor's own input validation catches it first, and the free row explains exactly why,
with an empty `sourceUrl` because no request was ever sent:

```json
{
    "recordType": "storefront_snapshot",
    "brand": "tinder",
    "platform": "appstore",
    "country": "zz",
    "appId": "547702041",
    "observedAt": "2026-09-26T15:49:15.162Z",
    "appTitle": null,
    "rating": null,
    "ratingCount": null,
    "ratingHistogram": null,
    "installs": null,
    "version": null,
    "lastUpdated": null,
    "releaseNotes": null,
    "containsAds": null,
    "iapPriceRange": null,
    "inAppPurchases": [],
    "planSummary": {},
    "fx": null,
    "fxNote": null,
    "sourceUrl": null,
    "status": "invalid_country",
    "found": false,
    "error": ""
}
```

#### Failure path 2: the source did not answer after retries

**A simulated exhausted-retries 429, run through the real code path.** This Actor's fetch
layer retries a rate-limited request with backoff before giving up; the row below is what
the real `runDatingAppIntel` entry point produces once every retry has been exhausted,
captured via the exact same `fetchImpl` injection point `tests/injected-fetch-paths.test.js`
uses to test this path without spamming Apple's real servers on every `node --test` run —
the row shape is identical to what a genuine live 429 produces, because it is the same code,
the same real Apify `ChargingManager`, just with the network call itself swapped for a
scripted response:

```json
{
    "recordType": "storefront_snapshot",
    "brand": "tinder",
    "platform": "appstore",
    "country": "us",
    "appId": "547702041",
    "observedAt": "2026-09-26T15:49:06.492Z",
    "appTitle": null,
    "rating": null,
    "ratingCount": null,
    "ratingHistogram": null,
    "installs": null,
    "version": null,
    "lastUpdated": null,
    "releaseNotes": null,
    "containsAds": null,
    "iapPriceRange": null,
    "inAppPurchases": [],
    "planSummary": {},
    "fx": null,
    "fxNote": null,
    "sourceUrl": "https://apps.apple.com/us/app/id547702041",
    "status": "rate_limited",
    "found": false,
    "error": "http 429 after retries"
}
```

The free run-level summary for that same run (`OUTPUT` key in the key-value store) shows
exactly how a rate-limited storefront is accounted for — `storefrontsBilled: 0`,
`partial: true`, and the specific storefront named in `rateLimitedStorefronts`:

```json
{
    "brand": "tinder",
    "watchId": "default",
    "storefrontsRequested": 1,
    "storefrontsProcessed": 1,
    "storefrontsBilled": 0,
    "storefrontsFree": 1,
    "truncated": false,
    "maxStorefronts": 1,
    "deadlineHit": false,
    "rateLimitedStorefronts": [
        "appstore:us"
    ],
    "notAvailableStorefronts": [],
    "sourceErrorStorefronts": [],
    "invalidCountryStorefronts": [],
    "partial": true,
    "fx": null,
    "cheapestByFamily": {},
    "mostExpensiveByFamily": {},
    "generatedAt": "2026-09-26T15:49:06.513Z"
}
```

### Field dictionary

| Field | Type | Meaning |
|---|---|---|
| `recordType` | string | Always `"storefront_snapshot"` — one fixed shape for every row this line's actors produce, across all five brands. |
| `brand` | string | This Actor's own brand key, always `"tinder"` here. |
| `platform` | string | `"appstore"` or `"googleplay"` — which storefront this row is about. |
| `country` | string | null | The ISO-3166 alpha-2 code requested. `null` only ever appears on a sibling actor's RuStore row (RuStore doesn't vary by country) — every Tinder row always carries a real country code, since Tinder has no RuStore listing. |
| `appId` | string | The App Store numeric id or the Google Play package name this row was fetched against — `547702041` or `com.tinder`, always the same two values for this Actor regardless of country. |
| `observedAt` | string (ISO 8601) | The exact moment this Actor read the page, in UTC. Two rows from the same run typically differ by a few hundred milliseconds. |
| `status` | string | One of five values, each described in its own row below. |
| `found` | boolean | `true` only for `status: "ok"`. `false` for every other status — a quick filter for "did this storefront actually deliver data" without checking the exact status string. |
| `appTitle` | string | null | The store's own title for the app in that storefront's own language — `"Tinder Dating App: Chat & Date"` on the US App Store, `"Tinder Dating App: Chat & Date‑App"` on the German one (note the real, published German title literally ends in a non-breaking hyphen before "App" — reported exactly as Apple publishes it). `null` whenever `found` is `false`. |
| `rating` | number | null | The store's own current average rating, out of 5, at the time of the read. On the App Store this Actor also cross-checks the rating against the page's own 1-through-5-star histogram sum and prefers the histogram's own total when one is present, since it reflects this exact parse rather than a potentially stale summary elsewhere on the same page. |
| `ratingCount` | number | null | Total number of ratings behind the `rating` figure. |
| `ratingHistogram` | object | null | App Store only — `{"1": n, "2": n, "3": n, "4": n, "5": n}`, the count of ratings at each star level. Google Play's public page does not expose this breakdown, only the two aggregate numbers above, so this is always `null` on a `googleplay` row. |
| `installs` | string | null | Google Play only — the store's own bucketed install-count label (`"500M+"`, `"100M+"`, and so on). The App Store does not publish an install count at all, on any page this line reads, so this is always `null` on an `appstore` row. |
| `version` | string | null | The currently listed app version string. |
| `lastUpdated` | string (ISO 8601) | null | The date the store itself lists as the app's last update. |
| `releaseNotes` | string | null | The "What's new in this version" text, in the storefront's own language, taken as-is including its own line breaks. |
| `containsAds` | boolean | null | Google Play only — the store's own "Contains ads" disclosure. The App Store's own feed does not carry an equivalent field, so this is always `null` on an `appstore` row. |
| `iapPriceRange` | object | null | Google Play only — `{min, max, currency}` across every in-app item the listing sells, with no plan names attached. Always `null` on an `appstore` row, where the full named list lives in `inAppPurchases[]` instead. |
| `inAppPurchases` | array | App Store only — one entry per plan the page listed, `{name, priceText, amount, currency, planFamily, durationDays, usd}`. Always `[]` on a `googleplay` row. |
| `inAppPurchases[].name` | string | The plan's own name exactly as the storefront prints it — `"Tinder Gold"`, `"1 Boost"`, `"3 Super-Likes"`. Never rewritten or normalized; `planFamily` is the normalized read, this field is the raw one. |
| `inAppPurchases[].priceText` | string | The storefront's own formatted price string, exactly as printed — `"$14.99"`, `"13,99 €"`. |
| `inAppPurchases[].amount` | number | null | The numeric amount parsed out of `priceText`, locale-aware (comma vs. period as the decimal separator is decided by counting digits after the last separator, not by assuming one convention). `null` only when the currency itself could not be resolved with confidence — this Actor never reports an amount without knowing what currency it is in. |
| `inAppPurchases[].currency` | string | null | ISO-4217 currency code resolved from the price string's own symbol/marker together with the storefront's own country (needed to disambiguate a bare `$`, which several currencies share). |
| `inAppPurchases[].planFamily` | string | This brand's normalized plan family — see the plan dictionary table right below this one. `"other"` when the name does not match anything in the dictionary; never guessed. |
| `inAppPurchases[].durationDays` | number | null | Set only when the plan's own name states a duration in words ("7 Days", "1 month") — Tinder's own IAP names on the App Store never do this for any plan this line has observed; every Tinder entry captured so far has `durationDays: null` for exactly that reason, not because the Actor failed to find one. |
| `inAppPurchases[].usd` | number | null | The `amount` converted to USD via yesterday's ECB daily reference rate, when `convertToUsd` is on and the ECB publishes a rate for that currency. `null` with a same-row `fxNote` explaining why otherwise (RUB, KZT, AED and several others are not in the ECB's daily feed at all). |
| `planSummary` | object | One entry per `planFamily` seen in this row's `inAppPurchases[]`, each `{minAmount, maxAmount, currency, minUsd, maxUsd}` — the cheapest and most expensive price point for that family within this single storefront. `{}` on a `googleplay` row or any row where `found` is `false`. |
| `fx` | object | null | `{source: "ECB", date, rate}` — the exact EUR-denominated rate this Actor used for this row's currency, or `null` when `convertToUsd` is off or the row's status means there was nothing to convert. |
| `fxNote` | string | null | Set only when `convertToUsd` was requested but the ECB has no rate for this row's currency — states which currency and why `usd` stayed `null`. |
| `error` | string | Empty for `not_available_in_country` and `invalid_country` — the source (or this Actor's own input check) answered cleanly, it is genuinely "no", not a failure. A real, specific message for `rate_limited` and `source_error`. |
| `sourceUrl` | string | null | The exact URL this row's data came from — `https://apps.apple.com/{cc}/app/id547702041` or the equivalent Google Play URL. `null` only for `invalid_country`, where no request was ever sent. |
| `changes[]` | array | Populated only with `compareWithPreviousRun: true`. Empty on a fresh `watchId`'s first run; contains one `baseline_reset` entry if the previously saved snapshot for that `watchId` could not be read; otherwise one entry per detected change (new plan, removed plan, price move, rating move ≥0.05, version bump). |

#### Plan dictionary — how `planFamily` is decided

| planFamily | Matched from (case-insensitive substring, longest match wins) |
|---|---|
| `Plus` | `"Tinder Plus"` |
| `Gold` | `"Tinder Gold"` |
| `Platinum` | `"Tinder Platinum"` |
| `Select` | `"Tinder Select"` |
| `Boost` | `"Boost"` (any name containing the word, EXCEPT one that also contains "Super" — that one is `SuperBoost` instead, since the longer match wins) |
| `SuperBoost` | `"Super Boost"` |
| `SuperLike` | `"Super Like"`, `"Super Likes"`, `"Super-Likes"` — every real spelling variant observed live, hyphenated or not, singular or plural |
| `other` | anything not matching the above — reported honestly rather than forced into the nearest-sounding family |

### Evidence and boundaries

**What this Actor reads, exactly.** Two public product pages, nothing else:
`https://apps.apple.com/{cc}/app/id547702041` and
`https://play.google.com/store/apps/details?id=com.tinder&hl=en&gl={cc}`. Both are checked
by fetching them, structurally parsing the returned HTML for the specific blocks this line's
parser is built against (a JSON-LD `aggregateRating` block and an `AnnotationItem` structure
carrying `textPairs` on the App Store; a similar structured block on Google Play), and
validating the page's own shape before trusting anything it says — a canonical-id mismatch,
a missing rating block, or an unrecognized page structure all fail closed with a
`source_error`, never a best-effort guess dressed up as data.

**robots.txt, checked, not assumed.** apps.apple.com's robots file disallows `/api/*`,
`/v1/*`, `/WebObjects/*`, `/includes/*` and any `*/search?*` path; the plain product page
path this Actor fetches is not among them. play.google.com's robots file disallows
`/store/getreviews`, `/store/search`, `/store/xhr` and `/store/apps/datasafety*`; again, the
plain product page path is not among them. This Actor never touches any of the disallowed
paths on either domain — it does not search either store, does not read reviews through
either store's dedicated review endpoint, and does not call either company's private API.

**429 is a measured, engineered-around risk, not a guess.** From this line's own test
infrastructure's IP range, hitting Apple's App Store every 8 seconds produced 5 real `200`
responses and 7 real `429`s out of 12 consecutive requests — a genuine, reproduced rate limit,
not a theoretical one. The same IP, paced at 30 seconds between requests with a 45-second
retry after any 429, produced 5 real `200`s out of 5. This Actor's own fetch layer caps
concurrent requests toward Apple at 2 per run (Google Play carries no equivalent constraint
in this line's testing) and retries a rate-limited request with backoff before giving up and
reporting `rate_limited` — a real engineering response to a measured problem, not a
documentation promise about a problem nobody checked for.

**What the App Store's own feed does NOT publish, stated plainly rather than papered over.**
No plan ever carries a duration in this line's own observation of Tinder's IAP list — Apple's
page states a price and a name, never "per month" or "per week" as a separate machine-
readable field, so `durationDays` is `null` for every Tinder plan this Actor has ever
captured, and will stay that way unless Apple's own feed changes. The App Store's product
page likewise does not publish the app's age rating anywhere this line's parser reads, nor
an install count (that is Google Play's own disclosure, not Apple's). None of these are gaps
in this Actor's parsing; they are gaps in what the source itself chooses to make public on
this specific page, and this README says so directly rather than leaving a buyer to discover
it by trial and error.

**What this Actor does NOT do, and will not be extended to do.** It does not create an
account, log in, or authenticate as a Tinder user in any way. It does not read, request or
infer anything about any real Tinder user — no profiles, no photos, no names, no messages,
no match data, no geolocation. It does not read App Store or Google Play customer reviews
through either store's dedicated review feed or endpoint (both are disallowed by robots.txt
and neither is fetched). It does not search either store — every app identifier this Actor
ever uses is hard-coded into the brand's own configuration, never looked up by a search
query. It calls no private API of Match Group's, Apple's or Google's. Every one of these is a
deliberate, permanent boundary of this Actor's design, not a "not yet implemented" gap.

### Decision routing

**Use this Actor when** you specifically want Tinder's own subscription prices, normalized
into a stable `planFamily` you can compare across countries, with an app-title/rating/version
read attached, and you are fine with the App Store and Google Play as the two sources (no
Tinder API, because there is no public one that discloses pricing this way).

**Use a general-purpose App Store or Google Play scraper instead** — the kind of Actor
already listed on Apify's Store as an "App Store scraper" or "Google Play scraper" without a
brand name in its own title — when you want ANY app's page data, not specifically a dating
app's subscription pricing normalized into a plan dictionary. Those Actors are broader and
typically cheaper per row for a single raw page read; this Actor is narrower and deliberately
adds the normalization, currency conversion and change-tracking layer on top, which is the
whole reason it costs more per storefront than a bare page fetch would.

**Use one of this Actor's own siblings instead of Tinder** when the brand you actually care
about is a different dating app: the same underlying engine, plan-dictionary approach and
pricing model also ship as separate, brand-specific Actors for Twinby (adds RuStore, since
Twinby is sold there and Tinder is not), Bumble, Hinge and Badoo — each with its own real
plan dictionary tuned to that brand's own naming (Bumble's Premium/Premium+, Hinge's
Hinge+/Membership/Roses, Badoo's Premium/Super Powers/Credits). If you need prices for
several of these brands side by side, run each brand's own Actor with the same `countries`
list and merge the datasets yourself, or watch for a future combined-brand version of this
line.

**Use this Actor's `compareWithPreviousRun` feature, not a fresh manual comparison,** when
the question is "did anything change since last time" rather than "what is the price right
now" — the diff logic already exists, is billed the same way as a fresh read, and avoids the
easy mistake of comparing rows that were captured under different `convertToUsd` settings or
different `watchId`s by accident.

**Do not use this Actor** if what you actually need is Tinder USER data — profiles, swipes,
matches, messages, or anything else that would require an account and would read information
about real people rather than about the store listing itself. No configuration of this Actor
will ever do that; it is a boundary, not a missing feature request.

### Commercial playbooks

**Weekly pricing-power tracking for an equity research desk.** Schedule this Actor once a
week against every storefront relevant to Match Group's disclosed revenue geography (the
larger US/UK/DE/FR/JP/BR/AU set is a reasonable starting list), with `compareWithPreviousRun`
on and a stable `watchId` like `"mtch-coverage"`. Export the dataset to a spreadsheet or feed
it straight into a model via the API. Over a quarter, this produces a real time series of
Gold/Plus/Platinum pricing per country that did not exist as a maintained dataset before —
every price move becomes a dated row in `changes[]`, not something reconstructed from memory
or a one-off manual check the week before an earnings call.

**Competitive benchmarking before a Boost/Super Like price change.** A product or growth team
at a competing dating app reads this Actor's output on Tinder specifically, as the market
leader's own price, against the exact countries they are about to launch or reprice in, pulls
the current Gold/Boost/Super Like price points for that market, and uses them as one direct
input into their own pricing committee's decision — a same-day read instead of a multi-hour
manual App Store visit across a dozen country storefronts.

**ASO agency monthly client report.** An agency already tracking a client's own app ranking,
rating trend and review velocity adds a "competitive pricing" section built entirely from
this Actor's dataset, filtered to the two or three direct dating-app competitors most
relevant to that client, refreshed on the same monthly cadence as the rest of the report,
with zero extra manual research time per client per month.

**Currency and purchasing-power research.** A researcher studying price discrimination in
digital subscriptions across countries uses Tinder Gold's price ladder (and the equivalent
from this line's other brand Actors) as a clean, comparable input, since App Store and
Google Play prices are set deliberately per country with no shipping or import-tax noise to
strip out — pulling a fresh cross-country snapshot takes one run rather than sixty manual
App Store visits with the storefront set to a different country each time.

**Personal cost tracking while relocating or traveling.** An individual keeping a Tinder Gold
subscription active while their billing country changes runs this Actor with their old and
new country in the same `countries` list and a personal `watchId`, and reads the price
difference directly from `planSummary` in the resulting rows before deciding whether to
re-subscribe in the new country or keep the old billing region.

### Integration recipes

**Schedule it (Apify Console).** Open this Actor's page, go to the **Schedules** tab, and
create a schedule with a cron expression — `0 8 * * 1` for "every Monday at 08:00 UTC" is a
common choice for weekly price tracking. Attach the exact input you want repeated (the
prefill above, or your own country/platform list with `compareWithPreviousRun: true` and a
fixed `watchId`), and every scheduled run lands in the same Actor's dataset history,
automatically diffed against the run before it.

**Call it from the REST API.**

```bash
curl -X POST "https://api.apify.com/v2/acts/zinin~tinder-app-intel/runs?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"countries": ["us", "gb", "de"], "platforms": ["appstore", "googleplay"], "convertToUsd": true}'
```

The response includes a run id; poll `GET /v2/actor-runs/{runId}` for status, then read
`GET /v2/datasets/{defaultDatasetId}/items` for the rows once the run finishes — or use the
synchronous `run-sync-get-dataset-items` endpoint if you want the rows back in the same
request and your country list is short enough to finish within that endpoint's timeout.

**Call it from Node.js or Python via Apify's own SDK.**

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('zinin/tinder-app-intel').call({
    countries: ['us', 'gb'],
    platforms: ['appstore', 'googleplay'],
    convertToUsd: true,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

The Python client (`apify-client` on PyPI) mirrors the same call shape:
`client.actor('zinin/tinder-app-intel').call(run_input={...})`.

**Wire it to a webhook.** From this Actor's page or via the API's
`POST /v2/webhooks` endpoint, attach a webhook fired on `ACTOR.RUN.SUCCEEDED` (and, if you
want to react to a partial run too, `ACTOR.RUN.SUCCEEDED` still fires when `OUTPUT.partial`
is `true` — check that field in the payload rather than relying on the run's overall status
alone) pointing at your own endpoint. Apify posts the run's summary, including the dataset
id, so your endpoint can immediately pull the fresh rows rather than polling.

**Call it as a tool from an LLM agent, via Apify's MCP server.** Apify exposes published
Actors as callable tools through its own Model Context Protocol server
(`https://mcp.apify.com`, documented on Apify's own site); once this Actor is published, an
MCP-aware agent (Claude, or any other MCP client) can call it by name with a structured input
exactly like any other tool, without you writing custom integration code — the same
`countries`/`platforms`/`convertToUsd` input shape works there as anywhere else.

**Export straight to Google Sheets.** From the Dataset tab, use Apify's built-in "Export
data" action and choose the Google Sheets format, or connect this Actor's dataset to a
Sheets-sync integration (Apify's own Google Sheets integration, or a Zapier/Make automation
watching for `ACTOR.RUN.SUCCEEDED`) to keep a live spreadsheet updated on the same schedule
as your scheduled runs — no code required for this path at all.

**Plain CSV/Excel for a one-off analysis.** The Dataset tab's **Export** button produces a
CSV or Excel file of exactly the rows in that run, one row per `country × platform` pair,
ready to open directly in a spreadsheet without any further transformation — the same shape
as the JSON rows shown throughout this README, just flattened into columns.

### Operating guide

**Reading a partial run.** Check `OUTPUT.partial` first, not the run's own top-level status —
a run can finish successfully from the platform's point of view while some storefronts came
back `rate_limited` or `source_error`. `partial: true` plus a non-empty
`rateLimitedStorefronts` or `sourceErrorStorefronts` array tells you exactly which
`country:platform` pairs to re-run, without re-running the whole list. A re-run of just the
missing storefronts costs exactly what those storefronts would have cost the first time —
nothing extra, and nothing double-charged for the storefronts that already succeeded.

**When to retry a rate-limited storefront, and when not to bother.** A single `rate_limited`
row on an otherwise-successful run is usually transient — Apple's rate limiting is real but
short-lived at the pacing this Actor already uses internally (see Evidence and boundaries).
Waiting a few minutes and re-running just that storefront is normally enough. If the SAME
storefront comes back `rate_limited` repeatedly across several attempts spread over an hour
or more, that is worth treating as a real, ongoing block from that IP range rather than
transient noise, and is exactly the kind of pattern this Actor's free, honest `rate_limited`
status is designed to surface rather than hide behind a retry loop that never tells you.

**Choosing a `watchId` strategy.** One `watchId` per logical tracking list, not per run — if
you track "MTCH investor coverage" weekly and "competitive Boost pricing" separately, give
them two different `watchId`s so their histories never mix. Reusing the same `watchId` for
two different `countries`/`platforms` lists on different schedules will not corrupt data (the
diff is keyed per `country`/`platform`/`watchId` combination, not per whole run), but it does
make the resulting `changes[]` history harder to reason about later, since two different
audiences' expectations end up sharing one label.

**maxStorefronts on a large country list.** If you list more `countries × platforms` combos
than `maxStorefronts` allows, the excess combinations are dropped before any request is made
and reported in one free row (`status: "truncated"`) that names exactly which storefronts
were skipped, in the same dataset as the successful rows — not a separate log you have to go
find. Raise `maxStorefronts` (up to 120) if you genuinely want a run this large in one go;
otherwise, splitting a long country list across a few scheduled runs keeps each run's
`OUTPUT.deadlineHit` comfortably `false` (this Actor's own soft wall-clock budget for one run
is 280 seconds on the prefill-sized path, checked between storefronts rather than mid-fetch,
so a combo already in flight is always allowed to finish).

**Interpreting a `usd: null` price.** This means exactly one thing: the ECB's daily reference
rate feed does not include that currency, not that the price itself is unknown or unreliable.
`fxNote` on the same row states which currency and repeats that fact in plain language. If
you need a USD equivalent for one of these currencies anyway, treat `amount`/`currency` as
the ground truth and apply whatever FX source your own use case already trusts — this Actor
will not fabricate a number from a source it cannot vouch for.

### FAQ

**Why does Google Play never show named plans, only a price range?** Because that is
genuinely all Google Play's own public listing discloses — one aggregate
`min - max per item` figure across every in-app item the app sells, never a per-plan
breakdown. This is not a gap in this Actor's parsing; the App Store's listing is the one that
names plans individually, and this Actor reports each store's real, different shape rather
than inventing a merged view neither store actually publishes.

**Do prices include currency conversion by default?** Yes — `convertToUsd` defaults to
`true`. Turn it off if you specifically want each storefront's own local price and currency
with no FX applied at all.

**What is the difference between `not_available_in_country` and `invalid_country`?**
`not_available_in_country` means the country code was real and this Actor genuinely asked
the App Store or Google Play, and the store answered "this app is not sold here" (a real
404, in Tinder's case, for `ru` at the time this README was captured). `invalid_country`
means the code itself was never a real ISO-3166 territory in the first place (`zz`), so no
request was ever sent to either store at all. Both are free and both leave `error` empty,
because in both cases the outcome is a clean "no" rather than a technical failure — but they
are different facts and this Actor reports them as different statuses rather than collapsing
them into one generic "not found".

**Why is there no `durationDays` on any Tinder plan?** Because Apple's own IAP feed for
Tinder never states one — no plan's name says "per month" or "7 days" as a separate,
parseable fact on the page this Actor reads. `durationDays` is populated whenever a
storefront's own text states a duration (this happens on some of this line's other brand
Actors, where a plan is literally named "7 Days Premium"); for Tinder specifically, the
honest answer is that the source does not disclose it, so this Actor does not either.

**Does this Actor read anything about real Tinder users?** No, and it never will. It reads
one public product page per country per platform — the same page anyone visiting
apps.apple.com or play.google.com already sees, with no login. It does not create an
account, does not authenticate, and has no code path that could read a profile, a photo, a
message or any other personal data belonging to a real person. See Evidence and boundaries
for the exact, permanent list of what this Actor does not do.

**What happens if Apple rate-limits my run?** The affected storefront comes back
`status: "rate_limited"`, `found: false`, a non-empty `error` naming the HTTP status, and is
never charged. The run's free `OUTPUT` summary sets `partial: true` and lists the exact
storefront in `rateLimitedStorefronts`, so you know precisely what to re-run rather than
re-running the whole list. This Actor already paces its own requests toward Apple (at most 2
concurrent) specifically to keep this rare in normal use — see Evidence and boundaries for
the measured numbers behind that choice.

**Can I run this against every country in the world in one go?** Up to 60 countries and up
to 2 platforms per run (120 combinations at most, or fewer if `maxStorefronts` is set lower),
enforced before any request is made. A run this large against Apple, at this Actor's own
pacing, will take meaningfully longer than the default two-country example — see Operating
guide for how the soft run-length budget and `maxStorefronts` truncation interact on a large
list.

**Found a wrong result, or need a country/plan-name edge case handled that this Actor
missed?** Open an issue on this Actor's own page. This line already fixes real parsing bugs
found this way — several of the field-level details described in this README (the German
title's non-breaking hyphen, the two different Super-Like spellings, a last-entry-dropped
regex bug an earlier version of this parser had) were found and fixed exactly this way,
against real captured pages, not assumed away.

### Related app intelligence tools

- [App Store App Intel](https://apify.com/zinin/appstore-app-intel) — Inspect another iOS app by app ID or App Store URL.
- [Google Play App Intel](https://apify.com/zinin/googleplay-app-intel) — Get a public Google Play app card by package ID or URL.
- [App Store Customer Reviews](https://apify.com/zinin/appstore-customer-reviews-scraper) — Collect public App Store review text for separate review analysis.

### Sources and rights

Every fact in this Actor's output comes from one of two public sources: Apple's own App
Store product page at `apps.apple.com/{cc}/app/id547702041`, and Google's own Google Play
product page at `play.google.com/store/apps/details?id=com.tinder`. Both are pages any
visitor can open in a browser with no account; this Actor reads the same HTML a browser
would receive, respects both domains' published `robots.txt` (see Evidence and boundaries
for exactly which paths are and are not touched), and does not call any private, undocumented
or authenticated API belonging to Apple, Google or Match Group.

Currency conversion, when requested, uses the European Central Bank's own published daily
reference rate feed (`ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml`) — public data, no
key or account required, and not this Actor's own estimate.

This Actor collects no personal data of any kind. It does not create an account with either
store, does not authenticate, and has no code path that reads, infers or stores anything
about a real Tinder user. "Tinder" and its associated plan names (Gold, Plus, Platinum,
Boost, Super Like) are Match Group's own trademarks and terminology, reported here only
because they are the storefront's own public labels for its own public prices — this Actor
is an independent tool built against public store pages, and is not affiliated with, endorsed
by, or operated on behalf of Match Group, Tinder, Apple or Google.

Found a wrong result, or need a case this README doesn't cover? Open an issue on this
Actor's own page.

***

Built by [zinin](https://apify.com/zinin). Questions? Telegram [@timzinin](https://t.me/timzinin).

# Actor input Schema

## `countries` (type: `array`):

Storefront countries to check, ISO-3166 alpha-2 codes (e.g. `us`, `gb`, `de`). 1-60 codes; duplicates are removed; a code that isn't a real ISO country produces a free `invalid_country` row.

## `platforms` (type: `array`):

Which storefronts to check for each country.

## `convertToUsd` (type: `boolean`):

Add a `usd` field to every in-app purchase using the ECB daily reference rate. When the ECB has no rate for a storefront's currency (e.g. RUB), `usd` is left null rather than guessed — the local-currency price is still delivered.

## `compareWithPreviousRun` (type: `boolean`):

Diff each storefront's prices/rating/version against the last run of this Actor with the same `watchId` (stored in your account's key-value store, isolated per buyer). First run for a `watchId` reports a clean baseline, not a false "change".

## `watchId` (type: `string`):

Label for the comparison baseline (letters, digits, `-`, `_`, up to 40 chars). Use different watch IDs to track separate country/platform sets independently.

## `maxStorefronts` (type: `integer`):

Safety cap on countries x platforms for this run. Extra combinations are dropped (not charged for) before any work starts.

## Actor input object example

```json
{
  "countries": [
    "us",
    "gb"
  ],
  "platforms": [
    "appstore",
    "googleplay"
  ],
  "convertToUsd": true,
  "compareWithPreviousRun": false,
  "watchId": "default",
  "maxStorefronts": 10
}
```

# Actor output Schema

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

API URL for the default dataset items produced by this run.

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

Free per-run summary (counts, rate-limited/skipped storefronts, FX status).

# 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 = {
    "countries": [
        "us",
        "gb"
    ],
    "platforms": [
        "appstore",
        "googleplay"
    ],
    "convertToUsd": true,
    "maxStorefronts": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("zinin/tinder-app-intel").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 = {
    "countries": [
        "us",
        "gb",
    ],
    "platforms": [
        "appstore",
        "googleplay",
    ],
    "convertToUsd": True,
    "maxStorefronts": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("zinin/tinder-app-intel").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 '{
  "countries": [
    "us",
    "gb"
  ],
  "platforms": [
    "appstore",
    "googleplay"
  ],
  "convertToUsd": true,
  "maxStorefronts": 10
}' |
apify call zinin/tinder-app-intel --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,zinin/tinder-app-intel"
        }
    }
}
```

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/Qn8DAtMWfInGnfUqn/builds/6VN9wkTa0Ha8idAYb/openapi.json
