mcpField
N
nansen-alerts-webhook-listener

nansen-alerts-webhook-listener

github.com/nansen-ai開発144165 回インストール今日更新
リポジトリ

nansen-alerts-webhook-listener について

nansen-alerts-webhook-listenerは、NansenのスマートアラートWebhookを受信するローカルサーバーを設定するスキルです。署名を検証し、検証済みの通知をOpenClawへ転送できますが、アラートの作成と変更は手動です。

主な機能

  1. 01ローカルNode.js HTTPリスナーの作成
  2. 02HMAC-SHA256署名の検証
  3. 03ngrokまたはlocaltunnelで公開
  4. 04検証済みペイロードをOpenClawへ転送
  5. 05リクエスト方式と本文サイズの制限

活用シーン

  1. 01NansenスマートアラートWebhookのローカル受信
  2. 02検証済み通知のOpenClaw Gatewayへの転送
  3. 03アラート設定時のWebhookペイロード確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/nansen-ai/nansen-cli --skill nansen-alerts-webhook-listener

インストール後、Claude Code で/nansen-alerts-webhook-listenerを入力して読み込まれたか確認します。

詳細

認証

Nansen アカウントの API 呼び出しでは、選択した nansen:api ブラウザーセッション、または同じ権限を持つ従来の API キーを使用できます。NANSEN_API_KEY が優先されます。任意の primaryEnv を指定すると、設定済みキーの注入を維持できます。オフラインで選択するには、nansen auth status を実行してください。キャッシュされたアクセス権の有効期限が切れただけの場合は、認可されたタスク中に自動更新できます。匿名の選択、無効な状態、更新のブロックまたは不確実な状態、実際の認証失敗が発生した場合は停止してください。認証情報を破棄したり、匿名の x402 決済にフォールバックしたりしないでください。ブラウザーでログインしても、ウォレットへの署名、特権サービスの ID、永続的な MCP 統合キーは付与されません。以下の確認、署名、制裁対象および地理的なチェックをすべて維持してください。ブラウザーログインは、macOS arm64 のプレビュー対象範囲内で CLI 2.0.0 から利用できます。

Alert Webhook Listener

Nansen のスマートアラートのペイロードをリアルタイムで受信するローカル HTTP サーバーをセットアップします。

仕組み

Nansen のスマートアラートは、チャネルタイプとしてWebhookに対応しています。アラートが発火すると、Nansen は JSON ペイロードを含む HTTP POST を Webhook URL に送信します。このスキルでは、以下をセットアップします。

  1. アラートのペイロードを受信して表示するローカル HTTP サーバー(Node.js、外部依存関係なし)
  2. 本物の Nansen ペイロードだけを受け付ける HMAC-SHA256 署名検証
  3. Nansen のサーバーがローカルマシンにアクセスできるようにするパブリックトンネル

**このスキルはアラートを作成または変更しません。**リスナーのインフラストラクチャをセットアップし、アラートの受信を開始するためにユーザーが行う必要のある操作の概要を提示します。

OpenClaw ユーザー:OpenClaw が同じマシン上でローカルに実行されている場合、Webhook サーバーは検証済みのアラートペイロードを OpenClaw の Gateway(/hooks/agent)に転送し、アラートごとにエージェントの処理を開始できます。有効にするには、OPENCLAW_GATEWAY_URL 環境変数を設定してください。以下のOpenClaw との統合セクションを参照してください。

セキュリティに関する警告

続行する前に、ユーザーに次の内容を伝えてください。

このスキルはマシン上で HTTP サーバーを起動し、トンネル(ngrok または localtunnel)を介してインターネットに公開します。サーバーは localhost(127.0.0.1)にのみバインドされるため、ローカルネットワーク上の他のユーザーが直接アクセスすることはできませんが、トンネルによって、インターネット上の誰もがリクエストを送信できるパブリック URL が作成されます。

