Bluesky Posts, Threads, Feeds & Trending Topics (no login) avatar

Bluesky Posts, Threads, Feeds & Trending Topics (no login)

Pricing

from $1.05 / 1,000 bluesky data rows

Go to Apify Store
Bluesky Posts, Threads, Feeds & Trending Topics (no login)

Bluesky Posts, Threads, Feeds & Trending Topics (no login)

Public Bluesky data as rows: an account's posts, the replies and quote posts of any post, or any public custom feed - with text, language, engagement counts, hashtags and links. Plus today's trending topics with post counts, and bulk account profiles. Keyless AT Protocol API, UTC dates.

Pricing

from $1.05 / 1,000 bluesky data rows

Rating

0.0

(0)

Developer

Samat Makatov

Samat Makatov

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

4 days ago

Last modified

Share

Public Bluesky data as rows: an account's posts, the replies and quote posts of any post, or any public custom feed - with text, language, engagement counts, hashtags and links. Plus today's trending topics with post counts, and bulk account profiles. Keyless AT Protocol API, UTC dates.

Five questions, one run each: what an account posted, what people replied to one post, who quote-posted it, what is in a public feed, which topics are trending right now, and how big the audience of a handle is. Everything is read from the public AT Protocol AppView, which answers JSON without an account, a key or a cookie, so there is no login, no browser and no proxy in the way. Rows carry the post text, the declared language, likes, reposts, replies, quotes and bookmarks, hashtags, mentions, linked pages and the post link; every timestamp is UTC ISO-8601.

Need it tailored to your workflow? Missing a field, a filter, another source or a different output format? Open an issue and describe what you need — we read every request and adapt the actor.

Use cases

  • Brand and media monitoring after a forum closes its feeds: list the posts of the accounts you watch (accountPosts) on a schedule with sinceHours: 24 and onlyNew, and every run holds only what is new.
  • Community and support work: turn one post link into its discussion (threadReplies) with the depth of every reply, so you can answer the whole branch instead of the newest comment.
  • PR after an announcement: collect who quote-posted it and what they wrote (includeQuotes), the reaction a reply tree never shows.
  • Dataset and agent input: harvest any public feed by its link (customFeed), filtered to the languages you can process, and keep the columns small with fields.
  • Social listening and newsrooms: read the topics the network is discussing this hour with a post count and a start time (trendingTopics), then feed a topic's feedUri back into customFeed to read the posts behind it.
  • Influencer and competitor research: resolve a list of handles to followers, following and post counts in one go (profiles, 25 accounts per request).

Input

Examples live in prefill: the Console form opens filled in, so a first run from the Console works without editing anything. prefill is a Console feature only - a run started from the API, a schedule, an integration or MCP gets the defaults below and nothing else, so it names its own handles, postUrls or feedUrls (without them the run ends at once with a message that names the missing field). Defaults only change behaviour, never the subject of a search.

FieldTypeDefaultUsed byNotes
modestringaccountPosts-accountPosts, threadReplies, customFeed, trendingTopics, profiles - see Modes
handlesstring[]prefill ["bsky.app","nytimes.com"]accountPosts, profilesHandles (nytimes.com), @handle, profile links or DIDs
accountFilterstringposts_no_repliesaccountPostsWhich half of the timeline - see Account filters
postUrlsstring[]prefill one post linkthreadRepliesWeb links (bsky.app/profile/<handle>/post/<id>) or AT-URIs
threadDepthnumber3threadReplies0 = the post alone, 1 = direct replies, max 10
includeQuotesbooleanfalsethreadRepliesAlso write the posts quoting the given post
feedUrlsstring[]prefill the "What's Hot" feedcustomFeedWeb links (bsky.app/profile/<handle-or-did>/feed/<id>) or AT-URIs
trendStatusstringanytrendingTopicsany, trending, cooling - see Topic statuses
languagesstring[]allpost modesKeep posts whose first declared language matches - see Languages
minLikesnumbernonepost modesLike floor at fetch time
sinceHoursnumbernonepost modesRolling UTC window, 1-8760 hours; in accountPosts paging stops at its edge, in the other post modes it filters what the source returns
onlyNewbooleanfalsepost modesSkip posts written by earlier runs of the same actor or task; only posts actually written are remembered
includeRepostsbooleanfalseaccountPosts, customFeedKeep what the account amplified, marked isRepost: true
maxItemsnumber50-Hard row cap, 1-1000; split over the accounts, feeds or post links you named (in profiles: the number of accounts read)
fieldsstring[]all-Keep only these columns, in this order; the identifying columns always stay (see below)

