feat(socials): credit-gate both scrape endpoints (5 + posts credits) + auth on bulk#744
Conversation
Charges 5 credits plus 1 credit per requested post for each social profile scraped, priced from measured Apify costs (recoupable/chat#1836 cost research: worst case is YouTube at ~$0.003 × 2×posts items; 5+posts at 1 credit = $0.01 keeps ≥1.75x margin at posts=100). - getSocialScrapeCreditCost: 5 + (posts ?? 0) - ensureSocialScrapeCredits / deductSocialScrapeCredits: mirror the research family's gate (ensureCreditsOrShortCircuit → 402 with checkoutUrl) and deduct-on-success (recordCreditDeduction, never throws) - POST /api/socials/{id}/scrape: validator gates after auth+access; handler deducts only when the Apify run started (no charge on unsupported platform or scrape-start failure) - POST /api/artist/socials/scrape: gains auth (validateAuthContext) + artist access check (checkAccountArtistAccess) — it previously had none; gates on (5+posts) × linked profiles, deducts per profile whose scrape actually started; [] with no charge when the artist has no socials Contract: recoupable/docs#259. Closes the credit-gate item on recoupable/chat#1836. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 38 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (6)
📒 Files selected for processing (6)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
1 issue found across 12 files
Confidence score: 3/5
- In
lib/socials/deductSocialScrapeCredits.ts,deductSocialScrapeCreditsforwardscreditswithout validating for positive finite values, so a negative/zero/NaN input could create invalid billing ledger entries and incorrect customer balances if merged as-is — add a guard that rejects non-finite or non-positive amounts before calling the downstream deduction path.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="lib/socials/deductSocialScrapeCredits.ts">
<violation number="1" location="lib/socials/deductSocialScrapeCredits.ts:8">
P2: This billing wrapper doesn't validate the `credits` amount before passing it downstream. If an upstream caller ever passes a negative, zero, or non-finite value, it could write invalid ledger deductions without any runtime protection. Consider adding a defensive guard — for example, early-return (or log and return) when `credits` is not a positive finite integer — so a bug in a caller can't silently corrupt the credit ledger.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Client as "External Client"
participant Route as "API Route"
participant Auth as "validateAuthContext"
participant Access as "checkAccountArtistAccess"
participant Credits as "ensureSocialScrapeCredits"
participant Gate as "ensureCreditsOrShortCircuit"
participant Cost as "getSocialScrapeCreditCost"
participant Socials as "selectAccountSocials / selectSocials"
participant Apify as "scrapeProfileUrl / Batch"
participant Deduct as "deductSocialScrapeCredits"
participant Deduction as "recordCreditDeduction"
Note over Client,Deduction: "POST /api/socials/{id}/scrape (single profile)"
Client->>Route: "POST /api/socials/{id}/scrape?posts=N"
Route->>Auth: "validateAuthContext(request)"
Auth-->>Route: "authResult (accountId)"
Route->>Socials: "selectSocials(social_id)"
Socials-->>Route: "social record"
Route->>Access: "checkAccountArtistAccess(accountId, artistId)"
Access-->>Route: "boolean"
alt No access
Route-->>Client: "403 Unauthorized"
else Access granted
Route->>Cost: "getSocialScrapeCreditCost(posts)"
Cost-->>Route: "5 + (posts ?? 0)"
Route->>Credits: "ensureSocialScrapeCredits(accountId, cost)"
Credits->>Gate: "ensureCreditsOrShortCircuit(accountId, cost, successUrl)"
alt Insufficient credits
Gate-->>Credits: "402 NextResponse (checkoutUrl)"
Credits-->>Route: "402 response"
Route-->>Client: "402 Payment Required"
else Sufficient credits
Gate-->>Credits: "null"
Credits-->>Route: "null"
Route->>Apify: "scrapeProfileUrl(profile_url, username, posts)"
alt Unsupported platform
Apify-->>Route: "null"
Route-->>Client: "400 Unsupported"
else Scrape started
Apify-->>Route: "{ runId, datasetId }"
Route->>Deduct: "deductSocialScrapeCredits(accountId, cost)"
Deduct->>Deduction: "recordCreditDeduction(accountId, cost, \"api\")"
Note over Deduct: "Never throws, logs on failure"
Deduct-->>Route: "void"
Route-->>Client: "200 { runId, datasetId }"
end
end
end
Note over Client,Deduction: "POST /api/artist/socials/scrape (bulk)"
Client->>Route: "POST /api/artist/socials/scrape?posts=N"
Route->>Auth: "validateAuthContext(request)"
alt No auth
Auth-->>Route: "401 NextResponse"
Route-->>Client: "401 Unauthorized"
else Auth OK
Auth-->>Route: "authResult (accountId)"
Route->>Access: "checkAccountArtistAccess(accountId, artistId)"
alt No access
Route-->>Client: "403 Forbidden"
else Access granted
Route->>Socials: "selectAccountSocials(artistId)"
Socials-->>Route: "linked social profiles []"
alt No socials
Route-->>Client: "200 [] (no charge)"
else Has socials
Route->>Cost: "getSocialScrapeCreditCost(posts)"
Cost-->>Route: "5 + (posts ?? 0)"
Route->>Credits: "ensureSocialScrapeCredits(accountId, cost * N_profiles)"
Credits->>Gate: "ensureCreditsOrShortCircuit(accountId, totalCost, successUrl)"
alt Insufficient credits
Gate-->>Credits: "402 NextResponse"
Credits-->>Route: "402 response"
Route-->>Client: "402 Payment Required"
else Sufficient credits
Gate-->>Credits: "null"
Credits-->>Route: "null"
Route->>Apify: "scrapeProfileUrlBatch(profilesArray, posts)"
Apify-->>Route: "results (each with runId/datasetId/error)"
alt At least one scrape started
Route->>Deduct: "deductSocialScrapeCredits(accountId, cost * startedRuns)"
Deduct->>Deduction: "recordCreditDeduction(...)"
Note over Deduct: "Deduct only for profiles that started"
Deduct-->>Route: "void"
end
Route-->>Client: "200 [results]"
end
end
end
end
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| * never thrown — a billing hiccup must not fail a scrape that is already | ||
| * running (mirrors the research family's deduction). | ||
| */ | ||
| export async function deductSocialScrapeCredits(accountId: string, credits: number): Promise<void> { |
There was a problem hiding this comment.
P2: This billing wrapper doesn't validate the credits amount before passing it downstream. If an upstream caller ever passes a negative, zero, or non-finite value, it could write invalid ledger deductions without any runtime protection. Consider adding a defensive guard — for example, early-return (or log and return) when credits is not a positive finite integer — so a bug in a caller can't silently corrupt the credit ledger.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/socials/deductSocialScrapeCredits.ts, line 8:
<comment>This billing wrapper doesn't validate the `credits` amount before passing it downstream. If an upstream caller ever passes a negative, zero, or non-finite value, it could write invalid ledger deductions without any runtime protection. Consider adding a defensive guard — for example, early-return (or log and return) when `credits` is not a positive finite integer — so a bug in a caller can't silently corrupt the credit ledger.</comment>
<file context>
@@ -0,0 +1,18 @@
+ * never thrown — a billing hiccup must not fail a scrape that is already
+ * running (mirrors the research family's deduction).
+ */
+export async function deductSocialScrapeCredits(accountId: string, credits: number): Promise<void> {
+ try {
+ await recordCreditDeduction({
</file context>
Preview verification (2026-07-02) ✅Preview:
Not triggerable live on this account, covered by unit tests: 402 (balance is ~1B credits) and 403 (caller is RECOUP_ORG admin, so Docs reconciliation (docs#259, second commit Merge order: docs#259 → this. |
Summary
Implements the credit-gate item on recoupable/chat#1836, priced per the Apify cost research. Contract: recoupable/docs#259 — merge order: docs → api.
Pricing: 5 credits + 1 per requested post, per profile scraped (1 credit = $0.01). Worst-case Apify cost is YouTube at
$0.003 × 2×posts items ($0.60 at posts=100) → 105 credits keeps ≥1.75× margin; default scrape (5 credits) has 3–20× margin on every platform.lib/socials/getSocialScrapeCreditCost.ts5 + (posts ?? 0)lib/socials/ensureSocialScrapeCredits.tsensureCreditsOrShortCircuit→ 402 withcheckoutUrl(auto-recharge path included)lib/socials/deductSocialScrapeCredits.tsrecordCreditDeduction; never throws (mirrors research)POST /api/socials/{id}/scrapePOST /api/artist/socials/scrapevalidateAuthContext→ 401) + artist access (checkAccountArtistAccess→ 403) — previously fully open (audit); gates on (5+posts) × linked profiles, deducts per profile whose scrape startedTests (TDD, red → green)
All new behavior written test-first and confirmed failing (10 failures across 6 files) before implementation:
getSocialScrapeCreditCost: 5 / 6 / 25 / 105ensureSocialScrapeCredits/deductSocialScrapeCredits: gate passthrough, 402 short-circuit, never-throws deductionaccount_id; ensure called with 5 (default) / 25 (posts=20); 402 short-circuit; deduct on success only (not on unsupported platform or failed start)[]with zero charge when no socialsAffected domains (
lib/socials,lib/artist,lib/apify,lib/credits): 324/324 pass. Full suite passes;tsc --noEmitunchanged at 200 pre-existing errors (none new); ESLint clean.Preview verification to follow as a PR comment.
🤖 Generated with Claude Code
Summary by cubic
Adds credit gating to both social scrape endpoints at 5 credits + 1 per requested post per profile, and adds auth + artist access checks to the bulk artist scrape. Aligns the API with the cost model and docs in recoupable/docs#259 and closes the credit-gate requirement in recoupable/chat#1836.
New Features
checkoutUrl; cost = 5 + posts per profile.POST /api/socials/{id}/scrape: validates auth and artist access, forwardsposts, deducts only if the Apify run starts.POST /api/artist/socials/scrape: now requires auth and artist access; gates on (5 + posts) × linked profiles; deducts per profile that starts; returns [] with no charge when no socials.Migration
recoupable/docs#259).Written for commit 5dc4678. Summary will update on new commits.