The big picture

A loop, not an agent

Seven parts in a fixed order, run on a schedule. The LLM works inside a part with a narrow job and a typed output; it never decides what runs next.

Alexandria big picture: the taste registry feeds a nightly pipeline of Discover, Shortlist, Review check and Recommend, run by GitHub Actions. Tracked sites are crawled politely into Discover; the AI gateway judges at Shortlist and writes the digest at Review check. The pick reaches me as a page on my blog; my want, not, finished and comment signals become rewards that Learn uses to update the taste registry.
Open the interactive version ↗ · Everything below is the detail.
Status: P1 system design, for review. No code until the open decisions in section 12 are settled.
Cadence 1 book / day, 07:00 ICT Stages Discover → Shortlist → Review → Recommend → React Data polite web crawl, no paid APIs AI any model through one gateway Run cost $0 on free tiers, ≈ $3–8 / month paid

What you could learn

Finishing Alexandria teaches more than the code. These lessons carry over to any AI product you build next.

Key Takeaways

Short answers to the questions people ask first.

Is this production-ready, or just a proof of concept?

Both, depending on who it serves. For one reader it is a working production system. For many readers it is still a proof of concept.

Why it counts as production for one reader

  • It runs unattended: two scheduled GitHub Actions jobs a day, one to pick and one to publish.
  • The database is closed to the public and reachable only through owner-checked functions.
  • AI spend has a daily cap, and every task has fallback models. In the first live runs Gemini returned overload errors and timeouts, and the free OpenRouter model took over, so the runs still finished.
  • An offline test suite covers the crawl, the extraction, the scoring, the database lock-down and the full pipeline.

Why it is still a proof of concept beyond that

  • One owner, one taste profile, one pick a day. There are no accounts and no per-reader profiles.
  • The public reaction call has no rate limit of its own.
  • Crawling runs from GitHub’s shared servers, and some stores refuse them. Two Vietnamese bookstores returned nothing when crawled from there, and the crawler does not yet record why.
  • It has only just gone live, so the weekly and monthly upkeep described in “Life after launch” is still untested.
Open concern

It will not scale to 100,000 simultaneous readers as designed

GitHub Actions is a scheduler for batch jobs, not a way to serve users. Two different things hide in this concern:

  • Reading and reacting do not run on Actions. The pages are static files and the reaction buttons call the database directly, so Actions still runs just two jobs a day however many people read. The limits that matter there belong to the page host and to Supabase: bandwidth, request rates and free-tier quotas. I have not load-tested either.
  • Personal picks for 100,000 readers would. The crawl can be shared, but the shortlist, judge and digest steps would repeat for every reader every day. One scheduled job with run-time limits and capped parallel runs cannot do that. It would need a job queue with stateless workers, a profile and a spend cap for each reader, and per-reader publishing.

Status: open. It is not needed for the one-reader version and it is not designed yet. The first step would be to decide whether Alexandria stays a personal tool or becomes a service.

1. What Alexandria is, and what it isn't

"Autonomous" here means it runs every day without me: it wakes on a schedule, does the same five stages in the same order, and stops. It is not an open-ended agent that decides its own next step. That choice makes it cheap, testable, and easy to replay when a pick goes wrong.

RequirementWhat it means in practice
F1 Taste registryMy tastes live in a database with their history. I add, reweight or pause a taste from a web form on this site; Telegram commands come later.
F2 DiscoverCrawl the bookstore and publisher sites I track. I add sites from the same web form; a seed list comes preloaded.
F3 ShortlistFilter and score candidates against my taste; plan which taste gets today's slot.
F4 Review checkGather ratings and reviews from the tracked sites and critics' pages, and write an honest digest with sources.
F5 RecommendOne book a day, published as a page on this site. That page is the whole channel for the proof of concept; Telegram comes later.
F6 ReactionsRecord four signals on that page: Want it, Not for me, Finished, and a comment.
F7 LearnTurn reactions into rewards that change future picks, the way RL does, at a data rate of one label a day.
N1 TruthfulEvery rating, quote and price comes from a fetched page with a date. No source, no claim.
N2 PoliteThe crawler obeys robots.txt and crawl delays, says who it is, and keeps a site link-only when its terms forbid robots.
N3 Affordable and QuietNo paid data APIs. The only paid service is the AI model, and free tiers can cover the proof of concept. No server to babysit; one status line a day on the console page.
N4 Model-agnosticEvery AI call goes through one gateway, so changing the model is a config edit, and models can be compared on my own data.

