API keys
Una API key es una credencial de firma ECDSA delegada y determinista, derivada de su wallet principal mediante una firma única. Una vez registrada on-chain mediante /account/link, la API key autentica sesiones en nombre de su wallet principal sin más pop-ups de MetaMask — exactamente lo que necesita para bots, scripts e indexadores. Se admiten varias API keys por wallet mediante etiquetas; cada una puede limitarse a un subconjunto de acciones y revocarse de forma independiente.
- Abra la página API Keys desde el menú “More”, asigne un nombre a su API key (por ejemplo, “trading-bot-1”) y elija las acciones que podrá realizar.
- Firme el mensaje de derivación una vez en MetaMask: su navegador deriva la API key localmente y envía únicamente la firma on-chain.
- Copie la API key mostrada. Solo se muestra una vez; la cadena nunca la almacena. Si la pierde, deberá crear una nueva.
- En su script, use esta API key como firmante y pase la dirección de su wallet PRINCIPAL (0xABC) como userAddress al abrir la sesión. La cadena verifica la dirección recuperada de la firma contra authorized_signers y deja pasar la API key.
- Revóquela en cualquier momento desde la página API Keys o mediante POST /account/unlink con una nueva firma de la wallet PRINCIPAL.
Ejemplo mínimo de extremo a extremo utilizando directamente la interfaz HTTP de la cadena (sin SDK). El mismo flujo con el SDK TypeScript se reduce a una línea: await session.order(...); consulte la sección SDK más abajo.
Python — enviar una orden limitada con una API key
Sección titulada «Python — enviar una orden limitada con una API key»import requestsfrom eth_account import Accountfrom eth_utils import keccak
USER = "0xABC..." # wallet PRINCIPAL — también userAddress en la sesiónAPI_KEY = "0x..." # API key (mostrada una sola vez en el modal)BASE = "https://api.bitnxs.com"
# 1. Solicitar un challenge para la wallet.chal = requests.post(f"{BASE}/session/start", json={"user": USER}).json()
# 2. Firmar **directamente** keccak256(challenge_bytes) con la API key.# La cadena verifica con secp256k1 ECDSA crudo sobre el hash keccak —# **no use eth_sign / personal_sign / EIP-191**. Estos anteponen# "\x19Ethereum Signed Message:\n" y producen un hash distinto;# la cadena devolverá 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 devuelve 27/28; la cadena espera {0,1} },}).json()SID = ses["sessionId"]
# 3. Enviar una orden — sin firma por petición, sessionId es la credencial.print(requests.post(f"{BASE}/session/order", json={ "sessionId": SID, "requestId": 10001, # clave de idempotencia (opcional, no cero) "asset": 1, # asset id numérico, desde /info { type: "meta" } "isBuy": True, "limitPx": "65000.00", "sz": "0.01", "reduceOnly": False, "orderType": {"type": "limit", "tif": "Gtc"},}).json())
# 4. Las lecturas no necesitan sesión — /info es público.print(requests.post(f"{BASE}/info", json={ "type": "clearinghouseState", "user": USER,}).json())Banderas de permisos
Sección titulada «Banderas de permisos»Cada API key incluye cuatro banderas booleanas establecidas en el momento de su creación. Las banderas deshabilitadas son rechazadas por la cadena ANTES de que la acción entre al matcher: útil para limitar el radio de impacto si una API key se filtra. Las sesiones firmadas directamente por la wallet (sin pasar por una API key) siempre disponen de permisos completos.
| Flag | Covers |
|---|---|
place_orders | POST /session/order — colocar órdenes limit / market / post-only / IOC / FOK |
cancel_orders | POST /session/cancel + /session/modify — modificar implica cancelar, comparten el mismo bit |
change_leverage | POST /session/leverage — cambiar el apalancamiento por activo y el modo aislado/cruzado |
set_triggers | POST /session/trigger + /session/cancel_trigger — órdenes disparadoras TP/SL |
Límites de tasa
Sección titulada «Límites de tasa»Bucle de trading en producción
Sección titulada «Bucle de trading en producción»Un bot real necesita tres cosas sobre el ejemplo mínimo: (1) re-autenticarse antes de que expire el TTL de 1 hora de la sesión, (2) idempotencia basada en requestId para que un reintento de red nunca duplique una orden, y (3) una suscripción WebSocket para eventos de fill sub-segundo en vez de hacer polling a /info. El script siguiente implementa los tres.
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"
# ─── helper de sesión: abrir y renovar antes del TTL 1h ───_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 # ~100s de margen
def session_id(): global _sid, _sid_exp if not _sid or time.time() >= _sid_exp: _sid, _sid_exp = _open_session() return _sid
# ─── colocación idempotente de órdenes ──────────────────_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()
# ─── stream de fills por 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("fill 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())Códigos de error
Sección titulada «Códigos de error»Las respuestas de error de /account/* y /session/* tienen la forma { status: "err", response: "<code>" } (HTTP 400 salvo nota contraria). Códigos que más probablemente encontrará al escribir un cliente:
| Code | Meaning | Fix |
|---|---|---|
invalid-signature | r/s/v no se pudo parsear como una firma secp256k1 válida | Verifique que r y s sean hex de 32 bytes con prefijo 0x y v ∈ {0,1} |
signature-address-mismatch | La dirección recuperada no es ni la wallet ni ninguna API key registrada | API key incorrecta, o se usó EIP-191/personal_sign en lugar de keccak crudo |
signer-derivation-mismatch | Solo /account/link: signer declarado ≠ dirección derivada de keccak256(r‖s‖v_norm) | Use v ∈ {27,28} (no {0,1}) al computar el hash de dk |
invalid-challenge-id | challengeId desconocido o ya consumido | Los challenges son de un solo uso — llame a /session/start para uno nuevo |
challenge-expired | Challenge mayor a 60s | Reinicie desde /session/start |
session-not-found | sessionId expirado, revocado o nunca existió | Reejecute el handshake de sesión (el script de arriba lo hace solo) |
api-key-permission-denied | La API key no tiene la bandera de permiso para esta acción | Recree la API key con permisos más amplios (p. ej. cancel_orders) |
signer-already-linked-to-different-user | /account/link: la dirección de la API key ya está vinculada a otra wallet | Use una etiqueta distinta para derivar una nueva API key |
rate-limited (HTTP 429) | Token bucket del firmante agotado (50 burst / 20 rps) | Espere ~50ms y reintente — el bucket recarga 20 tokens/seg |
oracle-not-ready | El nodo aún está cargando precios iniciales del oráculo al arrancar | Verifique GET /ready primero; reintente tras unos segundos |
insufficient-margin | La orden violaría requisitos de margen en el preflight de admission | Recargue saldo, reduzca tamaño o use un precio limit más ajustado |
invalid-asset | asset id desconocido para este nodo | Obtenga el universe actual desde /info { type: “meta” } |
Revocar una clave API
Sección titulada «Revocar una clave API»Solo la wallet PRINCIPAL puede revocar una API key — por diseño, una API key filtrada no puede revocarse a sí misma. La wallet firma sobre keccak256("nexus-delegate-unlink:" + user + ":" + signer); al tener éxito, la entrada de la API key se elimina de authorized_signers y cualquier sesión activa que la usaba fallará en su próxima petición con session-not-found.
import requestsfrom eth_account import Accountfrom eth_utils import keccak
WALLET_KEY = "0x..." # clave privada de la wallet PRINCIPAL (NO la API key)USER = "0xABC..." # dirección wallet PRINCIPAL, 0x + 40 hex minúsculasAPI_KEY_ADDR = "0xXYZ..." # dirección de la API key a revocar (ver /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..." }