# Bluesky Scraper — Profiles, Posts & Followers (`insight.solutions/bluesky-scraper`) Actor

Scrape Bluesky as data. One row per profile, post, reply, follower, follow or like: text, engagement counts, images, links, mentions and quoted posts. Search the account directory, walk a thread, list who liked a post. No API key, no login, no app password.

- **URL**: https://apify.com/insight.solutions/bluesky-scraper.md
- **Developed by:** [Insight Solutions](https://apify.com/insight.solutions) (community)
- **Categories:** Social media, Lead generation, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.25 / 1,000 item returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Bluesky Scraper — Posts, Profiles & Followers

**Get Bluesky as data.** Give this Actor a list of handles and get back one row per post: the text exactly as written, exact like, repost, reply and quote counts, the images with their alt text, the links, the mentions, and what it quotes. Ask for followers or follows and each is its own row. Paste a post URL and get the whole thread flattened, plus everyone who liked it.

No API key. No login. No app password. No cookies to paste. Bluesky's public AppView answers anonymously, and this Actor never asks it for anything else. **$0.40 per 1,000 rows**, proxy included, accounts that do not exist are free, and a run that returns nothing costs nothing at all.

### Try it in 30 seconds

```json
{
  "profiles": ["bsky.app", "https://bsky.app/profile/pfrazee.com"],
  "maxPostsPerProfile": 50,
  "feedFilter": "posts_no_replies"
}
```

A bare handle and a profile URL — both work, and so do DIDs.

### What comes back

```jsonc
{
  "ok": true,
  "rowType": "post",                 // profile | post | reply | follower | follow | like | repost | diagnostic

  "uri": "at://did:plc:xb2urvqt5f4zzccjs46hysbf/app.bsky.feed.post/3muplmkx56s2k",
  "cid": "bafyreidu2rxt55wzm3wmadbp6fzfrik6idvmh34qk23gwsssx6fqdgxbqm",
  "url": "https://bsky.app/profile/cee.wtf/post/3muplmkx56s2k",

  "authorDid": "did:plc:xb2urvqt5f4zzccjs46hysbf",
  "authorHandle": "cee.wtf",
  "authorDisplayName": "cee",
  "text": "throng.cee.wtf i've invented a new way to browse bluesky",

  "createdAt": "2026-09-04T18:25:36.591Z",   // when the author posted it
  "indexedAt": "2026-09-04T18:26:09.958Z",   // when Bluesky first saw it
  "langs": ["en"],

  "likeCount": 7993,                 // exact — Bluesky publishes integers, not "8K"
  "repostCount": 1883,
  "replyCount": 342,
  "quoteCount": 625,

  "isReply": false,
  "parentUri": null,
  "rootUri": null,
  "isRepost": true,                  // it is in this feed because bsky.app reposted it
  "repostedByHandle": "bsky.app",
  "profileHandle": "bsky.app",       // whose feed this row came from

  "embedType": "video",              // images | video | external | record | recordWithMedia
  "images": [],
  "videoUrl": "https://video.bsky.app/watch/…/playlist.m3u8",
  "externalLink": null,
  "quotedPostUri": null,
  "links": ["https://throng.cee.wtf"],
  "mentions": [],
  "tags": [],
  "labels": [],

  "position": 2,
  "error": null,
  "errorType": null,
  "scrapedAt": "2026-09-09T15:26:31.402Z",
  "source": "bsky.app",
  "sourceUrl": "https://bsky.app/profile/bsky.app"
}
```

Every row carries the same keys, whatever it describes. A `profile` row fills `did`, `handle`, `displayName`, `description`, `followersCount`, `followsCount`, `postsCount`, `isVerified` and leaves the post columns null. A `follower`, `follow`, `like` or `repost` row is that account plus `subjectDid` or `subjectUri`, naming what it is a follower of or a like on.

### Use cases

- **Audience and competitor research** — what an account posts, what lands, and who follows it. `likeCount` and `repostCount` are exact integers, not rounded display strings, so they aggregate honestly.
- **Lead lists from engagement** — the accounts that liked one specific post are a warmer list than any follower dump. `includeLikes` gives you them with handles, display names and bios.
- **Sentiment and topic analysis** — one flat array of text with `langs`, `links` and `mentions` already split out, ready to hand to a model.
- **Thread and conversation mining** — one post URL returns the whole reply tree flattened, with `depth` and `parentUri`, six generations deep.
- **Community mapping** — `includeFollows` on a set of seed accounts gives you the edges of a graph in one run.
- **Brand and mention monitoring** — walk the profiles you care about on a schedule and diff on `cid`, which changes when a post is edited.
- **Alt-text and accessibility auditing** — `images[].alt` is the author's own alt text, and Bluesky's culture of writing it makes this unusually rich.

### How it works, and why it keeps working

Bluesky is not a walled garden. It runs on AT Protocol, and its **public AppView at `public.api.bsky.app` serves every public read without a token** — no OAuth, no app password, no session. That is a deliberate design decision by Bluesky, not a hole, which is why this Actor reads it directly rather than driving a browser.

| Step | Request | What it gets |
|---|---|---|
| Profile | `app.bsky.actor.getProfile` | The account: bio, avatar, banner, exact follower / following / post counts, verification, creation date |
| Posts | `app.bsky.feed.getAuthorFeed` | Up to 100 posts a page, with the filter you chose, paged on a cursor |
| Followers | `app.bsky.graph.getFollowers` / `getFollows` | One page of accounts at a time, paged the same way |
| Search | `app.bsky.actor.searchActors` | The account directory, ranked |
| Thread | `app.bsky.feed.getPostThread` | The post and its whole reply tree in one request, six generations deep |
| Likes | `app.bsky.feed.getLikes` / `getRepostedBy` | Who engaged with one post |
| Resolve | `com.atproto.identity.resolveHandle` | A handle to a DID, only when a post URL needs it |

Two shapes in that data are worth knowing about, because they are where cheaper scrapers go wrong:

1. **A post is published twice in the same object.** The author's own record holds the text, the timestamp and the rich-text facets; the AppView's *view* of it holds the CDN URLs. A record's image is a content hash, not a link — only the view has one. So the text comes from the record and the media comes from the view, and reading either half alone gets you blob hashes or missing text.
2. **Links, mentions and hashtags are not in the text.** The text is plain. They arrive as *facets*: byte ranges over the UTF-8 encoding of that text, plus a feature. A mention's feature carries only the DID, so the handle a person recognises has to be sliced out of the text at those byte offsets — **byte** offsets, which one emoji earlier in the post moves by four. Get that wrong and every mention in a post with an emoji comes back truncated.

Underneath: **Apify proxy, one pinned session per parallel worker.** When Bluesky refuses an exit IP — HTTP 429, HTTP 403, an HTML page where JSON belongs — that session is retired and **the same page is asked for once more from a different address**. Retrying on an address that was just refused only deepens the block, so it is never done. If the second address is refused too, the walk stops, **keeps every row it already delivered**, and files one free `blocked` row saying where it stopped.

Pages of the same entry are spaced 250–600 ms apart. Nothing forces that; it is the difference between reading a profile and hammering one.

### How it compares

- **No login, ever.** No app password, no OAuth, no session token. The endpoints this reads are the anonymous public ones, so there is no account of yours to get rate-limited, suspended, or leaked.
- **Datacenter proxy, not residential.** Bluesky's AppView answers a datacenter address exactly as it answers a residential one — we captured the same profile through both and got byte-identical responses apart from the follower counter ticking. That is why this costs $0.40 per 1,000 rather than the $1–2 a residential-only scraper has to charge.
- **Exact counts.** Bluesky publishes real integers for likes, reposts, replies and quotes. Nothing in this dataset is expanded from "8K".
- **Threads are rows, not a nested blob.** One row per reply with `depth`, `parentUri` and `rootUri`, so a spreadsheet or a SQL table holds the conversation without a JSON parser.
- **Failures are free and legible.** A handle that does not exist, a deleted post, an empty search, a block — each is a diagnostic row with an `errorType` you can branch on, and no charge. A run that returns nothing at all finishes **FAILED** with the reason in its status message, never a green run containing an apology.
- **A partial walk is kept, not thrown away.** Hit `maxRunSecs` or your charge ceiling on page 12 and you keep pages 1–11.

### Input reference

Any combination of the three input kinds runs in one job.

| Field | Type | Default | What it does |
|---|---|---|---|
| `profiles` | array of strings | prefilled with `bsky.app` | Handles (`bsky.app`, `@nasa.gov`), DIDs, or profile URLs. Duplicates are read, and billed, once |
| `includePosts` | boolean | `true` | Return each profile's posts. Off gives profile rows only — one request per account |
| `maxPostsPerProfile` | integer | `100` | Posts per profile. `0` = the whole feed |
| `feedFilter` | enum | `posts_no_replies` | `posts_no_replies`, `posts_with_replies`, `posts_with_media` or `posts_and_author_threads` |
| `includeFollowers` | boolean | `false` | One `follower` row per account following each profile |
| `includeFollows` | boolean | `false` | One `follow` row per account each profile follows |
| `maxFollowsPerProfile` | integer | `100` | The cap on each of those two lists, per profile. `0` = everything |
| `profileSearchQueries` | array of strings | `[]` | Search the **account** directory. One `profile` row per match |
| `maxResultsPerQuery` | integer | `50` | Accounts per query. `0` = everything the directory returns |
| `postUrls` | array of strings | `[]` | `https://bsky.app/profile/<handle>/post/<id>` URLs or `at://` URIs |
| `maxRepliesPerPost` | integer | `100` | Replies per post. `0` = the whole thread, six generations deep |
| `includeLikes` | boolean | `false` | One `like` row per account that liked each post, with the time of the like |
| `includeReposts` | boolean | `false` | One `repost` row per account that reposted each post |
| `maxEngagementPerPost` | integer | `100` | The cap on each of those two lists, per post. `0` = everything |
| `maxConcurrency` | integer | `4` | Entries walked in parallel, each with its own proxy session |
| `maxRunSecs` | integer | `240` | Whole-run wall-clock budget. What is already written is kept and billed |
| `proxyConfiguration` | object | Apify datacenter | Leave it alone unless you start seeing `blocked` rows |

### Output reference

Every row carries the same keys. `ok: true` is data; `ok: false` is a free diagnostic row.

| Field | What it is |
|---|---|
| `rowType` | `profile`, `post`, `reply`, `follower`, `follow`, `like`, `repost` or `diagnostic` |
| `input`, `query`, `position` | The entry this row came from, the search that produced it, and where it fell in the order |
| `url` | The bsky.app page for this profile or post |
| `did`, `handle`, `displayName`, `description`, `avatarUrl`, `bannerUrl` | Account rows: who they are |
| `followersCount`, `followsCount`, `postsCount`, `isVerified` | Account rows, from `profiles` only — Bluesky's list endpoints publish no counts |
| `uri`, `cid` | Post rows: the AT URI, and the content hash that changes when a post is edited |
| `authorDid`, `authorHandle`, `authorDisplayName` | Who wrote the post. On a repost, the original author |
| `text`, `createdAt`, `indexedAt`, `langs` | The post, when it was written, when Bluesky saw it, what languages it declares |
| `likeCount`, `repostCount`, `replyCount`, `quoteCount` | Exact integers |
| `isReply`, `parentUri`, `rootUri`, `depth` | Where this post sits in its thread |
| `isRepost`, `repostedByHandle`, `profileHandle` | Whether a repost put it in the feed, who reposted it, and whose feed it came from |
| `embedType`, `images`, `videoUrl`, `externalLink`, `quotedPostUri` | What is attached: images with alt text, an HLS video playlist, a link card, a quoted post |
| `links`, `mentions`, `tags` | Rich-text facets, split out |
| `subjectDid`, `subjectUri` | Relationship rows: the account followed, or the post liked |
| `labels` | Moderation label values on the account or the post |
| `ok`, `error`, `errorType` | Whether this row is data, and if not, why not |
| `scrapedAt`, `source`, `sourceUrl` | When, and from where |

`errorType` on a diagnostic row is one of:

| Value | Meaning | Charged? |
|---|---|---|
| `not-found` | No such account, or a post that was deleted or is behind a block | No |
| `no-results` | The search matched no accounts | No |
| `blocked` | Bluesky refused our requests from two different exit IPs. Rows already returned for that entry are kept | No |
| `invalid-input` | The entry was not a handle, DID, profile URL, post URL or query | No |
| `timeout` | The run's `maxRunSecs` budget ran out before this entry was reached | No |
| `unavailable` | An unexpected failure | No |

### Pricing

**$0.40 per 1,000 rows.** Pay-per-event, with the proxy already inside that number — there is no separate proxy line on your bill for this Actor.

| Event | What triggers it | FREE | Starter | Scale | Business |
|---|---|---|---|---|---|
| **Item returned** *(primary)* | One profile, post, reply, follower, follow, like or repost row written to your dataset | $0.0004 | $0.0004 | $0.0003 | $0.00025 |
| Run started | Once per run, after the first row | $0.001 | $0.001 | $0.001 | $0.001 |

**Worked example.** 100 profiles at 50 posts each, of which 3 handles no longer exist:

- 97 profile rows + 97 × 50 posts = 4,947 rows × $0.0004 = **$1.979**
- 1 run start = **$0.001**
- 3 handles that do not exist = **$0.00**
- **Total: $1.98**

What you are never charged for: an account that does not exist, a deleted post, a search that matched nothing, an entry that was not a handle or a URL, an entry the run never reached before `maxRunSecs`, or a page Bluesky blocked. If a whole run comes back empty it finishes FAILED and bills **nothing at all**, start fee included.

Set `ACTOR_MAX_TOTAL_CHARGE_USD` on a run and the Actor stops walking once the ceiling is in sight, rather than handing you rows it cannot bill or billing you for rows it cannot hand over. It finishes SUCCEEDED with the ceiling named in its status message, and everything already delivered is yours.

### Limits, and the ones that might bite

**Keyword post search is not available.** Bluesky's post-search endpoint answers every non-browser client with an HTML "403 Forbidden" page rather than results — we verified this on 2026-09-09 through residential proxy, through datacenter proxy and from an unproxied connection, and got the same page all three times. So this Actor does not offer it, rather than shipping a feature that returns an error row every time. **What to do instead:** search the account directory with `profileSearchQueries` to find the accounts in your topic, then read their feeds with `profiles` — usually better targeted than a keyword sweep anyway. For one known conversation, `postUrls` returns its whole thread. Bluesky also publishes a public firehose, which is the right tool if you genuinely need every post matching a term as it happens.

**Follower and follow rows carry no counts.** Bluesky's list endpoints return a lighter profile object than `getProfile` does: handle, display name, bio, avatar, labels, and nothing more. `followersCount`, `followsCount`, `postsCount` and `bannerUrl` are null on `follower`, `follow`, `like`, `repost` and search rows. Getting them means asking for each account by name, which is one request per account and a different job.

**No timestamp for a follow or a repost.** `getFollowers`, `getFollows` and `getRepostedBy` publish who, not when. Those rows carry the account's own creation date in `createdAt` instead, which is documented per row type in the dataset schema. `like` rows are the exception — `getLikes` does publish the moment of the like, and that is what the column holds there.

**Threads reach six generations.** One `getPostThread` request returns the post and its replies six levels deep, which covers essentially every real conversation. A reply below that is not returned and is not counted.

**Deleted and blocked posts inside a thread are skipped.** Bluesky substitutes a placeholder carrying only a URI. Those are counted and dropped rather than emitted as rows full of nulls, and anything below them is unreachable.

**Content you can only see signed in.** Accounts that opt out of the logged-out view carry the `!no-unauthenticated` label and this Actor returns what the AppView gives an anonymous reader. Direct messages, muted words and moderation queues need an account. This Actor does not log in, does not accept an app password and does not take a session token, and it never will — that is a deliberate line, not a missing feature.

**Bluesky may change the format.** This reads a public API that Bluesky versions but does not freeze. When a shape changes, rows stop arriving and you get free `blocked` or `unavailable` diagnostic rows rather than quietly wrong data, and a run that returns nothing bills nothing.

**Rate and reliability.** Requests go out through Apify proxy with per-worker sessions, one rotation per block, and a 250–600 ms pause between pages of the same entry. Four entries in parallel is the default because it is where throughput and rate-limit headroom balance; raising `maxConcurrency` speeds a long list up and makes 429s more likely.

### Use it from an AI agent, or from code

One JSON object in, one flat array out — the shape agent runtimes want. The Actor runs with **limited permissions**, uses **pay-per-event** pricing and never enters Standby, so it works over the Apify MCP server and with x402 agentic payments. The **Integrations** tab pushes results to Slack, a webhook, Zapier, Make, Google Sheets, Snowflake or BigQuery.

```bash
curl -X POST "https://api.apify.com/v2/acts/insight.solutions~bluesky-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"profiles":["bsky.app"],"maxPostsPerProfile":50}'
```

```python
## pip install apify-client
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("insight.solutions/bluesky-scraper").call(run_input={
    "profiles": ["bsky.app", "pfrazee.com"],
    "maxPostsPerProfile": 100,
    "feedFilter": "posts_no_replies",
    "postUrls": ["https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l"],
    "includeLikes": True,
})

for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    if not row.get("ok"):
        print("skipped:", row["input"], row["errorType"])
    elif row["rowType"] == "post":
        print(f'{row["authorHandle"]} ({row["likeCount"]} likes): {row["text"][:80]}')
    elif row["rowType"] == "profile":
        print(f'{row["handle"]}: {row["followersCount"]} followers')
```

Turn `includePosts` off when you only need to enrich a list of handles — one request per account, and the profile row carries the exact follower count.

### FAQ

**Do I need a Bluesky account or an app password?**
No. Bluesky's AppView serves public reads to anyone, and this Actor uses nothing else. There is no account of yours involved at any point.

**Can I search for posts containing a keyword?**
Not through this Actor. Bluesky's post-search endpoint refuses non-browser clients outright — see the first item under Limits, and the two things to do instead.

**Why is this cheaper than most social scrapers?**
Because Bluesky lets us read it properly. There is no browser to drive, no login wall to work around and no need for residential proxy, so the cost per row is genuinely lower and the price reflects that rather than the market rate for the same data elsewhere.

**How many posts can I get from one account?**
As many as it has. Set `maxPostsPerProfile: 0` and raise `maxRunSecs`. Bluesky serves 100 per page with a pause between pages, so a 10,000-post account is about a hundred requests — plan for minutes, and bound it with `ACTOR_MAX_TOTAL_CHARGE_USD` if you are not sure what you are asking for.

**What is a DID, and should I use one?**
It is the identifier behind a handle — `did:plc:z72i7hdynmk6r22z27h6tvur` is `bsky.app`. Handles are domain names and can change; DIDs never do. Use the `did` column as your join key, and feed DIDs back in as input when you are tracking an account over time.

**Are followers and replies charged?**
Yes, at the same rate as posts — each is a row. That is why `includeFollowers`, `includeFollows`, `includeLikes` and `includeReposts` are all off by default and every one of them is capped.

**What happens if one entry fails?**
The others still run. The failed one produces a free diagnostic row and the run finishes `SUCCEEDED`. If *every* entry fails, the run finishes `FAILED` and you are billed nothing at all.

**Is the data fresh?**
Live. Every run reads Bluesky at that moment; nothing is cached.

### Legal and data-protection notes

- **Public data only.** Every source is a public Bluesky endpoint served to anonymous readers. The Actor never logs in, never accepts an app password or session token, never takes an API key belonging to anyone else, and never touches direct messages or anything behind an account.
- **Posts and profiles are personal data in most jurisdictions.** A row carries a display name, a handle, a bio, an avatar and a DID, and under the GDPR and similar laws that is personal data about an identifiable person. You are the controller of whatever you collect: have a lawful basis, keep only what you need, honour deletion requests, and remember that a post deleted on Bluesky stays in your dataset until you remove it. `includePosts: false` and the four engagement switches exist so that analysis which does not need a category of data does not carry it.
- **Posts are their authors' words.** Republishing them, or training on them, is your call and your responsibility, subject to Bluesky's terms and to the law where you operate. Aggregation, sentiment analysis and quotation are the ordinary uses and are what this is built for.
- **Moderation labels are carried, not applied.** The `labels` column passes through what Bluesky's own labellers published. It is not a judgement by this Actor, and it is not complete — labelling is a service a reader subscribes to, and an anonymous read sees only the defaults.
- **Not affiliated with Bluesky Social PBC, with the AT Protocol project, or with any account whose content you retrieve.** All product names and trademarks belong to their respective owners and are used only to describe which public endpoints this Actor reads.

### Our other Actors

Every Insight Solutions Actor is pay-per-result with no browser, no login and no API key, and every one of them returns free diagnostic rows instead of billing for failures. Prices are per 1,000 results.

**Video, audio & social**

- [YouTube Transcript API](https://apify.com/insight.solutions/youtube-transcript-api) — captions as timed segments, text, SRT or VTT, with language fallback and translation.
- [YouTube Comments API](https://apify.com/insight.solutions/youtube-comments-api) — comments and replies with likes, pinned and hearted flags, newest or top sort.
- [YouTube Channel API](https://apify.com/insight.solutions/youtube-channel-api) — a channel's videos, Shorts and live streams, plus YouTube search.
- [Podcast Search, Episodes & Charts API](https://apify.com/insight.solutions/podcast-api) — Apple Podcasts search, charts and full episode feeds.
- [Telegram Channel Scraper](https://apify.com/insight.solutions/telegram-channel-scraper) — posts, views and channel stats from public Telegram channels.
- [Substack Scraper](https://apify.com/insight.solutions/substack-scraper) — posts with full free text, comments and publication profiles.

**News, documents & the web**

- [Google News Search, Topics & Real Article URLs](https://apify.com/insight.solutions/google-news-api) — news search and topic feeds with the publisher's real URL decoded.
- [Website to Markdown — Content Extractor for LLMs & RAG](https://apify.com/insight.solutions/website-content-extractor) — any site as clean Markdown, text and heading-aware chunks.
- [Internet Archive API](https://apify.com/insight.solutions/internet-archive-api) — archive.org search, item metadata, files and reviews.
- [Wayback Machine Toolkit](https://apify.com/insight.solutions/wayback-toolkit) — archived URL inventories, snapshots and text diffs between dates.
- [Website Technology Detector](https://apify.com/insight.solutions/website-tech-detector) — the tech stack behind any site, with the evidence for each detection.
- [Domain Intelligence API](https://apify.com/insight.solutions/domain-intelligence-api) — DNS, RDAP registration, TLS certificate and HTTP facts in one row per domain.
- [SEO Page Audit](https://apify.com/insight.solutions/seo-page-audit) — sitemap crawl with on-page checks, structured data and broken-link reports.
- [Keyword Suggestions API](https://apify.com/insight.solutions/keyword-suggestions-api) — Google, YouTube, Bing, Amazon and eBay autocomplete with alphabet and question expansions.
- [Website Contact Extractor](https://apify.com/insight.solutions/website-contact-extractor) — emails, phone numbers and social profiles from any list of websites.

**Business, finance & jobs**

- [Congress & Insider Trades API](https://apify.com/insight.solutions/congress-insider-trades-api) — STOCK Act periodic transaction reports and SEC Form 4 insider trades in one schema.
- [SEC EDGAR API](https://apify.com/insight.solutions/sec-edgar-api) — filings, XBRL financials and full-text search by ticker or CIK.
- [Y Combinator Companies, Batches & Founders](https://apify.com/insight.solutions/yc-companies-directory) — the YC directory with founders and social links, filterable by batch, industry and hiring status.
- [Career Site Jobs API](https://apify.com/insight.solutions/ats-jobs-api) — jobs straight from Greenhouse, Lever, Ashby, Workable and 10+ other ATS career sites.
- [New Job Postings Monitor](https://apify.com/insight.solutions/job-postings-monitor) — new, closed and changed postings on the career sites you watch.
- [Shopify Products API](https://apify.com/insight.solutions/shopify-products-api) — any Shopify store's catalogue, variants, prices and stock signals.

**Apps & games**

- [App Store & Google Play Reviews API](https://apify.com/insight.solutions/app-reviews-api) — reviews from both stores with ratings, versions and developer replies.
- [App Store Top Charts & App Search API](https://apify.com/insight.solutions/app-charts-api) — Apple top charts by country and genre, plus app search and details.
- [Steam Reviews API](https://apify.com/insight.solutions/steam-reviews-api) — Steam reviews with playtime, helpfulness and game details.
- [Steam Game Data API](https://apify.com/insight.solutions/steam-store-stats-api) — prices, tags, review scores, live player counts and top charts.

# Actor input Schema

## `profiles` (type: `array`):

One entry per account. A handle (`bsky.app`, `@nasa.gov`), a DID (`did:plc:z72i7hdynmk6r22z27h6tvur`), or a profile URL (`https://bsky.app/profile/bsky.app`) — all three work. Each profile returns one `profile` row and, unless you turn posts off, its recent posts. Duplicates are read, and billed, once.

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

Walk each profile's feed and return one `post` row per post. Turn it off to get profile rows only — one request per account, which is the cheapest way to enrich a list of handles.

## `maxPostsPerProfile` (type: `integer`):

How many posts to return for each profile. Bluesky serves up to 100 per page, so the walk stops on the page that reaches your number. Set 0 for the whole feed, which on a busy account means thousands of rows and a run bounded only by your time and charge budgets.

## `feedFilter` (type: `string`):

Which of Bluesky's own four author-feed filters to walk. `posts_no_replies` is the account's own timeline as a visitor sees it. `posts_with_replies` adds every reply they wrote to somebody else. `posts_with_media` keeps only posts with an image or a video. `posts_and_author_threads` keeps their own threads whole. Reposts appear in all four and are marked `isRepost`.

## `includeFollowers` (type: `boolean`):

Return one `follower` row per account following each profile, up to the cap below. Followers are billed at the same rate as posts and a popular account has millions, so this is off by default and always bounded.

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

Return one `follow` row per account each profile follows, up to the cap below. Usually a much shorter list than the followers, and often the more interesting one.

## `maxFollowsPerProfile` (type: `integer`):

The cap on `follower` rows and, separately, on `follow` rows for a single profile. Ignored when both switches above are off. Set 0 for the whole list — on an account with a million followers that is 10,000 requests, so bound it with `maxRunSecs` and `ACTOR_MAX_TOTAL_CHARGE_USD` before you try.

## `profileSearchQueries` (type: `array`):

Search Bluesky's account directory and return one `profile` row per match — handle, display name, bio, avatar. Matches handles, display names and bios. Note this searches **accounts, not posts**: keyword post search is not available to any non-browser client (see the README).

## `maxResultsPerQuery` (type: `integer`):

How many accounts to return for each search query. Bluesky ranks them, so the first page is the useful one; deep paging returns increasingly loose matches. Set 0 for everything the directory will hand over.

## `postUrls` (type: `array`):

One entry per post: a `https://bsky.app/profile/<handle>/post/<id>` URL or an `at://` URI. Each returns one `post` row for the post itself plus a `reply` row per answer in its thread, six generations deep. Optionally the accounts that liked or reposted it.

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

The cap on `reply` rows for a single post. Bluesky returns the whole thread in one request, so lowering this saves rows and money but not time. Set 0 for every reply the thread holds down to the sixth generation.

## `includeLikes` (type: `boolean`):

Return one `like` row per account that liked each post in `postUrls`, with the time of the like. A high-signal audience list — the people who engaged with one specific thing.

## `includeReposts` (type: `boolean`):

Return one `repost` row per account that reposted each post in `postUrls`. Bluesky does not publish when a repost happened through this endpoint, so those rows carry the account's creation date instead.

## `maxEngagementPerPost` (type: `integer`):

The cap on `like` rows and, separately, on `repost` rows for a single post. Ignored when both switches above are off. Set 0 for everything, which on a post with 8,000 likes is 80 requests.

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

How many profiles, searches and posts are walked at once. Each parallel worker keeps its own proxy session, so an entry that gets rate-limited burns only its own exit IP. Pages *within* one entry cannot be parallelised — each page hands over the cursor for the next — so this is the only lever on speed.

## `maxRunSecs` (type: `integer`):

Wall-clock budget for the whole run. When it is reached the Actor stops walking, keeps and bills for every row it already wrote, and files a free diagnostic row for each entry it never reached. An entry stopped part-way keeps its rows and is not marked as failed.

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

Bluesky's public API answers datacenter addresses as readily as residential ones, so the default is Apify's datacenter proxy — far cheaper, and its cost is already inside the per-row price. Sessions rotate automatically when an exit IP is refused. Switch to residential only if you see `blocked` rows, which would mean the rate limits have tightened.

## Actor input object example

```json
{
  "profiles": [
    "bsky.app",
    "https://bsky.app/profile/pfrazee.com",
    "did:plc:z72i7hdynmk6r22z27h6tvur"
  ],
  "includePosts": true,
  "maxPostsPerProfile": 50,
  "feedFilter": "posts_no_replies",
  "includeFollowers": false,
  "includeFollows": false,
  "maxFollowsPerProfile": 100,
  "profileSearchQueries": [
    "nasa",
    "climate scientist"
  ],
  "maxResultsPerQuery": 50,
  "postUrls": [
    "https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l",
    "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3l6oveex3ii2l"
  ],
  "maxRepliesPerPost": 100,
  "includeLikes": false,
  "includeReposts": false,
  "maxEngagementPerPost": 100,
  "maxConcurrency": 4,
  "maxRunSecs": 240,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `results` (type: `string`):

One row per profile, post, reply, follower, follow, like or repost — same columns on every row. Entries that could not be read get a free diagnostic row saying why. Delivered as JSON items in the default dataset.

# 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 = {
    "profiles": [
        "bsky.app"
    ],
    "includePosts": true,
    "maxPostsPerProfile": 50,
    "feedFilter": "posts_no_replies",
    "includeFollowers": false,
    "includeFollows": false,
    "maxFollowsPerProfile": 100,
    "profileSearchQueries": [],
    "maxResultsPerQuery": 50,
    "postUrls": [],
    "maxRepliesPerPost": 100,
    "includeLikes": false,
    "includeReposts": false,
    "maxEngagementPerPost": 100,
    "maxConcurrency": 4,
    "maxRunSecs": 240,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("insight.solutions/bluesky-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 = {
    "profiles": ["bsky.app"],
    "includePosts": True,
    "maxPostsPerProfile": 50,
    "feedFilter": "posts_no_replies",
    "includeFollowers": False,
    "includeFollows": False,
    "maxFollowsPerProfile": 100,
    "profileSearchQueries": [],
    "maxResultsPerQuery": 50,
    "postUrls": [],
    "maxRepliesPerPost": 100,
    "includeLikes": False,
    "includeReposts": False,
    "maxEngagementPerPost": 100,
    "maxConcurrency": 4,
    "maxRunSecs": 240,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("insight.solutions/bluesky-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 '{
  "profiles": [
    "bsky.app"
  ],
  "includePosts": true,
  "maxPostsPerProfile": 50,
  "feedFilter": "posts_no_replies",
  "includeFollowers": false,
  "includeFollows": false,
  "maxFollowsPerProfile": 100,
  "profileSearchQueries": [],
  "maxResultsPerQuery": 50,
  "postUrls": [],
  "maxRepliesPerPost": 100,
  "includeLikes": false,
  "includeReposts": false,
  "maxEngagementPerPost": 100,
  "maxConcurrency": 4,
  "maxRunSecs": 240,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call insight.solutions/bluesky-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,insight.solutions/bluesky-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/J4I8RdPIiqm9YCbbK/builds/p6JHKcwcgVndLHMbN/openapi.json
