# X (Twitter) Account Monitor — Posts & Engagement (`foxlabs/x-account-monitor`) Actor

Watch a list of X accounts: each one's newest posts with likes, reposts, replies and quotes, the engagement per 1,000 followers, and the account's own row (followers, posts per day, age). Only-new mode pays for what appeared since the last run. No login, no API key.

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

## Pricing

from $0.06 / 1,000 posts

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) Account Monitor — Posts, Engagement & Profiles 𝕏

**Give it a list of X accounts; get their newest posts with the numbers that matter — likes, reposts, replies, quotes — plus each account's own row.** Built for watching accounts over time: competitors, clients, portfolio companies, journalists, your own brand. No login, no API key, no browser: one read per account, charged per row.

- 📊 **The four engagement counts on every post** — likes, reposts, replies, quotes — plus what we compute from them: **engagement total**, **engagement per 1,000 followers** (comparable across accounts of different sizes) and **engagement per hour** since the post went up
- 👤 **The account itself** — followers, following, posts, listed count, bio, website, location, verification, account age, **posts per day**, **followers-to-following ratio**
- 🔁 **Only-new mode** — keeps a list of what it has already delivered, so an hourly watchlist pays only for posts that appeared since the last run
- 🧾 **Honest about its limits** — X's embed backend hands out an account's **newest page only** (23–105 posts, no paging) and no keyword search. This Actor says so in the run's message instead of pretending otherwise
- 🤖 **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 and tweets on other people's websites. For each account you name it fetches that account's newest page of posts, turns every post into a flat row, and adds one row for the account itself. Nothing is logged in, no cookie jar is kept, and no browser is started, which is why a run is fast and cheap and why it does not break when X changes its web app.

**Use it when you need:** a watchlist of accounts checked on a schedule; engagement benchmarking across accounts; a client or competitor report; the current numbers on posts you already know.

**Use something else when:** you need keyword or hashtag **search**, **follower lists**, **reply texts**, or an account's **history beyond its newest page** — X's embed backend does not serve those, so this Actor does not claim them. The Input tab says the same thing where it matters.

### Quick start (API)

Watch three accounts, five newest posts each:

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

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. **Pick how many posts per account** (default 25). X hands out one page per account, so anything above ~105 simply takes everything that page holds.
3. **Turn on "Only posts new since the last run"** if this is a watchlist you will run on a schedule. The first run delivers the page; later runs deliver only what appeared since.

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `accounts` | array | — | The accounts to read: `@handle`, `x.com/handle` or numeric id. **Required.** |
| `maxPostsPerAccount` | integer | 25 | Newest posts per account (1–110). With only-new mode on, this counts the posts **looked at**, so the run never pages backwards into history. |
| `includeProfileRow` | boolean | `true` | One extra row per account with the account's own fields. |
| `onlyNewPosts` | boolean | `false` | Deliver only posts not delivered before (keeps a list in your own key-value store). |
| `listName` | string | `default` | Which list only-new mode uses. One list per watchlist. |
| `skipReplies` | boolean | `false` | Leave out posts that start with a mention. |
| `postedWithinDays` | integer | `0` | Only posts from the last N days (0 = no limit). |
| `minEngagement` | integer | `0` | Only posts with at least this many likes + reposts + replies + quotes. |
| `maxPosts` | integer | `0` | Cost cap across all accounts (0 = only the per-account limit). |
| `concurrency` | integer | `8` | Accounts read side by side. |
| `rememberAccountIds` | boolean | `true` | Keeps each handle's numeric id after the first read, so later runs take the cheap road. |
| `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 post, one row per account. Download as JSON, CSV, Excel or read it from the API.

```json
{
  "type": "tweet",
  "id": "2109382322516799677",
  "url": "https://x.com/Nike/status/2109382322516799677",
  "text": "Zheng Qinwen claims her first WTA 1000 title in Beijing. A winner's name, etched on home soil.",
  "createdAt": "2026-10-11T20:35:10.000Z",
  "language": "en",
  "likes": 6398,
  "reposts": 527,
  "replies": 92,
  "quotes": 63,
  "engagement": 7080,
  "engagementPer1kFollowers": 0.745,
  "engagementPerHour": 42.24,
  "ageHours": 167.6,
  "isReply": false,
  "media": [{ "type": "photo", "url": "https://pbs.twimg.com/media/HUYHTdWXQAAFWCq.jpg", "width": 2160, "height": 2700 }],
  "mediaTypes": ["photo"],
  "hashtags": [],
  "mentions": [],
  "links": [],
  "authorHandle": "Nike",
  "authorFollowers": 9504725,
  "authorVerified": true,
  "scrapedAt": "2026-10-18T20:13:06.871Z"
}
```

#### Post fields

| Field | Type | Description |
|---|---|---|
| `id` / `url` | string | Post id and its address on x.com |
| `text` | string | The post's text as X states it |
| `createdAt` | string | ISO 8601, UTC |
| `likes` / `reposts` / `replies` / `quotes` | integer | The four counts X states on an embedded post |
| `engagement` | integer | The four counts added up |
| `engagementPer1kFollowers` | number | `engagement / followers × 1,000` — the figure that compares a small account with a large one |
| `engagementPerHour` | number | `engagement / hours since posted` — how fast it is travelling |
| `ageHours` | number | Hours since it went up |
| `isReply` | boolean | The post starts with a mention |
| `conversationId` | string | The thread it belongs to |
| `language` | string | Language code X assigned |
| `media[]` / `mediaCount` / `mediaTypes` | array / integer / array | Photos and videos: type, direct URL, width, height |
| `hashtags[]` / `mentions[]` / `links[]` | array | As X states them (links are the expanded addresses) |
| `sensitive` / `edited` | boolean | Flags X states; `null` when it states none |
| `authorHandle` / `authorName` / `authorId` / `authorFollowers` / `authorVerified` | — | The author, flattened onto the row so an export needs no join |
| `scrapedAt` | string | When this row was read |

#### Account fields (`type: "profile"`)

`handle`, `name`, `bio`, `location`, `website`, `followers`, `following`, `tweets`, `likesGiven`, `listedIn`, `mediaPosted`, `verified`, `verifiedType`, `protected`, `profileImage`, `bannerImage`, `createdAt`, **`accountAgeDays`**, **`tweetsPerDay`**, **`followersPerFollowing`**, `withheldInCountries`.

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

### Use cases

- **Competitive watch.** A list of competitor accounts, run hourly with only-new mode: you pay for new posts and see their engagement next to yours.
- **Engagement benchmarking.** `engagementPer1kFollowers` makes a 9,000-follower account and a 90-million-follower account comparable in the same table.
- **Client and campaign reporting.** Post text, time, media and the four counts — the columns a report needs, in CSV.
- **Feeding a dashboard or an LLM.** Flat rows, stable field names, ISO timestamps.
- **Keeping an eye on your own brand.** Your account plus the accounts that matter to you, on a schedule.

### Integration

**MCP / AI agents:** ask for this Actor by name — `foxlabs/x-account-monitor` — 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-account-monitor').call({
    accounts: ['@NASA', '@nike'],
    maxPostsPerAccount: 10,
    onlyNewPosts: true,
    listName: 'brands',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.filter((row) => row.type === 'tweet').map((row) => `${row.authorHandle}: ${row.engagement}`));
```

**Python**

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("foxlabs/x-account-monitor").call(run_input={
    "accounts": ["@NASA", "@nike"],
    "maxPostsPerAccount": 10,
})
for row in client.dataset(run.default_dataset_id).iterate_items():
    if row["type"] == "tweet":
        print(row["authorHandle"], row["likes"], row["url"])
