Skip to content

Social scrape: optional posts=N depth so X + YouTube Shorts scrapes return per-post metrics (docs + api) #1836

Description

@sweetmantech

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:

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)

PR Item Base State
docs#258 docs: posts param on both scrape endpoints + per-platform dataset shapes (OpenAPI social.json + releases.json) main ✅ merged 2026-07-02 — see Done
api#741 api: plumb posts → Twitter maxItems, YouTube maxResults/maxResultsShorts (default 1/0 unchanged) test ✅ merged 2026-07-02 — see Done
docs#259 docs: bill social scrape on credits.mdx (5 + posts, per profile) + 402s + auth on bulk endpoint main ✅ merged 2026-07-02 (f70b9ff)
api#744 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 testmain.

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#258posts 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 test 4290c72, promoted to main via api#743 6ae6e79, test re-synced d5f649flive 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 d72c826live 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/scrape gained 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 creditspromoted 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.

Source references

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions