KDP royalty digest from the report export
From the Subzero Labs video "Your AI Has Amnesia. This Free Code Makes It Remember".
Free, no sign-up. Copy it, use it, share it.
For: KDP authors
The job: KDP has no public sales API, and the report mixes formats, marketplaces and currencies.
You give it: the royalties report and the KENP pages report from KDP Reports (CSV, or the .xlsx as it comes).
It is one agent of the free AI OS starter (https://subzero.bodymindbeing.me/free/ai-os-starter/) - download the whole starter, it runs on sample data out of the box. Nothing is sent anywhere: replies come out as drafts.
Download the AI OS starter (zip)
MIT licence. Python 3.10+, no other installs. Unzip, then follow README.md.
Run this one
- Set up the starter (the steps on the starter page).
- python -m aios run royalty_digest
- Edit config.json: point "royalty_digest" at your own files (paths relative to config.json).
Check it yourself
- Filter the report to the last 30 days and add up the Royalty column per currency - it must match.
- Currencies are never converted or mixed; no AI in this agent.
"""KDP 1 - royalty digest. Amazon has no public API for KDP sales or royalties, so this reads the report you
download from KDP Reports (CSV or the .xlsx as it comes). The CODE totals royalties per book and per currency
(no currency conversion - each currency is added up on its own), units, refunds and Kindle Unlimited pages read,
this period vs the one before. Plain code, no AI: money numbers should never be guessed."""
from collections import defaultdict
from datetime import timedelta
from ._common import col, money, table, to_date, today
TITLE = "KDP royalty digest"
def run(cfg, data, s):
now, days = today(cfg), s.get("period_days", 30)
start, before = now - timedelta(days=days), now - timedelta(days=2 * days)
roy = defaultdict(lambda: defaultdict(float)) # period -> (title, currency) -> royalty
units = defaultdict(lambda: defaultdict(int))
refunds = defaultdict(int)
for r in table(cfg, s["royalties"]):
d = to_date(col(r, "royalty date", "date", "order date"))
if not d or d < before:
continue
p = "now" if d >= start else "before"
key = (col(r, "title"), col(r, "currency") or "?")
roy[p][key] += money(col(r, "royalty"))
units[p][key[0]] += int(money(col(r, "net units sold", "units sold")))
refunds[p] += int(money(col(r, "units refunded")))
pages = defaultdict(int)
if s.get("kenp"):
for r in table(cfg, s["kenp"]):
d = to_date(col(r, "date"))
if d and d >= before:
pages["now" if d >= start else "before"] += int(money(col(r, "kenp read", "kindle edition normalized pages (kenp) read", "pages read")))
def by_cur(p):
t = defaultdict(float)
for (_title, c), v in roy[p].items():
t[c] += v
return ", ".join(f"{v:.2f} {c}" for c, v in sorted(t.items())) or "0"
books = sorted({k[0] for k in roy["now"]} | {k[0] for k in roy["before"]})
lines = [f"{b}: {units['now'][b]} units ({units['before'][b]} before), royalties "
+ ", ".join(f"{v:.2f} {c}" for (t, c), v in sorted(roy["now"].items()) if t == b) for b in books]
facts = (f"Last {days} days: royalties {by_cur('now')} (the {days} days before: {by_cur('before')}). "
f"Refunded units: {refunds['now']} (before: {refunds['before']}). "
f"Kindle Unlimited pages read: {pages['now']} (before: {pages['before']}).")
summary = (f"Last {days} days: {by_cur('now')} (before: {by_cur('before')}). "
f"Kindle Unlimited pages read: {pages['now']} (before: {pages['before']}).")
attention = []
if refunds["now"] > refunds["before"] and refunds["now"] >= 3:
attention.append(f"Refunds up: {refunds['now']} vs {refunds['before']} - check reviews for a problem")
for b in books:
if units["before"][b] >= 5 and units["now"][b] <= units["before"][b] / 2:
attention.append(f"{b}: units fell from {units['before'][b]} to {units['now'][b]}")
return {"summary": summary, "items": [facts] + lines, "attention": attention}
Shared as shown in the video. Replace any <PLACEHOLDERS> with your own values.