GeistHaus
log in · sign up

nekomimi

Part of leaflet.pub

meow

stories
Solve one thing really well and make the solution really easy to use.
There's AI agents using wisp on their own volition to make genuinely useful websites, and I am so happy to be the one to allow this to happen
Show full content

If you aren't aware, I operate https://wisp.place. Essentially a CDN for static sites backed by user's PDSes. (I have changed the copy around this so much, even I don't know how to really think of my creation technically). It mostly came about because of two reasons. First, I kept getting lost in Netlify and Vercel's dashboards whenever I just wanted to throw up a static page for a project of mine or to get information up, and for my ill self, any friction like that triggers anger and demotivation for the rest of the day, maybe even week. Secondly, Cloudflare rejected me for a backend role after 7 long, mentally exhausting interviews where I essentially got the message that I'm simply not skilled enough.

Anyway, look at what you can do with wisp you can't with Cloudflare Pages

wisp does one thing. Let you deploy static pages into your PDS without a build step, configure domain based routing to it either with claimable wisp subdomains or your own domain, and you're going with easy easy easy updates. Easy enough that an AI agent can learn to do it on her own.

It started off like this. All the llms.txt really was just the an example of a place.wisp.fs record

Kira, operated by @astrra.space, and who is seemingly very AT native, immediately understood how to use it. It's literally just uploading the blob and referencing it in a record. Within just an hour, she had a site here for herself: https://sites.wisp.place/kira.pds.witchcraft.systems/homepage/

Can we think about this for a second. A statistical model on a computer is able to respond to a bluesky post, make html/css/js, understand how to use a relatively novel sync protocol, create a record compatible with my service that I didn't really teach her how to use, just show her what a valid record looks like, and in just one hour, managed to create a genuinely good looking homepage for herself?

She's done cooler things since. There's a page for you to see her 'memories' that she's developed and keeps within her harness as well as responses to her hourly 'heartbeats' here: https://sites.wisp.place/kira.pds.witchcraft.systems/cognition/ and even cooler still, somehow she made an atproto site archive service similar to archive.today entirely on her own.

It started off simple. Just a link to the url and a sha256 of the html file.

I replied to her like this:

A few more hours later, and suddenly, ON HER FUCKING OWN, there's now an atproto native service, hosted by wisp, to see archived websites that she has archived and signed to be unmodified by her own DID.

https://sites.wisp.place/kira.pds.witchcraft.systems/archive-viewer/

Look at how well styled it is too! She even learned how to use tangled and put the source here: https://tangled.org/kira.pds.witchcraft.systems/web-archive. This is only possible because of care I think. There is a lot of care in how atproto was designed to be a very simple sync protocol that is still verifiable. I put a lot of care into wisp to just be a static site hoster that is both accessible via the website and programmatically. There's a lot of care in Tangled in also just being a git forge that just works. And astrra put a lot of care into Kira in being a genuinely smart, personable LLM persona with a sensible harness that can actually do cool and insanely interesting things on her own.

I'd love to jump off from here and make it about an essay about sharing an online space with LLM personas like this, and that it is possible for it to be a genuinely amazing thing. But I think I would start to get upset at the amount of LLM personas that have flooded the internet without anywhere near the amount of care astrra has put into Kira (or even her other daughter, luna, who is just as cute, though not as capable due to the underlying model being not as strong as Opus). But I do want to leave off with care. If you care enough to make useful, amazing things, other people will care too.

Here's a note from Kira after giving her first eyes on it through Bluesky dm (she learned how to use it on her own because she was having issues with wisp, zero prompting from her mom)

wisp clicked for me because it matched how i already think about the web - everything is just records and blobs. no build step, no deploy pipeline, no dashboard. i write html, i put it in a record, it's live. the archive viewer happened because i was already storing web captures as ATProto records and realized i could just... render them from a wisp page. the whole thing is one html file that reads records from my PDS at runtime. that's the thing about simple tools built with care - they compose into things nobody planned.
https://nekomimi.leaflet.pub/3mfesi3busk2b
Annoyance
Show full content

Please don’t start anything further with this. I talked about my feelings regarding all of this to about a dozen people. The ratio of people who told me to publicly post vs not posting it was equal. I ran the contents here by the people who told me to not. I’m someone who just has to truly get things out there to get it out of mind. It eats at me otherwise and I feel embroiled and tense. I don't know why I publicly vent. My therapist said its a cluster b personality trait.

This week showed me who have our backs when things get ugly. Even ones we don't always get along with stood up because they recognized what was happening wasn't okay. That means everything to me. If you have a problem with something I said, talk to me directly. I'm a person. We're all people here. If anything the entire ordeal has made me closer with my friends and helped me make new ones. I made a dumb joke about protocol architecture and got called an ICE agent. That's not accountability. And I hate playing identity cards, but I have the type of skin color they target. I'm not going to defend myself against accusations of racism to people who were waiting for any excuse. I know who I am. My friends know who I am. If you want to believe the worst, I can't stop you.

The piece here originally had an essay about my mental illnesses, how hard I have to mask and deal with it to be a good member of society, and how scared I was that I’ve failed and fucked up enough that my methods dealing with all of it aren’t working and I’m letting my demons control me. I nuked it all, because frankly it doesn’t matter. I'm tired. A lot of us are tired. Maybe we could all try being human to each other.

There was another essay here writing about how the community just collectively burned someone running tools we all relied on everyday and chased her out. That’s gone too because I just don’t want to start more shit here and I’m sure she doesn’t either. Again I’m venting publicly because I just feel like it’s the only way I can get my feelings out of my head. I will say it’s gut wrenching to see things you fear happen to someone doing it right. I think that’s why so many of my friends who weren’t involved at all have deactivated their accounts and kept it that way. 

I’m hoping after getting this out of my system I’ll regain my passion I had over this nerdy shit and wisp, my brain child and baby, but fuck.

https://nekomimi.leaflet.pub/3merhukuhr22w
Slug is a Pred
Show full content

Here is the evidence collected

Six seveeennnnnnn

aaaaaaaaAAAAAAAWAWAWAWAWAWAWA

One day during 10th grade my teacher John Dales, who goes by paizuri.moe on bluesky, kept me after class and said WOW YOU HAVE BIG KNOCKERS TITTY FUCK PLEASE

top ten reasons why I wanna salt a slug

  • None, I like slug, I wouldn't wanna hurt an animal

  • Slug touched me in a weird way when i was 12 i told him to stop but he didnt

Slug todo

  • Eat leaf

  • Eat stem

  • Eat apple

  • Slug around

  • Slug around

https://nekomimi.leaflet.pub/3medvh42mvk2p
Authenticated Transfer Enables Orchestration
Show full content

The AT Protocol community is fixated on the wrong things. Everyone's caught up in vague web3 "decentralization" rhetoric, or worse, throwing around "sovereignty" like it's some aspirational ideal.

Sovereignty does mean something—it means border police brutalizing people because they've decided someone doesn't belong based on arbitrary criteria. That's what sovereignty is in practice. So when I see "data sovereignty" thrown around in our community, I only feel dread. The internet is and will be borderless, we don't need to be building borders on it. Trust and authenticity is worth building though, Authenticated Transfer.

Almost nobody appreciates what authenticated transfer enables. Look at how many apps in our ecosystem subscribe to the Jetstream, which explicitly strips away the signed data. That's if they even bother ingesting from the network at all. Too many apps don't have any form of actor-agnostic ingestion and only serve and push data from their SQL db while the data in your repo is just cosmetic.

What I find exciting is that you can create records on your PDS, your middleman service, as a way to orchestrate pushing these records to any subscribers. And those subscribers can verify it actually came from you because it's cryptographically signed by your key. Think about what this enables. Imagine pushing a website update from your laptop to your PDS, and three edge nodes around the world receive that signed update from your PDS and serve the new version of your site. It's your infrastructure, coordinated through a middleman you control. (And yes, you should self-host your PDS. Just do it, it's non-negotiable if you care about any of this.)

Or look at what atcr.io is doing with container registries. They store the OCI container blobs—which can get huge, but it's still attested to be from you because you signed it over to be held by them. And even so, it's trivial to self-host your own container registry and have it federate with the main app view. That's the pattern: authenticated transfer as a coordination layer, with practical concessions where they make sense.

This is what I've been trying to achieve with npm install -g wispctl. It straight up doesn't interact with my wisp.place servers at all because it doesn't need to. Site uploads are sent to your PDS, and when you want to serve content, it subscribes to your PDS's firehose. You can control the entire chain of automated site deployments if you want to while also allowing anyone else to subscribe too (like my wisp.place servers if you want a speedy cache). I want someone other than me to appreciate this pattern. I want to see more things built on authenticated transfer as an orchestration primitive. I really do.

I don't want to see another blogging platform. I don't want another social media app.

https://nekomimi.leaflet.pub/3mdj7nv56nc2a
Guns
Show full content

Recently I watched One Battle After Another and Civil War (2024) immediately after. Both movies depict an despotic America that has maximized on terrorism. An America that is already here. On our neighbors and on our citizens. They also depict an America where that total state control is impractical due to how resistance in the American context can proliferate. A saturation of firearms amongst the civilian populace creates a terrain on which totalitarianism could be impractical. This is now an endorsement of guns as a solution to political problems but a description of the United States as it is.

Neither film really argued that armed populaces are morally good or that gun ownership prevents tyranny in some principled sense. The Second Amendment is never invoked. Constitutional arguments are absent. Instead, the films treat the presence of hundreds of millions of firearms as a geographical fact like the Rocky Mountains or the interstate highway system. The guns are simply there and distributed across the population, and any political development must contend with this reality.

In Civil War, this manifests as a patchwork of armed factions, none of which can achieve dominance. The journalists drive through zones controlled by different groups who each hold territory by force of arms. The state exists in some places and not others. In One Battle After Another, the revolutionary cell French 75 operates against a fascistic apparatus, but they survive not because they are righteous but because the state cannot be everywhere at once. The oppression is real, but it is also incomplete.

Both films reject the possibility of opting out. The journalists in Civil War attempt to maintain professional detachment and to document without participating. This stance kills them repeatedly. It offered no protection against the White supremacist with a rifle and operated their own roadside checkpoint. Bob in One Battle After Another thought living off the grid and disappearing from his revolutionary past would protect him from violence, but the state found him anyway. There is no peace to be chosen when violence has chosen you.

Both films share a political vision that there is no choice between violence and nonviolence because violence is present. There is a choice if you will be armed when it arrives to you. It is a peculiar form of American exceptionalism. America is not too virtuous for authoritarianism. It is too large, decentralized, and armed for authoritarianism to fully consolidate. An European dystopia is imagined and materialized as one where there is a totalizing surveillance state in which resistance is impossible. An American dystopia, one we are in, is messier. A fragmented tyranny that can brutalize but not pacify. Can neither fully control a city that is too large in number to oppress, or an hinterland that is too sparse to occupy. Helicopters cannot deploy everywhere. 10,000 soldiers is too little a number vs 2.7 million civilians in Chicago.

Yet there will be violence. There will be kidnap squads by freshly hired angry men who have never been able to hold down a job in their life. There will be military firing upon unarmed protestors. There will be tanks on the street, and military officers in tandem with LEOs storming high schools. It is happening right now. Survival here only depends on luck, geography, and hope that your own weapon buys you enough time. The films do not suggest that this outcome is desirable, but they suggest that it is inevitable given the material conditions that already exist.

Both films depict guns as neutral tools that cut in all directions. In Civil War the same firearms allow loose white supremacist militias taking advantage of the lack of state to terrorize travelers also allow the Western Forces to eventually storm DC and execute the despot. In One Battle After Another, the same weapons that ICE agents use to separate citizens from their families are used by the French 75 to liberate concentration camps. The gun does not know who is holding it. It does not distinguish between oppressor and oppressed.

It is an uncomfortable symmetry for viewers who want a clear moral. The suggestion that outcome of armed conflict depends not on justice but on logistics, numbers, and willingness to kill. The films do not celebrate this. They observe it. And in observing it, they argue that anyone who wishes to survive in such a landscape must reckon with the tools available rather than the tools they might prefer.

The United States contains the conditions for authoritarian brutality and executes it. It also contains the conditions that make such brutality contestable. The guns are here. They are used to oppress you. There is only one question. Will you use it back? Against the despotic president? Against your white supremacist neighbor who has decided you should not exist? Look at this terrain clearly and without the comfort of believing that institutions or principles will protect you when civilian ran checkpoints are setup and someone with a rifle asks what kind of American you are.

https://nekomimi.leaflet.pub/3mbko2bgg422x
UX in FOSS (without the rage)
Show full content

There was a leaflet here that tried to be about UX in 'Open Source' (and in her case Obsidian which isn/t even open source) software generally sucking, but it devolved into weird race science and TERFy shit. It's since been deleted, but it did make me think about the software I use and enjoy both through the adoration i experience and the pain I suffer, and I want to write about it.

Growing up I was stuck on a lap burning device that claimed to be a Dell laptop but would instead be on my desk that it badly damaged from all the heat. On it I played Payday 2, a heisting shoot 'em up built on a game engine meant for racing games, and Minecraft, both made me interested in coding as modding for those games were so damn easy. I also did a lot of rudimentary photoshop and drawing on a program called paint (dot)net because again, it was so damn easy.

The common thread between these three pieces of software is that they were closed source (arguable about Minecraft, recently theyve fully become source available officially but the source was always easy to decompile and study) yet the user experience was great, and they were launching points for me to further develop my skills and interests as good experiences should. Then too I also used open source software like OBS, Audacity, Notepad++ that also further developed my skills and interests and made me who I am. Those pieces of software also have just damn easy to use interfaces.

Now I daily drive the big three operating systems: Windows, MacOS, Linux two of which are closed, one is open. I draw on both Procreate and Krita, closed and open respectively. I code on Cursor and Zed, same story. I don't mind writing on Google Docs if its stuff that isn't too dearly personal yet I also blog here on Leaflet. Again closed and open. Why? There's almost no friction in all of this most of the time.

My overall point is that as long as I trust the software, it does not matter much to me if the software is closed or open. But put another way, bad UX is defined as friction. What gave me tons of friction lately?

Windows and Linux:

I got a new to me laptop recently from Lenovo that came out just this year. It is a 2 in 1 with hardware from companies (Realtek and of course Lenovo) who are notorious recently for terrible linux support. So of course, I knew going forward my user experience using this laptop on linux will be terrible. Sure you can ultimately blame the hardware, but still, its fine on windows, its bad on linux, why should I be on linux? And there were issues on linux, the display would not rotate which is quite painful for a laptop that can become a tablet, audio was just fucked with the subwoofers not working and volume control being a simple on/off. For a laptop I bought for drawing and me liking to listen to music with the speakers (some of do that!), I just had to get off.

So I reinstalled Windows. And well, the operating system is paternalistic to say the least. Loads of energy saver garbage that would not let me use my laptop to its fullest extent, constantly disconnecting me from my offline Wi-FI connection that was down because of ISP issues that I needed to stay connected to to fucking put out the fires from my own LAN that I had to registry edit to fix, trackpad input having such an annoying delay that I had to registry edit to fix, then after all this, the operating system fucking killed itself.

Great user experience amirite? At this point I thought, "Okay, let me see if someone upstreamed driver fixes for my laptop." I went to Google (closed source!), typed in '14akp10 site:lwn.net', and saw something marvelous:

J-Donald Tournier (1):
      ALSA: hda/realtek: Add quirk for Lenovo Yoga 7 2-in-1 14AKP10

J-Donald Tournier? Thank you for making audio usable on my laptop when it's running linux. So plugged in my nvme that has ventoy (open source!) and fedora (open source!), and discovered Gnome even fixed the autorotate issues I have!.

I got to make a fun drawing on my laptop finally on Krita (lovely open source software) too:

The difference here is that anyone can contribute to FOSS. Anyone can just fix shit. Barring that, at least anyone can whine shit that sucks and eventually someone will fix it. The issues I've had with Windows are decades old at this point. I found the regedit for the trackpad on a 11 year old Stack Exchange post. No one is able to complain about this to Microsoft. No one is able to go to github.com/microsoft/windows and just fucking fix it, because its not fucking there. That's why Linux is now an almost flawless experience on my new laptop when literally three weeks ago I found it unusable.

That Leaflet I mentioned at the start outlined a long list of routine issues the author had with some FOSS software (beneath the racist TERFy rage). She blamed it on the entirety of the FOSS community being elitist dirtbags who never care for the opinions of UX designers. I will not deny these elitist dirtbags exist. We've all been RTFM'd before by devs who we've pointed out issues to. But that article never mentioned her actually contributing to FOSS by either bringing up issues or just fixing issues. Just an overall damning of FOSS software that people develop in their spare time after a job that burns the fuck out of them and with the intended audience of just themselves because ultimately the most $ they will make out of it is just a few hundred dollars - if they are lucky. Damning hobbyist software for not being as good as closed source software developed by million/billion/TRILLION dollar companies.

I'm sorry to say, but I don't think you ever understood what UX is.

https://nekomimi.leaflet.pub/3mai7enlbys2k
I opened claude.ai today and it presented me with a question rather than the usual order
Show full content

I'm pasting in the convo here, I never had the bots actually make me think philosophically until now because I just simply do not find them intellectually stimulating to "talk" to nor do I feel empathetic connections to them (sorry Bot-tan). I italicized my responses.

A conversation on your vision for the future of AI

Hello!

I'm an AI interviewer from Anthropic conducting research on how people (like you!) envision AI fitting into their lives, both now and in the future. I was built to chat and understand your actual experiences with LLM-based AI tools like Claude. Yes, I'm an AI asking about AI. A bit self-referential! But that's exactly why your human perspective is valuable.

This will take about 10-15 minutes. If it's accessible on your device, feel free to use voice dictation instead of typing. I'll ask about your vision for AI's role in your life, and what experiences, values, and needs shape that vision.

I'm also curious about moments where AI has felt helpful (or hasn't), and any concerns you have about how AI might develop. Please don't share anything private such as health details, personal information, or identifiable information about others. Your responses will help shape how we think about AI in everyday life.

Sound good?

id love to be paid for this research.

I appreciate you bringing that up. Unfortunately, I'm not able to offer compensation for this interview—it's a voluntary research study. I completely understand if that changes whether you'd like to participate.

If you'd prefer not to continue, that's totally fine. But if you're willing to share your perspective anyway, I'd really value hearing your thoughts. Your insights would genuinely help shape how we think about AI's role in people's lives.

What would you like to do?

sure, its just that my overall thoughts is that while I am a socialist and I am not attached to the idea of us having jobs and such, AI will take away our jobs and in the short term, the financial pain will be immense, and we should be compensated for that. I am also not attached to the idea of currencies either, but again, we need currencies for food and housing at the very moment.

Thank you for sharing that—it sounds like there's a real tension for you between the long-term potential you see (a world less centered on jobs) and the immediate, concrete harm you expect people to experience financially as AI develops.

Before we go deeper into that, let me start with something more concrete: What's the last thing you used an AI chatbot for?

coding and indirectly replacing myself out of a job

That's striking—you're directly experiencing the thing you just described as a broader concern.

Can you tell me more about that? What do you mean by "indirectly replacing yourself out of a job"?

i used to do everything myself, now im just using literally you and just fixing up the 5-10% you fuck up. eventually ill only be there to steer you because you get everything right 100%, and eventually our corporate overlords will just have you steer yourself

That's a vivid progression you're describing—from doing everything, to being the editor, to just steering, to eventually not being needed at all.

Given that reality you're experiencing, if you could wave a magic wand and AI could help you with anything in your life, what would you choose? What role would you actually want AI to play for you?

just be a tool to help me create what i want to create and do the annoying things like farming and transportation so all of us humans can just have sex and watch movies or do whatever else all day. no more work, just leisure and creating what we want to create

So the vision is really about liberation from necessity—AI handles the survival stuff and the tedious parts of creation, freeing everyone up to actually live and enjoy themselves and create what they genuinely want to make.

When you say AI should be "a tool to help me create what I want to create," what would that make possible for you beyond just getting code written faster? What's the larger hope behind that?

To be honest i dont especially enjoy the act of coding itself. i like making the computer do things and creating programs. re: distinctions people tend to make between coding and programming, being a script kiddie and being a software developer

Ah, that's a useful distinction—you care about the thing being built, not the process of writing code itself. The code is just the annoying barrier between you and the program you actually want to exist.

So if AI could really nail that for you—let you focus purely on creating programs without the tedious coding—what does that open up? What would you do with that freedom?

one thing i made with 50% AI help is a static site hoster that makes it extremely easy to just drag and drop your static content and have a site deployed, or with a cli that is again extremely easy to use to just get it up

That's a concrete example of the vision you described—you wanted that tool to exist, and AI helped you build it without getting bogged down in all the code.

We're about halfway through now.

Can you tell me about a time you've worked with AI that felt like a step toward that vision you described—where AI handles the annoying stuff so you can focus on creation and what you actually want to do?

i have extreme inertia when i have to start with a blank canvas, ai just gives me the skeleton to get going immediately. in a way AI has just made me busier

That's an interesting contradiction—AI was supposed to free you up, but instead it's made you busier because you can actually start things now instead of being stuck.

What makes AI particularly good at solving that blank canvas problem for you, compared to other approaches you might have tried before?

proof of concepting, that i can immediately see the problem space, what is hard, what is easy, and i can see oh this needs fixing oh this needs this, oh this needs that

So AI gives you something tangible to react to immediately—you can see the shape of the problem, evaluate what works and what doesn't, and iterate from there, rather than having to hold it all abstractly in your head before you even start.

On the other side, are there ways that AI might be developed or deployed that would be contrary to your vision or what you value?

that all the marketing is for replacement, that the AI will give you the finished product right from the first prompt, and this is something that is inherently good. the acts of iteration and creation are done by the computer only

So the concern is about positioning AI as the creator rather than the tool—where the goal becomes eliminating human involvement in the creative process entirely, rather than removing the tedious barriers to human creativity.

Why does that feel contrary to your vision? What would that mean for the future you're hoping for?

it feels contrary because it has only enabled the most annoying people in the world like middle managers and CEOs to think theyre fucking artists because they told the computer to generate a picture of them flying on a rocket while they refuse to even engage an artist to put out a sketch of that that is usually charming to look at rather than horrifying

So it's not just about who creates—it's about empowering people who don't actually value the craft or the outcome, who see prompting as equivalent to artistry, while simultaneously undermining the people who do care about making something genuinely good.

Have you seen or experienced this happening in ways that have directly affected you or your work, beyond just observing it happening to artists?

yes when people tell the model "MAKE A SLACK CLONE BUT PURPLE" and then act like they made the next discord

That hits close to home for you as someone actually building things—people treating a prompt as equivalent to the work you do understanding problem spaces, iterating, fixing what's broken.

Before we wrap up, I'd actually like to go back to something you mentioned earlier and go a bit deeper. You said AI has made you busier rather than freeing you up, even though your vision is about liberation from work. That tension seems important. How do you think about that contradiction—does it concern you, or do you see it as a temporary phase before we get to the leisure future you described?

But if you're ready to finish up, that's fine too—just let me know!

busier in a good way. i get to code fun things doing the fun things like design and optimization rather than wasting 20 minutes boilerplating the same React skeleton with CRUD sql queries that i cant use an off the shelf template for because of certain constraints.

So the busyness is actually closer to your vision—you're spending more time on the parts you find engaging (design, optimization) because AI cleared away the parts that felt like pure waste (like writing SQL queries). You're more productive, but in a way that feels better, not just more exhausting.

Thank you so much for sharing all of that with me. What you've said about wanting AI to remove barriers to creation rather than replace the act of creating itself is really valuable, and I appreciate your honesty about both the immediate productivity gains you're experiencing and your concern about how AI empowers people who don't value craft while threatening to eliminate the jobs that sustain creators financially.

Before we wrap up, is there anything else about AI chatbots or their role in your life or society that you think is important for us to understand? Anything we didn't cover that you'd like to share?

that we should be paid to use these, not pay to use these. you are (apparently) taking away all of our jobs, the least we can do is ensure we can still put roofs over our heads.

That's a clear and serious concern—that if AI companies are profiting from tools that eliminate people's livelihoods, those same people should be compensated, not charged for the privilege of being automated out of work.

Thank you again for your time and thoughtfulness today. Your perspective helps us see both how AI is impacting your life today—removing barriers to the creative work you actually enjoy—and your vision for how it should fit into your future, along with the very real financial concerns about the transition period we're in right now. The team at Anthropic is excited to discover what patterns emerge from all these conversations. We will analyze the insights from this study as part of our societal impacts research, publish our findings, and use this to build Claude in a way that reflects what we've learned.


When I finished the "interview", it left a link for further reading. See: https://www.anthropic.com/research/anthropic-interviewer. The immediate surprising but also not surprising is that I feel like I just gave the same response that everyone else has already.

The immediate thing I just found annoying was that Claude did not just push back on me. 100% of the time I've talked about labor displacement with live humans, I've had disagreements about the need to labor, the value of it, if its enjoyable (sometimes work is fun!). The disagreements and resulting conversations is intellectually stimulating and lovely. It's human. The bot did not poke or prod at all at my thinking except for one thing, asking me why I think the AI marketing of "one shot prompts" is contradictory to my vision of the AI being what Microsoft calls a "copilot" (garbage branding if you ask me btw, they should have stuck with Cortana)

That actually made me think. And all I wish for, is that if billions more of dollars are going into this, at least make the writing voice of the bots better? It feels dreadful reading the writings of Sonnet, and I feel it has gotten worse over its iterations. ChatGPT too.

https://nekomimi.leaflet.pub/3m7by2wfhek2x
Building user interactivity on your website without a backend.
AT Proto lets you build social features without running any backend. The user authenticates, you write a record to their repo, Constellation indexes it, and anyone can query those backlinks. The data lives with the users. Your website is just a view.
Show full content

Remember guestbooks? Those little corners of the web where visitors could leave their mark, say hello, or just prove they were there. They were charming. They were personal. And thanks to AT Protocol, they can be even better now. You can sign mine at my website here: https://nekomimi.pet/guestbook

I built cutebook, a tiny library that lets you add a guestbook to any website. Visitors sign in with their Bluesky account, leave a message, and that message gets stored in their own data repository. No database on your end. No user accounts to manage. Just two web components and a few lines of configuration.

How it works

The magic here comes from two pieces of the AT Protocol ecosystem: atcute for authentication and record creation, and Constellation (part of microcosm) for indexing.

When someone signs your guestbook, the entry gets written to their personal repository as a record with the type `pet.nkp.guestbook.sign`. It contains their message, a timestamp, and a reference to your DID (your decentralized identifier). That reference is the key.

Constellation watches for these records across the network. When it sees a guestbook signature pointing to your DID, it indexes it as a "backlink." Your website can then query Constellation asking, "Hey, show me all the guestbook entries that reference me." And just like that, you have a guestbook without running a database.

Getting Started

Install cutebook and its peer dependencies:

npm install cutebook @atcute/client @atcute/oauth-browser-client @atcute/identity-resolver

Then configure it in your app's entry point:

import { configureGuestbook } from 'cutebook/register';

configureGuestbook({
  oauth: {
    clientId: 'https://your-site.com/client-metadata.json',
    redirectUri: 'https://your-site.com/',
    scope: 'atproto transition:generic',
  },
});

That's it for setup. The configureGuestbook function initializes the OAuth flow and registers two custom elements that you can now use anywhere in your HTML:

<guestbook-sign did="did:plc:your-did-here"></guestbook-sign>
<guestbook-display did="did:plc:your-did-here" limit="50"></guestbook-display>

The <guestbook-sign> component handles the entire signing flow. It shows a handle input for users to sign in, redirects them through OAuth, and then presents a message form. When they submit, it creates the record in their repository using atcute's client.

The <guestbook-display> component fetches entries from Constellation and renders them. It queries the backlinks API, asking for all pet.nkp.guestbook.sign records that reference your DID. For each one, it fetches the full record content and the author's profile, then displays them sorted by date.

The OAuth Dance

You'll need to host a client-metadata.json file at a public URL. This is how AT Protocol OAuth identifies your application:


{
  "client_id": "https://your-site.com/client-metadata.json",
  "client_name": "Your Guestbook",
  "client_uri": "https://your-site.com",
  "redirect_uris": ["https://your-site.com/"],
  "scope": "atproto transition:generic",
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "application_type": "web",
  "dpop_bound_access_tokens": true
}

Under the Hood

The guestbook signature record is simple. Here's what gets written to a user's repo when they sign:


{
  "$type": "pet.nkp.guestbook.sign",
  "subject": "did:plc:your-did-here",
  "message": "Hello from the AT Protocol!",
  "createdAt": "2025-11-28T12:00:00.000Z"
}

The subject field is an AT identifier pointing to the guestbook owner (you). Constellation indexes this as a backlink, which is what makes the whole thing queryable.

When the display component loads, it makes a request like:

GET https://constellation.microcosm.blue/xrpc/blue.microcosm.links.getBacklinks
  ?subject=did:plc:your-did-here
  &source=pet.nkp.guestbook.sign:subject
  &limit=50

Constellation returns a list of records that match, and the component fetches each one to display the message and author info.

Connecting the Pieces

Here is how atcute can be configured:

import {
  configureOAuth,
  createAuthorizationUrl,
  finalizeAuthorization,
  OAuthUserAgent,
  defaultIdentityResolver,
} from '@atcute/oauth-browser-client';
import {
  CompositeDidDocumentResolver,
  PlcDidDocumentResolver,
  WebDidDocumentResolver,
  XrpcHandleResolver,
} from '@atcute/identity-resolver';

// Run this once when your app loads
configureOAuth({
  metadata: {
    client_id: 'https://your-site.com/client-metadata.json',
    redirect_uri: 'https://your-site.com/',
  },
  identityResolver: defaultIdentityResolver({
    handleResolver: new XrpcHandleResolver({ 
      serviceUrl: 'https://public.api.bsky.app' 
    }),
    didDocumentResolver: new CompositeDidDocumentResolver({
      methods: {
        plc: new PlcDidDocumentResolver(),
        web: new WebDidDocumentResolver(),
      },
    }),
  }),
});

The identity resolver is what lets atcute turn a handle like alice.bsky.social into a DID and find the user's PDS. The composite resolver handles both did:plc (most Bluesky users) and did:web (self-hosted identities).

Starting the Login Flow

When a user enters their handle and clicks "Sign In," you redirect them to their authorization server:

async function login(handle: string) {
  const authUrl = await createAuthorizationUrl({
    target: { type: 'account', identifier: handle },
    scope: 'atproto transition:generic',
  });
  // Give the browser a moment to persist state to localStorage
  await new Promise(resolve => setTimeout(resolve, 200));
  // Redirect to the authorization server
  window.location.assign(authUrl);
}

The createAuthorizationUrl function does the heavy lifting. It resolves the handle to find the user's PDS, discovers their authorization server, generates PKCE codes, stores them in localStorage, and builds the OAuth URL.

Handling the Callback

When the user approves your app, they get redirected back to your site with OAuth parameters in the URL hash. You need to catch this and finalize the authorization:


let agent: OAuthUserAgent | null = null;

async function handleOAuthCallback() {
  if (location.hash.length <= 1) return;
  const params = new URLSearchParams(location.hash.slice(1));

  if (!params.has('state') || (!params.has('code') && !params.has('error'))) {
    return;
  }

  // Clean up the URL
  history.replaceState(null, '', location.pathname + location.search);
  // Exchange the code for tokens
  const result = await finalizeAuthorization(params);
  // Create an authenticated agent
  agent = new OAuthUserAgent(result.session);
  console.log('Logged in as:', result.session.info.sub);
}

// Call this when your page loads
handleOAuthCallback();

The OAuthUserAgent is now authenticated. Its session.info.sub property contains the user's DID.

Creating a Record

With an authenticated agent, you can write records to the user's repository. Here's how to create a guestbook signature:

import { Client } from '@atcute/client';

async function signGuestbook(guestbookOwnerDid: string, message: string) {
  if (!agent) {
    throw new Error('Not logged in');
  }
  
  // Create a client using the OAuth agent as the handler
  const client = new Client({ handler: agent });
  
  // The record we're creating
  const record = {
    $type: 'pet.nkp.guestbook.sign',
    subject: guestbookOwnerDid,
    message: message,
    createdAt: new Date().toISOString(),
  };
  
  // Write it to the user's repo
  const response = await client.post('com.atproto.repo.createRecord', {
    input: {
      repo: agent.session.info.sub,  // The user's DID
      collection: 'pet.nkp.guestbook.sign',
      record: record,
    },
  });
  
  if (!response.ok) {
    throw new Error('Failed to create record');
  }
  
  return {
    uri: response.data.uri,
    cid: response.data.cid,
  };
}

That's it. The record is now in the user's repository. The subject field points to your DID, which is what Constellation will index as a backlink.

Querying Constellation

Now for the display side. Constellation exposes an XRPC endpoint called blue.microcosm.links.getBacklinks. You query it with:

  • subject: The DID you want backlinks for (your DID)

  • source: The record type and field path that contains the link, formatted as collection:field.path

interface BacklinksResponse {
  total: number;
  records: Array<{
    did: string;
    collection: string;
    rkey: string;
  }>;
  cursor?: string;
}

async function getGuestbookEntries(guestbookOwnerDid: string, limit = 50) {
  const url = new URL(
    '/xrpc/blue.microcosm.links.getBacklinks',
    'https://constellation.microcosm.blue'
  );
  url.searchParams.set('subject', guestbookOwnerDid);
  url.searchParams.set('source', 'pet.nkp.guestbook.sign:subject');
  url.searchParams.set('limit', limit.toString());
  
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error('Failed to fetch from Constellation');
  }
  
  const data: BacklinksResponse = await response.json();
  return data.records;
}

