Want to create a Bluesky bot? Bluesky's open ATProtocol makes it easy to build automated posting bots, engagement bots, and utility bots. In this tutorial, we'll walk through building a real-world bot using TypeScript – the same approach we used to build Skyscraper's trending hashtags bot.
What You Can Build with a Bluesky Bot
Bluesky bots can:
- Post automated content – Trending topics, news aggregation, scheduled posts
- Respond to mentions – Customer service, information lookup
- Follow back users – Community building automation
- Track and report data – Analytics, trend tracking
- Moderate content – Labeling, community management
Prerequisites
Before starting, you'll need:
- Node.js 18+ installed
- A Bluesky account for your bot
- Basic TypeScript knowledge
Step 1: Create a Bot Account
First, create a new Bluesky account for your bot:
- Go to bsky.app and create a new account
- Use a descriptive handle like
@yourbot.bsky.social - Fill in the bio explaining what your bot does
- Go to Settings → App Passwords → Add App Password
- Save this app password securely – you'll use it for authentication
Important: Never use your main account password. Always create an App Password specifically for your bot.
Step 2: Set Up Your Project
Create a new Node.js project and install the ATProtocol SDK:
mkdir bluesky-bot
cd bluesky-bot
npm init -y
npm install @atproto/api typescript ts-node dotenv
npm install -D @types/node
Create a tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"outDir": "./dist"
}
}
Step 3: Configure Environment Variables
Create a .env file for your bot credentials:
# Bluesky Bot Credentials
BSKY_BOT_USERNAME=yourbot.bsky.social
BSKY_BOT_PASSWORD=your-app-password-here
Never commit this file to git! Add .env to your .gitignore.
Step 4: Create the Bot
Here's a complete bot that posts content with proper RichText handling for mentions, links, and hashtags:
import { BskyAgent, RichText } from '@atproto/api';
import dotenv from 'dotenv';
dotenv.config();
// Initialize the agent
const agent = new BskyAgent({ service: 'https://bsky.social' });
/**
* Login to Bluesky
*/
async function login(): Promise<void> {
const username = process.env.BSKY_BOT_USERNAME;
const password = process.env.BSKY_BOT_PASSWORD;
if (!username || !password) {
throw new Error('Missing BSKY_BOT_USERNAME or BSKY_BOT_PASSWORD');
}
await agent.login({
identifier: username,
password: password,
});
console.log('Bot logged in successfully');
}
/**
* Post a message with RichText (handles @mentions, #hashtags, and links)
*/
async function postMessage(text: string): Promise<void> {
// RichText automatically detects and creates facets for:
// - @mentions (links to user profiles)
// - #hashtags (searchable tags)
// - URLs (clickable links)
const rt = new RichText({ text });
// detectFacets makes API calls to resolve @mentions to DIDs
await rt.detectFacets(agent);
await agent.post({
text: rt.text,
facets: rt.facets,
});
console.log('Posted:', text);
}
/**
* Main function
*/
async function main() {
await login();
// Post a simple message
await postMessage('Hello from my Bluesky bot! 🤖');
// Post with a mention and hashtag
await postMessage(
'Check out @getskyscraper.com for #Bluesky tools! #BlueskyBot'
);
}
main().catch(console.error);
Step 5: Scheduled Posting
Most bots need to post on a schedule. Here's how we implement scheduled posting in Skyscraper's trending hashtags bot:
import { BskyAgent, RichText } from '@atproto/api';
// Post intervals
const HOURLY_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
const DAILY_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
let agent: BskyAgent | null = null;
let hourlyInterval: NodeJS.Timeout | null = null;
/**
* Start the bot scheduler
*/
export async function startBot(): Promise<void> {
// Login
agent = new BskyAgent({ service: 'https://bsky.social' });
await agent.login({
identifier: process.env.BSKY_BOT_USERNAME!,
password: process.env.BSKY_BOT_PASSWORD!,
});
console.log('Bot logged in successfully');
// Schedule hourly posts
hourlyInterval = setInterval(() => {
postHourlyContent().catch(console.error);
}, HOURLY_INTERVAL_MS);
console.log('Bot scheduler started');
}
/**
* Stop the bot scheduler
*/
export function stopBot(): void {
if (hourlyInterval) {
clearInterval(hourlyInterval);
hourlyInterval = null;
}
agent = null;
console.log('Bot stopped');
}
/**
* Post hourly content
*/
async function postHourlyContent(): Promise<void> {
if (!agent) {
console.warn('Agent not initialized');
return;
}
// Generate your content here
const content = generateHourlyContent();
const rt = new RichText({ text: content });
await rt.detectFacets(agent);
await agent.post({
text: rt.text,
facets: rt.facets,
});
console.log('Posted hourly content');
}
function generateHourlyContent(): string {
const now = new Date();
return `Hourly update at ${now.toISOString()} #BlueskyBot`;
}
Step 6: Follow-Back Feature
Want your bot to automatically follow back users? Here's how:
/**
* Follow back a user who followed the bot
*/
export async function followBackUser(
agent: BskyAgent,
userDid: string
): Promise<void> {
try {
await agent.follow(userDid);
console.log(`Followed back user: ${userDid}`);
} catch (error) {
// User might already be followed
console.debug('Failed to follow back (may already be following)');
}
}
Step 7: Real-World Example – Trending Hashtags Bot
Here's how Skyscraper's trending hashtags bot works. It posts the top 10 trending hashtags every hour:
import { BskyAgent, RichText } from '@atproto/api';
async function postTrendingHashtags(
agent: BskyAgent,
hashtags: string[]
): Promise<void> {
// Format hashtags with # prefix
const hashtagList = hashtags.map((h) => `#${h}`).join(' ');
// Compose the post
const text = `Here are the #Top10 trending hashtags on #Bluesky the past hour:
${hashtagList}
Powered by @getskyscraper.com`;
// Create RichText to properly format mentions and hashtags
const rt = new RichText({ text });
await rt.detectFacets(agent);
// Post to Bluesky
await agent.post({
text: rt.text,
facets: rt.facets,
});
console.log(`Posted ${hashtags.length} trending hashtags`);
}
// Example usage
const trendingHashtags = [
'Bluesky', 'NFL', 'Politics', 'Tech', 'Art',
'Music', 'Gaming', 'News', 'Science', 'Sports'
];
postTrendingHashtags(agent, trendingHashtags);
Best Practices for Bluesky Bots
1. Use App Passwords
Never use your main account password. App passwords can be revoked without affecting your account.
2. Respect Rate Limits
Bluesky has rate limits. Don't post too frequently. The typical limit is around 1,500-3,000 points per 5 minutes depending on the endpoint.
3. Use RichText for Formatting
Always use RichText and detectFacets() to properly format mentions, hashtags, and links. This creates the proper facets that make them clickable.
4. Handle Errors Gracefully
Network issues happen. Wrap your API calls in try/catch and implement retry logic.
5. Log Everything
Good logging helps debug issues when your bot runs unattended.
6. Identify Your Bot
Make it clear in your bot's bio that it's automated. Transparency builds trust.
Deploying Your Bot
For production deployment, consider:
- Railway – Easy Node.js deployment with environment variable support
- Render – Free tier available for simple bots
- DigitalOcean – Droplets for more control
- AWS Lambda – For serverless scheduled execution
See our Railway deployment guide for detailed instructions.
API Reference
Key methods from @atproto/api:
| Method | Description |
|---|---|
agent.login() |
Authenticate with Bluesky |
agent.post() |
Create a new post |
agent.follow(did) |
Follow a user by DID |
agent.like(uri, cid) |
Like a post |
agent.repost(uri, cid) |
Repost a post |
RichText.detectFacets() |
Parse mentions, links, hashtags |
Next Steps
- Read the ATProtocol Getting Started Guide
- Learn about Bluesky Facets for Rich Text
- Understand Bluesky Rate Limits
- See Skyscraper's Trending Hashtags (powered by a bot!)