Creating a bot using the Bluesky API
What is Bluesky
Bluesky is a new, decentralized social app focused on giving users more control over their online experience. Started by Twitterâs ex-CEO Jack Dorsey, Bluesky is built on the AT Protocol, which lets people move between platforms without losing their followers or data. The goal? A more open, customizable social network where you control your feed and data instead of being at the mercy of big tech.
Introduction
Having recently started using Bluesky, I was interested in how good the API is. It turns out its extremely straightforward. So I decided to build a bot and write a short post on it. The project will be a bot which sends a daily skeet (Bluesky tweet đ) which lists which horse racing meetings are on (in UK + IRE), along with their first race time.
Prerequisites
To successfully build this bot, you will need 2 things:
- RapidAPI account (Free) - The API we will be using is https://rapidapi.com/ortegalex/api/horse-racing
- Bluesky Account - I created a new one for this prokject, it is ukracemeetings.bsky.social
Getting Started
So firstly we need to set up a base Node project. Create an empty folder and add an index.ts
. Then run an npm init
to generate a package.json
. There are not many dependencies, so you can install the packages individually or copy/pasta this in, tweak it and run an npm i
.
{
"name": "bluesky-daily-horse-racing-bot",
"version": "1.0.0",
"main": "index.js",
"author": "Craig Jones",
"dependencies": {
"@atproto/api": "^0.13.14",
"@types/lodash": "^4.17.13",
"axios": "^1.7.7",
"cron": "^3.1.8",
"dayjs": "^1.11.13",
"dotenv": "^16.4.5",
"lodash": "^4.17.21",
"process": "^0.11.10"
}
}
You will also need typescript
and ts-node
. These can be installed locally or globally. I opted for the global option based on the Bluesky documentation.
npm i -g typescript ts-node
Next we will need to add a tsconfig.json
. This can be generated using tsc --init
or creating it manually. The contents of this file should look as follows:
{
"compilerOptions": {
"target": "esnext",
"module": "NodeNext",
"moduleResolution": "nodenext",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitAny": false,
"skipLibCheck": true
},
"ts-node": {
"esm": true
}
}
The final thing we need to do is generate a .env
file with our Bluesky credentials and our RapidApi key. It should look like thisâŚ
BLUESKY_USERNAME=**********
BLUESKY_PASSWORD=**********
RAPID_API_KEY=**********
That is most the basic setup out the way. We are now ready to start getting the skeet sent.
Setting up the AtpAgent
So next step is to start populating our index.ts
. I worked from an example on the Bluesky docs. Its really straightforward.
import { BskyAgent } from '@atproto/api';
import * as dotenv from 'dotenv';
import * as process from 'process';
dotenv.config();
// Create a Bluesky Agent
const agent = new BskyAgent({
service: 'https://bsky.social'
});
async function sendSkeet() {
await agent.login({ identifier: process.env.BLUESKY_USERNAME!, password: process.env.BLUESKY_PASSWORD! });
await agent.post({
text: 'đ'
});
console.log('Skeet Sent!');
}
sendSkeet();
This is the minimum code reqired to send a skeet. Now if you compile your code by running npx tsc
, you should see that an index.js
is created. Now if you run the following commandâŚ
node index.js
You will hopefully see the console log âSkeet Sent!â and if you check your Bluesky timeline, you will hopefully see a skeet containing âđâ.
Adding the actual Skeet Content
So in hardly any time, we have got the skeet sending programmatically. So now we just need to build out the actual content. So what I wanted was a list of Horse Racing meetings for the current day, ordered by start time of the first race. So it would look something like thisâŚ
Meeting C 13:45 Meeting A 14:00 Meeting D 14:10 Meeting B 17:15
I am not going to go into too much detail on how I achieved this, more just higher level about getting data from the API and parsing it into my desired format.
First step is to get the data from the APIâŚ
// Get Daily Racecards
const { data } = await axios
.get<Race[]>('https://horse-racing.p.rapidapi.com/racecards', {
params: { date: getTodaysDate() },
headers: {
'x-rapidapi-key': process.env.RAPID_API_KEY,
'x-rapidapi-host': 'horse-racing.p.rapidapi.com'
}
})
.catch((error) => {
console.error(error);
console.log(`Post Failed with Error: ${error}`);
return { data: [] as Race[] };
});
// Get Skeet Text
const skeetText = getMeetingsWithFirstRaceTime(data);
// Send Skeet with text
agent
.post({
text: skeetText
})
.then(() => console.log('Skeet Sent'))
.catch((err) => console.error(err));
The above code code returns an array of the days races. getTodaysDate()
is simply a util which returns the date in the format required by the API - YYYY-MM-DD.
I then created a util called getMeetingsWithFirstRaceTime
. This does all the heavy lifting in terms of parsing, sorting and spitting out the final text which I need for the Skeet. That file looks like thisâŚ
import { Race } from '../types.js';
import _ from 'lodash';
import dayjs from 'dayjs';
export const getMeetingsWithFirstRaceTime = (races: Race[]) => {
return Object.values(_.groupBy(races, 'course'))
.sort((a, b) => dayjs(a[0].date).valueOf() - dayjs(b[0].date).valueOf())
.map(([firstRace]) => {
const date = dayjs(firstRace.date);
return `đ ${firstRace.course} - ${date.format('HH:mm')}`;
})
.join('\r\n');
};
Then thats it really. Run npx tsc
again to recompile the code, then node index.js
and providing everything has been done correctly, you should see the âSkeet Sentâ console log, and when checking your timeline, you should see the skeet as followsâŚ
Next steps
So now we have it sending the correct text via the API, we just need to set it up to do this at a given time every day. 9am feels as good a time as any. So we can again refer to the the Bluesky docs as their example uses a cron job to handle regular posts.
So in the index.ts
we need the following code at the bottomâŚ
// Run this on a cron job
const scheduleExpression = '0 9 * * *'; // Run at 9am every day
const job = new CronJob(scheduleExpression, sendSkeet);
job.start();
This will execute the sendSkeet
function at 9am everyday when the script is running.
And that is that really. You can change the cron schedule expression to something like every minute to test that aspect of it is working.
Hosting
I guess the final piece of the puzzle is where to host the bot. There are several options which have free tiers or small fee hosting. Vultr, DigitalOcean, Linode, Oracle Cloud, Google Cloud Platform, AWS and probably many more. I havenât decided on where to do this yet. I will have a play around and try and get it up and running for free and update the post when this is done.
Conclusion
In just a few steps, weâve built a bot that automatically posts daily horse racing schedules to Bluesky! This project demonstrates how simple it is to interact with Blueskyâs API and bring in external data through a third-party API like RapidAPI. By setting up a daily cron job, we automated the process so the bot posts reliably at 9am every day, delivering timely information without any manual intervention.
This project not only highlights Blueskyâs versatility but also the ease of setting up automated social bots with minimal code. Hopefully, this guide provides a solid starting point for anyone looking to build similar automation on Bluesky! Feel free to experiment with the bot and customize it to post other types of data or messages.