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:

  1. Go to bsky.app and create a new account
  2. Use a descriptive handle like @yourbot.bsky.social
  3. Fill in the bio explaining what your bot does
  4. Go to Settings → App Passwords → Add App Password
  5. 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:

Terminal
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:

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:

.env
# 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:

src/bot.ts
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:

src/scheduledBot.ts
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:

src/followBack.ts
/**
 * 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:

src/trendingBot.ts (simplified)
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

Download Skyscraper for iOS →