Inspiration
In May 2025 a newspaper summer reading insert recommended ten books that do not exist, each attributed to a real author. Librarians have been getting those requests at the desk ever since: a patron arrives with a title and an author a chatbot gave them, sure the book is real. The librarian then has a few minutes to search two or three catalogs, say "I'm not finding it" to someone who feels accused of making it up, and send them away with nothing. I wanted the desk to end that moment with evidence and a book in hand.
What it does
Type the title the patron asked for. Ghost Shelf checks it against three sources at once (the library's own collection, Open Library, and the Internet Archive, or Google Books if you give it a key) and opens a drawer for each as it answers, with the result, the time it took, and a link to repeat the search yourself.
The card you turn to the patron says one of four things: no record in three sources, it exists and we hold it, it exists and we don't hold it (with an interlibrary loan link), or one source did not answer so the count drops to two. It never says "does not exist". It says where we looked, and it keeps the caveat that recent and small-press titles may be missing. It prints as a slip.
When there is no record, you add what the patron actually liked: a film, a band, an author, a book, a show. Qloo turns those into three books this library holds, each with the share every seed contributed ("because you named The Martian 57%, Radiohead 43%") and Qloo's affinity score. One click reserves the three in the demo holds list; another logs the invented title into a registry other desks can read. The registry ships with the ten titles from the newspaper insert.
There is a second desk for reading-list requests: a patron's form (liked, didn't like, avoid, format) becomes five to ten held books with reasons, you remove any you disagree with, approve, and a draft reply appears. Nothing is sent.
What makes it Qloo-powered
The recovery step is the part only Qloo can do here. A catalog can match an author. It cannot take "Like Water for Chocolate" and "Buena Vista Social Club" and return three novels the library has on the shelf, with a reason the librarian can say out loud.
How the calls work:
- Search resolves each thing the patron named to a Qloo entity (
/searchwithtypesset from the seed's kind: movie, artist, person, book, tv_show). - Insights asks for books that fit all of them together:
/v2/insightswithfilter.type=urn:entity:book,signal.interests.entities= the resolved seeds,feature.explainability=true,take=50, andfilter.results.entities= the Qloo entity ids of the library's own collection, sent in chunks of 200 ids. That last filter is what keeps the answer to books the library can hand over. - The holdings map behind that filter (ISBN to Qloo entity id) was built once with the key: 1,165 of the 3,000 held titles resolved to a Qloo book entity. It lives in the database, not the repository.
- Explainability becomes the reason line on each book, and the affinity is shown as "match 0.79, from Qloo".
- A request panel on the desk shows every call as it was sent (parameters, chunk count, a response excerpt) with the key redacted, and offers a copy as curl.
Take the key away and the shelf says so: the checks and the card still run live, and the recovery step shows a labelled sample mode instead of pretending.
How we built it
Next.js 16 on Vercel, with Server-Sent Events so each drawer opens the moment its source answers. Drizzle on Neon Postgres for runs, holds, the registry, the holdings map and a one-day cache of Qloo answers (the hackathon key rate-limits bursts). The collection is a real slice of Seattle Public Library's public-domain checkout data: the 3,000 most-borrowed book titles with ISBNs from January to September 2025. Covers come from Open Library by cover id, resolved once for the collection and served through our own proxy with a long cache, because Open Library rate-limits cover lookups by ISBN.
Every designed state of the desk is reachable with a URL parameter, and a Playwright script screenshots all twenty of them at three widths and fails on a horizontal scrollbar or a console error. A second script runs the whole demo three times with a reset in between, drops the connection mid-run to check that the drawers report it, and runs axe on four routes.
Challenges we ran into
Google Books rate-limits keyless requests from shared hosts, so the third drawer kept reading "did not answer". The Internet Archive's advanced search is keyless and fast, so it took the third drawer by default, with Google Books as an opt-in. The archive is full of uploads titled after famous books, so a match without an author needs an ISBN or a publisher on the record.
The holdings map was the other hard part. Qloo's Insights URL tops out near 10 KB, so the collection goes in chunks of 200 entity ids rather than one call. Matching 3,000 library titles to Qloo entities by title alone picked wrong editions, so the builder prefers an exact ISBN, then the same title within two years of ours.
The key arrived after the build was done, so the product had to be complete and honest without it. That is where sample mode came from, and it stayed as the fallback for when Qloo does not answer.
Accomplishments that we're proud of
The patron card. It was the easiest thing to get wrong, and the wording never calls the patron's book fake. The drawers opening one by one with real timings. Three novels appearing from a film and a band. The request panel, which shows the technical judge the calls rather than describing them.
What we learned
Explainability on Insights is what makes a recommendation sayable across a desk. "Because you named Radiohead" is a sentence a librarian will use; a score alone is not. Restricting results to holdings with filter.results.entities turns a taste graph of 250 million entities into something a small library can act on.
What's next
A real catalog behind the first drawer instead of the demo slice. The rest of the collection mapped to Qloo entities. The registry of invented titles shared between libraries, so the second desk that hears a title already knows it.
What is real and what is simulated (for the form's honesty section, if there is one)
The three checks, the card, the Qloo recovery, the reading-list recovery and the covers are live. Holds go to this demo's own holds table, not a library system. The registry is this demo's own table. The reading-list reply is a draft on screen and is never sent. The collection is real public-domain data from one library.
Pre-existing work and tools
Planning (idea research, a UI spec) came before the first commit on 19 October 2026; all code was written inside the hackathon window. Claude Code wrote and ran most of the code under the builder's direction, with the builder reviewing, testing and deciding what shipped. The video narration is a synthetic voice.
Built With
- drizzle
- internet-archive
- neon
- next.js
- node.js
- open-library
- playwright
- postgresql
- qloo
- react
- server-sent-events
- typescript
- vercel
Log in or sign up for Devpost to join the conversation.