Claude Code に X の DM・メンション監視サーバーを作らせた——X の API には一切触れずに — UnifyPort
X の API に無料プランはない。DM の読み取りは 1 通あたり $0.015、投稿の読み取りは $0.005。2 人のチームで、X 上の DM やメンションを知りたいだけなのに、公式の開発者プラットフォームはクレジットのチャージ、プロジェクトの作成、承認待ち、従量課金の開始を求めてくる——1 通のメッセージも処理していない段階で。
全部スキップした。Claude Code を開き、UnifyPort の webhook リファレンスをコンテキストに貼り付けて、Node.js で X の DM・メンション監視サーバーを作るよう指示した。40 分後、配信ごとの署名検証、構造化ログへの書き込み、Discord チャンネルへのアラート送信をこなす Express サーバーが手元にあった。X の開発者アカウント不要、従量課金なし、承認キューなし。
最終的に手に入るもの
Node.js Express サーバー:
- UnifyPort の統一 webhook から
message.receivedイベントを受信 - HMAC-SHA256 と
signing_secretで各配信の署名を検証 - X のメッセージを
messages.jsonlファイルに書き出し - Discord チャンネルに webhook 経由でアラートを送信
所要時間:1 時間以内。必要なのは、UnifyPort Exporter ブラウザ拡張機能で X アカウントを接続した UnifyPort ワークスペース(ワンクリックのセッションインポート、開発者資格情報不要)と、Discord の webhook URL。
準備:ドキュメントを Claude Code のコンテキストに入れる
プロンプトの前に、API リファレンスをエージェントのコンテキストに入れる。Claude Code はファイルを直接読めるので、プロジェクトルートに unifyport-reference.md を作成し、以下を含める:
message.receivedイベントのペイロード構造x-unifyport-signatureヘッダーと HMAC-SHA256 署名検証の説明POST /v1/webhook-endpointsの作成呼び出し
会話に直接貼り付けても構わない。ポイント:正しいフィールド名を入力すれば、正しいコードが出力される。リファレンスがなければ、エージェントは X 専用の SDK や存在しない dm.received イベントタイプを勝手に作り出す。
Cursor、Windsurf、Copilot でも同じ方法が使える——@Docs、ドキュメントパネル、またはコンテキストへの直接貼り付け。ツールは二次的、ドキュメントが本質。
ビルド:プロンプトごとに
最初のプロンプト——骨格:
unifyport-reference.md を読んでください。Express で server.js を書いて、POST /webhook ルートを作ってください。event が “message.received” のとき、provider、from、text フィールドをログに出力。すべてのリクエストに 200 を返してください。
Claude Code の出力:
import express from "express";
const app = express();
app.use(express.json());
app.post("/webhook", (req, res) => {
const evt = req.body;
if (evt.event === "message.received") {
console.log(`[${evt.provider}] ${evt.from}: ${evt.text}`);
}
res.sendStatus(200);
});
app.listen(3000, () => console.log("listening on :3000"));
ハンドラーはわずか 6 行。6 つのプラットフォームでイベント構造が統一されているため、X 固有のパース処理は不要——evt.provider は "x" だが、構造は WhatsApp や Telegram のメッセージとまったく同じ。
2 つめのプロンプト——署名検証:
各配信には x-unifyport-signature ヘッダーが含まれます。環境変数の signing_secret で HMAC-SHA256 検証を行ってください。HMAC は生のリクエストボディに対して計算すること——再シリアライズした JSON ではなく。失敗したら 401 を返してください。タイミングセーフな比較を使ってください。
Claude Code が検証レイヤーを追加:
import crypto from "crypto";
const SIGNING_SECRET = process.env.UNIFYPORT_SIGNING_SECRET;
app.use(express.json({
verify: (req, _res, buf) => { req.rawBody = buf; }
}));
function isValid(req) {
const sig = req.get("x-unifyport-signature") || "";
const expected = crypto
.createHmac("sha256", SIGNING_SECRET)
.update(req.rawBody)
.digest("hex");
return sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
verify コールバックは Express が JSON をパースする前に生のバッファをキャプチャする——エージェントが req.body を再シリアライズしていたら、HMAC は絶対に一致しない。プロンプトで「生のリクエストボディ」と指定したので、一発で正しく書けた。
3 つめのプロンプト——ログと Discord アラート:
2 つの機能を追加:(1) message.received イベントを messages.jsonl に追記——1 行 1 JSON オブジェクトで timestamp、provider、from、text、message_id を含む。(2) 環境変数 DISCORD_WEBHOOK_URL の Discord webhook に 1 行のアラートを送信。
完成した server.js:
import express from "express";
import crypto from "crypto";
import { appendFileSync } from "fs";
const SIGNING_SECRET = process.env.UNIFYPORT_SIGNING_SECRET;
const DISCORD_WEBHOOK = process.env.DISCORD_WEBHOOK_URL;
const app = express();
app.use(express.json({
verify: (req, _res, buf) => { req.rawBody = buf; }
}));
function isValid(req) {
const sig = req.get("x-unifyport-signature") || "";
const expected = crypto
.createHmac("sha256", SIGNING_SECRET)
.update(req.rawBody)
.digest("hex");
return sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
app.post("/webhook", async (req, res) => {
if (!isValid(req)) return res.sendStatus(401);
const evt = req.body;
if (evt.event === "message.received") {
const record = JSON.stringify({
timestamp: evt.timestamp,
provider: evt.provider,
from: evt.from,
text: evt.text,
message_id: evt.message_id,
});
appendFileSync("messages.jsonl", record + "\n");
if (DISCORD_WEBHOOK) {
await fetch(DISCORD_WEBHOOK, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
content: `**[${evt.provider}]** ${evt.from}: ${evt.text}`,
}),
});
}
}
res.sendStatus(200);
});
app.listen(3000, () => console.log("listening on :3000"));
50 行以下。署名検証、ディスクログ、Discord 転送がすべて揃っている。ハンドラーは provider で分岐しない——イベント構造は同じで、メッセージが X、WhatsApp、どのプラットフォームからでも変わらない。
実行して X のメッセージが届くのを確認
UnifyPort ダッシュボードで X アカウントを接続する。X は UnifyPort Exporter ブラウザ拡張機能でセッションインポートする——拡張機能のアイコンをクリック、認証すれば数秒で接続完了。開発者ポータル不要、API キーの申請不要、承認キュー不要。
サーバーを指す webhook エンドポイントを登録し、subscribed_events: ["message.received"] と signing_secret を設定する。サーバーを起動し、別のアカウントから X アカウントにテスト DM を送る。イベントが届く:
{
"event": "message.received",
"account_id": "acct_7Kp3mR",
"provider": "x",
"from": "user_9d4f2b",
"text": "Hey, are you still taking freelance projects?",
"timestamp": 1750521600,
"message_id": "x_msg_8c1e5a"
}
サーバーが署名を検証し、レコードを messages.jsonl に追記し、Discord にアラートを送信する。署名検証に失敗(401)した場合は、UNIFYPORT_SIGNING_SECRET が webhook エンドポイントに設定した値と一致しているか確認を。
拡張:コード変更ゼロで Telegram を追加
同じワークスペースで Telegram アカウントを接続し、同じ webhook エンドポイントをサブスクライブする。Telegram のメッセージはまったく同じ構造で届く——provider フィールドが違うだけで、構造は同一:
{
"event": "message.received",
"account_id": "acct_2Xn5wL",
"provider": "telegram",
"from": "user_4a7c8d",
"text": "Can you hop on a call tomorrow?",
"timestamp": 1750521660,
"message_id": "tg_msg_3b9f1e"
}
新しいハンドラー不要。Telegram 固有のパース不要。messages.jsonl には X と Telegram の両方のイベントが同じスキーマで入り、Discord には両方のストリームからアラートが表示される。WhatsApp、LINE、Zalo、TikTok の追加もまったく同じ手順——アカウントを接続すれば、既存のハンドラーがすべてを処理する。
これと X の API を直接使うことの違いは、コストだけではない——アクセスそのものだ。X は承認済みの開発者アカウント、資金をチャージしたプロジェクト、従量課金への同意を求める。UnifyPort のセッションインポートはブラウザで個人の X アカウントを接続し、すべての DM とメンションを他の 5 チャンネルとまったく同じイベント構造に正規化する。Claude Code をこのインターフェースに向ければ——イベントスキーマ 1 つ、署名方式 1 つ——ランチまでに検証・ログ・アラート付きのリスナーが動く。そのあと他のすべてのチャンネルを追加しても、ハンドラーには一切触れずに済む。