Skip to main content
# pip install eth-account websockets
import asyncio
import json
import os

import websockets
from eth_account import Account
from eth_account.messages import encode_defunct


AUTH_DOMAIN = b"longshot:mm:ws-auth:v1:longshot.xyz"


async def authenticate(url: str) -> str:
    private_key = os.environ["MM_PRIVATE_KEY"]
    wallet_address = Account.from_key(private_key).address

    async with websockets.connect(url) as ws:
        await ws.send(json.dumps({"type": "auth"}))

        challenge = json.loads(await ws.recv())
        if challenge.get("type") != "auth_challenge":
            raise RuntimeError(f"unexpected message: {challenge}")

        nonce_hex = challenge["nonce"]
        timestamp = int(challenge["timestamp"])

        nonce = bytes.fromhex(nonce_hex.removeprefix("0x"))
        if len(nonce) != 32:
            raise ValueError("auth nonce must decode to 32 bytes")

        auth_message = AUTH_DOMAIN + nonce + timestamp.to_bytes(8, "little", signed=False)
        if len(auth_message) != 75:
            raise ValueError("auth message must be exactly 75 bytes")

        signed = Account.sign_message(
            encode_defunct(primitive=auth_message),
            private_key=private_key,
        )

        auth_response = {
            "type": "auth_response",
            "wallet_address": wallet_address,
            "signature": signed.signature.hex().removeprefix("0x"),
        }
        await ws.send(json.dumps(auth_response))

        result = json.loads(await ws.recv())
        if result.get("type") != "auth_result" or not result.get("success"):
            raise RuntimeError(f"auth failed: {result}")

        return result["session_token"]


asyncio.run(authenticate("ws://127.0.0.1:3001/ws"))
Market-maker RFQ auth is a WebSocket-only challenge-response flow. Do not use POST /v1/auth/wallet or POST /v1/auth/session for the RFQ bot handshake; those endpoints are for first-party/API user login. The WebSocket handshake returns the MM bearer session used by MM-authenticated HTTP routes.

Flow

1

Open the WebSocket

Connect to the environment URL and send { "type": "auth" }.
2

Receive the challenge

Server replies with auth_challenge containing a 32-byte hex nonce and a Unix-seconds timestamp.
3

Sign the 75-byte message

Build b"longshot:mm:ws-auth:v1:longshot.xyz" || nonce_bytes || little_endian_u64(timestamp) and sign those 75 bytes with EIP-191 personal-sign. Do not sign the nonce hex string.
4

Submit the response

Send auth_response with wallet_address and signature.
5

Store the session token

Server responds with auth_result { success, session_token }. The session token is a bearer token for MM-authenticated HTTP routes and is valid for 1 hour.

Messages

auth_challenge — server → client

type
string
Always "auth_challenge".
nonce
string
64 hex characters (32 random bytes).
timestamp
uint64
Server time in Unix seconds.

auth_response — client → server

type
string
required
Must be "auth_response".
wallet_address
string
required
Registered EVM wallet address (EIP-55 checksum or lowercase hex).
signature
string
required
130 hex characters. EIP-191 personal-sign over b"longshot:mm:ws-auth:v1:longshot.xyz" || nonce_bytes || little_endian_u64(timestamp) (65-byte r || s || v; v may be 0/1 or 27/28). The signed payload is 75 bytes before the EIP-191 prefix is applied: 35-byte domain, 32-byte decoded nonce, and 8-byte little-endian timestamp.

auth_result — server → client

type
string
Always "auth_result".
success
boolean
true when the signature verified and the wallet matches a known market maker.
error
string | null
Failure reason when success is false.
session_token
string | null
Bearer session token when success is true. Use it as Authorization: Bearer <session_token> for MM-authenticated HTTP routes such as taker PnL. Valid for 1 hour.

Operational Limits

  • Authenticate within 10 seconds of connecting.
  • Submit auth_response before the server challenge expires. The challenge lifetime is 30 seconds.
  • Heartbeat interval is 15 seconds; the server disconnects after 3 missed heartbeats.

Heartbeats

The server sends JSON heartbeats:
{ "type": "ping", "timestamp": 1712345678901 }
Reply with a pong:
{ "type": "pong" }
The WebSocket auth state is connection-scoped and valid for 1 hour. The bearer session token returned by auth_result has the same 1-hour lifetime. Reconnect and re-authenticate if the connection drops, the server sends AUTH_EXPIRED, or the bearer session expires.