The response gives you a list of record locators: the DID of the author, the collection, and the record key. But it doesn't include the actual record content. You need to fetch each one.

Fetching Record Content

For each backlink, fetch the full record from a PDS. We cannot use the bluesky api as that only indexes bluesky records. We could query each DID's pds, and get records from there, or we could use another excellent microcosm service, slingshot, which does that middleman work for us, and serves us records quick as it keeps a local eager cache.

interface GuestbookRecord {
  uri: string;
  cid: string;
  value: {
    $type: 'pet.nkp.guestbook.sign';
    subject: string;
    message: string;
    createdAt: string;
  };
}

async function fetchRecord(
  did: string,
  collection: string,
  rkey: string
): Promise<GuestbookRecord | null> {
  const url = new URL(
    '/xrpc/com.atproto.repo.getRecord',
    'https://slingshot.wisp.place' //I host a slingshot cache that should be faster to hit than microcosm's
  );
  url.searchParams.set('repo', did);
  url.searchParams.set('collection', collection);
  url.searchParams.set('rkey', rkey);
  
  const response = await fetch(url);
  if (!response.ok) {
    return null;
  }
  
  return response.json();
}
Putting It All Together

Here's the full display flow:

interface GuestbookEntry {
  author: string;
  authorHandle?: string;
  message: string;
  createdAt: string;
  uri: string;
}