実施済みの緩和策:

  • HMAC-SHA256 署名検証により、Nansen の署名がないリクエストをすべて拒否します
  • 1 MB の本文サイズ制限により、メモリの悪用を防ぎます
  • POST /webhook と GET /health のみを受け付け、それ以外はすべて 404 を返します

次の点にご注意ください:

  • トンネル URL は公開されており、発見される可能性があります(ngrok の URL は列挙される可能性があります)
  • 署名のないリクエストもマシンに届きます。拒否されますが、接続は確立されます
  • 作業が終わったらトンネルを停止し、パブリックエンドポイントを閉じてください

続行する前に、ユーザーが作業を進めることを確認するまで待ってください。

実行計画

以下の手順を順番どおりに実行してください。署名検証は必須のため、省略しないでください。

ステップ 0:トンネルプロバイダーを選択する

開始する前に、使用するトンネルプロバイダーをユーザーに確認します。

ngrok(推奨)localtunnel
安定性安定 — キープアライブによる永続的な接続不安定 — 無料のリレーはアイドル状態の接続を予告なく切断し、トンネルがランダムに停止します
インストールbrew install ngrok + ngrok.com で無料アカウントを作成インストール不要(npx localtunnel)
HTTPSはいはい
認証が必要はい(ngrok.com で取得できる無料の authtoken)いいえ

ngrok を推奨してください。 localtunnel は便利ですが、信頼性に欠けます。テストでは、数分後にトンネルが予告なく終了し、アラートが「503 Tunnel Unavailable」で失敗しました。ngrok は安定した接続を維持します。

ngrok が利用可能か確認します。

which ngrok && ngrok version

インストールされていない場合は、ユーザーに次の手順を伝えます。

  1. brew install ngrok(または ngrok.com からダウンロード)
  2. ngrok.com で無料アカウントを作成し、authtoken をコピー
  3. ngrok config add-authtoken <token>

ユーザーが localtunnel を希望する場合、または ngrok をインストールできない場合は、localtunnel を使用します。ただし、トンネルが切断される可能性があり、その場合は再起動してアラートの webhook URL を更新する必要があることを警告してください。

ステップ 1:Webhook シークレットを生成する
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

出力を保存します。サーバーとアラートの設定の両方で必要です。この時点以降、シークレットをログに記録したり、表示したりしないでください。

ステップ 2:Webhook 受信スクリプトを作成する

現在の作業ディレクトリに nansen-webhook-server.mjs を作成します。Node.js の組み込みモジュール(node:http、node:crypto)のみを使用します。npm install は不要です。

要件 — 必ず遵守してください。

要件詳細
バインドアドレス127.0.0.1 のみ — 0.0.0.0 は絶対に使用しないでください
デフォルトポート9477(PORT 環境変数で上書き可能)
Webhook パスPOST /webhook — それ以外のメソッドとパスの組み合わせはすべて 404 で拒否
ヘルスチェックGET /health → 200 {"status":"ok"}
署名の検証x-nansen-signature ヘッダーを HMAC-SHA256 とタイミングセーフな比較で検証します。不一致の場合は 401 で拒否します。
シークレットの検証WEBHOOK_SECRET 環境変数がない場合、または 16 文字未満の場合は、起動時に終了します。
ペイロードのログ記録有効な JSON ペイロードを ISO タイムスタンプ付きで整形して標準出力に出力します。
リクエストサイズ制限メモリの過剰使用を防ぐため、1 MB を超えるボディを拒否します(413)。
グレースフルシャットダウンSIGINT と SIGTERM を処理します — サーバーを閉じてから終了します。
OpenClaw への転送OPENCLAW_GATEWAY_URL 環境変数が設定されている場合、検証済みのペイロードを <url>/hooks/agent に POST で転送します。設定されている場合は OPENCLAW_AUTH_TOKEN を Bearer トークンとして含めます。転送の成功または失敗をログに記録します。
依存関係なしnode:http、node:https、node:crypto のみ — npm のパッケージは使用しません。

