Skip to main content
This is the minimum bot skeleton: open a WebSocket, run the auth handshake, decode every incoming rfq, sign and return a quote, and reply to ping heartbeats. It quotes every RFQ with a fixed odds and fill size — real pricing logic belongs where the comments mark it.

Prerequisites

Export your market-maker credentials before running any example:
export MM_PRIVATE_KEY=0x...     # the EVM key registered with your MM account

Get Your User ID

Use the same EVM key you plan to run as a market maker. Wallet auth signs the plain-text Longshot sign-in message, sends the 65-byte signature as base64, and returns the user_id needed for whitelist requests.
Python
# pip install eth-account
import base64
import json
import os
import time
import urllib.request

from eth_account import Account
from eth_account.messages import encode_defunct


BASE_URL = "https://api.longshot.xyz"
PRIVATE_KEY = os.environ["MM_PRIVATE_KEY"]


def post_json(path: str, body: dict) -> dict:
    req = urllib.request.Request(
        BASE_URL + path,
        data=json.dumps(body).encode(),
        method="POST",
        headers={"content-type": "application/json", "accept": "application/json"},
    )
    with urllib.request.urlopen(req, timeout=15) as res:
        return json.loads(res.read())


account = Account.from_key(PRIVATE_KEY)
timestamp_ms = int(time.time() * 1000)
message = (
    f"Sign in to longshot.xyz\n\n"
    f"Address: {account.address}\n"
    f"Timestamp: {timestamp_ms}"
)
signed = Account.sign_message(encode_defunct(text=message), PRIVATE_KEY)

session = post_json("/v1/auth/wallet", {
    "address": account.address,
    "message": message,
    "signature": base64.b64encode(signed.signature).decode(),
})

print(session["user_id"])

Authenticate

Open the WebSocket, run the challenge-response handshake, and get a session token. Keep the same ws around — the Build Quote section attaches onto it.
# 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


URL = "ws://127.0.0.1:3001/ws"
PRIVATE_KEY = os.environ["MM_PRIVATE_KEY"]
AUTH_DOMAIN = b"longshot:mm:ws-auth:v1:longshot.xyz"


async def authenticate(ws) -> str:
    wallet = Account.from_key(PRIVATE_KEY).address
    await ws.send(json.dumps({"type": "auth"}))

    challenge = json.loads(await ws.recv())
    nonce_hex = challenge["nonce"].removeprefix("0x")
    nonce = bytes.fromhex(nonce_hex)
    msg = AUTH_DOMAIN + nonce + challenge["timestamp"].to_bytes(8, "little")
    sig = Account.sign_message(
        encode_defunct(primitive=msg),
        private_key=PRIVATE_KEY,
    ).signature

    await ws.send(json.dumps({
        "type": "auth_response",
        "wallet_address": wallet,
        "signature": sig.hex().removeprefix("0x"),
    }))
    result = json.loads(await ws.recv())
    if not result.get("success"):
        raise RuntimeError(f"auth failed: {result}")
    return result["session_token"]


async def main():
    async with websockets.connect(URL) as ws:
        await authenticate(ws)
        await receive_loop(ws)


asyncio.run(main())

Build Quote

build_quote fills the 97-byte QuoteResponse layout, signs the first 32 bytes with EIP-191, and wraps it in the { type, data } envelope. The receive loop attached below subscribes to RFQs, calls build_quote on every rfq, and pongs server pings.
# pip install eth-account websockets
import base64
import json
import os
from uuid import UUID

from eth_account import Account
from eth_account.messages import encode_defunct


PRIVATE_KEY = os.environ["MM_PRIVATE_KEY"]
ODDS_BPS = 25_000          # 2.5x
MAX_FILL_MICROS = 1_000_000  # 1 USDC


def build_quote(request_id: UUID) -> dict:
    buf = bytearray(97)

    # request_id from the decoded RFQ broadcast payload
    buf[0:16] = request_id.bytes

    # odds in basis points (25000 = 2.5x)
    buf[16:20] = ODDS_BPS.to_bytes(4, "little")

    # max_fill_micros — cap in USDC micros
    buf[20:28] = MAX_FILL_MICROS.to_bytes(8, "little")

    # bytes 28..31 reserved (zero)

    # EIP-191 personal-sign over the first 32 bytes
    sig = Account.sign_message(
        encode_defunct(primitive=bytes(buf[0:32])),
        private_key=PRIVATE_KEY,
    ).signature
    buf[32:97] = sig

    data = base64.b64encode(bytes(buf)).rstrip(b"=").decode()
    return {"type": "quote", "data": data}


async def receive_loop(ws):
    """Run on the authenticated ws from the Authenticate section."""
    await ws.send(json.dumps({
        "type": "subscribe",
        "subscriptions": [{"kind": "all"}],
    }))

    async for raw in ws:
        msg = json.loads(raw)
        mtype = msg.get("type")
        if mtype == "subscribed":
            continue
        elif mtype == "rfq":
            payload = base64.b64decode(
                msg["data"] + "=" * ((-len(msg["data"])) % 4)
            )
            request_id = UUID(bytes=bytes(payload[0:16]))
            # TODO: replace fixed quote with real pricing.
            await ws.send(json.dumps(build_quote(request_id)))
        elif mtype == "ping":
            await ws.send(json.dumps({"type": "pong"}))

Next Steps

Authentication

Deep dive on the challenge-response handshake and session lifecycle.

Receive RFQs

Full BroadcastRfqRequest byte layout and typed decoder helpers.

Submit Quotes

Full QuoteResponse layout, liability math, and build examples.

Acks and Outcomes

Handle quote_ack and quote_result notifications.