Webhookレシピ
アラートは市場の動きを伝えます。WebhookとTelegramは、ご自身のツールとアカウントでそれに対応するための手段です。私たちが代わりに取引することはなく、資金を預かることも、取引権限のあるAPIキーを求めることもありません。
始める前に
- アラートページで、カスタムJSON Webhookを追加します。VIP Quantとトライアルに含まれます。
- 署名シークレットをコピーしてください。一度だけ表示されます。
- 「テスト送信」を押し、エンドポイントが受信することを確認します。
- ルールの送信先に設定します。既定のアラートも、オンの間はそこに送信されます。
8秒以内に任意の2xxで応答してください。送信に失敗した場合は同じidで再試行されるため、それで重複を排除してください。以下の各スニペットは、実際の署名付き配信で動作確認済みです。これらのイベントから自動化したものについては、お客様の責任となります。
イベント
opportunity.alert:ルートがルールまたは既定のアラートに一致しました。既定のアラートのrule.idはdefaultです。listing.alert:市場が新規上場しました。上場ごとに1イベントです。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(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")))
}実行ツール
以下の各例は、ツールが受け付けるボディを当社の標準ペイロードに基づいて記述したものです:山括弧内の各値は、そこに入れる当社のフィールドです。キャッチWebhookの後のZapierまたはMakeのステップ、または「独自スクリプトを実行する」の受信プログラムのような独自のリレーで組み立て、ツールのWebhook URLにPOSTしてください。大文字の値はご自身のものです。ツールに届けるべきなのはopportunity.alertとlisting.alertだけです:testとその他すべてのタイプは除外し、終了の通知で取引が開始されないようにしてください。私たちは取引を行いません:ツールがメッセージで何をするかは、お客様とツール次第です。
各ツールは自身のフィールドを随時変更します。利用する前に、例をツールの最新のドキュメントと照らし合わせてください。
3Commas signal bot
3Commasのシグナルボットのカスタムwebhook向けで、シークレットとボットUUIDはボットのメッセージ設定から取得します。シグナルボットは1つの取引所で取引するため、ルールの取引所フィルターで、その取引所で買うルートに限定してください。
{
"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.comのURLを貼り付けます。 - ntfy(Starter以上):スマートフォンとデスクトップへの無料通知。
https://ntfy.sh/your-topicのようなトピックURL、またはご自身のサーバーのURLを貼り付け、ntfyアプリで同じトピックを購読します。公開トピックは名前を知っていれば誰でも読めるため、長くランダムな名前を選ぶか、保護されたトピックにアクセストークンを追加してください。各アラートには両方の取引所へのボタンがあります。 - メールダイジェスト(Starter以上):前回以降のすべてのアラートをまとめた1通のメールを、アカウントのアドレスに毎時0分または毎日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 });
},
};スプレッドシートに記録する
Pythonの標準ライブラリだけで、アラートごとに1行をCSVに追記します。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
カスタムWebhookはシンプルなJSONのPOSTなので、どちらのキャッチWebhook URLも送信先として使えます。typeとtestでフィルターステップを追加し、opportunity.coinやopportunity.netBpsなどのフィールドをマッピングしてください。これらのステップは署名を確認しないため、キャッチURLはパスワードとして扱い、見知らぬ人に開始させたくない処理はそこから開始しないでください。
Telegram
スマートフォンでアラートを受け取るだけなら、これらはすべて不要です。アラートページで「Telegramを接続」を押し、開いたチャットで「開始」を押してください。アラートのドキュメントをご覧ください。