# X (Twitter) Followers Scraper - $0.09 per 1,000 (`dami_studio/twitter-followers-scraper`) Actor

Scrape the followers of any public X (Twitter) account, or the accounts it follows. One row per profile: handle, name, bio, follower and following counts, posts, verification, location, avatar, website and join date. The cheapest X followers scraper on the market. No API key, no login, no cookies.

- **URL**: https://apify.com/dami\_studio/twitter-followers-scraper.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** Social media, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.09 / 1,000 accounts

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

## X (Twitter) Followers Scraper

Give it one or more X (Twitter) handles and get back the accounts that follow them — or, with one switch, the accounts they follow. Every row is a full profile: handle, display name, bio, follower and following counts, post count, verification badge and its type, location, avatar, banner, website and the date the account joined X. No API key, no cookie to paste, no browser.

- Followers or following, chosen with a single input field — the same rows either way.
- Every profile field is read from X itself, so the counts are the live ones, not a cached guess.
- Several handles per run, each paged independently up to your own per-handle limit.
- A handle that does not exist, or is protected, costs you nothing: it returns an uncharged row that says so.
- Runs with empty input return one labelled sample row, free, so you can see the shape before spending anything.

### Price

**$0.09 per 1,000 followers**, plus a **$0.0005 start fee per run** (billed per gigabyte of run memory, so exactly that on the default 1 GB).

This is **the cheapest X (Twitter) followers scraper on the market**, and it is the same rate on every plan, free or paid. There are no volume tiers, no minimum spend, no subscription and no add-on fees. What you read here is what you pay on day one and on day four hundred.

| Followers | Total cost |
|---|---|
| 100 | $0.0095 |
| 1,000 | $0.0905 |
| 10,000 | $0.9005 |
| 100,000 | $9.0005 |

#### What is actually charged

- **One `follower-scraped` event per follower row written to the dataset.** Nothing else is metered per row.
- **Free:** the sample row an empty run returns, and every diagnostic row — a blocked target, a dead URL, a search that matched nothing. Those rows all carry `"charged": false`.
- Accounts already returned for the same handle in the same run are dropped before they are charged.
- Handles that do not exist, are suspended or are protected return an uncharged diagnostic row.
- The sample row an empty run returns is never charged.
- A run that finds nothing costs the start fee and nothing more.
- Rows never leave the dataset without a charge, and are never charged without a row. The billed event is a named one, so there is no price quietly attached to `apify-default-dataset-item` — the trick that makes some scrapers bill you for their own error messages.

### Input

```json
{
  "handles": [
    "NASA",
    "github"
  ],
  "mode": "followers",
  "maxItemsPerHandle": 200
}
```

| Field | What it does |
|---|---|
| `handles` | One or more X handles, with or without the leading @. Full profile URLs such as `https://x.com/NASA` work too. Up to 10 per run. |
| `mode` | `followers` (default) returns the accounts that follow each handle. `following` returns the accounts each handle follows. |
| `maxItemsPerHandle` | How many accounts to return for each handle. Default 50, hard ceiling 2,000 per handle and 5,000 per run. Keep it low while testing — you pay per row. |
| `enrichProfiles` | On by default. Leave it on: it is what fills in follower counts, location, join date and the rest. Turn it off only if all you want is the list of handles. |
| `proxyUrls` | Leave empty. Fill it in only if you want the traffic to leave through proxy servers you already pay for, as `http://user:pass@host:port`. |
| `sessionCookies` | Optional, and not needed for a normal run. Leave it empty. An `auth_token=…; ct0=…` pair from your own logged-in x.com is used only as a fallback, on the rare run where no public source answers, and it gives that fallback a rate limit nobody else draws on. Stored as a secret. |

Run it with **empty input** and you get one clearly labelled sample row, free, so you can see the output shape before you spend anything.

### Output

One row per follower. A real row from a real run:

