본문으로 건너뛰기

실시간 하이퍼리퀴드 고래 알림 봇 만들기

업데이트 날짜:
2026년 4월 22일

읽는 데 24분 소요


하나의 스트림, 하나의 비용, 여러 목적지

이제 Quicknode 스트림을 통해 단일 파이프라인에서 여러 대상에 동시에 데이터를 전송할 수 있습니다. 중복 스트림이 발생하지 않으며, 구성 불일치도 없고, 대상당 추가 요금도 부과되지 않습니다. 스트림당 이용 가능한 대상 수는 요금제에 따라 다릅니다. 자세히 알아보기.

개요

종종 ‘고래’ 활동이라고 불리는 온체인 대규모 토큰 이동을 모니터링하면 시장 심리와 잠재적인 가격 변동에 대한 귀중한 통찰력을 얻을 수 있습니다.

이 가이드에서는 Hyperliquid 블록체인의 HYPE 토큰을 위한 실시간 고액 거래 알림 봇을 만드는 과정을 단계별로 안내합니다. 여러분은 대규모 거래를 감지할 뿐만 아니라, HyperCore의 실시간 USD 시세를 데이터에 반영하고 텔레그램 채널로 즉시 알림을 전송하는 시스템을 구축하게 될 것입니다. 이를 위해 Quicknode Streams의 강력한 기능과 효율성을 활용할 것입니다.

주요 업무


  • 만들기 퀵노드 스트림 HYPE를 걸러내는 이체 Hyperliquid EVM의 이벤트
  • 처리하기 전에 HMAC 서명을 통해 페이로드의 진위 여부를 확인하십시오
  • HyperEVM 프리컴파일을 통해 HyperCore의 HYPE 현물 가격을 확인하세요
  • 텔레그램 채널에 단계별 고래 알림(물고기, 돌고래, 고래)을 전송합니다

준비물


  • Hyperliquid EVM 엔드포인트가 연결된 Quicknode 계정
  • Node.js 20 이상, npm(또는 다른 패키지 관리자), 그리고 VS Code와 같은 코드 편집기
  • 텔레그램 계정 (텔레그램 봇을 만들고자 하는 경우)
  • 자바스크립트에 대한 기초 지식
  • ngrok이나 localtunnel 과 같이 로컬 서버를 인터넷에 노출시키는 도구 (로컬에서 웹훅을 테스트해야 하는 경우)
왜 퀵노드 스트림스인가요?

Quicknode Streams는 “푸시” 방식으로 작동합니다. 사용자가 블록체인에 새 데이터를 반복적으로 요청(폴링)하는 대신, Streams가 체인을 모니터링하여 이벤트가 발생하는 즉시 관련 데이터를 애플리케이션으로 푸시합니다.

이 접근 방식은 매우 효율적이며, 다음과 같은 몇 가지 주요 이점을 제공합니다:


  • 실시간 데이터: 폴링 주기로 인한 지연 없이 즉시 알림을 수신합니다.
  • 운영 비용 절감: 복잡하고 많은 자원을 소모하는 폴링 인프라를 관리할 필요가 없습니다.
  • 강력한 필터링: Quicknode 측에서 데이터를 처리하고 필터링하므로, 귀하의 서버는 꼭 필요한 정보만 수신하게 됩니다.
  • 비용 효율성: 전달된 이벤트당만 요금을 지불하므로, 실시간 데이터 모니터링을 위한 경제적인 솔루션입니다.

Whale Alert 봇 프로젝트

고래 경보 봇은 서로 연결된 여러 구성 요소로 이루어져 있으며, 이 구성 요소들이 함께 작동하여 실시간 알림을 제공합니다:

  1. 하이퍼리퀴드 블록체인: A 이체 이벤트 (Transfer(주소, 주소, uint256)) HYPE 토큰의 경우, 전송이 발생하면 발행됩니다.

  2. 필터링 기능이 적용된 Quicknode 스트림: 스트림은 체인을 지속적으로 모니터링하며, 우리가 정의한 필터 함수에 따라 해당 이벤트를 포착합니다.

  3. 웹훅 전송: Quicknode는 필터링된 페이로드를 보안이 적용된 POST 요청을 통해 당사 서버 엔드포인트로 전송합니다.

  4. Node.js 서버: 당사 서버는 데이터를 수신하고, 웹훅의 보안 토큰을 사용하여 데이터의 진위 여부를 확인한 뒤 이를 처리합니다.

  5. 가격 조회: 서버는 Hyperliquid의 HyperCore 사전 컴파일 계약을 호출하여 HYPE의 현재 USD 가격을 확인합니다.

  6. 텔레그램 봇: 마지막으로, 서버는 내용이 풍부하고 읽기 쉬운 메시지를 작성한 뒤 텔레그램 봇 API를 사용하여 지정된 채널로 알림을 전송합니다.

하이퍼리퀴드 고래 알림 봇 아키텍처

이것이 우리가 구현할 엔드투엔드 이벤트 흐름입니다. 자, 이제 Hyperliquid 고래 알림 봇을 만들어 보겠습니다.

1단계: 텔레그램 봇과 채널 만들기

먼저, 텔레그램 봇과 해당 봇이 알림을 게시할 수 있는 채널이 필요합니다.

BotFather로 봇 만들기


  1. 텔레그램을 열고 ‘BotFather’를 검색하세요.
  2. BotFather와 채팅을 시작하고 다음 명령어를 사용하세요. /newbot 새로운 봇을 생성하려면.
  3. 화면의 안내에 따라 봇의 이름과 사용자 이름을 설정하세요.
  4. BotFather가 봇 토큰을 제공해 드립니다. 이 토큰을 안전하게 보관해 두세요. 이 토큰은 .env 파일.

채널 만들기


  1. 텔레그램에서 새 채널을 만드세요. 공개 채널이나 비공개 채널로 설정할 수 있습니다.
  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을 복사하여 잘 보관해 두세요. 이 URL을 .env 나중에 이 파일을 처리합니다.

3단계: Quicknode 스트림 생성하기

자, 이제 Hyperliquid 블록체인을 모니터링할 Quicknode 스트림을 설정해 보겠습니다.

스트림 생성하기


  1. Quicknode 대시보드로 이동한 후, ‘스트림’ 섹션으로 이동하세요.
  2. ‘스트림 생성’을 클릭하고 블록체인으로 ‘Hyperliquid EVM 메인넷’을 선택하세요.
  3. 데이터셋으로 ‘영수증이 있는 블록’을 선택하세요
  4. ‘페이로드 사용자 지정’을 클릭하여 필터를 적용하고 ‘사용자 정의 필터 작성’ 옵션을 선택하세요.

필터 설정하기

이 가이드에서는 계층형 알림 로직을 구현하기 위해 사용자 정의 자바스크립트 함수를 만들어 보겠습니다.

우리가 사용할 이 함수는 새로운 블록마다 포함된 모든 트랜잭션을 검사합니다. 특히 표준 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"
}
]
}

보안 토큰을 가져오고 웹훅 URL을 설정하세요

다음 단계로 넘어가면 목적지를 선택하라는 메시지가 표시됩니다. 다음 중 하나를 선택하세요. 웹훅 을 목적지 유형으로 지정합니다. 그러면 Quicknode가 자동으로 보안 토큰 수신 요청이 정당한지 확인하기 위해 이 토큰을 다음 위치에 복사하십시오. .env 파일을 WEBHOOK_SECRET 변수.

