7 min read

ATProto Statusphere Setup In WSL, Pulling Records from Bluesky

For today, the first day of my 30 Days of Protocols, I'm just following the ATProto Quickstart Guide. It's a fun opportunity to work with JS after largely living in Python land for the last few months.

This is probably pretty easy and straightforward if you're not a Windows idiot like me (the general consensus from ATProto devs seems to be that Macs are superior, which I don't disagree with. However, I am stuck in Windows land for the foreseeable future, so I have to live like this regardless).

It seems easy and straightforward on WSL until you get past the OAuth step:

The default "Statusphere" homepage
Enter your bluesky ID (I use my own domain name but my account is hosted by bluesky, so this shouldn't be an issue)
Everything is fine so far - using bluesky as an OIDC provider, cool!
And, suddenly, I'm in server hell again.

I've been using WSL for long enough that I'm used to bullshit like this - it does a good enough job of emulating linux most of the time, but port forwarding and hardware stuff can always be finicky. A lot of open-source projects provide WSL-specific setup instructions, but since this is both in Typescript and, as I mentioned, made by spiteful Mac users, this quickstart doesn't come with them out of the box. The issue as far as I can tell is that WSL is forwarding localhost but not 127.0.0.1, which is an insane thing to do but that's beside the point.

I tried switching HOST in the project's .env to 127.0.0.1 and 0.0.0.0 to no avail. Searching for a hardcoded 127.0.0.1 somewhere reveals a redirect in client.ts:

Rude!

Changing this to localhost throws a bunch of errors:

Come on man.

In the spirit of "learning more about protocols" I looked up the relevant RFC but quickly decided I ain't reading all that. Some OAuth requirement that probably makes more sense in production. Look I'm already out of my comfort zone here I don't need to read an RFC on top of that.

This probably wouldn't be an issue if I weren't on WSL, so I can't blame the devs too much here, but I still don't love it. Anyway, since I don't seem to be able to make an easy fix, I'll change my WSL settings to "mirrored" networking mode:

Run wsl --shutdown in Powershell, then restart my WSL terminal and VSCode and restart the app: npm run dev.

Sweet! I can now access the app at 127.0.0.1:

And I'm able to authorize through bluesky and set my status:

Laptop guy feels accurate for today.

If I go to Bluesky Repo Explorer, I can see I have my first non-Bluesky record in my user repo:

Cool, I've at least got the example app running on WSL and writing to my repo (stored by Bluesky) now! But I don't know that I actually learned much yet about ATProto or how apps talk to each other, so let's see if we can add some features that pull in other records from Bluesky.

(Author's note: from here on out this is mostly me tinkering with the Bluesky API without any notable results, although I did start to understand how PDSes work a bit better).


Aside 1: I'm pleasantly surprised with the code complete results for Bluesky and ATProto's APIs - I'm just using Copilot with Claude 4, but whether it's through the training materials or just the code in this sample repo most of its first guesses at how to query something have been correct.


Aside 2: If you're following along with this or the quickstart you'll probably run into an annoying "token refresh" error when hot reloading your application that forces a log out. This is out of scope for this tutorial but it is extremely annoying to have to log in every time you make a change to the app. I assume Bluesky's engineers don't like having to log in every time their code hot reloads either, so I figure this is solvable, but couldn't find a quick fix for in my first couple searches.


I'll take inspiration from the tutorial's suggestions and see if I can fetch status records from my follow graph. I found this endpoint in the bluesky API docs, so I'll ping it and log the output to start - in routes.ts, I add the following to createRouter:

      const followGraphResponse = await agent.com.atproto.repo.listRecords({
        repo: agent.assertDid,
        collection: 'app.bsky.graph.follow',
      }).catch((e) => console.log('error fetching follow graph: ', e));

      console.log('follow graph response: ', followGraphResponse?.data)

Which gives me this extremely readable output:

I then list each record's value and log it:

      const followGraphStatuses = followGraphRecords?.filter(record => record && record.value && record.value.subject).map(record => {
        console.log('follow graph record: ', record.value)
      })

Which gives me a more recognizable series of follows with their relevant did:

I'll then map and filter this to see if anyone else I follow has done this tutorial. I rewrote the code above to better reflect the steps and added logging for requests.

      const followGraphRecords = followGraphResponse?.data.records;
      
      const followGraph = followGraphRecords?.filter(record => record && record.value && record.value.subject)
      
      const getFollowGraphStatuses = followGraph.map(async record => {
        const statuses = await agent.com.atproto.repo.listRecords({
          repo: record.value.subject,
          collection: 'xyz.statusphere.status',
        }).then((res) => {
          console.log('fetched statuses for ', record.value.subject, res.data)
          return res
        }).catch((e) => {
          console.log('error fetching statuses for ', record.value.subject, e)
          return undefined
      });
        
        return {'did': record.value.subject, 'statuses': statuses}
      })
      const followGraphStatuses = await Promise.all(getFollowGraphStatuses)
      const filteredFollowGraphStatuses = followGraphStatuses.filter(res => {
        return res.statuses != undefined && res.statuses.data.records.length > 0
      }).map(res => {
        return {'did': res.did, 'statuses': res.statuses?.data}
      })

      console.log('follow graph statuses: ', filteredFollowGraphStatuses)

For the grand reveal that, surprise, as far as I'm able to find no one I follow has done this tutorial. Exciting!

I'm more interested in fairly high number of errors I'm getting when requesting status records, which is a whole lot of this:

I'd expect blank records (which is what I got for the API requests that were successful), but these invalid requests seem to imply that I'm either getting throttled or my request is somehow malformed, since it's a client error. If we check the API docs, we see this warning about listRecords:

This endpoint is part of the atproto PDS repository management APIs. Requests usually require authentication (unlike the com.atproto.sync.* endpoints), and are made directly to the user's own PDS instance.

From browsing a few on the Repo Explorer, the only unifying trait is that all the profiles I'm successfully able to request have a PDS on https://amanita.us-east.host.bsky.network/. This is weird! I would like to understand what's going on here - specifically whether the issue is that my request is malformed in a way that only this PDS accepts, OR other PDSes are currently having issues, OR there is some other weird thing going on I don't understand here.

Keeping my first pass at that for posterity, but turns out I'm an idiot! I'm only successfully requesting my own PDS. If I had read the actual warning more closely maybe I could have figured this out - I'm authed to amanita.us-east and can only request a list of records from that PDS. It doesn't appear that the default request structure of the starter app is designed to get anything that's not stored in your own PDS. Kind of a weird way to do a starter kit, if you ask me, since the whole selling point of this protocol is to let apps and PDSes and so on talk to each other, and the guide itself encourages you to try fetching stuff from your follow graph as a next step, but I guess that'll be tomorrow's exploration. I withhold the right to determine I was being dumb about this somehow.