さとまたwiki

🐧 ラズパイ SNS自動投稿システム — Node.js × cron 実装ガイド

最終更新: 2026-03-31

💀 X API現実直視 — 失神しそうになった話

⚠️ 2026年3月時点のX API料金体系

これが現実だ。目を背けずに見ろ。

Tier月額投稿上限/月読み取り注意点
Free$0500本(約17本/日)❌ 不可書き込みのみ。エンゲージメント確認不可
Basic$200/月(約3万円)3,000本✅ 可旧$100から値上げ済み
Pro$5,000/月300,000本✅ 可個人には非現実的
Pay-per-use従量課金read上限200万/月2026年2月〜。操作ごとに課金

📊 俺たちの投稿量計算

日20本 × 30日 = 600本/月

Freeの上限は500本 → オーバー

→ 日17本以内ならFreeで戦える

→ フル自動ならBasic $200/月

🚫 自動化禁止事項

❌ 自動いいね・リツイート

❌ 自動フォロー・アンフォロー

❌ エンゲージメントファーミング

✅ 自動投稿はOK(bot明記必須)

🔥 戦略的結論

Freeで日17本に抑えてX自動投稿を維持しつつ、Facebook/Instagram/Threadsはコストゼロで全力展開。noteは別途対応。Xは量より質で勝負する。

🏗️ システム全体設計

アーキテクチャ概要

🧠 LLM層

OpenRouter API

claude-3-5-haiku

キャラクター声でコンテンツ生成

⚙️ 実行層

Node.js(常駐プロセス)

node-cron

RaspberryPi 5上で稼働

📡 配信層

X API v2

Meta Graph API

note(Puppeteer)

プロジェクト構成:

raspi-sns-bot/
├── index.js          # メインエントリ(cron登録)
├── config.js         # 環境変数読み込み
├── llm.js            # OpenRouter連携
├── platforms/
│   ├── x.js          # X API v2
│   ├── facebook.js   # Meta Graph API
│   ├── instagram.js  # Meta Graph API(IG)
│   ├── threads.js    # Threads API
│   └── note.js       # Puppeteer
├── prompts/
│   ├── penguin.js    # ペンギンちゃんプロンプト
│   └── sipe.js       # SIPE公式プロンプト
├── logs/             # 投稿ログ
├── .env              # 認証情報(gitignore必須)
└── package.json

package.json の依存関係:

{
  "name": "raspi-sns-bot",
  "version": "1.0.0",
  "type": "module",
  "dependencies": {
    "node-cron": "^3.0.3",
    "axios": "^1.6.0",
    "dotenv": "^16.4.0",
    "twitter-api-v2": "^1.17.0",
    "puppeteer": "^21.0.0"
  }
}

🧠 OpenRouter LLM連携

コンテンツ生成の核心部分。ペンギンちゃんの声でLLMに書かせる。

// llm.js
import axios from 'axios';

export async function generatePost(prompt, platform = 'x') {
  const maxTokens = platform === 'x' ? 150 : 400;

  const response = await axios.post(
    'https://openrouter.ai/api/v1/chat/completions',
    {
      model: 'anthropic/claude-haiku-4-5',
      max_tokens: maxTokens,
      messages: [
        {
          role: 'system',
          content: `あなたはファーストペンギンちゃん。総合心理教育研究所(SIPE)の広報キャラクター。
口調:ゆるくて親しみやすい。でも心理学の知識は本物。
禁止:売り込み・押し付け・長文説明
必須:改行多め・具体的な数字か事例・共感できる日常エピソード`
        },
        {
          role: 'user',
          content: prompt
        }
      ]
    },
    {
      headers: {
        'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
        'Content-Type': 'application/json',
        'HTTP-Referer': 'https://sipe.example.com'
      }
    }
  );

  return response.data.choices[0].message.content.trim();
}

💡 プロンプト戦略

毎回「今日のテーマ」をランダム選択してプロンプトに渡す。同じ投稿が二度出ないようにテーマリストで管理。