The closed lists (mode, accountFilter, trendStatus, languages) take only the values listed in Reference: Apify checks them before the run starts and refuses a misspelled value with the list of accepted ones, so a run never quietly answers a different question. Handles are forgiving - spaces, a leading @, capital letters and a pasted profile link (https://bsky.app/profile/nytimes.com) are all read as the handle. A handle that does not exist, a deleted post or an account that hides itself from logged-out viewers produces one row with found: false and the reason in notFoundReason - never a silent empty success.

fields is free text: letter case is corrected (AuthorHandle -> authorHandle), a name that is not a column of the mode is left out and named in the status message with the nearest column, and a list in which no name is a column of the mode stops the run with the list of valid columns instead of quietly writing every column. The identifying columns stay in every row whatever fields lists: rowType, postUri, sourceRef, found in the post modes; rowType, topic in trendingTopics; rowType, handle, found in profiles.

Reference

Modes

modeWhat one row isSource endpointDataset view
accountPostsA post of an account you named, newest firstauthor feed, paged by cursorPosts
threadRepliesThe post itself (replyDepth: 0) and every reply the source serves, plus quote posts when askedpost thread (+ quotes)Posts
customFeedA post of a public feed, in the feed's own orderfeed generatorPosts
trendingTopicsA topic trending right now, with its post count and feed linktrendsTrending topics
profilesAn account with follower, following and post countsprofiles, 25 per requestProfiles

Account filters

ValueWhat the source returns
posts_no_repliesOnly what the account published itself - the "what did this brand say" list (default)
posts_with_repliesThe whole timeline, including the account's replies to other people
posts_and_author_threadsPosts plus the threads the account wrote to itself
posts_with_mediaOnly posts with an image or a video attached

The filter is applied by the source, so posts outside it are never fetched and never charged.

Topic statuses

trending (still gaining posts) and cooling (past its peak) are the statuses you can filter by. The source also uses others - saturating was seen on 2026-10-01, stale on 2026-10-02 - and those rows are kept, untouched, when trendStatus is any. Most topics in a typical answer are not trending: of the 25 topics served on 2026-10-01 only three were, and at 06:00 UTC on 2026-10-02 none was (4 cooling, 21 stale). A trending-only run is therefore short by nature and can be empty in quiet hours - the status message then lists the statuses the source did serve.

Languages

en English, pt Portuguese, ja Japanese, de German, es Spanish, fr French, it Italian, nl Dutch, tr Turkish, ru Russian, uk Ukrainian, pl Polish, ko Korean, zh Chinese, id Indonesian, fa Persian, th Thai, ar Arabic.

Pick the two-letter codes from this list; any other value (a name like English, a three-letter code like eng) is refused by Apify before the run starts. The language is declared by the app that published the post, not detected here: a post of a news account often declares none, and such posts are dropped when languages is set. A tag like en-US is reported as language: "en", with the full tag kept in langs.

What you haveWhat it means
bsky.app/profile/nytimes.comhandles: ["nytimes.com"]
bsky.app/profile/bsky.app/post/3mwolmfws5k2rA post: handle + record id -> at://<did>/app.bsky.feed.post/3mwolmfws5k2r
bsky.app/profile/bsky.app/feed/whats-hotA feed: handle + feed id -> at://<did>/app.bsky.feed.generator/whats-hot
/profile/did:plc:…/feed/b74101f118b8 (a topic's link)The feed behind a trending topic - paste it into feedUrls

Both shapes work in postUrls and feedUrls; the handle is resolved to its DID before the URI is built, which is what makes quote posts and third-party feeds answer at all.

Examples

What two newsrooms published, newest first

{ "mode": "accountPosts", "handles": ["nytimes.com", "theonion.com"], "accountFilter": "posts_no_replies", "includeReposts": false, "maxItems": 20 }

The discussion under one post, three levels deep

{ "mode": "threadReplies", "postUrls": ["https://bsky.app/profile/bsky.app/post/3mwolmfws5k2r"], "threadDepth": 3, "maxItems": 25 }

Who quote-posted an announcement (no reply tree)

{ "mode": "threadReplies", "postUrls": ["https://bsky.app/profile/aster.id/post/3mwqgsd4cgs2a"], "threadDepth": 0, "includeQuotes": true, "maxItems": 15 }

English posts of a public feed

{ "mode": "customFeed", "feedUrls": ["https://bsky.app/profile/bsky.app/feed/whats-hot"], "languages": ["en"], "maxItems": 20 }

What is trending right now

{ "mode": "trendingTopics", "trendStatus": "trending", "maxItems": 20 }

Audience numbers for a handful of accounts

{ "mode": "profiles", "handles": ["bsky.app", "nytimes.com", "theonion.com", "atproto.com"], "maxItems": 10 }

Monitoring run for a schedule: only new posts of the last day that travelled

{ "mode": "accountPosts", "handles": ["nytimes.com"], "sinceHours": 24, "minLikes": 10, "onlyNew": true, "fields": ["authorHandle", "text", "likeCount", "createdAt", "url"], "maxItems": 50 }

Output

A real row from the cloud run YDFsTLQ4jo3cXAXu5 (2026-10-01, accountPosts):

{
"rowType": "post",
"postUri": "at://did:plc:eclio37ymobqex2ncko63h4r/app.bsky.feed.post/3mwtnkjsrpk2i",
"postId": "3mwtnkjsrpk2i",
"cid": "bafyreietviuynzhap4qo3ecpqf2k7ocxpmersfw4khjg5ukoe3jyesrihe",
"url": "https://bsky.app/profile/nytimes.com/post/3mwtnkjsrpk2i",
"authorHandle": "nytimes.com",
"authorDisplayName": "The New York Times",
"authorDid": "did:plc:eclio37ymobqex2ncko63h4r",
"text": "Death row inmate Christa Pike is in critical condition after Tennessee's failed attempt to execute her, her lawyers said. Follow live updates:",
"language": null,
"langs": [],
"createdAt": "2026-10-01T20:01:17.935Z",
"indexedAt": "2026-10-01T20:01:18.568Z",
"likeCount": 13,
"repostCount": 1,
"replyCount": 4,
"quoteCount": 6,
"bookmarkCount": 0,
"isReply": false,
"isRepost": false,
"isQuotePost": false,
"replyToUri": null,
"replyToHandle": null,
"threadRootUri": "at://did:plc:eclio37ymobqex2ncko63h4r/app.bsky.feed.post/3mwtnkjsrpk2i",
"replyDepth": null,
"quotedPostUri": null,
"quotedAuthorHandle": null,
"hashtags": [],
"mentions": [],
"links": [],
"externalUrl": "https://nyti.ms/4jAP7nD",
"externalTitle": "Live Updates: Christa Pike in Critical Condition After Botched Tennessee Execution",
"embedType": "external",
"imageCount": 0,
"imageAlts": [],
"labels": [],
"sourceKind": "accountPosts",
"sourceRef": "nytimes.com",
"found": true,
"notFoundReason": null,
"fetchedAt": "2026-10-01T20:08:51.972Z"
}

Post row (rowType: "post")

FieldTypeAlways filledMeaning
rowTypestringyespost
postUristringyesAT-URI of the post
postIdstringyesRecord id - the last part of the post link
cidstringyesContent id of this version of the post
urlstringyesLink to the post in the web app
authorHandlestringyesWho wrote it (for a repost: the original author)
authorDisplayNamestringnoDisplay name at fetch time
authorDidstringyesStable account id
textstringyesPost text; empty for an image-only or video-only post
languagestringnoFirst declared language as a two-letter code; empty when the app declared none
langsstring[]yesAll declared language tags, as declared (["en-US"])
createdAtstringyesWhen the author posted it, UTC
indexedAtstringnoWhen the source indexed it, UTC
likeCount, repostCount, replyCount, quoteCount, bookmarkCountnumberyesEngagement at fetchedAt
isReplybooleanyesThe post answers another post
isRepostbooleanyesThe row came into the feed as a repost
isQuotePostbooleanyesThe post embeds another post
replyToUristringnoThe post this one answers
replyToHandlestringnoAuthor of that parent, when the source served it in the same answer
threadRootUristringyesIn threadReplies: the post the thread was read from; otherwise the root of the post's own thread
replyDepthnumbernothreadReplies only: 0 for the read post, 1 for a direct reply
quotedPostUristringnoThe quoted post
quotedAuthorHandlestringnoAuthor of the quoted post
hashtagsstring[]yesTags, without the #
mentionsstring[]yesMentioned handles
linksstring[]yesURLs in the text
externalUrl, externalTitlestringnoThe link card attached to the post
embedTypestringnoimages, video, external, quote, quoteWithMedia, feedCard, listCard, starterPackCard
imageCountnumberyesAttached images
imageAltsstring[]yesNon-empty alt texts of those images
labelsstring[]yesModeration labels on the post
sourceKindstringyesThe mode that produced the row
sourceRefstringyesThe exact input value that produced it (handle, post link or feed link)
foundbooleanyesfalse on a row that explains why an input gave nothing
notFoundReasonstringnoWhy, in words: deleted post, unknown handle, optedOutOfLoggedOutVisibility
fetchedAtstringyesWhen this run read it, UTC

Trend row (rowType: "trend")

topic (readable name), topicId, description, category, status, postCount, startedAt (UTC), sampleAuthors (up to 10 handles seen in the topic), feedUri (paste into feedUrls; null on the rare topic whose link is not a feed), url, fetchedAt. Example from run SnSQV1ePRYRMdTQjK: "Celebrity Traitors series 2 premieres", category entertainment, status trending, postCount: 784, started 2026-10-01T17:54:23.570Z.

Profile row (rowType: "profile")

handle, did, displayName, description, followersCount, followsCount, postsCount, createdAt (UTC), avatarUrl, bannerUrl, labels, verifiedStatus (valid, none, …), url, found, notFoundReason, fetchedAt. Image URLs are reported as strings; no image is downloaded.

Dataset views: Posts (the three post modes), Trending topics, Profiles. Each run also writes a SUMMARY record into the default key-value store (linked from the run's output as summary) with the request count, rows read and written, rows per account/feed/post, what was skipped (reposts, languages, like floor, time window, already-seen posts, replies the source hides), every input the source would not serve and every input not read because the run stopped first.

Use it from code / agents

curl -X POST "https://api.apify.com/v2/acts/yadroo~bluesky-public-posts/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"mode":"accountPosts","handles":["nytimes.com"],"sinceHours":24,"maxItems":20}'
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('yadroo/bluesky-public-posts').call({ mode: 'trendingTopics', trendStatus: 'trending', maxItems: 20 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("yadroo/bluesky-public-posts").call(run_input={
"mode": "customFeed",
"feedUrls": ["https://bsky.app/profile/bsky.app/feed/whats-hot"],
"languages": ["en"],
"fields": ["authorHandle", "text", "likeCount", "createdAt", "url"],
"maxItems": 20,
})
items = client.dataset(run["defaultDatasetId"]).list_items().items

For an agent, fields is the cheap way to keep the context small: five columns instead of forty, in the order you name them. apify call yadroo/bluesky-public-posts works the same way from the CLI.

MCP: add https://mcp.apify.com to Claude / Cursor / any MCP client and call the yadroo/bluesky-public-posts tool with the same JSON input.

Pricing

Pay per event: $0.001 per run start plus $0.0015 per dataset row (FREE tier). Every run is charged the start event, including a run that ends with no rows.

RunRowsCost
A thread or a short monitoring run20$0.001 + 20 × $0.0015 = $0.031
The default run50$0.001 + 50 × $0.0015 = $0.076
A feed harvest at the cap1000$0.001 + 1000 × $0.0015 = $1.501

The row price follows your Apify plan (Bronze -10 %, Silver -20 %, Gold and above -30 %); the start event is the same on every plan, and platform usage is included in these prices. maxItems is a hard cap, so a run can never cost more than you planned, and sinceHours, minLikes and onlyNew cut rows before they are written - a schedule that finds nothing new costs the start event alone. The run also respects your Maximum cost per run: it stops writing at the last row that budget pays for and ends with "Stopped at your spending limit: N rows delivered"; with onlyNew, only the rows delivered are remembered, so the next run picks up the rest.

Limits & FAQ

  • No keyword or hashtag search. The source's post-search route answers HTTP 403 to callers without a session - checked from this runner and from an Apify run on 2026-10-01 - so this actor does not offer it, and nothing here pretends to. The closest replacement: take a topic from trendingTopics and read its feedUri with customFeed, or use a public feed that already collects your subject.
  • Only what is public without a login. Accounts that ask not to be shown to logged-out viewers come back as one row with found: false and notFoundReason: "optedOutOfLoggedOutVisibility"; no other route is tried.
  • No follower or following lists, no liked-by lists - by design, not by accident. This actor reads posts, public profile cards, topics and feeds.
  • Engagement counts are a snapshot. The same post read tomorrow has different numbers, which is why every row carries fetchedAt.
  • language is declared, not detected. The posting app writes it; accounts that leave it out get language: null and are dropped by a languages filter.
  • Trending topics are an unstable route. They live in the source's own "unspecced" namespace and may change shape or disappear; the run then ends with a readable message instead of a crash, and the other four modes are untouched. At most 25 topics are served per call.
  • Third-party feeds can be empty or slow. A feed is built by whoever published it; when it serves nothing, the run says so per feed in the status message and in SUMMARY instead of failing.
  • Deleted or unknown posts give HTTP 400 at the source and become one found: false row with the reason - a typo in a list of ten links never disappears silently.
  • Rate limits. The source asks callers to back off on HTTP 429 and to keep concurrency low; requests are therefore serial with a pause, and a 429 is retried after the delay the source names. A typical run makes 2-6 requests.
  • Hidden replies. Replies removed by their author or by moderation are not served; they are counted in SUMMARY rather than guessed at.
  • Repeats are billed once. A post an input serves twice (a feed repeating it, a reply that is also a quote post) is one row; the same post reached from two inputs (two accounts, two thread links) is one row per input, each with its own sourceRef. In profiles, a handle and a DID that name one account give one row.
  • Run timeout. A run that gets close to its timeout stops starting new requests, saves what it has read and ends successfully with "Stopped before the run timeout: N rows saved". A typical run takes seconds; the default timeout is 15 minutes.
  • Thread depth costs rows, not requests: the source returns the whole branch in one call, so threadDepth: 10 and threadDepth: 1 cost the same request and differ only in rows written.

Made by Yadroo. Sibling actors: google-news-search, hackernews-search, stackexchange-search, rss-to-json, crypto-news.