nansen-alerts-webhook-listener について
nansen-alerts-webhook-listenerは、NansenのスマートアラートWebhookを受信するローカルサーバーを設定するスキルです。署名を検証し、検証済みの通知をOpenClawへ転送できますが、アラートの作成と変更は手動です。
主な機能
- 01ローカルNode.js HTTPリスナーの作成
- 02HMAC-SHA256署名の検証
- 03ngrokまたはlocaltunnelで公開
- 04検証済みペイロードをOpenClawへ転送
- 05リクエスト方式と本文サイズの制限
活用シーン
- 01NansenスマートアラートWebhookのローカル受信
- 02検証済み通知のOpenClaw Gatewayへの転送
- 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 に送信します。このスキルでは、以下をセットアップします。
- アラートのペイロードを受信して表示するローカル HTTP サーバー(Node.js、外部依存関係なし)
- 本物の Nansen ペイロードだけを受け付ける HMAC-SHA256 署名検証
- 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
インストールされていない場合は、ユーザーに次の手順を伝えます。
brew install ngrok(または ngrok.com からダウンロード)- ngrok.com で無料アカウントを作成し、authtoken をコピー
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:次の手順をまとめる
アラートは作成も変更もしないでください。 代わりに、何を設定したか、次に何をする必要があるかをユーザーに明確に説明するサマリーを出力してください。
サマリーには、必ず以下を含めてください。
- 作成したもの(サーバースクリプトのパスと生成されたシークレット)の確認
- サーバーとトンネルを起動するコマンド(実際のシークレットを入力済みの状態)
- ユーザーが実行する正確な
nansen alerts createまたはnansen alerts updateコマンド。--webhookと--webhook-secretフラグにはトンネル URL とシークレットを入力し、アラート固有のフラグ(--name、--type、--chainsなど)はユーザーが入力できるようプレースホルダーのままにしてください - アラートを作成する前にサーバーとトンネルを起動しておく必要があるという注意(Nansen は作成時に Webhook エンドポイントを検証します)
- トンネル 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 への転送を有効にする前に、次の点を確認してください。
- OpenClaw はローカルで実行されていますか? ポートはいくつですか?
- Gateway には認証が必要ですか? 必要な場合、Bearer トークンは何ですか?
分からない場合、または OpenClaw を実行していない場合は、スキップしてください — サーバーは単独でも問題なく動作します。
注意事項
- このサーバーには npm 依存関係がありません — Node.js の組み込み機能のみを使用します
- 同じ Webhook シークレットを共有している限り、1 つのサーバーで複数の Nansen アラートからの通知を受信できます
- 本番環境で使用する場合は、静的 URL を持つクラウドホストにデプロイし、TLS を使用するリバースプロキシの背後で実行してください
x-nansen-signatureヘッダーの形式はsha256=<HMAC-SHA256(secret, rawBody)>です — 比較する前にsha256=プレフィックスを取り除いてください