Puente Talent Technical Challenge
Pricing
Pay per usage
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
6 days ago
Last modified
Categories
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.
searchTermsis rejected because guest authentication cannot accessSearchTimeline.
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 cinpm run verify
The repository uses Prettier with spaces and a visual tab width of two characters:
npm run formatnpm 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 buildnpm run start:http
The API listens at http://127.0.0.1:3000 by default.
| Method | Route | Purpose |
|---|---|---|
GET | /health | Process liveness. |
GET | /ready | Admission capacity and SQLite readiness. |
POST | /v1/scrapes | Execute the complete challenge input. |
GET | /v1/tweets/:tweetId | Hydrate one public tweet. |
GET | /v1/users/:username | Return a normalized public profile. |
GET | /v1/users/:username/tweets | Scrape one author's timeline. |
GET | /v1/storage/tweets | List stored tweets with keyset pagination. |
GET | /v1/storage/tweets/:tweetId | Read one stored tweet without contacting X. |
GET | /v1/storage/runs | List runs and their sanitized inputs. |
GET | /v1/storage/stats | Return counts, WAL mode, and SQLite schema version. |
GET | /docs/ | Interactive Swagger UI. |
GET | /docs/json | OpenAPI 3.0.3 as JSON. |
GET | /docs/yaml | OpenAPI 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 upbash scripts/project.sh credentialsbash scripts/project.sh logsbash 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 LRActor[Apify Actor input] --> UseCase[ScrapeTweetsUseCase]HTTP[Fastify endpoints] --> UseCaseUseCase --> 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] --> GateState[Apify persisted state] --> UseCaseSQLite --> 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
ResultSinkcontract; tests use substitutes with the same observable behavior. - Interface segregation:
XDataSource,ResultSink,EntitlementResolver,TweetReadRepository,ScrapeRunRepository,ProxyUrlProvider,Clock, andLoggerare small contracts. - Dependency inversion:
ScrapeTweetsUseCasedepends on those ports, never directly on Apify, Fastify, orundici.
Request Flow
src/main.tsinitializes the Apify runtime and validates input through the platform schema and Zod.- Runner
userId,actorId, andactorRunIdcome from the execution environment, never from input. - The entitlement resolver calls a signed service or private KVS. Failure, missing configuration, or unknown identity fails closed to free tier.
- Each author handle is resolved through
UserByScreenName; the resulting ID feedsUserTweetsorUserTweetsAndReplies. Explicit tweet IDs useTweetResultByRestId. - 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.
- Each response is parsed, normalized, schema-validated, filtered with AND semantics, and sent to the protected result sink.
- The sink serializes deduplication and capacity reservation. Concurrency, a large
maxResults, or extra input fields cannot bypass the cap. - Actor mode pushes tweets to Dataset. HTTP mode commits accepted data in short SQLite transactions; network calls never run inside a database transaction.
- The summary reports
succeeded,partial, orfailed, 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:
UserByScreenNameresolves a handle and supplies normalized author fields.UserTweetsreads a public author's recent timeline.UserTweetsAndRepliesis selected whenincludeRepliesis enabled.TweetResultByRestIdhydrates 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
- Public X web-client operations were compared with metadata maintained by
fa0311/twitter-openapi: operation names, query IDs, variables, and feature flags. - A guest session was activated through
/1.1/guest/activate.jsonwith the public web-client bearer, keeping guest token and proxy IP together as one session. - Candidate operations were replayed directly over HTTP. Only operations returning public data without account cookies were accepted.
- Node variants, cursors, and tombstones were checked against live responses before defining the parser and normalization contracts.
SearchTimelinewas excluded after confirming its authenticated-session requirement.- 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:
ENTITLEMENTS_ENDPOINTplusENTITLEMENTS_SIGNING_SECRET: an HMAC-signed POST containingactorId,actorRunId,runnerUserId, timestamp, and nonce.ENTITLEMENTS_KVS_ID: the owner's private KVS with anENTITLEMENTSrecord 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.sqliteis the main database.x-tweet-scraper.sqlite-walholds committed write-ahead-log pages awaiting checkpoint.x-tweet-scraper.sqlite-shmcoordinates 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-statsbash 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:sqliteis 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
maxResultsnormalized 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 cinpm run verify
Use the repository as the Actor source in Apify Console:
- Open Actors, select Develop new, and choose GitHub as the source.
- Connect GitHub and select
Bellota22/puente-talent-technical-challengewithmasteras the default branch. - Confirm that Apify detects .actor/actor.json and the root Dockerfile, then start the build.
- 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:
- Resolve the runner ID with
GET https://api.apify.com/v2/users/meusing the same Bearer token. - In Code > Environment variables, set secret
ENTITLEMENTS_KVS_IDto a new KVS name such aspuente-owner-entitlements-v1. LeaveENTITLEMENTS_ENDPOINTandENTITLEMENTS_SIGNING_SECRETunset. - Build and run once. The Actor creates the named KVS; this bootstrap run remains free because the allow-list record does not exist yet.
- Open that Actor-created store under Storage > Key-value stores and add a JSON record with key
ENTITLEMENTS:
{"paidUserIds": ["YOUR_APIFY_USER_ID"]}
- Run again with the same user's token. The ID in
paidUserIdsis the exactdata.idreturned 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:
- Dataset contains normalized tweet rows and never exceeds the authorized cap.
- 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/runsPOST https://api.apify.com/v2/actors/kbbsiDsoqSdAeTkza/run-sync-get-dataset-itemsGET 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 && echocurl --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.jsonunset 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 loginnpx 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:
- src/domain/input.ts: strict Zod input, defaults, required targets, dates,
and explicit
searchTermsrejection. - src/domain/tweet.ts: exact output contract, including required
nullfields and rejection of accidental properties. - src/application/ports.ts: layer boundaries.
- src/application/scrape-tweets.ts: target orchestration, pagination, filtering, bounded work, errors, and summary.
- src/application/gated-result-sink.ts: serialized entitlement gate, deduplication, and emission cap.
- src/infrastructure/x/x-graphql-client.ts: GraphQL HTTP calls, retry budget, jittered backoff, and 403/429 rotation.
- src/infrastructure/x/guest-session-manager.ts: binds a guest token to one proxy session.
- src/infrastructure/x/operation-registry.ts: checked-in snapshot plus bounded opt-in remote refresh.
- src/infrastructure/x/response-parser.ts and src/infrastructure/x/tweet-normalizer.ts: cursor/node extraction and final schema normalization.
- src/application/storage.ts: segregated storage ports, query types, and persisted-input allowlist.
- src/infrastructure/storage/sqlite-scrape-store.ts: SQLite facade delegating schema, SQL, codecs, and utilities to adjacent modules.
- src/infrastructure/security/redaction.ts: recursive redaction and bounded error messages.
- src/config.ts: typed HTTP configuration and external-bind secret rules.
- src/main.ts: Actor adapter,
Actor.init(), input, migration state, Dataset, and output. - src/http-server.ts: Fastify adapter, admission, deadline, authentication, access logs, and HTTP persistence.
- src/openapi.ts: shared OpenAPI schemas, errors, and executable Swagger examples.
- .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:
| Path | Observed result |
|---|---|
npm run verify | 93/93 tests, Prettier, strict TypeScript, and Node build passed. |
npm audit --omit=dev | Zero production dependency vulnerabilities. |
docker compose up --build --wait | API and sidecar healthy on apify/actor-node:24. |
| Container user, root, and secrets | UID 100, read-only root, no mounted keys in docker inspect. |
/docs/, /docs/json, and /docs/yaml | Swagger UI and OpenAPI 3.0.3 with executable examples. |
| SQLite migration, WAL, rollback, UPSERT, cursor, and secret | Persistent volume, transactions, and secret canaries verified. |
apify-cli validate-schema and regression tests | Input, Draft-07 Dataset, Output, and local references pass. |
Actor, fromUsers: ["apify"], maxResults: 10 | 40 fetched, 20 matched, 10 emitted, zero errors, 2.824 seconds. |
Docker GET /v1/users/apify | 401 without a key; normalized profile with the correct key. |
Docker POST /v1/scrapes, maxResults: 3 | Three items, succeeded, persisted in SQLite, zero errors. |
Docker POST /v1/scrapes, maxResults: 1000, free | 10 items, partial, limited: true, reason: free_tier. |
| Remote resolver against HMAC sidecar | Paid tier, signature, expiry, and replay protection verified. |
Live runtime, paid entitlement, maxResults: 100 | 100 items in 9.385 s; 222 fetched, 104 matched, no truncation/errors. |
GET /v1/tweets/2090084776988262560 | Hydrated 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
| Requirement | Status | Evidence or note |
|---|---|---|
| Node.js, strict TypeScript, Apify SDK v3 | Complete | package.json, tsconfig.json, and src/main.ts. |
| Native Actor, schemas, Dataset, and Docker | Complete and built | Validated schemas and Node 24 image. |
| No browser automation | Complete | undici only; no Playwright, Puppeteer, or Selenium. |
| Author timeline, tweet by ID, and profile | Complete and tested | Guest operations and local/Docker smoke tests. |
| Inputs, AND filters, and inclusive dates | Complete | Strict Zod contract and domain/filter.ts. |
searchTerms | Stretch goal not implemented | Explicit rejection; SearchTimeline requires user authentication. |
sortBy: latest | Partial across multiple targets | Per-timeline order; no global chronological merge yet. |
sortBy: top | Transparent partial behavior | Ranking over a bounded scan; summary reports ranking_window. |
Exact output, null, HTML, and expanded links | Complete and tested | Includes normal and media t.co URL expansion. |
| Dataset, cursors, and global deduplication | Complete | Apify adapter, per-author state, and global gate. |
| Guest token, rotation, 403/429/5xx retry, and jitter | Complete and tested | Direct GraphQL client tests and deterministic substitutes. |
| Configurable Apify Proxy | Complete in code | Sticky session per target with coordinated rotation. |
| Migration and resume | Implemented; live test pending | Actor.useState, persistState, and migrating. |
| Authoritative fail-closed free cap | Complete; production source pending | Signed local sidecar; owner endpoint/private KVS needed on Apify. |
| Required tests and additional coverage | Complete | 93/93 across domain, network, HTTP, OpenAPI, security, and SQLite. |
| Documented guest-operation research | Complete | Reproducible method and checked-in snapshot described above. |
| Public source repository | Complete | GitHub repository. |
| Apify cloud build and free-tier smoke run | Complete | Private Actor built and emitted 10 capped Dataset items. |
| Public Store page and shareable run URL | Pending publication | Publish or share the private Actor with reviewers. |
| Residential benchmark for 100 results | Partial | 100 live results verified; repeat on Apify residential proxy. |
| Cost per 1,000 results and completion webhook | Optional work pending | Measure 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: latestpreserves 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: topranks only the bounded scanned window and reports that limitation in the summary.node:sqliteis 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:
- Apify SDK for JavaScript quick start
- Actor lifecycle
- Actor definition
- Input schema
- Dataset schema
- Output schema
- State persistence and migration
- Apify storage and retention
- Secret environment variables
- Proxy configuration
- Synchronous Actor endpoint
- Apify SDK repository
- Crawlee repository
- Official Actor templates
- Fastify Swagger
- Fastify Swagger UI
- Prettier configuration
- Node.js
node:sqlite - SQLite write-ahead logging
Unofficial reference for X's volatile internal protocol:
- fa0311/twitter-openapi, used as a metadata research source rather than a code dependency or official X API.