メインコンテンツへスキップ

リアルタイムの「Hyperliquid」クジラアラートボットを作成する

更新日:
2026年4月22日

読了時間:24分


1つのストリーム、1つのコスト、複数の配信先

Quicknode Streamでは、1つのパイプラインから複数の宛先へ同時に配信できるようになりました。ストリームの重複がなく、設定のずれも発生せず、宛先ごとの追加料金もかかりません。ストリームごとに利用可能な宛先はプランによって異なります。詳細はこちら

概要

オンチェーン上の大規模なトークン移動、いわゆる「クジラ」の活動を監視することで、市場のセンチメントや今後の価格動向に関する貴重な知見が得られる。

このガイドでは、Hyperliquidブロックチェーン上のHYPEトークン向けに、リアルタイムの「ホエールアラート」ボットを作成する手順を解説します。大規模な送金を検出するだけでなく、HyperCoreからのリアルタイムの米ドル相場データで情報を充実させ、Telegramチャンネルに即座に通知を送信するシステムを構築します。これを実現するために、Quicknode Streamsの強力な機能と効率性を活用します。

主な業務内容


  • [作成] Quicknode Stream HYPEをフィルタリングする 移籍 Hyperliquid EVMからのイベント
  • 処理を行う前に、HMAC署名を用いてペイロードの真正性を確認する
  • HyperEVMのプリコンパイルを介して、HyperCoreのHYPEスポット価格を確認する
  • Telegramチャンネルに、レベル別のクジラ情報(魚、イルカ、クジラ)を送信する

必要なもの


  • Hyperliquid EVMエンドポイントを備えたQuicknodeアカウント
  • Node.js20以降、npm(または他のパッケージマネージャー)、およびVS Codeなどのコードエディタ
  • Telegramアカウント(Telegramボットを作成したい場合)
  • JavaScriptの基礎知識
  • ローカルサーバーをインターネットに公開するためのツール。例えば、ngroklocaltunnelなど(ローカルで Webhook をテストする必要がある場合)
なぜQuicknode Streamsなのか?

Quicknode Streams は「プッシュ」モデルで動作します。ユーザーがブロックチェーンに繰り返し新しいデータを問い合わせる(ポーリング)のではなく、Streams が代わりにチェーンを監視し、イベントが発生した瞬間に、関連するデータをアプリケーションにプッシュします。

このアプローチは非常に効率的であり、いくつかの重要な利点があります:


  • リアルタイムデータ:ポーリングサイクルの遅延なしに、即座に通知を受け取ることができます。
  • オーバーヘッドの削減:複雑でリソースを大量に消費するポーリングインフラの管理の手間を省きます。
  • 強力なフィルタリング:Quicknode側でデータを処理・フィルタリングするため、お客様のサーバーには必要な情報のみが届きます。
  • コスト効率に優れる:配信されたイベントごとにのみ課金されるため、リアルタイムのデータ監視において予算に優しいソリューションとなっています。

Whale Alert ボット・プロジェクト

Whale Alertボットは、相互に連携してリアルタイムの通知を配信する、いくつかの相互接続されたコンポーネントで構成されています:

  1. ハイパーリキッド・ブロックチェーン: A 移籍 イベント (Transfer(アドレス, アドレス, uint256)) HYPEトークンの発行は、転送が行われた際に発生します。

  2. フィルタリング機能付きQuicknodeストリーム:このストリームはチェーンを常時監視しており、定義したフィルタ関数に基づいてこのイベントを捕捉します。

  3. Webhookの配信:Quicknodeは、フィルタリングされたペイロードを、安全なPOSTリクエストを介して当社のサーバーエンドポイントに送信します。

  4. Node.js サーバー:当社のサーバーはデータを受信し、Webhook のセキュリティトークンを使用してその正当性を検証した上で、処理を行います。

  5. 価格取得:サーバーは、Hyperliquid上のHyperCoreプリコンパイル契約を呼び出し、HYPEの現在の米ドル価格を取得します。

  6. Telegramボット:最後に、サーバーが読みやすいリッチなメッセージを作成し、TelegramボットAPIを使用して、指定されたチャンネルにアラートを送信します。

Hyperliquidのクジラアラートボットのアーキテクチャ

これが、これから実装するエンドツーエンドのイベントフローです。それでは、Hyperliquidの「Whale Alert」ボットの構築を始めましょう。

