Puente Talent Technical Challenge avatar

Puente Talent Technical Challenge

Pricing

Pay per usage

Go to Apify Store
Puente Talent Technical Challenge

Puente Talent Technical Challenge

Unofficial browserless scraper for public X posts by author or tweet ID.

Pricing

Pay per usage

Rating

0.0

(0)

Developer

gabriel angel villanueva vega

gabriel angel villanueva vega

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

6 days ago

Last modified

Share

X Tweet Scraper

A browserless Apify Actor and HTTP API for extracting public posts from X. It uses Node.js, strict TypeScript, Apify SDK v3, undici, guest tokens, and the internal GraphQL operations that power X's public web views.

Verified status, August 19, 2026: the project builds, passes 93 automated tests, and has been exercised against X locally and in Docker for author timelines, user profiles, and tweet lookup by ID. The native Actor was built on Apify and a cloud smoke run returned 10 free-tier results. A local live paid-entitlement run returned 100 results. The source repository is published at Bellota22/puente-talent-technical-challenge. Wiring the owner's cloud entitlement, running the residential-proxy benchmark, and publishing the currently private Actor remain deployment tasks. searchTerms is rejected because guest authentication cannot access SearchTimeline.

What Is Implemented

  • Native Apify Actor lifecycle, input/output schemas, Dataset output, KVS state, and migration hooks.
  • Public author timelines and tweet lookup through X GraphQL without Playwright, Puppeteer, or Selenium.
  • Strict normalization, AND-combined filters, deduplication, cursor pagination, and bounded retries.
  • An authoritative free-tier gate that emits at most 10 tweets unless a trusted resolver grants paid access.
  • A Fastify API with executable OpenAPI 3.0.3 documentation and sanitized structured logs.
  • A transactional SQLite read model for the standalone HTTP mode.
  • Docker Compose, a one-command Bash workflow, Prettier with two-space indentation, 93 tests, and a Node.js 24 GitHub Actions verification workflow.

The original assignment is available in senior-x-scraper-test-v2.pdf. The challenge compliance matrix distinguishes completed, partial, and deployment-dependent work.

Quick Start

Requirements: Node.js 22.16.0 or newer and npm 10 or newer. Node.js 24 LTS is recommended and matches the Docker image.

npm ci
npm run verify

The repository uses Prettier with spaces and a visual tab width of two characters:

npm run format
npm run format:check

A local Actor run without an INPUT record uses the safe default target fromUsers: ["apify"]:

$npm start

Local Apify output is written to storage/datasets/default; the run summary is written to storage/key_value_stores/default/OUTPUT.json. To use a different input, place the contents of examples/input.json in storage/key_value_stores/default/INPUT.json before starting the Actor.

HTTP API And Swagger

The HTTP adapter reuses the same application use cases as the Actor:

npm run build
npm run start:http

The API listens at http://127.0.0.1:3000 by default.

MethodRoutePurpose
GET/healthProcess liveness.
GET/readyAdmission capacity and SQLite readiness.
POST/v1/scrapesExecute the complete challenge input.
GET/v1/tweets/:tweetIdHydrate one public tweet.
GET/v1/users/:usernameReturn a normalized public profile.
GET/v1/users/:username/tweetsScrape one author's timeline.
GET/v1/storage/tweetsList stored tweets with keyset pagination.
GET/v1/storage/tweets/:tweetIdRead one stored tweet without contacting X.
GET/v1/storage/runsList runs and their sanitized inputs.
GET/v1/storage/statsReturn counts, WAL mode, and SQLite schema version.
GET/docs/Interactive Swagger UI.
GET/docs/jsonOpenAPI 3.0.3 as JSON.
GET/docs/yamlOpenAPI 3.0.3 as YAML.

Open Swagger UI to execute routes with ready-to-run example values. Loopback mode does not require a service API key. The Authorize dialog accepts x-api-key, x-storage-token, and an optional Apify token used only to resolve entitlement.

curl -X POST http://127.0.0.1:3000/v1/scrapes \
-H "content-type: application/json" \
-d '{"fromUsers":["apify"],"maxResults":10}'

Without an authoritative entitlement, a run is treated as free and cannot emit more than 10 tweets. Full configuration is documented in .env.example. A non-loopback bind requires an HTTP_API_KEY of at least 32 characters. Request-selected proxy configuration is rejected unless a trusted deployment explicitly enables HTTP_ALLOW_INPUT_PROXY=true.

