← 全記事
チュートリアル

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 サーバー:

  1. UnifyPort の統一 webhook から message.received イベントを受信
  2. HMAC-SHA256 と signing_secret で各配信の署名を検証
  3. X のメッセージを messages.jsonl ファイルに書き出し
  4. 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 つ——ランチまでに検証・ログ・アラート付きのリスナーが動く。そのあと他のすべてのチャンネルを追加しても、ハンドラーには一切触れずに済む。