ステップ1:Telegramのボットとチャンネルを作成する

まず、Telegramのボットと、そのボットがアラートを投稿できるチャンネルが必要です。

BotFatherを使ってボットを作成する


  1. Telegramを開き、「BotFather」を検索してください。
  2. BotFatherとチャットを開始し、次のコマンドを使用してください /newbot 新しいボットを作成するには。
  3. 画面の指示に従って、ボットの名前とユーザー名を設定してください。
  4. BotFather からボットトークンが発行されます。このトークンは安全に保管してください。これは、 .env ファイル。

チャンネルの作成


  1. Telegramで新しいチャンネルを作成します。公開チャンネルでも非公開チャンネルでも構いません。
  2. 公開チャンネルの場合は、覚えやすいユーザー名(例:@hyperliquid_whales)を付けましょう。このユーザー名はあなたの TELEGRAM_CHANNEL_ID.
  3. プライベートチャンネルの場合は、そのチャンネルの数値のチャットIDが必要になります。このIDは、チャンネルからボットなどにメッセージを転送することで取得できます。 @JsonDumpCUBot、そしてそこで表示されるチャットIDを確認し(つまり、 forward_from_chat.id).

チャンネルにボットを追加する


  1. 新しく作成したチャンネルの設定を開いてください。
  2. ボットを追加し、管理者として設定してください。

これで、あなたの TELEGRAM_BOT_TOKEN そしてあなたの TELEGRAM_CHANNEL_ID.

ステップ 2: Quicknode Hyperliquid EVM エンドポイントを作成する

それでは、Hyperliquid Core と連携して HYPE の価格データを取得するために使用する Quicknode Hyperliquid EVM エンドポイントを作成しましょう。

まず、Quicknodeのアカウントが必要です。すでにアカウントをお持ちの場合は、ログインしてください。Quicknodeのダッシュボードにアクセスしたら:


  • 「エンドポイント」ページに移動します
  • [新しいエンドポイント] ボタンをクリックします
  • Hyperliquid EVM メインネットネットワークを選択してください
  • エンドポイントを作成する

エンドポイントの作成が完了したら、そのURLをコピーして手元に保管しておいてください。これを .env 後の手順でファイルを保存します。

ステップ 3: Quicknode ストリームを作成する

それでは、Hyperliquidブロックチェーンを監視するQuicknode Streamを設定しましょう。

ストリームを作成する


  1. Quicknodeダッシュボードにアクセスし、「ストリーム」セクションに移動してください。
  2. ストリームを作成」をクリックし、ブロックチェーンとして「Hyperliquid EVM Mainnet」を選択します。
  3. データセットとして「領収書付きブロック」を選択してください
  4. ペイロードをカスタマイズ」をクリックしてフィルターを適用し、「独自のフィルターを作成」オプションを選択してください

フィルタを設定する

このガイドでは、段階的なアラート処理ロジックを実装するために、カスタム JavaScript 関数を作成します。

ここで使用する関数は、新しいブロック内のすべてのトランザクションを検査します。具体的には、標準のERC20に準拠したイベントログを探します。 移籍 署名 (0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef) であり、その起源は HYPE トークン契約。例えば、 移籍 イベントの場合、topics 配列には送信者と受信者が含まれていますが、 log.data 金額が格納されています。当社のコードはこの情報をデコードし、その金額を閾値と比較してチェックを行い、送金額が一定額以上である場合にのみ、クリーンなデータペイロードを返します。この前処理は非常に効率的であり、サーバーが不要なデータにリソースを浪費することを防いでいます。

関数ボックスに、次のコードを貼り付けてください:

