# W Social Scraper (`sashaebashu/wsocial-scraper`) Actor

Read any account on W Social, the European AT-Protocol network: whether the network verified them as a human and how, their posts with every engagement count, follower movement, hashtags, follow graph and livestreams — plus alerts when a verification changes or an account goes quiet.

- **URL**: https://apify.com/sashaebashu/wsocial-scraper.md
- **Developed by:** [Sasha Ebashu](https://apify.com/sashaebashu) (community)
- **Categories:** Social media, News, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $15.00 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#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

## W Social Scraper — Verified Accounts, Posts & Engagement

W Social is the European social network built on the AT Protocol, where every account carries the
network's own answer to the question everyone else guesses at: **is this a human?** This Actor reads
any public account — its verification, its posts with every engagement count, who it follows,
its livestreams — and reads individual posts by link.

Public API. No key, no login, no proxy needed.

### Paste a post link, get its numbers

Drop `https://wsocial.eu/profile/anna.wsocial.eu/post/3mtnzyolvcs2i` into **Post links** and get back
likes, replies, reposts, quotes, bookmarks, the engagement rate against the author's real follower
count, what the post replies to, and — if you ask for it — the whole reply thread with each reply's
own engagement and how deep it sits.

One post from the sample: **115 likes, 17 replies, 13 reposts, 2 quotes, 5 bookmarks**, a 4.65%
engagement rate, and 23 replies read down four levels.

### The field that exists here and nowhere else

Every account carries `accountType` — `human`, `organization`, `bot`, `service` or `unverified` —
and this is the network's own classification, not an inference. Across 576 accounts sampled: **385
human, 144 unverified, 35 organization, 8 bot, 2 service**.

Two honest notes about it. The verification method (`wid` for people, `admin` for everything the
staff approved) is **not an independent signal** — it follows from the type exactly, in all 576
cases. And an account with *no* type is not an unverified one: W Social federates with the wider AT
Protocol network, so a followed account may live on Bluesky, where this field does not exist. Those
are flagged `isFederated` rather than being quietly counted as unverified.

### What you get per account

| | |
|---|---|
| **Identity** | Handle, DID, display name, bio, avatar, banner, join date, custom domain, labels |
| **Verification** | Account type, verification route, human flag, federated flag |
| **Audience** | Followers, following, follower ratio |
| **Output** | Posts, originals, replies and reposts counted apart; posts in the last 7/30/90 days; dormancy |
| **Engagement** | Total and average likes, reposts and replies; engagement per post; engagement rate against followers; the top post |
| **Content** | Hashtags ranked by use, languages ranked by use, outbound links |
| **Graph** | Who the account follows — resolved into real handles with their follower counts and verification, plus how many of them are humans |
| **Livestreams** | The account's stream records, a W Social feature standard AT Protocol clients do not carry |

### Discovery: read the whole network

The server publishes its own account list, so this Actor can find accounts instead of only reading
the ones you name. Measured: **27,981 accounts enumerated in 7.5 seconds**, then filtered by minimum
followers, minimum posts, account type, verification, or "posted within N days".

Filters run in cost order — followers and post counts are checked on the profile before any feed is
read, and "posted within" is answered from the first page, so an account that fails it costs one
request instead of a full walk.

### Monitor mode

| `changeType` | Means |
|---|---|
| **`verification_change`** | The account's type or verification moved — on a network whose whole proposition is verified humans, this is the event worth waking up for |
| **`handle_change`** | They changed the domain they post under, with the old and new value |
| `new_posts` | They published since the last run, with the posts themselves |
| `profile_change` | Display name or bio moved |
| `went_quiet` | The run where they crossed your dormancy threshold — announced once, not repeated |
| `followers_up` / `followers_down` | Audience moved, with `followersDelta` |
| **`suspended`** | The network has taken the account down — reported as its own event, not folded into "gone", because who removed it is the whole story |
| **`deactivated`** | The owner switched their own account off |
| **`no_longer_available`** | The account is gone for some other reason — deleted or renamed |
| `new`, `unchanged` | First sighting / nothing moved |

Those three are told apart by what the network actually answers — `AccountTakedown`, `AccountDeactivated`, or nothing at all — enumerated across 1,000 live accounts rather than guessed at. In that sample 21 accounts were suspended and 5 deactivated.

**History is tracked by DID, not by handle.** On this network a handle is a domain name and can
change; keyed by handle, a rename would read as one account vanishing and another appearing.

An account that 404s but was **never** in your baseline is a typo, and is reported as one rather
than as news.

### Notes & limits

Properties of the source, measured rather than assumed:

- **The author feed carries replies and reposts as well as original posts**, which is why it returns
  more items than the profile's own post counter — 1,014 items for an account reporting 973. Those
  973 are matched exactly once reposts are excluded, and reposts are never averaged into the
  account's own engagement, because they are somebody else's numbers.
- **A repost carries the original post's date, not the date it was reposted.** Counting reposts into
  a "posts this month" window would therefore date them wrongly, so activity windows use original
  posts only.
- **A window the read did not cover is left empty.** On a prolific account 100 posts can span three
  days; reporting that as the monthly total would under-count them and look like a slowdown that
  never happened. `oldestPostRead` says exactly how far back the read got, and `firstPostAt` is
  filled in only when the walk actually reached the account's first post.
- **Every 400 from this network is named `InvalidRequest`** — a missing account and a malformed
  request are the same error code, and only the message tells them apart. This Actor reads the
  message, so a bug in a request is reported as a bug rather than as "that account does not exist".
- **The server's own account list is not to be trusted about who is inactive.** Of 56 repositories it
  marked inactive, all 16 taken-down ones really were gone — but **13 of the 40 marked "deactivated"
  served a full profile anyway**. Only the taken-down flag is believed here; everything else is
  asked about directly, because skipping them all silently dropped live accounts.
- **`handle.invalid` is not a handle.** It is what the protocol shows when an account's domain stops
  proving ownership, several unrelated accounts carry it at once, and a link built from it goes
  nowhere. Those accounts are flagged `handleInvalid` and linked by DID instead.
- **The follow graph is closed on the app API and open on the repository.** Followers, search, likes
  and the timeline all answer `AuthMissing`; follows, posts and livestreams are readable straight
  from the account's own repository, which is where this Actor reads them.
- **Follow records name only a DID.** Handing over `did:plc:wn2b3igg…` would be handing over
  something unusable, so each is resolved back into a handle with its follower count and
  verification — 25 per request, the documented ceiling.
- **`replies` and `repliesRead` are different numbers on purpose.** The first is the platform's count
  of direct replies; the second is how many were actually read, including nested ones deeper in the
  thread.
- **No rate limiting observed.** 80 accounts with full post lists finished in 28 seconds; a full walk
  of one account's 1,014 posts took 23 seconds.
- The Actor honours your **Maximum cost per run** and stops when the cap is reached.

### Input

| Field | What it does |
|---|---|
| **Accounts** | Handles, DIDs or profile URLs — `ingmar.wsocial.eu`, `did:plc:…`, or a full link. Institutions use their own domain. |
| **Post links / Include the reply thread** | Metrics for individual posts, optionally with the replies underneath. |
| **Discover accounts / Accounts to scan** | Read the network instead of naming anyone. |
| **Minimum followers / posts, Account types, Only verified, Posted within** | Filters. They narrow a named list too, not only discovery. |
| **Read posts / Posts per account** | How far back to read. This is what the averages and windows are built from. |
| **Include the individual posts** | Adds every post read rather than just the totals. |
| **Include the follow graph / Follows per account** | Who they follow, resolved into handles. |
| **Include livestreams** | Their stream records. |
| **Treat as dormant after** | Your definition of quiet, not a hidden constant. |
| **Monitor mode / Output only changes** | The alert behaviour above. |

# Actor input Schema

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

W Social handles, DIDs or profile URLs. Paste `ingmar.wsocial.eu`, `https://wsocial.eu/profile/anna.wsocial.eu`, or a `did:plc:...`. Institutional accounts use their own domain, like `vonderleyen.ec.europa.eu`.

## `posts` (type: `array`):

Paste links to individual posts to get their metrics — likes, replies, reposts, quotes, bookmarks, the author and what the post replies to. `https://wsocial.eu/profile/anna.wsocial.eu/post/3mtnzyolvcs2i` or an `at://` URI both work. Can be used on its own or alongside accounts.

## `includeReplies` (type: `boolean`):

For each post link, also read the replies underneath it — author, text, date and their own engagement, down through nested replies.

## `maxRepliesPerPost` (type: `integer`):

How many replies to read per post, across the whole thread.

## `discoverAccounts` (type: `boolean`):

Read the whole network instead of naming accounts yourself. The server publishes its own account list, so this walks it and keeps whoever matches the filters below.

## `maxAccountsToScan` (type: `integer`):

How many accounts discovery may check. Checking one costs a single request, and most will not match your filters — so scan far more than you expect to keep.

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

Keep only accounts with at least this many followers. Checked on the profile, before any posts are read.

## `minPosts` (type: `integer`):

Keep only accounts that have published at least this many posts.

## `accountTypes` (type: `array`):

Keep only these types: `human`, `unverified`, `organization`, `bot`, `service`. Leave empty for any. This is the network's own classification, not a guess.

## `verifiedOnly` (type: `boolean`):

Keep only accounts that carry a verification.

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

Keep only accounts that posted this recently. Answered from the first page of the feed, so an account that fails it costs one request rather than a full walk. 0 turns it off.

## `includePosts` (type: `boolean`):

Reads the account's feed for engagement, cadence, hashtags and languages. This is what the averages and the activity windows are built from.

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

How far back to read, 100 per request. Set it high enough to cover the period you care about — the feed reaches back to the account's first post.

## `includePostList` (type: `boolean`):

Adds every post read to the output — text, link, timestamp, each engagement count, hashtags and outbound links. Off by default because it makes rows large.

## `includeFollows` (type: `boolean`):

Reads who this account follows, straight from its repository, and resolves each one into a real handle with its follower count and verification. 100 follows per request plus one lookup per 25.

## `maxFollowsPerAccount` (type: `integer`):

How many follows to read per account.

## `includeLivestreams` (type: `boolean`):

Reads the account's livestream records — a W Social feature that standard AT-Protocol clients do not carry. One extra request per account.

## `dormantAfterDays` (type: `integer`):

Days since the last post before an account counts as dormant. Drives `isDormant` and the `went_quiet` alert.

## `maxItems` (type: `integer`):

Stop after this many results.

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

4 is comfortable. Raising it is safe when reading many accounts.

## `monitorMode` (type: `boolean`):

Compare against the last run and label what changed: new posts, a verification gained or lost, a handle change, follower movement, an account going quiet, being suspended, deactivated or disappearing. History is kept per account, so editing the list does not reset it.

## `onlyChanges` (type: `boolean`):

With monitor mode on, skip accounts where nothing moved — an empty run then means nothing needs your attention.

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

Optional and off by default. These endpoints answer without a proxy; switch one on only if you get errors.

## Actor input object example

```json
{
  "accounts": [
    "anna.wsocial.eu",
    "ingmar.wsocial.eu",
    "wsocial.eu"
  ],
  "posts": [],
  "includeReplies": false,
  "maxRepliesPerPost": 50,
  "discoverAccounts": false,
  "maxAccountsToScan": 2000,
  "minFollowers": 0,
  "minPosts": 0,
  "accountTypes": [],
  "verifiedOnly": false,
  "activeWithinDays": 0,
  "includePosts": true,
  "maxPostsPerAccount": 100,
  "includePostList": false,
  "includeFollows": false,
  "maxFollowsPerAccount": 500,
  "includeLivestreams": false,
  "dormantAfterDays": 30,
  "maxItems": 100,
  "maxConcurrency": 4,
  "monitorMode": false,
  "onlyChanges": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `verification` (type: `string`):

No description

## `engagement` (type: `string`):

No description

## `alerts` (type: `string`):

No description

## `full` (type: `string`):

No description

## `postsView` (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": [
        "anna.wsocial.eu",
        "ingmar.wsocial.eu",
        "wsocial.eu"
    ],
    "posts": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("sashaebashu/wsocial-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": [
        "anna.wsocial.eu",
        "ingmar.wsocial.eu",
        "wsocial.eu",
    ],
    "posts": [],
}

# Run the Actor and wait for it to finish
run = client.actor("sashaebashu/wsocial-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": [
    "anna.wsocial.eu",
    "ingmar.wsocial.eu",
    "wsocial.eu"
  ],
  "posts": []
}' |
apify call sashaebashu/wsocial-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,sashaebashu/wsocial-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/V4lXJfbiuG5hgCFmw/builds/mKlw3dpVcYpzBaBIS/openapi.json