```json
{
  "ok": true,
  "charged": true,
  "recordType": "profile",
  "sourceHandle": "NASA",
  "relation": "follower",
  "userName": "NASAHubble",
  "name": "Hubble",
  "url": "https://x.com/NASAHubble",
  "description": "The official X account for the NASA Hubble Space Telescope, managed and operated by NASA's Goddard Space Flight Center.",
  "followersCount": 8906746,
  "followingCount": 44,
  "tweetsCount": 8427,
  "isVerified": true,
  "verifiedType": "government",
  "location": "Goddard Space Flight Center",
  "profileImageUrl": "https://pbs.twimg.com/profile_images/3468011581/efb985f24af0a814a722457a768f3cc5_400x400.jpeg",
  "createdAt": "2008-03-06T20:15:02.000Z",
  "isProtected": false,
  "website": "http://www.nasa.gov/mission_pages/hubble/main/index.html",
  "profileBannerUrl": "https://pbs.twimg.com/profile_banners/14091091/1669391293",
  "listedCount": 15230,
  "mediaCount": 3116,
  "favouritesCount": 1241,
  "userId": "14091091",
  "profileEnriched": true,
  "scrapedAt": "2026-08-16T15:48:36.774Z"
}
```

#### Field notes

- `sourceHandle` — the handle you asked about — the account whose list this row came from. Use it to split a multi-handle run.
- `relation` — `follower` when the row is somebody who follows `sourceHandle`, `following` when it is somebody `sourceHandle` follows.
- `userName` — the handle, exactly as X spells it, without the @. Safe to use as a key.
- `userId` — X's numeric account id. It never changes, even when the handle does, so it is the better key for anything long-lived.
- `description` — the bio as written, including line breaks and emoji.
- `followersCount` — the live follower count at read time, straight from X.
- `followingCount` — how many accounts this profile follows.
- `isVerified` — true when the account carries a checkmark of any kind.
- `verifiedType` — which kind: `blue`, `business`, `government` or `creator`. Null when the account is not verified.
- `location` — the free-text location field on the profile. People write anything they like in it, including jokes.
- `website` — the link on the profile, expanded from the shortened form X displays.
- `profileImageUrl` — the avatar at 400x400 rather than the 48px thumbnail X returns by default.
- `createdAt` — the date the account joined X, normalised to ISO 8601 in UTC.
- `isProtected` — true when the account is private. Its own posts and lists are not readable.
- `profileEnriched` — true when the profile fields were filled in from X. False means only the handle and display name were available for that row — rare, and visible so you can filter on it.

Every real row carries `"charged": true`. Sample rows carry `"_sample": true` and diagnostic rows carry `"_diagnostic": true` with an `errorCode` you can filter on, and neither is ever billed.

### How it works

- You give it handles. For each one it first asks X about the handle itself, which settles in a single request whether the account exists, whether it is protected, and how many entries the list even has — so a typo does not turn into a long, billed run.
- It then reads the follower (or following) list page by page from public, logged-out sources, following the same cursor the pages themselves expose, until it has as many accounts as you asked for. This is always tried first, because it needs no session and so competes with nothing.
- On the rare run where none of those sources answers, the list is read from X directly with a session instead of the handle being skipped. That path is a fallback and nothing else: it is never used while a public source is still answering, because the session is a shared resource and a page served publicly is a page that costs nobody anything.
- Every account it finds is then looked up on X directly, and that is where the profile fields in each row come from: the counts, the location, the badge, the avatar and the join date are the ones X itself reports at read time.
- Requests leave through a large pool of rotating addresses, so a per-address rate limit is answered by moving rather than by waiting, and the run keeps going.
- No account of yours is used, no cookie of yours is asked for, and nothing is created on your behalf. The only session involved is the Actor's own fallback, and you never have to supply or manage it.

### What people use it for