// Quicknode Stream Filter for Tiered Wrapped HYPE Token Transfers
function main(payload) {
// --- Configuration ---
// The specific token contract address for Wrapped HYPE
const WHYPE_ADDRESS = "0x5555555555555555555555555555555555555555";

// Define the thresholds for each tier (with 18 decimals)
const TIER_THRESHOLDS = {
whale: BigInt("10000000000000000000000"), // 10,000 HYPE
dolphin: BigInt("5000000000000000000000"), // 5,000 HYPE
small_fish: BigInt("1000000000000000000000"), // 1,000 HYPE
};

// --- Static Data ---
const TRANSFER_SIGNATURE =
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";

const { data } = payload;
const block = data[0].block;
const receipts = data[0].receipts || [];

const categorizedTransfers = [];

const blockNumber = parseInt(block.number, 16);
const blockTimestamp = parseInt(block.timestamp, 16);

for (const receipt of receipts) {
for (const log of receipt.logs || []) {
// Primary filter: Is this a Transfer event from the W-HYPE contract?
if (
log.address.toLowerCase() === WHYPE_ADDRESS &&
log.topics[0] === TRANSFER_SIGNATURE
) {
const transferValue = BigInt(log.data);
let tier = null;

// Tiering Logic: Check from the highest threshold down to the lowest.
if (transferValue >= TIER_THRESHOLDS.whale) {
tier = "whale";
} else if (transferValue >= TIER_THRESHOLDS.dolphin) {
tier = "dolphin";
} else if (transferValue >= TIER_THRESHOLDS.small_fish) {
tier = "small_fish";
}

// If the transfer meets any of our thresholds, process it.
if (tier) {
const fromAddress = "0x" + log.topics[1].slice(26);
const toAddress = "0x" + log.topics[2].slice(26);

categorizedTransfers.push({
tier: tier,
tokenContract: log.address,
from: fromAddress,
to: toAddress,
value: transferValue.toString(),
transactionHash: receipt.transactionHash,
blockNumber: blockNumber,
timestamp: blockTimestamp,
});
}
}
}
}

if (categorizedTransfers.length > 0) {
return {
largeTransfers: categorizedTransfers,
};
}

return null;
}

フィルターの性能をテストしよう

ブロックを選択してください(例: 12193297) を使用して、フィルタ条件をテストし、アラートが正しく発動することを確認してください。次のような、分類済みの転送を含むペイロードが表示されるはずです:

{
"largeTransfers": [
{
"blockNumber": 12193297,
"from": "0x7c97cd7b57b736c6ad74fae97c0e21e856251dcf",
"tier": "small_fish",
"timestamp": 1756245832,
"to": "0xaaa2851ec59f335c8c6b4db6738c94fd0305598a",
"tokenContract": "0x5555555555555555555555555555555555555555",
"transactionHash": "0xafe522067fca99d4b44030d82885cabb757943255b991b3f2e95564807dbe0f7",
"value": "2200000000000000000000"
}
]
}

セキュリティトークンを取得し、Webhook URLを設定する

次のステップに進むと、目的地を選択するよう求められます。選択してください Webhook を宛先タイプとして指定します。すると、Quicknode が自動的に セキュリティトークン 受信リクエストが正当であることを確認するためです。このトークンを .env ファイルの値を WEBHOOK_SECRET 変数。

ストリームの宛先URLには、一般に公開されているエンドポイントが必要です。開発中は、 ngrok または localtunnel ローカルサーバーを公開するには、次のコマンドを実行します。 ngrok http 3000 (サーバーがポート3000で動作している場合)サーバーが起動したら、HTTPS転送用URLをコピーしてください。必ず末尾に /webhook それについて(例: https://your-ngrok-id.ngrok.io/webhook) ここが、Webhookリスナーを構築する場所となるためです。

サーバーはまだ構築されていないため、ここでは一旦中断し、サーバーの構築後にストリームのテストと有効化を行うことにします

ステップ4:Webhookサーバーの構築

それでは、Webhookからのデータを受信して処理するNode.jsアプリケーションを作成しましょう。

プロジェクトの設定と依存関係

まず、プロジェクト用のディレクトリを作成し、必要な依存関係をインストールします。以下の方法を使用できます。 npm またはその他のパッケージマネージャー(例えば , pnpm, パン) これを行うには。例えば:

mkdir hyperliquid-whale-alert-bot && cd hyperliquid-whale-alert-bot
npm init -y
npm i express dotenv node-telegram-bot-api viem
npm i -D nodemon

次に、あなたの package.json 以下の内容を含める:

"type": "module",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
}

プロジェクトの構成

プロジェクトの基本構造を整えるために、以下のファイルを作成してください:

├── config.js           // 設定
├── index.js // メインのエントリポイント
├── priceService.js // 価格関連のロジック
├── security.js // セキュリティ関連のロジック
├── telegramService.js // Telegramボットの連携
└── .env // 環境変数
└── .gitignore // Gitの無視設定 ファイル