스트림의 대상 URL로는 누구나 접근할 수 있는 엔드포인트가 필요합니다. 개발 단계에서는 다음을 사용할 수 있습니다. ngrok 또는 localtunnel 로컬 서버를 공개하려면 다음 명령을 실행할 수 있습니다. ngrok http 3000 (서버가 3000번 포트에서 실행된다고 가정할 때) 서버가 실행되면 HTTPS 전달 URL을 복사하세요. 다음을 추가하는 것을 잊지 마세요. /웹훅 그것에 (예: https://your-ngrok-id.ngrok.io/webhook) 이곳에서 웹훅 리스너를 구축하게 될 것이기 때문입니다.

서버가 아직 구축되지 않았으므로, 여기에서 잠시 중단하고 서버 구축이 완료된 후 다시 돌아와 스트림을 테스트하고 활성화하겠습니다.

4단계: 웹훅 서버 구축

자, 이제 웹훅에서 전송된 데이터를 수신하고 처리할 Node.js 애플리케이션을 만들어 보겠습니다.

프로젝트 설정 및 종속성

먼저, 프로젝트용 디렉터리를 생성하고 필요한 종속성을 설치하세요. 다음을 사용할 수 있습니다. npm 또는 그 밖의 다른 패키지 관리자(예: , pnpm, bun) 이를 수행하려면. 예를 들어:

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 // 텔레그램 봇 연동
└── .env // 환경 변수
└── .gitignore // Git 무시 목록 파일

터미널에서 다음 한 줄의 명령어를 실행하면 해당 파일들을 모두 즉시 생성할 수 있습니다:

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_BOT_TOKEN=your_telegram_bot_token
TELEGRAM_CHANNEL_ID=채널_ID

# 서버 구성
PORT=3000

# 스트림 웹훅 보안
WEBHOOK_SECRET=선택적_웹훅_시크릿

# Hyperliquid EVM RPC용 Quicknode 구성
HYPERLIQUID_RPC=https://your-endpoint.quiknode.pro/your-token/

# 환경
NODE_ENV=개발

코드 구현

.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 (하이퍼리퀴드의 가격 체계).

// 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

이 모듈에는 수신되는 웹훅을 검증하는 로직이 포함되어 있습니다. 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

이 모듈은 최종 메시지를 형식화하여 귀하의 텔레그램 채널로 전송합니다.

// 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 원시 바디 파서를 사용하여 /웹훅 서명을 검증할 수 있는 엔드포인트. 이를 통해 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 대시보드에서 웹훅 페이지로 돌아가세요.
  2. ngrok URL을 붙여넣으세요 (예: https://your-ngrok-id.ngrok.io/webhook)를 ‘웹훅 URL’ 필드에 입력한 후 저장합니다.
  3. 이제 ‘샘플 페이로드 보내기’ 버튼을 클릭하세요. Quicknode가 실행 중인 서버로 샘플 페이로드를 전송합니다.
  4. 서버의 콘솔 로그를 확인하고 텔레그램 채널에서 알림이 있는지 확인해 주세요.

다음은 최종 알림이 어떻게 표시될지 보여주는 예시입니다:

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 웹훅 설정에서 업데이트해야 합니다.

결론

축하합니다! Hyperliquid 블록체인을 위한, 실제 운영에 바로 투입 가능한 실시간 고래 알림 시스템을 성공적으로 구축하셨습니다. 온체인 데이터 처리를 위한 Quicknode Streams, 비즈니스 로직을 처리하는 보안이 강화된 Node.js 서버, 알림 전송을 위한 Telegram API의 장점을 결합함으로써, DeFi 생태계를 모니터링하는 데 유용한 도구를 만들어내셨습니다.

이 패턴은 유연성이 뛰어나므로, 사용 사례가 변화함에 따라 계층을 확장하거나 추가적인 온체인 컨텍스트를 반영하거나, 데이터 소스와 대상 위치를 변경할 수 있습니다.

다음 단계: 프로덕션 배포

한편 ngrok 개발용으로는 훌륭하지만, 운영 환경에서는 보다 안정적인 해결책이 필요합니다. 다음 위치에 애플리케이션을 배포하는 것을 고려해 보세요:


  • DigitalOcean이나 AWS와 같은 가상 사설 서버(VPS)를 PM2와 같은 프로세스 관리자를 사용하여 계속 가동시키는 것
  • Docker를 사용하는 컨테이너 기반 서비스
  • Heroku나 Render와 같은 PaaS(Platform-as-a-Service)

개선 방안

이 프로젝트는 여러분이 이를 바탕으로 더 발전시켜 나갈 수 있는 탄탄한 기반을 제공합니다. 봇을 한 단계 더 발전시킬 수 있는 몇 가지 아이디어를 소개합니다:

  • 다중 토큰 지원: 토큰 주소 배열을 받아들일 수 있도록 코드를 수정하세요. 이렇게 하면 서로 다른 임계값을 적용하여 여러 토큰을 모니터링하고, 그에 따라 알림을 보낼 수 있습니다.

  • 이력 데이터 대시보드: Quicknode Streams는 실시간 데이터뿐만 아니라 이력 데이터도 지원합니다. 수신된 전송 데이터를 데이터베이스(예: PostgreSQL, MongoDB)에 저장하세요. 그러면 웹 기반 dApp을 구축하여 이력 추세를 시각화하고, 특정 고래 지갑의 순유입량을 추적하며, 보다 심층적인 온체인 분석을 수행할 수 있습니다.

  • 알림 채널 추가: Discord 웹훅과 같은 다른 알림 서비스를 연동하세요.

  • 자동 거래 트리거: 보다 고급 사용 사례의 경우, 이러한 알림을 활용하여 온체인 작업을 실행할 수 있습니다. 예를 들어, 대규모 자금이 이체되면 스왑이 실행될 수 있습니다.

추가 자료


진행에 막히거나 궁금한 점이 있다면 저희 디스코드에 문의해 주세요. X(구 트위터) (@Quicknode)나 텔레그램 공지 채널을 팔로우하여 최신 소식을 받아보세요.

여러분의 피드백을 ❤️ 환영합니다!

의견이나 다루었으면 하는 새로운 주제가 있다면 알려주세요. 여러분의 의견을 기다리고 있습니다.

이 가이드를 공유하세요