2. The taste registry

Taste is data with a timeline. Every change is an event, so Alexandria can answer "what did I like in March?" and the learning loop can tell a real shift in taste from a bad week.

FacetStarting priorityNotes the scorer uses
History and stories of mathematics1.00Narrative over textbook; biographies of mathematicians count
History and stories of physics1.00Same; includes cosmology and the bomb-era histories
History of other sciences0.70Biology, chemistry, medicine, computing history
AI and computer science0.70Both technical and narrative; seeded from my reading list
Philosophy0.55Philosophy of science gets a bonus from the science facets
Psychology0.55Seeded by Thinking, Fast and Slow (rated 4★)
Pulitzer Prize winners0.50A quality signal that boosts any facet, plus its own slot
Classics0.40Rotates with new releases
Hot new releases0.40Published in the last 12 months, from the tracked sites' new-release pages

Register a new taste

A web form on this site first: a console page lists the current facets and lets me add, reweight or pause one. Only requests carrying my owner key count. A free-text box ("more books like The Code Breaker") is parsed into a proposed change that I confirm with one click. Telegram commands come later.

Kept time by time

taste_event is append-only (add, reweight, pause, remove, note). The current profile is a view over it. A monthly snapshot gives a taste timeline I can read later.

Cold start

Seed from the 8 books already on my reading list (titles, ratings, notes), a short form (10 favorites, 5 books I didn't enjoy), and a Goodreads CSV export if I have one. Day 1 picks use these priors.

3. Stage 1: Discover

Alexandria crawls the sites I track: bookstores and publishers I register in a web form, the same way I register tastes. A wide crawl every Sunday builds the candidate pool; a small daily crawl picks up new releases. No paid data APIs.

The form takes a site URL, its kind (bookstore, publisher, prize, review), the pages to watch (new releases, a category), and the language. Before the first crawl, Alexandria reads the site's robots.txt and sets its mode: crawl, or link-only (no fetching; store search links only). The seed list below is preloaded, with modes from robots.txt checked on Nov 2, 2026.

Seed siteWhat it watchesModeWhy
Penguin Random HouseNew releases; science, history and philosophy categoriescrawlrobots.txt allows catalog pages (blocks only search)
TikiBook categories and new arrivals; Vietnamese and imported editions with VN pricescrawl, slowrobots.txt allows product and category pages, and publishes a sitemap
FahasaBook categories; Vietnamese editions and pricescrawlrobots.txt blocks only account, cart and checkout pages
Nhã NamVietnamese translations of the books I'm offeredcrawl, 10 s apartrobots.txt asks for a 10-second crawl delay
Pulitzer PrizeWinners by categorycrawl, yearlyallowed
The Guardian booksCritics' reviews (used in Stage 3)crawlallowed
Amazon—link-onlyrobots.txt allows many pages, but Amazon's Conditions of Use forbid robots and data extraction
Shopee—link-onlyuntil its terms are checked; a search link is enough to buy
O'Reilly—link-only for nowits robots.txt returned 403 to a bot request

Read the rules first

robots.txt, crawl-delay, and the site's terms decide the mode. A site that forbids robots stays link-only, however useful it would be.

Find pages

Start from sitemaps and the watched listing pages. Never crawl search result pages, and never log in.

Extract

Read schema.org Book / Product data embedded in the page when it's there. Otherwise a small model behind the AI gateway extracts title, author, ISBN, price and rating into a typed record.

Be gentle

At least 10 seconds between requests to one site, a daily page cap per site, conditional requests (ETag) so unchanged pages cost nothing, and a user agent that names Alexandria and links back here.

Everything is stored per work (one book, many editions), keyed by ISBN-13 or normalized title and author, so the same book found on five sites is one candidate.

Design rule. Each tracked site has a mode, a rate limit, a cache and a health flag. When a site starts blocking or changes its terms, it drops to link-only and Alexandria keeps going; it never works around a block.

4. Stage 2: Shortlist by taste

From a few hundred candidates to five, in three passes: hard filters, a cheap score, then an LLM judge on the top twenty.

Pass 1

Hard filters

  • Not already read, recommended or rejected
  • Author cooldown: 30 days
  • Buyable in Vietnam, or a Vietnamese edition exists
  • Enough ratings to judge, except for releases under 3 months old
Pass 2

Cheap score

score = taste_match + quality + freshness + novelty − penalties, each weighted. Taste match is embedding similarity between the book and each facet. Quality is a Bayesian average of the ratings found on tracked pages, so a 4.6 from 12 readers doesn't beat a 4.3 from 40,000, plus a prize bonus.

Pass 3

LLM judge

The judge model, called through the AI gateway, reads my profile and the top 20 book cards and scores each against a fixed rubric (fit per facet, depth, readability, why now), with a one-line reason. Output is typed JSON, so the reasons are stored and auditable.

Which taste gets today? A slot planner picks the facet before the book. It uses Thompson sampling: each facet keeps a Beta distribution of "how often I liked its picks", seeded from my priorities, and the planner samples from each and takes the highest. Math and physics history start ahead, but a facet that keeps getting "Not for me" loses slots on its own. One day a week is a labeled wildcard outside my top facets, so the loop doesn't shrink my taste to what it already knows.

5. Stage 3: Review check

The top three of the shortlist get a full review check. The best of the three after reviews becomes today's pick; the other two are kept as runner-ups.

Gather evidence

  • Ratings and review counts on the tracked store and publisher pages
  • Critics' reviews on tracked review sites, such as The Guardian
  • Optional: the model's own web search, when the provider chosen in the gateway offers it, with every result cited

Each piece is stored with its URL and fetch time.

Write the digest

The digest model writes a structured summary: consensus, strengths, common criticisms, who it's for, how much math it assumes, and red flags (dated science, disputed claims). Every sentence carries the ID of the evidence it came from.

Check the digest

A validator drops any sentence whose evidence ID doesn't exist or whose source text doesn't support it. Ratings are copied from evidence, never written by the model. If too little survives, the book goes back to the pool and the runner-up moves up.

The comparison of the three books is stored too: which one won and why. That gives a ranked group every day, the same group-relative signal GRPO learns from in the Wikipedia agent tutorial.

6. Stage 4: Recommend, one book a day

At 07:00 ICT the pick is published as a page on this site. For the proof of concept that page is the only channel, and it is also where I react. Telegram comes later.

Daily pick page (mock-up)

Mock-up only: ratings, links and the digest are placeholders.

How the page works

  • One page a day in a new "Book suggestions" list on this site, with a short introduction, the review digest with sources, and links to find the book.
  • Reactions on the page: three buttons and a comment box. A small edge function stores the click. Only requests carrying my owner key count, saved once in my browser; visitors' clicks are kept apart and never train anything.
  • Later: a Telegram bot with the same buttons, sending the same stored recommendation.

Publishing. The page is built overnight and goes public at 07:00. From the console page I can hold today's page or skip a day.

Site change needed. One page a day would flood the blog index, so suggestions get their own list (content/book-suggestions.json, rendered on Books), and the blog index shows only the latest three.

7. Stage 5: Recognize my reaction

Only four signals, all on the daily page. Each becomes a row in reaction, then a number in reward. Rewards can be revised: finishing a book weeks later updates the reward of the day it was recommended.

SignalWhereReward
✅ FinishedButton on that day's page, any time later1.0
👍 Want itButton on the daily page0.6
👎 Not for meButton on the daily page0.0
💬 CommentComment box on the daily pageParsed into sentiment and aspects; moves the reward by up to ±0.2 and can propose a taste change

A comment such as "great story but too equation-heavy for a morning read" is parsed by a small model behind the gateway into structured fields: sentiment, aspects (narrative +, math density −), and a suggested change ("lower math depth for weekday picks"). Suggestions never change the registry by themselves; they wait on the console page for one-click confirmation.

Nothing else is recorded: no purchase tracking, no reading-time guesses, and silence is not treated as a reaction.

8. The learning loop, like RL but sized for one reader

One reader produces about 365 labels a year. Full RL on an LLM would starve on that. So Alexandria climbs in levels, and each level only switches on when there is enough data to beat the one below it on the offline eval.

Level 0 · day 1

Priors and rules

Starting priorities, cold-start seed books, the judge's rubric. Works with zero reactions.

Level 1 · from the first reaction

Slot bandit

Thompson sampling over taste facets, updated the moment a reaction arrives. Decides which kind of book I get.

Level 2 · about 50 reactions

Learned scorer

A small logistic model on book features and embeddings, refit weekly on my rewards, replacing the hand-set weights in pass 2. Decides which book.

Level 3 · about 300 reactions, optional

Post-trained judge

SFT on accepted picks plus DPO or GRPO on my preferences, trained on Tinker like the Wikipedia agent. Only if the Level 2 eval shows the off-the-shelf judge is the bottleneck.

Weekly reflection. Every Sunday, the reflection model reads the week's reactions and comments and proposes a profile diff in plain words ("you liked narrative physics history more than technical treatments; lower math depth on weekdays"). I approve or edit it on the console page, and it becomes a new profile_version.

Wikipedia agent tutorialSame idea in Alexandria
Eval first: lock a test setOffline replay: books I've already rated, held out. Metric: how many I rated ≥ 4★ land in the shortlist's top 5. Online: 30-day Want-it rate and finish rate
Harness: the same loop for eval and trainingThe five stages are one pipeline; replaying a past day runs the same code
SFT on its own correct answersPicks I wanted or finished, with the judge's reasons, become few-shot examples (and training data for Level 3)
GRPO: compare a group of attemptsEach day's three reviewed books are a group, ranked by the judge; my reaction scores the one I was shown
Ablation with confidence intervalsMonthly report: turn off reviews, the bandit, or embeddings on the replay set, with bootstrap intervals

9. Tech stack

A scheduled Python pipeline, one Postgres database, a few small edge functions, an AI gateway, and pages on this site. Nothing to keep running except the database.

Inputs
Tracked sitesbookstores, publishers, prize and review pages
Polite crawlerrobots-aware, cached, 10 s per site
Console formstastes and tracked sites, owner key only
Core
Pipeline (Python), GitHub Actions cronNightly 00:00 ICT; Sunday wide crawl and reflection. Each stage idempotent and resumable by run ID
AI gatewayone call for every AI job; routes per task; fallback, budget, logging, shadow comparison
Supabase Postgres + pgvectortastes, sites, books, evidence, picks, reactions, rewards, AI call log
Edge functionsconsole forms and reaction buttons
Outputs
This websitedaily pick page and console; commit via GitHub API → GitHub Pages
Telegramlater

The AI gateway

Every AI job (extract, judge, digest, write, parse a comment, reflect) goes through one function: gateway.run(task, input, schema). It returns typed output plus the model used, tokens, cost and latency. Which model serves which task lives in one config file, so switching providers is an edit, not a code change.

routes.yaml (example)
judge:      { model: gemini-3.8-flash,      fallback: [deepseek-v4-pro, claude-sonnet-5.5] }
digest:     { model: gemini-3.8-flash,      fallback: [deepseek-v4-pro] }
write:      { model: gemini-3.8-flash,      fallback: [claude-haiku-5.5] }
extract:    { model: gemini-3.1-flash-lite, fallback: [openrouter/free] }
parse:      { model: gemini-3.1-flash-lite, fallback: [openrouter/free] }
reflect:    { model: claude-sonnet-5.5 }
budget:     { usd_per_day: 0.50, on_exceed: free_routes_only }
shadow:     { judge: deepseek-v4-pro, share: 0.2 }

Adapters

Anthropic, OpenAI, Google Gemini, DeepSeek and OpenRouter. DeepSeek and OpenRouter speak the OpenAI-compatible API, so they share one adapter; OpenRouter also reaches the free models. A local model can be added later.

Same shape every time

Each task has a JSON schema. The gateway validates the answer, retries once with the error, and otherwise moves to the next model in the fallback list.

Fallback and budget

Rate limits, server errors, refusals and timeouts fall through to the next model. When the daily budget is spent, only free routes run.

Compare on my data

Shadow mode runs a second model on a share of a task and stores both answers. The replay eval scores both against my past reactions, so choosing a model is a measured decision. Every call is logged in llm_call.

Build or buyVerdict
Thin gateway in the pipeline's codeRecommended. A few hundred lines; no extra server; full control of routing, schemas and logs
LiteLLM proxyMore features, but it is a server to deploy and keep running
OpenRouter aloneOne key for many models, but every call depends on one company. Used as one adapter instead
Cloudflare AI GatewayLogging and caching in front of providers; can be added later without changing the code

Choices and why

DecisionChoiceWhy not the alternative
OrchestrationFixed pipeline in codeAn agent framework could run this on a schedule, but the order of steps never changes. Code is cheaper, and every day can be replayed exactly
DataPolite crawler over tracked sitesPaid data APIs are what this design avoids; crawling the sites I choose also covers Vietnamese stores that have no API
SchedulerGitHub Actions cron (private repo)A VPS would sit idle 23 hours a day. A run takes minutes
DatabaseSupabase Postgres with pgvectorOne store for rows and embeddings, reachable from Actions and from edge functions. Free projects pause after a week of inactivity; the daily run keeps it awake
AIAI gateway with per-task routesHard-wiring one provider makes cost and quality comparisons impossible
ChannelA page on this site; Telegram laterThe site already builds and deploys itself, and a page can carry the reaction buttons

Data model

TableKey columns
taste_facet, taste_eventfacet, priority, status · event type, payload, source, time
profile_versionprofile text, embedding, facet snapshot, approved_at
tracked_site, page_cacheURL, kind, watched pages, mode (crawl / link-only), crawl delay, health · URL, ETag, content hash, fetched_at
work, edition, offertitle, authors, year, subjects, description, embedding · ISBN-13, language, format · store, price seen, URL, checked_at
evidencework, source URL, rating, count, excerpt, fetched_at
candidate, recommendationrun ID, work, slot facet, scores, rank · date (unique), work, digest, judge reasons, runner-ups, exploration flag
reaction, rewardkind (want, not, finished, comment), comment text · value, components, updated_at
bandit_state, model_versionfacet α/β · scorer params and eval metrics
llm_call, run_logtask, provider, model, tokens, cost, latency, ok · stage, status, pages crawled, error

10. Guardrails

What it may do alone

  • Crawl tracked sites in crawl mode
  • Score and store
  • Publish the daily page

What needs me

  • Any change to the taste registry
  • Adding or removing a tracked site
  • Raising the spend cap

Hard limits

  • Daily caps on AI spend and pages crawled per site; the run stops cleanly when hit
  • One recommendation per date (unique key), so a rerun never double-posts
  • Pause, Skip today and Why this book on the console page

Honest content

  • No claim without evidence; ratings copied, never generated
  • Prices show their check time
  • Pages in my own words, at most one short quote

Polite crawler

  • robots.txt and crawl-delay obeyed
  • Names itself and links back here
  • Link-only when terms forbid robots

Private by default

  • Reactions and taste history stay in the database
  • The public page shows the recommendation, not my reaction
  • Only my owner key can change tastes or react

11. Cost estimation

Crawling and scheduling run inside free tiers, so the only real cost is the AI model. Depending on the route through the gateway, that is $0 on free tiers or about $3–8 a month on paid models.

Daily AI workload

JobKindCalls / dayTokens in / out
Extract new books from crawled pages, parse commentslight~10 (batched)80k / 15k
Judge the top 20heavy125k / 6k
Review digests for 3 booksheavy327k / 4.5k
Write the daily pageheavy16k / 2.5k
Weekly reflection, averaged per dayheavy1 a week6k / 0.7k
Heavy total≈ 664k / 13.7k

AI API comparison for this workload

Monthly cost of the heavy jobs (30 days × 64k in / 13.7k out) at each model's standard price. "Tier" is where the model sits in its own provider's line-up; how well a model actually picks books for me is measured by the gateway's shadow runs, not by this table.

ModelTier in its familyPrice in / out per 1MFree tierHeavy jobs / month
Claude Opus 5.5Top$4 / $20No$15.90 (batch $7.95)
Claude Sonnet 5.5Middle$2 / $10No$7.95 (batch $3.98)
GPT-6.1 SolMiddle (GPT-6 Astra is the top)$2 / $10No$7.95 (batch $3.98)
Gemini 4 ArgonNewest generation$2 / $10, then $4 / $20 from Dec 1No$7.95, then $15.90
Kimi K3.1 ProTop; leads open-weight models$2 / $10No$7.95
Gemini 3.8 FlashMiddle$0.75 / $3.75, then $1.50 / $7.50 from Jan 1Yes$2.98, then $5.96
Mistral Large 4Top$0.70 / $2.10No$2.21
DeepSeek V4 Pro (off-peak)Top$0.66 / $1.98No$2.08
Gemini 3.1 Flash-LiteSmall$0.25 / $1.50Yes$1.10
DeepSeek Flash (off-peak)Small$0.15 / $0.60No$0.53
Claude Haiku 5.5Small$0.10 / $0.50No$0.40
GPT-6 LunaSmall$0.10 / $0.50No$0.40
OpenRouter free models (e.g. Inkling, Nemotron 3 Ultra, Gemma 4 31B)Varies$0Yes$0

Gemini free tier

Covers Gemini 3.8 Flash, 3.6 Flash, 3.1 Flash-Lite, 2.5 Pro and Gemma 4, among others. Google doesn't publish per-model daily limits in its docs (AI Studio shows them per account), and third-party reports range from about 20 to 1,500 requests a day. Under Google's terms, free-tier prompts can be used to improve its products: fine for public book data, so keep personal notes on paid routes if that matters.

OpenRouter free models

17 free models in its catalog on Nov 2, 2026. Limits: 20 requests a minute and 50 a day, or 1,000 a day after buying at least $10 of credits. The list changes often, so they are a fallback, not the main route.

Quality, roughly

Public leaderboards in October 2026 put Gemini 4 Carbon, GPT-5.6 Sol and Claude Opus 5.5 at the top, with Kimi K3.1 Pro leading open-weight models, and the order shifts between index versions. Judging fit, summarizing reviews and writing a short page are mid-tier work, so a middle-tier model is likely enough; shadow runs will show whether the top tier earns its price.

Does free fit?

Yes for a proof of concept: about 6 heavy and 10 light calls a day. Route heavy jobs to Gemini 3.8 Flash (free) and light jobs to Flash-Lite (free), with OpenRouter free models as the fallback.

Four routes

RouteHeavy jobsLight jobs≈ per month
A. Free POCGemini 3.8 Flash, free tierGemini 3.1 Flash-Lite, free tier$0
B. BudgetDeepSeek V4 Pro, off-peakDeepSeek Flash, off-peak$2.71
C. BalancedClaude Sonnet 5.5 or GPT-6.1 Sol, batchClaude Haiku 5.5$4.44
D. Best qualityClaude Opus 5.5, batchClaude Haiku 5.5, batch$8.18

Plan: start on route A, shadow-run route B or C on 20% of judge calls (well under $1 a month), and let the replay eval decide after a month. DeepSeek's off-peak price applies to the nightly run: 00:00 ICT is 17:00 UTC, outside its peak hours.

Everything else: GitHub Actions (about 10 minutes a day, inside the free monthly minutes), Supabase free tier, GitHub Pages, and embeddings on Gemini's free tier. No paid data APIs.

Prices as of Nov 2, 2026: Google and DeepSeek from their own pricing pages, Anthropic from its price list, and OpenAI, Moonshot (Kimi) and Mistral from OpenRouter's public catalog. Gemini prices above change on Dec 1, 2026 and Jan 1, 2027, as noted. Check before switching routes.

12. Open decisions

Decided items are recorded so the design stays honest about what changed. Open items have a recommended default.

  1. Decided Channel. The proof of concept publishes a page on this site and collects my reactions there. Telegram comes later.
  2. Decided Editions. Show both the original and the Vietnamese edition when a translation exists.
  3. Decided Models. Through the AI gateway, compared by shadow runs and the replay eval.
  4. Decided Formats. Any format; no filter on print, ebook or audiobook.
  5. Open Starting route. A (free), B, C or D from section 11? Recommended: A, with B shadowed.
  6. Open Seed sites. Keep the nine seed sites and their modes, or add others now? Recommended: keep, add more from the form later.
  7. Open Where pages live. A separate "Book suggestions" list plus the latest three on the blog index, or every page in the blog index? Recommended: separate list.
  8. Open Publishing. Public at 07:00 with a Hold button on the console, or a private preview I approve first? Recommended: public at 07:00 with Hold.
  9. Open Monthly cap. Recommended: $10 hard cap, which only matters once a paid route is on.
  10. Open Delivery time. Recommended: 07:00 ICT.