```

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 or webhook → 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: **$0.06 per 1,000 delivered posts** (~$0.00006 each) and **$1 per 1,000 account rows** (~$0.001 each). Status rows and posts left out by a filter are **free** — you pay for rows you receive.

| What you ask for | Rows | Approx. cost |
|---|---|---|
| 100 accounts × 25 newest posts, with account rows | 2,500 posts + 100 accounts | ~$0.25 |
| 1,000 accounts watched hourly, only-new mode, ~2 new posts each per hour | ~2,000 posts/hour | ~$0.12/hour |
| One account, 100 newest posts | 100 posts | ~$0.006 |

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-18):

- **No paging.** An account's newest page only: 23–105 posts depending on the account. `maxId`, `cursor` and friends return the same page, so there is no way to walk history.
- **No search.** No keyword, hashtag, list or moment timelines.
- **No reply texts.** A post's reply *count* yes, the replies themselves no.
- **No follower lists.**
- **No view counts.** X does not state the view count on an embedded post.
- **Protected accounts** are not readable, and an account X has no public record of comes back as a free status row.

If you need search or history, use an Actor built for it — ours is built to watch accounts cheaply and reliably.

### 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 do I get 23 posts for one account and 105 for another?** That is how many X embeds for that account. The cap you set is an upper bound, not a promise.

**Can I watch 1,000 accounts?** Yes — one read per account, eight at a time by default. Use only-new mode so you pay for new posts only.

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

**Why is a post missing?** It is older than the newest page, it was left out by a filter, or it is already in your only-new list.

### Troubleshooting

| What you see | What it means |
|---|---|
| `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. |
| `X embedded nothing for @…` | No public account with that name, or it is suspended or protected. Free row. |
| `… was read but X embedded no post for it` | The account exists and has nothing embeddable (only replies, or protected posts). Free row. |
| `Nothing new: … already in the list` | Only-new mode working: no new posts since the last run. Nothing charged. |
| 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-18

- First release. Accounts by handle, profile address or numeric id; the newest page of posts with likes, reposts, replies, quotes and the engagement figures we compute; one row per account; only-new mode with its own list; filters for replies, age and engagement; handle → id remembered between runs; free status rows for every line that cannot be delivered.

***

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-account-monitor/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.

## `maxPostsPerAccount` (type: `integer`):

How many of each account's newest posts to deliver. X's embed backend hands out one page per account — between 23 and 105 posts, depending on the account — and there is no way to page further back, so a higher number simply takes everything that page holds.

## `includeProfileRow` (type: `boolean`):

One extra row per account with its name, bio, location, website, followers, following, posts, listed count, verification, account age, posts per day and followers-to-following ratio.

## `onlyNewPosts` (type: `boolean`):

Keeps a list of the posts already delivered, so a watchlist that runs every hour pays only for what appeared since last time. The list lives in a key-value store of your account and is not shared with anyone.

## `listName` (type: `string`):

Which list "only new posts" uses. Separate lists for separate watchlists; runs of the same list wait for each other.

## `skipReplies` (type: `boolean`):

Skips posts that start with a mention (a reply in someone else's thread), so only the account's own posts are delivered.

## `postedWithinDays` (type: `integer`):

0 means no time limit. A post older than this is not delivered and not charged.

## `minEngagement` (type: `integer`):

Likes + reposts + replies + quotes. A post under this is not delivered and not charged — useful for watching only what actually travelled.

## `maxPosts` (type: `integer`):

A cost cap across all accounts. 0 means only the per-account limit applies.

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

How many accounts are read side by side. Higher is faster; lower is gentler on the source.

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

## `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",
    "@elonmusk"
  ],
  "maxPostsPerAccount": 25,
  "includeProfileRow": true,
  "onlyNewPosts": false,
  "listName": "default",
  "skipReplies": false,
  "postedWithinDays": 0,
  "minEngagement": 0,
  "maxPosts": 0,
  "concurrency": 8,
  "rememberAccountIds": true,
  "accountIdStore": "x-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",
        "@elonmusk"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("foxlabs/x-account-monitor").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",
        "@elonmusk",
    ],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("foxlabs/x-account-monitor").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",
    "@elonmusk"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call foxlabs/x-account-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,foxlabs/x-account-monitor"
        }
    }
}
```

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/uAwRgoiMYDgwgJu2e/builds/6P9q4A76RGgeqyHRn/openapi.json