Docker Compose

docker-compose.yml is the recommended way to review the complete standalone API. Compose preserves the Actor batch CMD in the Dockerfile, but starts dist/server.js for Fastify and Swagger. It publishes only to 127.0.0.1, runs as a non-root user, uses a read-only root filesystem, and persists SQLite and local Apify storage in named volumes.

bash scripts/project.sh up
bash scripts/project.sh credentials
bash scripts/project.sh logs
bash scripts/project.sh down

up builds both services, creates local secrets under .secrets/, and waits for health checks. credentials prints the values used by Swagger. down preserves named volumes. The API and signed entitlement sidecar are a local review topology; the published batch Actor uses Dataset/KVS and does not expose Swagger. See docs/DEPLOYMENT.md for PowerShell instructions, paid testing, secret handling, all helper commands, and cleanup behavior.

Architecture

The project uses hexagonal architecture, also known as ports and adapters, with domain, application, and infrastructure boundaries. The codebase is small enough that a dependency injection framework would add little value; src/runtime.ts is the explicit composition root.

flowchart LR
Actor[Apify Actor input] --> UseCase[ScrapeTweetsUseCase]
HTTP[Fastify endpoints] --> UseCase
UseCase --> XPort[XDataSource port]
XPort --> XClient[X GraphQL HTTP client]
XClient --> Guest[Guest token + proxy session]
XClient --> Registry[Operation registry]
UseCase --> Filter[AND filters]
Filter --> Gate[EntitlementGatedResultSink]
Gate --> Dataset[Apify Dataset]
Gate --> SQLite[SQLite transactional read model]
Gate --> Response[Current HTTP response]
Entitlement[Remote service or protected KVS] --> Gate
State[Apify persisted state] --> UseCase
SQLite --> Admin[Protected storage endpoints]

SOLID Decisions

  • Single responsibility: validation, filtering, normalization, networking, entitlement, persistence, and HTTP transport live in separate modules.
  • Open/closed: another data source, result sink, or entitlement provider can implement a port without changing the use case.
  • Liskov substitution: Apify Dataset, SQLite, and the in-memory sink honor the same ResultSink contract; tests use substitutes with the same observable behavior.
  • Interface segregation: XDataSource, ResultSink, EntitlementResolver, TweetReadRepository, ScrapeRunRepository, ProxyUrlProvider, Clock, and Logger are small contracts.
  • Dependency inversion: ScrapeTweetsUseCase depends on those ports, never directly on Apify, Fastify, or undici.

Request Flow

  1. src/main.ts initializes the Apify runtime and validates input through the platform schema and Zod.
  2. Runner userId, actorId, and actorRunId come from the execution environment, never from input.
  3. The entitlement resolver calls a signed service or private KVS. Failure, missing configuration, or unknown identity fails closed to free tier.
  4. Each author handle is resolved through UserByScreenName; the resulting ID feeds UserTweets or UserTweetsAndReplies. Explicit tweet IDs use TweetResultByRestId.
  5. A guest token is acquired with the same proxy session used for GraphQL. Each author paginates sequentially, while independent authors and tweet IDs use bounded concurrency. A 403 or 429 rotates both token and sticky proxy session.
  6. Each response is parsed, normalized, schema-validated, filtered with AND semantics, and sent to the protected result sink.
  7. The sink serializes deduplication and capacity reservation. Concurrency, a large maxResults, or extra input fields cannot bypass the cap.
  8. Actor mode pushes tweets to Dataset. HTTP mode commits accepted data in short SQLite transactions; network calls never run inside a database transaction.
  9. The summary reports succeeded, partial, or failed, including truncation reasons, discarded rows, and completed/failed/pending targets. An all-target failure becomes HTTP 502, not an optimistic 200 or misleading 404.

X GraphQL

X's public web client calls private GraphQL operations identified by an operationName and a versioned queryId. This project reproduces only the public guest flow over HTTP:

  • UserByScreenName resolves a handle and supplies normalized author fields.
  • UserTweets reads a public author's recent timeline.
  • UserTweetsAndReplies is selected when includeReplies is enabled.
  • TweetResultByRestId hydrates a single public tweet.

SearchTimeline is intentionally unsupported because X requires an authenticated user session. The Actor does not use personal accounts or attempt to bypass that boundary; non-empty searchTerms produces a clear validation error.