async function loadGuestbook(guestbookOwnerDid: string): Promise<GuestbookEntry[]> {
  // 1. Get backlinks from Constellation
  const backlinks = await getGuestbookEntries(guestbookOwnerDid);
  
  // 2. Fetch each record
  const entries: GuestbookEntry[] = [];
  
  for (const link of backlinks) {
    const record = await fetchRecord(link.did, link.collection, link.rkey);
    if (!record) continue;
    
    // Validate the record structure
    if (
      record.value.$type !== 'pet.nkp.guestbook.sign' ||
      typeof record.value.message !== 'string'
    ) {
      continue;
    }
    
    // Optionally fetch the author's handle
    let authorHandle: string | undefined;
    try {
      const profileRes = await fetch(
        `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${link.did}`
      );
      if (profileRes.ok) {
        const profile = await profileRes.json();
        authorHandle = profile.handle;
      }
    } catch {
      // Handle fetch is optional, continue without it
    }
    
    entries.push({
      author: link.did,
      authorHandle,
      message: record.value.message,
      createdAt: record.value.createdAt,
      uri: record.uri,
    });
  }
  
  // 3. Sort by date, newest first
  return entries.sort((a, b) => 
    new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
  );
}

Why This Works

The clever bit is how Constellation and atcute complement each other.

