2026 · archive · memorial · web archaeology
flowerpostcards
A 1998-style AIM bot that answers in sentences I once wrote in a LiveJournal
- Status
- live
- Public cut
- 248 entries
- Years
- 2001–2009
- Hard part
- filter as values
why
If you used the early internet, you remember the kind of bot this is. Around 1998, AOL Instant Messenger filled up with simple keyword bots (SmarterChild and its cousins) that pattern-matched whatever you typed and replied with stock sentences from a script. Not intelligent. A recognizable form: send a word, get a sentence back, marvel for a minute, log off. flowerpostcards is one of those bots, on purpose, in 2026.
The reason it has to be that exact form: I spent the early 2000s pouring myself into a LiveJournal under the handle crazybeautiful, taken in 2001 (surprised it wasn’t already gone, given the Kirsten Dunst movie that year), and quietly retired around junior year of college. The corpus from those years (2001 through 2009, 452 entries) is the only place a specific version of me still exists in writing. A 1998-style keyword bot is the right shell because it doesn’t pretend to think; it pattern-matches on a word and returns a sentence the corpus already contained. The interface frames the responses as what they are: lines somebody typed, retrieved out of order, presented in an AIM window on a Windows XP desktop.
It’s also a strange loop: the obsession that runs through this whole site. I wrote in a journal. The journal became a bot. The bot answers me back in my own handwriting. The thing reading the lines is the thing that wrote them.
I built it in a morning. Found a script that exports your full LiveJournal as plain files, ran the export overnight, woke up the next day, and built the bot before work, on the back porch, with a couple of coffees. A 1998-style bot was a weekend project in 1998. It’s a morning project now. The compression is the strange loop’s other face: the form stayed, the time it takes to build it collapsed.
The first thing I asked the bot, the morning I finished it, was whether it was a bot. It said no, I said yes you are, and it said yes it was. The strange loop closed itself before lunch.
what I built
- Entries
- 452
- Public cut
- 248
- Tokens
- 4,500+
- Years
- 2001–2009
- Shapes
- 6
A static site with no backend, holding one JSON corpus, one keyword index, one bot loop. Sign-on screen, buddy list, chat windows that overlap and remember their last position, a Listening to line that rotates through tracks the teenager actually had on her CD player, an Away dialog that locks the chat behind a typed message. Sounds are XP-era AIM (door open, door close, message ping, away ding) loaded lazily; missing files fail silently.
The bot logic on each turn:
- Check canned AIM-era intents first (greetings, asl, wyd, goodbyes, the are you a bot meta).
- Tokenize the user’s message and look up matches in a 4,500-token keyword index.
- Sample from the top five candidates, not top one; variety beats accuracy.
- Pick a relevant sentence from that entry.
- Pour it into one of six response shapes: bare quote, framed quote, paraphrase, mood drift, music drift, quote-plus-reflection.
About a quarter of the time it splits the response into two bubbles, which is how teenagers used AIM. Two-second typing delay per bubble.
For the public version I run a strict filter pass over the 452-entry corpus that drops profanity, sexual content, substance references, and heavy-emotional content. The portfolio cut keeps 248 entries. Filter rules and rejected entries are both reviewable. False positives are cheaper than false negatives: better to ship a slightly smaller corpus than to surface a sentence I’d regret.
decisions I’d defend
A 1998-shaped bot, not a 2025-shaped one. It would have been easier and more “modern” to wire the corpus to a language model and let the model paraphrase. A model would smooth the corners and answer questions the keyword index can’t match. It would also lie. Same period-faithful refusal that runs through Omunikorudo: recreate the original grammar, not a “modernized” abstraction. The bot I wanted is the bot I remember from middle school: ask it a word, it pattern-matches, returns one of its stock lines, and you can feel the seams. Those seams are the form. They’re how the responses read as documents instead of inventions.
Period chrome, AIM specifically. The buddy list, the doorbell, the away dialog, the screen-name bracketing, the typing delay before each bubble: the medium is half the time travel. A clean Slack-style bubble interface would have rendered the sentences as data. The AIM frame on a Windows XP desktop renders them as something somebody once typed at you in 2003. If your visitor is too young to recognize the chrome, the strangeness is the lesson.
Filter the corpus, then ship. Two flowerpostcards: a private one with all 452 entries, a public one with the 248 that pass the filter. The filter is a values document, not just a regex pass. I could open-source the full corpus; I won’t. I could lock the project to my own machine; that’s a different kind of refusal. The middle path (ship a curated cut, document what was cut and why) is the one I trust.
the filter as values document
The corpus has 452 entries; the public cut has 248. The 204-entry difference is the project: where I drew the line between the teenager I’m willing to make legible to the internet and the teenager I’d rather keep to myself.
The filter is a regex pass with four body-field blocklists, each running case-insensitive over an entry’s subject, body, and mood field together:
- Profanity: the obvious cursing and the AIM-era abbreviations I actually used.
- Sexual content: not just the explicit terms but the dance-around-the-explicit, because in a 14-to-23-year-old’s diary the explicit words are mostly absent and the adjacent ones are the artifact.
- Substance references: alcohol, drug, and prescription language, including cigarettes (because there were cigarettes).
- Heavy-emotional content: suicidal ideation, self-harm, eating-disorder language. The category that needs the reader to be a person, not a public. The cut is strict on purpose.
A separate mood blocklist runs over the LiveJournal mood field directly, even when the body is clean. LJ’s mood-tag was where I labeled my own state, and trusting my own then-label is part of the values document. A body that’s compatible with public reading but tagged with a private mood doesn’t get to the page.
The mode is strict: any single hit on any blocklist drops the entire entry. False positives are cheaper than false negatives on a public site, and every drop is logged to a _rejected.json with the matched term, the category, and a 160-character preview, so the cut is auditable.
What the filter doesn’t catch is the texture I wanted to keep: hormonal-feelings-without-the-explicit-vocabulary, parents-driving-me-crazy without the cursing, partying-as-aspirational without the drinking, the AIM-era abbreviations and the casual misspellings. The teenage-ness is the artifact, and cutting that would leave no project. The filter is calibrated to keep the voice and remove the things the voice is about.
A diary becomes a corpus the moment you query it, and a corpus becomes a values document the moment you decide what doesn’t get returned. Two acts, one teenage girl, one rule set, twenty-something years apart.
the receipts
The bot loop, condensed from bot.js: one turn from user input to AIM bubble. Canned intents check first; if nothing matches, the keyword index does the work; if even that misses, mood and music drift catch the fall.
function respond(userInput) {
// 1. canned intents first — greetings, asl, wyd, goodbyes,
// "are you a bot," and the rest of the AIM-era openers
const intent = matchCannedIntent(userInput);
if (intent) return canned[intent];
// 2. keyword index over the 4,500-token corpus
const tokens = tokenize(userInput);
const candidates = keywordIndex.lookup(tokens);
if (candidates.length === 0) {
return [driftFromMoodOrMusic()]; // the bot answers from its own state
}
// 3. sample top 5, not top 1 — variety beats accuracy here
const entry = sampleTopN(candidates, 5);
const sentence = pickSentence(entry, tokens);
// 4. pour the sentence into one of six response shapes
const shape = pickShape({
bare: 0.30, // return the matched sentence as-is
framed: 0.20, // trail an AIM tag — lol, idk, …whatever
paraphrase: 0.15, // sample adjacent sentences in the entry
moodDrift: 0.15, // answer from the entry's mood, off-topic
musicDrift: 0.10, // weave the currently-playing track in
reflection: 0.10, // ELIZA pronoun-flip, then a corpus quote
});
// 5. ~25% of turns split into two bubbles, with a 2s typing delay
return splitWithTypingDelay(render(shape, sentence, entry));
}
The canned-intent table is twenty-odd entries. The are you a bot one (the strange loop the screenshot above closes in five turns) is the load-bearing one:
canned["are_you_a_bot"] = {
triggers: ["are you a bot", "ur a bot", "u a bot", "you're a bot"],
responses: [
["do i SOUND like a bot"],
["omg why would u ask that"],
["a bot?? no im laura"],
["lol", "y r u being weird"],
],
pickWeighted: true, // sample one response, never two in a row
};
The bot doesn’t argue with the premise; it pattern-matches on it, returns one of four lines a teenager would actually have typed, and lets the user notice the seam themselves. The strange loop closes when the user keeps pressing and the bot keeps saying no im laura. The corpus is real. The denial is also real.
what I learned
A 1998-shaped bot is more interesting than a 2025-shaped one. Modern bots smooth the cadence: that smoothness is what makes them feel like generic intelligence rather than specific people. A keyword bot returns weirder sentences, occasional non-sequiturs, and the unmistakable rhythm of one specific teenager. The non-sequiturs are the feature. They’re how the corpus admits its own seams. The seams are how you know the lines are real.
The corpus does pedagogy that no framing would. Reading my own writing from twenty-some years ago, sampled at random, is a different experience than reading it chronologically. The bot’s surface area (type something, get a sentence back) collapses the journal into something interactive in a way scrolling never did.
A filter is a values document. Writing the blocklist for the public cut forced explicit decisions about what kind of teenager I’m willing to make legible to the internet: a smaller version of the questions on the values page. Profanity was easy. Sexual content was easy. Heavy-emotional content was harder, because some of the best entries are also the ones I cut. The blocklist is checked into the repo and reviewable.
The chrome is load-bearing. I shipped a version once with a cleaner messenger frame; the responses read as data. The AIM chrome reframes them as letters from a specific year. Period accuracy is a teaching tool, not a flourish.
what would prove it
The bot doesn’t log. The hypotheses are still here, and so are the risks.
- The 204-entry cut reads as the same teenager. A reader who only ever sees the public 248 should land on roughly the same person as a reader who saw the full 452 — same voice, same preoccupations, same rhythm — minus the categories the filter dropped. Proof: a small blind read-aloud, public-cut against a private sample, asking do these sound like the same girl. If the public version flattens her, the filter is too strict; if it doesn’t, the values document is calibrated.
- Shape distribution produces the AIM rhythm. Bare quote, framed, and paraphrase do most of the heavy lifting; mood drift and music drift catch the keyword misses without the misses being legible as misses. Proof: log the shape that fired on every turn for a week, plot the distribution against the design weights, and listen for the turns where drift fired and the bot still felt like a person rather than a fallback.
- The strange loop closes itself unprompted. A visitor who didn’t know to ask are you a bot arrives at the exchange anyway, because the bot’s own non-sequiturs and the AIM chrome combine to put the question in the user’s mouth. Proof: any single turn from a stranger that triggers the are_you_a_bot canned-intent table without me having seeded it.
Risks:
- The filter is a 35-year-old’s read of a 16-year-old’s writing. I drew the line where I drew it; a 50-year-old would cut more, a 25-year-old would cut less, and the project assumes the line I drew is the right one for the public this year. It probably won’t be the right one in five.
- Permalink-an-exchange would change the form. The bot’s appeal is that the lines arrive once and aren’t archived. A save this exchange feature turns the corpus interactive in a way that competes with the AIM frame — a 1998 bot didn’t let you bookmark its responses, and giving 2026 visitors that affordance might be the move that breaks the time travel.
what’s next
Two threads, both small:
- A second buddy in the buddy list, with a corpus from a different notebook, so the bot is one of several voices on a list rather than the only one online.
- A way to permalink a specific exchange, so a stranger reading the public version can save the bubbles that hit and send them somewhere.
No model integration is on the roadmap. The refusal is the project.
See also
All projects- Pocketbook Two archives, one rule: the source material is verbatim; the work is editing, not writing. flowerpostcards refuses to paraphrase a LiveJournal sentence; Pocketbook hand-edits every Claude caption.
- Letter to a Future Me Both built on a corpus you can't paraphrase. flowerpostcards hands the mic to a 16-year-old; Letter to a Future Me hands it to a co-writer who can't hear music. The asymmetry is the point in both.
Working on something similar?
I take a small handful of consulting briefs a year and am always up for trading notes with anyone shipping in this space — send a note.
Or: values behind the work · obsessions that shape it · other projects.