You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Tracking issue for making the Apify social-scrape endpoints return recent per-post engagement instead of a single latest item. Perspective (out of scope here): internal weekly social measurement needs per-post X and YouTube Shorts numbers, and the X connector analytics path is a dead end (TWITTER_GET_POST_ANALYTICS → 403 client-not-enrolled; post lookup → 401) — the scrape pipeline is the only viable source, verified 2026-07-02.
Goal
POST /api/socials/{id}/scrape and POST /api/artist/socials/scrape accept an optional posts=N depth parameter (default 1, today's behavior) that flows into the platform Apify actor inputs, so a scrape of an X or YouTube profile returns the last N posts with engagement metrics in the GET /api/apify/runs/{runId} dataset:
X — apidojo/twitter-scraper-lite already returns likeCount, retweetCount, replyCount, quoteCount, bookmarkCount, and viewCount (impressions) per tweet; we just cap it at 1 item (api/lib/apify/twitter/startTwitterProfileScraping.ts L14: maxItems: 1).
Instagram already works (its profile actor bundles latestPosts — 12 posts with likes/comments/plays in one profile scrape); X and YouTube reach parity via the new param. Existing callers see zero behavior change.
api: credit-gate both scrape endpoints (5 + posts per profile) + auth on bulk
main (retargeted from test)
✅ merged 2026-07-02 — see Done
Merge sequencing: docs → api (documentation-driven; the OpenAPI change is the contract the api PR fulfills). api targets test, then test→main.
Context — verified 2026-07-02
All three platforms tested live via both POST /api/artist/socials/scrape (bulk) and POST /api/socials/{id}/scrape (direct); every run returned exactly 1 dataset item:
X (runs rMCM4jzjd9V9Y05mU, lbm3U2eqNzLpfCv8q): 1 item — the latest timeline entry (a retweet), but carrying full public metrics incl. viewCount: 699267. The account's own 4 posts from 2026-06-24 → 07-01 were not reachable.
YouTube (runs ZDGoi2IlDQj0KZL6k, 3OTYCnusYtUeUlrGU): 1 item — a long-form video from 2025-11-25; the channel's 4 Shorts published 2026-06-24 → 07-01 were all absent (maxResultsShorts: 0).
Instagram (run ewcxyNIY97dfQrdQb): profile item with latestPosts = 12 posts, each with likesCount/commentsCount/videoViewCount — the parity target.
Interim X workaround in use: the public syndication endpoint (cdn.syndication.twimg.com/tweet-result) — likes + conversation count only, no impressions.
Done
docs#258 — posts param on both scrape endpoints + per-platform dataset shapes.
✅ Shipped 2026-07-02 (merged to main, merge commit 615cf5d).
Adds the optional posts param (integer, 1–100) to POST /api/socials/{id}/scrape (query) and POST /api/artist/socials/scrape (body), expands the Scraper Results example with X tweet + YouTube video items, and moves the artist socials-scrape page into the Social Media nav group. Two rounds of review: descriptions reconciled with live preview behavior (YouTube depth is per content type — up to N videos + N Shorts; X returns latest N timeline items incl. RTs), then KISS'd per human review and default: 1 removed (omitted ≠ posts=1 on YouTube: omitted excludes Shorts, posts=1 includes one — a schema default would mislead SDK generators).
Verified: both OpenAPI files parse after every edit; contract semantics match the api#741 implementation exactly, which was itself verified live (see the preview verification).
api#741 — thread posts from the scrape routes into the platform actor inputs.
✅ Shipped 2026-07-02 (merged to test4290c72, promoted to main via api#7436ae6e79, test re-synced d5f649f — live on prod: posts=0 probe on api.recoupable.dev flipped 401→400 post-deploy, confirming the new validation is serving).
Optional posts (1–100) on both endpoints, validated before auth, threaded through scrapeProfileUrl/scrapeProfileUrlBatch into Twitter maxItems: posts ?? 1 and YouTube maxResults: posts ?? 1 / maxResultsShorts: posts ?? 0 (a requested depth includes Shorts; omitted keeps the byte-identical legacy input — unit-tested). TDD red→green (20 failing tests first); full suite 3745/3745.
Verified twice on preview (second time on the final head c868d45 via the authed path with a preview-env Privy token): X posts=20 → 20 tweets incl. pinned id with viewCount=188 (impressions); YouTube posts=20 → 40 items (20 videos + 20 Shorts) incl. all 4 pinned Shorts; all 400/401 paths. Final results table. Note: the older pinned tweet 2069618388926701915-era ids fall outside a 20-item timeline window (X depth counts RTs/replies) — documented in docs#258 rather than special-cased.
Open — contract first (shipped — see Done)
Follow-ups (shipped — see closure note)
Credit-gate the scrape endpoints (docs#259 + api#744) — 5 credits + 1 per requested post, per profile.
✅ Shipped 2026-07-02 (docs merged f70b9ff; api merged direct-to-main f45cd0f, test re-synced d72c826 — live on prod: unauthenticated bulk probe flipped 200→401 post-deploy).
Pricing from measured Apify cost (cost table: worst case YouTube ~$0.003 × 2×posts items; 5+posts keeps ≥1.75× margin at posts=100, 3–20× at default). Implementation mirrors the research family: ensureSocialScrapeCredits gates (402 + checkoutUrl, auto-recharge path) before any Apify run; deductSocialScrapeCredits deducts only for runs that started, never throws. POST /api/artist/socials/scrapegained auth + artist-access (was fully open). Docs: price lives on credits.mdx's What's-billed table (per review, not per-endpoint); OpenAPI documents the new 402s + bulk auth. TDD red→green (10 failures first); 324/324 affected-domain tests.
Verified on preview with balance reads around every call (results): default −5 exactly, posts=20 −25 exactly, bulk ×4 profiles −20 exactly; bulk 401 without auth; no-socials → [] with zero charge; 400s before auth. 402/true-403 unit-covered (test account is org-admin with ~1B credits).
Out of scope
LinkedIn per-post scrape. The LinkedIn path uses a person-profile actor (no post fields at all; verified against runs LfeuRTBHFITdh51SP, 9R4lnMCOCKigxFH46) and isn't in scrapeProfileUrl.ts's PLATFORM_SCRAPERS — supporting posts means selecting a different actor. Follow-up tracker if wanted; the LINKEDIN_LIST_REACTIONS connector action covers per-post reactions today.
Enrolling the X dev app for connector analytics (fixes the 403 at the OAuth-app level) — independent of this change, and the scrape path is strictly better (impressions without API-tier gating).
Deciding whether scrapes should charge credits — promoted to an Open item (2026-07-02, sweetman): credit-gating + Apify cost research now tracked above.
recoup-internal-social-ship-posts skill update to consume posts=N — lands in recoupable/skills after the api PR ships.
Architecture decisions
Default depth stays 1. Apify actors bill per dataset item; the existing scrape callers are profile-snapshot flows (onboarding, follower refresh) that don't need posts. Depth is strictly opt-in, so cost and behavior are unchanged unless a caller asks.
Scrape over connector for X metrics. The connector requires the linked X dev app to be enrolled in a paid API project for analytics (403 client-not-enrolled) and 401s on post lookup; the Apify actor returns richer public metrics (incl. impressions) with no per-account OAuth scope wall.
Tracking issue for making the Apify social-scrape endpoints return recent per-post engagement instead of a single latest item. Perspective (out of scope here): internal weekly social measurement needs per-post X and YouTube Shorts numbers, and the X connector analytics path is a dead end (
TWITTER_GET_POST_ANALYTICS→ 403client-not-enrolled; post lookup → 401) — the scrape pipeline is the only viable source, verified 2026-07-02.Goal
POST /api/socials/{id}/scrapeandPOST /api/artist/socials/scrapeaccept an optionalposts=Ndepth parameter (default1, today's behavior) that flows into the platform Apify actor inputs, so a scrape of an X or YouTube profile returns the last N posts with engagement metrics in theGET /api/apify/runs/{runId}dataset:apidojo/twitter-scraper-litealready returnslikeCount,retweetCount,replyCount,quoteCount,bookmarkCount, andviewCount(impressions) per tweet; we just cap it at 1 item (api/lib/apify/twitter/startTwitterProfileScraping.tsL14:maxItems: 1).streamers/youtube-scraperis capped at 1 long-form video and explicitly excludes Shorts (api/lib/apify/youtube/startYoutubeProfileScraping.tsL18-L19:maxResults: 1,maxResultsShorts: 0).Instagram already works (its profile actor bundles
latestPosts— 12 posts with likes/comments/plays in one profile scrape); X and YouTube reach parity via the new param. Existing callers see zero behavior change.PRs (updated 2026-07-02)
postsparam on both scrape endpoints + per-platform dataset shapes (OpenAPIsocial.json+releases.json)mainposts→ TwittermaxItems, YouTubemaxResults/maxResultsShorts(default 1/0 unchanged)testmainf70b9ff)main(retargeted fromtest)Context — verified 2026-07-02
All three platforms tested live via both
POST /api/artist/socials/scrape(bulk) andPOST /api/socials/{id}/scrape(direct); every run returned exactly 1 dataset item:rMCM4jzjd9V9Y05mU,lbm3U2eqNzLpfCv8q): 1 item — the latest timeline entry (a retweet), but carrying full public metrics incl.viewCount: 699267. The account's own 4 posts from 2026-06-24 → 07-01 were not reachable.ZDGoi2IlDQj0KZL6k,3OTYCnusYtUeUlrGU): 1 item — a long-form video from 2025-11-25; the channel's 4 Shorts published 2026-06-24 → 07-01 were all absent (maxResultsShorts: 0).ewcxyNIY97dfQrdQb): profile item withlatestPosts= 12 posts, each withlikesCount/commentsCount/videoViewCount— the parity target.Interim X workaround in use: the public syndication endpoint (
cdn.syndication.twimg.com/tweet-result) — likes + conversation count only, no impressions.Done
docs#258 —
postsparam on both scrape endpoints + per-platform dataset shapes.✅ Shipped 2026-07-02 (merged to
main, merge commit615cf5d).Adds the optional
postsparam (integer, 1–100) toPOST /api/socials/{id}/scrape(query) andPOST /api/artist/socials/scrape(body), expands the Scraper Results example with X tweet + YouTube video items, and moves the artist socials-scrape page into the Social Media nav group. Two rounds of review: descriptions reconciled with live preview behavior (YouTube depth is per content type — up to N videos + N Shorts; X returns latest N timeline items incl. RTs), then KISS'd per human review anddefault: 1removed (omitted ≠posts=1on YouTube: omitted excludes Shorts,posts=1includes one — a schema default would mislead SDK generators).Verified: both OpenAPI files parse after every edit; contract semantics match the api#741 implementation exactly, which was itself verified live (see the preview verification).
api#741 — thread
postsfrom the scrape routes into the platform actor inputs.✅ Shipped 2026-07-02 (merged to
test4290c72, promoted tomainvia api#7436ae6e79,testre-syncedd5f649f— live on prod:posts=0probe on api.recoupable.dev flipped 401→400 post-deploy, confirming the new validation is serving).Optional
posts(1–100) on both endpoints, validated before auth, threaded throughscrapeProfileUrl/scrapeProfileUrlBatchinto TwittermaxItems: posts ?? 1and YouTubemaxResults: posts ?? 1/maxResultsShorts: posts ?? 0(a requested depth includes Shorts; omitted keeps the byte-identical legacy input — unit-tested). TDD red→green (20 failing tests first); full suite 3745/3745.Verified twice on preview (second time on the final head
c868d45via the authed path with a preview-env Privy token): Xposts=20→ 20 tweets incl. pinned id withviewCount=188(impressions); YouTubeposts=20→ 40 items (20 videos + 20 Shorts) incl. all 4 pinned Shorts; all 400/401 paths. Final results table. Note: the older pinned tweet2069618388926701915-era ids fall outside a 20-item timeline window (X depth counts RTs/replies) — documented in docs#258 rather than special-cased.Open — contract first (shipped — see Done)
Follow-ups (shipped — see closure note)
✅ Shipped 2026-07-02 (docs merged
f70b9ff; api merged direct-to-mainf45cd0f,testre-syncedd72c826— live on prod: unauthenticated bulk probe flipped 200→401 post-deploy).Pricing from measured Apify cost (cost table: worst case YouTube ~$0.003 × 2×posts items; 5+posts keeps ≥1.75× margin at posts=100, 3–20× at default). Implementation mirrors the research family:
ensureSocialScrapeCreditsgates (402 +checkoutUrl, auto-recharge path) before any Apify run;deductSocialScrapeCreditsdeducts only for runs that started, never throws.POST /api/artist/socials/scrapegained auth + artist-access (was fully open). Docs: price lives on credits.mdx's What's-billed table (per review, not per-endpoint); OpenAPI documents the new 402s + bulk auth. TDD red→green (10 failures first); 324/324 affected-domain tests.Verified on preview with balance reads around every call (results): default −5 exactly, posts=20 −25 exactly, bulk ×4 profiles −20 exactly; bulk 401 without auth; no-socials →
[]with zero charge; 400s before auth. 402/true-403 unit-covered (test account is org-admin with ~1B credits).Out of scope
LfeuRTBHFITdh51SP,9R4lnMCOCKigxFH46) and isn't inscrapeProfileUrl.ts'sPLATFORM_SCRAPERS— supporting posts means selecting a different actor. Follow-up tracker if wanted; theLINKEDIN_LIST_REACTIONSconnector action covers per-post reactions today.Deciding whether scrapes should charge credits— promoted to an Open item (2026-07-02, sweetman): credit-gating + Apify cost research now tracked above.recoup-internal-social-ship-postsskill update to consumeposts=N— lands inrecoupable/skillsafter the api PR ships.Architecture decisions
client-not-enrolled) and 401s on post lookup; the Apify actor returns richer public metrics (incl. impressions) with no per-account OAuth scope wall.Source references
lib/apify/twitter/startTwitterProfileScraping.ts,lib/apify/youtube/startYoutubeProfileScraping.ts, routing inlib/apify/scrapeProfileUrl.ts.sweetman/posts-log.md→ "Measurement paths").