Bluesky Scraper: Profiles, Posts & Followers avatar

Bluesky Scraper: Profiles, Posts & Followers

Pricing

from $1.50 / 1,000 profile scrapeds

Go to Apify Store
Bluesky Scraper: Profiles, Posts & Followers

Bluesky Scraper: Profiles, Posts & Followers

Scrape Bluesky through the public AT Protocol API: profiles, follower and following counts, post history, engagement and bios. Open protocol, no authentication.

Pricing

from $1.50 / 1,000 profile scrapeds

Rating

0.0

(0)

Developer

Arman Hossain

Arman Hossain

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

0

Monthly active users

a day ago

Last modified

Share

Bluesky Scraper: Public Bluesky profiles, handle, bio, follower counts, recent posts and moderation labels

Bluesky Scraper reads public Bluesky profiles through the AT Protocol AppView, handle, display name, bio, avatar, banner, follower and following counts, post count, account age, moderation labels, and optionally the account's recent posts with engagement counts and its follower list.

Bluesky is an open protocol, and its public AppView answers public reads. No proxy setup, no browser, no credentials to manage.

Agent skill: SKILL.md

https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/bluesky-profile-scraper.md

What you get

Output fieldMeaning
didThe account's permanent decentralised identifier, the only stable key. Handles change; DIDs do not
handleCurrent handle, e.g. jay.bsky.team
displayNameChosen display name, null if unset
descriptionProfile bio
avatar, bannerCDN image URLs, null if unset
followersCount, followsCountFollowers and following
postsCountLifetime post count as the AppView has indexed it
createdAtAccount creation time, account age for influence scoring
postsArray of recent posts when includePosts is on, otherwise null
followersArray of follower records when includeFollowers is on, otherwise null
labelsModeration label values applied to the account, e.g. !no-unauthenticated
indexedAtWhen the AppView last reindexed this profile
scrapedAtRun timestamp

Each entry in posts carries uri, cid, text, createdAt, indexedAt, langs, replyCount, repostCount, likeCount, quoteCount, plus isRepost and isReply flags.

Each entry in followers carries did, handle, displayName, avatar and createdAt.

A RUN_SUMMARY record in the key-value store holds per-run counts, the options applied and every handle that could not be resolved.

Input

FieldTypeDefaultNotes
handlesarray-Required. Handles, @handles, profile URLs or raw DIDs. Mixed input is fine, everything is normalised.
includePostsbooleanfalseFetch the author's recent posts with engagement counts.
maxPostsPerProfileinteger50Cap posts per profile. Only used when includePosts is on.
includeFollowersbooleanfalseFetch the profile's followers, capped at 500 per profile.

Which combinations make sense

  • handles alone is the cheapest and fastest mode: 25 profiles per request, no follow-up calls. Use it for scheduled follower-count tracking.
  • includePosts costs one extra request per 100 posts per profile. maxPostsPerProfile: 50 is a good default for engagement analysis; go higher only if you need history.
  • includeFollowers is the expensive option, 5 extra requests per profile at the 500 cap. Turn it on for a small, deliberate handle list, not a bulk sweep.

All four input forms resolve to the same profile:

You passResolves to
bsky.appbsky.app
@bsky.appbsky.app
https://bsky.app/profile/bsky.appbsky.app
did:plc:z72i7hdynmk6r22z27h6tvurbsky.app

Passing several forms of the same account is safe, records are de-duplicated by DID before anything is saved, so you are never billed twice for one profile.

Example input

{
"handles": ["bsky.app", "@jay.bsky.team", "https://bsky.app/profile/pfrazee.com"],
"includePosts": true,
"maxPostsPerProfile": 5,
"includeFollowers": false
}

Output

A real record from the run above, with posts abridged to one entry:

{
"did": "did:plc:oky5czdrnfjpqslsw2a5iclo",
"handle": "jay.bsky.team",
"displayName": "Jay 🦋",
"description": "Founder & Chief Innovation Officer @ Bluesky\n\nWorking on @attie.ai\n\n🌱 🪴 🌳",
"avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:oky5czdrnfjpqslsw2a5iclo/bafkrei….jpeg",
"banner": "https://cdn.bsky.app/img/banner/plain/did:plc:oky5czdrnfjpqslsw2a5iclo/bafkrei….jpeg",
"followersCount": 595228,
"followsCount": 3974,
"postsCount": 4110,
"createdAt": "2022-11-17T06:31:40.296Z",
"posts": [
{
"uri": "at://did:plc:v7lt2fu7igydcjszoexxrvl2/app.bsky.feed.post/3ms7bligcf22z",
"cid": "bafyreie46c5unflqnrqi7xw3uzotpxzbobsmqul3iolbgnrth6f352jtym",
"text": "New corner of the internet, zero idea who to follow? …",
"createdAt": "2026-08-03T19:53:03.901Z",
"indexedAt": "2026-08-03T19:53:05.435Z",
"langs": ["en"],
"replyCount": 9,
"repostCount": 9,
"likeCount": 72,
"quoteCount": 1,
"isRepost": true,
"isReply": false
}
],
"followers": null,
"labels": [],
"indexedAt": "2026-03-29T21:16:33.460Z",
"scrapedAt": "2026-08-06T11:35:03.807Z"
}

