PKCryptolisting
WebSocket
串接指南
僅需幾行程式碼,即可接收 Upbit、Bithumb、Binance、Bybit 的即時公告。
請先於 Telegram 機器人取得 API 金鑰,再依下方指南進行連接。
ENDPOINT · KR
wss://kr.pkcryptolisting.com
僅限 Upbit · Bithumb
ENDPOINT · TOKYO
wss://tokyo.pkcryptolisting.com
Upbit · Bithumb · Binance · Bybit
PROTOCOL
WebSocket Secure (WSS)
AUTH
API Key (透過 Telegram 發放)
SERVER LOCATION
AWS Seoul (ap-northeast-2) / AWS Tokyo (ap-northeast-1)
1. 連線與驗證
建立 WebSocket 連線後,必須立即傳送驗證訊息。 5 秒內未完成驗證將自動終止連線連線將自動終止
若僅想接收特定交易所之公告,請加入
若僅想接收特定交易所之公告,請加入
exchange 欄位。
⚠️ 注意事項
•
•
•
wss://kr.pkcryptolisting.com(Korea) 端點僅支援
UPBIT ·
BITHUMB。•
wss://tokyo.pkcryptolisting.com(Tokyo) 端點支援所有交易所,包含
UPBIT ·
BITHUMB ·
BINANCE ·
BYBIT 。
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: # or wss://tokyo.pkcryptolisting.com for all 4 exchanges # 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"); // or wss://tokyo.pkcryptolisting.com for all 4 exchanges 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)// or wss://tokyo.pkcryptolisting.com for all 4 exchanges // 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();// or wss://tokyo.pkcryptolisting.com for all 4 exchanges 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']} {msg.get('timezone', '')}")
// 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} ${msg.timezone || ''}`); } });
// 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 %v ", msg["symbol"], msg["detectedTime"], msg["timezone"]) } }
// 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"], msg["timezone"]); } } }
4. 接收資料格式
公告資料將以下列格式傳遞。
symbol 欄位可以是單一字串、陣列,或空字串。
⚠️ 注意事項
•
•
•
title, category
欄位將以 UPBIT · BITHUMB 接收,適用於 韓文;而BINANCE · BYBIT 則接收 英文原文。•
detectedTime —
我方引擎偵測到公告的時間點(微秒級精度)
{ "title": "펏지펭귄(PENGU) 신규 거래지원 안내 (KRW, BTC, USDT 마켓)", "category": "거래", "symbol": "PENGU", "exchange": "Upbit", "detectedTime": "2025-05-09 15:00:00.333851", "timezone": "KST", "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"、"Binance"、"Bybit"
|
| detectedTime | string | 我方引擎偵測到公告之時間點(微秒級精度)。Basic 方案套用 +30ms 額外延遲 |
| timezone | string | detectedTime 所使用的基準時區。KST(Upbit、Bithumb)或 UTC(Binance、Bybit) |
| url | string | 公告原文連結 |
方案差異
所有方案皆使用相同的基礎架構,僅延遲時間與幣種公開與否有所不同。
Free
0ms 延遲
幣種代號 *** 遮罩
Basic
+30ms 延遲
幣種代號完全公開
Premium
0ms 延遲
幣種代號完全公開
⚠️ 注意事項
• 使用單一 API Key 無法同時從 同一區域內無法從多台裝置連線。若已存在連線,其他位置嘗試連線時,新連線嘗試將以錯誤代碼
• 使用單一 API Key 無法同時從 同一區域內無法從多台裝置連線。若已存在連線,其他位置嘗試連線時,新連線嘗試將以錯誤代碼
4006遭到封鎖。
錯誤代碼
連線或驗證失敗時,將回傳下列代碼。
| CODE | 說明 | 處理方式 |
|---|---|---|
| 4001 | 驗證逾時 | 連線後 5 秒內傳送 auth 訊息。 |
| 4002 | 無效的 JSON | 請確認 JSON 格式。 |
| 4003 | 缺少驗證訊息 | type: "auth"須傳送驗證訊息。 |
| 4004 | 無效的金鑰 | 於 Telegram 機器人確認金鑰 |
| 4005 | 訂閱已到期 | 於 Telegram 機器人更新方案 |
| 4006 | 重複連線 | 請先中斷既有連線後再重新連接 |
| 4029 | 請求過於頻繁(速率限制) | 請稍後再試 |
分類清單
這是所接收到的數值清單。
category 欄位值在 UPBIT · BITHUMB 為韓文,在 BINANCE · BYBIT 則為英文原文。
完整範例程式碼
這是包含自動重連與指數退避機制的完整可執行程式碼。只需替換
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", "") timezone = evt.get("timezone", "") url = evt.get("url", "") print(f"[{exchange}] [{category}] {title}") print(f" symbol={symbol}") print(f" detected={detected} {timezone}") 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} ${evt.timezone || ''}`); 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,Timezone,URL string Code,DelayMs int; Symbol interface{} } func handleNotice(e Evt) { fmt.Printf("[%s] [%s] %s symbol=%v detected=%s %s ", e.Exchange, e.Category, e.Title, e.Symbol, e.DetectedTime, e.Timezone, 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={} {} {}", e["exchange"],e["category"],e["title"],e["symbol"],e["detectedTime"],e["timezone"],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 金鑰
可於 Telegram 機器人免費取得。
付費方案升級亦可透過機器人完成。