API key
API key는 메인 지갑의 일회성 서명으로 파생되는 결정론적 위임 ECDSA 서명 자격증명입니다. /account/link로 온체인에 등록되면 API key는 추가 MetaMask 팝업 없이 메인 지갑을 대신해 세션을 인증할 수 있습니다 — 봇, 스크립트, 인덱서에 가장 적합한 형태입니다. 같은 메인 지갑에 라벨로 여러 API key를 등록할 수 있으며, 각 API key는 작업 범위를 제한하거나 독립적으로 폐기할 수 있습니다.
- “More” 메뉴에서 API Keys 페이지를 열고 API key 이름(예: “trading-bot-1”)을 정한 뒤 수행 가능한 작업을 선택합니다.
- MetaMask에서 파생 메시지에 한 번 서명합니다. 브라우저가 API key를 로컬에서 파생하고, 온체인으로는 서명만 전송됩니다.
- 표시된 API key를 복사합니다. 한 번만 표시되며 체인에는 저장되지 않습니다. 분실 시 새 API key를 만들어야 합니다.
- 스크립트에서 이 API key를 서명자로 사용하고, 세션 생성 시 메인 지갑 주소(0xABC)를 userAddress로 전달합니다. 체인은 서명에서 복원한 주소를 authorized_signers와 대조해 API key를 통과시킵니다.
- API Keys 페이지에서 또는 메인 지갑의 새 서명을 동반한 POST /account/unlink로 언제든 폐기할 수 있습니다.
체인의 HTTP 인터페이스를 직접 사용하는(SDK 없는) 최소 예시입니다. TypeScript SDK를 사용하면 await session.order(...) 한 줄로 끝납니다 — 아래 SDK 섹션을 참고하세요.
Python — API key로 지정가 주문 제출
섹션 제목: “Python — API key로 지정가 주문 제출”import requestsfrom eth_account import Accountfrom 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를 거치지 않음)은 항상 전체 권한을 가집니다.
| Flag | Covers |
|---|---|
place_orders | POST /session/order — 지정가 / 시장가 / Post-Only / IOC / FOK 주문 제출 |
cancel_orders | POST /session/cancel + /session/modify — 수정은 취소를 수반하므로 동일 비트 공유 |
change_leverage | POST /session/leverage — 자산별 레버리지 및 격리/교차 모드 변경 |
set_triggers | POST /session/trigger + /session/cancel_trigger — TP/SL 트리거 주문 |
요청 속도 제한
섹션 제목: “요청 속도 제한”프로덕션 트레이딩 루프
섹션 제목: “프로덕션 트레이딩 루프”실제 봇은 최소 예시 위에 세 가지가 더 필요합니다: (1) 1시간 세션 TTL 만료 전 자동 재인증, (2) 네트워크 재시도로 인한 중복 주문을 방지하는 requestId 기반 멱등성, (3) /info 폴링 대신 WebSocket의 executionReports 구독으로 초 이하의 체결 이벤트. 아래 스크립트는 이 세 가지를 모두 구현합니다.
import asyncio, json, time, requestsimport websocketsfrom eth_account import Accountfrom 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, 0def _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). 클라이언트 작성 중 자주 마주치는 항목:
| Code | Meaning | Fix |
|---|---|---|
invalid-signature | r/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-id | challengeId가 알 수 없거나 이미 사용됨 | challenge는 일회용 — /session/start로 새것 받기 |
challenge-expired | challenge가 60초 초과 | /session/start부터 다시 시작 |
session-not-found | sessionId 만료, 폐기 또는 존재하지 않음 | 세션 핸드셰이크 재실행 (위 스크립트는 자동) |
api-key-permission-denied | API 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-margin | admission preflight에서 증거금 부족 | 입금, 사이즈 축소, 또는 더 타이트한 limit 가격 사용 |
invalid-asset | 이 노드가 모르는 asset id | /info { type: “meta” }에서 현재 universe 조회 |
API 키 폐기
섹션 제목: “API 키 폐기”API key는 메인 지갑만 폐기할 수 있습니다 — 설계상 유출된 API key는 스스로를 폐기할 수 없습니다. 지갑은 keccak256("nexus-delegate-unlink:" + user + ":" + signer)에 서명합니다. 성공 시 API key는 authorized_signers에서 제거되고, 그것을 사용 중이던 모든 진행 중 세션은 다음 요청에서 session-not-found가 반환됩니다.
import requestsfrom eth_account import Accountfrom eth_utils import keccak
WALLET_KEY = "0x..." # 메인 지갑 개인키 (API key 아님)USER = "0xABC..." # 메인 지갑 주소, 소문자 0x + 40 hexAPI_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..." }