Query IDs are not a supported public API and can change without notice. The default registry is a reviewed snapshot committed in src/infrastructure/x/operations-snapshot.ts for deterministic builds. X_OPERATION_REGISTRY_URL enables an optional remote registry whose payload is validated, coalesced, rate-limited by cooldown, bounded by timeout, and capped at 2 MiB.

How Guest Operations Were Identified

  1. Public X web-client operations were compared with metadata maintained by fa0311/twitter-openapi: operation names, query IDs, variables, and feature flags.
  2. A guest session was activated through /1.1/guest/activate.json with the public web-client bearer, keeping guest token and proxy IP together as one session.
  3. Candidate operations were replayed directly over HTTP. Only operations returning public data without account cookies were accepted.
  4. Node variants, cursors, and tombstones were checked against live responses before defining the parser and normalization contracts.
  5. SearchTimeline was excluded after confirming its authenticated-session requirement.
  6. Approved IDs were committed to the snapshot. Remote refresh stays opt-in because a mutable third-party source must not change production behavior silently.

fa0311/twitter-openapi is an unofficial research reference, not a runtime dependency or an official X API. The snapshot makes builds reproducible; it cannot make X's internal protocol stable.

Entitlement And Threat Model

The Actor supports two authoritative sources, neither accepted from input:

  1. ENTITLEMENTS_ENDPOINT plus ENTITLEMENTS_SIGNING_SECRET: an HMAC-signed POST containing actorId, actorRunId, runnerUserId, timestamp, and nonce.
  2. ENTITLEMENTS_KVS_ID: the owner's private KVS with an ENTITLEMENTS record shaped as { "paidUserIds": ["user-id"] }.

Secrets and KVS access belong in the official Actor's secure environment. A source fork does not inherit them and therefore fails closed. A user can remove limits from their own fork of public code; no check embedded in a public repository can prevent that. The defensible boundary is that a fork cannot impersonate the commercial service or acquire its entitlements.

In HTTP mode, Authorization: Bearer <APIFY_TOKEN> is sent only to /v2/users/me to resolve the runner identity. It is cached by hash with a bounded TTL/LRU policy and is never logged, persisted, or sent to X. HTTP_API_KEY controls access to the service but does not grant paid entitlement.

Storage

The standalone HTTP server uses data/x-tweet-scraper.sqlite by default. Actor batch mode does not: an Actor may migrate between hosts, so Dataset and KVS are its authoritative durable stores.

There is one logical SQLite database. Files ending in -wal and -shm are SQLite runtime sidecars, not extra databases:

  • x-tweet-scraper.sqlite is the main database.
  • x-tweet-scraper.sqlite-wal holds committed write-ahead-log pages awaiting checkpoint.
  • x-tweet-scraper.sqlite-shm coordinates readers and the writer using shared memory.