ターミナルで以下の1行のコマンドを実行すれば、これらのファイルをすべて瞬時に作成できます:

touch config.js index.js priceService.js security.js telegramService.js .env .gitignore

# 「touch」コマンドが利用できない場合は、以下のように実行してください:
# (echo > config.js) && (echo > index.js) && (echo > priceService.js) && (echo > security.js) && (echo > telegramService.js) && (echo > .env) && (echo > .gitignore)

環境変数

を更新する .env プロジェクトのルートディレクトリに、環境変数を保存するためのファイルを作成します。以下の変数を追加してください:

# Telegramの設定
TELEGRAM_BOT_TOKEN=あなたのTelegramボットトークン
TELEGRAM_CHANNEL_ID=あなたの_チャンネル_ID

# サーバー設定
PORT=3000

# ストリームの Webhook セキュリティ
WEBHOOK_SECRET=任意のWebhookシークレット

# Hyperliquid EVM RPC 用の Quicknode 設定
HYPERLIQUID_RPC=https://your-endpoint.quiknode.pro/your-token/

# 環境
NODE_ENV=development

コードの実装

.gitignore

重要なのは、 .gitignore 機密情報や不要なファイルをコミットしないように、プロジェクトにファイルを追加してください。次のファイルを作成してください。 .gitignore プロジェクトのルートディレクトリにファイルを作成し、以下の行を追加してください:

node_modules
.env
config.js

このファイルには、環境変数やその他の定数など、アプリケーションの設定情報が含まれています。

その スポット price precompile は次の場所にあります 0x...0808, 一方、 オラクル precompile の価格は 0x...0807。価格情報源に応じてどちらでも使用できます。このガイドでは スポット. 住所については、必ず公式文書や最新のガイドで確認してください。

import dotenv from "dotenv";

// Load environment variables
dotenv.config();

export const TIERS = {
whale: {
emoji: "🐋",
label: "WHALE",
},
dolphin: {
emoji: "🐬",
label: "DOLPHIN",
},
small_fish: {
emoji: "🐟",
label: "FISH",
},
};

export const EXPLORER = {
tx: "https://hypurrscan.io/tx/",
address: "https://hypurrscan.io/address/",
block: "https://hypurrscan.io/block/",
};

export const HYPERCORE = {
SPOT_PX_PRECOMPILE: "0x0000000000000000000000000000000000000808",
HYPE_SPOT_INDEX: 107, // Mainnet HYPE spot ID
RPC_URL: process.env.HYPERLIQUID_RPC || "https://api.hyperliquid.xyz/evm",
};

export const MESSAGE_DELAY_MS = 1000; // Delay between Telegram messages

export const PORT = process.env.PORT || 3000;
priceService.js

このサービスは、HyperCoreからHYPEの価格を取得する役割を担っています。

これを SPOT price は、32 バイトの ABI エンコードされたインデックスを使用して直接プリコンパイルされます。このプリコンパイルは、 uint64 ここで、小数点以下の桁数は資産の szDecimals (Hyperliquidの価格体系)。

// priceService.js
// Fetches HYPE price from HyperCore using precompile

import { createPublicClient, http, encodeAbiParameters, formatUnits } from "viem";
import { HYPERCORE } from "./config.js";

// Create viem client for HyperEVM
const client = createPublicClient({
transport: http(HYPERCORE.RPC_URL),
});

// Cache price for 30 seconds to avoid excessive RPC calls
let priceCache = {
price: null,
timestamp: 0,
};
const CACHE_DURATION = 30000; // 30 seconds

