# Changelog of Bluesky Scraper — Posts, Profiles, Followers & Search (`yasaslive/bluesky-scraper`) Actor

- **URL**: https://apify.com/yasaslive/bluesky-scraper/changelog.md
- **Full Actor documentation**: https://apify.com/yasaslive/bluesky-scraper.md

## Changelog

All notable changes to this Actor are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### \[1.0.0] — 2026-09-26

#### Added

- Six modes: `search`, `profile`, `authorFeed`, `followers`, `follows`, `thread`.
- Flat post output: text, engagement counts, languages, hashtags, mentions, links, images (full size + alt), video, link card, flattened quoted post, reply / repost / thread context.
- Flat profile output: counts, bio, avatar, banner, join date, pinned post, website parsed from the bio, and follower/follows relation context.
- Pay-per-event charging: `actor-start`, `post-scraped`, `profile-scraped` (see `.actor/pay_per_event.json`).
- Optional Bluesky login (handle + app password) that unlocks paginated search with server-side date filters.
- `STATS` record with categorized error counters (blocked / rateLimited / proxy / network / parse / notFound / other), per-input status and charges, logged every 30 s.

#### Decisions (and why)

- **Plain XRPC over HTTPS instead of `@atproto/api` or a browser.** Only 8 read-only GET endpoints are needed, so a ~300-line client keeps the image small and gives exact control over retries, rate limits and host fallback. There's no browser: everything is available as JSON.
- **Cursor loop instead of Crawlee `HttpCrawler`.** Each page depends on the previous page's cursor, so a request queue adds nothing. A loop enforces `maxItems`, the budget and `ratelimit-reset` precisely. `got-scraping` and Apify Proxy session rotation are still used.
- **Search goes to `api.bsky.app`, everything else to `public.api.bsky.app`.** The public AppView answers `searchPosts` with a CDN 403. Any other method that gets a 403 on the primary host automatically switches to the fallback host.
- **Logged-out search returns one page (~100 posts) per term.** As of 2026-09, Bluesky refuses unauthenticated `searchPosts` calls that use a cursor (403 "forbidden by administrative rules") or `since`/`until` (400). We don't work around this. Date filters are applied locally, and deep search needs the user's own login (owner decision, 2026-09-26).
- **Login is optional, search-only, and app-password-only.** The password must match the app-password format, so main passwords are rejected. Authenticated calls go direct rather than through rotating proxies, so the account keeps one stable IP. Tokens live in memory only. The service URL must be a public `https://` host (SSRF guard).
- **Accounts with the `!no-unauthenticated` self-label are skipped** (posts, profiles, followers, threads, quoted posts), matching Bluesky's logged-out web app. They're counted in `STATS.optOutSkipped` (owner decision, 2026-09-26).
- **`maxItems` is per input** (per search term, handle or thread), matching the product brief's field name, instead of the template's `maxResults`. In Profile mode it caps the number of handles.
- **Results are deduplicated across the whole run** by post AT-URI (reposts by reposter + URI; graph entries by relation + subject + DID). A post matched by two search terms is saved and charged once.
- **Followers / follows are enriched** with `getProfiles` (1 extra request per 25 profiles), so follower counts are never missing. If enrichment fails, the basic profile is saved with null counts rather than dropped.
- **Charging happens after `pushData` succeeds.** The chargeable count is checked first, so the platform's overcharge-by-one safeguard is never triggered. Dedupe keys are persisted on `persistState`, so a migrated run never re-pushes or re-charges.
- **`since` / `until` and the login fields have no default or prefill.** A default would silently filter or authenticate every run. Every other input field has a default and a prefill, and the default input returns results.
- **Vitest 3 rather than 5.** Vitest 5 requires Node ≥ 22, while the Actor image is Node 20.

#### Known advisories (accepted)

- `npm audit` reports moderate advisories in `vitest` / `@vitest/mocker` (dev-only; the fix needs Node 22) and in `stream-json` via `@crawlee/core` (a transitive dependency of `apify`; the vulnerable JSON filter functions aren't used by this Actor). CI fails on high or critical production advisories.
