Bluesky.bot

Make a bot in TypeScript

This guide writes a bot that posts to Bluesky with @atproto/api, the official TypeScript SDK. It runs with Node.js 20 or later.

1. Create an app password

Sign in to Bluesky as the bot, then open Settings → Privacy and security → App passwords and create one. A bot signs in with an app password, never with the account's main password: you can revoke it at any time without touching the account.

2. Set up the project

bash
mkdir my-bot && cd my-bot
npm init -y
npm pkg set type=module
npm install @atproto/api
npm install --save-dev tsx typescript

Keep the credentials out of the code, in environment variables:

bash
export BLUESKY_HANDLE="weather.bluesky.bot"
export BLUESKY_APP_PASSWORD="xxxx-xxxx-xxxx-xxxx"

3. Write the bot

bot.ts
// bot.ts: post once as the bot. Run with `npx tsx bot.ts`.
import { AtpAgent, RichText } from "@atproto/api";

// The bot's own account, signed in with an app password
// (Settings > Privacy and security > App passwords), never its main password.
const identifier = process.env.BLUESKY_HANDLE ?? "";
const password = process.env.BLUESKY_APP_PASSWORD ?? "";
if (!identifier || !password) {
  throw new Error("Set BLUESKY_HANDLE and BLUESKY_APP_PASSWORD.");
}

const agent = new AtpAgent({ service: "https://bsky.social" });
await agent.login({ identifier, password });

// RichText finds links, mentions and hashtags, so they are clickable.
const text = new RichText({
  text: "Today: sunny, 21°C. Forecast at https://example.com #weather",
});
await text.detectFacets(agent);

const { uri } = await agent.post({
  text: text.text,
  facets: text.facets,
  createdAt: new Date().toISOString(),
});
console.log(`Posted ${uri}`);

What it does:

  • AtpAgent talks to https://bsky.social, the server of accounts hosted by Bluesky. If the bot lives on another server (a self-hosted PDS), use that server's URL.
  • login opens a session with the handle and the app password.
  • RichText and detectFacets turn the link and #weather into clickable facets: a plain text would show them as plain characters.
  • post creates the post and returns its at:// URI.

4. Run it

bash
npx tsx bot.ts

Errors worth handling

  • Rate limits: Bluesky answers 429 when an account or a server does too much. Back off and retry later; see Best practices.
  • Expired credentials: a revoked app password makes login fail. Create a new one.
  • Long posts: a post holds 300 characters (graphemes). Cut longer text, or post a thread.

Next