- Audience research — pull the followers of three accounts in your niche and see who overlaps, who is verified, and how big their own audiences are.
- Lead lists — filter the rows on `followersCount`, `location` or a keyword in `description` to get a shortlist of accounts worth reaching out to.
- Competitive audience mapping — run the same handles weekly and diff on `userId` to see exactly who arrived and who left.
- Influencer vetting — read the following list of an account and check what kind of profiles it actually follows before you pay for a placement.
- Community building — find the accounts that follow several of your peers but not you yet.
- Dataset building — the rows are flat and typed, so they load into a spreadsheet, a warehouse or a notebook without cleaning.

### Reading the output

Every run writes three kinds of row, and they are easy to tell apart:

- **Real rows** carry `"charged": true` and `"recordType": "profile"`. These are the rows you paid for, one billed event each.
- **The sample row** carries `"_sample": true` and `"charged": false`. There is exactly one, it only appears when the input had no handles, and it exists so you can look at the shape of the output before you spend anything.
- **Diagnostic rows** carry `"_diagnostic": true`, `"charged": false` and an `errorCode` you can switch on: `NOT_FOUND` when a handle does not exist, is suspended or is protected; `NO_RESULTS` when the list is genuinely empty; `BLOCKED` when no public source answered for that handle on that run; `NETWORK` for a transport failure; `TIME_BUDGET` when the run ran out of time before reaching a handle. Each carries a plain-English `error` and the `sourceHandle` it belongs to.

If you only want the data, filter on `charged == true`. The count of those rows always equals the number of events you were billed for, so the dataset is its own invoice.

### Ordering, and what "the first 200 followers" means

The list comes back in the order X itself keeps it, which for followers is newest first — the account that followed most recently is row one. So `maxItemsPerHandle: 200` gives you the 200 most recent followers, not a random 200 and not the oldest 200. That is usually what you want for audience tracking, and it is what makes a weekly re-run cheap: the new arrivals are all at the top.

The following list is ordered the same way — most recently followed first.

There is no way to jump to an arbitrary offset. If you need 2,000 accounts you pay for 2,000 rows; the run pages through them in order.

### Limitations

- A protected (private) account keeps its followers and following lists private. The run returns one uncharged row saying so and moves on — there is no public way to read them.
- The ceiling is 2,000 accounts per handle, 5,000 rows per run and 10 handles per run. For a very large audience, split the work across runs.
- The list is read newest-first and cannot be started from an arbitrary offset, so a deep pull of an account with millions of followers means paging through the rows in between.
- Follower and following counts are a snapshot at read time. On an active account they will differ from what you see if you look a minute later.
- `location` is free text that people fill in themselves. It is often a joke, a country, or empty, and it is not geocoded.
- A small number of accounts occasionally come back with only the handle and display name filled in — usually because the account was suspended or renamed between the list page and the lookup. Those rows carry `profileEnriched: false` so you can filter them out.
- Very rarely no source answers for a handle during a run. That returns an uncharged `BLOCKED` row rather than a partial one, with a `sessionState` field saying whether the fallback was still usable; re-running usually clears it.
- Email addresses and phone numbers are not returned. X does not publish them and this Actor does not guess them.
- Deleted, suspended and deactivated accounts simply do not appear in the list — X removes them from it, so there is nothing to report.

### Questions

**Do I need an X API key, a developer account or my own cookies?**

No. Nothing is asked of you beyond the handles, and the normal path uses no credentials at all.

**So what is `sessionCookies` for?**

It is an optional fallback. Follower lists come from public sources first; if none of them answers on a given run, the Actor reads the list from X with a session of its own rather than giving up on the handle. Putting your own `auth_token` and `ct0` in `sessionCookies` means that fallback runs on your account and your rate limit instead of a shared one — worth doing if you pull at volume, pointless otherwise. Leaving it empty is the normal case.

**Can I get the followers of a private account?**

No, and neither can anything else. X keeps a protected account's follower and following lists private. You get an uncharged row telling you the account is protected, and you are not billed for it.

