콘텐츠로 이동
거래하기

API key

API key는 메인 지갑의 일회성 서명으로 파생되는 결정론적 위임 ECDSA 서명 자격증명입니다. /account/link로 온체인에 등록되면 API key는 추가 MetaMask 팝업 없이 메인 지갑을 대신해 세션을 인증할 수 있습니다 — 봇, 스크립트, 인덱서에 가장 적합한 형태입니다. 같은 메인 지갑에 라벨로 여러 API key를 등록할 수 있으며, 각 API key는 작업 범위를 제한하거나 독립적으로 폐기할 수 있습니다.

  1. “More” 메뉴에서 API Keys 페이지를 열고 API key 이름(예: “trading-bot-1”)을 정한 뒤 수행 가능한 작업을 선택합니다.
  2. MetaMask에서 파생 메시지에 한 번 서명합니다. 브라우저가 API key를 로컬에서 파생하고, 온체인으로는 서명만 전송됩니다.
  3. 표시된 API key를 복사합니다. 한 번만 표시되며 체인에는 저장되지 않습니다. 분실 시 새 API key를 만들어야 합니다.
  4. 스크립트에서 이 API key를 서명자로 사용하고, 세션 생성 시 메인 지갑 주소(0xABC)를 userAddress로 전달합니다. 체인은 서명에서 복원한 주소를 authorized_signers와 대조해 API key를 통과시킵니다.
  5. API Keys 페이지에서 또는 메인 지갑의 새 서명을 동반한 POST /account/unlink로 언제든 폐기할 수 있습니다.

체인의 HTTP 인터페이스를 직접 사용하는(SDK 없는) 최소 예시입니다. TypeScript SDK를 사용하면 await session.order(...) 한 줄로 끝납니다 — 아래 SDK 섹션을 참고하세요.

Python — API key로 지정가 주문 제출

섹션 제목: “Python — API key로 지정가 주문 제출”
import requests
from eth_account import Account
from eth_utils import keccak
USER = "0xABC..." # 메인 지갑 — 세션의 userAddress와 동일
API_KEY = "0x..." # API key (생성 모달에서 일회성 표시)
BASE = "https://api.bitnxs.com"
# 1. 지갑에 대한 challenge 요청
chal = requests.post(f"{BASE}/session/start", json={"user": USER}).json()
# 2. keccak256(challenge_bytes)를 API key로 **직접** 서명합니다.
# 체인은 keccak 해시 위에서 raw secp256k1 ECDSA로 주소를 복원합니다 —
# **eth_sign / personal_sign / EIP-191 사용 금지**. 그것들은
# "\x19Ethereum Signed Message:\n" 프리픽스를 추가하여 다른 해시를
# 만들고 signature-address-mismatch가 반환됩니다.
sig = Account.unsafe_sign_hash(
keccak(chal["challenge"].encode()),
private_key=API_KEY,
)
ses = requests.post(f"{BASE}/session/complete", json={
"challengeId": chal["challengeId"],
"signature": {
"r": hex(sig.r),
"s": hex(sig.s),
"v": sig.v - 27, # eth_account는 27/28 반환; 체인은 {0,1}
},
}).json()
SID = ses["sessionId"]
# 3. 주문 제출 — 이후 요청별 서명 불필요, sessionId가 인증
print(requests.post(f"{BASE}/session/order", json={
"sessionId": SID,
"requestId": 10001, # 멱등성 키 (선택, 0 이외)
"asset": 1, # 숫자 asset id, /info { type: "meta" }에서 조회
"isBuy": True,
"limitPx": "65000.00",
"sz": "0.01",
"reduceOnly": False,
"orderType": {"type": "limit", "tif": "Gtc"},
}).json())
# 4. 읽기는 세션 불필요 — /info는 공개
print(requests.post(f"{BASE}/info", json={
"type": "clearinghouseState", "user": USER,
}).json())

각 API key는 생성 시 4개의 불리언 플래그를 가집니다. 비활성화된 플래그는 매처에 진입하기 전에 체인이 거부하므로, API key 유출 시 영향을 줄이는 데 유용합니다. 지갑이 직접 서명한 세션(API key를 거치지 않음)은 항상 전체 권한을 가집니다.

FlagCovers
place_ordersPOST /session/order — 지정가 / 시장가 / Post-Only / IOC / FOK 주문 제출
cancel_ordersPOST /session/cancel + /session/modify — 수정은 취소를 수반하므로 동일 비트 공유
change_leveragePOST /session/leverage — 자산별 레버리지 및 격리/교차 모드 변경
set_triggersPOST /session/trigger + /session/cancel_trigger — TP/SL 트리거 주문

실제 봇은 최소 예시 위에 세 가지가 더 필요합니다: (1) 1시간 세션 TTL 만료 전 자동 재인증, (2) 네트워크 재시도로 인한 중복 주문을 방지하는 requestId 기반 멱등성, (3) /info 폴링 대신 WebSocket의 executionReports 구독으로 초 이하의 체결 이벤트. 아래 스크립트는 이 세 가지를 모두 구현합니다.