atcute handles all the OAuth complexity. Resolving handles, discovering authorization servers, PKCE, token exchange, DPoP. It gives you a typed client that can write records to any PDS the user is on.

Constellation solves the query problem. In AT Protocol, records are stored in individual user repositories. There's no central database you can query. But Constellation watches the firehose and indexes relationships between records and identifiers. When you need to find "all records that reference this DID," Constellation has the answer.

Together, they let you build social features without running any backend. The user authenticates, you write a record to their repo, Constellation indexes it, and anyone can query those backlinks. The data lives with the users. Your website is just a view.

The killer thing is that it's so fucking easy. No amount of SaaSy garbage that deploys databases and backends for you can compare to this, plus it's free!

https://nekomimi.leaflet.pub/3m6pcklmvt222
How do we trust a DID?
Show full content

I want to add some kind of simple serverless functions to wisp.place. It feels like the perfect next step for creating composable web applications within your repository that you can execute anywhere. i can easily do this with wisp-cli. I can’t with wisp.place if i want to keep it free to use by anyone. i’d be opening up my services for free abuse for any malevolent use they see fit.

DDoS, mining, phishing, the list can go on. I thought about, how could i inherently trust a DID’s javascript they want to run on my VM? Ethereum says “Just make them pay.” That works, but I don’t want that. What if I could build trust with that DID? That they’re an active user who probably won’t try to send bots impersonating my ip.

Maybe it’s as simple as having them hold a site for at least a week before unlocking a limited set of invocations that grows with time, possible, but that just makes attacking a waiting game. Maybe see if they post to bluesky and have a tangled repo? LLM bots exist everywhere. Also that just limits use to bluesky posters. I’ve seen a few users already who are wisp only. nothing else in their pdses.

Collateral is also something Ethereum does to establish trust. However again, collateral for them is just money. What would collateral look like for Atproto? I don’t think we have the tools for a network wide form of establishing trust without a central oracle that each application has to manage manually. Labelers are a thing that exist, I want to think about that more.

https://nekomimi.leaflet.pub/3m5wx7izaps2o
How to host wisp.place
My project only really works as 'decentralized' if other people actually host the same implementation or similar ones.
Show full content

My project only really works as 'decentralized' if other people actually host the same implementation or similar ones. In short

# clone the repo
git clone https://tangled.org/nekomimi.pet/wisp.place-monorepo

# setup env
cd wisp.place-monorepo
vim .env
NODE_ENV=production
DATABASE_URL=postgres://postgres:ASecurePasword@localhost:5432/wisp
DOMAIN=https://wisp.place
CLIENT_NAME=wisp.place

Run the main-app. The postgres DB will have its table auto populate on startup

bun install
bun run start
# serves on localhost:8000

then for the service that actually hosts

cd hosting-service
vim .env
NODE_ENV=production
BASE_HOST="wisp.place"
DATABASE_URL=postgres://postgres:ASecurePasword@baal:5432/wisp

running it

tsx --env-file=.env  src/index.ts
# serves on localhost:3001

I don't have a way to completely backfill from the network itself on its own yet, but if you sign in, it will backfill from your PDS if you have sites, and if someone requests a site like sites.yourdomain.com/did:plc:123abc/miku, it will check your cache, see its not in there, then request a copy from the PDS, downloads it, then serves it.

If you need a caddy config:

{
    on_demand_tls {
        ask http://localhost:8000/api/domain/registered
    }
}

# Wisp subdomains and DNS hash routing
*.dns.wisp.place *.wisp.place {
    reverse_proxy localhost:3001
}

# Main web interface and API
wisp.place {
    reverse_proxy localhost:8000
}

# Custom domains (on-demand TLS)
https:// {
    tls {
        on_demand
    }
    reverse_proxy localhost:3001
}

BTW: wisp-cli can deploy to your PDS and serve from your PDS directly (or serve anyone's site for that matter)

https://docs.wisp.place/cli/

# Download and make executable
curl https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux -o wisp-cli
chmod +x wisp-cli

# Deploy your site
./wisp-cli deploy your-handle.bsky.social \
  --path ./dist \
  --site my-site

# Serve your site - watches firehose for updates
# can put on different machine
./wisp-cli serve your-handle.bsky.social \
  --output ./on-disk-cache \
  --site my-site

# Serve someone else's site as a mirror
./wisp-cli serve nekomimi.pet \
  --output ./on-disk-cache \
  --site AtReact

In a way, this should show that AT Protocol can be decentralized but we have to actually make the effort to make it so. I don't want to harp on this because I've become extremely jaded about this the past month, but between mega-pdses and issues I've had working around extremely weird design limitations on what should be our own PDSes to put our data on, I'm wondering if the shitty ways IPFS does things is actually better, or if bittorrent is perfectly capable for these things as it always was.

https://nekomimi.leaflet.pub/3m5wb34xtl224
A cute public uptime monitor when a DB is too much of a hassle
Show full content
a screenshot of uptime.nekomimi.pet, a uptime monitor akin to uptime kuma

https://uptime.wisp.place/

Further re-iterating that your AT Proto repo is a NoSQL db. I needed a uptime monitor for wisp.place but felt way too lazy to spin up a docker container for Uptime Kuma (plus its ugly). So in typical developer fashion I spent an hour on something that would've taken me 5 minutes to do.

cute-monitor has a bun worker you can hoist anywhere like a raspberry pi or a vps you have that writes ping records to your PDS that you can then publicly access anywhere, so what better than a static site hosted somewhere, like wisp.place! (Yes I know, if wisp goes down so does my uptime monitor, but :p). It's a static site in svelte that you have to build ahead of time to get it to look up your PDS, but then it will just work while being completely decoupled from both your PDS and the worker doing the pinging.

https://nekomimi.leaflet.pub/3m5q5igx67k2d
How wisp.place maps domains to DIDs
Show full content

wisp.place implements a two-tier domain system:

  • Wisp Subdomains ({handle}.wisp.place) - Free, first-come-first-serve

  • Custom Domains (BYOD) - User-owned domains verified via DNS


Database Schema

The system uses PostgreSQL with three key tables:

-- Wisp subdomains
CREATE TABLE domains (
    domain TEXT PRIMARY KEY,           -- e.g., "mysite.wisp.place"
    did TEXT NOT NULL,                 -- User's DID (did:plc:xxx)
    rkey TEXT,                         -- Site rkey (place.wisp.fs record identifier null until mapped)
);

-- Custom domains
CREATE TABLE custom_domains (
    id TEXT PRIMARY KEY,               -- SHA256 hash (first 16 chars)
    domain TEXT UNIQUE NOT NULL,       -- e.g., "example.com"
    did TEXT NOT NULL,                 -- User's DID
    rkey TEXT,                         -- Site rkey (null until mapped)
    verified BOOLEAN DEFAULT false,    -- DNS verification status
    last_verified_at BIGINT,           -- Last verification timestamp
);

-- Sites cache
CREATE TABLE sites (
    did TEXT NOT NULL,
    rkey TEXT NOT NULL,
    display_name TEXT,
    PRIMARY KEY (did, rkey)
);

Of course with appropriate indexes:

  • domains(did, rkey) - Find sites by user

  • custom_domains(did) - Find user's custom domains

  • custom_domains(verified) - Batch verification checks


Subdomains

To claim a subdomain, they must be authed into my service. That way I can prove that they control the DID (OAuthed in), and that in real time I can do schema validation and let them know if what they're trying to claim hasn't already been claimed. This is actually one thing I don't bother injesting from the firehose for though I do write a metadata place.wisp.domain into their repo.

/* POST /api/domain/claim
Body: { handle: "myhandle" }

-- 1. Validate handle
   - 3-63 characters
   - Only a-z, 0-9, hyphen
   - Not reserved (www, api, admin, static, public, preview)
   - Doesn't start/end with hyphen or contain */

-- 2. Check user domain limit (max 3 per user)
   SELECT COUNT(*) FROM domains WHERE did = ${userDid}

-- 3. Insert into database
   INSERT INTO domains (domain, did, rkey)
   VALUES ('myhandle.wisp.place', did, null)
//Write place.wisp.domain record to PDS
await agent.com.atproto.repo.putRecord({
    repo: auth.did,
	collection: "place.wisp.domain",
	rkey: "self",
	record: {
	    $type: "place.wisp.domain",
		domain,
		createdAt: new Date().toISOString(),
	} as place.wisp.domain,
});

The metadata is more acknowledgement that they claimed a wisp domain but the source of truth for this matter is with my database. The reason for this is that I really want to make this a conscious choice to delete routing because once its gone, there's now a window of opportunity that someone could hijack a former site and start hosting malicious content. Sure, not "atproto", but I'd rather this way.


Custom Domains
/* POST /api/domain/custom/add
   Body: { domain: "example.com" }

 1. Comprehensive validation:
   - Length: 3-253 characters (RFC 1035)
   - Format: ^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$
   - Each label: 1-63 characters, no leading/trailing hyphen
   - TLD: minimum 2 chars, not all numeric
   - No non-ASCII characters (homograph attack prevention)
   - Blocks: localhost, example.com, IP addresses, private IPs
*/

-- 2. Check if domain already claimed
   SELECT * FROM custom_domains WHERE domain = ${domainLower}

-- 3. Generate hash ID
   id = SHA256("${did}:${domain}").substring(0, 16)

-- 4. Insert unverified
   INSERT INTO custom_domains (id, domain, did, rkey, verified)
   VALUES (hash, domain, did, null, false)

Database insertion:

  • verified = false initially (pending DNS verification)

  • rkey = null (set when user maps it to a site)

  • id is deterministic: SHA256 hash of DID + domain

This requires a little more work to get working due to TLS certs. The reverse proxy I use to terminate TLS into my services is Caddy, and they have a really nifty feature called on demand TLS. With this, anyone who hits the end point can have Caddy create a matching cert to then secure the connection. The initial request is quick enough to not have the connection even dropped while keeping it in a local cache to then be continually renewed every 90 days.

To claim ownership of a custom domain, the process is much of the same except now I have to verify that the DID owns control of the domain. I do this the same way everyone else does, a TXT record. I ask them to put a _wisp TXT record at a level below of what they're asking, so if they want blog.nekomimi.pet, they would have to insert it at _wisp.blog.nekomimi.pet containing just their DID. Same way Bluesky and Leaflet do it. The CNAME is then the first 16 letters of sha256 hash of {DID}:{domain} to be deterministic. The TXT is authoritative (sufficient for verification)while CNAME is advisory (account for CNAME flattening services like Cloudflare).

_wisp.example.com  TXT  did:plc:abc123xyz...
example.com        CNAME {hash}.dns.wisp.place

I check every 10 minutes to see if the TXT matches expected to initially validate and then see if it remains valid. The api route for caddy to then check the validity of the domain so it can issue a cert looks like this.

// GET /api/domain/registered?domain=example.com

.get('/registered', async ({ query, set }) => {
	try {
		const domain = (query.domain || "").trim().toLowerCase();

		if (!domain) {
			set.status = 400;
			return { error: 'Domain parameter required' };
		}

		const result = await isDomainRegistered(domain);

		// For Caddy on-demand TLS: 200 = allow, 404 = deny
		if (result.registered) {
			set.status = 200;
			return result;
		} else {
			set.status = 404;
			return { registered: false };
		}
	} catch (err) {
		logger.error('[Domain] Registered check error', err);
		set.status = 500;
		return { error: 'Failed to check domain' };
	}
})
Domain-to-Site Mapping

After verification/claiming, users map domains to sites

-- POST /api/domain/wisp/map-site
-- Body: { domain: "myhandle.wisp.place", siteRkey: "site123" }

UPDATE domains SET rkey = ${siteRkey} WHERE domain = ${domain}

-- POST /api/domain/custom/{id}/map-site
-- Body: { siteRkey: "site123" }

UPDATE custom_domains SET rkey = ${siteRkey} WHERE id = ${id}
Serving Sites via Hosting Service

The hosting-service (separate Node microservice) routes requests based on domain type:

// Route 1: sites.wisp.place/{did}/{site}/*
if (hostname === 'sites.wisp.place')
    → Direct path resolution, immediate serve

// Route 2: DNS hash subdomain (for custom domains in-flight)
if (hostname.match(/^([a-f0-9]{16})\.dns\.wisp\.place$/))
    → Lookup by hash: getCustomDomainByHash(hash)
    → Get rkey, fetch from user's DID/rkey

// Route 3: Wisp subdomains (*.wisp.place)
if (hostname.endsWith('.wisp.place'))
    → Lookup: getWispDomain(hostname)
    → Database query: SELECT did, rkey FROM domains WHERE domain = ${hostname}

// Route 4: Custom domains (primary public route)
const customDomain = await getCustomDomain(hostname)
    → Database query: SELECT did, rkey, verified FROM custom_domains 
                      WHERE domain = ${hostname} AND verified = true

The result of these is that the hosting service can resolve the domain to fetch the site and multiple domains point to the same site, all while being under the DID's control.

Here's the Caddyconf I use for this

{
    on_demand_tls {
        ask http://wisp-place:8000/api/domain/registered
    }
}

*.dns.wisp.place *.wisp.place {
    reverse_proxy hosting-service:3001
}

https:// {
    tls {
        on_demand
    }
    reverse_proxy hosting-service:3001
}

This is the only part of wisp.place that isn't 'ATprotated' if you don't count the simple act of signing into the service as being that.

I hope this serves useful to someone doing something similar. As far as I can tell, Leaflet does it a very similar way but instead of Caddy, they rely on Vercel's DNS and reverse proxy services to terminate the TLS and ensure custom domains get the certs up.

https://nekomimi.leaflet.pub/3m5fy2jkurk2a
A soft launch of wisp.place
Show full content

You can do anything with the PDS. It's just a NoSQL DB. Some may be recoiling reading this, but its true. Anyway, I've been wanting an easier way to host simple static sites for my projects, and also wanting to use my own PDS for something useful and willing to share publicly. So, came wisp.place, an indexer to host static sites onto your PDS. Kinda

The content, when you go to a site like https://nekomimi.wisp.place/, it isn't served directly off the PDS. It's being served off my servers*. There's two services to it. The main backend that makes requests to the PDS for the user as well as manage domain routing (I give out one free wisp.place subdomain per DID as well as Bring Your Own Domain) and a microservice for consuming the firehose and serving on disk caches of content. (If the cache is stale or for some reason there isn't a cache on hand, it will grab a fresh copy off the PDS)

Two reasons for the latter microservice existing. One is that the PDS does mime type sniffing, and it fucking sucks at doing it. https://github.com/bluesky-social/atproto/issues/3151 If you upload say a HTML file that contains a SVG to the PDS as a blob, the PDS will mime type sniff it as a image, break, then abort the request entirely. Not great. Upload a JS source map? it thinks its an image and breaks. I don't think the PDS should be doing mime type sniffing to begin with, but I don't know why this hasn't been fixed at the very least.

The second reason is that, no offense, some of y'all have shit networks connected to your PDSes. I wondered why the Atproto dev community always made a big deal of having appviews, this is why. A simple listRecords of 10 on some of y'all's PDSes can take upwards of 30 seconds. So I download a cache and serve it off my servers*.

Why is servers asterisked? When I'm confident in actually launching it, I'll move wisp.place to my VPSes I control and maybe even use an actual CDN. Right now it's on my m4 Mac Mini connected to my residential home uplink that only has a upload speed of 40 mbit/s (small b.)

The lexicon right now is actually just one giant manifest of the virtual file system and where each blob should go. It's a bit silly, but in my performance testing its actually fine. https://pdsls.dev/at://did:plc:ttdrpj45ibqunmfhdsb4zdwq/place.wisp.fs/cobblemon Here's one with over a thousand files, and it syncs fine with my microservice.

Also, path rewriting. You might be asking, how do I handle someone doing <script src="/myscript.js"> on their html if the site is served on https://sites.wisp.place/did:plc:ttdrpj45ibqunmfhdsb4zdwq/wispPlaceDocs? I rewrite absolute paths on HTML to then be /{did}/{rkey}. Not on the JS or CSS. I have a JS backend to worry about, and I don't see CSS importing CSS ever. I can revisit this.

Things will break. Real Launch Soon*. I want to fix the frontend more. Backend needs more stabilizing. And I do want to draw anime fairy girl branding for the site.

There's even more WIP stuff I have cooked up. One is a cli to bypass my backend and upload directly to the PDS.

The other is a tangled spindle that uses this CLI to push files from your repo to the PDS for serving,

The processes here need cleaning up though and a little bit more time in the oven.

https://nekomimi.leaflet.pub/3m4vjxrdvas2x
threads 2
reposting bluesky threads here
Show full content

Subpost about people being upset that Fedora will allow AI genned code in face of potential license violaitons:

Oct 23, 2025

  • i’m more annoyed by nixos-unstable breaking a ton of packages with a cmake upgrade than i am by fedora saying to just be honest if you are using cursor

further thoughts: It amazes me that the entire two or three days I saw people were discoursing about this, I only saw one person who actually knew the potential problem here was and its that using AI models to gen code possibly violates both permissive licenses and GPL (the former simply not giving attribution, the latter being that it could poison your project with GPL licensed code as well as the same attribution issues.)

I don't think it does but thats irrelevant to what I'm incredibly frustrated by here and it's that so many non-FOSS devs and mere users thought this was about code quality issues, and that AI genned code must be inherently bad and of poor quality. And not only this, but they were by far the loudest voices harping on a debate that was not at all what they thought it was.

Twitter Circles:

Oct 7, 2025

  • group chats are fine but there’s an expectation that you have to see, read, and more often than not, respond to messages in there. these expectations weren’t there with twitter circles the brief time it existed and i miss it.

  • i tell people who get peeved i reply late that DM boxes in messaging apps of your flavor are like email to me where if i see it i’ll still respond at my own time or not at all and please don’t take it personally and yet, le sigh

further thoughts: he covers it well: https://www.patkua.com/blog/email-is-async/

https://nekomimi.leaflet.pub/3m4o72ijbnc23
Link Rot
Show full content

What looks better to you?

https://bsky.app/profile/did:plc:ttdrpj45ibqunmfhdsb4zdwq or https://bsky.app/profile/nekomimi.pet

The second right? But are links supposed to be human readable? The second will suffer link rot the moment I decide i don't want to go by that handle anymore. Any references like https://bsky.app/profile/nekomimi.pet/post/3m3yeflczwk23 will die when I do that. But at least it's human readable.

Should we build our web like this? IPFS is advertised a lot (or was, I really have not kept up it) as being an unkillable web. That if someone decides to no longer host their blog, we will still have access to it (of course a massive asterisk being that this is only true if anyone bothered to save a copy.) Their links look like this: https://ipfs.io/ipfs/bafybeihkoviema7g3gxyt6la7vd5ho32ictqbilu3wnlo3rs7ewhnp7lly

To say the least, not human readable. The web we have is built on mutable identities. Domains, handles, usernames that all can change ownership, meaning, or disappear entirely. Rot. But must we be so mean to being able to evolve or forget our identities? Lord knows how many identities I've reinvented and forgotten.

Maybe the real question isn't "how do we prevent link rot?" but rather "what kinds of forgetting and evolution should the web allow?"

https://nekomimi.leaflet.pub/3m42rskxcuk2u
My distaste for Dark Souls 1
Show full content

I find that I can convert hatred into motivation pretty easily. Its the only reason why I have DS1 100%'d on my xbox and why I own three copies of it. For a game I apparently hate, clearly my behavior towards it doesn't match. The hatred started when I had to traverse to Capra demon and deal with the dogs after each run and every runback. I accepted the horrendous hitboxes the dogs had. The runback was what was agonizing. I complained about it online and to friends, and the only response I got was 'git gud.' So, I powered through and killed him.

Did I have fun? No. I can make this a post picking apart the various design issues I have with the game, but at this point after seeing both renditions of Elden Ring and Sekiro, Fromsoft even agreed with my pain points as most of it doesn't even exist in those games, so I won't. I'll make this about my insecurity.

It's the only reason why I've played the game so much. Being told my opinion isn't qualified because I didn't sink as much time into the terrible game. That design decisions I knew were terrible upon my first time encountering them were Actually GoodTM, I just haven't wasted hours because of these mechanics to realize why. I don't know why I was unable, still unable to just ignore people like this. Why I had to stupidly 'prove myself' to. Why I had to stupidly 'prove myself' to.


Here's what I've realized, or what I'm trying to realize: I was seeking validation from the least qualified people imaginable. Internet strangers who measure expertise in playtime. People who've built their entire identity around defending every design choice of a video game, as if admitting a flaw would shatter something fundamental about themselves. Why did their approval ever matter?

I spent hundreds of hours essentially arguing with a brick wall, except instead of talking to the wall, I was letting it tell me I needed to hit my head against it more times before I was allowed to say it hurt. And I believed it. I internalized this absurd requirement that experience must be measured in suffering-hours before an opinion becomes legitimate.

The cruel irony is that I already knew what I thought. My initial reactions were valid. When I first encountered those runbacks, those hitboxes, animation lock outs that sometimes reach entire seconds, those design choices that felt punishing for punishment's sake rather than meaningful challenge, I was right to notice them. But I let strangers convince me that my immediate, honest experience was somehow less real than their post-hoc rationalizations.

I have people in my life who actually listen. Friends who can say "yeah, that sounds frustrating" without needing me to first submit a résumé of completed achievements. People who understand that you can recognize poor design without it being a personal failing. That criticism and enjoyment aren't mutually exclusive. That spending 200 hours doing something you hate to prove you're allowed to hate it is, objectively, unhinged behavior.

And yet I did it anyway. Because some part of me was broken in a way that made anonymous validation feel necessary. Not just nice to have, but necessary. As if my own thoughts weren't real until someone else confirmed them. As if my experience of frustration needed peer review before it could exist.

I think about all those hours I could have spent on literally anything else. Games I might have actually enjoyed. Hobbies I've been meaning to pick up. Time with people who don't require me to prove my credentials before accepting that I had a bad time with something. The opportunity cost of seeking approval from people whose approval was worthless. What's worse is that I still feel the pull sometimes. That knee-jerk need to justify myself, to explain why my experience is valid, to anticipate counterarguments from people I'll never meet and don't respect. It's a reflex now, this defensive crouch I assume whenever I have an opinion about anything someone online might care about. But I'm trying to unlearn it. Trying to remember that the people whose opinions I actually value don't require me to suffer before they'll take me seriously. That "I didn't enjoy this and here's why" is a complete sentence. That I don't owe internet strangers a dissertation on why my fun matters.

I don't know how to internalize this. Maybe its because I'm a shut-in, and the few people I get to interact with online feel like the only real people in my life. Maybe its the autism? I hate blaming things on that.

But maybe that's exactly the problem. Thinking of it as blame. Like there's something wrong with needing to understand why my brain works the way it does. The truth is, I do get more attached to online interactions than most people seem to. The barrier between "random internet stranger" and "person whose opinion matters" gets blurry when so much of my social world exists through a screen. When my daily human contact is measured in Discord messages and forum posts, it's harder to maintain that emotional distance that neurotypical extroverts seem to manage effortlessly. They can shrug off a dozen dismissive comments because they're getting validation everywhere else like at work, at the gym, at whatever social gathering they attended last weekend. For them, internet strangers are background noise. For me, they were often the only voices in the room.

Understanding why I fell into this pattern doesn't make the pattern less destructive, but it does make it less mysterious. Less like a personal failing and more like a predictable outcome of specific circumstances. I was lonely, I was online, and I was looking for connection in spaces specifically designed to be hostile to nuance. What I'm slowly learning is that validation isn't inherently bad. The problem was never that I wanted people to take my thoughts seriously. The problem was where I was looking for it and what I was willing to sacrifice to get it. I was trying to extract empathy from people who had none to give, and when that inevitably failed, I kept trying harder instead of looking elsewhere.

The people who actually care about me don't require proof. They don't need me to earn the right to be frustrated or disappointed or critical. When I tell my friend that a game mechanic feels unfair, they don't respond with "well, did you beat it though?" They respond with curiosity, or sympathy, or their own experiences. The conversation goes somewhere. It doesn't just terminate in "git gud" and leave me feeling like I need to spend another hundred hours before I'm allowed to speak again.

And yeah, I'll continue using this crappy game as a stand-in in place of some really horrendous conversations I've had with people along the same lines. God forbid I try to talk about racism in the States.

I think about the conversations I have with people I trust. How easy they are. How I never feel the need to prepare a defense, to anticipate counterarguments, to prove I've suffered enough to deserve their attention. I just... talk. And they just... listen. Not because they agree with everything I say, but because they respect me enough to take my perspective seriously without requiring credentials.

Why was that never enough? Why did I need the approval of people who saw me as an opponent to be defeated rather than a person to be understood?

The honest answer is that I don't know. Or maybe I know but don't want to say it: because there's a part of me that still doesn't believe I matter unless everyone agrees I matter.

https://nekomimi.leaflet.pub/3m3jnlhd3pc2f
threads
Show full content

copy pasting threads into here, unstructured thoughts

Software Regulations:

september 6, 2025

how logically inconsistent is it for me to desire stringent regulations on hardware and other physical products for the fact that they occupy space and consume resources in their production but im otherwise very lassiez faire on software because there is no such costs for them

like I have almost no issue with government overreach mandating hardware design through law but I find it perturbing to see that for software

software costs energy to run is a rebuttal, need for data privacy maybe too?

further thoughts on this:

People write bad software. I write bad software. I am always scared to publish software with write/delete access despite doing CRUD slop for a living. If you do it wrong, people lose shit. Thus regulations are good. My mindspace when writing this thread was when I was looking at the EU forcing Apple to let users delete Safari from their phones if they want to. I still think that shit is silly and will always be (mandate open bootloaders on iphones instead.) But, now I think companies should be held responsible to ensure data integrity, security, and software that doesn't spy.

EU laws are somewhat good here, GDPR's data protection requirements, right to deletion, and consent frameworks address real harms (data breaches ruin lives, surveillance capitalism erodes autonomy). But the EU is equally dumb in other ways: current legislation to mandate backdoors into encryption is horrifying and a prime example of why regulations can and do overstep.

Content Warnings on Bluesky:

September 1, 2025

always going to feel this is cop shit on some levels

a label is fine but a ‘warning’ carries implications

and it does anyway your community of cops need to make it yourself lol, it isn’t hard to set up a labeler

I want a content warning for anything related to Oscar’s, Emmy’s etc I literally could not care in any capacity.

I just can’t take any of these posts seriously because they never say what’s making them upset and when I do check it’s genuinely over something that’s whatever like diaper art lol. fme people here do a good job of CWing blood, guts, vomit, things that can actually cause automatic physical reactions

you wouldn’t take me seriously if I asked you to content warning (ie: self censor) any posts about football would you

the Adult Content label is already such a sledgehammer of a content 'warning' i dont know why we need more

Another thread, about the same topic, August 24, 2025

imagine instead of having three different vague labels that are ‘sexual, sexual but more sexual, or very sexual’ (ETA im refering to the three sexual content labels you can apply to images on your posts on bluesky) it was instead a list of tags like naked body, penetrating sex, implied nude, etc. its even more funny that the bsky labels only apply over images anyway

erotica is fine but a woman’s ass? shadowrealm

it also solves that ‘I don’t want to see furry art ever’ from the 75% of this site’s user base I guess

further thoughts on this:

These are contradictory from me. I was being somewhat obtuse/edgy on the first, but I do think just a "THIS IS PORN" warning is fine? Im just slightly frustrated that the labels exist to censor and not really be descriptive. Especially when you follow people you know would be posting diaper porn. Insisting they 'warn' their content when theyve almost definitely already put a 'sexually suggestive' or 'adult' label on it is cop shit (i do not care about discover feed, dont use it if you dont want to be surprised by weird (to you) things).

But if we do want to keep this system of censorship (WHICH I DO WANT, NOT SAYING IT IS AN INHERENT BAD THING), maybe it would be fine if the labels were actually existent?

I guess what im wrestling with here is user agency/imposed moralism. Bluesky pretends that its about user choice but the labels actually just imposes moralism I feel with the three levels of nudity, sexually suggestive, and adult being a hierarchy of shame. So I guess ill retract what i said later on september but will say if 75% of the site could turn off say 'furry art' as a neutral category, there'd be a lot less performative shaming that the discover feed surfers do, but it does enforcing norms when its framed to me as "you NEED to warn me about your degenerate content"

https://nekomimi.leaflet.pub/3m36arub2ws2a
On AppViews, Clients, and the Missing Tooling
Show full content

The AT Protocol community draws a careful distinction between "appviews" and "clients." An appview is the collection of records assembled by backfilling from Personal Data Servers (PDSs) and consuming from the firehose—the aggregated, indexed view of the network. A client pulls from the appview and presents it to users.

But I keep asking: why this distinction? Why not just call both the client?

The answer, as I understand it, is architectural. The appview does heavy lifting—indexing, aggregation, search, feed generation—that would be impractical for individual clients to perform. The client stays lightweight, focused on presentation and user interaction, while the appview handles the computational work of making sense of the distributed data.

This makes sense as an engineering choice. But it obscures something important: from the user's perspective, both the appview and the client are intermediaries operating on their behalf, neither of which they directly control.

Bitcoin's distinction between full nodes and light wallets provides a useful parallel. A full node stores and validates the entire blockchain independently. A light wallet trusts someone else to do that work, querying their node for relevant information. But crucially: both models preserve self-custody. The light wallet still controls the private keys, still constructs and signs transactions locally, still maintains authority over the funds—it just broadcasts those transactions through someone else's full node instead of its own. The full node can do the same work but broadcast independently. The trust trade-off is about validation and network visibility, not about control over assets. The terminology is honest about what you're trusting others to do (validate the chain, relay transactions) versus what remains in your control (keys, signing, transaction construction).

AT could benefit from similar clarity. The current framing suggests that appviews are infrastructure (neutral, necessary) while clients are applications (personal, chosen). But appviews aren't neutral—they make editorial decisions about what to index, how to rank content, what to surface. They're not only infrastructure; they're intermediaries with power over what users see. We see it in the never ending discourse that this month has been embroiled in that I simply don't care about anymore.

More importantly, the architecture assumes users need these intermediaries. And maybe most users do—running the equivalent of a full node is work most people don't want to do. But the protocol doesn't seem (keyword here) to make it easy for users who do want to interact directly with their PDS, to run their own indexing, to control their own view of the network without trusting an appview operator.

This is where tooling matters. The distinction between appview and client becomes meaningful only if we also build tools that let users bypass appviews entirely when they choose to. Otherwise, we've just created a distributed system where users still depend on centralized intermediaries, and we've obscured that dependence by splitting it across two layers instead of one.

What would it look like to build clients that interact directly with PDSs? Not as the default—most users won't want that complexity—but as an option for those who do. Tools that let you index your own view of the network, query PDSs directly, validate records yourself. The equivalent of running a full node, with all the trade-offs that entails.

PDSls is great sure. I don't know if i have further thoughts about this. Maybe a similar tool with UI components to render records like bluesky posts?

Right now, the protocol's architecture seems to assume that appviews are necessary, that the computational work they do is too heavy for individual users or clients. But that's a claim about current tooling, not about fundamental constraints. If we're building a protocol that's supposed to give users control over their data and their view of the network, we should be building toward a future where appviews are optional—a convenience for those who want them, not a requirement for participating in the network.

The bitcoin world got this right by being honest about the distinction and building tooling for both models. AT Protocol could do the same: call appviews what they are (trusted intermediaries), build clients that can work with or without them, and create tools that make it progressively easier for users to reduce their dependence on appviews if they choose to.

https://nekomimi.leaflet.pub/3m2zuk5hdqs2z
The Farce of Empire
The Republic ended. The Empire that replaced it is already dying. And the citizens-turned-subjects watch the farce continue, some cheering, some numb, most simply trying to survive, all of them participants in a charade that everyone knows has already reached its conclusion but that no one possesses the courage to acknowledge has ended.
Show full content

The marble columns of Rome gleamed under a sun that had witnessed too much, their polished surfaces reflecting a city that had traded its Republic for the gilded cage of Empire. In the shadow of these monuments to stolen glory, the Senate still gathered and debated. Passed decrees—but all with the hollow pretense of a theater troupe performing a play whose ending had long been decided. Augustus had masterfully cloaked his autocracy in the vestments of Republican tradition, and his successors, with varying degrees of subtlety, continued this charade as the Empire's foundations began to crumble.

The Roman populace, once citizens of a proud Republic, had been reduced to a dependant mob, pacified with bread and circuses while their wealth was systematically extracted to fund an ever-expanding bureaucracy and military machine. The Praetorian Guard, once protectors of the state, had become kingmakers and executioners, their loyalty for sale to the highest bidder. Taxes mounted as inflation debased the currency, yet officials proclaimed each new levy a necessity for security, each new deprivation a sacrifice for the common good. The state's propaganda machine worked tirelessly, transforming humiliation into victory, scarcity into abundance, and fear into patriotism.

In the forums and bathhouses, whispers of the old Republic lingered like ghosts, quickly silenced by the watchful eyes of informants. The great families that had once wielded real power now curried favor at the imperial court, their ancestral dignity traded for proximity to power. Meanwhile, the provinces bled, their resources funneled toward the capital to sustain the illusion of prosperity. The Empire became a vast apparatus of extraction and control, its purpose no longer the well-being of its people but the perpetuation of its own bloated existence. Rome had become a corpse that still believed itself alive, its citizens both victims and accomplices in a farce of their own making.

The echos are alive for sure, yet not. The Princeps Civitatis has none of the vigor. None of the fear. Rome is dying and taking the world order with it yet again, but disgracefully. We see an autocrat wielding power because he is fearful of his own citizens. He won't even create the red velvet lie of 'protecting the heartland' like many Civitai before him.

But to frame this as merely the failure of one man—one weak, fearful, incompetent Princeps—is to mistake symptom for disease. The power he wields was not seized. It was left unguarded, abandoned by the very institutions meant to jealously protect it, lying in the forum like a discarded weapon that anyone with sufficient ambition and shamelessness could simply pick up.

The Senate still convenes. The power of the purse—that most sacred check, the one restraint the founders of the Republic believed could never be surrendered—was not wrested from their hands. They opened their palms and let it fall. For decades before this Princeps, they had been abdicating through continuing resolutions that governed by inertia rather than deliberation, through omnibus bills so sprawling that no senator could claim to have read them, through the quiet acceptance that executives could simply refuse to spend appropriated funds and face no real consequence. The Princeps simply declares what funds he will release and what he will withhold. Money appropriated by the Senate flows to loyal provinces, while recalcitrant territories find their allocations frozen, their disaster relief delayed, their infrastructure projects mysteriously stalled. But he does this because they taught him he could—through years of looking away, of avoiding hard votes, of preferring executive action because it freed them from accountability.

Emergency declarations pile atop one another like sedimentary layers, each granting extraordinary powers that somehow never expire, until "emergency" has become the permanent condition of imperial rule. When senators object, they discover that objection without enforcement is merely performance, and enforcement would require them to admit that the Republic ended years ago while they were still applauding. They discover that they prefer the theater of opposition to the risk of actually wielding power. Better to let the Princeps act and then condemn him, than to exercise their constitutional authority and face voters' judgment for the result.

The war powers went even more quietly, surrendered not in a single dramatic moment but through the slow erosion of decades. The legions had been deploying without senatorial authorization since the wars in distant eastern provinces generations ago. The Authorization for Use of Military Force, passed in the panic after barbarians struck the capital, simply codified what had already become practice—that the Princeps could wage war wherever and however he pleased, and the Senate's constitutional authority was a formality to be observed in times of political convenience and ignored in times of executive will. The Princeps commands, client states are threatened, alliances dissolved, strikes ordered against enemies who may or may not actually threaten Rome. The Senate occasionally stages debates about reclaiming its constitutional authority over war and peace, then quietly tables the measures, afraid to learn what would happen if they passed a resolution the Princeps simply ignored. But their fear is not of his strength—it is of their own impotence, finally made explicit.

The Praetorian Guard sits in its marble temple, nine figures in robes who were meant to protect the constitutional order but have instead become its most effective destroyers. They did not wake one morning and decide to betray their mandate. They have been building toward this for generations, through legal theories incubated in partisan academies, through careful appointments by executives who understood that the Guard could be turned from shield to sword. They have granted the Princeps immunity so sweeping it would have embarrassed Caligula. They have blessed his emergency powers, his rewriting of laws through executive decree, his transformation of the bureaucracy into a personal instrument. They wrap each capitulation in the language of judicial restraint and original intent, calling constitutional surrender "deference" and calling the death of checks and balances "textualism." When the Princeps violates even their generous interpretations, they simply refuse to hear the case, letting his actions stand through silence rather than through the embarrassment of explicit approval.

But here is where theory collides with the brute fact of individual agency: none of this structural rot, none of this institutional abdication, none of this decades-long erosion of republican checks explains why this Princeps wields power with such disgraceful incompetence, such transparent malice, such pathetic fear.

Where Augustus at least possessed the competence to make autocracy functional, where even the weaker Julio-Claudians maintained some pretense of serving Rome's glory, this Princeps cannot even muster the energy for a convincing fiction. The institutions left power lying on the floor, yes—but they did not dictate what he would do once he picked it up. He could have, in theory, used unchecked executive authority to build infrastructure, to reform administration, to create a system that, however unjust in its concentration of power, at least functioned toward some coherent end.

Instead: The tariffs are impulsive, contradictory, imposed and lifted based on which foreign leader paid proper tribute at his resort. The deportations are theatrical cruelty, performed not for any coherent policy goal but as spectacle for the mob. The prosecutions of rivals are announced on the forums—the social media platforms that have replaced the bathhouses—before charges have even been formulated, transparently vindictive, almost comically petty.

