ボットは、ネットワーク上で自動投稿するアカウントです。直近の地震の規模を投稿するボットや、アーカイブの写真を定期的に投稿するボットなどが人気です。
このチュートリアルでは、アプリパスワードで認証し、ネットワークとやり取りするボットを作成します。
前提条件
このチュートリアルでは、TypeScriptアプリケーションをゼロから構築します。TypeScriptとNode.jsの基本的な理解を前提としています。
以下をインストールしておいてください。
- Node.js 18以上
homebrewに対応したプラットフォームでは、次のコマンドでインストールできます。
brew install node
さらに、lex CLIツールをグローバルにインストールしておく必要があります。
npm install -g @atproto/lex tsx
次に、新しいTypeScriptプロジェクトディレクトリを作成し、プロジェクトを初期化します。
mkdir my-agent
cd my-agent
npm init -y
npm i -D typescript @types/node dotenv cron @atproto/lex @atproto/lex-password-session
npx tsc --init --verbatimModuleSyntax false
既存アカウントから投稿するつもりでなければ、ボット用に新しいアカウントを作成してください。また、そのアカウント用のアプリパスワードも生成してください。
パート1:Lexion
Lexiconはatprotoのレコードを定義します。ボットはapp.bsky.feed.post Lexiconを使ってBluesky投稿を作成します。lexを使えば、このLexiconをダウンロードしてプロジェクトに組み込めます。
lex install app.bsky.feed.post app.bsky.actor.profile
必要であれば、ダウンロードしたLexiconファイルをlib/lexicons/app.bsky.post.jsonで確認できます。ここからlex buildを実行すると、インストール済みLexiconすべてに対してTypeScriptの型を生成できます。
lex build
生成されたコードはsrc/lexiconsに配置されます。これでボットスクリプトを作成できます。
パート2:ボット作成
src/index.tsに新しいファイルを作成してください。このファイルにボットのコードを書きます。
まず必要なimportを追加し、PasswordSession.create()を呼び出してアプリパスワードでボットを認証します。
import { Client } from '@atproto/lex'
import { PasswordSession } from '@atproto/lex-password-session'
import * as dotenv from 'dotenv'
import { CronJob } from 'cron'
import * as process from 'process'
import * as app from './lexicons/app.js'
dotenv.config()
// Create a session
async function login() {
const service = process.env.BOT_PDS!
const identifier = process.env.BOT_HANDLE!
const password = process.env.BOT_PASSWORD!
const session = await PasswordSession.login({
service, // eg 'https://bsky.social'
identifier, // eg 'alice.bsky.social'
password,
})
return session
}
次に、投稿する関数を追加します。認証済みセッションを使って新しいClientを生成し、投稿を作成します。app.bsky.feed.post Lexiconには2つのパラメータが必要です。投稿本文(ここでは"🙂")とタイムスタンプです。
// Posting logic
async function makePost(session: PasswordSession) {
const client = new Client(session)
await client.create(app.bsky.feed.post, {
text: '🙂',
createdAt: new Date().toISOString(),
})
console.log('Just posted!')
}
async function main() {
const session = await login()
await makePost(session)
}
main().catch(console.error)
最後に、cronパッケージを使って、3時間ごとに投稿するコードをmain()に追加します。
async function main() {
const session = await login()
await makePost(session)
// Run this on a cron job
const scheduleExpression = '0 */3 * * *'
const job = new CronJob(scheduleExpression, () => makePost(session))
job.start()
}
これでボットに必要なコードはすべて揃いました。次はボットとしてラベル付けします。
パート3:ボットとしてラベル付け
ベストプラクティスとして、ボットアカウントはプロフィールにセルフラベルを追加して自身を識別するべきです。これにより、ユーザーやモデレーションツールが自動化されたアカウントを認識しやすくなります。src/index.tsに以下の関数を追加してください:
async function labelAsBot(session: PasswordSession) {
const client = new Client(session)
// Get the current profile (if any) to preserve existing fields
let existingProfile = {}
try {
const { data } = await client.get(app.bsky.actor.profile)
existingProfile = data
} catch {
// Profile doesn't exist yet, that's fine
}
// Update profile with bot self-label
await client.put(app.bsky.actor.profile, {
...existingProfile,
labels: {
$type: 'com.atproto.label.defs#selfLabels',
values: [{ val: 'bot' }],
},
})
}
ボットを初めてセットアップする際に、labelAsBot(session)を一度呼び出してください。これは一度だけ行えば十分です。ラベルはプロフィールに永続的に保存されます。cronジョブを開始する前にmain()から呼び出し、初回実行後にその呼び出しを削除できます。
パート4:ボット実行
ボットのホスト(PDS)、ユーザー名、パスワードを.envファイルに保存します。
BOT_PDS= # eg 'https://bsky.social' if you aren't self-hosting your PDS
BOT_HANDLE= # eg 'alice.bsky.social'
BOT_PASSWORD=
これでnpx tsxを使ってボットをテストできます。
npx tsx src/index.ts
このボットは手元のマシンで実行することも、Fly.io のようなプラットフォームにデプロイすることもできます。
ボットはネットワークのレート制限を守る必要がある点に注意してください。例えば、Blueskyには独自のレート制限があります。
パート5:ボットからエージェントへ
ここまでは、一定間隔で投稿するボットを作成してきました。ここからは、他のユーザーとやり取りできる、より高度なエージェントへ拡張していきます。
このセクションでは、Vercel AI SDKを使います。このライブラリは、Vercel AI Gateway経由でAnthropicなどのモデルプロバイダーを統一されたAPIで利用できます。APIキーを取得するには、無料のHobby projectに登録する必要があります。
APIキーを取得したら、プロジェクトにAI SDKを追加します。
npm i ai
次に、.envファイルへ環境変数を1つ追加します。
BOT_PDS=
BOT_HANDLE=
BOT_PASSWORD=
AI_GATEWAY_API_KEY=xxxxxxxxx
最後に、メンション取得のため追加のLexiconをインストールする必要があります。次のコマンドで必要なLexiconのインストールとビルドを行います。
lex install app.bsky.notification.listNotifications app.bsky.feed.getPostThread
lex build --override
あと少しです。src/index.tsにaiのimportを追加します。
import { generateText } from 'ai'
これで、スクリプト内のlogin関数以降を、AI SDKを使ってリプライを生成する新しいロジックに置き換えられます。
async function generateReply(mentionText: string): Promise<string> {
const { text } = await generateText({
model: 'anthropic/claude-sonnet-4-5',
system: 'You are a mystical fortune teller bot on Bluesky named "The Oracle". Speak in a dramatic, mysterious tone with occasional emoji (🔮 ✨ 🌙 ⭐). If someone asks a yes/no question, give a cryptic but leaning answer. If they ask for general guidance, offer a brief horoscope-style prediction. Keep responses under 280 characters. Never break character.',
prompt: mentionText,
})
return text
}
// Check for new mentions and reply
async function checkMentions(session: PasswordSession) {
const client = new Client(session)
const notifications = await client.call(app.bsky.notification.listNotifications, {
limit: 20,
})
for (const notif of notifications.notifications) {
// Only process unread mentions
if (notif.reason !== 'mention' || notif.isRead) continue
// Get the post that mentioned us
const post = await client.call(app.bsky.feed.getPostThread, {
uri: notif.uri,
})
const mentionText = post.thread.post.record.text
const reply = await generateReply(mentionText)
// Reply to the post
await client.create(app.bsky.feed.post, {
text: reply,
createdAt: new Date().toISOString(),
reply: {
root: { uri: notif.uri, cid: post.thread.post.cid },
parent: { uri: notif.uri, cid: post.thread.post.cid },
},
})
console.log(`Replied to mention: ${mentionText}`)
}
}
async function main() {
const session = await login()
const job = new CronJob('*/1 * * * *', () => checkMentions(session))
job.start()
console.log('Agent is running...')
}
main().catch(console.error)
ここではいくつか新しい概念が出てきます。app.bsky.feed.postのロジックは見覚えがあるはずですが、ここではさらにapp.bsky.notification.listNotificationsとapp.bsky.feed.getPostThreadも使用します。前者は通知一覧の取得に使い、後者は指定した投稿URIのスレッド全体を取得するために使います。
もちろん、プロンプトの内容は好きなように変更してください。
ボットを再デプロイすれば、メンションに返信するようになります。
アカウントへ定期的に投稿するボットは、このネットワークでは歓迎されています。ボットが他のユーザーとやり取りすること(いいね、リポスト、リプライなど)は、そのユーザーがボットアカウントをタグ付けしたときだけにしてください。オプトイン型のやり取りであるべきで、そうでない場合はスパムとして報告される可能性があります。
結論
これで、ネットワーク上にレコードを作成する自動化されたatprotoアプリを構築できました。
さらに多くのガイドやチュートリアルはGuidesセクションで、より多くのサンプルアプリはCookbookリポジトリで確認できます。ぜひ開発を楽しんでください。