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
- Node.js and npm installed
- Your favorite code editor
- A Bluesky account for your bot (we’ll need the user credentials)
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:
- Is defined as a procedure that recieves and resolves objects with the encoding type
application/json
. - The input object has two required fields:
identifier
andpassword
. - The output object has multiple fields, but the most important for us are
did
andaccessJwt
.
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:
- The input and outputs are JSON objects too.
- The input object has three required fields:
repo
,collection
, andrecord
. - This input fields has his own definitions:
- The
repo
field is the user’s DID. - The
collection
field is the NSID of the collection where we want to add the record. - The
record
field is the message to publish, and requires a$type
field with the collection.
- The
- The output object has multiple fields, but we don’t need them for this tutorial.
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:
$type
: The type of the record, in this case,app.bsky.feed.post
.text
: The text of the message.createdAt
: The date and time when the message was created.
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:
- We are using the NSID of the Lexicon
com.atproto.repo.createRecord
. - We are using the user’s DID as the
repo
field. - We are using the collection NSID
app.bsky.feed.post
. - We are using a record with the type of the collection and other fields like
text
andcreatedAt
. - We are formatting the
createdAt
field to follow the Lexicon definition.
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! 🌟