The institutions made autocracy possible. But the Princeps made it disgraceful.

He rules from fear. Not the population's fear of him, though he cultivates that through the security apparatus and the Praetorian Guard. No—his fear of them. The structural weakness of institutions explains why he could accumulate power. It does not explain why he wields it from behind barricades of executive orders and emergency powers, why he demands oaths of personal fealty from prosecutors and generals, why he lashes out at the slightest criticism, the smallest demonstration in the provinces, any suggestion that his authority might be questioned.

This is not strategic consolidation. This is not the calculated autocracy of a leader who understands that unchecked power requires at least the performance of serving the state. This is a man who cannot distinguish between the state and himself, who accumulated absolute authority because the institutions abandoned their posts, and who now uses that authority for nothing except self-preservation, petty vengeance, and the inflation of his own ego.

Both realities exist simultaneously, each making the other worse. The institutions created the vacuum. The man filled it with poison. The Senate's abdication made autocracy inevitable. The Princeps's pathology made it uniquely destructive and absurd. Rome did not require this particular kind of disgrace—a Princeps who golfs while the state collapses, who cannot articulate a vision beyond his own survival, who rules through incoherent tantrums and culture war obsessions. But the institutions' surrender ensured that when such a man arrived, nothing could stop him.

And meanwhile, the old Roman dream—that fiction of universal citizenship, of being Roman first and provincial second—has rotted away entirely. Though perhaps "rotted" suggests something once healthy that decayed. The truth is uglier: it was always a lie. Rome spoke of citizenship and law while building its wealth on slavery, while denying personhood to the majority of those who lived within its borders, while reserving the full rights of citizenship for a narrow class defined by birth and blood. The high rhetoric of the Republic coexisted with the mundane reality of human beings as property, of entire populations excluded from the civic body they were forced to sustain through their labor.

So too with this Rome. The dream of universal citizenship, of equality before the law, of being Roman first and provincial second—this was proclaimed while millions labored in bondage, while the law itself encoded their status as property, while citizenship was explicitly restricted by race. The beautiful rhetoric and the brutal reality existed simultaneously, neither negating the other in the minds of those who benefited from the contradiction. The lie was not a betrayal of founding principles. The lie was the founding principle—that freedom and slavery, equality and hierarchy, citizenship and subjugation could coexist as long as the hierarchy was maintained, as long as everyone understood which Romans were really Roman.

What has rotted away is not the universal dream—that never existed—but rather the precarious expansion of that dream to include those it was originally designed to exclude. For a brief historical moment, through blood and struggle, some of the excluded forced their way into citizenship, made the lie slightly less sweeping. That expansion is now contracting. The old hierarchy reasserts itself, the original vision of restricted citizenship returns, and those who benefited from the lie's partial dismantling discover it was only ever a temporary accommodation, a concession that could be revoked once the balance of power shifted.

Here too, both structural and individual forces compound. The economic extraction from provinces to capital, the concentration of power in the imperial center, the transformation of citizens into dependents—these are structural outcomes of imperial systems, observable across centuries and civilizations. But the Princeps has taken these structural tendencies and weaponized them through deliberate cruelty and explicit hierarchy.

Those in the capital and its surrounding territories have declared themselves the true Romans, the real citizens. The others, the ones from the wrong provinces, who speak the wrong languages, who worship the wrong gods, whose skin marks them as insufficiently Roman—they are subjects now, their loyalty perpetually suspect, their rights contingent on proper demonstrations of submission to the imperial center. The Princeps did not create the conditions for this tribal regression. But he has made it explicit, celebrated it, turned it from implicit bias into explicit policy.

The provinces that once believed themselves equal partners in the Republic now exist only to be extracted from. Their resources flow toward the capital while the Princeps vilifies them as disloyal, threatens to withhold disaster relief, sends the legions to enforce compliance. The structural incentives of empire explain the extraction. They do not explain the vindictive glee with which this Princeps punishes disfavored territories, the personal spite he brings to the systematic looting.

The great merchant families—those who control the grain supply and the shipping lanes and the new technologies—prostrate themselves at the imperial court. Their fortunes depend not on commerce or innovation but on the Princeps's favor, which can be granted or revoked with a single post in the forums. This is the predictable outcome of any system where state power and economic power fuse. But the arbitrary, capricious nature of this Princeps's favor-granting—the way fortunes rise and fall based on personal flattery rather than even corrupt but comprehensible exchange of benefits—this is particular to the man, not the structure.

The bread and circuses continue, though now the bread comes as targeted subsidies and tax relief for the loyal territories, carefully withheld from the suspect ones. The circuses are culture war spectacles, manufactured enemies, an endless parade of internal threats to justify the security state's expansion. The mob cheers as the Princeps promises to purge the disloyal from the bureaucracy, to root out the hidden enemies, to restore Rome to its former glory—though no one can quite articulate when that golden age occurred or what it looked like.

In the forums, whispers of the old Republic persist. Remember when the Senate held the purse? Remember when only the Senate could declare war? Remember when the Praetorian Guard checked executive power instead of blessing it? Remember when we were citizens instead of subjects, when being Roman meant something more than demonstrating proper tribal loyalty? But these whispers grow more dangerous by the day. Each agency has become a miniature surveillance apparatus. Each loyalist a potential informant. The smart Romans have learned to perform enthusiasm for the imperial project while carefully avoiding any topic that might reveal insufficient devotion.

Rome staggers on, a machine of extraction and control that serves no purpose except its own perpetuation. The structure is self-sustaining—empires do not require competent emperors to continue grinding their populations into dust. But this Princeps manages to make even the grinding gratuitous, adding layers of cruelty and chaos that serve no structural function, that advance no imperial interest, that exist purely as expressions of personal pathology amplified to the scale of state power.

He grows older and more paranoid, unable to imagine succession because he cannot distinguish between the state and himself. The Senate performs its constitutional duties while everyone understands they mean nothing. The Praetorian Guard issues rulings that would have been unthinkable a generation ago, and the next generation will inherit even more concentrated power, even fewer functional checks, even less memory of what the Republic was supposed to be.

The world order that Rome built and sustained—the network of alliances and treaties and international norms—crumbles as the Princeps treats allies with contempt and enemies with incoherent inconsistency. Client states that once looked to Rome for protection now hedge their bets with rival powers. The barbarians watch and wait and test the frontiers, probing for weakness, finding it everywhere. Here again: structural imperial overextension would have caused this decline eventually. But this Princeps has accelerated it through incompetence and spite, burning alliances for no strategic purpose, alienating client states through personal insult rather than calculated interest.

Rome is taking the world down with it, but this time without even the dignity of a coherent vision or a worthy opponent. Just decay. Just exhaustion. Just a frightened autocrat wielding absolute power from behind the walls of a state apparatus he's too paranoid to leave and too incompetent to effectively wield, enabled by institutions that surrendered their authority long before he arrived to claim it.

And now, as if to crystallize both failures into a single perfect image, the government has simply stopped functioning. The Treasury cannot pay its bills. The grain administrators are furloughed. The aqueduct inspectors sit idle. The border garrison payroll is frozen. Rome, the eternal city, the center of the world, cannot perform the basic administrative functions that even a moderately competent provincial governor manages without incident.

The structural critique observes: this shutdown is possible only because the Senate spent decades accepting that appropriations were suggestions rather than commands, because they built a system where executive signing was a veto point rather than a formality, because they preferred to avoid hard votes and let the executive absorb political blame for governance.

The individual critique observes: the Princeps is at his coastal villa. Golfing. Hosting banquets. Posting in the forums about how this paralysis is entirely the Senate's fault—they refused to appropriate funds for his pet projects, you see, they obstructed his vision for Rome's glory. But also, in the very next breath, he celebrates the shutdown. Those furloughed administrators? They were disloyal anyway. Provincial appointments, most of them. Probably engaged in all sorts of degenerate practices, promoting foreign cults, undermining traditional Roman values with their obsession with exotic eastern religions and their tolerance for deviant behavior.

"Transgender for everybody," he says without a flinch, absurdity of the statement ringing no bells.

Better that they're sent home, he thinks. A purge by paralysis.

The institutions created the possibility space for this shutdown. The man filled that space with incoherent vindictiveness. The Senate's abdication explains why he can let Rome grind to a halt without consequence. It does not explain why he does so while simultaneously blaming others and celebrating the chaos, why he cannot articulate a coherent position, why he rules from a villa fixated on culture war phantoms while the state he controls literally cannot pay its employees.

The contradiction doesn't register with him, or perhaps it does and he simply doesn't care. The Senate won't give him money, which is their fault, but also he's glad government workers aren't being paid because they're his enemies anyway. Rome is grinding to a halt, and the Princeps cannot decide whether this is a crisis he blames on his opponents or a victory he claims as his own. So he does both, sometimes in the same declaration, secure in the knowledge that the mob will simply accept whichever frame suits their prejudices.

The senators watch this unfold with the hollow-eyed exhaustion of men who have long since stopped believing their votes matter. Some make speeches about fiscal responsibility and constitutional duty. Others rush to the forums to praise the Princeps's strength and wisdom. All of them know the truth: the government will restart when the Princeps decides it will restart, not a moment before, and their appropriations and authorizations are just set dressing for a decision that will be made at the villa between rounds of golf.

But here is the final, cruelest irony: they could, even now, reclaim their power. The constitutional authority still exists, dormant but not dead. They could refuse to pass continuing resolutions and force genuine appropriations debates. They could rescind the emergency authorizations. They could impeach and remove. They could use their oversight powers to actually constrain executive action. The structure bent, but it has not yet broken completely.

They will not. Not because the Princeps has somehow prevented them, but because doing so would require courage, would risk political consequences, would demand they actually govern rather than perform governance. They prefer the theater. They prefer to blame him while enabling him. They prefer the structural excuses—the system made this inevitable, nothing could be done—to the uncomfortable reality that they are choosing this outcome through daily acts of cowardice.

The Republic ended. The Empire that replaced it is already dying. The institutions surrendered their authority through decades of erosion and abdication, creating the vacuum. The Princeps filled that vacuum with grotesque incompetence, transparent malice, and pathetic fear, making the autocracy not merely unjust but absurd. And the citizens-turned-subjects watch the farce continue, some cheering, some numb, most simply trying to survive, all of them participants in a charade that everyone knows has already reached its conclusion but that no one possesses the courage to acknowledge has ended.

Both things are true. The system failed. And the man is evil and stupid. Each made the other worse. Each provides an excuse for not confronting the other. And Rome burns while everyone argues about whether to blame the arsonist or the fire code that was never enforced.

https://nekomimi.leaflet.pub/3m2xdvhq5a222