RUN_SUMMARY, from a run that deliberately included one bad handle:

{
"handlesRequested": 5,
"handlesFailed": 1,
"failures": [
{ "handle": "definitely-not-real-xyz123.bsky.social", "error": "profile not found" }
],
"profilesSaved": 3,
"filters": {
"handles": ["bsky.app", "jay.bsky.team", "pfrazee.com", "did:plc:z72i7hdynmk6r22z27h6tvur", "definitely-not-real-xyz123.bsky.social"],
"includePosts": true,
"maxPostsPerProfile": 5,
"includeFollowers": true,
"maxFollowersPerProfile": 500
},
"finishedAt": "2026-08-06T11:35:40.714Z"
}

Five handles in, three profiles out: one was a bad handle, and did:plc:z72i7hdynmk6r22z27h6tvur is bsky.app again, so it collapsed into a single record.

Use cases

1. Track influencers on Bluesky. Run a fixed handle list on a schedule and diff followersCount and postsCount over scrapedAt to get growth curves.

{
"handles": ["bsky.app", "jay.bsky.team", "pfrazee.com"],
"includePosts": false,
"includeFollowers": false
}

2. Social listening on an open network. Pull recent posts with engagement counts for a watchlist and rank by likeCount + repostCount. isRepost lets you separate original output from amplification.

{
"handles": ["bsky.app", "jay.bsky.team"],
"includePosts": true,
"maxPostsPerProfile": 100
}

3. Build a creator-discovery tool. Fetch a seed account's followers, then feed those handles back in as a second run to score them on follower count, post volume and account age.

{
"handles": ["pfrazee.com"],
"includeFollowers": true
}

Limits and behaviour

  • Use public.api.bsky.app. The bsky.social host returns 403 to unauthenticated reads; this Actor never touches it.
  • Profiles are batched 25 per request. A 100-handle run costs 4 profile calls, not 100.
  • Unknown handles are dropped, not errored. getProfiles returns 200 with the missing account simply absent, so the Actor diffs requested against returned and records each miss in RUN_SUMMARY.failures.
  • De-duplicated by DID. A handle and its own DID are different inputs but the same account; only one record is saved.
  • Follower collection stops at 500 per profile. Follower lists are unbounded, bsky.app has over 34 million, so an uncapped fetch would run until the Actor timed out. Bluesky returns followers newest-first.
  • The author feed includes reposts and replies. They are flagged with isRepost / isReply rather than dropped, so postsCount and posts.length will not always agree. Filter client-side if you want originals only.
  • A failing profile never aborts the run. Posts or followers failing for one account are logged, recorded in RUN_SUMMARY.failures, and the profile is still saved with that field set to []. The run only errors out if no profile could be resolved at all.
  • Transient errors are retried. 429 and 5xx get three attempts with linear backoff. A 400 InvalidRequest fails fast, because retrying an invalid handle never helps.
  • Public data only. No authentication, no personal data beyond what the account publishes, no access-control bypass. Accounts carrying the !no-unauthenticated label expose less through the public AppView by design, and that shows up in labels.

FAQ

Do I need a proxy? No. Proxy configuration is not required to run this Actor.

Do I need a Bluesky account or app password? No. You supply no credentials.

What happens if a handle is wrong? It is reported in RUN_SUMMARY.failures and the run continues. The run only fails outright if not a single profile resolved.

Can I schedule it? Yes, it is designed for scheduled runs. Follower-count time series is the most common pattern; use did as your join key, since handles change.

Why does posts.length not match postsCount? postsCount is the account's lifetime total; posts is capped by maxPostsPerProfile and includes reposts and replies from the author feed.

Can I get more than 500 followers per profile? Not in one run. The cap keeps runtime bounded. For a large follower graph, page through it yourself with the follower dids from consecutive runs.

Can I get the accounts someone follows, rather than their followers? Not currently, followsCount is included, but the follows list is not.

Are DIDs or handles the right key? DIDs. Handles are user-changeable domain names; the did:plc: identifier is permanent.

Can I integrate it with something else? Yes, Apify API, client libraries, webhooks, scheduled runs, dataset exports (JSON/CSV/Excel) or MCP. Output is structured JSON.

API example

curl -X POST "https://api.apify.com/v2/acts/arman-bd~bluesky-profile-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"handles": ["bsky.app", "jay.bsky.team"],
"includePosts": true,
"maxPostsPerProfile": 25
}'

JavaScript example

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('arman-bd/bluesky-profile-scraper').call({
handles: ['bsky.app', '@jay.bsky.team', 'https://bsky.app/profile/pfrazee.com'],
includePosts: true,
maxPostsPerProfile: 25,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const p of items) console.log(`${p.handle}, ${p.followersCount} followers, ${p.postsCount} posts`);