← Hannah Schlacter
ConsumerAIFull-StackAutomation

Kindle Agent

A private, invite-only book search that sends any title straight to my family's Kindles — five sources, one tap.

The hard part isn't finding the book — it's that Amazon silently rejects most EPUBs. The real product is the Calibre formatting pipeline, not the search box in front of it.

The problem

I read on a Kindle, and so do my mom and sister. Getting a book on there means a weeks-long Libby hold or Amazon's price — and Send-to-Kindle is a manual hunt-download-email loop where half the EPUBs you find fail silently.

My hypothesis

Put a search box in front of every free and self-hosted source, wire it into Amazon's Send-to-Kindle pipe, and the whole dance collapses to one tap. The bet: aggregation plus reliable formatting is the product, not any single source.

What I built

An invite-only Next.js app with magic-link and password auth. Search fans out across five sources — Standard Ebooks, Gutenberg, Open Library, LibGen, and a self-hosted torrent bridge — deduped into one ranked list, then the pick is cleaned through Calibre and emailed to your Kindle via Resend. The torrent source runs on a free Oracle VM (Jackett + qBittorrent + Calibre in Docker, over Tailscale). Built for three accounts from day one — my mom and sister each get their own library and history.

What broke

Amazon rejects almost every EPUB on the internet with an opaque E999 error. I burned time on AZW3 and MOBI before finding the fix: run every file through Calibre, which re-zips it to spec. Magic links kept dying to Gmail's link prefetcher, so the token consume had to go POST-only. And LibGen ranks so badly that "the nightingale" returns Florence Nightingale biographies.

What I learned

The reliability layer is the product, and it's invisible until it fails. Building for three users instead of one forced the real work — uniqueness checks, send caps, and a bounce webhook so a silent delivery failure actually surfaces.

If I kept going

Per-job fetch tokens so a leaked bridge token can't pull any file. Query enrichment to fix LibGen's ranking. And smoother onboarding for the one manual step every user still does themselves — allowlisting the sender on Amazon.

What was built

Frontend & Auth

Invite-only app, magic-link + password

  • Next.js 16 / React 19
  • Tailwind CSS 4
  • Magic-link + password (bcryptjs)
  • Upstash Redis (users, history)

Search Aggregation

Five sources deduped into one ranked list

  • Standard Ebooks
  • Project Gutenberg
  • Open Library
  • LibGen
  • Self-hosted torrent bridge
  • Source + seeder chips per result

Send Pipeline

Calibre clean → Resend → Kindle

  • Calibre EPUB reparse (the E999 fix)
  • Resend (kindle@schlacter.me sender)
  • Per-user Kindle address
  • 20/day per-user send cap

Torrent Bridge

Self-hosted, zero-cost, publicly reachable

  • Oracle Cloud always-free VM
  • Tailscale Funnel (public HTTPS)
  • Jackett + qBittorrent
  • Flask + Calibre in Docker

Multi-user Safety

Built for three accounts from day one

  • Kindle-address uniqueness index
  • Per-user send quota
  • Resend bounce webhook
  • Delivery test + drift banner

Prototype

The front door: a private, invite-only library for family and friends — sign in with a password or a magic link.

The front door: a private, invite-only library for family and friends — sign in with a password or a magic link.

One search fans out across every source — Standard Ebooks, Gutenberg, Open Library, LibGen, and a self-hosted torrent bridge — each result tagged with its source and, for torrents, its seeder count.

One search fans out across every source — Standard Ebooks, Gutenberg, Open Library, LibGen, and a self-hosted torrent bridge — each result tagged with its source and, for torrents, its seeder count.

Your library: every book you've sent, with cover art and a reading-status tracker.

Your library: every book you've sent, with cover art and a reading-status tracker.

Search history: every query, how many results it returned, and when.

Search history: every query, how many results it returned, and when.

View live ↗