X Tweet Scraper
Pricing
Pay per usage
X Tweet Scraper
Browserless X/Twitter tweet scraper built with Node.js and TypeScript. Extracts and filters tweets using HTTP requests only, with pagination, rate-limit handling, retries, deduplication, resumable state, proxy support, and strict free-tier result limits.
Pricing
Pay per usage
Rating
0.0
(0)
Developer
Luciano Casacci
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
1
Monthly active users
7 days ago
Last modified
Categories
Share
TypeScript Apify Actor for searching public X posts through an HTTP-only data source, normalizing them to the assessment contract, applying authoritative local filters, deduplicating results, and enforcing server-authoritative FREE/PAID entitlement limits.
Status
The Actor is deployed and has been validated end-to-end on Apify.
Live validation includes:
- FREE tier enforcement
- PAID entitlement resolution
latestsearchtopsearch- multi-page pagination
- combined filters
- hashtag filtering
- canonical output validation
- residential Apify Proxy
- 100-result paid performance benchmark
The project is locally tested and clean-clone buildable.
Requirements
- Node.js 22+
- npm
TWEETAPI_API_KEYin private configuration or a local.env- Authenticated Apify CLI for deployment
The API key is never part of Actor input, source control, schemas, persisted state, or logs.
Local development
npm cinpm testnpm run typechecknpm run build
The production entrypoint is:
dist/main.js
Run locally with:
$npm start
or with the Apify CLI:
$apify run
A clean-clone validation was performed successfully with dependency installation, tests, type checking, and production build.
Deployment
$apify push --force
The repository includes:
Dockerfile.actor/actor.json.actor/INPUT_SCHEMA.json- strict TypeScript configuration
- production build configuration
The Actor uses Apify SDK v3 and runs natively on the Apify platform.
Execution flow
- Initialize the Apify Actor.
- Read and validate input using Zod.
- Resolve the runner identity from the Apify runtime.
- Resolve FREE/PAID entitlement against an owner-controlled private store.
- Fail closed to FREE when entitlement cannot be verified.
- Calculate the effective collection limit.
- Restore resumable state and validate the input fingerprint.
- Configure Apify Proxy when requested.
- Build the provider HTTP query.
- Fetch pages using cursor-based pagination.
- Normalize provider responses into strict
CanonicalTweetobjects. - Apply authoritative local filters.
- Deduplicate results.
- Validate each item again at the output boundary.
- Enforce the entitlement limit immediately before dataset emission.
- Push valid results to the default Apify dataset.
- Persist the final run summary to KVS
OUTPUT.
Project structure
src/main.ts— Actor lifecycle.src/orchestrator.ts— production orchestration.src/input.ts— strict runtime input validation.src/types/— canonical tweet contract and schemas.src/providers/tweetapi/— HTTP provider integration, query construction, response validation, retry handling, and transport.src/normalize.ts— provider-to-canonical normalization boundary.src/filter.ts— authoritative local filtering.src/pipeline/paginator.ts— cursor pagination and deduplication.src/pipeline/result-sink.ts— final validation, entitlement enforcement, and dataset output.src/entitlement/resolver.ts— runtime identity and fail-closed entitlement.src/entitlement/apify-store.ts— private Apify KVS entitlement lookup.src/state.ts— resumable state and deterministic input fingerprinting.src/integration.ts— Apify Proxy, migration, and reconciliation adapters.scripts/benchmark.ts— deterministic local processing benchmark.
Input
Primary selectors include:
searchTermsfromUserstoUsersmentioninghashtags
At least one useful search selector must be supplied.
Additional filters include:
- date range
- language
- minimum likes
- minimum retweets
- minimum replies
- verified-only filtering
- media type
- replies
- retweets
- ordering (
latest/top) maxResultsproxyConfiguration
Important semantics:
- Unspecified
includeRepliesmeans no reply constraint. includeReplies: falseexcludes replies.includeRetweetsdefaults tofalse.- FREE users receive at most
min(maxResults, 10). - PAID users may receive up to their requested
maxResults. - Entitlement cannot be selected through Actor input.
Input is runtime-validated. Undocumented fields cannot be used to grant PAID access or bypass the output limit.
HTTP-only extraction
Extraction is browserless and HTTP-only.
The implementation does not use:
- Playwright
- Puppeteer
- Selenium
- browserless
- headless Chromium
- personal X cookies
auth_tokenct0- a logged-in personal X account
The final implementation uses TweetAPI as the assessment-permitted equivalent HTTP data source.
Direct logged-out X SearchTimeline / guest-token global search was investigated during implementation but was not usable for the required global-search flow in the tested environment. Rather than introducing browser automation or a personal authenticated X session, the Actor uses an HTTP provider boundary.
Because the selected equivalent HTTP provider does not expose X guest tokens,
X guest-token rotation is not applicable to this implementation. Equivalent
transport resilience is implemented at the HTTP provider boundary through
bounded retries, exponential backoff with jitter, Retry-After handling,
timeouts, pagination, and configurable Apify residential proxy support.
The provider is isolated behind its own integration layer so the extraction source can be replaced without changing the canonical output, filtering, entitlement, or dataset boundaries.
Canonical output
Every dataset item is validated as a strict CanonicalTweet.
The output contains string IDs, ISO-8601 UTC timestamps, author information,
engagement metrics, relationship flags, hashtags, mentions, expanded URLs,
normalized media, source information, and scrapedAt.
Missing permitted values are represented as null rather than silently
omitted.
Example:
{"id": "1770000000000000001","url": "https://x.com/example/status/1770000000000000001","text": "Example post","lang": "en","createdAt": "2024-03-20T09:12:00.000Z","conversationId": null,"isReply": false,"isRetweet": false,"isQuote": false,"inReplyToId": null,"quotedTweetId": null,"author": {"id": "42","username": "example","name": "Example","verified": false,"followers": 0,"following": 0},"metrics": {"likes": 0,"retweets": 0,"replies": 0,"quotes": 0,"bookmarks": null,"views": null},"entities": {"hashtags": [],"mentions": [],"urls": [],"media": []},"source": null,"scrapedAt": "2024-03-20T09:13:00.000Z"}
Provider-specific response shapes are normalized before reaching the dataset.
For example, TweetAPI Top responses may contain wrapped content and non-tweet
modules; valid tweet content is unwrapped while non-tweet modules are ignored.
IDs are never fabricated to satisfy the output schema.
Filtering
Provider-side query construction is used to reduce unnecessary traffic, but local filtering remains authoritative.
This means the Actor does not rely solely on the upstream provider to honor the requested constraints.
Live runs have validated combinations including:
- language
- minimum likes
- replies exclusion
- retweets exclusion
- hashtags
latesttop
Filter conditions are applied together using AND semantics where applicable.
Free-tier protection
Free-tier protection is enforced server-side and does not trust Actor input.
The runner identity is obtained from the Apify runtime using
Actor.getEnv().userId.
That identity is resolved against an owner-controlled private Apify key-value store. The entitlement source is not part of the public repository and cannot be modified through Actor input.
Valid private entitlement records resolve to FREE or PAID. Unknown users, missing runtime identity, malformed records, lookup failures, and unavailable entitlement infrastructure all fail closed to FREE.
There is no:
paidinput field- user-controlled
tierinput PAID=trueenvironment override- hardcoded public list of paid users
- client-side-only entitlement decision
Enforcement
The requested limit and effective limit are deliberately separate.
For FREE:
effectiveLimit = min(requestedMaxResults, 10)
For PAID:
effectiveLimit = requestedMaxResults
The effective limit constrains collection so a FREE run stops collecting once the required valid result count has been reached.
More importantly, the limit is enforced again in ResultSink immediately
before Actor.pushData.
This provides defense in depth: changing pagination behavior or manipulating
maxResults cannot by itself cause the output path to emit item 11 for a FREE
run.
When the FREE cap is applied, the run summary reports:
{"limited": true,"reason": "free_tier","cap": 10}
The Actor also emits a clear sanitized log when FREE-tier limiting is applied.
Live entitlement validation
The entitlement path was tested on the deployed Actor.
A FREE run requesting substantially more than 10 results produced:
effectiveLimit: 10pushed: 10limited: truereason: free_tiercap: 10
A controlled PAID identity requesting 25 results produced:
effectiveLimit: 25pushed: 25limited: falsecap: null
Anti-fork reasoning
A fork can modify or remove checks in its own copy of the source code. No public-source-only mechanism can prevent somebody from changing code they own.
The security boundary for the canonical Actor therefore does not rely on the public repository.
PAID entitlement for the canonical deployment requires a successful lookup against an owner-controlled private entitlement source. A user can fork the repository and change their own Actor, but they cannot use Actor input, user-controlled environment variables, or modifications to the public source to grant PAID entitlement in the canonical deployment.
A production billing system could replace the private KVS lookup with a signed entitlement API or billing service without changing the enforcement boundary.
Resilience and rate limiting
The HTTP client implements:
- request timeouts
- bounded retry budgets
- exponential backoff
- jitter
- HTTP 429 classification
- HTTP 5xx retry handling
- network failure classification
Retry-Aftersupport- fatal authentication/request classification
- external cancellation handling
- response-shape validation
A single transient network failure, 429, or retryable server response therefore does not immediately hard-crash the run.
Pagination is cursor-based and results are deduplicated by tweet ID.
Apify Proxy
proxyConfiguration is supported as Actor input.
When enabled, the Actor creates an Apify ProxyConfiguration and routes HTTP
requests through the configured proxy.
Residential proxy operation was validated on the deployed Actor with:
{"useApifyProxy": true,"apifyProxyGroups": ["RESIDENTIAL"]}
The Node.js HTTP transport uses Undici's ProxyAgent through the supported
dispatcher mechanism.
A stable Apify proxy session is used during the run to avoid unnecessary identity churn. Proxy URLs and credentials are never written to logs or persisted state.
Without proxyConfiguration, requests continue through the normal direct HTTP
transport.
Resumable state
KVS STATE contains only safe execution state such as:
- cursor
- pushed count
- seen IDs
- input fingerprint
- query state
- statistics
API keys, proxy credentials, private entitlement data, private URLs, and raw provider responses are not persisted.
The input fingerprint prevents incompatible state from being resumed using a different query.
The state/reconciliation layer supports continuation without blindly restarting the same workload from zero.
Tests
The Vitest suite covers:
- input validation
- normalization
- filtering
- pagination
- deduplication
- HTTP response validation
- retry/backoff behavior
- entitlement resolution
- fail-closed entitlement cases
- FREE cap enforcement
- ResultSink
- persisted state
- integration adapters
- proxy transport
- proxy credential redaction
Run:
$npm test
Also verify:
npm run typechecknpm run build
The project has been validated from a clean clone.
Performance benchmark
A live end-to-end paid Apify run was executed using:
- broad keyword query
sortBy: latestmaxResults: 100- Apify residential proxy
- PAID entitlement
- no rate-limit failure
- no dropped-to-error run
- no duplicate outputs
Observed result:
Requested: 100Effective limit: 100Raw fetched: 120Normalized: 107Filtered: 7Duplicates: 0Pushed: 100Pages fetched: 6Run status: SUCCEEDEDApify duration: 22 sRun cost: ~$0.007
The deployed Actor therefore returned 100 valid dataset items in a successful 22-second Apify run.
Under the assessment performance rubric:
Grade A: < 30 seconds
this observed run falls within Grade A.
The reported 22 seconds is the full duration displayed by Apify. The assessment timer starts at the first outbound request and stops when the 100th schema-conforming item is pushed, excluding Actor cold-start/build time. Therefore the assessment-defined request-to-result interval for this run cannot be greater than the reported full-run duration.
The observed compute/run cost extrapolates to approximately $0.07 per 1,000 results if cost scaled linearly. This is an estimate only: actual cost varies with filtering selectivity, number of pages, proxy traffic, retries, execution time, and Apify pricing.
Live validation summary
The deployed Actor has been successfully exercised for:
- FREE cap enforcement
- PAID entitlement
latesttop- multi-page pagination
- combined filters
- hashtag search/filtering
- canonical normalization
- residential proxy routing
- cross-run global deduplication
- overlapping-query global seen-set behavior
- optional finish webhook delivery
- 100-result end-to-end performance
The 100-result benchmark required six provider pages and completed without duplicates or rate-limit failure.
Global seen-set behavior was also validated live with overlapping runs. Globally seen items were excluded from the effective target, duplicate counters were reported, and pagination continued until new unique items satisfied the requested limit.
The optional finish webhook was validated live and delivered only the safe run summary metadata.
Empty-result and unavailable-account handling are covered by deterministic provider and pipeline tests. A final live empty-result validation could not be completed because the upstream TweetAPI request allowance was exhausted; the resulting HTTP 429 was correctly classified after bounded retries.
Compliance and production considerations
This project accesses public X data through an HTTP data provider and was built for the assessment.
Before deploying an equivalent system for a production client, I would review:
- X's current Terms of Service and developer/platform policies
- the upstream HTTP provider's terms and permitted uses
- applicable robots/access restrictions
- rate limits and acceptable request volume
- privacy and personal-data obligations
- data retention requirements
- jurisdiction-specific legal requirements
- downstream redistribution/storage restrictions
The Actor intentionally avoids personal logged-in X sessions and browser automation.
Public availability of a post should not by itself be treated as permission for unrestricted collection, retention, redistribution, or commercial reuse. Production deployment should be reviewed against the specific use case and current platform/provider terms.
Known limitations and trade-offs
The implementation intentionally uses TweetAPI as the assessment-permitted equivalent HTTP data source instead of depending on a personal X session or browser automation.
Direct guest-token SearchTimeline global search was investigated but was not used for the final extraction path. Consequently, X guest-token rotation is not implemented because the selected provider does not expose those tokens.
Instead, resilience is handled at the provider HTTP boundary with retries,
backoff, jitter, Retry-After support, timeouts, cursor pagination, response
classification, and configurable Apify residential proxy support.
Global cross-run deduplication is implemented through a separate private Apify KVS containing tweet IDs only. Because Apify KVS does not provide an atomic compare-and-set primitive for this design, simultaneous overlapping runs retain a small distributed race window; exactly-once delivery is therefore not claimed.
The optional finish webhook is implemented as best-effort delivery. Webhook failure does not invalidate an otherwise successful scraping run.
Empty result sets and known protected/private, suspended, deleted, or other unavailable-account provider entries are handled gracefully at the provider boundary. These cases are covered by deterministic tests. Exact future TweetAPI response representations cannot be exhaustively predicted and are not claimed as supported until verified.
The Actor also depends on the availability and request quota of the upstream HTTP provider. Provider-side rate limits or exhausted quotas are classified and handled by the retry policy, but cannot be bypassed by the Actor.
Delivery
Apify Actor
https://console.apify.com/actors/XaAxi9PSS9kTiOjcj
GitHub repository
https://gitlab.com/lucasacci/x-tweet-scraper#
Optional finish webhook
Set finishWebhookUrl to an absolute HTTP or HTTPS URL to receive the final
safe run summary after a successful run:
{ "searchTerms": ["openai"], "maxResults": 10, "finishWebhookUrl": "https://example.com/hooks/scraper" }
This bonus webhook is best-effort. It retries network, 429, and 5xx failures with a bounded budget and never fails an otherwise successful Actor run. Only safe summary metadata is sent; tweets, credentials, proxy URLs, and entitlement data are excluded.
Empty and unavailable accounts
Empty search pages complete successfully with zero dataset items and a normal
sourceExhausted summary. Known non-tweet or unavailable-account entries from
TweetAPI are skipped safely; valid tweets on the same page continue through the
pipeline. No tweet or ID is fabricated. Exact protected/suspended/deleted
representations remain provider-specific and depend on the upstream HTTP
response shape.
Optional global seen-set
The canonical Actor can maintain cross-run deduplication in the separate
private Apify KVS x-tweet-scraper-global-seen. Records are bucketed as
seen_<sha256-prefix> and contain only string tweet IDs, so overlapping queries
share the same set independently of query fingerprints. An ID is marked only
after a successful dataset push; failed writes do not poison the set. KVS
failures degrade to the existing same-run deduplication behavior. Apify KVS
does not provide an atomic compare-and-set here, so simultaneous overlapping
runs retain a small race window and exactly-once delivery is not claimed.
Bucket compaction/TTL is an operational retention task.