Building LibbyScout: A Case Study
LibbyScout
A Chrome extension that surfaces public library eBook availability across all five NYC boroughs, directly on Goodreads book pages.
LibbyScout in action: Surfacing NYC library eBook availability directly on Goodreads book pages.
Conceived, built, and shipped in four weeks, March–April 2026. Three Libby-enabled NYC public library systems. All five boroughs. Public on GitHub.
The Problem
I was tired of my scattered TBR pile—across Goodreads, Notion databases, Google Keep notes and Google Docs pages, The New York Times "Want to Read" list, good old-fashioned paper notebooks—and even more tired of checking Libby for titles, running the calculus of where books were available and in which systems I had space for holds...all to walk away with a bunch of holds, reminders to recheck certain books later, and no actual eBooks in hand.
Even after consolidating my TBR on Goodreads (where I usually ended up anyway, Googling books to remind myself what they were about and why they might have landed on my TBR), the list was long enough to remain unwieldy—88 books, and it's been growing since. So I built a Python batch-checker to scan availability across my Libby libraries and output the results to a CSV file, which I could import into Google Sheets for easy scanning. That still required exporting and re-running the full TBR every time anything changed; didn't address the recurring need to refresh my memory about specific books; and required extra steps to make the output scannable at a glance.
I designed LibbyScout to address that friction. Using the search logic from the Python script as a proof of concept, I built a browser extension that lives directly on the Goodreads page—checking availability automatically, with no need for library exports, full-list re-runs, or leaving the page.
The Python Proof of Concept
LibbyScout began its life as a Python script on March 2nd, 2026 (available in the python-script folder of the GitHub repository for the Chrome extension). Initially named LibbyChecker, it was a batch-checker that pulled the "Want to Read" shelf data from a CSV export of a user's Goodreads library, queried each book via publicly accessible OverDrive metadata endpoints that underlie the Libby app, and generated a CSV file listing book titles and authors alongside availability status at each of the three library systems I subscribe to.
That first evening, having spent a concentrated amount of time triaging and updating my TBR on Goodreads, I'd wanted nothing more than to borrow a book. I'd then followed a creative impulse to solve the problem I faced—running selections from my still-88-book-strong TBR through Libby manually, in the hopes that one of them would be available to borrow immediately—by coming up with another solution. (It helped that I'd been looking for another excuse to work with Python, given my interest in UX research and research in industry rather than academic contexts, and this was a software-shaped problem.)
Once my script generated an initial CSV file listing availability, I imported it into a Google Sheet, color-coded it for easy scannability, and snagged a library book before bed: The Long Run: A Creative Inquiry by Stacey D'Erasmo. I'd discovered that book via Celine Nguyen, a writer I follow on Substack. I'd assumed it might not be owned by the public libraries I had cards for...let alone available to borrow immediately at two of them.
The script was working for me.
Refining the Search Logic
That initial output had served its purpose on the day—walking away with an eBook in hand—but it wasn't without its errors (some of which you might notice in the screengrab, and are discussed below). A few days later, on March 5th, I returned to the script to refine its search logic and, therefore, its reliability.
The Challenge
A key challenge was striking a balance. It became quickly clear that the search logic needed to be strict enough to reduce false positives, strategically permissive enough to handle the messiness of book metadata, and smart enough to account for edge cases such as transliterated character accents or short author names. Striking that balance while working with AI as a pair-programmer (Gemini for the Python script, Claude for the Chrome extension) required domain knowledge, critical thinking, and the judgment to push back against inaccurate results.
This dynamic played out repeatedly. The AI would mischaracterize outcomes in a flattering light, or propose a solution that worked only for an obvious case. Then, domain knowledge and strategic testing would reveal gaps in the outcome or surface new edge cases that a solution either hadn't accounted for or had directly introduced.
The Solution, Stated Simply
The core of the logic developed through this process is a two-query search (via the API) with a three-pass author validation funnel (applied locally, using the API's returns and the TBR data on disk). First, LibbyScout runs a strict search using both title and author; if that fails, it falls back to a title-only search with subtitles truncated, then validates the result against the expected author before accepting it.
The Fallback Search and the False Positive Problem
The fallback step was implemented because a strict title-plus-author search was too fragile: it failed on minor metadata inconsistencies between Goodreads and the library catalogs. Minor variations in author name formats would result in a book being marked as "Not Owned" by LibbyScout, despite being available when searched for directly in Libby.
To widen the net, the fallback query initially worked by dropping the author entirely and searching by title only, with subtitles truncated. So, for example, Against Breaking: On the Power of Poetry by Ada Limón became Against Breaking, and What Is Real?: The Unfinished Quest for the Meaning of Quantum Physics by Adam Becker became What Is Real? for the second-pass query.
This caused an apparent leap in the number of books "caught" by the search that the original version of the script had "missed"—a leap that Gemini was ready to celebrate:
I just ran a quick Python script in the background to compare your library_availability_v1.csv and library_availability_v2.csv row by row.
The results are genuinely fantastic!
The new fallback logic managed to rescue 29 books out of your 88-book pile that the original script had completely missed and marked as "Not Owned". That is nearly a 33% increase in successful matches, and best of all, 0 books were lost in the transition (meaning the fallback didn't break anything that was already working).
I was skeptical of that (laudatorily validating) outcome, and spot-checked specific books.
I knew, for example, that Against Breaking (amongst others) hadn't been released at the time. It had just landed on my radar from a New York Times overview of upcoming titles. Other books on my TBR had been drawn from academia and were less public-facing, like Coed Revolution: The Female Student in the Japanese New Left by Chelsea Szendi Schieder. I was skeptical that the public libraries I was querying would have such academic titles available as eBooks, and a manual search via Libby confirmed that hunch. Even beyond these specific books, the net had become too wide.
The Author Validation Funnel
In order to account for false positives arising when truncated titles caused a mismatch with other books that shared a partial title, a validation step was added to the fallback query. Rather than accepting the top result blindly, the script was modified to examine the author of every returned result and compare it against the expected author listed in the TBR. Only when these matched did it assume a book had been correctly identified.
The validation logic itself was subject to iterative refinement, with each strategy surfacing, during spot-checking, new edge cases that required targeted fixes.
Strategy 1: Normalization
TBR Example: Sixteen Ways to Defend a Walled City by K.J. Parker
The first validation attempt normalized both the expected author name and the found author name: stripping out punctuation and spaces so that, for example, "K.J. Parker" and "K. J. Parker" (inconsistently punctuated across different library metadata) would both reduce to "kjparker" before being compared. This fixed the punctuation inconsistency problem that had caused the strict search to miss certain books, like K.J. Parker's Sixteen Ways to Defend a Walled City. The issue hadn't been the search query, but the manner in which author names were compared after the fact.
Strategy 2: Last-Chunk Validation (A Name-Ordering Fix)
I realized, however, that while normalization worked for punctuation, it failed when a library had dropped a middle initial or inverted an author's first and last name. To account for the fact that Goodreads and library databases sometimes format full names in different orders, another logic was introduced. It split the author's name—listed as a single text string in the Goodreads TBR data (e.g. "Alix E. Harrow" in a single CSV column, rather than dedicated first and last name columns)1—into chunks, and grabbed the last chunk as the validation target. The thinking was that this presented a single, stable anchor (typically the last name) to search for, regardless of metadata inconsistencies around first and middle names.
Strategy 3: The 3-Letter Rule (An Initial Fix)
TBR Example: The Once and Future Witches by Alix E. Harrow
This introduced a new problem. Inverted name listings, such as "Harrow, Alix E.," would cause the script to grab "E." as its last chunk, and then attempt to validate against that. After normalization stripped punctuation and spaces, "E" would become just the letter "e"—a string that appears in a huge range of normalized author names, making it essentially unusable as a validation target due to the near-certainty of false positives.
To account for that possibility, Gemini proposed a rule that ignored any chunk shorter than three letters before selecting a validation target—a means of ensuring that the validation target selected by the script was always a statistically meaningful string.
As an aside, there's something of a happy developmental accident worth noting here, for completeness: when an inverted entry like "Harrow, Alix E." is processed, dropping "E." leaves the script targeting "Alix" rather than "Harrow"—that is, the first name rather than the last. Happily, this still returns a valid result. Because the matching logic checks for the presence of the target word anywhere in the normalized found author string (rather than checking for an exact full-name match), "alix" is a sufficiently unique identifier to validate correctly. In scenarios like these, the code functioned as intended even when it grabbed the wrong part of the name.
Strategy 4: Short Name Validation (On Algorithmic Assumptions)
TBR Example: Things in Nature Merely Grow by Yiyun Li
The 3-letter rule suggested by Gemini worked for initials, but broke validation for authors with legitimately short last names. I identified this gap because Yiyun Li is a favorite author of mine. With "Li" filtered out, the script fell back to validating against "Yiyun" instead. That happened to be unique enough to work without modification. However, because first names are less reliable validation anchors than last names, and because last names like Li are extremely common, globally, it was important that author validation not rely on happenstance of that kind.
The fix mattered beyond this specific case. The 3-letter rule encoded an assumption about name length that doesn't hold across naming conventions, particularly for East Asian surnames like Li or Ng, which are common, legitimate, and short. Building validation logic that systematically filtered them out would have quietly excluded a substantial category of authors from reliable results.2 A strategic exception was created for two-letter chunks, ensuring authors like Yiyun Li and Celeste Ng were validated against their actual last names and didn't slip through the gaps in the search logic.
Fixing the length assumption led naturally, during additional spot-checking, to the next gap: character assumptions. Normalization handled punctuation, and the last-chunk logic handled name ordering. However, neither could account for cases where the characters themselves differed, not due to formatting, but due to transliteration.
Strategy 5: Transliteration
TBR Example: The School of Night by Karl Ove Knausgård
In the initial TBR availability sheet generated by the original LibbyChecker script, The School of Night by Karl Ove Knausgård was listed as "Not Owned" across all three of my libraries. Given the profile of the author, I suspected an inaccurate return. Directly checking Libby revealed that in actuality, the volume was owned by all three libraries, and was in fact popular enough that it was only available to hold across the three; nowhere was it available to borrow outright.
I concluded that the issue was variations in transliteration practices (the "å" being transliterated as "aa", in this case).
To account for that and similar cases, a fuzzy matching logic was introduced. This applied an 85% similarity threshold to the author's name, comparing the final significant word from the Goodreads data against the equivalent chunk from the API response (a more reliable target than the full name, given the formatting inconsistencies already discussed).
The resulting search logic successfully identified The School of Night as being owned by all three library systems, without introducing additional false positives or losing books that had been previously checked accurately.
A Known Limitation: Multi-volume Graphic Novels and Collected Comic Book Volumes
Limitation Example: Giant Days, Vol. 8 by John Allison
One outlier that I identified was multi-volume graphic novels. In my Goodreads data, I had Giant Days, Vol. 8 by John Allison on my TBR. The Python script listed it as "Not Owned" across all three library systems. Conducting a manual search, I found it in Libby, available at BPL and NYPL. It was listed in the library catalog as Giant Days (2015), Volume 8. After investigating listings for other volumes in that series and for other comic book collections across both Goodreads and Libby, I decided to make peace with this version of the script being unable to account for comic book collections or multi-volume graphic novels.
There's no consistent standard for how graphic novel volumes are entered into library systems. Moreover, introducing a fuzzy matching step to the title search (rather than for author name matching, as discussed in the transliteration fix) would likely break the algorithm: it would need too low a threshold to account for partial matches, and even a minute threshold would likely mix up volumes (mistaking "Volume 9," for example, for "Volume 8"). The inconsistent volume cataloging standards across Goodreads and library systems made reliable matching difficult without introducing false positives elsewhere.
I marked this as a known and acceptable limitation for the initial version of LibbyChecker, and moved forward.
The Outcome of the Refinement
The iteratively-refined author validation process, developed in a concentrated single-session sprint on March 5th, handles several modes of messiness in library metadata: punctuation inconsistencies (K.J. Parker vs. K. J. Parker), missing middle initials, transliterations (Knausgård vs. Knausgaard), and authors with short last names (e.g. Yiyun Li) that simpler matching logic would filter out.
With the multi-pass query and validation strategy in place, the additional books "found" across my libraries dropped from 29 to 4: fewer books, but a sounder, more reliable algorithm.
From Personal Tool to Public Product
I'd gotten the Python script to the point where I was happy enough with its logic that I could personally rely on its results. I let it sit for a few days. When I came back to it, on March 9th, it was to consider how I might share it.
The Streamlit Exploration
The first possibility I explored was creating a Streamlit web app. Streamlit felt like a natural home for LibbyChecker. LibbyChecker was a Python script, and Streamlit is an open-source Python framework. Much of my refinement work on LibbyChecker had centered on resolving book identities across two inconsistently formatted databases, and Streamlit is tailor-made for building interfaces around that kind of underlying Python logic. In my own usage of LibbyChecker, I'd pull the availability CSV it generated into Google Sheets for color-coding and parsing; and Streamlit's affordances include charts and interactive dataframes for that kind of data presentation.
While I did build a rapid prototype of a Streamlit web app with Gemini to see the possibilities in action, this direction was abandoned quickly for a number of design-specific reasons.
The Streamlit web app could, indeed, accept a user-uploaded Goodreads library CSV, run the Python script, and present the outcome visually (as well as generating a raw availability CSV for users to download). That said, I realized that I had no interest in spending time on beautifying the Streamlit output presentation as the next challenge. This was partly because I'd already noticed some points of friction in my personal usage of LibbyChecker. In considering a public-facing version of the tool, one that functioned as a live product rather than a packaged showcase of what I'd already built, I recognized that those same points of friction would likely become user pain-points in a public-facing context.
A key issue was that the Streamlit results, beautified or otherwise, wouldn't persist when the app was refreshed: the entire TBR would need to be run through LibbyChecker again upon each load. Alternately, the user would have to download the output CSV (as I had, initially) and set it up as a spreadsheet in a third-party program. I could design the Streamlit web app to output a well-formatted spreadsheet for download at the same time it presented an in-browser version via dataframes. That said, a user would still need to run their updated TBR through the web app again, each time they needed an updated availability sheet.
I'd already noticed this friction in my own usage of the script. It's worth noting that the friction registered—as is often the case in design—on both a functional and an emotional level. My TBR, even after I'd triaged it, was 88 books long at the time. I felt bad each time I had to run the entire list through LibbyChecker, purely based on the number of API calls a single update entailed via batch-checking. And beyond that affective response, a longer TBR meant a longer wait time for the process to complete. I could imagine some users having much longer lists, and so having a longer wait time (especially given the pause coded in between each query, in keeping with rate limiting best practices) when they used the script.
Moreover, quite aside from the lack of persistence and the length of a potential wait while the batch-checking completed, anyone wanting to use LibbyChecker would be presumed to have a Goodreads account with an actively maintained or substantive "Want to Read" shelf; and would need to log in to Goodreads, export their library, and upload it to the Streamlit app before they could begin using it. That was a lot of steps, if the stated goal was less friction between reviewing books on a TBR and being able to snag an eBook from the library.
What If the Tool Lived Elsewhere?
I started considering what it would look like if the tool didn't just output a static sheet that would need to be referenced and updated proactively.
What if, instead, it ran closer to the point of origin? At the moment of discovery, rather than after the fact? I tended to find myself on Goodreads book pages when looking up information about a book, whether or not a book was actually on my TBR at that point. What if my LibbyChecker lived there?
Not only would that scenario simplify the steps to my specific goals—getting an eBook in hand quicker—but, for other potential users, it wouldn't even require a Goodreads account, let alone a multi-step input and output process on a different website.
That thinking led me organically to considering a Chrome extension, one which triggered when a user landed on a Goodreads book page.
Validating Feasibility Before Committing
Before committing to a Chrome extension, I needed to confirm whether that architecture was actually viable. The key question was whether a browser-based tool could query OverDrive's API directly, given that standard web pages are blocked from making requests to third-party servers for security reasons. Since the Chrome extension version of LibbyChecker would work (ideally) by injecting an availability display panel into a Goodreads page directly, it was possible that it might get tripped up by CORS (Cross-Origin Resource Sharing) security features. CORS prevents a script running on one website (like Goodreads) from requesting data from a different website (like the OverDrive/Libby API). If CORS rules applied to the LibbyChecker extension, the avenue would be a technical non-starter.
I built a minimal probe to test whether a Chrome extension could bypass that restriction. Happily, it could. As I learned during this process, Chrome extensions have a special bypass for CORS; explicitly declaring the Libby API URLs in the extension's manifest.json allowed the extension to safely fetch data across different domains. The CORS probe functioning meant a green light for extension development—a process that came (excitingly, for me) with its own problems to solve and opportunities to engage.
A Transitional Aside: New Software Shape, New Context, New Name
The question of sharing meant thinking about LibbyChecker in a new way—as a public-facing product rather than a personal tool. Having initially created it in a single-night sprint, I'd had a chance to sit with the project for several days, at this point, too. In translating it to a new form factor and context, I wanted to rename it.
LibbyChecker became LibbyScout: a name that to me felt a little more active, and a little more friendly at the same time.
LibbyChecker (fairly arbitrarily named, of necessity) had been about batch-checking, and evoked for me a sense of the archive—both the archive of my TBR, and of the libraries it was checking that TBR against. LibbyScout felt in my mind like it lived on the fieldsite, in a way, since it was embedded on the Goodreads books page directly. It wasn't checking the archive; rather it was running off, as needed, to scout out availability from the field. (I'd considered "LibbyRanger" as a possibility, for similar reasons.) I would pick up thinking in terms of such images and metaphors when I turned to designing the icon for LibbyScout. But before following that thread, I turned to building the extension itself.
Building the Extension
The core logic of the Python script was translated directly into the JavaScript that drives the Chrome extension: the same two-query strategy, the same three-pass validation funnel.
The initial query is triggered when the user loads a book page on Goodreads. LibbyScout automatically pulls the book's details from the page, queries the OverDrive API for the book's availability in the three major Libby-enabled NYC public library systems, and displays that availability in an unobtrusive panel directly on the page itself, so the user can immediately see where it's available to borrow or to put on hold.
Beyond this baseline implementation, which I got up and running the same day (March 9th), the new form factor both provided opportunities for additional features, and gave rise to edge cases of its own.
New Extension-Specific Features
Some new features were added fairly immediately, as natural extensions to my mind.
For example, library toggles, which connected back to my affective response to large-batch queries. My Queens Public Library card needed to be renewed, and I didn't like the idea of making API calls for a library I knew I wouldn't be borrowing eBooks from at the time. To solve for that, I implemented a settings panel wherein queries to particular libraries could be toggled off. Libraries that had been switched off would be displayed on the LibbyScout availability panel with a dimmed, struck-through font. That approach was taken for two reasons: because having the library remain visible helped with debugging during the development and refinement process; and so that, ultimately, users would have a visual reminder of available libraries, in case the LibbyScout extension wasn't pinned (making the settings panel a step further away, out of sight and perhaps out of mind). For those users, the toggle feature would allow customization based on card ownership rather than (as was in my case) temporary deactivation.
Another immediately-added feature was a flag that noted when a book had yet to be released. This displayed a "Not yet released" flag alongside the expected release date for unreleased titles, to provide more context for certain books listed by LibbyScout as "Unavailable." In this case, unlike library toggles, I made the decision to still fire the API call to any toggled-on libraries. Pre-order holds do crop up in Libby sometimes, and that's precisely the kind of information that I (and, I suspected, other avid readers) would want to know about.
New Edge Cases
The new software shape introduced its own problems to solve, too. One in particular relates directly to the "Not yet released" flag addition described above.
Goodreads maintains individual book pages for multiple versions of a title, which led to an interesting edge case for the now-visual tool: an upcoming version of an already-released book, such as a paperback release for a book initially released as a hardcover and eBook. On the Goodreads page for Brandon Sanderson's Wind and Truth (the fifth novel in Sanderson's bestselling Stormlight Archive), I noticed both a "Not yet released" flag and the availability of the book at multiple libraries. The combination made me wonder, first, whether there was a bug in the search query being fired by LibbyScout. Next, a gut reaction where I second-guessed my awareness of the genre. I could've sworn that the book had been released the previous year (it was a much-anticipated title at the time), but still wondered briefly whether the libraries had erroneously made a highly-anticipated title available early.
It became clear, quickly, however, that I'd merely landed on the Goodreads page for the upcoming paperback edition. LibbyScout was using the title and the author of the book to correctly identify the eBook as being available, but was also reading the listed release date as a trigger for the "Not yet released" flag.
To solve for this, I investigated the DOM structure of Goodreads pages, to see how they were displaying and labeling release date information. I discovered that for entirely new releases, Goodreads listed an "Expected publication" date. On the other hand, for new editions of previously-released books, it listed a "First published" date. I updated the LibbyScout logic so that if a "First published" date was found within the Goodreads book page, the "Not yet released" flag was suppressed, regardless of the edition in question.
This handling error was caught in the context of a recent release; that said, it was a crucial element to account for, given both the number of editions that Goodreads tends to have individual book pages for (such as localized editions, eBook editions, paperbacks, hardbacks, re-releases), and to account for works—such as those in the public domain—that have been released by multiple publishers over time.
Beyond targeted testing and usage, moving LibbyScout (now a tool designed to be shipped publicly) through the product development life cycle brought additional needed adjustments to light. For example, when capturing clean screengrabs for the MP4 demo of LibbyScout embedded in the GitHub README, I set up a new Chrome user profile and captured Goodreads pages without first logging into my personal account. Depending on how I'd arrived at a book's page (i.e. via Google search results or Goodreads' own search bar), I noticed that LibbyScout was failing to fire. What felt like it could be a catastrophic failure turned out to have a simple cause and an easy fix.
Since I wasn't logged into Goodreads directly, I was sometimes landing on locale-specific URL variants (/en, /es, /fr) for book pages, and the URL trigger was failing to fire as a result. I implemented a wildcard pattern into the target URLs, so that LibbyScout would activate regardless of locale variants, including not just those I'd personally encountered, but any others that Goodreads employed, as well.
A Polishing Pass
As the last development anecdote might suggest, I had at this point moved into a polish and presentation stage. I took a copyediting and refinement pass at both the Python script (which I intended to post within the LibbyScout repository alongside the extension itself) and each Chrome extension file.
This included reviewing all the AI-generated code by hand. I had been doing this to some degree for much of the process, to use the opportunity to understand more concretely how certain elements of the script and the extension were structured; to refresh my knowledge of CSS and HTML; and to keep token overheads low (I was using the free tier of Claude at that point) by making copy and style adjustments directly rather than by triggering an AI-driven rebuild (one that might, unbeknownst to me, revert elements of the code or make changes I might lose track of). At the same time, I cleaned up the annotations in the code, often in order to edit out for clarity the conversation that Gemini, in particular, seemed to have been having with itself and with me while generating or debugging its code.
Annotations aside, the visible adjustments ranged from the minor to the holistic. Minor tweaks addressed the small things that I nevertheless cared about at this stage: modifying the width of the LibbyScout panel, for example, and capitalizing the "O" in the "Off" label that appeared for switched-off library systems. A more holistic change involved restructuring the LibbyScout header so that the extension label was stacked above the book title, and dynamically truncating longer book titles via CSS. That restructure was motivated, in part, by the fact that I wanted to create a custom icon for LibbyScout.
Visual Identity
Throughout the development of the Chrome extension, I'd used a hard-coded emoji in the LibbyScout display panel and left in place the default lettermark for the taskbar icon. Given my background in illustration, familiarity with entertainment design, and interest in design broadly, I thought designing a custom icon might be a two-fold opportunity. For me, it would be a project-driven opportunity to build on my existing skillset (in illustration) by trying out a graphic design workflow. At the same time, it would be an opportunity to deliver a more polished experience for interested users.
Icon Design
The compressed design process I employed for the logo included creating a design brief, ideating via word lists and then via rapid sketching, before narrowing down to select and refine a final concept.
The brief included LibbyScout's core functionality, the context in which the logo would appear, the constraints associated with the latter (for example, since the icon would appear in the Chrome toolbar it needed to be readable at 16px), and what it didn't need to do (for example, despite my ideation naturally moving in this direction, the visuals associated with the icon didn't need to anchor a broader, illustrated brand campaign).
The word lists ranged across the literal (nouns and "what it is" associations, like "Bookmark," "Ranger"), the functional (what it does, like "Checks" and "Scans"), and the metaphorical (representational associations like "Path" or "Study").
During rapid sketching, I kept returning to architectures that I associate with the liminal: windows, for example, and bridges. Reading is, for me, a liminal activity: a way of exploring and becoming, not to mention a window to other worlds and ideas. (This thread carries through to the MP4 demo I created for LibbyScout, which begins with Book Lovers by Emily Henry and ends with Alix E. Harrow's portal-fantasy, The Ten Thousand Doors of January.) I liked the idea of a bridge, in particular—something that in relation to LibbyScout (a tool designed to help bridge the gap between your TBR and having an eBook in hand) could sit across literal, functional, and metaphorical registers.
The initial concept I decided to refine further included a representation of a scout on a bridge made out of books. I had an inkling that a representational figure—no matter how abstracted—could fail in the face of sizing constraints, and already had a backup option in mind. That said, I wanted to explore this direction for learning opportunities and future possibilities.
In my current illustration workflow, I employ Blender to create exploratory 3D mockups and reference images. These untextured scenes are interim artifacts for digital illustrations developed and finalized in Procreate and Photoshop, but I have future projects in mind that would be 3D-first, and animated. The scout character in the initial direction for the LibbyScout logo was an excuse to rig a rudimentary blockout while exploring poses and framing. If it had moved forward, it would have been an opportunity to model a complete character—one that would be more cartoonish (and therefore more approachable) than the more realistic figures that appear in my current illustration portfolio, and could have been an asset I would return to while exploring texturing, lighting, and eventually animation in Blender.
While the rigging worked, the logo, I realized, wouldn't. Having tested out possible compositions via the mockup, I pivoted to my alternative idea: a staircase made out of books. This direction had some of the same underlying logic—liminal architecture that bridges the gap between platforms—but without quite the same literalism. It presented learning opportunities of its own: I lit and textured the 3D model to explore a 3D render as final design output, before discarding that avenue in favor of hand-drawn finals via my usual illustration workflow.
Moreover, there was an emotional element to the design that resonated with me, and which I wanted to honor given the origins of LibbyScout as a personal project. I grew up reading with three sisters, the four of us trawling bookstores and libraries and swapping books across genres and tastes. The staircase in the LibbyScout logo has four steps. One for each of us, making the staircase an architectural metaphor that connects to not only the books that I am reading, nowadays, but also a shared personal history of reading.
Implementation and Packaging
Replacing the placeholder emoji with the custom icon required more implementation adjustments than a simple image swap. Because LibbyScout injects its availability panel into a third-party page (Goodreads), Chrome's security architecture requires an explicit declaration of which extension assets are accessible to external pages, and a file path needs to be generated at runtime (as a standard relative URL would be inaccurate). Lastly, I implemented a flexbox adjustment to handle alignment, given that an image element sits differently alongside text than an inline emoji does.
Designing the icon also prompted me to investigate licensing norms and practices in this space more closely. I ultimately landed on a split licensing decision. I employed the MIT License for the code, retained copyright for the visual assets, and documented that split in the public LICENSE and README files in LibbyScout's GitHub repository.
Going Public, or Quality Assurance Through Real Use
Alongside developing the LibbyScout logo and my copyediting passes, I'd been drafting and refining a README and creating a demo MP4 for the GitHub repository in advance of making it publicly available. I flipped the switch from private to public on April 1st.
I'd shown LibbyScout privately to a few people prior to that point, but this was the first time it would be accessible to others for real-world installation and use. My partner was the first user. Her installation went smoothly, which meant the README instructions were working. The extension, broadly, functioned as designed. That said, she discovered another false positive near-immediately: The Ten Thousand Doors of January by Alix E. Harrow.
Discovering and Diagnosing the Bug
Recall that Alix E. Harrow had been part of my personal TBR dataset (in the form of her second novel, The Once and Future Witches—which turns out to be a critical point), and so had been part of the query and validation funnel for author names I'd developed for the Python script that preceded LibbyScout. Later, when collecting clean screenshots for the MP4 demo, I'd used Ten Thousand Doors as an example—indeed, given its meta focus on stories and my personal associations with architectural portals such as windows, doorways, staircases, etc. (discussed previously in this case study, and in my personal writing), it was the final example in that demo. Given the development context, I didn't pay too much attention to the book's listed availability: Available to Borrow at NYPL, and to Hold at BPL and QPL.
My partner had reason to have looked more closely at the book: her book club was reading it, partly on my strong recommendation. She'd been unable to borrow it from NYPL via Libby. The mismatch surfaced a particularly interesting case that I hadn't at that point accounted for.
It turned out that NYPL didn't hold an English eBook edition of The Ten Thousand Doors of January—but it did own an eBook of the French edition, Les dix mille portes de January.
Since I don't have direct access to the OverDrive database and API logic, identifying the cause of the mismatch involved another round of systematic testing and API behavior analysis.
This time, I focused on Goodreads pages for translated editions. LibbyScout behaved correctly when the Goodreads page language matched the available edition. Searching from the French Goodreads page for the same title correctly surfaced the French edition's availability at NYPL, and correctly showed it as "Unavailable" at BPL and QPL, which didn't hold the French edition. It appeared that the bug occurred when the Goodreads page was for a language edition a library didn't hold, and the API silently substituted the closest available edition.
My testing suggested that OverDrive's search treats the query as keywords rather than exact matches, returning translated editions when the author name and enough title words match and no closer direct match is available. The asymmetry is telling: searching with the English title returns the French edition, likely because the word "January" survives into the French title as a keyword anchor; the reverse search returns nothing, since the French keywords have no overlap with the English title. OverDrive's relevance ranking appears to behave unpredictably in cross-language cases such as these.
The Fix and an Inclusive Design Question
Since LibbyScout had at this point shipped publicly, I took the opportunity to log this bug as Issue #1 on GitHub, making my diagnosis, reasoning, and proposed solution public.
Stated simply, the issue is that when a book exists in a library's OverDrive catalog only as a translated edition, the strict search (a title + author query) can return that edition as a match and report it as available. My proposed fix, planned for a v1.1 update of LibbyScout, is to add title similarity validation to the strict search result before accepting it, using the same fuzzy matching logic already applied to author names in the fallback search.
I'm favoring this solution over the more technically-driven fix represented by language tag validation (filtering out results that don't match the expected language) because title similarity validation is more inclusive. A language filter would systematically exclude bilingual users searching for non-English editions, which feels like the wrong trade-off for a tool designed for one of the most linguistically diverse cities in the world.
Product Scope and Positioning
LibbyScout provides five-borough coverage in NYC by simultaneously querying the three Libby-enabled NYC public library systems: NYPL (which services Manhattan, the Bronx, and Staten Island), BPL (which services Brooklyn), and QPL (which services Queens).
Because LibbyScout runs entirely in the user's browser—scraping the Goodreads page, querying OverDrive directly, and displaying results in the injected panel—there's no intermediary server, no usage analytics, and no stored data of any kind. Reading habits are genuinely sensitive: what someone is reading or wants to read touches on intellectual freedom in a way that library systems take seriously. LibbyScout's architecture ensures that a user's reading habits never leave their browser, and that there's nothing to breach, log, or misuse. This was a consequence of building the simplest viable architecture. As an outcome, it's also well-aligned with the specific needs of this domain and this type of tool, and with the humanistic ethos I bring to my work as an interdisciplinary researcher.
LibbyScout's code is provided to users under the MIT License. Copyright for visual assets, on the other hand, is fully retained through a copyright carveout.
The initial release of LibbyScout is scoped to cover Libby-enabled libraries in NYC only, as a deliberate constraint. As such, it functions as a focused, complete MVP, but with clear expansion paths, discussed below.
Future Directions
I've identified a number of future directions and features for LibbyScout, ranging from functional fixes and additions to open design questions. These include:
- Clickable book titles in the LibbyScout panel, which would trigger a direct Libby search for closer-to-one-tap borrowing.
- Audiobook availability displayed alongside eBook results.
- Support for additional browsers. Straightforward to implement, requiring a single manifest addition.
- A v1.1 fix, discussed earlier in this case study, which implements title similarity validation on strict searches to catch false positives when a library only holds a different language edition of the book a user is querying. As part of this work, I would also evaluate consolidating the current two-pass author length validation into a single, cleaner rule.
- An "also available in [language]" flag. The v1.1 fix described above opens a door to a broader design question: the possibility of a future version of LibbyScout that, rather than filtering out non-matching editions of an eBook, surfaces other-language editions as additional options. This could serve multilingual users whose intent is to find the work regardless of edition, but would require a concentrated exploration, testing, and architecting period.
- A zip code lookup in settings, to allow users to find, query, and display library systems beyond the three NYC-based systems in the current version.
- A fallback flag and link that routes book details to a Bookshop.org search when a book is unavailable across all queried libraries, providing a path to supporting local, independent bookstores rather than the dead end users currently encounter in those scenarios. The current version of LibbyScout is intentionally non-commercial in scope. A future version built around a more formalized integration or partnership model could revisit affiliate links as a logical monetization strategy.
Closing Reflections
I took LibbyScout from concept to a shipped product in four weeks. The initial Python batch-checker was conceptualized and built on March 2nd; the Chrome extension was made publicly available 30 days later, on April 1st.
I developed it, initially, not as a portfolio exercise but to solve a personal problem. As a project, it touched on domains and interests that I've been embedded in for a long time.
I've been reading books and stories on screens since well before the advent of modern eBook readers. I had fairly early access to the internet: we had a computer in our household, growing up, since the days of Windows 3.1, and I distinctly remember connecting to the internet via Netscape Navigator. I grew up reading both fanfiction (during the peak of the Harry Potter era), and reading online copies of books that were at the time unavailable to purchase in Pakistan. In high school, when my father gifted me a PalmPilot given to him by a colleague, I was quick to use it to read stories on the tiny screen.
Given both this personal history and my time in academia, I've been connected to archives, online and otherwise, not only through traditional cultural institutions such as university libraries, but also through what Abigail De Kosnik has called "rogue archives" for cultural memory, such as fanfiction repositories.3 Across such spaces, the question of what gets categorized and how are important ones to think through.
Aside from being a reader, I'm broadly interested in the world of books and reading. I've been a longtime listener of podcasts such as Book Riot and The Book Review; have completed a publishing training course; and am connected to a literary ecosystem through workshops (I'm an alumni of Tony Tulathimutte's CRIT Workshop), NYC-based events and institutions, and the broader world of Substack.
All this is to say: I've spent a lot of time thinking about books, reading, the culture around them, and the ecosystems through which we access them. LibbyScout solved exactly the kind of problem—small in scope, but meaningful to me—that sits at the intersection of those interests.
At the same time, once I decided to share LibbyScout, working on it afforded me an opportunity to think about public-facing tools in ways that I've been actively looking to practice and demonstrate, alongside opportunities to learn by doing at the intersection of culture and technology.
As described in this case study, developing LibbyScout required the ability to identify friction; prototype iteratively; maintain quality control through domain knowledge; explore directions to build capabilities (with future projects in mind) as well as discard directions through reevaluation; document the development process;4 problem-solve and maintain quality control pre- and post-launch; and to scope and ship a viable product.
I'm excited to share LibbyScout, and am carrying the experience gained by developing and shipping it into other projects.
If you'd like to try out LibbyScout for yourself, you can do so via its GitHub repository. You can see more of my work, and follow future projects, at my website. My more personal writing—essays that take books I'm reading as a point of departure for thinking across domains—lives at my Substack, ResonanceNotes.
Notes and References
1. The Goodreads export also includes an "Author l-f" column with pre-inverted names...something I noticed only after building the extraction logic from scratch. Happily, the current logic is ultimately more robust in handling API response data, since it accounts for messiness on both sides of the comparison: in the Goodreads data and in the data returned by the API. ↩
2. Patrick McKenzie, "Falsehoods Programmers Believe About Names," Kalzumeus Software (blog), June 16, 2010, https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/. A well-known reference in software engineering, documenting (among other things) the assumption that names are always longer than two characters. ↩
3. Abigail De Kosnik, Rogue Archives: Digital Cultural Memory and Media Fandom (Cambridge, MA: MIT Press, 2016). ↩
4. Indeed, building LibbyScout and then writing this development narrative prompted me to create a comprehensive documentation system that I'm continuing to refine and am actively employing across other projects. ↩