Applied Module 12 · AI-Powered Music Production Workflows

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.

💬This is real money

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.

Software pattern: Data reconciliation engine

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 multiple
distributors. 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 sample data is your test suite

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.html

Set it up

Terminal window
cd royalty-tool
npm install

Try it with sample data first

Terminal window
node src/cli.js --generate-sample -o ./test-report

This creates three sample CSVs in ./test-report/. Now reconcile them:

Terminal window
node src/cli.js -p "Q4 2025" -o ./test-report \
./test-report/sample-distrokid.csv \
./test-report/sample-tunecore.csv \
./test-report/sample-cdbaby.csv

Open ./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

  1. Export your royalty CSV from DistroKid (Account → Payments → Download CSV)
  2. Export your royalty CSV from TuneCore (Reports → Earnings → Export)
  3. Run the tool with your real files:
Terminal window
node src/cli.js -p "Q4 2025" \
~/Downloads/distrokid-earnings.csv \
~/Downloads/tunecore-report.csv
You don't need all three distributors

Most 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:

Terminal window
node src/cli.js -p "Q4 2025" distrokid-q4.csv tunecore-q4.csv

The report shows:

TrackPlatformDistroKid StreamsTuneCore StreamsDifference
JADEDSpotify12,45012,380-0.6%
KICK IT W/USpotify8,2008,190-0.1%
MOODApple Music3,1002,540-18.1%
LIVE IT UPSpotify5,8005,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.

This catches errors, not fraud

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

ProblemFollow-up prompt
CSV parsing fails with encoding errorsThe 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 correctlyThe 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 wrongSome 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 tracksThe 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 quarter
Save the current reconciliation as a JSON file in the history directory for
future 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 is
what my accountant needs at tax time -- just the revenue numbers grouped by
country, nothing else.

Build a projected earnings calculator

Add a "Projections" section to the HTML report. Based on the current period's
stream 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 (from
the history directory). If any track's streams dropped more than 30% or revenue
dropped more than 25% compared to the previous period, print a prominent alert
in the terminal and add a red banner to the top of the HTML report. This catches
situations where a track gets removed from playlists or a distributor stops
reporting.

🔧

When Things Go Wrong

Use the Symptom → Evidence → Request pattern: describe what you see, paste the error, then ask for a fix.

Symptom
The auto-detect picks the wrong parser for my CSV
Evidence
Running the tool on my DistroKid export says 'Detected format: tunecore' and the parsed data is all wrong
What to ask the AI
"The auto-detection is matching the wrong format. Make the header matching more specific: require at least 3 header matches (not just 1) before selecting a format. DistroKid must match 'Reporting Date' AND 'Store' AND 'Earnings (USD)'. TuneCore must match 'Posted Date' AND 'Platform' AND 'Total Earned'. Print which headers were matched so I can debug."
Symptom
The reconciler shows every track as a discrepancy
Evidence
The discrepancy table has 40+ rows and every single track is flagged, even ones where the numbers are close
What to ask the AI
"The discrepancy threshold is too sensitive. Only flag stream differences >15% AND revenue differences >10%. Small variations (under 5%) are normal due to reporting timing. Add a 'tolerance' zone: <5% = no flag, 5-15% = info, >15% = warning, >25% = alert."
Symptom
The HTML report is unstyled or looks broken
Evidence
Opening report.html shows raw text with no formatting, or the dark theme isn't applied
What to ask the AI
"The HTML template isn't rendering correctly. Make sure the Handlebars template has all CSS inline in a <style> tag inside the <head>. Don't use external stylesheets -- the report needs to work as a standalone file. Check that the Handlebars compile and render steps are actually writing the final HTML, not the raw template."
Symptom
Revenue numbers show way too many decimal places
Evidence
The report shows values like '$42.876543210987' instead of '$42.88'
What to ask the AI
"Revenue values aren't being rounded. After all revenue calculations, format to 2 decimal places using toFixed(2). Apply this in the reporter when inserting values into the template, not in the reconciler (keep full precision for calculations, round only for display)."
Symptom
The tool crashes on a CSV with empty rows or trailing commas
Evidence
Error: Cannot read property 'trim' of undefined on line 847 of reconciler.js
What to ask the AI
"The CSV has empty rows at the end (common in Excel exports) and some rows have trailing commas creating empty columns. Filter out rows where all values are empty or undefined before passing to the parsers. Add a null check on every field access in the parsers."

Try it yourself

  1. Open your AI CLI tool in an empty folder.
  2. Paste the main prompt.
  3. Run npm install in the generated royalty-tool/ directory.
  4. Generate sample data with --generate-sample and reconcile it.
  5. Open the HTML report and verify the three planted discrepancies show up.
  6. 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.

KNOWLEDGE CHECK

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.