署名の検証 — タイミングセーフな比較を使用してください。

import { createHmac, timingSafeEqual } from 'node:crypto';

function verifySignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false;
  // Nansen sends "sha256=<hex>" — strip the prefix before comparing
  const sig = signatureHeader.startsWith('sha256=') ? signatureHeader.slice(7) : signatureHeader;
  const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
  try {
    return timingSafeEqual(Buffer.from(sig, 'utf8'), Buffer.from(expected, 'utf8'));
  } catch {
    return false; // length mismatch
  }
}

サーバーの完全なテンプレート:

import { createServer } from 'node:http';
import { createHmac, timingSafeEqual } from 'node:crypto';

const PORT = parseInt(process.env.PORT || '9477', 10);
const SECRET = process.env.WEBHOOK_SECRET;
const MAX_BODY = 1_048_576; // 1 MB

// Optional: forward verified payloads to a local OpenClaw Gateway
const OPENCLAW_URL = process.env.OPENCLAW_GATEWAY_URL; // e.g. http://localhost:3000
const OPENCLAW_TOKEN = process.env.OPENCLAW_AUTH_TOKEN;

if (!SECRET || SECRET.length < 16) {
  console.error('WEBHOOK_SECRET env var required (minimum 16 characters).');
  console.error('Generate one: node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"');
  process.exit(1);
}

function verifySignature(rawBody, signatureHeader) {
  if (!signatureHeader) return false;
  // Nansen sends "sha256=<hex>" — strip the prefix before comparing
  const sig = signatureHeader.startsWith('sha256=') ? signatureHeader.slice(7) : signatureHeader;
  const expected = createHmac('sha256', SECRET).update(rawBody).digest('hex');
  try {
    return timingSafeEqual(Buffer.from(sig, 'utf8'), Buffer.from(expected, 'utf8'));
  } catch {
    return false;
  }
}

async function forwardToOpenClaw(payload) {
  if (!OPENCLAW_URL) return;
  const url = `${OPENCLAW_URL.replace(/\/+$/, '')}/hooks/agent`;
  const headers = { 'Content-Type': 'application/json' };
  if (OPENCLAW_TOKEN) headers['Authorization'] = `Bearer ${OPENCLAW_TOKEN}`;
  try {
    const res = await fetch(url, {
      method: 'POST',
      headers,
      body: JSON.stringify(payload),
    });
    if (res.ok) {
      console.log(`[${ts()}] Forwarded to OpenClaw (${res.status})`);
    } else {
      console.error(`[${ts()}] OpenClaw forward failed (${res.status})`);
    }
  } catch (err) {
    console.error(`[${ts()}] OpenClaw forward error: ${err.message}`);
  }
}

function ts() { return new Date().toISOString(); }

const server = createServer((req, res) => {
  if (req.method === 'GET' && req.url === '/health') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    return res.end('{"status":"ok"}');
  }

  if (req.method !== 'POST' || req.url !== '/webhook') {
    res.writeHead(404);
    return res.end();
  }

  let size = 0;
  const chunks = [];

  req.on('data', (chunk) => {
    size += chunk.length;
    if (size > MAX_BODY) {
      res.writeHead(413);
      res.end('{"error":"Payload too large"}');
      req.destroy();
      return;
    }
    chunks.push(chunk);
  });

  req.on('end', () => {
    if (res.writableEnded) return;

    const rawBody = Buffer.concat(chunks).toString('utf8');
    const signature = req.headers['x-nansen-signature'];

    if (!verifySignature(rawBody, signature)) {
      console.error(`[${ts()}] REJECTED — invalid signature`);
      res.writeHead(401, { 'Content-Type': 'application/json' });
      return res.end('{"error":"Invalid signature"}');
    }

    let payload;
    try {
      payload = JSON.parse(rawBody);
      console.log(`\n[${ts()}] Alert received:`);
      console.log(JSON.stringify(payload, null, 2));
    } catch {
      console.error(`[${ts()}] WARNING — valid signature but malformed JSON`);
    }

    // Forward to OpenClaw if configured (fire-and-forget — don't block response)
    if (payload) forwardToOpenClaw(payload);

    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end('{"received":true}');
  });
});

