How to Build Your First Bluesky Bot

· 15 minutes · Tutorial

Bluesky is the new twitter-like social network, and it’s gaining popularity every day.
The best part is that Bluesky is an open network, that allows developers to build their own things on top of it.

As you know, recently I’m studing a lot about Bluesky and its protocol, the AT protocol.
So, I thought it would be a great idea to build a Bluesky bot using all this concepts.

Prerequisites

Introduction to the AT protocol

Bluesky is an open source social network, build on top of the AT protocol.
The AT protocol is a simple protocol based on HTTP and DNS, that allows us to build open, public, and decentralized social networks.

The team behind AT Protocol use the term “ATmosphere” to refer to the ecosystem of AT-based social networks.
And Bluesky is one of the most popular ones in the ATmosphere.

Some concepts before we start

AT Proto defines some concepts that we need to understand before we start building our bot:

DID

The DID or Decentralized Identifier is a unique identifier in the ATmosphere.
Every user has his own DID.

Handle

The handle is a human-readable identifier for a user in the ATmosphere.
In other words is an alias for the user’s DID.

PDS

The PDS or Personal Data Server is the server that hosts the user and the user’s data.
All this user data and other data in the PDS is called the data repo, or just repo.
The Bluesky sever is an example of a PDS.

Lexicon

The Lexicons are schemas that define the structure of the data records stored in the PDS.
In other words, Lexicons defines de structure of the posts, messages, user data, etc.

NSID

The NSID or Namespace Identifier is a unique identifier for a Lexicon.
And is written in the reverse DNS format.

For example, the “createSession” Lexicon has the NSID:

com.atproto.server.createSession

And the “createRecord” Lexicon has the NSID:

com.atproto.repo.createRecord

Record

The records are the data that we share in the ATmosphere, like messages, posts, etc.
In a more technical way, a record is a JSON document that follows a Lexicon definition.
The type of the record is defined by the $type field in the record.

Collection

A collection is a group of records that follow the same Lexicon.
Every collection is identified by an NSID.

For example, the collection of publications has the NSID:

app.bsky.feed.post

To learn more about this concepts, you can read the AT Proto Glossary.

How to use the Lexicons to navigate the ATmosphere

Understanding the Lexicons is the key to interact with the ATmosphere.
You can use the Lexicons to login, publish messages, follow users, etc.

In this tutorial, we’ll use three Lexicons:

The “createSession” Lexicon

This Lexicon is used to login to Bluesky.
It has the NSID:

com.atproto.server.createSession

If you read the Lexicon definition, you can see that it has the next parts:

See the full definition of this Lexicon here.

The “createRecord” Lexicon

This Lexicon is used to publish a data record to Bluesky.
It has the NSID:

com.atproto.repo.createRecord

If you read the Lexicon definition, you can see that it has the next parts:

In other words, this Lexicon allows us to publish a post in the posts collection of the user.

See the full definition of this Lexicon here.

The Posts Collection

This is the collection where we can publish posts in Bluesky.
It has the NSID:

app.bsky.feed.post

And the record for this collection has the next fields:

See the full definition of this Lexicon here.

You can see all the Lexicons used by Bluesky in the AT Protocol Reference Implementation.

As you can see, the Lexicons tell us everything we need to know to interact with the ATmosphere.

So, now that we understand the main concepts and the Lexicons used in this tutorial, let’s start building our bot (finally).

Setup the project

Let’s create the project using Node.js and TypeScript.

I’ve created a folder called bluesky-bot for this project, so I’m running the next commands inside this.

First, go to your work directory and create your project using:

npm init -y

Now, let’s install the TypeScript dependencies:

npm install --save-dev typescript ts-node

And create a tsconfig.json file with the TypeScript configuration:

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "CommonJS",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "dist"
  }
}

Now, let’s install the node-fetch package to make HTTP requests:

npm install node-fetch

This package is a lightweight module that brings the fetch API to Node.js.

The next step is to create the main file for our bot.
I’ve created a file called index.ts in the src folder.

