エージェント作成

Blueskyに自動投稿するボットの作成

ボットは、ネットワーク上で自動投稿するアカウントです。直近の地震の規模を投稿するボットや、アーカイブの写真を定期的に投稿するボットなどが人気です。

このチュートリアルでは、アプリパスワードで認証し、ネットワークとやり取りするボットを作成します。

前提条件

このチュートリアルでは、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.tsaiの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.listNotificationsapp.bsky.feed.getPostThreadも使用します。前者は通知一覧の取得に使い、後者は指定した投稿URIのスレッド全体を取得するために使います。

もちろん、プロンプトの内容は好きなように変更してください。

ボットを再デプロイすれば、メンションに返信するようになります。

結論

これで、ネットワーク上にレコードを作成する自動化されたatprotoアプリを構築できました。

さらに多くのガイドやチュートリアルはGuidesセクションで、より多くのサンプルアプリはCookbookリポジトリで確認できます。ぜひ開発を楽しんでください。