/**
* Fetches HYPE spot price from HyperCore precompile
* Price is returned with 6 decimals precision for HYPE
* Details: To convert to floating point numbers, divide the returned price by 10^(8 - base asset szDecimals) for spot
* Source: https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/hyperevm/interacting-with-hypercore
* @returns {Promise<number|null>} HYPE price in USD
*/
export async function getHypePrice() {
try {
// Check cache first
if (
priceCache.price &&
Date.now() - priceCache.timestamp < CACHE_DURATION
) {
console.log("Using cached HYPE price:", priceCache.price);
return priceCache.price;
}

// Encode the spot index as a uint32 parameter
const encodedIndex = encodeAbiParameters(
[{ name: "index", type: "uint32" }],
[HYPERCORE.HYPE_SPOT_INDEX]
);

// Call the spot price precompile
const result = await client.call({
to: HYPERCORE.SPOT_PX_PRECOMPILE,
data: encodedIndex,
});

// szDecimals for HYPE is 2.
const szDecimals = 2;

const priceRaw = BigInt(result.data);
const price = formatUnits(priceRaw, 8 - szDecimals); // Convert to decimal string

// Update cache
priceCache = {
price,
timestamp: Date.now(),
};

console.log(`Fetched HYPE price from HyperCore: $${price}`);
return price;
} catch (error) {
console.error("Error fetching HYPE price from HyperCore:", error);
// Return cached price if available, otherwise null
return priceCache.price || null;
}
}

/**
* Formats USD value based on HYPE amount and price
* @param {string} hypeAmount - HYPE amount as string
* @param {number} hypePrice - HYPE price in USD
* @returns {string} Formatted USD value
*/
export function formatUSD(hypeAmount, hypePrice) {
if (!hypePrice) return "";

const usdValue = parseFloat(hypeAmount) * hypePrice;
return `($${usdValue.toLocaleString("en-US", {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
})})`;
}
security.js

このモジュールには、受信したWebhookを検証するためのロジックが含まれています。Quicknodeでは、ヘッダーを使用したHMAC検証を推奨しています。 X-QN-Nonce, X-QN-タイムスタンプ、および X-QN-シグネチャー そして、このモジュールはその検証を実装しています。

詳細については、「ストリームの署名の検証」ガイドを参照してください。

// security.js
// Validates incoming webhook signatures from Quicknode

import crypto from "crypto";

/**
* Validates the webhook signature from Quicknode
* Based on Quicknode's HMAC-SHA256 signature validation
*
* @param {string} secretKey - The webhook secret key
* @param {string} payload - The request body as string
* @param {string} nonce - The nonce from headers
* @param {string} timestamp - The timestamp from headers
* @param {string} givenSignature - The signature from headers
* @returns {boolean} Whether the signature is valid
*/
export function validateWebhookSignature(
secretKey,
payload,
nonce,
timestamp,
givenSignature
) {
if (!secretKey || !nonce || !timestamp || !givenSignature) {
console.warn("⚠️ Missing required parameters for signature validation");
return false;
}

try {
// Concatenate nonce + timestamp + payload as strings
const signatureData = nonce + timestamp + payload;

// Convert to bytes
const signatureBytes = Buffer.from(signatureData);

// Create HMAC with secret key converted to bytes
const hmac = crypto.createHmac("sha256", Buffer.from(secretKey));
hmac.update(signatureBytes);
const computedSignature = hmac.digest("hex");

// Use timing-safe comparison to prevent timing attacks
const isValid = crypto.timingSafeEqual(
Buffer.from(computedSignature, "hex"),
Buffer.from(givenSignature, "hex")
);

if (isValid) {
console.log("✅ Webhook signature validated successfully");
} else {
console.error("❌ Invalid webhook signature");
}

return isValid;
} catch (error) {
console.error("Error validating webhook signature:", error);
return false;
}
}

/**
* Middleware for Express to validate webhook signatures
* Quicknode sends nonce, timestamp, and signature in headers
*/
export function webhookAuthMiddleware(req, res, next) {
// Skip validation if no secret is configured
const secretKey = process.env.WEBHOOK_SECRET;
if (!secretKey) {
console.log("ℹ️ Webhook secret not configured, skipping validation");
return next();
}

// Get Quicknode headers
const nonce = req.headers["x-qn-nonce"];
const timestamp = req.headers["x-qn-timestamp"];
const givenSignature = req.headers["x-qn-signature"];

if (!nonce || !timestamp || !givenSignature) {
console.error("🚫 Missing required Quicknode headers");
return res.status(400).json({
error: "Missing required headers",
message:
"x-qn-nonce, x-qn-timestamp, and x-qn-signature headers are required",
});
}

// Get the raw body as string
// Note: Express's JSON middleware already parsed the body, so we need to stringify it back
const payloadString = JSON.stringify(req.body);

// Validate the signature
const isValid = validateWebhookSignature(
secretKey,
payloadString,
nonce,
timestamp,
givenSignature
);

if (!isValid) {
console.error("🚫 Webhook validation failed");
return res.status(401).json({
error: "Invalid signature",
message: "The webhook signature could not be validated",
});
}

next();
}
telegramService.js

