연결
WebSocket 엔드포인트
wss://realtime-dex.chainstream.io/connection/websocket
인증
연결 수립 시 URL에 Access Token을 제공합니다:wss://realtime-dex.chainstream.io/connection/websocket?token=YOUR_ACCESS_TOKEN
- SDK 사용 (추천)
- 네이티브 WebSocket 사용
- 커맨드 라인 테스트
SDK가 연결과 인증을 자동으로 처리합니다. subscribe 메서드를 호출하기만 하면 됩니다:
import { ChainStreamClient } from '@chainstream-io/sdk';
import { Resolution } from '@chainstream-io/sdk/openapi';
const client = new ChainStreamClient(process.env.CHAINSTREAM_ACCESS_TOKEN);
// 직접 구독, SDK가 연결과 인증을 자동으로 처리
client.stream.subscribeTokenCandles({
chain: 'sol',
tokenAddress: '6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN',
resolution: Resolution._1m,
callback: (data) => {
console.log('Received data:', data);
}
});
SDK는 연결 상태를 자동으로 감지하고 필요할 때 연결을 수립합니다. 수동으로
connect()를 호출할 필요가 없습니다.네이티브 WebSocket 사용 시 연결 후
connect 메시지를 보내 인증을 완료합니다:const token = process.env.CHAINSTREAM_ACCESS_TOKEN;
const ws = new WebSocket(
`wss://realtime-dex.chainstream.io/connection/websocket?token=${token}`
);
ws.onopen = () => {
console.log('WebSocket connection established');
// connect 메시지를 보내 인증 완료
ws.send(JSON.stringify({
connect: {
token: token,
name: 'js'
},
id: 1
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// connect 응답 처리
if (data.connect) {
console.log('✅ Authentication successful, client ID:', data.connect.client);
// 인증 후 구독 시작
ws.send(JSON.stringify({
subscribe: {
channel: 'dex-candle:sol_6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN_1m'
},
id: 2
}));
}
// 구독 데이터 처리
if (data.push) {
console.log('Received data:', data.push.pub.data);
}
};
wscat으로 테스트:wscat -c "wss://realtime-dex.chainstream.io/connection/websocket?token=YOUR_ACCESS_TOKEN"
{"connect":{"token":"YOUR_ACCESS_TOKEN","name":"test"},"id":1}
연결 응답
인증 성공 시 다음과 같은 응답을 받게 됩니다:{
"id": 1,
"connect": {
"client": "0f819f5f-7d8b-4949-9433-0e91bbfe1cdb",
"version": "0.0.0 OSS",
"expires": true,
"ttl": 86002,
"ping": 25,
"pong": true
}
}
| 필드 | 설명 |
|---|---|
client | 고유 클라이언트 식별자 |
ttl | 토큰 잔여 유효시간 (초) |
ping | 하트비트 간격 (초) |
pong | pong 응답 지원 여부 |
구독 유형
ChainStream WebSocket은 다양한 데이터 구독 유형을 지원합니다:| 카테고리 | 채널 접두사 | 설명 |
|---|---|---|
| 캔들 (USD) | dex-candle: | 토큰 캔들 (토큰 / 풀 / 페어 변형) |
| 캔들 (네이티브) | dex-candle-in-native: | 위와 동일하나 체인의 네이티브 자산으로 가격 표시 |
| 풀 캔들 | dex-pool-candle: / dex-pair-candle: | 풀 기준 또는 페어 기준 캔들 |
| 토큰 통계 | dex-token-stats: | 다중 시간 창 거래 통계 (1m, 5m, … 1W, 1M) |
| 홀더 통계 | dex-token-holding: | 홀더 분포 및 잔고 태그 |
| 토큰 공급 | dex-token-supply: | 공급량 및 시가총액 업데이트 |
| 유동성 | dex-token-liquidity: / dex-token-total-liquidity: | 최대 풀 / 전체 유동성 |
| 신규 토큰 | dex-new-token: / dex-new-tokens: / dex-new-tokens-metadata: | 신규 상장 (개별 이벤트 / 배치 / 메타데이터) |
| 토큰 거래 | dex-trade: | 토큰 주소로 필터링된 거래 |
| 지갑 잔고 | dex-wallet-balance: | 지갑 토큰 잔고 변동 |
| 지갑 거래 | dex-wallet-trade: | 지갑 주소로 필터링된 거래 |
| 지갑 PnL | dex-wallet-token-pnl: / dex-wallet-pnl-list: | 토큰별 / 집계 지갑 PnL |
| 랭킹 | dex-ranking-list: / dex-ranking-token-stats-list: / dex-ranking-token-holding-list: / dex-ranking-token-supply-list: / dex-ranking-token-bounding-curve-list: | 랭킹 멤버십 + 토큰별 통계 |
| DEX 풀 | dex-pool-balance: | 풀 유동성 스냅샷 |
전체 구독 유형, 파라미터, 응답 형식은 WebSocket API 레퍼런스를 참고하세요. SDK(
client.stream.subscribeTokenCandles, subscribeTokenStats, subscribeTokenTrade, …)는 이러한 채널 문자열을 래핑하므로 직접 구성할 필요가 없습니다.구독 형식 예시
// 캔들 데이터 구독
ws.send(JSON.stringify({
subscribe: {
channel: 'dex-candle:sol_6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN_1m'
},
id: 2
}));
// 토큰 통계 구독
ws.send(JSON.stringify({
subscribe: {
channel: 'dex-token-stats:sol_6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN'
},
id: 3
}));
// 새 토큰 구독
ws.send(JSON.stringify({
subscribe: {
channel: 'dex-new-token:sol'
},
id: 4
}));
구독 해제
ws.send(JSON.stringify({
unsubscribe: {
channel: 'dex-candle:sol_6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN_1m'
},
id: 5
}));
메시지 형식
요청 메시지
Connect 메시지 (인증):{
"connect": {
"token": "YOUR_ACCESS_TOKEN",
"name": "client_name"
},
"id": 1
}
{
"subscribe": {
"channel": "dex-candle:sol_xxx_1m"
},
"id": 2
}
{
"unsubscribe": {
"channel": "dex-candle:sol_xxx_1m"
},
"id": 3
}
응답 메시지
구독 확인:{
"id": 2,
"subscribe": {}
}
{
"push": {
"channel": "dex-candle:sol_xxx_1m",
"pub": {
"data": {
"o": 0.001234,
"c": 0.001256,
"h": 0.001280,
"l": 0.001200,
"v": 1234567,
"t": 1706745600
}
}
}
}
{
"id": 2,
"error": {
"code": 100,
"message": "invalid channel"
}
}
하트비트
WebSocket 연결을 유지하려면 주기적인 하트비트 메시지가 필요합니다. connect 응답의ping 필드(보통 25초)를 기준으로 이 간격 내에 하트비트를 보내세요:
// 하트비트 메시지
ws.send(JSON.stringify({}));
// 또는 ping 전송
ws.send(JSON.stringify({ ping: {} }));
지정된 시간 내(보통 ping 간격의 3배)에 메시지가 전송되지 않으면 서버가 연결을 끊습니다.
전체 예시
const WebSocket = require('ws');
class ChainStreamWebSocket {
constructor(accessToken) {
this.accessToken = accessToken;
this.ws = null;
this.messageId = 0;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.subscriptions = new Set();
this.pingInterval = null;
}
connect() {
const url = `wss://realtime-dex.chainstream.io/connection/websocket?token=${this.accessToken}`;
this.ws = new WebSocket(url);
this.ws.onopen = () => {
console.log('WebSocket connection established');
this.reconnectAttempts = 0;
// connect 메시지 전송
this.send({
connect: {
token: this.accessToken,
name: 'nodejs'
}
});
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handleMessage(data);
};
this.ws.onclose = (event) => {
console.log(`Connection closed: ${event.code}`);
this.stopPing();
if (event.code !== 1000) {
this.scheduleReconnect();
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error.message);
};
}
handleMessage(data) {
// connect 응답 처리
if (data.connect) {
console.log('✅ Authentication successful');
this.startPing(data.connect.ping || 25);
this.resubscribe();
return;
}
// 데이터 푸시 처리
if (data.push) {
console.log(`[${data.push.channel}]`, data.push.pub.data);
return;
}
// 오류 처리
if (data.error) {
console.error('Error:', data.error.message);
return;
}
}
send(message) {
message.id = ++this.messageId;
this.ws.send(JSON.stringify(message));
}
subscribe(channel) {
this.subscriptions.add(channel);
this.send({ subscribe: { channel } });
}
unsubscribe(channel) {
this.subscriptions.delete(channel);
this.send({ unsubscribe: { channel } });
}
resubscribe() {
this.subscriptions.forEach(channel => {
this.send({ subscribe: { channel } });
});
}
startPing(interval) {
this.pingInterval = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send('{}');
}
}, interval * 1000);
}
stopPing() {
if (this.pingInterval) {
clearInterval(this.pingInterval);
this.pingInterval = null;
}
}
scheduleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('❌ Max reconnect attempts reached');
return;
}
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
console.log(`⏳ Reconnecting in ${delay}ms... (attempt ${this.reconnectAttempts + 1})`);
this.reconnectAttempts++;
setTimeout(() => this.connect(), delay);
}
close() {
this.stopPing();
if (this.ws) {
this.ws.close(1000, 'Normal closure');
}
}
}
// 사용 예시
const client = new ChainStreamWebSocket(process.env.CHAINSTREAM_ACCESS_TOKEN);
client.connect();
// 연결 수립 후 구독
setTimeout(() => {
client.subscribe('dex-candle:sol_6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN_1m');
client.subscribe('dex-new-token:sol');
}, 1000);
import asyncio
import websockets
import json
import os
class ChainStreamWebSocket:
def __init__(self, access_token):
self.access_token = access_token
self.ws = None
self.message_id = 0
self.subscriptions = set()
self.ping_interval = 25
async def connect(self):
url = f"wss://realtime-dex.chainstream.io/connection/websocket?token={self.access_token}"
async with websockets.connect(url) as ws:
self.ws = ws
print("WebSocket connection established")
# connect 메시지 전송
await self.send({
"connect": {
"token": self.access_token,
"name": "python"
}
})
# 하트비트와 메시지 수신 시작
await asyncio.gather(
self.heartbeat(),
self.receive()
)
async def send(self, message):
self.message_id += 1
message["id"] = self.message_id
await self.ws.send(json.dumps(message))
async def receive(self):
async for message in self.ws:
data = json.loads(message)
await self.handle_message(data)
async def handle_message(self, data):
if "connect" in data:
print("✅ Authentication successful")
self.ping_interval = data["connect"].get("ping", 25)
await self.resubscribe()
elif "push" in data:
channel = data["push"]["channel"]
pub_data = data["push"]["pub"]["data"]
print(f"[{channel}]", pub_data)
elif "error" in data:
print(f"Error: {data['error']['message']}")
async def subscribe(self, channel):
self.subscriptions.add(channel)
await self.send({"subscribe": {"channel": channel}})
async def resubscribe(self):
for channel in self.subscriptions:
await self.send({"subscribe": {"channel": channel}})
async def heartbeat(self):
while True:
await asyncio.sleep(self.ping_interval)
if self.ws and self.ws.open:
await self.ws.send("{}")
# 사용 예시
async def main():
client = ChainStreamWebSocket(os.environ["CHAINSTREAM_ACCESS_TOKEN"])
# 구독 사전 추가
client.subscriptions.add("dex-candle:sol_6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN_1m")
client.subscriptions.add("dex-new-token:sol")
await client.connect()
asyncio.run(main())
모범 사례
성능 최적화
필터 사용
필요한 데이터만 구독하여 대역폭을 줄이세요. CEL 표현식으로 데이터를 필터링하세요.
배치 처리
고빈도 데이터는 하나씩 처리하지 말고 배치로 처리하세요. 메시지 큐를 사용하여 버퍼링하세요.
로컬 캐싱
토큰 정보와 같은 정적 데이터를 캐시하여 반복 처리를 줄이세요.
연결 재사용
하나의 연결로 여러 채널을 구독할 수 있습니다. 여러 연결을 생성하지 마세요.
오류 처리
- 오류 이벤트 리스닝 — 연결 및 데이터 오류를 즉시 처리
- 재시도 메커니즘 구현 — 재연결 시 지수 백오프 사용
- 로그 기록 — 문제 해결을 위한 주요 이벤트 기록
- 그레이스풀 디그레이데이션 — WebSocket 사용 불가 시 폴링으로 전환
리소스 관리
// ✅ 불필요한 채널은 즉시 구독 해제
client.unsubscribe('dex-candle:sol_xxx_1m');
// ✅ 연결을 우아하게 종료
function gracefulClose() {
// 1. 하트비트 중지
client.stopPing();
// 2. 연결 종료
client.close();
}
관련 문서
WebSocket API 레퍼런스
전체 구독 유형 및 파라미터
가격 알림 봇
실습: 가격 모니터링 봇 만들기

