Bluesky Scraper: Profiles, Posts & Followers
Pricing
from $1.50 / 1,000 profile scrapeds
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
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
0
Monthly active users
a day ago
Last modified
Categories
Share

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 field | Meaning |
|---|---|
did | The account's permanent decentralised identifier, the only stable key. Handles change; DIDs do not |
handle | Current handle, e.g. jay.bsky.team |
displayName | Chosen display name, null if unset |
description | Profile bio |
avatar, banner | CDN image URLs, null if unset |
followersCount, followsCount | Followers and following |
postsCount | Lifetime post count as the AppView has indexed it |
createdAt | Account creation time, account age for influence scoring |
posts | Array of recent posts when includePosts is on, otherwise null |
followers | Array of follower records when includeFollowers is on, otherwise null |
labels | Moderation label values applied to the account, e.g. !no-unauthenticated |
indexedAt | When the AppView last reindexed this profile |
scrapedAt | Run 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
| Field | Type | Default | Notes |
|---|---|---|---|
handles | array | - | Required. Handles, @handles, profile URLs or raw DIDs. Mixed input is fine, everything is normalised. |
includePosts | boolean | false | Fetch the author's recent posts with engagement counts. |
maxPostsPerProfile | integer | 50 | Cap posts per profile. Only used when includePosts is on. |
includeFollowers | boolean | false | Fetch the profile's followers, capped at 500 per profile. |
Which combinations make sense
handlesalone is the cheapest and fastest mode: 25 profiles per request, no follow-up calls. Use it for scheduled follower-count tracking.includePostscosts one extra request per 100 posts per profile.maxPostsPerProfile: 50is a good default for engagement analysis; go higher only if you need history.includeFollowersis 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 pass | Resolves to |
|---|---|
bsky.app | bsky.app |
@bsky.app | bsky.app |
https://bsky.app/profile/bsky.app | bsky.app |
did:plc:z72i7hdynmk6r22z27h6tvur | bsky.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. Thebsky.socialhost 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.
getProfilesreturns 200 with the missing account simply absent, so the Actor diffs requested against returned and records each miss inRUN_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.apphas 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/isReplyrather than dropped, sopostsCountandposts.lengthwill 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
InvalidRequestfails 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-unauthenticatedlabel expose less through the public AppView by design, and that shows up inlabels.
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`);