for (const sig of ['SIGINT', 'SIGTERM']) {
  process.on(sig, () => {
    console.log(`\n${sig} — shutting down`);
    server.close(() => process.exit(0));
  });
}

server.listen(PORT, '127.0.0.1', () => {
  console.log(`Webhook listener ready — http://127.0.0.1:${PORT}/webhook`);
  if (OPENCLAW_URL) console.log(`OpenClaw forwarding → ${OPENCLAW_URL}/hooks/agent`);
  console.log('Waiting for alerts… (Ctrl+C to stop)\n');
});
ステップ 3:サーバーとトンネルを起動する

サーバーを起動します。

WEBHOOK_SECRET='<secret>' node nansen-webhook-server.mjs

次に、Nansen のサーバーからアクセスできるよう、公開トンネルを起動します。

ngrok(推奨):

ngrok http 9477

ngrok の出力またはローカル API から公開 URL を取得します。

curl -s http://127.0.0.1:4040/api/tunnels | node -e "process.stdin.on('data',d=>console.log(JSON.parse(d).tunnels[0]?.public_url))"

Webhook URL は https://<subdomain>.ngrok-free.dev/webhook です。

localtunnel(代替 — 信頼性に欠けます):

npx localtunnel --port 9477

https://xxx.loca.lt のような URL が表示されます。Webhook URL は https://xxx.loca.lt/webhook です。

警告: localtunnel の無料リレーは、数分後に接続を予告なく切断します。この場合、トンネルを再起動してアラートの webhook URL を更新するまで、すべてのアラートが「503 Tunnel Unavailable」で失敗します。特別な理由がない限り、ngrok を使用してください。

注: トンネル URL は一時的なもので、再起動のたびに変わります。常時利用する環境では、固定 URL を持つホストにサーバーをデプロイしてください。

ステップ 4:次の手順をまとめる

アラートは作成も変更もしないでください。 代わりに、何を設定したか、次に何をする必要があるかをユーザーに明確に説明するサマリーを出力してください。

サマリーには、必ず以下を含めてください。

  1. 作成したもの(サーバースクリプトのパスと生成されたシークレット)の確認
  2. サーバーとトンネルを起動するコマンド(実際のシークレットを入力済みの状態)
  3. ユーザーが実行する正確な nansen alerts create または nansen alerts update コマンド。--webhook と --webhook-secret フラグにはトンネル URL とシークレットを入力し、アラート固有のフラグ(--name、--type、--chains など)はユーザーが入力できるようプレースホルダーのままにしてください
  4. アラートを作成する前にサーバーとトンネルを起動しておく必要があるという注意(Nansen は作成時に Webhook エンドポイントを検証します)
  5. トンネル URL は一時的なもので、再起動すると変わるという注意

サマリーの形式例:

## Webhook listener ready

**Server script:** ./nansen-webhook-server.mjs
**Port:** 9477

### To start receiving alerts:

1. Start the server (keep this terminal open):
   WEBHOOK_SECRET='<actual-secret>' node nansen-webhook-server.mjs

2. In a new terminal, start the tunnel:
   ngrok http 9477          # recommended
   # or: npx localtunnel --port 9477  (unreliable — tunnel drops silently)

3. Create an alert pointing to your webhook (fill in your alert details):
   nansen alerts create \
     --name '<your alert name>' \
     --type <sm-token-flows|common-token-transfer|smart-contract-call> \
     --chains <chains> \
     --webhook 'https://<your-tunnel-url>/webhook' \
     --webhook-secret '<actual-secret>' \
     [type-specific flags...]

   Or add the webhook to an existing alert:
   nansen alerts update <alert-id> \
     --webhook 'https://<your-tunnel-url>/webhook' \
     --webhook-secret '<actual-secret>'

