Engineering the DID Ring System
This post is a record of how I engineered a novel system — DID Ring — with the aid of LLMs. The method matters more than the tool: I treat AI like a list of library books I have not read. I do not treat AI output as the books themselves. I think doing my own research means relying on primary sources, not the summary. What follows are my prompts, the few sentences of each reply I actually responded to, and a marker at each point where I opened a new chat. Every new chat describes a new goal, but the underlying reason I start one is context engineering: when my goal changes, that changes the required context, and I do not want LLM output to trip over context that describes unrelated or wrong concepts. I am always trying to engineer the context (in a chat, a terminal tool, or an agentic harness) to maximize the probability that the LLM output is correct and useful.
I decided I liked pfrazee.com.
My prompthttps://www.pfrazee.com uses what blog software?
ClaudeThe site runs on the Tailwind Nextjs Starter Blog theme — a Next.js–based blogging template by Timothy Lin.
…
Some of Paul Frazee’s posts aren’t traditional blog entries but “leaflets” hosted on his ATProto PDS and syndicated to the blog.
I did not like depending on Javascript. So I laid out what I actually wanted, in priority order.
My promptI want to explore the contemporary options for blogging. I have the following desired characteristics, in priority order with highest priority first:
- Threading/topic grouping. I want to display connections of posts on a single topic on a single web page.
- Low backend server overhead, implemented in Rust.
- Native phone and desktop scaling, especially for support for 4K widescreen monitors. Not every view should be a single column, but that is clearly correct for phone.
- Easy theming with published theme galleries.
- First class support for non HTML content: PNG, SVG, markdown.
- Non-chronological post ordering.
- Infinite scroll to find new content.
- WYSIWYG editor.
- Compatibility/interoperability with the Atmosphere AT Protocol
- Compatibility/interoperability with phpbb3.
- Compatibility/interoperability with Discord.
ClaudeNo single contemporary tool satisfies all eleven, and the reason is structural: the list straddles two architectural forks that don’t currently coexist in one piece of software.
…
So #2 (Rust) and #9 (ATProto) are effectively mutually exclusive in any turnkey tool today.
…
Zola is the unambiguous winner on the top-tier non-threading requirements.
My promptI like SSGs. The topics/threading can be static. The key is I do not want my home page to be chronological. I want to define the content for that and a few other pages according to other concerns. If taxonomy gives me that facility, I think Zola would work. I might be interested in booking on 7 afterwards. I will forgo 8+.
That settled the platform: this site is Zola. The unsolved half was the dynamic part — comments and likes with real federated identity. That required different context, so I opened a new chat.
My promptI want to make posts on my website that can be replied to by users of BlueSky, and display some fraction of the replies on my website. My curation is non-negotiable; I must have full control over everything that appears on my website. Is AT Protocol the right technical base for this endeavor? My ideal solution space involves creating an open source tool that allows for Disqus-style comments using federated identity. I would very much like to have a record of who engaged with my posts, including their federated identity and posting history, regardless of whether it was a reply or a like or a repost. I do not care about the network connections of BlueSky users, just the interactions they have with my posts on my website.
ClaudeMode A — Observer/mirror (no auth). Users reply/like/repost on Bluesky; you fetch and curate. Simplest, zero auth, zero DevOps beyond your static site. This is the “Disqus replacement” most people build.
Mode B — Native commenting with federated identity (atproto OAuth).
My promptMode B for certain. There’s no existing WordPress plugin or something similar which would enable Mode B interaction?
ClaudeAs of two weeks ago there’s a serious WordPress contender: ATmosphere, from Automattic / the team behind the ActivityPub plugin
…
But it is not your Mode B: it connects a single site account
…
It’s also a poller, not a streamer: comments are pulled on a cron schedule — the O(N)-per-cycle pattern you’re right to distrust.
ATmosphere does something so close to what I wanted I almost decided to use it. The primary problem was I did not like having to set up a cron job to enable interactions. The internet should be more immediate than that.
My promptLet’s punctuate the sketch of the data model with an estimate of the ingested data per my post/per reply/per hour on the Jetstream side. Solutions that aren’t O(N) in my post count are suspect. I also need a data model that can be made eventually consistent when a viral post generates a huge spike in traffic (both in viewing as well as in interacting). It’s ok if not every interaction is seen in real time, as long as I eventually see every interaction.
ClaudePer-event ingest/filter: O(1) per event, O(network event rate) total — independent of N. You watch the whole network’s like/repost/post stream and discard ~99.99% with a hash-set lookup. Same ~1 GB/hr for 1 post or 100,000.
This surfaced the problem that what I was asking for required parsing the firehose of data coming from the Fediverse to achieve what I was asking, which is way too expensive! A gigabyte an hour to catch a few interactions is the wrong architecture. The goal changed, so the required context changed: I opened a new chat.
My promptI want to create a Jetstream AT Protocol app targeted at the cheapest possible daily costs. I am willing to limit myself to specific DID, if those queries can limit the volume of data I need to ingest. What are my options?
Claude
wantedDidsis an array of repo DIDs that filters which records you receive on your stream, and you can specify at most 10,000 wanted DIDs. The filtering happens server-side, so you only pay for the bandwidth of matching events crossing the wire.…
wantedCollectionsfilters by Collection NSID, supports prefixes likeapp.bsky.graph.*, and is capped at 100 collections/prefixes.…
Resolve each DID to its PDS, call
com.atproto.sync.getLatestCommitto check whether therev/head CID changed, and only then fetch the delta viacom.atproto.repo.listRecordsorgetRecord.
My promptDefine PDS, CID, and Collection
I had no idea what any of these terms were nor how the AT Protocol was supposed to work. The citations I instruct Claude to give me with every prompt mean I always check upstream docs, not rely on a likely correct summary from Claude.
My promptI want a system built on DID and AT Protocol that operates a bit like classic 1990s web rings, but instead of a connection between sites I want to foster connections between DIDs. The goal is simple: I enter a record into my PDS using my DID. Other DIDs interact with my record (for example, app.bsky.feed.like). I get notified of which DIDs interact with my record. I want to avoid ingesting the public Jetstream instances as much as possible (zero is ideal, but only on startup is fine, or for rare events).
ClaudeIn AT Protocol, an interaction record lives in the interactor’s repo, not yours.
…
Discovering inbound interactions fundamentally requires someone to have watched the firehose and built a reverse index. The entire trick to your zero-ingest goal is therefore: make sure that someone isn’t you.
…
Constellation is a global backlink index for AT Proto.
…
This is not hypothetical — the pattern is already in use as “PDS guestbooks.” Someone publishes a guestbook record; visitors sign it by writing a record in their own repo pointing at the guestbook’s URI; Constellation indexes the backlink; the owner queries it.
The reply cited a blog post about “PDS guestbooks” as prior art, which sounds like the kind of system I wanted now that I understood what PDS means. The link had rotted:
My promptPost not found. No blog post with id
a-guestbook-and-welcome-message-for-my-pds— https://dame.is/blogging/a-guestbook-and-welcome-message-for-my-pds
ClaudeApologies — that link rotted.
…
The nekomimi “cutebook” writeup is the closest to your build, with the full fetch flow.
That led me to cutebook, which was already very close to the design I wanted. I read the write-up itself, not a summary of it. Design work needs different context than exploration: new chat.
My prompthttps://nekomimi.leaflet.pub/3m6pcklmvt222 I want to create a web ring with guest book using the cutebook architecture in Rust. Let’s create a plan and a data model. Requirements:
- Open source package to be freely distributed to be used in a variety of different apps and websites. The concept of “web ring” is to be extended to a “DID ring”. I don’t care if members of the ring have web sites or use apps or what, as long as they have DIDs.
- I receive lists of DIDs that interact with my posts on the guest book (I expect I will be posting, much like a blog, and guests will like or reply to my posts).
- I receive lists of DIDs that are in rings with the DIDs I query, because those DIDs have chosen to publish their rings in a collection.
- I choose which DIDs I accept into my ring.
- Only interactions from DIDs in my ring are displayed in my guest book.
- I can revoke inclusion of a DID in my ring at any time, and it retroactively filters all collections from that DID from my guest book.
ClaudeBecause the view is recomputed on every read, the “ring” is just an allow-list, and the displayed guestbook is the set intersection
interactions ∩ ring_members, evaluated at read time.…
The two decisions I wanted a read on first, since they change the schema: ring topology (personal-ring-per-owner vs. named multi-host rings) and consent (unilateral roster vs. requiring the membership handshake).
Claude generated an initial Did ring plan doc. I had to clarify the DID ring concept to match what was in my head, which was I get to choose whoever I want to be in my ring but they get to choose whether to make it mutual.
My promptI want to choose to publish my personal ring in a collection, and query that collection to see the personal rings of others.
ClaudeThat resolves both forks: topology = one personal ring per owner, and consent = unilateral (everyone publishes their own ring; discovery is just reading others’).
…
your
pub.didring.membercollection is your ring.
One structural demand remained — the library had to stand alone:
My promptThis is great. The only piece that is missing is the front end and the didring need to be in separate repos. We want many folks to use didring as a standalone crate.
The generated Did ring plan doc matched what I needed after that correction. I switched to Claude Code at this point to actually implement DID ring.
I knew nothing about AT Protocol when I started thinking about blogging. I learned along the way what I really wanted was to use AT Protocol to produce a better engineered version of the internet I grew up using.