🐧 ラズパイ SNS自動投稿システム — Node.js × cron 実装ガイド
最終更新: 2026-03-31
💀 X API現実直視 — 失神しそうになった話
⚠️ 2026年3月時点のX API料金体系
これが現実だ。目を背けずに見ろ。
| Tier | 月額 | 投稿上限/月 | 読み取り | 注意点 |
|---|---|---|---|---|
| Free | $0 | 500本(約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:00 | X / Threads | 各1本 | 朝の心理豆知識 |
| 08:30 | 1本 | 経営者向け事例 | |
| 12:00 | X / Instagram | 各1本 | 昼休み向けライト心理ネタ |
| 15:00 | X / Threads | 各1本 | 午後の気付き投稿 |
| 18:00 | Facebook / Instagram | 各1本 | 退勤後の共感コンテンツ |
| 21:00 | X / 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日黙って働く。俺たちが寝ている間も、ペンギンちゃんは投稿し続ける。これが攻めの自動化だ。