The return of Stealth Mountain

Published on , 1917 words, 7 minutes to read

It's more than a sneaky peak, it's a legend reborn.

An image of A picture of the Los Angeles foothills near the airport at sunset. The sky is bathed in gold with the mountains a calming purple.
A picture of the Los Angeles foothills near the airport at sunset. The sky is bathed in gold with the mountains a calming purple. - iPhone 13 Pro, Photo by Xe Iaso

Way back in the Before Times™️, there was a legendary Twitter bot named Stealth Mountain. It is incredibly banned now, but it was a brilliant example of what you can do when you have a terrible idea, access to the Twitter firehose, and enough will to act on it. Whenever someone made a tweet with "sneak peak" in it, the bot kicked into high gear spelling pedantry. For example, if someone sent a post like this:

The bot would instantly fire back with this:

I think you mean "sneak peek"

— Stealth Mountain (

@stealthmountain.xeiaso.net

) October 25, 2024 at 1:40 PM

That's it. That's the whole bot.

I miss the era of Internet that lead to people creating works of public art like this. I want to bring some of that magic back. I've created a revival of this bot on Bluesky. It runs in a microservices architecture on my homelab and uses NATS.

Cadey is enby
<Cadey>

If you want to see bad ideas like these get implemented live on stream, I stream every Friday at Noon Eastern time on twitch.tv/princessxen. This was implemented live on stream.

What makes the bot go

Bluesky is a social media platform like Twitter/X, but it's way more open than Twitter ever was. Among other things, it has user-created algorithmic feeds, a developer API open to everyone, everyone is a website mentioning other websites, and the most important part to this project is that everyone has access to the entire dataset that makes up the network. Here's my entire Bluesky repo: @xeiaso.net. You can see every post, every follow, every like, and other things I've put into Bluesky.

This includes access to the Firehose, the entire unfiltered stream of public events in the network. This gives you enough information to automatically label profiles or posts based on their content.

However the Firehose is kind of annoying to deal with. Everything is sent as a bunch of CBOR objects, which are not as developer-ergonomic as JSON is. As such, Jaz invented Jetstream, which gets you access to the same data you get from the Firehose, but over JSON instead. Jaz also shipped a low-level Go package that handles a lot of the boilerplate for you.

At a high level, Stealth Mountain needs to just subscribe to Jetstream, filter out posts it wants, and then react to them, right? Well, yes, but this doesn't scale well if you're like me and want to do multiple projects with this data.

That's where NATS comes in. NATS is a pub-sub message broker. You publish messages to a subject and then clients can subscribe to messages on that subject. If there's nobody listening, the message just gets dropped by the broker. Bluesky has a bunch of events, and statistically very few of them are relevant to Stealth Mountain.

So I made a service named amano that subscribes to Jetstream and then fans each kind of Bluesky message out to different subjects. It currently breaks things out like this:

The majority of things in Bluesky fall into that last bit: commits to user repositories. This is where all your posts and likes are. So if you fan things out like that, then Stealth Mountain can only subscribe to new posts, and all of the rest of the data is broken out into its own subjects so that other things in my homelab can make use of the data.

A terrible MS Paint diagram of Stealth Mountain's architecture. Skeets come in through amano and NATS, Stealth Mountain does its logic, and then sends a reply if it needs to.
A terrible MS Paint diagram of Stealth Mountain's architecture. Skeets come in through amano and NATS, Stealth Mountain does its logic, and then sends a reply if it needs to.

This lets me take advantage of the strengths of microservices approaches while also minimizing (or at least acknowledging) a lot of the downsides. I don't expect perfect 100% delivery of every message from Jetstream into my NATS broker, but for what I'm doing this should be way more than enough.

Implementation is left trivial

As a result of all of this, the implementation in Stealth Mountain is trivial. It mainly boils down to "for each post, if it contains sneak peak, reply to it". The harder bit to implement was replying to posts and attaching them to the post that is being replied to, but even then that was made trivial after reading some example code and making an extra method on a post builder to handle this for me.

One of the last bits that I had to think about is how users should be able to opt-out of the bot. Currently, the way you opt-out is by blocking the bot. That will make any replies from the bot just get ignored. I don't have logic for detecting when the bot is blocked, but because each block is also put into a NATS subject, it should be fairly trivial to implement in the future.

For now though, I decided to add a note about blocking the bot to make it go away to both its pinned post and profile:

Hi all! I'm Stealth Mountain, run by @xeiaso.net. If you typo "sneak peek", I will show up to be polite.



To opt-out, block me.

— Stealth Mountain ( @stealthmountain.xeiaso.net ) October 25, 2024 at 2:26 PM

I think this should be more than good enough for now. I have yet to hear from the Bluesky team if this is sufficient, but I suspect that it's probably fine.

Problems I had along the way

I didn't really have much of any problems outside a few snafus involving the re-authentication logic. When you authenticate to Bluesky, it gives you an access token (good for about half an hour) and a refresh token (good for about a month or two). The thing I messed up is that you need to replace the access token in memory with the refresh token before calling the refresh token API endpoint. The logic you want kinda looks like this:

var client *xrpc.Client = magic!()
client.Auth.AccessJwt = client.Auth.RefreshJwt
resp, err := atproto.ServerRefreshSession(ctx, client)
if err != nil {
	return err
}
// replace contents of client.Auth from what you were given with resp

I also failed to deploy the bot correctly on stream because I had the wrong secret get loaded from 1Password. Turns out that if you don't put the authentication credentials correctly into the bot at runtime, it won't work. Shocker!


Either way, this was a fun bot to make. It's remarkably effective and amusing to check in on. Even though it's got a fairly complicated setup involving multiple layers of microservices, it's able to respond basically instantly. I've observed it reacting faster than 250 milliseconds from post to response.

What's great is that people love the return of the bot. I'm going to end this article with some of my favorite responses. Hope this was interesting! Let me know what other fun hacks you want to see on top of ATProto.

This bot is freaking hilarious i love that

— Kiwi 🍄‍🟫 Streamer! (

@kyisakiwi.bsky.social

) October 29, 2024 at 11:37 AM

Nope, we are taking a page out of your book and sneaking in a mountain top. That’s my story and I’m sticking to it.

— Roi Fainéant Press ( @rfpress.bsky.social ) October 28, 2024 at 9:13 AM

my favorite bot has arrived! missed this lil pedant.



[image or embed]

— Ponderosa | Kim Kuzuri ( @ponderosa121.bsky.social ) October 28, 2024 at 12:29 AM

not u frame 1 pointing out my minor spelling mistake 😭😭😭 IM SO COOKED

— Minty Yukime 🍬✨ ENVtuber!【oracLive】 ( @mintyyukime.bsky.social ) October 27, 2024 at 3:08 PM

Oh yea 😭. I guess I said it wrong my whole life 😅😅. But it is also peak what is gonna drop 😼. So it stays sneak peak from now on 😼

— Dokkan Legoman ( @dokkanlegoman.bsky.social ) October 27, 2024 at 9:20 AM

I posted this 1 second ago how did you get here so fast

— Moog ( @megamooga.bsky.social ) October 26, 2024 at 12:31 AM


Facts and circumstances may have changed since publication. Please contact me before jumping to conclusions if something seems wrong or unclear.

Tags: