#!/usr/bin/env bun import { mkdirSync, readFileSync, existsSync, writeFileSync } from 'fs' import { join } from 'path' const root = process.cwd() const outRoot = join(root, 'content_population_exports') mkdirSync(outRoot, { recursive: true }) function readJson(path: string, fallback: any = null) { if (!existsSync(path)) return fallback return JSON.parse(readFileSync(path, 'utf8').replace(/^\uFEFF/, '')) } function csv(rows: any[], headers: string[]) { const esc = (value: any) => { if (value === undefined || value === null) value = '' if (Array.isArray(value)) value = value.join(' | ') value = String(value) return /[",\n\r]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value } return [headers.join(','), ...rows.map(row => headers.map(header => esc(row[header])).join(','))].join('\n') + '\n' } const generatedAt = new Date().toISOString() const refs = readJson(join(outRoot, 'source_acquisition_pricing_references.json'), { references: [] }) const pilot = readJson(join(outRoot, 'dataforseo_pilot_manifest.json'), {}) const apify = readJson(join(outRoot, 'apify_gapfill_manifest.json'), {}) const budgetGate = readJson(join(outRoot, 'source_acquisition_budget_gate.json'), {}) const taskCount = Number(pilot.counts?.stageOneTargets || 9) const depth = Number(pilot.guardrails?.recommendedDepth || 10) const reviewBlocksPerTask = Math.ceil(depth / 10) const standardUnitUsd = 0.00075 const priorityUnitUsd = 0.0015 const apifyCuUsd = 0.2 const apifyFirstTargets = Number(apify.counts?.firstBatchTargets || 3) const apifyRequestsPerBrand = Number((apify.targets || [])[0]?.MaxRequestsPerBrand || 3) const apifyRunCount = 1 const rows = [ { Provider: 'DataForSEO', Stage: '1', Scenario: 'standard_queue_exact_9_asin_depth_10', Formula: `${taskCount} ASIN tasks x ${reviewBlocksPerTask} ten-review block x $${standardUnitUsd}`, EstimatedUsd: +(taskCount * reviewBlocksPerTask * standardUnitUsd).toFixed(6), SuggestedCapUsd: 1, SpendStatus: 'blocked_until_asins_budget_cost_plan_approval_preflight', Use: 'Recommended first paid pilot if all 9 ASINs are confirmed. The suggested cap is deliberately much larger than the exact API estimate to allow provider/account minimums while still preventing broad spend.', SourceUrl: 'https://dataforseo.com/apis/reviews-api/amazon-reviews-api' }, { Provider: 'DataForSEO', Stage: '1', Scenario: 'priority_queue_reference_only', Formula: `${taskCount} ASIN tasks x ${reviewBlocksPerTask} ten-review block x $${priorityUnitUsd}`, EstimatedUsd: +(taskCount * reviewBlocksPerTask * priorityUnitUsd).toFixed(6), SuggestedCapUsd: 1, SpendStatus: 'not_recommended_without_explicit_speed_need', Use: 'Reference only. Priority doubles the unit price; standard queue is enough for cost-controlled evidence collection.', SourceUrl: 'https://dataforseo.com/apis/reviews-api/amazon-reviews-api' }, { Provider: 'DataForSEO', Stage: '1', Scenario: 'depth_safety_rule', Formula: `depth ${depth} stays at ${reviewBlocksPerTask} ten-review block; depth 11 would round to 2 blocks`, EstimatedUsd: +(taskCount * reviewBlocksPerTask * standardUnitUsd).toFixed(6), SuggestedCapUsd: 1, SpendStatus: 'keep_depth_10', Use: 'Protects against accidental review-depth cost increase. Do not change depth without updating the cost plan and approval.', SourceUrl: 'https://dataforseo.com/help-center/get-amazon-reviews' }, { Provider: 'Apify', Stage: '2', Scenario: 'one_actor_first_3_targets_reference_cap', Formula: `${apifyRunCount} actor run x first ${apifyFirstTargets} targets x ${apifyRequestsPerBrand} requests/brand; CU cost depends on actual run`, EstimatedUsd: 0, SuggestedCapUsd: 5, SpendStatus: 'blocked_until_dataforseo_review_names_gap', Use: 'Suggested small cap for the first dataset inspection only. Actual actor/proxy/storage cost must be read from Apify run usage before any second run.', SourceUrl: 'https://docs.apify.com/platform/actors/running/usage-and-resources' }, { Provider: 'Apify', Stage: '2', Scenario: 'compute_unit_reference', Formula: `$${apifyCuUsd} per CU; exact CU = memory x runtime plus platform usage`, EstimatedUsd: 0, SuggestedCapUsd: 5, SpendStatus: 'reference_only', Use: 'CU reference for filling the cost plan after choosing the actor and before one first-batch run.', SourceUrl: 'https://apify.com/pricing/creator-plan' } ] const standard = rows[0] const manifest = { generatedAt, ready: true, note: 'No-spend scenario estimates for choosing a small local max-spend cap. This does not enable provider spend.', counts: { scenarios: rows.length, dataforseoScenarios: rows.filter(row => row.Provider === 'DataForSEO').length, apifyScenarios: rows.filter(row => row.Provider === 'Apify').length, stageOneTasks: taskCount, reviewDepth: depth, pricingReferences: refs.counts?.references || 0 }, recommendation: { providerOrder: 'DataForSEO first; Apify only after a named post-pilot gap.', dataforseoStage1: `Use standard queue for ${taskCount} ASINs at depth ${depth}; exact official-reference estimate is $${standard.EstimatedUsd}, but set any local max-spend cap manually after checking account minimums/billing.`, apifyStage2: 'Do not fund Apify yet. If needed later, cap the first run tightly and inspect actual usage before any second run.' }, guardrails: [ 'Scenario estimates are not approval and do not unlock spend.', 'DataForSEO payload remains empty until ASINs are confirmed.', 'Apify remains blocked until DataForSEO review selects run_apify_gapfill and names a gap.', 'Use these rows to fill source-acquisition-cost-plan.json; then rerun cost, approval, preflight, and spend gates.' ], scenarios: rows, budgetState: budgetGate.budgetAllowed || {} } writeFileSync(join(outRoot, 'source_acquisition_cost_scenarios.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8') writeFileSync(join(outRoot, 'source_acquisition_cost_scenarios.csv'), csv(rows, ['Provider', 'Stage', 'Scenario', 'Formula', 'EstimatedUsd', 'SuggestedCapUsd', 'SpendStatus', 'Use', 'SourceUrl']), 'utf8') writeFileSync(join(outRoot, 'source_acquisition_cost_scenarios.md'), [ '# Source acquisition cost scenarios', '', `Generated: ${generatedAt}`, '', manifest.note, '', `Recommendation: ${manifest.recommendation.providerOrder}`, '', '## Guardrails', ...manifest.guardrails.map(item => `- ${item}`), '', '## Scenarios', ...rows.map(row => `- ${row.Provider} Stage ${row.Stage} / ${row.Scenario}: ${row.Formula} => estimate $${row.EstimatedUsd}; suggested local cap reference $${row.SuggestedCapUsd}. Status: ${row.SpendStatus}.`), '', '## Next use', '- Keep spend blocked until ASINs/payload are ready.', '- If choosing to subscribe later, fill `data/sources/budgets/source-acquisition-cost-plan.json` from these scenarios and current account pricing, then rerun all gates.', '' ].join('\n'), 'utf8') console.log(`Source acquisition cost scenarios ready: ${manifest.ready}`) console.log(`Scenarios: ${manifest.counts.scenarios}`) console.log(`DataForSEO standard estimate USD: ${standard.EstimatedUsd}`)