Do not delete or copy these files individually while the server is running. A clean shutdown checkpoints and truncates the WAL. All data/*.sqlite files are ignored by Git because they are local databases or exported snapshots, not application source. In Compose, the live database is inside the named volume x-tweet-scraper_scraper_data; a host file under ./data is separate and may be empty. Inspect or export the real volume database with:

bash scripts/project.sh db-stats
bash scripts/project.sh db-export

db-export uses node:sqlite backup() while the service remains online and writes a consistent, dated snapshot under ./data/. Open that snapshot in DB Browser for SQLite.

The implementation is split by responsibility even though SQLite stores one physical database:

  • sqlite-scrape-store.ts: adapter facade implementing application ports.
  • sqlite-schema.ts: migrations, tables, and indexes.
  • sqlite-statements.ts: prepared statements.
  • sqlite-codecs.ts: row mapping, validation, and opaque cursors.
  • sqlite-utils.ts: transactions, permissions, and SQLite conversions.

SQLite uses STRICT tables, foreign keys, prepared statements, WAL, BEGIN IMMEDIATE, observation-ordered UPSERTs, keyset pagination, and configurable 30-day retention. Abandoned runs are marked interrupted at startup. The persisted-input allowlist excludes proxy configuration, authorization, Apify/X tokens, guest cookies, HMAC signatures, upstream responses, and raw exceptions. SQLite is not encrypted by this application; use an encrypted volume and the backup policy in docs/STORAGE.md.

Storage routes are administrative. When exposed, they require an independent STORAGE_READ_TOKEN through x-storage-token; otherwise they remain loopback-only or disabled.

Capacity And Deployment Modes

  • Apify Actor: every run uses an isolated process, Dataset, and KVS. It does not use SQLite. Concurrency and scale are governed by Apify account, memory, proxy, and platform limits.
  • Standalone HTTP: one process defaults to exactly two active remote scrape jobs through HTTP_MAX_CONCURRENT_SCRAPES=2. A third simultaneous scrape is rejected immediately with HTTP 429; it is not placed in an unbounded queue.
  • SQLite: node:sqlite is synchronous and the process shares one connection. WAL lets readers coexist with the writer, but SQLite still has one writer and does not provide horizontal scale.
  • Memory: each synchronous HTTP scrape retains up to maxResults normalized items for its response. Large paid exports are better served by an Actor Dataset.

No load test has been performed, so this repository makes no RPS or SLA claim. The observed 9.385-second live run for 100 results implies only a rough best-case ceiling of about 12 comparable heavy runs per minute at concurrency two (2 * 60 / 9.385), before upstream throttling, retries, proxy latency, storage contention, or CPU/memory overhead. It is an illustration, not a capacity commitment. Horizontal HTTP scale requires a managed database plus distributed admission and rate limiting; the natural scale-out path for this challenge is isolated Apify Actor runs.

Deploy To Apify

Run the complete preflight before publishing:

npm ci
npm run verify

Use the repository as the Actor source in Apify Console:

  1. Open Actors, select Develop new, and choose GitHub as the source.
  2. Connect GitHub and select Bellota22/puente-talent-technical-challenge with master as the default branch.
  3. Confirm that Apify detects .actor/actor.json and the root Dockerfile, then start the build.
  4. Confirm that the build finishes and the input, Dataset, and output schemas render correctly.

The current private deployment is available to authorized collaborators at Apify Console. Its stable Actor ID is kbbsiDsoqSdAeTkza; its API alias is gab27~puente-talent-technical-challenge.

A URL such as https://<run-host>.runs.apify.net is only the temporary container URL for one run. This Actor is a batch job and exits after writing Dataset and KVS output, so opening that URL after SUCCEEDED correctly reports that the run has already finished. It is not the Actor page or a result URL. Use the stable Console link above, the run link from the Runs tab, or the Dataset API.

The API token authenticates the runner but does not itself grant this application's paid entitlement. For an owner-only smoke test that preserves Apify's Limited permissions:

  1. Resolve the runner ID with GET https://api.apify.com/v2/users/me using the same Bearer token.
  2. In Code > Environment variables, set secret ENTITLEMENTS_KVS_ID to a new KVS name such as puente-owner-entitlements-v1. Leave ENTITLEMENTS_ENDPOINT and ENTITLEMENTS_SIGNING_SECRET unset.
  3. Build and run once. The Actor creates the named KVS; this bootstrap run remains free because the allow-list record does not exist yet.
  4. Open that Actor-created store under Storage > Key-value stores and add a JSON record with key ENTITLEMENTS:
{
"paidUserIds": ["YOUR_APIFY_USER_ID"]
}
  1. Run again with the same user's token. The ID in paidUserIds is the exact data.id returned by /v2/users/me, not an API token, username, or email address.

This bootstrap order matters: a Limited-permission Actor cannot read an arbitrary pre-existing user KVS, but it retains access to a named store that it created. Without a reachable authoritative KVS or signed endpoint, the Actor intentionally fails closed to the free cap of 10.

For third-party public runners, use the signed remote entitlement endpoint as the production-safe path. Access to an owner's private KVS depends on the credentials available to each run, so the KVS setup must not be assumed to authorize every customer execution automatically.

Run this smoke input from Apify Console:

{
"fromUsers": ["apify"],
"tweetIds": [],
"searchTerms": [],
"hashtags": [],
"includeReplies": false,
"includeRetweets": false,
"sortBy": "latest",
"maxResults": 100,
"proxyConfiguration": {
"useApifyProxy": true
}
}

Verify both output surfaces after the run:

  1. Dataset contains normalized tweet rows and never exceeds the authorized cap.
  2. The default KVS OUTPUT record contains the honest run summary, including status, requested, fetched, matched, pushed, limited, entitlement, partialReasons, and target counts.

Repeat with the residential proxy group required by the challenge. Record wall-clock time from the first request through the 100th Dataset push, memory, compute units, proxy transfer, and Dataset item count. Save the public Actor URL and one successful run URL as review evidence. Never place an API token in the repository, input example, screenshots, or video.

The recommended public interface is the batch Actor because every invocation has an isolated run, Dataset, KVS state, and entitlement counter. Apify exposes it through:

POST https://api.apify.com/v2/actors/kbbsiDsoqSdAeTkza/runs
POST https://api.apify.com/v2/actors/kbbsiDsoqSdAeTkza/run-sync-get-dataset-items
GET https://api.apify.com/v2/datasets/<datasetId>/items

Send the token in Authorization: Bearer ..., never in Actor input or a committed URL. The example examples/apify-100.json is ready for the synchronous endpoint:

read -rsp 'Apify API token: ' APIFY_TOKEN && echo
curl --fail-with-body --location \
'https://api.apify.com/v2/actors/kbbsiDsoqSdAeTkza/run-sync-get-dataset-items?clean=true&limit=100' \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H 'Content-Type: application/json' \
--data-binary @examples/apify-100.json
unset APIFY_TOKEN

The synchronous endpoint returns Dataset items directly but has a maximum HTTP wait of 300 seconds. Fastify remains useful for local review or a separate service deployment. Standby mode is disabled because sharing one process across callers complicates per-run isolation and commercial caps.

An optional CLI deployment from the repository root is also available:

npx apify-cli login
npx apify-cli push

This README does not claim that the cloud Actor has already been deployed; update the verification and compliance sections only after the Apify build and smoke run succeed. The complete procedure, including the residential input and output checklist, is in docs/DEPLOYMENT.md.

Repository Tour

Read the implementation in this order:

  1. src/domain/input.ts: strict Zod input, defaults, required targets, dates, and explicit searchTerms rejection.
  2. src/domain/tweet.ts: exact output contract, including required null fields and rejection of accidental properties.
  3. src/application/ports.ts: layer boundaries.
  4. src/application/scrape-tweets.ts: target orchestration, pagination, filtering, bounded work, errors, and summary.
  5. src/application/gated-result-sink.ts: serialized entitlement gate, deduplication, and emission cap.
  6. src/infrastructure/x/x-graphql-client.ts: GraphQL HTTP calls, retry budget, jittered backoff, and 403/429 rotation.
  7. src/infrastructure/x/guest-session-manager.ts: binds a guest token to one proxy session.
  8. src/infrastructure/x/operation-registry.ts: checked-in snapshot plus bounded opt-in remote refresh.
  9. src/infrastructure/x/response-parser.ts and src/infrastructure/x/tweet-normalizer.ts: cursor/node extraction and final schema normalization.
  10. src/application/storage.ts: segregated storage ports, query types, and persisted-input allowlist.
  11. src/infrastructure/storage/sqlite-scrape-store.ts: SQLite facade delegating schema, SQL, codecs, and utilities to adjacent modules.
  12. src/infrastructure/security/redaction.ts: recursive redaction and bounded error messages.
  13. src/config.ts: typed HTTP configuration and external-bind secret rules.
  14. src/main.ts: Actor adapter, Actor.init(), input, migration state, Dataset, and output.
  15. src/http-server.ts: Fastify adapter, admission, deadline, authentication, access logs, and HTTP persistence.
  16. src/openapi.ts: shared OpenAPI schemas, errors, and executable Swagger examples.
  17. .actor/actor.json, INPUT_SCHEMA.json, .actor/dataset_schema.json, and .actor/output_schema.json: Apify platform contract.

For deeper review, see docs/ARCHITECTURE_REVIEW.md and docs/STORAGE.md.

Verification Evidence

Local and Docker checks performed on August 19, 2026, without a proxy unless specified:

PathObserved result
npm run verify93/93 tests, Prettier, strict TypeScript, and Node build passed.
npm audit --omit=devZero production dependency vulnerabilities.
docker compose up --build --waitAPI and sidecar healthy on apify/actor-node:24.
Container user, root, and secretsUID 100, read-only root, no mounted keys in docker inspect.
/docs/, /docs/json, and /docs/yamlSwagger UI and OpenAPI 3.0.3 with executable examples.
SQLite migration, WAL, rollback, UPSERT, cursor, and secretPersistent volume, transactions, and secret canaries verified.
apify-cli validate-schema and regression testsInput, Draft-07 Dataset, Output, and local references pass.
Actor, fromUsers: ["apify"], maxResults: 1040 fetched, 20 matched, 10 emitted, zero errors, 2.824 seconds.
Docker GET /v1/users/apify401 without a key; normalized profile with the correct key.
Docker POST /v1/scrapes, maxResults: 3Three items, succeeded, persisted in SQLite, zero errors.
Docker POST /v1/scrapes, maxResults: 1000, free10 items, partial, limited: true, reason: free_tier.
Remote resolver against HMAC sidecarPaid tier, signature, expiry, and replay protection verified.
Live runtime, paid entitlement, maxResults: 100100 items in 9.385 s; 222 fetched, 104 matched, no truncation/errors.
GET /v1/tweets/2090084776988262560Hydrated tweet, 16 root fields, one emitted result, zero errors.

The 100-item live test verifies pagination, cardinality, and paid gating. It does not replace the challenge benchmark inside Apify with a residential proxy, measured from the first request through the 100th Dataset push.

Technical Challenge Compliance

RequirementStatusEvidence or note
Node.js, strict TypeScript, Apify SDK v3Completepackage.json, tsconfig.json, and src/main.ts.
Native Actor, schemas, Dataset, and DockerComplete and builtValidated schemas and Node 24 image.
No browser automationCompleteundici only; no Playwright, Puppeteer, or Selenium.
Author timeline, tweet by ID, and profileComplete and testedGuest operations and local/Docker smoke tests.
Inputs, AND filters, and inclusive datesCompleteStrict Zod contract and domain/filter.ts.
searchTermsStretch goal not implementedExplicit rejection; SearchTimeline requires user authentication.
sortBy: latestPartial across multiple targetsPer-timeline order; no global chronological merge yet.
sortBy: topTransparent partial behaviorRanking over a bounded scan; summary reports ranking_window.
Exact output, null, HTML, and expanded linksComplete and testedIncludes normal and media t.co URL expansion.
Dataset, cursors, and global deduplicationCompleteApify adapter, per-author state, and global gate.
Guest token, rotation, 403/429/5xx retry, and jitterComplete and testedDirect GraphQL client tests and deterministic substitutes.
Configurable Apify ProxyComplete in codeSticky session per target with coordinated rotation.
Migration and resumeImplemented; live test pendingActor.useState, persistState, and migrating.
Authoritative fail-closed free capComplete; production source pendingSigned local sidecar; owner endpoint/private KVS needed on Apify.
Required tests and additional coverageComplete93/93 across domain, network, HTTP, OpenAPI, security, and SQLite.
Documented guest-operation researchCompleteReproducible method and checked-in snapshot described above.
Public source repositoryCompleteGitHub repository.
Apify cloud build and free-tier smoke runCompletePrivate Actor built and emitted 10 capped Dataset items.
Public Store page and shareable run URLPending publicationPublish or share the private Actor with reviewers.
Residential benchmark for 100 resultsPartial100 live results verified; repeat on Apify residential proxy.
Cost per 1,000 results and completion webhookOptional work pendingMeasure or implement from real Apify runs; do not estimate blindly.

The core evaluation surface is implemented. The submission should not be described as fully closed until the Actor is published, the owner's entitlement source is configured, and the residential benchmark is attached.

Limitations And Next Steps

  • X may change query IDs, feature flags, or response shapes without notice. Monitor public web bundles and update the reviewed snapshot deliberately.
  • sortBy: latest preserves order within each scanned timeline, but concurrently processed targets are not merged into one globally stable chronology. A strict merge needs round-robin cursor scheduling and a larger buffer.
  • sortBy: top ranks only the bounded scanned window and reports that limitation in the summary.
  • node:sqlite is synchronous and the HTTP response retains its own result set in memory. Use Dataset/KVS or a managed database for large exports, high RPS, or multiple replicas.
  • SQLite does not encrypt data at rest and must not be placed on NFS/SMB. Protect volumes, backups, retention, and administrative endpoints.
  • Public HTTP deployment needs gateway-level distributed quotas, key rotation, and audit trails in addition to the process-local admission limit.
  • Protected, suspended, deleted, or unavailable accounts/posts can yield no items and a target error without terminating unrelated targets.
  • Before operating at scale, review X terms, robots guidance, lawful basis, data minimization, retention/deletion, privacy, and customer-specific contractual limits.
  • Do not publish a time-to-100 or cost-per-1,000 figure until it is measured in Apify with the required residential proxy.

References

Official documentation:

Unofficial reference for X's volatile internal protocol: