My AT Protocol Repository Goes Back to 2021. AT Protocol Doesn’t.

I exported my entire AT Protocol repository and got back a roughly 63 MB CAR file containing 133,977 live records across 190 collections.

Most of it is music. There are 101,112 fm.teal.feed.play records, alongside 8,989 Bluesky posts, 9,202 reposts, 3,668 likes and 1,096 follows. There are also 515 site.standard.document records, 843 Streamplace chat messages, and records from enough smaller ATProto applications that I need the collection names in front of me to remember some of them.

The oldest music event represented in the repository is dated 9 September 2021.

My DID was created on 15 April 2024.

If I read those dates as belonging to one chronology, I apparently have nearly five years of AT Protocol history despite joining less than two and a half years ago. More impressively, I would have been using AT Protocol more than a year before it was called AT Protocol.

I was sixteen in September 2021. I was listening to music. I was not participating in a decentralised social protocol which had not yet been publicly specified.

The timestamp itself is fine. What took me a moment was realising exactly what it was dating.

The timestamp belongs to the listen

Bluesky had existed as a project since 2019, but the technical work which became AT Protocol emerged publicly later. The team wrote about a self-authenticating social protocol in 2022, followed by ADX, the Authenticated Data Experiment, before renaming it the Authenticated Transfer Protocol on 18 October.

The network I eventually joined was still taking shape after that. Bluesky opened production federation to outside PDS operators on 22 February 2024, less than two months before my own DID appeared in PLC on 15 April.

There is therefore no plausible interpretation where a Teal record from September 2021 was already sitting in an AT Protocol repository at the time. What happened in 2021 was considerably less technical: I listened to some music, and one of the services I used kept enough information about that event for me to recover it later.

Years later, I imported it.

That provenance becomes harder to see once everything is sitting in the same repository. A Bluesky post I wrote in 2024 was generally created through ATProto at roughly the time its createdAt says it was. A Teal record saying I listened to something in 2021 was created later, but the event it describes genuinely happened in 2021. Some of my long-form documents introduce the same complication because older writing was migrated while retaining its original publication date.

The repository has no reason to force all of those histories to begin together. Records can describe events older than themselves, which becomes quite important when the point of the software creating them is specifically to preserve old history.

In my case, that software is Malachite.

I wanted my listening history back

I originally wrote Malachite because I wanted to move years of Last.fm and Spotify history into Teal and couldn’t find anything which would do it.

There wasn’t a larger plan behind that. I wanted a tool, the tool did not appear to exist, and after being annoyed about that for long enough I wrote it myself.

The original name was atproto-lastfm-importer, which I later described in its README as “generic as fuck”. It sounded like exactly what it was at the beginning: a script for one particular job, with no real reason to assume anybody would still care about it a few months later.

By January 2026 I’d renamed it Malachite. The mineral has the preservation-and-transformation association I wanted, and the green-blue colour gave it an obvious connection to Teal without making the name depend on one particular source service. Last.fm had already stopped being the whole project by then anyway.

The first implementation was JavaScript, and it taught me fairly quickly that importing years of somebody’s listening history is not the same workload as an ordinary social application creating a few records every so often. I already had crude batching, delays and rate-limit protection and still managed to push it hard enough that I effectively DoSed the entire PDS.

That was at about one in the morning, because of course it was.

Rate limiting became much less theoretical after that. January brought an adaptive limiter, duplicate detection, Spotify support, combined imports, resumable state and better handling for historical TIDs. February contains an almost absurd amount of git history concerned with what different PDSes actually mean when they expose rate-limit information, how much headroom an importer ought to leave and how to recover without either giving up prematurely or hammering the server again.

Later versions moved repository inspection onto CAR exports, added dynamic batching and proactive pacing, and accumulated support for more sources. Malachite now understands Last.fm, Spotify, Apple Music, YouTube Music and ListenBrainz, with enough source-specific handling around them that “convert listening history into Teal records” hides quite a lot of unpleasant detail.

Apple changed its export schema. ListenBrainz exports turned out not to arrive in one conveniently documented shape. MusicBrainz identifiers showed up in forms Teal’s Lexicon wouldn’t accept. Spotify and YouTube occasionally don’t know the artist. I have developed increasingly strong opinions about software filling that gap with "Unknown Artist" and then permanently publishing a value it simply invented into somebody’s repository.

Most of that complexity appeared because Malachite stopped being the script I personally ran once.

Shahoob contributed the first ListenBrainz work through a pull request in June. I merged it before the entire flow was actually finished, which was my mistake, and later completed and repaired the integration once real exports started exposing assumptions in it. In August Matt, one of the Teal developers, sent the pull request which moved Malachite onto Teal’s production fm.teal.feed.play namespace while retaining reads from the old fm.teal.alpha.feed.play collection.

I followed that with a polish mode for migrating the old alpha records into the production collection, including the batching and rate-limit work necessary to move tens of thousands of records without rediscovering the first version’s relationship with my PDS.

Malachite is still primarily my project. I do most of the development and maintenance, and its direction is usually determined by whichever part of music-data portability is annoying me that week. It is no longer solely my code, and it stopped being solely for me quite a while ago.

At this point it has effectively become the de facto public migration tool for Teal. That isn’t an official designation — Malachite isn’t a Teal project — but if somebody has years of listening history in one of the services it supports and wants to bring that history across, it is the general-purpose tool available to them.

That is how more than a hundred thousand historical listening events ended up under a DID created in 2024. It is also roughly where one importer started turning into several other projects.

Malachite kept producing other problems I wanted to solve

My pkgs monorepo technically existed before Malachite was moved into it, so saying the repository itself began with Malachite would be wrong. The current Croft.click toolkit, though, grew very obviously out of the work around it.

I imported the Malachite CLI and web histories into pkgs on 13 March 2026. Eleven days later I added Bismuth, which works in almost the opposite direction: it takes long-form Leaflet and Standard.site content already stored in ATProto and converts it back into Markdown.

Jasper followed in April for Instagram exports. One of its early commits explicitly describes its logger as mirroring Malachite’s, while other pieces reused the earlier work more directly. By the end of April I had enough related tools that croft.click became a directory for them, initially with Malachite and Jasper sitting next to each other.

Bismuth gained a web interface. Opal arrived for old social-media archives. Tourmaline grew around analysing Teal listening history rather than importing it. The projects started sharing enough of the same assumptions that maintaining independent versions became a worse option than extracting them: layouts moved into landing-ui, TID handling became reusable, and authentication, CAR processing, publishing and rate limiting increasingly accumulated in croft-click-core.

This was not a planned product family. It was mostly what happened after I solved the same class of problem enough times that copying the solution again became more irritating than creating shared infrastructure.

The individual tools still do quite different things. Malachite brings old listening history into Teal. Jasper imports an Instagram archive. Opal deals with old social posts. Bismuth gets long-form records back out into an ordinary format. Tourmaline assumes the music is already in the repository and tries to tell you something useful about it.

What joins them is that I kept running into cases where an application had accumulated useful history and saw no particularly good reason that changing application should mean starting again.

Bismuth pushed that interest further because working on long-form exports meant spending a lot of time around Standard.site: publication and document records, Leaflet blocks, facets, body formats and the awkward bits where several applications can understand the same publishing metadata while representing the document itself differently.

After spending enough time there, I noticed another gap.

There was no native mobile application for the Standard.site ecosystem.

No iOS app. No Android app.

This annoyed me for essentially the same reason the missing Teal importer had annoyed me. There was an ecosystem I was already using, nothing about the protocol prevented a proper native client from existing, and nobody had built the application I wanted to install.

So on 19 June 2026 I started Inkwell.

It was also my first native mobile application.

Instead of learning native development with something small and safely disposable, I decided my first attempt should involve AT Protocol OAuth, PDS resolution, cross-repository discovery, multiple open-ended publishing formats and a reader which had to tolerate records created by applications I did not control.

Standard.site support, reader and writer flows and interoperability tests were already landing the next day. Within a few more days I was modelling all eighteen Leaflet block types, rendering them natively and reproducing parts of Leaflet.pub’s reader behaviour.

Android began separately on 24 June, written in Kotlin and Jetpack Compose. The website developed separately too. Those histories grew alongside each other until August, when I brought Android and the website into the current Inkwell repository with git subtree, preserving where they had actually come from instead of manufacturing a cleaner monorepo history after the fact.