このモジュールは、最終的なメッセージをフォーマットして、Telegramチャンネルに送信します。

// telegramService.js
// Handles Telegram bot messaging

import TelegramBot from "node-telegram-bot-api";
import { formatEther } from "viem";
import { TIERS, EXPLORER, MESSAGE_DELAY_MS } from "./config.js";
import { getHypePrice, formatUSD } from "./priceService.js";

// Initialize Telegram bot
const bot = new TelegramBot(process.env.TELEGRAM_BOT_TOKEN, { polling: false });
const CHANNEL_ID = process.env.TELEGRAM_CHANNEL_ID;

/**
* Format an address for display
*/
function formatAddress(address) {
return `${address.slice(0, 6)}...${address.slice(-4)}`;
}

/**
* Format transaction hash for display
*/
function formatTxHash(hash) {
return `${hash.slice(0, 10)}...`;
}

/**
* Display time
*/
function getTime(timestamp) {
const date = new Date(timestamp * 1000);
return date.toLocaleString("en-US");
}

/**
* Create formatted Telegram message for a transfer
*/
async function createMessage(transfer) {
const tierConfig = TIERS[transfer.tier];
const formattedValue = formatEther(BigInt(transfer.value));
const hypePrice = await getHypePrice();
const usdValue = formatUSD(formattedValue, hypePrice);

// Create message with Markdown formatting
const message = `
${tierConfig.emoji} *${tierConfig.label} ALERT* ${tierConfig.emoji}

💰 *Amount:* \`${parseFloat(formattedValue).toLocaleString("en-US", {
maximumFractionDigits: 2,
})} HYPE\` ${usdValue}

📤 *From:* [${formatAddress(transfer.from)}](${EXPLORER.address}${
transfer.from
})
📥 *To:* [${formatAddress(transfer.to)}](${EXPLORER.address}${transfer.to})

🔗 *TX:* [${formatTxHash(transfer.transactionHash)}](${EXPLORER.tx}${
transfer.transactionHash
})
📦 *Block:* [#${transfer.blockNumber}](${EXPLORER.block}${transfer.blockNumber})
⏰ *Time:* ${getTime(transfer.timestamp)}

Powered by [Hyperliquid](https://hyperliquid.xyz) & [Quicknode Streams](https://www.quicknode.com/streams)`;

return message;
}

/**
* Send message to Telegram with retry logic
*/
export async function sendMessage(message, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
await bot.sendMessage(CHANNEL_ID, message, {
parse_mode: "Markdown",
disable_web_page_preview: true,
});

console.log("✅ Message sent to Telegram successfully");
return true;
} catch (error) {
console.error(`❌ Telegram send attempt ${i + 1} failed:`, error.message);

if (i < retries - 1) {
// Wait before retrying (exponential backoff)
await new Promise((resolve) =>
setTimeout(resolve, 1000 * Math.pow(2, i))
);
}
}
}

return false;
}

/**
* Process and send alerts to Telegram
*/
export async function processAlerts(transfers) {
console.log(`📨 Processing ${transfers.length} transfers for Telegram...`);

for (const transfer of transfers) {
const message = await createMessage(transfer);
const sent = await sendMessage(message);

if (!sent) {
console.error(
"Failed to send message for transfer:",
transfer.transactionHash
);
}

// Rate limiting between messages
if (transfers.indexOf(transfer) < transfers.length - 1) {
await new Promise((resolve) => setTimeout(resolve, MESSAGE_DELAY_MS));
}
}

// Send summary if there are multiple transfers
if (transfers.length > 3) {
const summaryMessage = `
📊 *Batch Summary*

Total transfers: ${transfers.length}
🐋 Whales: ${transfers.filter((t) => t.tier === "whale").length}
🐬 Dolphins: ${transfers.filter((t) => t.tier === "dolphin").length}
🐟 Fish: ${transfers.filter((t) => t.tier === "small_fish").length}

Block: #${transfers[0].blockNumber}
`;
await sendMessage(summaryMessage);
}
}
index.js

これは、すべてを結びつける中心となるファイルです。

