Công thức webhook
Cảnh báo cho bạn biết thị trường đang diễn biến thế nào. Webhook và Telegram là cách bạn hành động, bằng công cụ và tài khoản của riêng bạn. Chúng tôi không bao giờ giao dịch thay bạn, không bao giờ giữ tiền và không bao giờ yêu cầu API key có quyền giao dịch.
Trước khi bắt đầu
- Trên trang Cảnh báo, thêm một webhook JSON tùy chỉnh. VIP Quant và bản dùng thử có tính năng này.
- Sao chép khóa ký. Khóa chỉ hiển thị một lần.
- Nhấn Gửi thử và kiểm tra endpoint của bạn đã nhận được.
- Trỏ một quy tắc tới đích nhận này. Cảnh báo mặc định cũng gửi tới đó khi đang bật.
Trả lời bằng bất kỳ mã 2xx nào trong vòng 8 giây. Lần gửi thất bại được thử lại với cùng id, nên hãy loại trùng theo nó. Mỗi đoạn mã dưới đây đã được chạy với một lần gửi có chữ ký thật. Bạn chịu trách nhiệm cho mọi thứ bạn tự động hóa từ các sự kiện này.
Sự kiện
opportunity.alert: một tuyến khớp với quy tắc hoặc Cảnh báo mặc định.rule.idlàdefaultvới Cảnh báo mặc định.listing.alert: một thị trường vừa niêm yết, mỗi lần niêm yết một sự kiện.opportunity.closed: một tuyến bạn đã được cảnh báo đã đóng, nếu quy tắc yêu cầu.position.alert: có sự việc xảy ra trên một giao dịch đang theo dõi.notice.alert: một sàn đã thông báo hủy niêm yết, tạm ngưng nạp hoặc rút, bảo trì hoặc niêm yết, trên Cảnh báo mặc định.trackedcho biết việc này thuộc một giao dịch bạn đang theo dõi.watch.alert: một ví bạn giám sát đã nhận hoặc gửi một giao dịch chuyển, trên Cảnh báo mặc định.event.txlà null khi chưa biết hash giao dịch.alert.test: từ Gửi thử, với"test": truevà một coin giả tên 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", "...": "..." }
}Toàn bộ các trường có trong tài liệu webhook.
Kiểm tra chữ ký
Mọi request đều mang X-ArbitrageX-Timestamp và X-ArbitrageX-Signature, là sha256= cộng HMAC-SHA256 dạng hex của timestamp.rawBody. Kiểm tra trên byte thô, trước khi parse, vì JSON đã parse và mã hóa lại sẽ không khớp. Từ chối timestamp lệch quá 5 phút so với hiện tại, để chặn phát lại.
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")))
}Công cụ thực thi
Mỗi ví dụ dưới đây là body mà một công cụ yêu cầu, viết theo payload chuẩn của chúng tôi: mỗi giá trị trong ngoặc nhọn là trường của chúng tôi cần đặt vào đó. Tạo nó trong một bước Zapier hoặc Make sau catch webhook, hoặc trong relay của riêng bạn như bộ nhận ở mục Chạy script của riêng bạn, rồi gửi nó đến URL webhook của công cụ. Các giá trị viết hoa là của riêng bạn. Chỉ opportunity.alert và listing.alert nên đến một công cụ: lọc bỏ test và mọi loại khác, để một tin báo đóng không thể mở giao dịch. Chúng tôi không bao giờ giao dịch: công cụ làm gì với một tin nhắn là do bạn và công cụ đó quyết định.
Mỗi công cụ thay đổi các trường của nó theo thời gian. Hãy đối chiếu ví dụ với tài liệu hiện tại của công cụ trước khi dựa vào nó.
3Commas signal bot
Cho webhook tùy chỉnh của signal bot 3Commas, với secret và UUID của bot từ phần cài đặt tin nhắn của bot. Một signal bot giao dịch trên một sàn, nên hãy giới hạn quy tắc vào các tuyến mua ở sàn đó bằng bộ lọc sàn của quy tắc.
{
"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 nhận lệnh qua các giao diện riêng thay vì webhook, nên dữ liệu này đi đến một script strategy nhỏ của bạn, hoặc một cầu nối đến giao diện MQTT của nó. Dùng tên connector của chính bạn.
{
"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
Văn bản thuần theo định dạng tín hiệu của Cornix, cho địa chỉ webhook mà Cornix cấp cho một bot TradingView. Tự đặt stop của bạn, và đối chiếu định dạng với hướng dẫn hiện tại của 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
Tên các trường mà một cảnh báo TradingView gửi, cho các công cụ được xây dựng để nhận cảnh báo 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
Mọi số liệu ở cấp cao nhất, cho bảng tính, n8n hoặc cơ sở dữ liệu.
{
"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>"
}Kênh khác
Mọi kênh dưới đây được thiết lập trên trang Cảnh báo, có nút Gửi thử, và đọc giống tin nhắn Telegram, bằng ngôn ngữ của tài khoản. Quy tắc, thời gian chờ, giờ yên lặng, giới hạn tần suất và tự động tạm dừng hoạt động giống nhau trên tất cả các kênh.
- Slack (Pro Desk trở lên): thêm ứng dụng Incoming Webhooks vào một kênh trong Slack và dán URL
hooks.slack.commà nó cung cấp. - ntfy (Starter trở lên): thông báo miễn phí trên điện thoại và máy tính. Dán URL topic như
https://ntfy.sh/your-topic, hoặc một topic trên máy chủ của riêng bạn, và đăng ký cùng topic đó trong ứng dụng ntfy. Bất kỳ ai biết tên một topic công khai đều đọc được nó, nên hãy chọn tên dài và ngẫu nhiên, hoặc thêm access token cho topic được bảo vệ. Mỗi cảnh báo có nút cho cả hai sàn. - Email tổng hợp (Starter trở lên): một thư đến địa chỉ của tài khoản bạn với mọi cảnh báo kể từ thư trước, mỗi giờ vào đầu giờ hoặc mỗi ngày lúc 08:00 UTC. Không gửi gì khi không có gì mới.
WhatsApp không có cách miễn phí để một dịch vụ nhắn tin cho bạn, nên chúng tôi không cung cấp. Với mọi thứ khác, webhook tùy chỉnh là JSON thuần qua https.
Đăng lên Slack
Một Cloudflare Worker, không cần chạy máy chủ. Đặt ARBITRAGEX_SECRET và SLACK_WEBHOOK_URL làm secret, rồi dùng URL của Worker làm đích nhận. Cùng đoạn mã chạy được ở bất kỳ đâu có 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 });
},
};Ghi vào bảng tính
Thêm một dòng cho mỗi cảnh báo vào file CSV mà bạn mở được bằng bất kỳ bảng tính nào, chỉ với thư viện chuẩn của Python. Đặt nó sau https: chúng tôi chỉ gửi tới địa chỉ https công khai.
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()Chạy script của riêng bạn
Một bộ nhận không phụ thuộc thư viện nào, trả lời ngay và chuyển mỗi cảnh báo cho một hàm của bạn. Hàm đó làm gì là do bạn viết và bạn chịu trách nhiệm. Cảnh báo là ước tính từ dữ liệu thị trường, và một tuyến có thể đóng trước khi bất cứ việc gì bạn khởi động kịp đến nơi.
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 và Make
Webhook tùy chỉnh là một JSON POST thuần, nên URL catch webhook từ một trong hai dịch vụ đều dùng làm đích nhận được. Thêm bước lọc theo type và test, rồi ánh xạ các trường như opportunity.coin và opportunity.netBps. Các bước này không kiểm tra chữ ký của chúng tôi cho bạn, nên hãy coi URL catch như mật khẩu và đừng khởi động ở đó bất cứ việc gì mà bạn không để người lạ khởi động.
Telegram
Nếu bạn chỉ muốn nhận cảnh báo trên điện thoại, hãy bỏ qua tất cả phần này. Nhấn Kết nối Telegram trên trang Cảnh báo rồi nhấn Start trong cuộc trò chuyện vừa mở. Xem tài liệu cảnh báo.