Putting both applications together made the duplicated logic difficult to ignore. iOS and Android had their own versions of AT URI parsing, Markdown handling, facet conversion, theme resolution and other behaviour where maintaining two answers mostly creates opportunities for them to drift quietly.

Inkwell 2.0 moved that sort of code into a Kotlin Multiplatform shared core. Android consumes it directly; iOS gets it through an XCFramework and thin Swift wrappers. The presentation remains native because I want an iOS application to feel like an iOS application and an Android application to feel like Android, while something like UTF-8 facet conversion has no good reason to work differently between them.

Bismuth therefore did help lead to Inkwell, but not as part of some elegant roadmap. I was already working on long-form interoperability, noticed another thing I wanted which nobody had built, became annoyed about that, and opened Xcode.

It turns out that has been a fairly productive development process for me.

Then people started paying me

The part I didn’t expect from any of this was that people would start giving me money for it.

My mum suggested in February that I could have released Malachite as paid software. I didn’t particularly want to. A lot of the appeal of building this tooling around AT Protocol was that somebody could take their own data, use the software and leave again without me placing another payment gate between them and their history.

I opened a Ko-fi instead, without a private build of Malachite or features reserved for people who paid me.

Within hours somebody sponsored me for £15 a month.

I was surprised enough that my Bluesky post announcing it was essentially disbelief. The next morning I pointed out that the people supporting me had, quite literally, paid for my breakfast.

The support continued rather than remaining a novelty. More tips came in, I added GitHub Sponsors, and by the summer I had multiple people giving me money every month because they wanted the open-source work to continue. That original £15 sponsorship later meant I didn’t need to dip into money I’d put aside for my new Mac mini. In July another person added US$5 a month through GitHub, and the recurring support eventually became enough that I could justify switching my music subscription to Apple Music without thinking too hard about the extra expense.

On 20 August, somebody sponsored me for £125, which is the largest individual sponsorship I’ve received.

I wouldn’t call that a job. Sponsorship income is not a salary, and somebody giving me a few pounds each month does not get to direct the projects. It is still a decent chunk of money from software which remains open source, including several payments which now recur every month.

That has made me more comfortable with the distribution model I eventually want for Inkwell.

Once I have a stable income from an actual job, my current plan is to publish Inkwell on the App Store and Google Play for a flat £5 purchase. It will remain open source, and the free distribution methods will remain available: F-Droid on Android and AltStore Classic on iOS. Inkwell isn’t available through AltStore PAL.

I don’t intend the store build to contain a better version of the application. Someone willing to use F-Droid or AltStore Classic will still be able to get Inkwell without paying me. The £5 is for the ordinary mainstream-store route, including the installation and update experience people expect there, and it also gives people another way to support development without committing to a recurring sponsorship.

There is some less interesting work to do before then. Inkwell is currently AGPL-3.0, and I want to properly review how I handle app-store distribution and any licensing exception it requires instead of discovering that problem during submission. The README, legal documents, website and release metadata also describe F-Droid and AltStore Classic today because those are the channels which actually exist.

I like the arrangement precisely because it doesn’t require reversing the reason I rejected paid-only Malachite in the first place. The source remains available, free builds remain available, and paying me is something people can choose to do rather than a prerequisite for using the software.

That matters more to me now because the repository which started this entire train of thought is itself evidence of why I care about being able to move things around without asking the original application for permission.

The records have survived several applications.

The identity holding them has moved even more.

The repository moved with a DID that kept changing servers

My imported music explains how the repository can contain history from 2021. The identity attached to it has a separate chronology which really does begin in 2024, and its PLC audit log records most of it.

did:plc:ofrbh253gwicbkc5nktqepol was created on 15 April 2024. It initially pointed at Bluesky’s boletus.us-west.host.bsky.network PDS under the handle ewancroft26.bsky.social. The following morning I changed that to ewancroft.bsky.social, and later the same day I moved onto ewancroft.uk.

The DID has stayed the same since.

The server underneath it emphatically has not.

The audit history contains nine distinct PDS endpoints, including Bluesky’s hosting, pds.ewancroft.uk, altq.net, pds.tophhie.cloud, selfhosted.social, WAFRN, Eurosky and pds.croft.click, plus a private Tailscale endpoint which briefly appears during a rather more complicated episode in July.

Some temporary handles appear as collateral damage from various tests. hellothere.pds.ewancroft.uk lasted about half an hour. ewan.croft.click survived less than an hour. ewan.eurosky.social existed for under a minute before I put ewancroft.uk back.

Most of those migrations trained me into treating movement as relatively mundane. Move the repository, update the identity, wait for the network to follow the change and keep using the same DID somewhere else.

That confidence held until 14 July 2026, when I was testing Tranquil, an early alternative PDS implementation, and managed to leave the PLC state for my main account broken badly enough that the AppView could no longer resolve it properly.

The repository had not disappeared. Neither had the blobs. What I had damaged was the identity state telling everybody where that repository lived and which keys still had authority to change the DID.

My profile stopped resolving correctly and posts disappeared from view. I knew the underlying records were intact while watching the public identity people actually used to reach them become effectively unusable.

I spent roughly six hours trying to recover it that night and eventually went to bed without succeeding. My own blog post afterwards is quite clear about that. I stopped because it was light outside and I had run out of useful things to try, not because the account had recovered.

The next day I spent about half an hour on Signal with Bailey — @pds.dad — who created PDS MOOver. Bailey helped me work through what was actually wrong with the DID document and how to repair it.

The reason a repair was still possible was that I had a backup PLC rotation key. I still possessed a key with enough authority to sign new PLC state, so the broken document was recoverable rather than permanent. With Bailey helping me work through the state I had produced, I used goat, the Bluesky project’s Go AT Protocol CLI, to deal directly with the PLC and migration operations.

Eventually it worked.

My immediate Bluesky message to Bailey was:

IT WORKED OH MY FUCKING GOD

which is considerably closer to how that felt than the PLC audit log is.

A few minutes later I posted that I’d fucked up my DID document during a test migration, spent six hours trying to recover it and had finally got the account back. I also told people to use Bailey’s PDS MOOver rather than reproducing my manual adventure.

The PLC log reduces the whole thing to endpoint changes, key operations and one nullified attempt before valid state replaces it. A few days later the DID points at pds.croft.click again.

Technically, that is exactly what happened. Experientially, it leaves quite a lot out.

Until then I had tended to think of repository portability and account portability as parts of essentially the same property. July made the separation much harder to ignore. I had backups of the records. What frightened me was losing the authority required to make the identity point at them again.

The CAR was useful during all of this, but it was not enough on its own. Keeping control of the DID required something outside the repository entirely.

That distinction is also useful when looking back at the export now.

The export isn’t one timeline

Once I stopped treating every timestamp in the CAR as “this entered ATProto at this exact moment”, the apparent contradiction disappeared.

The music history reaches back into 2021 because I imported it later. Native ATProto activity begins in April 2024. Some migrated long-form documents preserve publication dates from before their current records existed. PLC records another history again, describing where the identity pointed and which keys were authorised to move it.

The repository specification doesn’t describe a chronological diary. A repository is a signed data structure associated with a DID, and the records inside it can represent whatever their Lexicons allow, including events which predate that DID.

The CAR is not a complete diary in another sense either. It represents the live repository I have now, rather than an immutable record of everything I have ever created and deleted. A music play can accurately say what I listened to and when without preserving why I chose it. A reply can remain after the surrounding conversation has disappeared. A publication date can survive while most of what was happening around me when I wrote something does not.

What it does contain is an unexpectedly broad amount of my digital history in one place.

The 190 collections are why I increasingly find “my Bluesky account” technically inadequate even though it remains convenient shorthand. Bluesky is still where most people know the DID from, and its records make up a lot of my ordinary social activity, but the same repository now contains five years of listening history, long-form publishing, Streamplace messages and data from an assortment of other ATProto applications.

The software I ended up building around it reflects the same tendency. Malachite assumes listening history can outlive the service which first recorded it. Bismuth assumes writing stored through one application should still be recoverable elsewhere. Inkwell assumes Standard.site documents do not belong exclusively to whichever web frontend originally published them.

I was sixteen when the oldest listen represented in this export happened. I’m twenty-one now. Spotify and Last.fm held parts of that history first, Malachite brought them across later, and those records now sit beside data produced by applications which didn’t exist when I joined Bluesky, never mind when I listened to that first track.

So the export isn’t evidence that my AT Protocol repository existed in 2021.

It is evidence that joining in 2024 did not require me to pretend the years before it hadn’t happened.