nansen-alerts-webhook-listener
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAlert Webhook Listener
告警Webhook监听器
Set up a local HTTP server to receive Nansen smart alert webhook payloads in real-time.
搭建本地HTTP服务器,实时接收Nansen智能告警webhook payload。
How It Works
工作原理
Nansen smart alerts support a webhook channel type. When an alert fires, Nansen sends an HTTP POST with a JSON payload to your webhook URL. This skill sets up:
- A local HTTP server (Node.js, zero external dependencies) that receives and displays alert payloads
- HMAC-SHA256 signature verification so only authentic Nansen payloads are accepted
- A public tunnel so Nansen's servers can reach your local machine
This skill does NOT create or modify alerts. It sets up the listener infrastructure and then provides a summary of what the user needs to do to start receiving alerts.
OpenClaw users: If OpenClaw is running locally on the same machine, the webhook server can forward verified alert payloads to OpenClaw's Gateway (), triggering an agent turn for each alert. Set the env var to enable this. See the OpenClaw Integration section below.
/hooks/agentOPENCLAW_GATEWAY_URLNansen智能告警支持webhook通道类型。当告警触发时,Nansen会向你的webhook URL发送携带JSON payload的HTTP POST请求。本技能会搭建以下内容:
- 本地HTTP服务器(Node.js,零外部依赖),用于接收并展示告警payload
- HMAC-SHA256签名验证,仅接收来自Nansen的合法payload
- 公共隧道,让Nansen的服务器可以访问你的本地机器
本技能不会创建或修改告警,它只会搭建监听基础设施,然后为用户提供后续开始接收告警所需的操作指引。
OpenClaw用户: 如果OpenClaw在同一台机器上本地运行,webhook服务器可以将验证通过的告警payload转发到OpenClaw的网关(),每个告警触发一次agent轮询。设置环境变量即可启用该功能,详见下方OpenClaw集成部分。
/hooks/agentOPENCLAW_GATEWAY_URLSecurity Warning
安全警告
Before proceeding, inform the user:
This skill starts an HTTP server on your machine and exposes it to the internet via a tunnel (ngrok or localtunnel). While the server only binds to localhost () — meaning no one on your local network can access it directly — the tunnel creates a public URL that anyone on the internet can send requests to.127.0.0.1Mitigations in place:
- HMAC-SHA256 signature verification rejects all requests not signed by Nansen
- 1 MB body size limit prevents memory abuse
- Only
andPOST /webhookare accepted; everything else returns 404GET /healthYou should be aware that:
- The tunnel URL is publicly discoverable (ngrok URLs can be enumerated)
- Unsigned requests still reach your machine — they're rejected, but the connection is made
- Stop the tunnel when you're done to close the public endpoint
Wait for the user to confirm they want to proceed before continuing.
继续操作前,请告知用户:
本技能会在你的机器上启动一个HTTP服务器,并通过隧道(ngrok或localtunnel)将其暴露到公网。虽然服务器仅绑定到localhost()——意味着你本地网络的其他用户无法直接访问它——但隧道会生成一个公网URL,互联网上的任何人都可以向这个地址发送请求。127.0.0.1已内置的安全措施:
- HMAC-SHA256签名验证会拒绝所有不是Nansen签名的请求
- 1MB的请求体大小限制可以防止内存滥用
- 仅接受
和POST /webhook请求,其他所有请求都返回404GET /health你需要了解:
- 隧道URL是公开可被枚举的(ngrok的URL可以被遍历发现)
- 未签名的请求仍然会到达你的机器——虽然会被拒绝,但连接已经建立
- 使用完成后请停止隧道,关闭公网端点
请等待用户确认要继续后再进行后续操作。
Execution Plan
执行计划
Follow these steps in order. Do not skip signature verification — it is mandatory.
按顺序执行以下步骤,不要跳过签名验证——这是强制要求。
Step 0: Choose a tunnel provider
步骤0:选择隧道提供商
Before starting, ask the user which tunnel provider they want to use:
| ngrok (recommended) | localtunnel | |
|---|---|---|
| Stability | Stable — persistent connections with keepalive | Flaky — free relay drops idle connections without warning, tunnels die randomly |
| Install | | Zero install ( |
| HTTPS | Yes | Yes |
| Auth required | Yes (free authtoken from ngrok.com) | No |
Recommend ngrok. localtunnel is convenient but unreliable — in testing, tunnels silently exit after minutes, causing alerts to fail with "503 Tunnel Unavailable". ngrok maintains stable connections.
Check if ngrok is available:
bash
which ngrok && ngrok versionIf not installed, tell the user:
- (or download from ngrok.com)
brew install ngrok - Create a free account at ngrok.com and copy the authtoken
ngrok config add-authtoken <token>
If the user prefers localtunnel or can't install ngrok, proceed with localtunnel but warn them that the tunnel may drop and they'll need to restart it and update their alert's webhook URL.
开始前,询问用户想要使用哪个隧道提供商:
| ngrok(推荐) | localtunnel | |
|---|---|---|
| 稳定性 | 稳定——带心跳的持久连接 | 不稳定——免费中继会无预警断开空闲连接,隧道会随机失效 |
| 安装要求 | | 零安装( |
| HTTPS支持 | 是 | 是 |
| 需要认证 | 是(来自ngrok.com的免费authtoken) | 否 |
推荐使用ngrok。localtunnel虽然方便但不可靠——测试中,隧道会在几分钟后静默退出,导致告警返回“503隧道不可用”,ngrok可以保持稳定连接。
检查ngrok是否可用:
bash
which ngrok && ngrok version如果未安装,告知用户:
- (或者从ngrok.com下载安装包)
brew install ngrok - 在ngrok.com创建免费账号并复制authtoken
ngrok config add-authtoken <token>
如果用户偏好localtunnel或者无法安装ngrok,可以使用localtunnel,但要警告用户隧道可能会断开,需要重启并更新告警的webhook URL。
Step 1: Generate a webhook secret
步骤1:生成webhook密钥
bash
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Store the output — you need it for both the server and the alert configuration. Never log or echo the secret after this point.
bash
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"存储输出结果,你在服务器和告警配置中都需要用到它。在此之后不要记录或打印密钥。
Step 2: Write the webhook receiver script
步骤2:编写webhook接收脚本
Create in the current working directory. Use only Node.js built-in modules (, ). No required.
nansen-webhook-server.mjsnode:httpnode:cryptonpm installRequirements — do not deviate:
| Requirement | Detail |
|---|---|
| Bind address | |
| Default port | |
| Webhook path | |
| Health check | |
| Signature verification | Verify |
| Secret validation | Exit on startup if |
| Payload logging | Pretty-print valid JSON payloads to stdout with ISO timestamp |
| Request size limit | Reject bodies > 1 MB (413) to prevent memory abuse |
| Graceful shutdown | Handle |
| OpenClaw forwarding | If |
| No dependencies | Only |
Signature verification — use timing-safe comparison:
javascript
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
}
}Full server template:
javascript
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');
});在当前工作目录创建,仅使用Node.js内置模块(、),无需执行。
nansen-webhook-server.mjsnode:httpnode:cryptonpm install要求——请勿修改:
| 要求 | 细节 |
|---|---|
| 绑定地址 | 仅 |
| 默认端口 | |
| Webhook路径 | |
| 健康检查 | |
| 签名验证 | 使用HMAC-SHA256和时序安全比较验证 |
| 密钥验证 | 启动时如果 |
| Payload日志 | 带ISO时间戳,将合法JSON payload格式化打印到标准输出 |
| 请求大小限制 | 拒绝大于1MB的请求体(413),防止内存滥用 |
| 优雅关机 | 处理 |
| OpenClaw转发 | 如果设置了 |
| 无依赖 | 仅使用 |
签名验证——使用时序安全比较:
javascript
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
}
}完整服务器模板:
javascript
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');
});Step 3: Start the server and tunnel
步骤3:启动服务器和隧道
Start the server:
bash
WEBHOOK_SECRET='<secret>' node nansen-webhook-server.mjsThen start a public tunnel so Nansen's servers can reach it.
ngrok (recommended):
bash
ngrok http 9477Get the public URL from ngrok's output or its local API:
bash
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))"The webhook URL is .
https://<subdomain>.ngrok-free.dev/webhooklocaltunnel (fallback — unreliable):
bash
npx localtunnel --port 9477Prints a URL like . The webhook URL is .
https://xxx.loca.lthttps://xxx.loca.lt/webhookWarning: localtunnel's free relay silently drops connections after minutes. When this happens, all alerts fail with "503 Tunnel Unavailable" until you restart the tunnel and update the alert webhook URL. Use ngrok unless you have a reason not to.
Note: Tunnel URLs are ephemeral — they change every restart. For permanent setups, deploy the server to a host with a static URL.
启动服务器:
bash
WEBHOOK_SECRET='<secret>' node nansen-webhook-server.mjs然后启动公共隧道让Nansen服务器可以访问它。
ngrok(推荐):
bash
ngrok http 9477从ngrok的输出或者本地API获取公共URL:
bash
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/webhooklocaltunnel(备选——不可靠):
bash
npx localtunnel --port 9477会打印类似的URL,webhook URL为。
https://xxx.loca.lthttps://xxx.loca.lt/webhook警告: localtunnel的免费中继会在数分钟后静默断开连接,发生这种情况时,所有告警都会返回“503隧道不可用”,直到你重启隧道并更新告警webhook URL。除非有特殊原因,否则请使用ngrok。
注意: 隧道URL是临时的——每次重启都会变化,如需永久部署,请将服务器部署到有静态URL的主机上。
Step 4: Provide a next-steps summary
步骤4:提供后续步骤总结
Do NOT create or modify any alerts. Instead, print a clear summary for the user explaining what was set up and what they need to do next.
The summary MUST include:
- Confirmation of what was created (the server script path and the generated secret)
- The commands to start the server and tunnel (with the actual secret filled in)
- The exact or
nansen alerts createcommand they should run, with thenansen alerts updateand--webhookflags filled in with the tunnel URL and secret — but leave the alert-specific flags (--webhook-secret,--name,--type, etc.) as placeholders for the user to fill in--chains - A reminder that the server and tunnel must be running before the alert is created (Nansen validates the webhook endpoint on creation)
- A note that tunnel URLs are ephemeral and will change on restart
Example summary format:
undefined不要创建或修改任何告警,而是为用户打印清晰的总结,说明已经搭建的内容,以及后续需要执行的操作。
总结必须包含:
- 确认创建的内容(服务器脚本路径和生成的密钥)
- 启动服务器和隧道的命令(填入实际的密钥)
- 他们需要运行的或
nansen alerts create命令的准确格式,nansen alerts update和--webhook参数填入隧道URL和密钥——但告警相关的参数(--webhook-secret、--name、--type等)留空让用户自行填写--chains - 提醒用户在创建告警前必须先启动服务器和隧道(Nansen在创建告警时会验证webhook端点)
- 说明隧道URL是临时的,重启后会变化
示例总结格式:
undefinedWebhook listener ready
Webhook监听器已就绪
Server script: ./nansen-webhook-server.mjs
Port: 9477
服务器脚本: ./nansen-webhook-server.mjs
端口: 9477
To start receiving alerts:
要开始接收告警:
-
Start the server (keep this terminal open): WEBHOOK_SECRET='<actual-secret>' node nansen-webhook-server.mjs
-
In a new terminal, start the tunnel: ngrok http 9477 # recommended
or: npx localtunnel --port 9477 (unreliable — tunnel drops silently)
-
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 for full flag reference per alert type.
nansen alerts create --helpundefined-
启动服务器(保持这个终端窗口打开): WEBHOOK_SECRET='<actual-secret>' node nansen-webhook-server.mjs
-
在新终端中启动隧道: ngrok http 9477 # 推荐
或: npx localtunnel --port 9477 (不可靠 — 隧道会静默断开)
-
创建指向你的webhook的告警(填写你的告警详情): 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...]或者将webhook添加到现有告警: nansen alerts update <alert-id>
--webhook 'https://<your-tunnel-url>/webhook'
--webhook-secret '<actual-secret>'
注意:每次重启隧道URL都会变化,如果重启了隧道请更新告警的webhook URL。
查看获取每个告警类型的完整参数参考。
nansen alerts create --helpundefinedSecurity Checklist
安全清单
- Always use a webhook secret — the server refuses to start without one
- Always verify signatures — never accept unverified payloads
- Bind to localhost only — the tunnel handles public exposure; direct binding exposes you to unauthenticated traffic
0.0.0.0 - Use HTTPS — both localtunnel and ngrok tunnel via HTTPS by default
- Body size limit — the 1 MB cap prevents memory exhaustion from oversized requests
- Timing-safe comparison — prevents timing side-channel attacks on the signature
- 始终使用webhook密钥——没有密钥服务器会拒绝启动
- 始终验证签名——永远不要接收未验证的payload
- 仅绑定到localhost——隧道负责公网暴露;直接绑定会让你暴露在未认证流量下
0.0.0.0 - 使用HTTPS——localtunnel和ngrok默认都通过HTTPS隧道传输
- 请求体大小限制——1MB上限可以防止超大请求导致内存耗尽
- 时序安全比较——防止针对签名的时序侧信道攻击
Troubleshooting
故障排查
| Symptom | Fix |
|---|---|
| "Invalid signature" on every request | Ensure the exact same secret is in |
| "Failed to send welcome message" on alert create | Start the server and tunnel before creating the alert |
| No alerts arriving | Check |
| Tunnel URL expired / tunnel died | Restart the tunnel, get the new URL, then |
| Port already in use | Set a different port: |
| 症状 | 解决方案 |
|---|---|
| 每次请求都返回“Invalid signature” | 确保 |
| 创建告警时返回“Failed to send welcome message” | 在创建告警之前先启动服务器和隧道 |
| 没有收到告警 | 检查 |
| 隧道URL过期/隧道失效 | 重启隧道,获取新URL,然后执行 |
| 端口已被占用 | 设置不同的端口: |
OpenClaw Integration
OpenClaw集成
If the user is running OpenClaw locally on the same machine, the webhook server can forward verified alert payloads to OpenClaw's Gateway, triggering an agent turn for each alert.
Flow:
Nansen → ngrok → webhook server (signature check) → OpenClaw /hooks/agent如果用户在同一台机器上本地运行OpenClaw,webhook服务器可以将验证通过的告警payload转发到OpenClaw的网关,每个告警触发一次agent轮询。
流程:
Nansen → ngrok → webhook服务器(签名校验)→ OpenClaw /hooks/agentAdditional env vars
额外环境变量
| Var | Required | Purpose |
|---|---|---|
| Yes | OpenClaw Gateway base URL (e.g. |
| If auth enabled | Bearer token for OpenClaw webhook endpoints |
| 变量 | 必填 | 用途 |
|---|---|---|
| 是 | OpenClaw网关基础URL(例如 |
| 开启认证时需要 | OpenClaw webhook端点的Bearer token |
Start command (with OpenClaw forwarding)
启动命令(带OpenClaw转发)
bash
WEBHOOK_SECRET='<secret>' \
OPENCLAW_GATEWAY_URL='http://localhost:3000' \
OPENCLAW_AUTH_TOKEN='<token>' \
node nansen-webhook-server.mjsThe server logs both the alert payload and the OpenClaw forward status. If OpenClaw is unreachable, the forward fails silently (the alert is still logged to stdout).
bash
WEBHOOK_SECRET='<secret>' \
OPENCLAW_GATEWAY_URL='http://localhost:3000' \
OPENCLAW_AUTH_TOKEN='<token>' \
node nansen-webhook-server.mjs服务器会同时记录告警payload和OpenClaw转发状态。如果OpenClaw不可达,转发会静默失败(告警仍会打印到标准输出)。
Ask the user
询问用户
Before enabling OpenClaw forwarding, ask:
- Is OpenClaw running locally? What port?
- Does their Gateway require auth? If so, what's the Bearer token?
If they don't know or aren't running OpenClaw, skip — the server works fine standalone.
在启用OpenClaw转发前,询问:
- OpenClaw是否在本地运行?端口是多少?
- 他们的网关是否需要认证?如果需要,Bearer token是什么?
如果他们不知道或者没有运行OpenClaw,跳过这部分——服务器可以独立正常运行。
Notes
注意事项
- The server uses zero npm dependencies — only Node.js built-ins
- One server can receive alerts from multiple Nansen alerts (as long as they share the same webhook secret)
- For production use, deploy to a cloud host with a static URL and run behind a reverse proxy with TLS
- The header format is
x-nansen-signature— strip thesha256=<HMAC-SHA256(secret, rawBody)>prefix before comparingsha256=
- 服务器零npm依赖——仅使用Node.js内置模块
- 一个服务器可以接收多个Nansen告警的消息(只要它们使用相同的webhook密钥)
- 生产环境使用时,部署到有静态URL的云主机,并且在带TLS的反向代理后运行
- 请求头格式是
x-nansen-signature——比较前要去掉sha256=<HMAC-SHA256(secret, rawBody)>前缀sha256=