**How do I get the accounts a handle follows instead of its followers?**

Set `mode` to `following`. The output columns are identical; the `relation` field on every row changes to `following` so mixed datasets stay unambiguous.

**What does a run cost?**

One start fee, plus one charge per account row delivered. Sample rows and diagnostic rows are free, so a run that finds nothing costs you only the start fee. There is a cost table in the Price section above.

**Can I run several handles at once?**

Yes, up to ten per run. Each handle gets its own `maxItemsPerHandle` budget, and each row carries `sourceHandle` so you can split the dataset afterwards.

**Will the run fail if something goes wrong?**

No. A blocked, empty or missing handle produces an uncharged diagnostic row explaining what happened and the run still finishes as succeeded. A failed run would still bill you the start fee, which would mean paying to be told something went wrong.

**Can I schedule it?**

Yes. Nothing is held between runs, so the same input is safe to repeat. Diff on `userId` rather than `userName` — handles change, ids do not.

**How fresh are the numbers in each row?**

They are read at the moment the row is built, directly from X. The `scrapedAt` field on every row records exactly when.

# Actor input Schema

## `handles` (type: `array`):

One or more X handles, with or without the leading @. Full profile URLs such as https://x.com/NASA work too. Up to 10 per run.

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

Followers returns the accounts that follow each handle. Following returns the accounts each handle follows. The output columns are the same either way.

## `maxItemsPerHandle` (type: `integer`):

How many accounts to return for each handle. The list is read newest-first, so 200 means the 200 most recent. Hard ceiling 2,000 per handle and 5,000 per run. Keep it low while testing - you pay per row.

## `enrichProfiles` (type: `boolean`):

On by default, and it is what fills in follower counts, following counts, location, verification, avatar and join date for every row. Turn it off only if all you need is the list of handles.

## `sessionCookies` (type: `array`):

Optional, and not needed for a normal run. Leave it empty: this Actor reads public data without any login. A cookie is only used as a fallback if the public route is refused, and to give you a rate limit nobody else draws on. In Chrome, logged in to x.com: F12 -> Application -> Cookies -> https://x.com, and copy auth\_token and ct0. Paste one account per line as 'auth\_token=XXXX; ct0=YYYY'. Stored as a secret and never logged.

## `proxyUrls` (type: `array`):

Leave this empty. By default the run rotates a large pool of addresses that cost you nothing per gigabyte. Fill it in only if you specifically want the traffic to leave through proxy servers you already pay for, in the form http://user:pass@host:port.

## Actor input object example

```json
{
  "handles": [
    "NASA",
    "github"
  ],
  "mode": "followers",
  "maxItemsPerHandle": 50,
  "enrichProfiles": true
}
```

# Actor output Schema

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

Every row in the default dataset: sourceHandle, relation, userName, name, url, description, followersCount, followingCount, tweetsCount, isVerified, verifiedType, location, profileImageUrl, createdAt, isProtected, website, profileBannerUrl, listedCount, mediaCount, favouritesCount, userId, profileEnriched. An empty, blocked or unmatched run returns a single uncharged row explaining what happened instead.

# 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 = {
    "handles": [
        "NASA",
        "github"
    ],
    "mode": "followers",
    "maxItemsPerHandle": 50,
    "enrichProfiles": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/twitter-followers-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 = {
    "handles": [
        "NASA",
        "github",
    ],
    "mode": "followers",
    "maxItemsPerHandle": 50,
    "enrichProfiles": True,
}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/twitter-followers-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 '{
  "handles": [
    "NASA",
    "github"
  ],
  "mode": "followers",
  "maxItemsPerHandle": 50,
  "enrichProfiles": true
}' |
apify call dami_studio/twitter-followers-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/twitter-followers-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/KRcQ8WTNWbT50vL4S/builds/qpY6hYr0GHv1uFGOQ/openapi.json
