The Royalty Reconciliation Tool
What you'll learn
~30 min- Build a CLI that ingests royalty CSVs from multiple distributors
- Understand royalty calculation mechanics including per-stream rates and territory splits
- Match and reconcile payments across platforms
- Generate a summary report with discrepancy flags
What you’re building
You got paid last quarter. DistroKid says one number. TuneCore says another. CD Baby’s report looks completely different. Spotify for Artists shows stream counts that don’t match what any distributor is reporting. You open three spreadsheets, squint at column headers that are all named differently, and try to figure out if the numbers add up.
They usually don’t. Independent artists lose an estimated 10-25% of their royalties to reporting errors, timing mismatches, and calculation mistakes. Not fraud — just the messy reality of music distribution. Different distributors report different metrics, use different column names, cover different time periods, and handle currency conversion differently. Nobody is trying to cheat you. The system is just complicated enough that mistakes slip through.
You’re going to build a tool that reads CSVs from multiple distributors, normalizes everything into a common format, cross-references the numbers, and flags anything that looks off. One command, one report, no more guessing.
If you have 50K monthly streams, you’re earning roughly $150-250/month depending on platform mix and territory. A 15% reporting error is $20-35/month you’re leaving on the table. Over a year, that’s $250-400. Over a catalog of 20 tracks across 5 years? The cumulative losses add up fast. This tool pays for itself the first time it catches a discrepancy.
Why royalties are confusing
Before you build the tool, it helps to understand why reconciliation is hard in the first place:
- Different column names: DistroKid calls it “Total Streams.” TuneCore calls it “Quantity.” CD Baby calls it “Units.” Same data, different headers.
- Different time periods: One distributor reports monthly. Another reports quarterly. Payment dates lag 2-3 months behind the actual streams.
- Per-stream rates vary: Spotify pays roughly $0.003-0.005 per stream in the US, but rates differ by territory, subscription tier (free vs. premium), and even time of year.
- Currency conversion: International streams get paid in local currency and converted. The exchange rate used varies by distributor and payment date.
- Track name matching: Your distributor might list “JADED” while another lists “Jaded” or “JADED (Original Mix).” Same track, different strings.
Your tool handles all of this automatically.
Ingest data from multiple sources with different schemas, normalize to a common format, cross-reference, and flag discrepancies. This pattern is used in accounting, banking, inventory management — anywhere multiple systems track the same underlying data.
The prompt
Start your AI CLI tool and paste this prompt:
Build a Node.js CLI tool for reconciling music royalty data across multipledistributors. The tool should be a structured project, not a single file.
PROJECT STRUCTURE:royalty-tool/ package.json src/ cli.js (entry point, argument parsing) parsers/ distrokid.js (DistroKid CSV parser) tunecore.js (TuneCore CSV parser) cdbaby.js (CD Baby CSV parser) auto-detect.js (header-pattern-based format detection) reconciler.js (cross-platform matching and comparison) reporter.js (HTML report generation) sample-data.js (generates realistic test CSVs) templates/ report.html (Handlebars-style HTML template for the summary report)
REQUIREMENTS:
1. CLI INTERFACE (src/cli.js) - Usage: node src/cli.js [options] <csv-files...> - Accept multiple CSV file paths as arguments - --output or -o: output directory for the report (default: ./royalty-report) - --period or -p: reporting period label (e.g., "Q4 2025") - --generate-sample: generate sample CSV files instead of reconciling - Use csv-parser npm package for CSV reading
2. AUTO-DETECTION (src/parsers/auto-detect.js) - Read the first row (headers) of each CSV - DistroKid headers include: "Reporting Date", "Sale Period", "Store", "Quantity", "Song/Album", "Country of Sale", "Earnings (USD)" - TuneCore headers include: "Posted Date", "Sale/Return", "Platform", "Quantity", "Release Title", "Song Title", "Country", "Total Earned" - CD Baby headers include: "Date", "Distributor", "Subtotal (USD)", "Quantity", "Artist", "Title", "UPC" - Match header patterns (case-insensitive, partial match) to select the right parser - If no format is recognized, print an error with the detected headers so the user can report it
3. PARSERS (src/parsers/*.js) - Each parser normalizes CSV rows to a common schema: { track_title: string, // normalized track name platform: string, // "Spotify", "Apple Music", etc. streams: number, // play/download count revenue: number, // USD amount territory: string, // country code or name period: string, // YYYY-MM format source: string // "distrokid", "tunecore", "cdbaby" } - Normalize track titles: trim whitespace, collapse multiple spaces, strip common suffixes like "(Original Mix)", "(Radio Edit)" - Normalize platform names: map variants to canonical names ("Spotify" not "spotify" or "Spotify Premium") - Parse revenue strings: strip currency symbols, handle commas, convert to float - Parse dates to YYYY-MM format regardless of input format
4. RECONCILER (src/reconciler.js) - Group normalized records by track_title + platform + period - For each group, compare across sources: - Stream count difference (absolute and percentage) - Revenue difference (absolute and percentage) - Flag discrepancies where: - Revenue is >10% below expected (based on known per-stream rates) - Stream counts differ >15% between distributors for the same platform - A track appears in one distributor but not another - Known per-stream rates (approximate, for flagging only): - Spotify: $0.003-0.005 per stream - Apple Music: $0.007-0.01 per stream - Amazon Music: $0.003-0.005 per stream - YouTube Music: $0.002-0.004 per stream - Tidal: $0.008-0.013 per stream - Calculate expected revenue from stream counts and flag if actual revenue falls below the low end of the range
5. REPORTER (src/reporter.js) - Generate an HTML report with these sections: a. Summary: total revenue per platform, total streams, reporting period b. Per-track breakdown: table with track name, platform, streams, revenue, revenue-per-stream for each distributor source c. Discrepancy table: rows where flags were triggered, with the discrepancy type, expected vs actual values, and severity (warning = 10-20% off, alert = >20% off) d. Missing tracks: tracks that appear in one source but not another - Dark theme: background #0a0a0a, cards #141414, borders #262626, text #e5e5e5, accent #f97316 - Include totals row at the bottom of each table - Currency values formatted with $ and 2 decimal places - Responsive layout
6. SAMPLE DATA GENERATOR (src/sample-data.js) - When --generate-sample is passed, create 3 CSV files in the output directory: - sample-distrokid.csv - sample-tunecore.csv - sample-cdbaby.csv - Simulate ~50K monthly streams across 5 platforms for an artist with 6 tracks - Introduce realistic discrepancies: - One track with 18% fewer streams in TuneCore than DistroKid - One track missing entirely from CD Baby - Revenue that's 12% below expected for one platform - Use realistic distributor header formats for each CSV
DEPENDENCIES: csv-parser, handlebars (for HTML template rendering)The --generate-sample flag creates CSVs with known discrepancies baked in. When you run the reconciler on sample data, you know exactly what flags should appear. If the tool catches all three planted discrepancies, it’s working correctly. This is a built-in sanity check.
What you get
After your AI CLI tool finishes, you’ll have a project folder:
royalty-tool/ package.json src/ cli.js parsers/ distrokid.js tunecore.js cdbaby.js auto-detect.js reconciler.js reporter.js sample-data.js templates/ report.htmlSet it up
cd royalty-toolnpm installTry it with sample data first
node src/cli.js --generate-sample -o ./test-reportThis creates three sample CSVs in ./test-report/. Now reconcile them:
node src/cli.js -p "Q4 2025" -o ./test-report \ ./test-report/sample-distrokid.csv \ ./test-report/sample-tunecore.csv \ ./test-report/sample-cdbaby.csvOpen ./test-report/report.html in your browser. You should see:
- Summary with total revenue broken down by Spotify, Apple Music, Amazon, YouTube Music, and Tidal
- Per-track breakdown showing each track’s streams and revenue from each distributor
- Discrepancy table with at least 3 flags (the ones planted in the sample data):
- A stream count mismatch (~18% difference between distributors)
- A missing track (present in DistroKid and TuneCore but absent from CD Baby)
- A revenue flag (actual revenue 12% below expected based on per-stream rates)
If all three discrepancies show up in the report, your tool is working.
With your real data
- Export your royalty CSV from DistroKid (Account → Payments → Download CSV)
- Export your royalty CSV from TuneCore (Reports → Earnings → Export)
- Run the tool with your real files:
node src/cli.js -p "Q4 2025" \ ~/Downloads/distrokid-earnings.csv \ ~/Downloads/tunecore-report.csvMost artists use one or two distributors. The tool works with any combination — two DistroKid files from different periods, one DistroKid and one TuneCore, or all three. The reconciler compares whatever you give it.
A worked example: reconciling Q4
Here’s what a real reconciliation workflow looks like. Say you distributed through DistroKid and TuneCore last quarter. You download both CSVs and run:
node src/cli.js -p "Q4 2025" distrokid-q4.csv tunecore-q4.csvThe report shows:
| Track | Platform | DistroKid Streams | TuneCore Streams | Difference |
|---|---|---|---|---|
| JADED | Spotify | 12,450 | 12,380 | -0.6% |
| KICK IT W/U | Spotify | 8,200 | 8,190 | -0.1% |
| MOOD | Apple Music | 3,100 | 2,540 | -18.1% |
| LIVE IT UP | Spotify | 5,800 | 5,790 | -0.2% |
That 18% difference on MOOD’s Apple Music streams? That’s a flag. Could be a reporting delay, could be a territory exclusion, could be an actual error. Either way, you now know about it. You can check with your distributor instead of assuming everything is fine.
Distributors aren’t stealing from you. The music royalty system involves dozens of intermediaries, currency conversions, and reporting timelines. Mistakes happen at scale. This tool helps you spot them so you can follow up — not so you can accuse anyone of wrongdoing. A polite “I noticed a discrepancy in my Q4 Apple Music numbers, can you check?” gets results.
If something is off
| Problem | Follow-up prompt |
|---|---|
| CSV parsing fails with encoding errors | The CSV parser is choking on my DistroKid export. It likely has BOM characters or uses Windows-1252 encoding instead of UTF-8. Add BOM stripping and try parsing with encoding detection. Use fs to read the raw buffer first and strip any BOM bytes before passing to csv-parser. |
| Date formats don’t parse correctly | The date parsing is failing on TuneCore's format. TuneCore uses "Jan 15, 2026" while DistroKid uses "2026-01-15". Add flexible date parsing that handles: YYYY-MM-DD, MM/DD/YYYY, "Mon DD, YYYY", and "DD Mon YYYY" formats. Normalize all to YYYY-MM. |
| Track names don’t match across distributors | "JADED" from DistroKid isn't matching "Jaded (Original Mix)" from TuneCore. The title normalization needs to: lowercase both strings, strip parenthetical suffixes like "(Original Mix)", "(Radio Edit)", "(Extended Mix)", "(Remix)", trim whitespace, then compare. |
| Currency conversion is wrong | Some rows in my CD Baby export have revenue in EUR and GBP, not USD. Add currency detection (look for currency symbol or a "Currency" column) and convert to USD using approximate rates: EUR×1.08, GBP×1.27, CAD×0.74, AUD×0.65. Flag any rows where currency conversion was applied. |
| Report shows zero revenue for some tracks | The revenue column is parsing as zero for tracks that definitely have earnings. Check if the CSV uses a different column name for revenue -- DistroKid sometimes uses "Earnings (USD)" and sometimes "Team Earnings (USD)". Add both as recognized column names in the parser. |
How the per-stream rates work
The reconciler uses approximate per-stream rates to flag suspicious revenue. Here’s why those numbers matter:
🔍Per-stream rate breakdown
Per-stream rates are not fixed. They vary by:
- Platform: Spotify pays ~$0.003-0.005, Apple Music pays ~$0.007-0.01, Tidal pays ~$0.008-0.013
- Territory: US streams pay more than streams from countries with lower subscription prices. A Spotify stream from India might pay $0.001 while a US stream pays $0.004.
- Subscription tier: Premium subscribers generate higher per-stream revenue than free-tier listeners.
- Time of year: Q4 rates tend to be slightly higher due to increased ad revenue during the holiday season.
- Total platform streams: Spotify uses a pro-rata model — your share of total revenue depends on your share of total streams. When a mega-artist drops an album, everyone else’s per-stream rate dips slightly.
The tool uses the low end of each platform’s range as the threshold. If your actual revenue falls below that floor, something is likely wrong. If your revenue is between the low and high estimates, it’s probably normal variation.
Customize it
Add historical trend analysis
Add a --history flag that accepts a directory of previous reports (saved as JSON).When provided, include a "Trends" section in the HTML report showing:- Revenue per track over the last 4 quarters (line chart using Chart.js)- Stream count trends per platform- Highlight any track with a >20% drop from the previous quarterSave the current reconciliation as a JSON file in the history directory forfuture comparisons.Add tax reporting summary
Add a --tax flag that generates a tax-summary.csv alongside the HTML report.Include columns: track_title, total_revenue_usd, platform, territory, quarter.Group by territory for international tax reporting. Add a totals row. This iswhat my accountant needs at tax time -- just the revenue numbers grouped bycountry, nothing else.Build a projected earnings calculator
Add a "Projections" section to the HTML report. Based on the current period'sstream counts and revenue, project next quarter's earnings assuming:- Catalog streams decay at ~5% per month for tracks older than 3 months- New releases get a 2x multiplier in their first month- Show projected revenue per track and per platform- Include a confidence range (optimistic/realistic/conservative)Label this clearly as an estimate, not a guarantee.Set up drop alerts
Add a --watch flag that compares the current report to the previous one (fromthe history directory). If any track's streams dropped more than 30% or revenuedropped more than 25% compared to the previous period, print a prominent alertin the terminal and add a red banner to the top of the HTML report. This catchessituations where a track gets removed from playlists or a distributor stopsreporting.When Things Go Wrong
Use the Symptom → Evidence → Request pattern: describe what you see, paste the error, then ask for a fix.
Try it yourself
- Open your AI CLI tool in an empty folder.
- Paste the main prompt.
- Run
npm installin the generatedroyalty-tool/directory. - Generate sample data with
--generate-sampleand reconcile it. - Open the HTML report and verify the three planted discrepancies show up.
- If you have real distributor CSVs, run those through and see what you find.
Key takeaways
- Distributor CSVs are messy — different column names, date formats, and encoding. Auto-detection and normalization handle this so you don’t have to.
- Per-stream rates are your benchmark — they’re not exact, but they give you a floor. Revenue below that floor means something is worth investigating.
- 10-25% of royalties have reporting issues — this isn’t paranoia, it’s the reality of a complex system with many intermediaries. The tool helps you catch what you’d otherwise miss.
- Reconciliation is a pattern, not a one-time task — run this every quarter when you get paid. Keep the reports. Trends over time reveal more than any single snapshot.
- This tool catches errors, not fraud — approach discrepancies as honest mistakes to be clarified, not accusations to be made.
Your reconciliation report shows that your track 'MOOD' earned $38 from 12,000 Spotify streams. The known per-stream rate range for Spotify is $0.003-0.005. Should this be flagged?
What’s next
In the final lesson, you’ll build The Release Ops Command Center — a static site that pulls together everything from Lessons 1-5 into a single weekly action-plan dashboard. Campaign status, revenue numbers, royalty flags, and platform analytics, all in one place. It’s the capstone of the module: one command builds a private operations hub for your entire release pipeline.