Tracking issue for two scrape-pipeline bugs that block sandbox agents from using their own scrape results, found 2026-07-02 while production-testing an artist's daily-briefing task. Sibling of chat#1839 (sandbox delivery fixes) — same failure shape (the agent does everything right; the platform drops the result), different subsystem.
Goal
- An agent authenticated with an account-scoped ephemeral key that starts a scrape via
POST /api/socials/{social_id}/scrape can poll that run's status and results via GET /api/apify/runs/{runId} — today it gets Forbidden because the endpoint is admin-only (api/lib/apify/validateGetScraperResultsRequest.ts calls validateAdminAuth).
- Successful TikTok and X/Twitter Apify runs persist their results (follower counts to
socials, post rows to posts/social_posts) the way YouTube runs already do, so GET /api/artists/{id}/posts and embedded account_socials counts reflect reality.
PRs (updated 2026-07-03)
| PR |
Item |
Base |
State |
| api#752 |
GET /api/apify/runs/{runId}: validateAdminAuth → validateAuthContext — any valid API key / Bearer can poll any run |
main |
✅ merged 2026-07-03, prod-verified |
| api#753 |
TikTok/X post persistence: shared persistPostsForSocial + handlers write posts/social_posts rows (socials counts + webhook registration already fixed by api#738/#740 under chat#1833) |
main |
✅ merged 2026-07-03, e2e-verified |
The two api PRs are independent — merge in either order. docs#262 closed unmerged: after the capability-model decision the existing docs page already matches the new behavior (it never documented the admin gate), so there is no contract drift to fix. database#39 (runId→account ownership map) was closed unmerged when the design moved to the capability model — see the architecture decision below. Preview note (2026-07-03): api preview deployments do not share the prod key store (a prod-minted key 200s on prod, 401s on previews), so key-based Done-when checks run against the test deployment post-merge — round-1 verification tables are on both api PRs.
Evidence (2026-07-02 production run, chat eac0b159-7473-4d72-a12b-f8bde3f084fc)
- Forbidden polls, 7/7 across two rounds. The agent kicked off 4 scrapes via
POST /api/socials/{social_id}/scrape with its ephemeral $RECOUP_API_KEY — all accepted, runIds returned (RMpBmxlGtptIyBnb3, 9TZRbi9gz2TgBMeMH, OeGhuGV3obde3CDGc, XBsWtbWYPlSnpWj5O; retry round 6fzZN5NjsVRIWS1Yi, HoJaej0Ue3PYafw5M, FulNVd3JxH2SeyQ9n). Every GET /api/apify/runs/{runId} with the same key returned {"status":"error","message":"Forbidden"}. The same polls with an admin token succeed. Root cause is by-design: validateGetScraperResultsRequest is admin-only ("Apify run identifiers are not account-scoped, and the poller is a backend-only caller (tasks)") — but the public docs (docs.recoupable.dev Scraper Results page), the recoup-platform-api-access skill, and existing customer task prompts (e.g. the OneRPM roster-wide YouTube report) all instruct agents to poll this endpoint. The asymmetry (can start, can't read) makes every scrape-dependent agent run fail its data step.
- TikTok/X results succeed on Apify but never persist. Runs
9AYX8xyaHWyHtnGtC (TikTok) and bx3asRqfbNnkKgogG (X) reached SUCCEEDED with real dataset items (the artist's TikTok posts and tweets, verified via admin poll 2026-07-02 ~23:10Z). The corresponding socials rows (d497a17b, 291753e9) kept followerCount: 0 and stale updated_at (2025-07-20); zero rows landed in posts/social_posts for the artist. By contrast the YouTube run from the same account persisted within minutes (socials.followerCount = 507, updated_at 2026-07-02 22:57), so the webhook/persist path (api/lib/apify/apifyWebhookHandler.ts + lib/apify/tiktok, lib/apify/twitter handlers) works for YouTube but not for TikTok/X runs started from POST /api/socials/{id}/scrape. Unknown whether the webhook isn't registered for those actors on this path or the handlers fail silently — first investigation step.
- Downstream symptom:
GET /api/artists/{id}/posts returned a 500 on first call, then {"posts": [], "total_count": 0} — the "What You've Been Posting" section of the briefing had nothing to read because nothing is ever written.
Done
Open — fixes (independent; pick either first)
Architecture decisions
- Open the endpoint to any authenticated key (decided 2026-07-03, supersedes the earlier owner-or-admin plan). Scrape datasets are public social content and Apify run ids are long random strings, so possession of a runId plus a valid credential is sufficient; authentication stays so anonymous callers can't use the endpoint as a free Apify proxy. This made the runId→account ownership map unnecessary — database#39 closed unmerged. Tightening later would break agents that depend on open reads, so this is the deliberate end state, not a stopgap.
Inherited gaps / Source references
- Sibling tracker: chat#1839 (web_fetch empty-body + ephemeral key refresh). This issue completes the "agent-blocking platform bugs" family surfaced by the 2026-07-02/03 task-email work.
- Known gaps noted, out of scope here: no API endpoint to link/unlink a social to an artist (Done item above needed direct data access);
POST /api/socials/{id}/scrape returns "Unsupported social media platform" for Spotify; POST /api/emails restricts accounts without a payment method to their own address (documented behavior discovered 2026-07-02 — correct, but surprising to test flows).
- Key files:
api/app/api/apify/runs/[runId]/route.ts, api/lib/apify/validateGetScraperResultsRequest.ts, api/lib/apify/apifyWebhookHandler.ts, api/lib/apify/getScraperResultsHandler.ts, api/lib/apify/tiktok/, api/lib/apify/twitter/, api/app/api/socials/[id]/scrape path.
Tracking issue for two scrape-pipeline bugs that block sandbox agents from using their own scrape results, found 2026-07-02 while production-testing an artist's daily-briefing task. Sibling of chat#1839 (sandbox delivery fixes) — same failure shape (the agent does everything right; the platform drops the result), different subsystem.
Goal
POST /api/socials/{social_id}/scrapecan poll that run's status and results viaGET /api/apify/runs/{runId}— today it getsForbiddenbecause the endpoint is admin-only (api/lib/apify/validateGetScraperResultsRequest.tscallsvalidateAdminAuth).socials, post rows toposts/social_posts) the way YouTube runs already do, soGET /api/artists/{id}/postsand embeddedaccount_socialscounts reflect reality.PRs (updated 2026-07-03)
GET /api/apify/runs/{runId}:validateAdminAuth→validateAuthContext— any valid API key / Bearer can poll any runmainpersistPostsForSocial+ handlers writeposts/social_postsrows (socials counts + webhook registration already fixed by api#738/#740 under chat#1833)mainEvidence (2026-07-02 production run, chat
eac0b159-7473-4d72-a12b-f8bde3f084fc)POST /api/socials/{social_id}/scrapewith its ephemeral$RECOUP_API_KEY— all accepted, runIds returned (RMpBmxlGtptIyBnb3,9TZRbi9gz2TgBMeMH,OeGhuGV3obde3CDGc,XBsWtbWYPlSnpWj5O; retry round6fzZN5NjsVRIWS1Yi,HoJaej0Ue3PYafw5M,FulNVd3JxH2SeyQ9n). EveryGET /api/apify/runs/{runId}with the same key returned{"status":"error","message":"Forbidden"}. The same polls with an admin token succeed. Root cause is by-design:validateGetScraperResultsRequestis admin-only ("Apify run identifiers are not account-scoped, and the poller is a backend-only caller (tasks)") — but the public docs (docs.recoupable.devScraper Results page), therecoup-platform-api-accessskill, and existing customer task prompts (e.g. the OneRPM roster-wide YouTube report) all instruct agents to poll this endpoint. The asymmetry (can start, can't read) makes every scrape-dependent agent run fail its data step.9AYX8xyaHWyHtnGtC(TikTok) andbx3asRqfbNnkKgogG(X) reachedSUCCEEDEDwith real dataset items (the artist's TikTok posts and tweets, verified via admin poll 2026-07-02 ~23:10Z). The correspondingsocialsrows (d497a17b,291753e9) keptfollowerCount: 0and staleupdated_at(2025-07-20); zero rows landed inposts/social_postsfor the artist. By contrast the YouTube run from the same account persisted within minutes (socials.followerCount = 507,updated_at 2026-07-02 22:57), so the webhook/persist path (api/lib/apify/apifyWebhookHandler.ts+lib/apify/tiktok,lib/apify/twitterhandlers) works for YouTube but not for TikTok/X runs started fromPOST /api/socials/{id}/scrape. Unknown whether the webhook isn't registered for those actors on this path or the handlers fail silently — first investigation step.GET /api/artists/{id}/postsreturned a 500 on first call, then{"posts": [], "total_count": 0}— the "What You've Been Posting" section of the briefing had nothing to read because nothing is ever written.Done
brauxelion.gd, Apifynot_found— account renamed) and TikTok was the non-canonicaltiktok.com/brauxelion. Linked the liveinstagram.com/@brauxelionandtiktok.com/@brauxelionsocials to the artist and unlinked the dead IG. Verified:account_socialsnow carries both live rows (+1 each) and the dead link is removed. Note: there is no API endpoint for linking/unlinking socials to an artist — tracked below as a gap.Open — fixes (independent; pick either first)
Open the scrape-results endpoint to any authenticated key. ✅ 2026-07-03 — api#752 merged to
main(first PR under the retired-test-branch flow, api#750) and verified on prod: a freshly minted non-admin agent key polled run9AYX8xyaHWyHtnGtC→ 200 SUCCEEDED with data (this exact call was 403 the same morning). Preview verification tables on the PR.validateGetScraperResultsRequestadmin-gates all polls — while docs/skills/prompts direct account-key agents to poll. Agents can commission scrapes they can never read.validateAdminAuth→validateAuthContext(any valid API key / Bearer passes; 401 stays for anonymous callers); no docs change needed — the existing page already matches (docs#262 closed unmerged). Unit tests: auth-error propagation, 400 bad runId, authenticated passthrough.Make TikTok/X scrape results persist like YouTube's. ✅ 2026-07-03 — socials counts shipped via api#738/Added tool result UI for web search #740 (chat#1833); posts rows via api#753, merged to
main(prod deploy from177c0410success). Verified live end-to-end against the production DB (previews share it): TikTok + X scrapes → webhook → 10+10posts/social_postsrows →GET /api/artists/{id}/postswent 49 (all Instagram) → 69 (49 ig + 10 x + 10 tiktok). Full results table on the PR. Review hardening:toIsoDateextracted to its own lib file; TikTok dates normalized so one malformedcreateTimeISOcan't fail an upsert batch.socials/postsuntouched (evidence above); YouTube's path persists. Suspect webhook registration or the per-platform result handlers (lib/apify/tiktok,lib/apify/twitter) on thePOST /api/socials/{id}/scrapepath.POST /api/apifyreceive it, which handler ran, what did it write; (2) fix the broken link (registration or handler mapping or payload-shape mismatch); (3) add a test per platform handler asserting a SUCCEEDED payload writesfollowerCount+ post rows.POST /api/socials/{id}/scrapeon a TikTok social and an X social → within one webhook cycle,socials.followerCount/updated_atare fresh andGET /api/artists/{id}/postsreturns the scraped posts.Architecture decisions
Inherited gaps / Source references
POST /api/socials/{id}/scrapereturns "Unsupported social media platform" for Spotify;POST /api/emailsrestricts accounts without a payment method to their own address (documented behavior discovered 2026-07-02 — correct, but surprising to test flows).api/app/api/apify/runs/[runId]/route.ts,api/lib/apify/validateGetScraperResultsRequest.ts,api/lib/apify/apifyWebhookHandler.ts,api/lib/apify/getScraperResultsHandler.ts,api/lib/apify/tiktok/,api/lib/apify/twitter/,api/app/api/socials/[id]/scrapepath.