PKCryptolisting
WebSocket
연동 가이드
단 몇 줄의 코드로 업비트·빗썸 실시간 공지를 수신할 수 있습니다.
텔레그램 봇에서 API 키를 발급받은 후 아래 가이드를 따라 연결하세요.
ENDPOINT
wss://kr.pkcryptolisting.com
PROTOCOL
WebSocket Secure (WSS)
AUTH
API Key (텔레그램 발급)
SERVER LOCATION
AWS Seoul (ap-northeast-2)
1. 연결 및 인증
WebSocket 연결 후 즉시 인증 메시지를 전송해야 합니다. 5초 내 인증하지 않으면 자동 종료됩니다.
특정 거래소 공지만 수신하려면
특정 거래소 공지만 수신하려면
exchange 필드를 추가하세요.pip install websockets
npm install ws
go get github.com/gorilla/websocket
[dependencies]
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = { version = "0.21", features = ["native-tls"] }
futures-util = "0.3"
serde_json = "1"
url = "2"import asyncio, json, websockets async def connect(): async with websockets.connect("wss://kr.pkcryptolisting.com") as ws: # send auth message await ws.send(json.dumps({ "type": "auth", "key": "YOUR_API_KEY", # issued via Telegram bot # "exchange": "Upbit" # optional: "Upbit" | "Bithumb" (omit for all) })) asyncio.run(connect())
// npm install ws const WebSocket = require("ws"); const ws = new WebSocket("wss://kr.pkcryptolisting.com"); ws.on("open", () => { // send auth message ws.send(JSON.stringify({ type: "auth", key: "YOUR_API_KEY", // issued via Telegram bot // exchange: "Upbit" // optional: "Upbit" | "Bithumb" (omit for all) })); });
// go get github.com/gorilla/websocket conn, _, _ := websocket.DefaultDialer.Dial("wss://kr.pkcryptolisting.com", nil) // send auth message auth, _ := json.Marshal(map[string]string{"type": "auth", "key": "YOUR_API_KEY"}) // "exchange": "Upbit" // optional: "Upbit" | "Bithumb" (omit for all) conn.WriteMessage(websocket.TextMessage, auth)
let url = url::Url::parse("wss://kr.pkcryptolisting.com").unwrap(); let (mut ws, _) = connect_async(url).await.unwrap(); // send auth message let auth = serde_json::json!({"type": "auth", "key": "YOUR_API_KEY" // "exchange": "Upbit" // optional: "Upbit" | "Bithumb" (omit for all) }).to_string(); ws.send(Message::Text(auth)).await.ok();
2. 인증 응답
인증 성공 시 플랜, 딜레이, 만료일 정보가 반환됩니다.
✓ 성공 (auth_ok)
{ "type": "auth_ok", "plan": "premium", "delay_ms": 0, "expires_at": "2026-06-30", "message": "Connected..." }
✕ 실패 (error)
{ "type": "error", "code": 4004, "message": "Invalid key" }
3. 메시지 수신
인증 후 새 공지가 감지되면 자동으로 수신됩니다.
async for를 사용하면 연결 종료 시 자동으로 루프가 탈출됩니다.
# message loop — exits cleanly on disconnect async for raw in ws: msg = json.loads(raw) msg_type = msg.get("type") if msg_type == "auth_ok": print(f"connected plan={msg['plan']} delay={msg['delay_ms']}ms") elif msg_type == "error": print(f"error {msg['code']}: {msg['message']}") break elif msg_type == "plan_updated": # plan changed print(f"plan updated: {msg['plan']} delay={msg['delay_ms']}ms") else: # announcement print(f"[{msg['exchange']}] {msg['title']}") print(f" symbol={msg['symbol']} detected={msg['detectedTime']}")
// message loop — exits cleanly on disconnect ws.on("message", (raw) => { const msg = JSON.parse(raw); if (msg.type === "auth_ok") console.log(`connected plan=${msg.plan} delay=${msg.delay_ms}ms`); else if (msg.type === "error") { console.log(`error ${msg.code}: ${msg.message}`); ws.close(); } else if (msg.type === "plan_updated") // plan changed console.log(`plan updated: ${msg.plan} delay=${msg.delay_ms}ms`); else { // announcement console.log(`[${msg.exchange}] ${msg.title}`); console.log(` symbol=${msg.symbol} detected=${msg.detectedTime}`); } });
// message loop — exits cleanly on disconnect for { _, raw, err := conn.ReadMessage() if err != nil { break } var msg map[string]interface{} json.Unmarshal(raw, &msg) switch msg["type"] { case "auth_ok": fmt.Printf("connected plan=%v delay=%vms ", msg["plan"], msg["delay_ms"]) case "error": fmt.Printf("error %v: %v ", msg["code"], msg["message"]); break case "plan_updated": // plan changed fmt.Printf("plan updated: %v delay=%vms ", msg["plan"], msg["delay_ms"]) default: // announcement fmt.Printf("[%v] %v ", msg["exchange"], msg["title"]) fmt.Printf(" symbol=%v detected=%v ", msg["symbol"], msg["detectedTime"]) } }
// message loop — exits cleanly on disconnect while let Some(Ok(raw)) = ws.next().await { let msg: Value = serde_json::from_str(&raw.to_string()).unwrap_or_default(); match msg["type"].as_str().unwrap_or("") { "auth_ok" => println!("connected plan={} delay={}ms", msg["plan"], msg["delay_ms"]), "error" => { println!("error {}: {}", msg["code"], msg["message"]); break; } "plan_updated" => // plan changed println!("plan updated: {} delay={}ms", msg["plan"], msg["delay_ms"]), _ => { // announcement println!("[{}] {}", msg["exchange"], msg["title"]); println!(" symbol={} detected={}", msg["symbol"], msg["detectedTime"]); } } }
4. 수신 데이터 형식
공지 데이터는 아래 형식으로 전달됩니다.
symbol은 단일 문자열, 배열, 또는 빈 문자열일 수 있습니다.
⚠️ 주의사항
•
•
•
•
title, category
필드는 한국어로 수신됩니다.•
detectedTime —
저희 엔진이 공지를 감지한 시각 (마이크로초 정밀도)•
postedTime —
거래소가 공지를 게시한 시각
{ "title": "펏지펭귄(PENGU) 신규 거래지원 안내 (KRW, BTC, USDT 마켓)", "category": "거래", "symbol": "PENGU", "exchange": "Upbit", "detectedTime": "2025-05-09 15:00:00.333851", "postedTime": "2025-05-09T15:00:00+09:00", "url": "https://www.upbit.com/service_center/notice?id=5104&view=share" }
| 필드 | 타입 | 설명 |
|---|---|---|
| title | string | 거래소 공지 원문 제목 (한국어) |
| category | string | 공지 카테고리 (한국어) — 아래 카테고리 목록 참고 |
| symbol | string | array |
추출된 코인 심볼. 단일 "BTC", 다중 ["BTC","ETH"], 없으면 "", Free 플랜은 "***"
|
| exchange | string |
공지 출처 거래소. "Upbit" 또는 "Bithumb"
|
| detectedTime | string | 저희 엔진이 공지를 감지한 시각 (마이크로초 정밀도). Basic 플랜은 +30ms 적용됨 |
| postedTime | string | 거래소가 공지를 게시한 시각. 거래소 원본 형식 그대로 전달됨 |
| url | string | 공지 원문 링크 |
플랜별 차이
모든 플랜은 동일한 인프라를 사용합니다. 딜레이와 심볼 공개 여부만 다릅니다.
Free
0ms 딜레이
심볼 *** 마스킹
Basic
+30ms 딜레이
심볼 전체 공개
Premium
0ms 딜레이
심볼 전체 공개
⚠️ 주의사항
• API Key 하나로 동시에 여러 기기 접속이 불가능합니다. 이미 연결된 상태에서 다른 곳에서 연결을 시도하면 새 연결 시도가 에러 코드
• API Key 하나로 동시에 여러 기기 접속이 불가능합니다. 이미 연결된 상태에서 다른 곳에서 연결을 시도하면 새 연결 시도가 에러 코드
4006으로 차단됩니다.
에러 코드
연결 또는 인증 실패 시 아래 코드가 반환됩니다.
| CODE | 설명 | 처리 방법 |
|---|---|---|
| 4001 | 인증 타임아웃 | 연결 후 5초 내 auth 메시지 전송 |
| 4002 | 잘못된 JSON | JSON 형식 확인 |
| 4003 | 인증 메시지 누락 | type: "auth" 메시지 전송 |
| 4004 | 유효하지 않은 키 | 텔레그램 봇에서 키 확인 |
| 4005 | 구독 만료 | 텔레그램 봇에서 플랜 갱신 |
| 4006 | 중복 접속 | 기존 연결 종료 후 재접속 |
| 4029 | 과도한 요청 | 잠시 후 다시 시도하세요 |
카테고리 목록
필드에서 수신되는 값 목록입니다.
category 필드 값은 한국어로 수신됩니다.
전체 예제 코드
자동 재연결 및 지수 백오프가 적용된 완성 코드입니다.
YOUR_API_KEY
만 교체하고 바로 실행하세요.
import asyncio, json, websockets KEY = "YOUR_API_KEY" # issued via Telegram bot ENDPOINT = "wss://kr.pkcryptolisting.com" RETRIES = 20 def handle_notice(evt: dict): exchange = evt.get("exchange", "") title = evt.get("title", "") category = evt.get("category", "") symbol = evt.get("symbol", "") detected = evt.get("detectedTime", "") posted = evt.get("postedTime", "") url = evt.get("url", "") print(f"[{exchange}] [{category}] {title}") print(f" symbol={symbol}") print(f" detected={detected} posted={posted}") print(f" {url}") async def listen(): for attempt in range(RETRIES): try: async with websockets.connect(ENDPOINT) as ws: await ws.send(json.dumps({"type": "auth", "key": KEY})) async for raw in ws: evt = json.loads(raw) evt_type = evt.get("type") if evt_type == "auth_ok": print(f"connected plan={evt['plan']} delay={evt['delay_ms']}ms expires={evt['expires_at']}") attempt = 0 # reset retry counter on success elif evt_type == "error": print(f"[{evt['code']}] {evt['message']}") if evt["code"] in (4004, 4005, 4006, 4029): return # do not reconnect break elif evt_type == "plan_updated": print(f"plan changed → {evt['plan']} delay={evt['delay_ms']}ms expires={evt['expires_at']}") else: handle_notice(evt) except websockets.ConnectionClosed as e: print(f"closed: {e}") except Exception as e: print(f"error: {e}") backoff = min(2 ** attempt, 300) print(f"retry in {backoff}s...") await asyncio.sleep(backoff) if __name__ == "__main__": asyncio.run(listen())
// npm install ws const WebSocket = require("ws"); const KEY = "YOUR_API_KEY"; // issued via Telegram bot const ENDPOINT = "wss://kr.pkcryptolisting.com"; const RETRIES = 20; function handleNotice(evt) { console.log(`[${evt.exchange}] [${evt.category}] ${evt.title}`); console.log(` symbol=${evt.symbol}`); console.log(` detected=${evt.detectedTime} posted=${evt.postedTime}`); console.log(` ${evt.url}`); } function listen(attempt = 0) { if (attempt >= RETRIES) return; const ws = new WebSocket(ENDPOINT); ws.on("open", () => ws.send(JSON.stringify({ type: "auth", key: KEY }))); ws.on("message", (raw) => { const evt = JSON.parse(raw); if (evt.type === "auth_ok") { console.log(`connected plan=${evt.plan} delay=${evt.delay_ms}ms expires=${evt.expires_at}`); attempt = 0; // reset retry counter on success } else if (evt.type === "error") { console.log(`[${evt.code}] ${evt.message}`); if ([4004,4005,4006,4029].includes(evt.code)) return; // do not reconnect ws.close(); } else if (evt.type === "plan_updated") console.log(`plan changed → ${evt.plan} delay=${evt.delay_ms}ms expires=${evt.expires_at}`); else handleNotice(evt); }); ws.on("close", () => { const b = Math.min(2 ** attempt, 300); console.log(`retry in ${b}s...`); setTimeout(() => listen(attempt + 1), b * 1000); }); ws.on("error", (e) => console.log(`error: ${e.message}`)); } listen();
// go get github.com/gorilla/websocket package main import ( "encoding/json"; "fmt"; "math"; "time" "github.com/gorilla/websocket" ) const (key = "YOUR_API_KEY"; endpoint = "wss://kr.pkcryptolisting.com"; retries = 20) // issued via Telegram bot type Evt struct { Type,Plan,Message,ExpiresAt,Exchange,Title,Category,DetectedTime,PostedTime,URL string Code,DelayMs int; Symbol interface{} } func handleNotice(e Evt) { fmt.Printf("[%s] [%s] %s symbol=%v detected=%s posted=%s %s ", e.Exchange, e.Category, e.Title, e.Symbol, e.DetectedTime, e.PostedTime, e.URL) } func listen() { for a := 0; a < retries; a++ { c, _, err := websocket.DefaultDialer.Dial(endpoint, nil) if err != nil { fmt.Println("error:", err) } else { b, _ := json.Marshal(map[string]string{"type":"auth","key":key}) c.WriteMessage(websocket.TextMessage, b) for { _, raw, err := c.ReadMessage(); if err != nil { fmt.Println("closed:", err); break } var e Evt; json.Unmarshal(raw, &e) switch e.Type { case "auth_ok": fmt.Printf("connected plan=%s delay=%dms expires=%s ",e.Plan,e.DelayMs,e.ExpiresAt); a=0 // reset retry counter on success case "error": fmt.Printf("[%d] %s ",e.Code,e.Message) if e.Code==4004||e.Code==4005||e.Code==4006||e.Code==4029 { return } // do not reconnect case "plan_updated": fmt.Printf("plan changed → %s delay=%dms expires=%s ",e.Plan,e.DelayMs,e.ExpiresAt) default: handleNotice(e) } }; c.Close() } bf := time.Duration(math.Min(math.Pow(2,float64(a)),300))*time.Second fmt.Printf("retry in %vs... ", bf.Seconds()); time.Sleep(bf) } } func main() { listen() }
use futures_util::{SinkExt, StreamExt}; use tokio_tungstenite::{connect_async, tungstenite::Message}; use serde_json::{json, Value}; use std::time::Duration; const KEY: &str = "YOUR_API_KEY"; // issued via Telegram bot const ENDPOINT: &str = "wss://kr.pkcryptolisting.com"; const RETRIES: u32 = 20; fn handle_notice(e: &Value) { println!("[{}] [{}] {} symbol={} detected={} posted={} {}", e["exchange"],e["category"],e["title"],e["symbol"],e["detectedTime"],e["postedTime"],e["url"]); } #[tokio::main] async fn main() { for attempt in 0..RETRIES { match connect_async(url::Url::parse(ENDPOINT).unwrap()).await { Ok((mut ws, _)) => { ws.send(Message::Text(json!({"type":"auth","key":KEY}).to_string())).await.ok(); while let Some(Ok(raw)) = ws.next().await { if let Ok(e) = serde_json::from_str::<Value>(&raw.to_string()) { match e["type"].as_str().unwrap_or("") { "auth_ok" => println!("connected plan={} delay={}ms expires={}", e["plan"],e["delay_ms"],e["expires_at"]), "error" => { println!("[{}] {}",e["code"],e["message"]); let c=e["code"].as_i64().unwrap_or(0); if [4004,4005,4006,4029].contains(&c) { return; } // do not reconnect break; } "plan_updated" => println!("plan changed → {} delay={}ms expires={}", e["plan"],e["delay_ms"],e["expires_at"]), _ => handle_notice(&e), } } } } Err(e) => println!("error: {}", e), } let b = (2u64.pow(attempt)).min(300); println!("retry in {}s...", b); tokio::time::sleep(Duration::from_secs(b)).await; } }
API 키 발급
텔레그램 봇에서 무료로 발급받을 수 있습니다.
유료 플랜 업그레이드도 봇에서 가능합니다.