A polished Chrome (Manifest V3) extension for readers on Archive of Our Own. Resume where you left off, theme the reader to taste, mute tropes you don't want to see, and keep a private library with notes. Everything is stored on-device.
Unaffiliated with AO3 / the Organization for Transformative Works.
AO3 has millions of devoted daily readers who already mod the site with fragile, technical userscripts (AO3 Savior, reader tweaks, etc.) — proven appetite for tooling, but no polished, one-click, freemium extension occupying that slot. The audience is sticky and passionate, the problems are recurring, and every feature here runs client-side, so there's no server to host and no per-user cost. That's the combination the 2026 extension market rewards: narrow audience, real pain, near-zero marginal cost, freemium Pro.
Free
- Resume reading — remembers your scroll position per work/chapter and offers to jump back in.
- Reading themes — Paper / Sepia / Dusk / Midnight, plus typeface, text size, line spacing, and line width controls. Applied live.
- Accurate reading time in the corner toolbar.
- Progress bar across the top of long chapters.
- NSFW filter — a one-click toggle that redacts a built-in list of slurs and SA/abuse trigger words (in the summary, notes and prose) to a solid bar. Click a bar to reveal (handles false positives like "Maine Coon"); passive reading never reveals. Toggles live.
Pro
- Library with private notes — save works, set status (To read / Reading / Finished), jot notes, search and sort. Backup and restore via JSON.
- Mute filters — hide works on any listing by tag/trope, fandom, or author,
with
*wildcards. Nothing is deleted — muted works collapse to a "hidden because…" row with a Show anyway link. Edits apply live (no reload). - Word masking — your own custom list, replaced with
****across the summary, notes and prose. Click a masked word to reveal it. Reversible/live. - Highlighter — select any passage and highlight it; highlights persist across reloads/closures, and "resume" jumps straight to your most recent one — a manual bookmark that's exactly where you stopped. Click a highlight to remove.
- EPUB export — download any work as a valid (XHTML-clean) EPUB, built entirely in the browser.
- Open
chrome://extensions. - Toggle Developer mode (top right).
- Click Load unpacked and select this folder (
Exte). - Visit any work on https://archiveofourown.org and look bottom-right.
Pro is sold through ExtensionPay (extension id
ribbon-ao3), which handles Stripe checkout, receipts, and paid-status sync —
no server of our own.
- Model (Option 1): free tier forever + 1-month free Pro trial +
$4.99 one-time to keep Pro. Set in
src/lib/licensing.js:TRIAL_DAYS = 30,GATE_EVERYTHING = false. - Switch to Option 2 (whole extension paywalled after the trial) by setting
GATE_EVERYTHING = true— that's the only change needed. - From the popup: Start 1-month free trial or Get Pro — $4.99. During the trial the popup shows days remaining.
Testing without paying: use ExtensionPay test mode (test Stripe card
4242 4242 4242 4242) — start a trial or "buy" and Pro unlocks with no real
charge. There is intentionally no offline license-key path, so there's no
free-Pro backdoor in the shipped code.
Pricing/products are configured in your ExtensionPay dashboard — set the one-time price to $4.99 and enable the free trial there to match the in-app copy.
The Pro gate lives in src/lib/licensing.js and is
backed by ExtensionPay:
isPro()is the single gate the whole app asks. It returns true if the user is paid, within the 1-month trial (trialStartedAt+TRIAL_DAYS), or has a valid offline dev key.status()powers the popup (paid / on-trial / days-left).openTrialPage()/openPaymentPage()hand off to ExtensionPay's hosted Stripe pages.onPaid()unlocks open tabs instantly after purchase.- The service worker runs
extpay.startBackground()so paid status syncs across the popup, content scripts, and library page.
Because ExtensionPay tracks status per account on their server, you get real enforcement: seat limits, revocation, refunds, and trial expiry are handled for you — none of the "a client-side key can't be revoked" problem. The offline checksum keys remain only as a dev/comp convenience and are clearly secondary.
Setup checklist (mostly done):
- ✅ ExtPay SDK vendored at
src/lib/ExtPay.js, wired into background, popup, content scripts, and library. - ✅ Extension id
ribbon-ao3set inlicensing.js. - ⬜ In your ExtensionPay dashboard: connect Stripe, set the product to a $4.99 one-time purchase, and enable a free trial.
manifest.json MV3 manifest
src/background.js service worker (seeds defaults, opens library)
src/lib/
ao3.js read-only AO3 DOM parsing (the one file to fix if AO3 changes markup)
storage.js chrome.storage wrapper + data shape
licensing.js the Pro gate (ExtensionPay + trial + offline dev keys)
ExtPay.js vendored ExtensionPay SDK (do not edit)
epub.js in-browser EPUB builder + tiny STORE-only ZIP writer
src/content/
reader.js / reader.css the on-page experience (theme, progress, toolbar, highlighter, masking)
blocklist.js Pro mute filters on listing pages
src/popup/ settings + Pro unlock UI
src/library/ the saved-works dashboard (Pro)
icons/ 16/48/128 px
- AO3 markup is the one fragile dependency. If a feature breaks after an AO3
redesign, the selectors are centralized in
src/lib/ao3.js. - The only network calls are ExtensionPay's (to
extensionpay.com, for paid status/checkout). No analytics, no other remote code. Permissions arestorage+ the AO3 and extensionpay.com hosts — keep this in mind for the Web Store privacy disclosure.