웹훅 레시피
알림은 시장 상황을 알려줍니다. 웹훅과 Telegram은 본인의 도구와 계정으로 이에 대응하는 방법입니다. 저희는 대신 거래하지 않으며, 자금을 보관하지 않고, 거래 권한이 있는 API 키를 요청하지 않습니다.
시작하기 전에
- 알림 페이지에서 커스텀 JSON 웹훅을 추가하세요. 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", "...": "..." }
}전체 필드는 웹훅 문서에 있습니다.
서명 확인
모든 요청에는 X-ArbitrageX-Timestamp와 X-ArbitrageX-Signature가 포함되며, 후자는 sha256= 뒤에 timestamp.rawBody의 HMAC-SHA256 16진수 값을 붙인 것입니다. 파싱 후 다시 인코딩한 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")))
}실행 도구
아래 각 예제는 도구가 기대하는 본문으로, 저희 표준 페이로드를 기준으로 작성했습니다: 꺾쇠괄호 안의 각 값은 그 자리에 넣을 저희 필드입니다. catch webhook 다음의 Zapier 또는 Make 단계에서, 또는 자체 스크립트 실행의 수신기 같은 자체 릴레이에서 만들어 도구의 웹훅 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 이상): Slack에서 채널에 Incoming Webhooks 앱을 추가하고, 앱이 제공하는
hooks.slack.comURL을 붙여넣으세요. - ntfy (Starter 이상): 휴대폰과 데스크톱에서 받는 무료 알림.
https://ntfy.sh/your-topic같은 토픽 URL이나 자체 서버의 토픽 URL을 붙여넣고, ntfy 앱에서 같은 토픽을 구독하세요. 공개 토픽은 이름을 아는 누구나 읽을 수 있으므로 길고 무작위한 이름을 고르거나, 보호된 토픽이라면 액세스 토큰을 추가하세요. 각 알림에는 두 거래소로 가는 버튼이 있습니다. - 이메일 다이제스트 (Starter 이상): 마지막 발송 이후의 모든 알림을 계정 주소로 한 통에 담아, 매시 정각 또는 매일 08:00 UTC에 보냅니다. 새로운 것이 없으면 아무것도 보내지 않습니다.
WhatsApp은 서비스가 무료로 메시지를 보낼 방법이 없어 제공하지 않습니다. 그 밖의 경우에는 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 });
},
};스프레드시트 기록
Python 표준 라이브러리만으로 알림마다 한 줄씩 CSV에 추가하며, 어떤 스프레드시트에서든 열 수 있습니다. 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 webhook URL도 수신처로 사용할 수 있습니다. type과 test에 필터 단계를 추가한 다음, opportunity.coin과 opportunity.netBps 같은 필드를 매핑하세요. 이 단계들은 서명을 확인해 주지 않으므로, catch URL을 비밀번호처럼 다루고 낯선 사람이 시작해도 괜찮지 않은 작업은 그곳에서 시작하지 마세요.
Telegram
휴대폰으로 알림만 받고 싶다면 이 모든 과정을 건너뛰세요. 알림 페이지에서 Telegram 연결을 누른 다음, 열리는 채팅에서 시작을 누르세요. 알림 문서를 참고하세요.