Webhook 示例
提醒告诉您市场正在发生什么。Webhook 和 Telegram 让您用自己的工具和自己的账户采取行动。我们从不替您交易,从不持有资金,也从不索要可交易权限的 API 密钥。
开始之前
- 在提醒页面添加一个自定义 JSON Webhook。VIP Quant 和试用包含此功能。
- 复制签名密钥,它只显示一次。
- 点击“发送测试”,检查您的端点是否收到。
- 将一条规则指向该渠道。默认提醒开启时也会发送到该渠道。
请在 8 秒内返回任意 2xx。发送失败会以相同的 id 重试,因此请按它去重。下面每段代码都已针对真实的签名推送运行过。您需对基于这些事件所做的任何自动化负责。
事件
opportunity.alert:某条路径匹配了规则或默认提醒。默认提醒的rule.id为default。listing.alert:某个市场刚刚上架,每次上架一个事件。opportunity.closed:您收到过提醒的路径已关闭(如规则要求)。position.alert:追踪中的交易发生了变化。notice.alert:交易所公告了下架、暂停充值或提现、维护或上架,所属提醒为 默认提醒。tracked表示该事项在您追踪的交易上。watch.alert:您监控的钱包收到或转出了一笔转账,所属提醒为 默认提醒。交易哈希未知时,event.tx为 null。alert.test:来自“发送测试”,带有"test": true和一个名为 TEST 的虚拟币种。
{
"id": "evt_7c1e...",
"type": "listing.alert",
"test": false,
"sentAt": "2026-11-02T10:00:00.000Z",
"rule": { "id": "...", "name": "New listings" },
"listing": { "coin": "ABC", "venue": "Binance", "kind": "spot", "symbol": "ABCUSDT", "listedMs": 1793000000000 },
"window": { "sizeUsd": 1000, "openMs": 4000, "avgWindowMs": null, "avgPeakNetBps": null, "windowSamples": 0 },
"opportunity": { "id": "ABC|Binance:spot|OKX:spot", "coin": "ABC", "...": "..." }
}完整字段请见 Webhook 文档。
校验签名
每个请求都带有 X-ArbitrageX-Timestamp 和 X-ArbitrageX-Signature,后者为 sha256= 加上 timestamp.rawBody 的十六进制 HMAC-SHA256。请在解析前基于原始字节校验,因为解析后重新编码的 JSON 不会匹配。拒绝与当前时间相差超过 5 分钟的时间戳以防重放。
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, headers, secret) {
const ts = headers["x-arbitragex-timestamp"];
if (!(Math.abs(Date.now() / 1000 - Number(ts)) <= 300)) return false;
const expected =
"sha256=" + createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
const given = Buffer.from(String(headers["x-arbitragex-signature"]));
const wanted = Buffer.from(expected);
return given.length === wanted.length && timingSafeEqual(given, wanted);
}Python
import hashlib
import hmac
import time
def verify(raw_body: bytes, headers: dict, secret: str) -> bool:
ts = headers.get("x-arbitragex-timestamp", "")
try:
if abs(time.time() - int(ts)) > 300:
return False
except ValueError:
return False
signed = ts.encode() + b"." + raw_body
expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected.encode(), headers.get("x-arbitragex-signature", "").encode())Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"math"
"net/http"
"strconv"
"time"
)
func Verify(rawBody []byte, headers http.Header, secret string) bool {
ts := headers.Get("X-ArbitrageX-Timestamp")
unix, err := strconv.ParseInt(ts, 10, 64)
if err != nil || math.Abs(float64(time.Now().Unix()-unix)) > 300 {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(ts + "."))
mac.Write(rawBody)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(headers.Get("X-ArbitrageX-Signature")))
}执行工具
下方每个示例都是工具所需的请求体,按我们的标准负载编写:尖括号中的每个值是应填入的我方字段。可在 Zapier 或 Make 中 catch webhook 之后的步骤里构建,或在您自己的中转程序(例如“运行您自己的脚本”中的接收端)中构建,然后 POST 到工具的 Webhook URL。大写的值由您自己填写。只有 opportunity.alert 和 listing.alert 应该发送到工具:请过滤掉 test 和所有其他类型,以免关闭通知触发开仓。我们从不交易:工具如何处理消息由您和工具决定。
各工具的字段会随时间变化。依赖某个示例之前,请对照该工具的最新文档核对。
3Commas signal bot
适用于 3Commas 信号机器人的自定义 Webhook,其密钥和机器人 UUID 来自机器人的消息设置。信号机器人只在一家交易所交易,因此请用规则的交易所筛选,将规则限定为在该交易所买入的路径。
{
"secret": "YOUR_3COMMAS_SECRET",
"bot_uuid": "YOUR_BOT_UUID",
"max_lag": "300",
"timestamp": "<sentAt>",
"trigger_price": "<opportunity.buy.price>",
"tv_exchange": "<opportunity.buy.venue in capitals, e.g. BINANCE>",
"tv_instrument": "<opportunity.coin>USDT",
"action": "enter_long"
}Hummingbot
Hummingbot 通过自己的接口而非 Webhook 接收命令,因此这会发送到您自己的一个小型脚本策略,或其 MQTT 接口的桥接程序。请使用您连接器自己的名称。
{
"connector": "<opportunity.buy.venue in lower case, e.g. binance>",
"trading_pair": "<opportunity.coin>-USDT",
"side": "BUY",
"price": <opportunity.buy.price>,
"amount_usd": <window.sizeUsd>,
"hedge_connector": "<opportunity.sell.venue in lower case>",
"hedge_price": <opportunity.sell.price>,
"net_bps": <opportunity.netBps>,
"event_id": "<id>"
}Cornix signal
采用 Cornix 信号格式的纯文本,发送到 Cornix 为 TradingView 机器人提供的 Webhook 地址。请自行设置止损,并对照 Cornix 的最新指南核对格式。
#<opportunity.coin>/USDT Exchanges: <opportunity.buy.venue> Signal Type: Regular (Long) Entry Targets: 1) <opportunity.buy.price> Take-Profit Targets: 1) <opportunity.sell.price> Stop Targets: 1) YOUR_STOP_PRICE
TradingView-style JSON
TradingView 提醒发送的字段名称,适用于专为接收 TradingView 提醒而构建的工具。
{
"ticker": "<opportunity.coin>USDT",
"exchange": "<opportunity.buy.venue in capitals>",
"action": "buy",
"price": <opportunity.buy.price>,
"time": "<sentAt>",
"message": "<opportunity.coin> <opportunity.buy.venue> to <opportunity.sell.venue>, net <opportunity.netBps> bps"
}Flat JSON
所有数字都位于顶层,适用于电子表格、n8n 或数据库。
{
"id": "<id>",
"coin": "<opportunity.coin>",
"net_bps": <opportunity.netBps>,
"net_usd_1k": <opportunity.netUsd.1000>,
"max_size_usd": <opportunity.maxSizeUsd>,
"buy_exchange": "<opportunity.buy.venue>",
"buy_price": <opportunity.buy.price>,
"buy_url": "<opportunity.buy.url>",
"sell_exchange": "<opportunity.sell.venue>",
"sell_price": <opportunity.sell.price>,
"sell_url": "<opportunity.sell.url>"
}其他渠道
下方每个渠道都在提醒页面设置,都有“发送测试”按钮,内容与 Telegram 消息一致,并使用账户的语言。规则、冷却时间、免打扰时段、频率限制和自动暂停在所有渠道上的效果都相同。
- Slack(Pro Desk 及以上):在 Slack 中将 Incoming Webhooks 应用添加到某个频道,然后粘贴它提供的
hooks.slack.comURL。 - ntfy(Starter 及以上):手机和桌面上的免费通知。粘贴一个主题 URL,例如
https://ntfy.sh/your-topic,或您自己服务器上的地址,并在 ntfy 应用中订阅同一主题。任何知道公开主题名称的人都能读取它,因此请选择一个较长的随机名称,或为受保护的主题添加访问令牌。每条提醒都有两家交易所的按钮。 - 邮件摘要(Starter 及以上):向您账户的邮箱发送一封邮件,包含自上次以来的所有提醒,每小时整点或每天 08:00 UTC 发送。没有新内容时不会发送。
WhatsApp 没有供服务免费向您发消息的方式,因此不提供。其他任何工具都可以使用自定义 Webhook,它是基于 https 的纯 JSON。
发送到 Slack
一个无需运行服务器的 Cloudflare Worker。将 ARBITRAGEX_SECRET 和 SLACK_WEBHOOK_URL 设为密钥,然后将 Worker 的 URL 作为接收渠道。同样的代码可在任何支持 Web Crypto 的环境中运行。
async function verify(rawBody, headers, secret) {
const ts = headers.get("x-arbitragex-timestamp");
if (!(Math.abs(Date.now() / 1000 - Number(ts)) <= 300)) return false;
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"],
);
const given = (headers.get("x-arbitragex-signature") ?? "").replace("sha256=", "");
if (!/^[0-9a-f]{64}$/.test(given)) return false;
const bytes = Uint8Array.from(given.match(/../g), (pair) => parseInt(pair, 16));
return crypto.subtle.verify("HMAC", key, bytes, encoder.encode(`${ts}.${rawBody}`));
}
export default {
async fetch(request, env) {
const rawBody = await request.text();
if (!(await verify(rawBody, request.headers, env.ARBITRAGEX_SECRET))) {
return new Response("bad signature", { status: 401 });
}
const event = JSON.parse(rawBody);
if (event.type === "opportunity.alert" || event.type === "listing.alert") {
const { coin, buy, sell, netBps } = event.opportunity;
await fetch(env.SLACK_WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `${coin}: ${buy.venue} to ${sell.venue}, net ${netBps.toFixed(1)} bps`,
}),
});
}
return new Response(null, { status: 204 });
},
};记录到电子表格
每条提醒向 CSV 追加一行,可用任何电子表格打开,只需 Python 标准库。请部署在 https 后面:我们只发送到公网 https 地址。
import csv
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
import hashlib
import hmac
import time
def verify(raw_body: bytes, headers: dict, secret: str) -> bool:
ts = headers.get("x-arbitragex-timestamp", "")
try:
if abs(time.time() - int(ts)) > 300:
return False
except ValueError:
return False
signed = ts.encode() + b"." + raw_body
expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected.encode(), headers.get("x-arbitragex-signature", "").encode())
SECRET = "your signing secret"
seen = set()
class Receiver(BaseHTTPRequestHandler):
def do_POST(self):
raw_body = self.rfile.read(int(self.headers["Content-Length"]))
headers = {key.lower(): value for key, value in self.headers.items()}
if not verify(raw_body, headers, SECRET):
self.send_response(401)
self.end_headers()
return
self.send_response(204)
self.end_headers()
event = json.loads(raw_body)
if event["test"] or event["id"] in seen or "opportunity" not in event:
return
seen.add(event["id"])
row = event["opportunity"]
with open("alerts.csv", "a", newline="") as file:
csv.writer(file).writerow(
[event["sentAt"], event["type"], row["coin"], row["buy"]["venue"], row["sell"]["venue"], row["netBps"]]
)
if __name__ == "__main__":
HTTPServer(("127.0.0.1", 8000), Receiver).serve_forever()运行您自己的脚本
一个无依赖的接收器,立即响应并将每条提醒交给您的一个函数处理。该函数做什么由您编写,也由您负责。提醒是基于市场数据的估算,路径可能在您启动的任何操作生效前就已关闭。
import { createServer } from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, headers, secret) {
const ts = headers["x-arbitragex-timestamp"];
if (!(Math.abs(Date.now() / 1000 - Number(ts)) <= 300)) return false;
const expected =
"sha256=" + createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
const given = Buffer.from(String(headers["x-arbitragex-signature"]));
const wanted = Buffer.from(expected);
return given.length === wanted.length && timingSafeEqual(given, wanted);
}
const seen = new Set();
function onAlert(event) {
// Your code goes here. You decide what happens next, and we never see it.
console.log(event.type, event.opportunity.coin, event.opportunity.netBps);
}
export const server = createServer((req, res) => {
const parts = [];
req.on("data", (part) => parts.push(part));
req.on("end", () => {
const rawBody = Buffer.concat(parts).toString("utf8");
if (!verify(rawBody, req.headers, process.env.ARBITRAGEX_SECRET)) {
return res.writeHead(401).end();
}
const event = JSON.parse(rawBody);
// Answer first: anything slower than 8 seconds counts as a failed delivery.
res.writeHead(204).end();
if (event.test || seen.has(event.id)) return;
seen.add(event.id);
if (event.type === "opportunity.alert" || event.type === "listing.alert") onAlert(event);
});
});Zapier 和 Make
自定义 Webhook 是纯 JSON POST,因此两者中任意一个的 catch webhook URL 都可作为接收渠道。添加一个基于 type 和 test 的筛选步骤,然后映射 opportunity.coin 和 opportunity.netBps 等字段。这些步骤不会替您校验我们的签名,因此请把 catch URL 当作密码对待,不要在那里触发任何您不愿让陌生人触发的操作。
Telegram
如果您只想在手机上收到提醒,可以跳过以上所有内容。在提醒页面点击“连接 Telegram”,然后在打开的聊天中点击 Start。请查看提醒文档。