私たちは、JSONミドルウェアのどの処理よりも前に、Expressのrawボディパーサーを使用しています。 /webhook 署名を検証できるようにするためのエンドポイント。これにより、HMAC検証のために、本文が元の形式のまま利用可能になることが保証されます。

// server.js
// Main webhook server for Hyperliquid whale alerts

import express from "express";
import dotenv from "dotenv";
import { PORT, HYPERCORE } from "./config.js";
import { processAlerts } from "./telegramService.js";
import { webhookAuthMiddleware } from "./security.js";

// Load environment variables
dotenv.config();

// Initialize Express app
const app = express();

// Custom middleware to capture raw body for signature validation
app.use((req, res, next) => {
if (req.path === '/webhook' && process.env.WEBHOOK_SECRET) {
// For webhook endpoint with security enabled, capture raw body
let rawBody = '';
req.setEncoding('utf8');

req.on('data', (chunk) => {
rawBody += chunk;
});

req.on('end', () => {
req.rawBody = rawBody;

// Parse JSON body
try {
req.body = JSON.parse(rawBody);
} catch (error) {
return res.status(400).json({ error: 'Invalid JSON payload' });
}

next();
});
} else {
// For other endpoints or when security is disabled, use normal JSON parsing
express.json()(req, res, next);
}
});

// Main webhook endpoint with security validation
app.post("/webhook", webhookAuthMiddleware, async (req, res) => {
try {
console.log("📨 Webhook received at", new Date().toISOString());

const { largeTransfers } = req.body;

if (!largeTransfers || largeTransfers.length === 0) {
console.log("No large transfers in this webhook");
return res.json({ success: true, processed: 0 });
}

console.log(`Processing ${largeTransfers.length} transfers...`);

// Send alerts to Telegram
await processAlerts(largeTransfers);

console.log(`✅ Processed ${largeTransfers.length} transfers successfully`);

res.json({
success: true,
processed: largeTransfers.length,
});
} catch (error) {
console.error("❌ Webhook processing error:", error);
res.status(500).json({
success: false,
error: error.message,
});
}
});

// Health check endpoint
app.get("/health", (req, res) => {
res.json({
status: "healthy",
timestamp: new Date().toISOString(),
environment: {
telegramConfigured: !!process.env.TELEGRAM_BOT_TOKEN,
webhookSecretConfigured: !!process.env.WEBHOOK_SECRET,
},
});
});

// Error handling middleware
app.use((error, req, res, next) => {
console.error("Unhandled error:", error);
res.status(500).json({
success: false,
error: "Internal server error",
});
});

// Start server
app.listen(PORT, () => {
console.log(`
╔════════════════════════════════════════════╗
║ Hyperliquid Whale Alert Server Started ║
╠════════════════════════════════════════════╣
║ Port: ${PORT}
║ Webhook: http://localhost:${PORT}/webhook ║
║ Health: http://localhost:${PORT}/health ║
╚════════════════════════════════════════════╝

Configuration:
- Telegram Bot: ${
process.env.TELEGRAM_BOT_TOKEN ? "✅ Configured" : "❌ Not configured"
}
- Telegram Channel: ${process.env.TELEGRAM_CHANNEL_ID || "Not configured"}
- Webhook Secret: ${
process.env.WEBHOOK_SECRET
? "✅ Configured"
: "⚠️ Not configured (validation disabled)"
}
- HyperCore RPC: ${HYPERCORE.RPC_URL}

Ready to receive webhooks...
`);
});

// Graceful shutdown
process.on("SIGTERM", () => {
console.log("SIGTERM received, shutting down gracefully...");
process.exit(0);
});

process.on("SIGINT", () => {
console.log("SIGINT received, shutting down gracefully...");
process.exit(0);
});

ステップ 5:システムの実行とテスト

これで、ボットを稼働させる準備が整いました。

サーバーを起動する

ターミナルで次のコマンドを実行して、サーバーを起動してください。 nodemon ファイルが変更された際に、サーバーが自動的に再起動するよう設定します。

# nodemon を使って開発モードで起動する
npm run dev

ローカルホストを公開する

まだ行っていない場合は、新しいターミナルウィンドウを開いて、次のコマンドを実行してください。 ngrok http 3000. HTTPS転送用URLをコピーしてください。