⏰ cronスケジュール設計

時刻プラットフォーム本数コンテンツ種別
07:00X / Threads各1本朝の心理豆知識
08:30Facebook1本経営者向け事例
12:00X / Instagram各1本昼休み向けライト心理ネタ
15:00X / Threads各1本午後の気付き投稿
18:00Facebook / Instagram各1本退勤後の共感コンテンツ
21:00X / Threads / note各1本夜の深い考察・SEO記事
// index.js
import cron from 'node-cron';
import {postToX} from './platforms/x.js';
import {postToFacebook} from './platforms/facebook.js';
import {postToInstagram} from './platforms/instagram.js';
import {postToThreads} from './platforms/threads.js';
import {postToNote} from './platforms/note.js';
import {generatePost} from './llm.js';
import {getRandomTheme} from './prompts/penguin.js';

// 07:00 朝の投稿
cron.schedule('0 7 * * *', async () => {
  const theme = getRandomTheme('morning');
  const content = await generatePost(theme, 'x');
  await postToX(content);
  await postToThreads(content);
  console.log('[07:00] X/Threads投稿完了:', new Date().toISOString());
});

// 08:30 Facebook(経営者向け)
cron.schedule('30 8 * * *', async () => {
  const theme = getRandomTheme('business');
  const content = await generatePost(theme, 'facebook');
  await postToFacebook(content);
  console.log('[08:30] Facebook投稿完了');
});

// 12:00 昼投稿
cron.schedule('0 12 * * *', async () => {
  const theme = getRandomTheme('lunch');
  const xContent = await generatePost(theme, 'x');
  const igContent = await generatePost(theme, 'instagram');
  await postToX(xContent);
  await postToInstagram(igContent);
  console.log('[12:00] X/Instagram投稿完了');
});

// 15:00 午後
cron.schedule('0 15 * * *', async () => {
  const theme = getRandomTheme('afternoon');
  const content = await generatePost(theme, 'x');
  await postToX(content);
  await postToThreads(content);
  console.log('[15:00] X/Threads投稿完了');
});

// 18:00 夕方
cron.schedule('0 18 * * *', async () => {
  const theme = getRandomTheme('evening');
  const fbContent = await generatePost(theme, 'facebook');
  const igContent = await generatePost(theme, 'instagram');
  await postToFacebook(fbContent);
  await postToInstagram(igContent);
  console.log('[18:00] Facebook/Instagram投稿完了');
});

// 21:00 夜の深い投稿
cron.schedule('0 21 * * *', async () => {
  const theme = getRandomTheme('night');
  const xContent = await generatePost(theme, 'x');
  const noteContent = await generatePost(theme, 'note');
  await postToX(xContent);
  await postToThreads(xContent);
  await postToNote(noteContent);
  console.log('[21:00] X/Threads/note投稿完了');
});

console.log('🐧 ファーストペンギンちゃん自動投稿システム起動');
console.log('投稿スケジュール: 07:00 / 08:30 / 12:00 / 15:00 / 18:00 / 21:00');

⚔️ X(Twitter)自動投稿

⚠️ Freeプランの制約

1日17本まで。本システムではXへの投稿は1日最大6本に抑えてFreeプランで運用する。

// platforms/x.js
import {TwitterApi} from 'twitter-api-v2';
import fs from 'fs';

const client = new TwitterApi({
  appKey: process.env.X_API_KEY,
  appSecret: process.env.X_API_SECRET,
  accessToken: process.env.X_ACCESS_TOKEN,
  accessSecret: process.env.X_ACCESS_SECRET,
});

export async function postToX(text) {
  try {
    const tweet = await client.v2.tweet(text);
    log('x', text, tweet.data.id);
    return tweet;
  } catch (error) {
    console.error('[X] 投稿失敗:', error.message);
    log('x', text, null, error.message);
    throw error;
  }
}

function log(platform, content, id, error = null) {
  const entry = {
    timestamp: new Date().toISOString(),
    platform,
    content: content.substring(0, 50) + '...',
    id,
    error
  };
  fs.appendFileSync('./logs/posts.jsonl', JSON.stringify(entry) + '\n');
}

🏢 Facebook自動投稿

Meta Graph APIを使用。ページアクセストークンが必要。

// platforms/facebook.js
import axios from 'axios';

const PAGE_ID = process.env.FACEBOOK_PAGE_ID;
const ACCESS_TOKEN = process.env.FACEBOOK_PAGE_TOKEN;
const API_BASE = 'https://graph.facebook.com/v19.0';

export async function postToFacebook(message) {
  try {
    const response = await axios.post(
      `${API_BASE}/${PAGE_ID}/feed`,
      {
        message,
        access_token: ACCESS_TOKEN
      }
    );
    console.log('[Facebook] 投稿完了:', response.data.id);
    return response.data;
  } catch (error) {
    console.error('[Facebook] 投稿失敗:', error.response?.data || error.message);
    throw error;
  }
}

📸 Instagram自動投稿

Instagramはテキストのみ投稿不可。画像URLが必要。カルーセルorリール推奨。テキスト投稿はキャプションとして添付。

// platforms/instagram.js
import axios from 'axios';

const IG_USER_ID = process.env.INSTAGRAM_USER_ID;
const ACCESS_TOKEN = process.env.INSTAGRAM_ACCESS_TOKEN;
const API_BASE = 'https://graph.facebook.com/v19.0';

// Instagramは画像投稿が必須。
// 定型画像(ペンギンちゃんのブランド画像)をベースにキャプションを変える戦略。
const BRAND_IMAGE_URL = process.env.INSTAGRAM_DEFAULT_IMAGE_URL;

export async function postToInstagram(caption) {
  try {
    // Step 1: メディアコンテナ作成
    const container = await axios.post(
      `${API_BASE}/${IG_USER_ID}/media`,
      {
        image_url: BRAND_IMAGE_URL,
        caption,
        access_token: ACCESS_TOKEN
      }
    );
    const containerId = container.data.id;

    // Step 2: 公開
    const publish = await axios.post(
      `${API_BASE}/${IG_USER_ID}/media_publish`,
      {
        creation_id: containerId,
        access_token: ACCESS_TOKEN
      }
    );

    console.log('[Instagram] 投稿完了:', publish.data.id);
    return publish.data;
  } catch (error) {
    console.error('[Instagram] 投稿失敗:', error.response?.data || error.message);
    throw error;
  }
}

🧵 Threads自動投稿

ThreadsはMeta Graph APIのThreads APIエンドポイントを使用。

// platforms/threads.js
import axios from 'axios';

const THREADS_USER_ID = process.env.THREADS_USER_ID;
const ACCESS_TOKEN = process.env.THREADS_ACCESS_TOKEN;
const API_BASE = 'https://graph.threads.net/v1.0';

export async function postToThreads(text) {
  try {
    // Step 1: コンテナ作成
    const container = await axios.post(
      `${API_BASE}/${THREADS_USER_ID}/threads`,
      {
        media_type: 'TEXT',
        text,
        access_token: ACCESS_TOKEN
      }
    );
    const containerId = container.data.id;

    // Step 2: 公開(少し待つ)
    await new Promise(r => setTimeout(r, 2000));

    const publish = await axios.post(
      `${API_BASE}/${THREADS_USER_ID}/threads_publish`,
      {
        creation_id: containerId,
        access_token: ACCESS_TOKEN
      }
    );

    console.log('[Threads] 投稿完了:', publish.data.id);
    return publish.data;
  } catch (error) {
    console.error('[Threads] 投稿失敗:', error.response?.data || error.message);
    throw error;
  }
}

📝 note自動投稿

⚠️ noteはAPIなし

note.comには公式APIが存在しない。Puppeteerでブラウザ操作を自動化する。ログイン状態をCookieで保持。

// platforms/note.js
import puppeteer from 'puppeteer';
import fs from 'fs';

const COOKIE_PATH = './data/note-cookies.json';

