コンテンツにスキップ
取引する

API キー

API キーは、メインウォレットからの一度きりの署名で導出される決定論的な委任 ECDSA 署名クレデンシャルです。/account/link でオンチェーンに登録すると、API キーはメインウォレットの代わりにセッションを認証でき、以降 MetaMask のポップアップは不要になります —— bot、スクリプト、indexer に最適な形態です。1 つのウォレットに対し複数の API キーをラベル付きで登録でき、各 API キーは操作範囲を制限したり、個別に取り消したりできます。

  1. 「More」メニューから API Keys ページを開き、API キーに名前(例:trading-bot-1)を付け、実行可能な操作を選択します。
  2. MetaMask で導出メッセージに一度署名します。API キーはブラウザ内で導出され、オンチェーンには署名のみが送信されます。
  3. 表示された API キーをコピーします。表示は一度きりで、チェーンには保存されません。紛失した場合は新しい API キーを作成する必要があります。
  4. スクリプト側ではこの API キーを signer とし、セッション作成時にメインウォレットアドレス(0xABC)を userAddress として渡します。チェーンは署名から復元したアドレスを authorized_signers と照合し、API キーを通過させます。
  5. 取り消しはいつでも、API Keys ページか、新しいメインウォレット署名を伴う POST /account/unlink で実行できます。

チェーンの HTTP インターフェースを直接利用する(SDK 非依存の)最小のエンドツーエンド例です。TypeScript SDK を使う場合は await session.order(...) の 1 行で済みます。下の SDK セクションを参照してください。

Python — API キーで指値注文を送信する

Section titled “Python — API キーで指値注文を送信する”
import requests
from eth_account import Account
from 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 キーを介さない)は常にすべての権限を持ちます。

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 トリガー注文

本番向けトレーディングループ

Section titled “本番向けトレーディングループ”

実運用の bot には最小例に加えて 3 つが必要です:(1) 1 時間のセッション TTL 切れ前に自動で再認証、(2) requestId による冪等性でネットワーク再送が二重発注を起こさない、(3) /info ポーリングではなく WebSocket の executionReports 購読で秒未満の約定通知。下のスクリプトはこの 3 つすべてを実装しています。

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 キーでもない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-idchallengeId が見つからない、または既に消費済みチャレンジは一回限り — /session/start で新しいものを取得
challenge-expiredチャレンジが 60 秒を超過/session/start からやり直し
session-not-foundsessionId が期限切れ、取り消し済み、または存在しないセッションハンドシェイクを再実行(上記スクリプトは自動)
api-key-permission-deniedAPI キーに該当操作の権限フラグがないより広い権限で 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-marginadmission preflight で証拠金不足入金、サイズ縮小、または limit 価格を厳しめに
invalid-assetこのノードが知らない asset id/info { type: “meta” } で現在の universe を取得

API キーの取り消しはメインウォレットのみが可能です — 設計上、漏洩した API キーは自分自身を取り消せません。ウォレットは keccak256("nexus-delegate-unlink:" + user + ":" + signer) に署名します。成功すると API キーは authorized_signers から削除され、それを使っていた既存セッションは次のリクエストで session-not-found が返ります。

import requests
from eth_account import Account
from eth_utils import keccak
WALLET_KEY = "0x..." # メインウォレットの秘密鍵(API キーではない)
USER = "0xABC..." # メインウォレットアドレス、小文字 0x + 40 hex
API_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..." }