ストリームをテストする


  1. QuicknodeダッシュボードのWebhookページに戻ってください。
  2. ngrokのURLを貼り付けてください(例: https://your-ngrok-id.ngrok.io/webhook) を「Webhook URL」フィールドに入力して、保存します。
  3. それでは、「サンプルペイロードを送信」ボタンをクリックしてください。Quicknodeが、実行中のサーバーにサンプルペイロードを送信します。
  4. サーバーのコンソールログを確認し、Telegramチャンネルでアラートがないか確認してください。

以下は、最終的なアラートがどのようなものになるかの例です:

Hyperliquid Whale Bot - メッセージ例

ストリームを有効にする

すべてが正常に動作していることを確認したら、「ストリームを作成」ボタンをクリックしてストリームを作成してください。これでボットが稼働を開始し、HYPEでの新しい送金をすべてリアルタイムで監視するようになります。

トラブルシューティング

場合によっては、サーバーや ngrok 正常にシャットダウンされない場合、再起動しようとすると「アドレスがすでに使用されています」といったエラーが発生することがあります。ここでは、その問題を素早く解決する方法をご紹介します。

手順 1:ポートを解放する

まず、ポート番号からプロセスID(PID)を特定し、そのプロセスを停止させます。以下のコマンドはmacOS/Linux用です。お使いのOSによって異なる場合があります。

# Find the Process ID (PID) using port 3004
lsof -i :3004

# Replace <PID> with the number you found and run:
kill -9 <PID>

ステップ 2:再起動と更新

サーバーを再起動して、 ngrok. 重要: ngrok 起動するたびに新しいURLが生成されます。この新しいURLをコピーし、QuicknodeのWebhook設定で更新する必要があります。

結論

おめでとうございます!Hyperliquidブロックチェーン向けの、本番環境対応のリアルタイム・ホエールアラートシステムの構築に成功しました。オンチェーンデータ処理のためのQuicknode Streams、ビジネスロジック処理のためのセキュアなNode.jsサーバー、そして通知機能のためのTelegram APIを組み合わせることで、DeFiエコシステムを監視するための貴重なツールを構築しました。

このパターンは柔軟性が高く、ユースケースの変化に応じて、階層を拡張したり、追加のオンチェーンコンテキストを盛り込んだり、データソースや宛先を切り替えたりすることができます。

次のステップ:本番環境への展開

一方、 ngrok 開発には最適ですが、本番環境ではより恒久的な解決策が必要になります。アプリケーションを以下の場所にデプロイすることを検討してください:


  • DigitalOcean や AWS などの仮想プライベートサーバー(VPS)を、PM2 などのプロセスマネージャーを使用して稼働させ続ける
  • Docker を使用したコンテナ化されたサービス
  • Heroku や Render のような Platform-as-a-Service(PaaS)

改善の余地

このプロジェクトは、さらに発展させることができる強固な基盤を提供します。ボットを次のレベルへと引き上げるためのアイデアをいくつかご紹介します:

  • マルチトークン対応:トークンアドレスの配列を受け入れられるようコードを修正してください。これにより、異なる閾値で複数のトークンを監視し、それに応じてアラートを送信できるようになります。

  • 履歴データダッシュボード:Quicknode Streamsは、リアルタイムデータだけでなく、履歴データもサポートしています。受信した送金データをデータベース(PostgreSQL、MongoDBなど)に保存します。これにより、WebベースのdAppを構築して、過去の傾向を可視化したり、特定の「クジラ」ウォレットの純流入を追跡したり、より詳細なオンチェーン分析を行ったりすることが可能になります。

  • 通知チャネルを追加する:DiscordのWebhookなど、他の通知サービスを連携させる。

  • 自動取引のトリガー:より高度なユースケースとして、これらのアラートをオンチェーンアクションのトリガーとして利用することができます。例えば、大規模な送金がスワップのトリガーとなるといった具合です。

その他の参考資料


行き詰まったり質問がある場合は、Discordまでお気軽にご連絡ください。最新情報は、X(旧Twitter)のアカウント(@Quicknode)やTelegramの告知チャンネルをフォローしてご確認ください。

皆様からのフィードバックを心よりお待ちしております!❤️

ご意見や新しいトピックに関するご要望などがありましたら、ぜひお知らせください。皆様からのご連絡を心よりお待ちしております。

このガイドをシェアする