The project structure looks like this:

bluesky-bot
├── node_modules
├── package-lock.json
├── package.json
├── tsconfig.json
└── src
    └── index.ts

Now you can add the start script to the package.json file to run the bot using TypeScript:

{
  "scripts": {
    "start": "ts-node src/index.ts"
  }
}

This is how the final package.json file looks like:

{
  "name": "bluesky-bot",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "ts-node src/index.ts"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "ts-node": "^10.4.0",
    "typescript": "^4.5.4"
  },
  "dependencies": {
    "node-fetch": "^3.1.0"
  }
}

Now that we have the project set up, let’s start coding the bot.

Login to Bluesky

You can interact with the Bluesky PDS using API calls in under the /xrpc endpoint.

So the base url for the API is:

https://bsky.social/xrpc

Fortunately, Bluesky has a simple login system that allows us to authenticate our bot using the user credentials.

This is a simple example of how to login to Bluesky using the createSession Lexicon:

import fetch from 'node-fetch'

interface LoginResponse {
  did: string
  accessJwt: string
}

export default async function login(identifier: string, password: string): Promise<LoginResponse> {
  const response = await fetch('https://bsky.social/xrpc/com.atproto.server.createSession', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      identifier,
      password,
    })
  })
  return await response.json()
}

I’ve created this in a file under src/api/login.ts

As you can see, the login function follows the Lexicon definition, receives the user’s identifier (handle) and password, and returns the user’s DID and the access JWT.

Note how we are building the API endpoint for the request using the base URL and the NSID of our Lexicon.

https://bsky.social/xrpc/com.atproto.server.createSession

And how we are making the request using the Lexicon definition.

Now you can import this function in your main file and use it to login to Bluesky.

// src/index.ts
import login from './api/login'

async function main() {
  const { did, accessJwt } = await login('your-handle', 'your-password')
  console.log('Logged in', did, accessJwt)
}

main()

Run the bot using:

npm start

And you should see the user’s DID and access JWT in the console.

Congratulations!
You have successfully logged in to Bluesky. 🎉

Publish a message

Now that we have a token, we can use it to publish a message.

This is the function:

import fetch from 'node-fetch'

interface PostResponse {
  cid: string
}

export default async function publish(did: string, token: string, text: string): Promise<PostResponse> {
  const collection = 'app.bsky.feed.post'
  const createdAt = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z')
  const response = await fetch('https://bsky.social/xrpc/com.atproto.repo.createRecord', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`
    },
    body: JSON.stringify({
      repo: did,
      collection,
      record: {
        $type: collection,
        text,
        createdAt
      }
    })
  })
  return await response.json()
}

I’ve created this in a file under src/api/publish.ts.

Note how we are using all our concepts here:

This is an example of how use the Bluesky API is more about understanding the concepts than the code itself.

You just need to know how to use the Lexicons to interact with any PDS in the ATmosphere.

Use this function in your main file to publish a message to Bluesky:

// src/index.ts
import login from './api/login'
import publish from './api/publish'

async function main() {
  const { did, accessJwt } = await login('your-handle', 'your-password')
  console.log('Logged in', did, accessJwt)

  await publish(did, accessJwt, 'Hello Bluesky!')
  console.log('Published')
}

main()

Run the bot using:

npm start

And that’s it!
You have your first Bluesky bot up and running. 🚀

Final thoughts

In this tutorial, we have learned how to build a Bluesky bot using the AT protocol.
We have learned the concepts behind the ATmosphere and how to use the Lexicons to interact with the Bluesky PDS.

But most important, we have now the fundamentals to build any thing we want using the AT protocol.

I hope you enjoyed this tutorial and that you can build amazing things.

I built my own bot using this knowledge and some Cloudflare Workers magic.
You can try it out, is named Year Progress Bot, and you can follow it on Bluesky.

If you liked this tutorial, consider sharing it with your friends and followers.

You can also follow me on Bluesky for more tutorials and tips.

Happy coding! 🌟