Note: The tunnel URL changes each time you restart. Update the alert
webhook URL if you restart the tunnel.

See `nansen alerts create --help` for full flag reference per alert type.

セキュリティチェックリスト

  • 必ず Webhook シークレットを使用してください — サーバーはシークレットがないと起動しません
  • 必ず署名を検証してください — 検証されていないペイロードは受け入れないでください
  • localhost のみでバインドしてください — 外部への公開はトンネルが担います。0.0.0.0 に直接バインドすると、認証されていないトラフィックにさらされます
  • HTTPS を使用してください — localtunnel と ngrok は、デフォルトで HTTPS 経由でトンネルを作成します
  • リクエストボディのサイズ制限 — 1 MB の上限により、サイズの大きすぎるリクエストによるメモリ枯渇を防ぎます
  • タイミングセーフな比較 — 署名に対するタイミングサイドチャネル攻撃を防ぎます

トラブルシューティング

症状対処法
すべてのリクエストで「Invalid signature」と表示されるWEBHOOK_SECRET と --webhook-secret に完全に同じシークレットが設定されていることを確認してください
アラート作成時に「Failed to send welcome message」と表示されるアラートを作成する前にサーバーとトンネルを起動してください
アラートが届かないnansen alerts list --table を確認してください — アラートは有効になっていますか? Webhook URL は正しいですか(/webhook を含んでいますか)?
トンネル URL の期限切れ/トンネルの停止トンネルを再起動して新しい URL を取得し、nansen alerts update <id> --webhook '<new-url>/webhook' を実行してください。この問題が繰り返し発生する場合は、localtunnel から ngrok に切り替えてください。
ポートがすでに使用されている別のポートを設定してください:PORT=9478 WEBHOOK_SECRET='...' node nansen-webhook-server.mjs。それに合わせてトンネルも更新してください

OpenClaw との連携

ユーザーが同じマシン上で OpenClaw をローカル実行している場合、Webhook サーバーは検証済みのアラートペイロードを OpenClaw の Gateway に転送し、アラートごとにエージェントのターンを起動できます。

フロー: Nansen → ngrok → webhook server (signature check) → OpenClaw /hooks/agent

追加の環境変数
変数必須用途
OPENCLAW_GATEWAY_URLはいOpenClaw Gateway のベース URL(例:http://localhost:3000)
OPENCLAW_AUTH_TOKEN認証が有効な場合OpenClaw Webhook エンドポイント用の Bearer トークン
起動コマンド(OpenClaw への転送あり)
WEBHOOK_SECRET='<secret>' \
OPENCLAW_GATEWAY_URL='http://localhost:3000' \
OPENCLAW_AUTH_TOKEN='<token>' \
node nansen-webhook-server.mjs

サーバーは、アラートペイロードと OpenClaw への転送ステータスの両方をログに記録します。OpenClaw に到達できない場合、転送はエラーを表示せずに失敗します(アラートは引き続き stdout に記録されます)。

ユーザーへの確認事項

OpenClaw への転送を有効にする前に、次の点を確認してください。

  1. OpenClaw はローカルで実行されていますか? ポートはいくつですか?
  2. Gateway には認証が必要ですか? 必要な場合、Bearer トークンは何ですか?

分からない場合、または OpenClaw を実行していない場合は、スキップしてください — サーバーは単独でも問題なく動作します。

注意事項

  • このサーバーには npm 依存関係がありません — Node.js の組み込み機能のみを使用します
  • 同じ Webhook シークレットを共有している限り、1 つのサーバーで複数の Nansen アラートからの通知を受信できます
  • 本番環境で使用する場合は、静的 URL を持つクラウドホストにデプロイし、TLS を使用するリバースプロキシの背後で実行してください
  • x-nansen-signature ヘッダーの形式は sha256=<HMAC-SHA256(secret, rawBody)> です — 比較する前に sha256= プレフィックスを取り除いてください

コメント