feat(apify): profile-level fallback for tweetless X accounts
Summary
X profile stats ride on tweet items (items[0].author), so an account with zero recent tweets succeeds on Apify with an empty dataset and persists nothing — the connected socials row stays stale forever. This adds a fallback: on an empty tweet dataset, recover the requested handle from the run INPUT and start a profile-level scrape (apidojo/twitter-user-scraper), whose results persist through the same webhook receiver.
Tracking issue: recoupable/chat#1851 (optional PR #3 — independent of api#755 and database#40).
What changed
lib/apify/twitter/startTwitterUserScraping.ts(new) — startsapidojo/twitter-user-scraper(resolved actor idV38PZzpEgOfeeWvZY) with{twitterHandles: [handle], maxItems: 1}+ the shared webhooks.lib/apify/twitter/handleTwitterUserScraperResults.ts(new) — persists the profile; reads the run INPUT to recover the requested handle and filters out the related-user padding the actor appends (verified live: a 1-handle request returned the user plus 9 similar accounts).lib/apify/twitter/handleTwitterProfileScraperResults.ts— empty-dataset branch: read INPUT handle → start the fallback → return{ social: null, fallbackRunId }. No-ops (unchanged behavior) when the payload has no key-value store id or the INPUT has no handle.lib/apify/getApifyResultHandler.ts— registry entry forV38PZzpEgOfeeWvZY.lib/apify/validateApifyWebhookRequest.ts— schema now passes through the optionalresource.defaultKeyValueStoreId(present on Apify's default payload; previously stripped by zod).
Verified live (2026-07-06, real Apify runs — not simulated)
| Claim | Evidence |
|---|---|
| Actor id resolves | GET /v2/acts/apidojo~twitter-user-scraper → V38PZzpEgOfeeWvZY |
| Output shape + padding behavior | live run with twitterHandles: ["ashnikko"] → requested user (followers: 239289, url: https://x.com/ashnikko) plus 9 related users → hence the handle filter |
twitterHandles input accepted | HTTP 201, run SUCCEEDED |
| Tweetless fixture behaves as described | the real handle that motivated this returns 0 items from the tweet actor; note it also returns 0 from the user scraper even with includeUnavailableUsers: true — that account appears deleted/renamed, so for it specifically no scraper can help; the fallback covers accounts that exist but don't tweet |
Tests (RED→GREEN)
New/updated suites failed before implementation (2 assertion failures + 2 module-not-found), pass after. vitest run lib/apify lib/socials → 128 tests passed. tsc --noEmit: 200 pre-existing errors on clean origin/test, 200 after — none new. eslint clean.
Pending preview verification
End-to-end webhook chain (empty tweet run → fallback run → profile persisted to the connected row) needs a preview/test-deployment pass with a real tweetless-but-live handle; will post the results table here.
🤖 Generated with Claude Code
Summary by cubic
Adds a profile-level fallback for tweetless X accounts so follower and bio stats still update. If the tweet scraper returns no items, we start apidojo/twitter-user-scraper for the handle and persist the profile via a new handler.
- New Features
- Empty dataset fallback: read the handle from the run INPUT (via
resource.defaultKeyValueStoreId) and startapidojo/twitter-user-scraperwithmaxItems: 1using shared webhooks. - Results handling: new handler filters out related-user padding, normalizes the
x.comprofile URL, and upserts only the requested handle’s profile tosocials. - Wiring: register actor id
V38PZzpEgOfeeWvZYin the result-handler map. - Webhook schema: accept optional
resource.defaultKeyValueStoreIdto read the run INPUT; no-op when missing or when the INPUT has no handle.
- Empty dataset fallback: read the handle from the run INPUT (via