Рецепты вебхуков
Оповещения говорят, что делает рынок. Вебхуки и Telegram — способ действовать с помощью своих инструментов и своих аккаунтов. Мы никогда не торгуем за вас, не храним средства и не запрашиваем API-ключи с правом торговли.
Перед началом
- На странице «Оповещения» добавьте свой JSON-вебхук. Он есть в VIP Quant и в пробном периоде.
- Скопируйте секрет подписи. Он показывается один раз.
- Нажмите «Отправить тест» и убедитесь, что ваш эндпоинт его получил.
- Направьте правило на этого получателя. Стандартное оповещение тоже идёт туда, пока оно включено.
Ответьте любым 2xx в течение 8 секунд. Неудачные отправки повторяются с тем же 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", "...": "..." }
}Все поля описаны в документации по вебхукам.
Проверка подписи
Каждый запрос содержит X-ArbitrageX-Timestamp и X-ArbitrageX-Signature — это sha256= плюс hex HMAC-SHA256 от timestamp.rawBody. Проверяйте её по сырым байтам до парсинга, так как разобранный и заново закодированный 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")))
}Инструменты исполнения
Каждый пример ниже — тело, которое ожидает инструмент, составленное по нашему стандартному payload: каждое значение в угловых скобках — наше поле, которое нужно туда подставить. Соберите его в шаге Zapier или Make после catch webhook или в собственном ретрансляторе, например в приёмнике из раздела «Свой скрипт», и отправьте на URL вебхука инструмента. Значения заглавными буквами — ваши собственные. До инструмента должны доходить только opportunity.alert и listing.alert: отфильтруйте test и все остальные типы, чтобы уведомление о закрытии не открыло сделку. Мы никогда не торгуем: что инструмент делает с сообщением, решаете вы и инструмент.
Каждый инструмент со временем меняет свои поля. Сверьте пример с актуальной документацией инструмента, прежде чем на него полагаться.
3Commas signal bot
Для пользовательского вебхука сигнального бота 3Commas, с его секретом и 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 принимает команды через собственные интерфейсы, а не через вебхук, поэтому это отправляется в вашу небольшую скриптовую стратегию или в мост к его 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. Задайте свой стоп и сверьте формат с актуальным руководством 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 и выше): добавьте приложение Incoming Webhooks в канал Slack и вставьте URL
hooks.slack.com, который оно выдаст. - ntfy (Starter и выше): бесплатные уведомления на телефоне и компьютере. Вставьте URL темы, например
https://ntfy.sh/your-topic, или темы на своём сервере и подпишитесь на ту же тему в приложении ntfy. Публичную тему может читать любой, кто знает её название, поэтому выберите длинное случайное название или добавьте токен доступа для защищённой темы. В каждом оповещении есть кнопки для обеих бирж. - Email-дайджест (Starter и выше): одно письмо на адрес вашего аккаунта со всеми оповещениями с прошлого письма — каждый час ровно или каждый день в 08:00 UTC. Если нового нет, ничего не отправляется.
У WhatsApp нет бесплатного способа, которым сервис мог бы вам писать, поэтому он не предлагается. Для всего остального подойдёт свой вебхук — обычный JSON по https.
Публикация в Slack
Cloudflare Worker без собственного сервера. Задайте ARBITRAGEX_SECRET и SLACK_WEBHOOK_URL как секреты, затем укажите URL Worker как получателя. Тот же код работает везде, где есть 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
Свой вебхук — это обычный JSON POST, поэтому catch-URL вебхука из любого из них подходит как получатель. Добавьте шаг фильтра по type и test, затем сопоставьте поля, например opportunity.coin и opportunity.netBps. Эти шаги не проверяют нашу подпись, поэтому относитесь к catch-URL как к паролю и не запускайте там ничего, что не позволили бы запустить постороннему.
Telegram
Если вам нужны только оповещения на телефон, пропустите всё это. Нажмите «Подключить Telegram» на странице «Оповещения», затем Start в открывшемся чате. См. документацию по оповещениям.