export async function postToNote(content) {
  const browser = await puppeteer.launch({
    headless: 'new',
    args: ['--no-sandbox', '--disable-setuid-sandbox'] // ラズパイ用
  });

  const page = await browser.newPage();

  try {
    // Cookie読み込み(ログイン維持)
    if (fs.existsSync(COOKIE_PATH)) {
      const cookies = JSON.parse(fs.readFileSync(COOKIE_PATH, 'utf8'));
      await page.setCookie(...cookies);
    }

    await page.goto('https://note.com/notes/new', {waitUntil: 'networkidle2'});

    // タイトル入力
    await page.click('[placeholder="タイトル"]');
    await page.type('[placeholder="タイトル"]', content.split('\n')[0]);

    // 本文入力
    await page.click('.editor-body');
    await page.type('.editor-body', content);

    // 公開ボタン
    await page.click('[data-type="publish"]');
    await page.waitForSelector('.publish-dialog');
    await page.click('.publish-dialog .submit-button');
    await page.waitForNavigation();

    console.log('[note] 投稿完了:', page.url());
  } finally {
    await browser.close();
  }
}

💡 Cookie保存方法

初回だけ手動でnoteにログインし、Cookie保存スクリプトを実行してから自動化を開始する。

node scripts/save-note-cookies.js

🚀 ラズパイデプロイ・常駐設定

pm2を使ってNode.jsプロセスを常駐させる。再起動後も自動復帰。

# インストール
npm install -g pm2

# 起動
pm2 start index.js --name "penguin-bot"

# 自動起動設定(再起動後も復帰)
pm2 startup
pm2 save

# ログ確認
pm2 logs penguin-bot

# 状態確認
pm2 status

📋 動作確認チェックリスト

  • ✅ .envファイルが正しく読み込まれている
  • ✅ OpenRouter APIキーが有効
  • ✅ X OAuth 1.0aトークンが設定済み
  • ✅ Meta Graph APIトークンが有効(有効期限注意)
  • ✅ pm2が起動している
  • ✅ logs/posts.jsonlに記録が残っている

⚠️ Meta APIトークン期限

Meta Graph APIの短期トークンは60日で失効する。長期トークン(60日)を取得し、カレンダーに更新リマインダーを設定すること。

🔑 環境変数・認証設定

# .env(gitignoreに追加必須!)

# OpenRouter
OPENROUTER_API_KEY=sk-or-...

# X API v2(OAuth 1.0a User Context)
X_API_KEY=...
X_API_SECRET=...
X_ACCESS_TOKEN=...
X_ACCESS_SECRET=...

# Facebook
FACEBOOK_PAGE_ID=...
FACEBOOK_PAGE_TOKEN=...

# Instagram
INSTAGRAM_USER_ID=...
INSTAGRAM_ACCESS_TOKEN=...
INSTAGRAM_DEFAULT_IMAGE_URL=https://...(ペンギンちゃん定型画像)

# Threads
THREADS_USER_ID=...
THREADS_ACCESS_TOKEN=...

🚨 .gitignore必須

.env
logs/
data/note-cookies.json
node_modules/

🛡️ エラーハンドリング

1プラットフォームが失敗しても他は続行する設計。

// エラーが出ても他のプラットフォームへの投稿を止めない
async function safePost(fn, platform) {
  try {
    await fn();
  } catch (error) {
    console.error(`[${platform}] エラー(スキップ):`, error.message);
    // エラーをSlackやDiscordに通知する場合はここに追加
  }
}

// 使用例
cron.schedule('0 7 * * *', async () => {
  const content = await generatePost(getRandomTheme('morning'), 'x');
  await safePost(() => postToX(content), 'X');
  await safePost(() => postToThreads(content), 'Threads');
});

🐧 ファーストペンギンちゃん、自動で毎日飛び込む

一度設定すれば、ラズパイは24時間365日黙って働く。俺たちが寝ている間も、ペンギンちゃんは投稿し続ける。これが攻めの自動化だ。