# X (Twitter) Profile Scraper — Accounts, Activity & Growth (`foxlabs/x-profile-scraper`) Actor

One row per X account: followers, following, posts, bio, website, location, verification, account age — plus what X does not state: posting cadence, average and median engagement, top post, and the follower change since your last run. No login, no API key.

- **URL**: https://apify.com/foxlabs/x-profile-scraper.md
- **Developed by:** [Berkan Kaplan](https://apify.com/foxlabs) (community)
- **Categories:** Social media
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.00 / 1,000 account rows

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

## X (Twitter) Profile Scraper — Accounts, Activity & Growth 𝕏

**Paste X accounts; get one row each with everything X states about the account — and three things it does not: how often it posts, how much engagement it actually gets, and how its following changed since your last run.** No login, no API key, no browser: one read per account, charged per row.

- 👤 **The whole account record** — followers, following, posts, likes given, listed count, media count, bio, website, location, verification and its type, protected and default-image flags, profile and banner images, join date, **account age**, **lifetime posts per day**, **followers-to-following ratio**
- 📈 **Activity measured, not guessed** — posts per day on the embedded page, days since the last post, average and median engagement, highest engagement, **engagement per 1,000 followers**, the top post with its link, languages, the share of posts with media, links, hashtags and replies, and the accounts it mentions most
- 🔁 **Growth between runs** — the follower change, the change per day, the percentage change and the posts added since this account was last read. The first run of an account says so instead of writing zeros
- 🧾 **Honest about its limits** — the activity figures come from the one page X embeds (20–105 posts, measured 2026-10-19), not from the whole history, and every row carries `pageSpanDays` so you know what they were measured over
- 🤖 **Agent-ready** — MCP, and pay-per-event so an AI agent can run it and settle the bill itself

### What does this Actor do?

It reads X's own **embed (syndication) backend** — the service that renders embedded timelines on other people's websites. One read per account brings two things at once: the account object X states (38 fields) and the account's newest page of posts. The first becomes the row's identity and counts; the second becomes the activity figures. Nothing is logged in, no cookie jar is kept and no browser is started, which is why a run is fast and cheap and does not break when X changes its web app.

**Use it when you need:** an influencer or competitor shortlist with real engagement figures; a weekly follower-growth table; a check of which accounts in a list are still alive; firmographics for accounts you already know.

**Use something else when:** you need **follower lists**, **keyword search**, an account's **full posting history**, or **e-mail addresses** — X's embed backend serves none of those, so this Actor does not claim them. For a watchlist of *posts* rather than accounts, use **[X (Twitter) Account Monitor](https://apify.com/foxlabs/x-account-monitor)**; for tweets you already have the addresses of, **[X (Twitter) Tweet Details Scraper](https://apify.com/foxlabs/x-tweet-details-scraper)**.

### Quick start (API)

Three accounts, with activity and growth:

```bash
curl -X POST "https://api.apify.com/v2/acts/foxlabs~x-profile-scraper/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "accounts": ["@NASA", "x.com/nike", "@Microsoft"], "includeActivity": true, "trackGrowth": true }'
```

Prefer no code? Open the **Input** tab, paste your accounts one per line, and click **Start**.

### Getting started (3 steps)

1. **Paste the accounts.** A handle (`@nasa`), a profile address (`x.com/nasa`) or the account's numeric id (`11348282`) — all three work, mixed in one list.
2. **Leave activity and growth on.** They cost nothing extra: the activity figures come from the same single read, and growth only needs the counts from your previous run.
3. **Run it on a schedule** (daily or weekly) if you want the growth columns to fill. The first run is the baseline; every run after it states what changed.

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `accounts` | array | — | The accounts to read: `@handle`, `x.com/handle` or numeric id. **Required.** |
| `includeActivity` | boolean | `true` | Measures the account's newest embedded page: cadence, engagement, top post, languages, content mix, most-mentioned accounts. |
| `trackGrowth` | boolean | `true` | States the change since this account was last read by a run of yours. |
| `historyStore` | string | `x-profile-history` | The key-value store that holds the counts last seen per account. |
| `minFollowers` | integer | `0` | Accounts under this are not delivered and not charged. |
| `onlyVerified` | boolean | `false` | Only accounts X marks as verified. |
| `activeWithinDays` | integer | `0` | Only accounts whose newest embedded post is within N days (0 = no limit). |
| `maxAccounts` | integer | `0` | Cost cap for the whole run (0 = every account). |
| `concurrency` | integer | `3` | Accounts read side by side. X refuses bursts (measured: eight at a time lost half a ten-account run, three at a time delivered all ten), and anything it still refuses is read again one at a time at the end of the run. |
| `rememberAccountIds` | boolean | `true` | Keeps each handle's numeric id after the first read, so later runs take the cheap road. |
| `accountIdStore` | string | `x-profile-account-ids` | The store that holds the handle → id pairs. It belongs to this Actor: a store another Actor created cannot be opened from here. |
| `proxyConfiguration` | object | Apify Proxy (datacenter) | Datacenter addresses answer every read made by numeric id. Handle lookups go through residential addresses, because a handle lookup from a datacenter address is rate-limited. |

### Output

One row per account. Download as JSON, CSV, Excel or read it from the API.

```json
{
  "type": "profile",
  "id": "11348282",
  "handle": "NASA",
  "url": "https://x.com/NASA",
  "name": "NASA",
  "bio": "Making the seemingly impossible, possible. ✨",
  "website": "http://www.nasa.gov/",
  "followers": 92320972,
  "following": 115,
  "tweets": 74438,
  "likesGiven": 17032,
  "listedIn": 96866,
  "mediaPosted": 28201,
  "verified": true,
  "verifiedType": "Government",
  "createdAt": "2007-12-19T20:20:32.000Z",
  "accountAgeDays": 6879,
  "tweetsPerDay": 10.82,
  "followersPerFollowing": 802791.06,
  "postsPerDay": 3.9,
  "lastPostAt": "2026-10-16T19:42:20.000Z",
  "daysSinceLastPost": 2.7,
  "avgEngagement": 2914.8,
  "avgEngagementPer1kFollowers": 0.032,
  "topPostUrl": "https://x.com/NASA/status/2109320622882873464",
  "activity": {
    "postsOnPage": 20,
    "pageSpanDays": 5.1,
    "postsLast7Days": 17,
    "medianEngagement": 1480,
    "maxEngagement": 25678,
    "languages": ["en"],
    "withMediaPercent": 45,
    "withLinkPercent": 55,
    "mentionsMost": ["Space_Station", "NASAJohnson", "NASAArtemis"]
  },
  "growth": {
    "previousReadAt": "2026-10-12T09:00:00.000Z",
    "daysSincePreviousRead": 7.1,
    "followersChange": 18422,
    "followersChangePerDay": 2594.6,
    "followersChangePercent": 0.02,
    "postsChange": 27
  },
  "scrapedAt": "2026-10-19T12:24:43.230Z"
}
```

#### Account fields

| Field | Type | Description |
|---|---|---|
| `id` / `handle` / `url` | string | Numeric account id, the handle, and the profile address |
| `name` / `bio` / `location` / `website` | string | As X states them; `null` when X states none (`location` is empty on most brand accounts) |
| `followers` / `following` / `tweets` | integer | The counts X states |
| `likesGiven` / `listedIn` / `mediaPosted` | integer | Likes the account gave, lists it appears on, media items it posted |
| `verified` / `verifiedType` / `protected` | — | Verification and its kind (`Business`, `Government`, `Blue`…) |
| `profileImage` / `bannerImage` | string | Full-size image addresses on `pbs.twimg.com` |
| `createdAt` / `accountAgeDays` | — | Join date and the account's age in days |
| `tweetsPerDay` | number | `tweets / accountAgeDays` — the lifetime average |
| `followersPerFollowing` | number | The ratio that separates a broadcaster from a follow-back account |
| `withheldInCountries` | array | Countries X says the account is withheld in |

#### Activity fields (`includeActivity`, also flattened onto the row)

`postsOnPage`, `newestPostAt`, `oldestPostAt`, **`pageSpanDays`**, `daysSinceLastPost`, **`postsPerDayOnPage`**, `postsLast7Days`, `postsLast30Days`, `totalEngagementOnPage`, **`avgEngagement`**, `medianEngagement`, `maxEngagement`, **`avgEngagementPer1kFollowers`**, `topPostId`, `topPostUrl`, `topPostText`, `topPostEngagement`, `topPostAt`, `languages`, `withMediaPercent`, `replyPercent`, `withLinkPercent`, `withHashtagPercent`, `mentionsMost`, `hashtagsMost`.

The row also carries `postsPerDay`, `lastPostAt`, `daysSinceLastPost`, `avgEngagement`, `avgEngagementPer1kFollowers` and `topPostUrl` as plain columns, so a CSV export can be sorted without opening the nested object.

#### Growth fields (`trackGrowth`)

`previousReadAt`, `daysSincePreviousRead`, **`followersChange`**, `followersChangePerDay`, `followersChangePercent`, `followingChange`, **`postsChange`**, `postsPerDaySincePreviousRead`, `listedInChange`, `likesGivenChange` — plus `followersChange` and `followersChangePerDay` as plain columns. `growth` is `null` the first time an account is read: there is nothing to compare against, and a zero would be a lie.

A line that cannot be delivered is written as a free `type: "status"` row with the reason — not an X account, no public record on X, an account the run could not read, or an account an earlier line already named. **Status rows are never charged.**

### Use cases

- **Influencer shortlist with real numbers.** Followers are easy to buy; `avgEngagementPer1kFollowers` and `postsPerDayOnPage` are not. One run turns a list of handles into a comparable table.
- **Weekly growth report.** Schedule it; every run states the follower change and the posts added since the last one.
- **Dead-account sweep.** `activeWithinDays` plus `daysSinceLastPost` separates live accounts from abandoned ones.
- **Competitor and portfolio monitoring.** Verification, listed count, bio and website changes are all in the row.
- **Feeding a dashboard or an LLM.** Flat rows, stable field names, ISO timestamps, `null` where X said nothing.

### Integration

**MCP / AI agents:** ask for this Actor by name — `foxlabs/x-profile-scraper` — on the Apify MCP server. It is pay-per-event, so an agent can run it and settle the bill per row.

**JavaScript**

```javascript
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('foxlabs/x-profile-scraper').call({
    accounts: ['@NASA', '@nike'],
    includeActivity: true,
    trackGrowth: true,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.map((row) => `${row.handle}: ${row.followers} (${row.followersChange ?? 'first read'})`));
```

**Python**

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("foxlabs/x-profile-scraper").call(run_input={
    "accounts": ["@NASA", "@nike"],
    "includeActivity": True,
})
for row in client.dataset(run.default_dataset_id).iterate_items():
    if row["type"] == "profile":
        print(row["handle"], row["followers"], row.get("avgEngagementPer1kFollowers"))
```

Written for `apify-client` 3 for Python, where `call()` returns a run object; with version 1 or 2 read `run["defaultDatasetId"]`.

**Automation (n8n / Zapier / Make):** schedule → HTTP request to the Actor API with your account list → handle the JSON dataset → push to a sheet, CRM or dashboard.

### Pricing

Pay-per-event: **$1 per 1,000 account rows** (~$0.001 each). Status rows and accounts left out by a filter are **free** — you pay for rows you receive. The activity figures and the growth columns cost nothing extra: they come from the same read and from your own stored history.

| What you ask for | Rows | Approx. cost |
|---|---|---|
| 500 influencer accounts, once | 500 | ~$0.50 |
| 1,000 accounts, weekly growth table | 4,000/month | ~$4/month |
| 50 competitor accounts, daily | 1,500/month | ~$1.50/month |

You also pay Apify for the compute the run uses, which is small: one HTTP read per account, no browser. Apify's **free tier** includes $5/month of credits.

### Limits — what this Actor cannot do

These are not omissions; they are what X's embed backend serves (measured 2026-10-19):

- **The activity figures come from one page.** X embeds an account's newest 20–105 posts and offers no paging, so cadence and engagement are measured over `pageSpanDays` — 5 days for a busy account, months for a quiet one. The field travels with the row for exactly that reason.
- **No follower lists.** The counts yes, the lists no.
- **No e-mail or phone.** X states neither on an account; `website` is whatever the account links to.
- **`location` is often empty.** It was empty on all three brand accounts measured; it stays `null` rather than guessed.
- **Growth needs a previous run.** The first read of an account has nothing to compare against and says so.
- **Protected accounts** are not readable, and an account X has no public record of comes back as a free status row.
- **The rate budget is shared.** X's embed backend limits how fast one account may read, across all of its runs — two runs of this family started at the same time will refuse each other's reads (measured 2026-10-19). Run them one after another, and leave the speed setting at its default; a read X refuses is tried again at the end of the run, one at a time.

### FAQ

**Do I need an X account, login or API key?** No. This reads X's public embed service.

**How fresh is the data?** Every run reads live; `scrapedAt` says when.

**Why is `postsPerDay` different from `postsPerDayOnPage`?** The first is the lifetime average (`tweets / accountAgeDays`); the second is the cadence on the page just read. A dormant account with a long history shows a high lifetime figure and a low current one — that difference is the signal.

**Where is the growth history kept?** In a key-value store of **your** Apify account (`x-profile-history` by default). It is not shared with us or anyone else, and you can use separate stores for separate watchlists. A store that another Actor created cannot be opened from here (Apify scopes a run's permissions to its own Actor); if the name you give cannot be opened, the run still delivers every row and says so in its message.

**What does a handle cost extra?** One lookup on a residential address the first time (a handle lookup from a datacenter address is rate-limited by X). The id is then remembered in your own key-value store and later runs take the cheap road. Give numeric ids and no lookup is ever needed.

**Can I read 1,000 accounts in one run?** Yes — one read per account, eight at a time by default.

### Troubleshooting

| What you see | What it means |
|---|---|
| `X has no public account record for @…` | No account with that name, or it is suspended, protected or has nothing embedded. Free row. |
| `… is the same account as an earlier line of this input` | A handle and its numeric id (or the same line twice) were both given. Delivered once, charged once. Free row. |
| `X's embed backend is rate-limiting handle lookups from this address` | The residential lookup for that handle did not get through. Run again, or give the account's numeric id. |
| `… accounts read for the first time: the next run will say what changed` | Growth tracking working: this run is the baseline. |
| Run ends FAILED with `did not answer the opening check` | X did not answer us at all. Nothing was read or charged; try again. |

### Is it legal to scrape this data?

This Actor reads a public embed service with no login and collects what X itself publishes on public profiles. It stores no private data and no credentials. Always check your own use against X's Terms of Service and the data-protection rules that apply to you, especially if you keep personal data or use it for monitoring individuals.

### Support & contact

Found a defect or need a field that is missing? Open an issue on the Actor's **Issues** tab with the run id — every run keeps a `SOURCE_REPORT` record with what it saw, read, delivered and left out, which makes a report quick to answer.

### Changelog

#### 0.1 — 2026-10-19

- First release. Accounts by handle, profile address or numeric id; the 38 fields X states plus account age, lifetime posts per day and the followers-to-following ratio; activity figures measured on the newest embedded page; growth between runs from your own key-value store; filters for followers, verification and activity; handle → id remembered between runs; free status rows for every line that cannot be delivered, including an account named twice in one input.

***

Part of the **[foXLabs data platform](https://data.foxlabs.com.tr/)** — public-data company, contact, social, procurement and AI-search intelligence scrapers.

# Changelog

This Actor's version history is a separate document: https://apify.com/foxlabs/x-profile-scraper/changelog.md

# Actor input Schema

## `accounts` (type: `array`):

One account per line: a handle (@nasa), a profile address (x.com/nasa) or the account's numeric id (11348282). A numeric id is the cheapest to read; a handle is looked up once and then remembered as its id.

## `includeActivity` (type: `boolean`):

Adds what X does not state, computed from the page of newest posts the same read already brings: posts per day, the days the page spans, days since the last post, average and median engagement, engagement per 1,000 followers, the top post, the languages it posts in, the share of posts with media, links, hashtags and replies, and the accounts it mentions most.

## `trackGrowth` (type: `boolean`):

Keeps each account's counts in a key-value store of your own account, so the next run can state the follower change, the change per day, the percentage change, and how many posts were added since. The first run of an account says so instead of writing zeros.

## `historyStore` (type: `string`):

Name of the key-value store that holds the counts last seen per account. Use separate stores for separate watchlists if you want their histories kept apart.

## `minFollowers` (type: `integer`):

An account under this is not delivered and not charged — useful when a list mixes real accounts with empty ones.

## `onlyVerified` (type: `boolean`):

Leaves out accounts X does not mark as verified. Not delivered means not charged.

## `activeWithinDays` (type: `integer`):

0 means no limit. Measured on the newest post X embeds for the account; an account with nothing embedded is reported as a free status row.

## `maxAccounts` (type: `integer`):

A cost cap. 0 means every account in the list is read.

## `concurrency` (type: `integer`):

How many accounts are read side by side. Measured on the platform 2026-10-19: ten accounts read eight at a time met X's rate limit and half of them came back empty, while three at a time delivered all ten. Three is therefore the default; raising it makes a run faster and more likely to need the second pass.

## `rememberAccountIds` (type: `boolean`):

After an account named by handle has been read once, its numeric id is kept in a key-value store of your account, and later runs read it the cheap way. Turn off to look every handle up again.

## `accountIdStore` (type: `string`):

Name of the key-value store that holds the handle → id pairs. It belongs to this Actor: a store another Actor created cannot be opened from here, and if the name you give cannot be opened the run still delivers its rows and says so.

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

Left as it is, the Actor uses Apify Proxy datacenter addresses, which X's embed backend answers for everything read by numeric id. Accounts named by handle are looked up through residential addresses, because a handle lookup from a datacenter address is rate-limited.

## Actor input object example

```json
{
  "accounts": [
    "@NASA",
    "x.com/nike",
    "@Microsoft"
  ],
  "includeActivity": true,
  "trackGrowth": true,
  "historyStore": "x-profile-history",
  "minFollowers": 0,
  "onlyVerified": false,
  "activeWithinDays": 0,
  "maxAccounts": 0,
  "concurrency": 3,
  "rememberAccountIds": true,
  "accountIdStore": "x-profile-account-ids",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

# API

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

## JavaScript example

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

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

// Prepare Actor input
const input = {
    "accounts": [
        "@NASA",
        "x.com/nike",
        "@Microsoft"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("foxlabs/x-profile-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 = {
    "accounts": [
        "@NASA",
        "x.com/nike",
        "@Microsoft",
    ],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("foxlabs/x-profile-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 '{
  "accounts": [
    "@NASA",
    "x.com/nike",
    "@Microsoft"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call foxlabs/x-profile-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,foxlabs/x-profile-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/fryS1prY7TR4rv0pS/builds/bYtekY18zmjRCNl2y/openapi.json