import asyncio, json, time, requests
import websockets
from eth_account import Account
from eth_utils import keccak
USER, API_KEY = "0xABC...", "0x..."
BASE = "https://api.bitnxs.com"
WS = BASE.replace("http", "ws") + "/ws"
# ─── 세션 헬퍼: 1시간 TTL 만료 전 자동 갱신 ──────────
_sid, _sid_exp = None, 0
def _open_session():
chal = requests.post(f"{BASE}/session/start", json={"user": USER}).json()
sig = Account.unsafe_sign_hash(keccak(chal["challenge"].encode()),
private_key=API_KEY)
ses = requests.post(f"{BASE}/session/complete", json={
"challengeId": chal["challengeId"],
"signature": {"r": hex(sig.r), "s": hex(sig.s), "v": sig.v - 27},
}).json()
if ses.get("status") != "ok":
raise RuntimeError(f"session/complete failed: {ses}")
return ses["sessionId"], time.time() + 3500 # 약 100초 버퍼
def session_id():
global _sid, _sid_exp
if not _sid or time.time() >= _sid_exp:
_sid, _sid_exp = _open_session()
return _sid
# ─── 멱등성 있는 주문 제출 ─────────────────────────
_req_seq = int(time.time() * 1000)
def next_req_id():
global _req_seq
_req_seq += 1
return _req_seq
def place(asset, is_buy, px, sz, req_id=None):
return requests.post(f"{BASE}/session/order", json={
"sessionId": session_id(),
"requestId": req_id or next_req_id(),
"asset": asset,
"isBuy": is_buy,
"limitPx": str(px),
"sz": str(sz),
"reduceOnly": False,
"orderType": {"type": "limit", "tif": "Gtc"},
}).json()
# ─── WebSocket 체결 스트림 ─────────────────────────
async def watch_fills():
async with websockets.connect(WS) as ws:
await ws.send(json.dumps({
"method": "subscribe",
"subscription": {"type": "executionReports", "user": USER},
}))
async for raw in ws:
msg = json.loads(raw)
if msg.get("channel") == "subscription":
for ev in msg.get("data", []):
print("체결 requestId=%s sz=%s" %
(ev.get("requestId"), ev.get("filledSz")))
async def main():
asyncio.create_task(watch_fills())
print(place(asset=1, is_buy=True, px=65000, sz=0.01))
await asyncio.sleep(60)
asyncio.run(main())

/account/* 및 /session/*의 실패 응답은 { status: "err", response: "<code>" } 형식입니다(별도 명시 없으면 HTTP 400). 클라이언트 작성 중 자주 마주치는 항목:

CodeMeaningFix
invalid-signaturer/s/v를 유효한 secp256k1 서명으로 파싱할 수 없음r/s가 0x로 시작하는 32바이트 hex이고 v ∈ {0,1}인지 확인
signature-address-mismatch복원된 주소가 지갑도 등록된 API key도 아님API key가 잘못되었거나 raw keccak 대신 EIP-191/personal_sign을 사용
signer-derivation-mismatch/account/link 한정: 신고된 signer ≠ keccak256(r‖s‖v_norm) 파생 주소dk 해시 계산 시 v ∈ {27,28} ({0,1} 아님) 사용
invalid-challenge-idchallengeId가 알 수 없거나 이미 사용됨challenge는 일회용 — /session/start로 새것 받기
challenge-expiredchallenge가 60초 초과/session/start부터 다시 시작
session-not-foundsessionId 만료, 폐기 또는 존재하지 않음세션 핸드셰이크 재실행 (위 스크립트는 자동)
api-key-permission-deniedAPI key에 해당 동작의 권한 플래그 없음더 넓은 권한으로 API key 재생성 (예: cancel_orders 활성화)
signer-already-linked-to-different-user/account/link: 해당 API key 주소가 다른 지갑에 이미 연결됨다른 라벨로 새 API key 파생
rate-limited (HTTP 429)서명자 토큰 버킷 소진 (50 burst / 20 rps)약 50ms 후 재시도 — 버킷은 초당 20씩 보충
oracle-not-ready노드 부팅 시 오라클 초기 가격 아직 미준비먼저 GET /ready 확인 후 몇 초 뒤 재시도
insufficient-marginadmission preflight에서 증거금 부족입금, 사이즈 축소, 또는 더 타이트한 limit 가격 사용
invalid-asset이 노드가 모르는 asset id/info { type: “meta” }에서 현재 universe 조회

API key는 메인 지갑만 폐기할 수 있습니다 — 설계상 유출된 API key는 스스로를 폐기할 수 없습니다. 지갑은 keccak256("nexus-delegate-unlink:" + user + ":" + signer)에 서명합니다. 성공 시 API key는 authorized_signers에서 제거되고, 그것을 사용 중이던 모든 진행 중 세션은 다음 요청에서 session-not-found가 반환됩니다.

import requests
from eth_account import Account
from eth_utils import keccak
WALLET_KEY = "0x..." # 메인 지갑 개인키 (API key 아님)
USER = "0xABC..." # 메인 지갑 주소, 소문자 0x + 40 hex
API_KEY_ADDR = "0xXYZ..." # 폐기할 API key 주소 (/info userApiKeys 에서 확인)
BASE = ""
msg = f"nexus-delegate-unlink:{USER.lower()}:{API_KEY_ADDR.lower()}"
sig = Account.unsafe_sign_hash(keccak(msg.encode()), private_key=WALLET_KEY)
print(requests.post(f"{BASE}/account/unlink", json={
"user": USER,
"signer": API_KEY_ADDR,
"revocationSig": {"r": hex(sig.r), "s": hex(sig.s), "v": sig.v - 27},
}).json())
# → { "status": "ok", "user": "0xABC...", "signer": "0xXYZ..." }