Work Text:
If you've already read / loaded the script from Userscript to return tailored kudos thanks' and styled comments' visibility client-side, then you know the drill. Just what it says on the tin. This next one's a userscript for inserting a new row into your list of stats.
Oh yeah, sure: with that little baby under my belt, I was feeling cocky.
I knew where to aim the fetch for this series' bookmarks' sum script, though that ended up being executed very differently from what I had intended; I knew where to aim it to uniquely identify the user from the rendered DOM (or obviously straight from the URL with currentUrl = window.location.href;); I dug through a bunch of scripts on Greasy Fork (?q=AO3) and gist.github.com (q= "==UserScript==" AO3) and learned some fascinating bits, which drove some changes in my earlier script; I considered that maybe the results weren't coming out as expected due to AO3 treating a script's requests differently from a simple button-click to go to the series page and look for myself. I didn't know nearly enough to put it all together, and in the end it was clear that there were huge gaps in what little I did know — well, at least those two parts I expected, anyway.
That said: I dug, I poked, I learned, and I was absolutely flummoxed by this one, and it turns out that it really was light years out of my league. For [humiliating] transparency, I must admit that I ended up turning to Google “AI” (LLM) with all of my notes, and went from there. Yes, my own work went into this, but no, it wasn't mostly me. 😞 From the level of the result, it's clear that the goal was so many light years out of my league that I couldn't see the scope of difference in approaching the problem (maybe not twenty minutes into the future, but definitely so right here, right now: even now, seeing the result, chunks of it I can read, chunks I can't). It wasn't a quick-fix either, as you can see from the version number.
It doesn't matter that it was JS instead of some prompt LLM-slop “fic” that I wouldn't piss on to put out a fire, the fact remains that I'm not proud of this.
Because of that, I can't present this proudly, but it does work (for me, in Violentmonkey, on Firefox, in a WIN 11 environment). I have 14 series at the time of this writing, so we'll see how it does when/if I reach 21+ (or if any readers use this script and have 21+ series, themselves; I searched for a while, but didn't find any).
I know that it's fine for me, at the time of this writing, in Violentmonkey on Firefox (because Chrome doesn't support it anymore, with the whole Manifest V2/V3 debacle; for that, most people seem to use Tampermonkey, which apparently is also G2G on Microsoft Edge, Safari [though it's a payware version there, so you might want to look at quoid /
userscripts for a freeware manager on Safari ], Opera Next, and Firefox), in a WIN 11 environment. There are apparently some ways for iPhone and Android, but I live on my desktop, so can't speak to how janky they might be. (And whichever device / browser / userscript manager combo you're using: please DO let me know if the script works, fails, or does anything unexpected! This could be valuable info for other readers.)
I did do some footwork, to be clear, but I can't take much credit for this in the end.
Come to think of it, I suppose that this could be expanded upon in a way to indicate somewhere when a fic or series has received a new bookmark (and which one(s)), but I'm not about to pursue that idea.
Incidentally, I tested the thousands-place comma notation by adjusting let total = 0; to a value of let total = 1000;, and sure enough the format was as one would expect (unsullied: the comma appeared where it does in the regular entries).
UPDATE 12 Feb: I was mucking with some more JS for a friend, and ended up writing a userscript manager installation How To, and used Tampermonkey as my example since I already had Violentmonkey. As a result I found that it happens to function perfectly well in Tampermonkey (though I make no claims about a zillion other managers of the *-monkey family).
It's on Greasy Fork, too, of course, but here it is for your immediate leisure / viewing pleasure (and don't trust merely visible code: check a copy in some program or site that reveals invisible characters, in case there's malformed code [presumably for ill intent], and hell, maybe ask an LLM to go over it in a sandbox to check for things [cf. bidirectional control characters, invisible Hangul filler characters, obfuscation, Base64, malcode in a referenced img file, 1x1px iframes, maybe GitHub/etc. repositories, and so forth: even .ttf font files are a risk]):
// ==UserScript==
// @name AO3: Series' [Public] Bookmarks total
// @namespace https://greasyfork.org/en/users/1555174-charles-rockafellor
// @version 3.6
// @description Adds a row to author's stats page to show Series' [Public] Bookmarks total
// @icon https://www.clipartmax.com/png/full/47-478415_mathematics-symbol-math-sig-sum-mathematics-sigma-png.png
// @author Charles Rockafellor
// @homepageURL https://archiveofourown.org/users/Charles_Rockafellor/collections
// @match *://archiveofourown.org/users/*/stats*
// @match *://www.archiveofourown.org/users/*/stats*
// @grant GM_xmlhttpRequest
// @license MIT; https://opensource.org/license/mit
// @connect archiveofourown.org
// ==/UserScript==
(function() {
'use strict';
function init() {
const statsList = document.querySelector('dl.statistics');
if (!statsList || document.querySelector('.series-total-row')) return;
// 1. DYNAMIC USERNAME EXTRACTION
// We grab the path directly from the URL bar: /users/USERNAME/stats
const pathParts = window.location.pathname.split('/');
const userIdx = pathParts.indexOf('users');
if (userIdx === -1) return;
const currentUsername = pathParts[userIdx + 1];
// 2. SETUP STATS DISPLAY
const target = statsList.querySelector('dd.bookmarks');
if (!target) return;
const dt = document.createElement('dt');
dt.className = 'series-total-row';
dt.innerHTML = 'Series\' [Public] Bookmarks:';
const dd = document.createElement('dd');
dd.className = 'series-total-row';
dd.textContent = 'Locating series index...';
target.after(dd);
dd.before(dt);
let total = 0;
let page = 1;
// 3. SECURE FETCH LOGIC
function fetchSeries() {
// Relative URL ensures the browser uses the same domain (org/net/gay) you are on
const url = `https://${window.location.hostname}/users/${currentUsername}/series?page=${page}`;
dd.textContent = `Reading p${page}...`;
GM_xmlhttpRequest({
method: "GET",
url: url,
anonymous: false, // Vital for logged-in sessions
onload: function(res) {
if (res.status === 404) {
dd.textContent = `404 Error: Could not find series for ${currentUsername}`;
return;
}
// Flexible Regex: Targets digits inside the /bookmarks link
// Handles potential 2026 whitespace changes with \s*
const regex = /\/series\/\d+\/bookmarks[^>]*>\s*([\d,]+)\s*<\/a>/g;
let match;
let foundOnPage = 0;
while ((match = regex.exec(res.responseText)) !== null) {
// Extract digits from the captured group (match[1])
const count = parseInt(match[1].replace(/\D/g, ''), 10);
if (!isNaN(count)) {
total += count;
foundOnPage++;
}
}
// Check for Pagination
if (res.responseText.includes('rel="next"')) {
page++;
setTimeout(fetchSeries, 500);
} else {
// Final display with standard AO3 comma formatting
dd.textContent = total.toLocaleString();
}
},
onerror: () => { dd.textContent = "Fetch failed (Network Error)"; }
});
}
fetchSeries();
}
// Delay start to allow AO3's dynamic totals to populate
setTimeout(init, 1000);
})();
Google “AI” (LLM) added the following:
Here are the three "secret ingredients" that finally made it work:
- The Path Index: Using indexOf('users') + 1 to find the username string in the URL bar, which is much safer than scraping the page text.
- The Hostname Hack: Using window.location.hostname so the script stays on whichever AO3 mirror (.org, .net, etc.) the user is currently logged into.
- The Global Non-Digit Strip: Using replace(/\D/g, '') to ensure that only raw numbers are added together, ignoring any commas or hidden HTML formatting.
O ~~~ O

![Author's stats list with Series' [Public] Bookmarks total inserted](https://i.pinimg.com/1200x/9f/fd/1d/9ffd1d93f1f0ccce408e0346184abb34.jpg)