API キー
API キーは、メインウォレットからの一度きりの署名で導出される決定論的な委任 ECDSA 署名クレデンシャルです。/account/link でオンチェーンに登録すると、API キーはメインウォレットの代わりにセッションを認証でき、以降 MetaMask のポップアップは不要になります —— bot、スクリプト、indexer に最適な形態です。1 つのウォレットに対し複数の API キーをラベル付きで登録でき、各 API キーは操作範囲を制限したり、個別に取り消したりできます。
- 「More」メニューから API Keys ページを開き、API キーに名前(例:trading-bot-1)を付け、実行可能な操作を選択します。
- MetaMask で導出メッセージに一度署名します。API キーはブラウザ内で導出され、オンチェーンには署名のみが送信されます。
- 表示された API キーをコピーします。表示は一度きりで、チェーンには保存されません。紛失した場合は新しい API キーを作成する必要があります。
- スクリプト側ではこの API キーを signer とし、セッション作成時にメインウォレットアドレス(0xABC)を userAddress として渡します。チェーンは署名から復元したアドレスを authorized_signers と照合し、API キーを通過させます。
- 取り消しはいつでも、API Keys ページか、新しいメインウォレット署名を伴う POST /account/unlink で実行できます。
チェーンの HTTP インターフェースを直接利用する(SDK 非依存の)最小のエンドツーエンド例です。TypeScript SDK を使う場合は await session.order(...) の 1 行で済みます。下の SDK セクションを参照してください。
Python — API キーで指値注文を送信する
Section titled “Python — API キーで指値注文を送信する”import requestsfrom eth_account import Accountfrom eth_utils import keccak
USER = "0xABC..." # メインウォレット — セッションの userAddress も同じAPI_KEY = "0x..." # API キー(作成モーダルで一度だけ表示)BASE = "https://api.bitnxs.com"
# 1. ウォレットの challenge を要求。chal = requests.post(f"{BASE}/session/start", json={"user": USER}).json()
# 2. keccak256(challenge_bytes) を API キーで直接署名する。# チェーンは生 secp256k1 ECDSA で keccak ハッシュからアドレスを# 復元する —— **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 キーは作成時に 4 つの真偽値フラグを保持します。無効化されたフラグの操作は、マッチングエンジンに入る前にチェーン側で拒否されます。API キーが漏洩した場合の影響範囲を限定するのに便利です。ウォレット直接署名のセッション(API キーを介さない)は常にすべての権限を持ちます。
| 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 トリガー注文 |
本番向けトレーディングループ
Section titled “本番向けトレーディングループ”実運用の bot には最小例に加えて 3 つが必要です:(1) 1 時間のセッション TTL 切れ前に自動で再認証、(2) requestId による冪等性でネットワーク再送が二重発注を起こさない、(3) /info ポーリングではなく WebSocket の executionReports 購読で秒未満の約定通知。下のスクリプトはこの 3 つすべてを実装しています。
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())エラーコード
Section titled “エラーコード”/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 キーでもない | API キーが間違っているか、生 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 が見つからない、または既に消費済み | チャレンジは一回限り — /session/start で新しいものを取得 |
challenge-expired | チャレンジが 60 秒を超過 | /session/start からやり直し |
session-not-found | sessionId が期限切れ、取り消し済み、または存在しない | セッションハンドシェイクを再実行(上記スクリプトは自動) |
api-key-permission-denied | API キーに該当操作の権限フラグがない | より広い権限で API キーを再作成(例:cancel_orders を有効化) |
signer-already-linked-to-different-user | /account/link:その API キーのアドレスが他のウォレットに既に紐付け済み | ラベルを変えて新しい API キーを派生 |
rate-limited (HTTP 429) | サイナーのトークンバケット枯渇(50 burst / 20 rps) | ~50 ms 待ってリトライ — バケットは毎秒 20 補充 |
oracle-not-ready | 起動時にオラクル初期価格がまだ未準備 | まず GET /ready で確認、数秒後リトライ |
insufficient-margin | admission preflight で証拠金不足 | 入金、サイズ縮小、または limit 価格を厳しめに |
invalid-asset | このノードが知らない asset id | /info { type: “meta” } で現在の universe を取得 |
API キーの取り消し
Section titled “API キーの取り消し”API キーの取り消しはメインウォレットのみが可能です — 設計上、漏洩した API キーは自分自身を取り消せません。ウォレットは keccak256("nexus-delegate-unlink:" + user + ":" + signer) に署名します。成功すると API キーは authorized_signers から削除され、それを使っていた既存セッションは次のリクエストで session-not-found が返ります。
import requestsfrom eth_account import Accountfrom eth_utils import keccak
WALLET_KEY = "0x..." # メインウォレットの秘密鍵(API キーではない)USER = "0xABC..." # メインウォレットアドレス、小文字 0x + 40 hexAPI_KEY_ADDR = "0xXYZ..." # 取り消す API キーのアドレス(/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..." }