#!/usr/bin/env bun import { mkdirSync, writeFileSync } from 'fs' import { join } from 'path' const root = process.cwd() const outRoot = join(root, 'content_population_exports') mkdirSync(outRoot, { recursive: true }) 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 rows = [ { Provider: 'DataForSEO', Stage: '1', PricingItem: 'Amazon Reviews API standard queue', Unit: 'every 10 reviews', Price: '$0.00075', SourceUrl: 'https://dataforseo.com/apis/reviews-api/amazon-reviews-api', SourceCheckedAt: generatedAt, AppliesToPlan: 'Stage 1 uses depth=10 per ASIN, so the first 9-ASIN pilot should be estimated from 9 units of 10 reviews before any buffer.', Caveat: 'Official page uses USD pricing. Convert/record the working budget currency in data/sources/budgets/source-acquisition-cost-plan.json before approvals.' }, { Provider: 'DataForSEO', Stage: '1', PricingItem: 'Amazon Reviews API priority queue', Unit: 'every 10 reviews', Price: '$0.0015', SourceUrl: 'https://dataforseo.com/apis/reviews-api/amazon-reviews-api', SourceCheckedAt: generatedAt, AppliesToPlan: 'Priority queue is not the default pilot recommendation; use only if turnaround time matters more than cost.', Caveat: 'Keep the pilot in standard queue unless a human explicitly updates the cost plan and approval.' }, { Provider: 'DataForSEO', Stage: '1', PricingItem: 'Depth billing rule', Unit: '10-review increments', Price: 'round depth up to the next 10-review block', SourceUrl: 'https://dataforseo.com/help-center/get-amazon-reviews', SourceCheckedAt: generatedAt, AppliesToPlan: 'Depth=10 avoids accidental double billing; depth=11 would be charged as 20 reviews.', Caveat: 'Do not change depth from 10 in asin-map.csv without updating the cost plan and approval cap.' }, { Provider: 'Apify', Stage: '2', PricingItem: 'Actor compute units on Creator/standard public pricing reference', Unit: 'compute unit', Price: '$0.2 per CU', SourceUrl: 'https://apify.com/pricing/creator-plan', SourceCheckedAt: generatedAt, AppliesToPlan: 'Stage 2 must stay one actor run, first 3 targets, 3 requests per brand, then dataset inspection.', Caveat: 'Actor-specific runs can also incur proxy/data/storage usage; inspect actual run usage before any second run.' }, { Provider: 'Apify', Stage: '2', PricingItem: 'Actor usage drivers', Unit: 'memory x duration plus platform services', Price: 'varies by CU, transfer, proxy, storage', SourceUrl: 'https://docs.apify.com/platform/actors/running/usage-and-resources', SourceCheckedAt: generatedAt, AppliesToPlan: 'Browser actors and heavy pages cost more; Stage 2 should use Apify only for named gaps after DataForSEO review.', Caveat: 'Use the first dataset run as the real cost sample; do not estimate full-catalogue scraping from generic CU pricing.' } ] const manifest = { generatedAt, ready: true, note: 'Official pricing references for filling source-acquisition-cost-plan.json. This report does not unlock spend; budget, cost-plan, approval, preflight, and spend gates still control paid usage.', counts: { references: rows.length, providers: [...new Set(rows.map(row => row.Provider))].length, dataforseoReferences: rows.filter(row => row.Provider === 'DataForSEO').length, apifyReferences: rows.filter(row => row.Provider === 'Apify').length }, guardrails: [ 'Record current provider pricing source and timestamp before enabling budget caps.', 'Keep DataForSEO Stage 1 at depth=10 and 9 ASINs until raw quality and actual cost are reviewed.', 'Keep Apify Stage 2 behind a DataForSEO review decision and named gap.', 'This reference report is not a substitute for checking account billing pages before spend.' ], references: rows } writeFileSync(join(outRoot, 'source_acquisition_pricing_references.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8') writeFileSync(join(outRoot, 'source_acquisition_pricing_references.csv'), csv(rows, ['Provider', 'Stage', 'PricingItem', 'Unit', 'Price', 'SourceUrl', 'SourceCheckedAt', 'AppliesToPlan', 'Caveat']), 'utf8') writeFileSync(join(outRoot, 'source_acquisition_pricing_references.md'), [ '# Source acquisition pricing references', '', `Generated: ${generatedAt}`, '', manifest.note, '', '## Guardrails', ...manifest.guardrails.map(item => `- ${item}`), '', '## References', ...rows.map(row => `- ${row.Provider} Stage ${row.Stage}: ${row.PricingItem} — ${row.Price} per ${row.Unit}. Source: ${row.SourceUrl}. Caveat: ${row.Caveat}`), '', '## How to use this', '- Copy the relevant source URL, checked timestamp, unit cost, and estimated total into `data/sources/budgets/source-acquisition-cost-plan.json`.', '- Then rerun `bun run source:cost-plan`, `bun run source:approvals`, and `bun run source:spend-gate`.', '- Do not run provider APIs just because this reference file exists.', '' ].join('\n'), 'utf8') console.log(`Source acquisition pricing references ready: ${manifest.ready}`) console.log(`References: ${manifest.counts.references}`)