Skip to main content

Overview

This guide walks through a complete Node.js/TypeScript integration with the Limitless Exchange API: authentication, market data, EIP-712 order signing with viem, order submission, and WebSocket subscriptions for real-time data.
Never expose your private key. Use environment variables and never commit secrets to version control. For production, consider a dedicated key management solution.

Prerequisites

Install the required dependencies:
pnpm add socket.io-client cross-fetch ethers viem
PackagePurpose
socket.io-clientWebSocket connection to real-time market data
cross-fetchHTTP requests (or use native fetch in Node 18+)
ethersOptional; viem handles EIP-712 signing
viemEIP-712 signing, wallet client, parseUnits
If using Node 18+, you can omit cross-fetch and use the built-in fetch.

Environment Variables

Create a .env file (and add it to .gitignore):
LMTS_TOKEN_ID=your_token_id
LMTS_TOKEN_SECRET=your_base64_secret
PRIVATE_KEY=0x...
API_URL=https://api.limitless.exchange
OWNER_ID=12345
OWNER_ID is your Limitless profile ID (numeric). Obtain it from your account settings or from an authenticated API response. It is required for order submission.

1. Authentication

Authenticated REST requests (e.g. submitting orders) are signed with HMAC-SHA256 using your scoped API token. Each request carries three headers — lmts-api-key, lmts-timestamp, and lmts-signature — computed over a canonical message of timestamp, HTTP method, request path (with query string), and body. Public market data (browsing markets, orderbooks) needs no authentication. See Authentication for the full reference.
import { createHmac } from 'crypto';

const API_URL = process.env.API_URL ?? 'https://api.limitless.exchange';
const TOKEN_ID = process.env.LMTS_TOKEN_ID;
const TOKEN_SECRET = process.env.LMTS_TOKEN_SECRET; // base64-encoded

// Build HMAC auth headers for a single request (method + path + body).
function signRequest(
tokenId: string,
secret: string,
method: string,
path: string,
body: string = '',
): Record<string, string> {
const timestamp = new Date().toISOString();
const message = `${timestamp}\n${method}\n${path}\n${body}`;
const signature = createHmac('sha256', Buffer.from(secret, 'base64')).update(message).digest('base64');
return { 'lmts-api-key': tokenId, 'lmts-timestamp': timestamp, 'lmts-signature': signature };
}

// Public endpoints (market data, orderbooks) require no authentication.
export async function apiGet<T>(path: string): Promise<T> {
const res = await fetch(`${API_URL}${path}`);
if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`);
return res.json();
}

// Authenticated POST — signs the exact body bytes that are sent.
export async function apiPost<T>(path: string, body: unknown): Promise<T> {
if (!TOKEN_ID || !TOKEN_SECRET) {
  throw new Error('Missing LMTS_TOKEN_ID / LMTS_TOKEN_SECRET. Derive a token at limitless.exchange → API Tokens');
}
const serialized = JSON.stringify(body);
const auth = signRequest(TOKEN_ID, TOKEN_SECRET, 'POST', path, serialized);
const res = await fetch(`${API_URL}${path}`, {
  method: 'POST',
  headers: { ...auth, 'Content-Type': 'application/json' },
  body: serialized,
});
if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`);
return res.json();
}
Generating lmts-signature by hand on the command line is awkward — for shell usage, run the signRequest helper above (or your SDK) to produce the three headers. See Authentication for ready-to-use signing snippets.

2. Fetching Market Data and Caching Venue Info

Fetch market details via GET /markets/:slug. The response includes venue (exchange, adapter) and tokens (the YES and NO token IDs) for CLOB markets.
1

Fetch market by slug

Call GET /markets/{slug} to retrieve market data including venue addresses.
2

Extract venue and token IDs

Use venue.exchange as the EIP-712 verifyingContract. Use tokens.yes for YES and tokens.no for NO.
3

Cache the venue

Venue data is static per market. Fetch once and reuse for all orders on that market.
interface Venue {
  exchange: string;
  adapter: string | null;
}

interface ClobMarket {
  slug: string;
  venue: Venue;
  tokens: { yes: string; no: string };  // YES / NO token IDs
}

export async function getMarket(slug: string): Promise<ClobMarket> {
  const data = await apiGet<ClobMarket>(`/markets/${slug}`);
  if (!data.venue?.exchange || !data.tokens?.yes || !data.tokens?.no) {
    throw new Error(`Market ${slug} is not a CLOB market or missing venue data`);
  }
  return data;
}

3. Creating Signed Orders with viem (EIP-712)

Build the order payload and sign it with viem’s signTypedData. The domain uses venue.exchange as verifyingContract.

EIP-712 Domain and Types

import { createWalletClient, http, privateKeyToAccount } from 'viem';
import { base } from 'viem/chains';
import { parseUnits } from 'viem';

const chainId = 8453;  // Base

const orderType = {
  Order: [
    { name: 'salt', type: 'uint256' },
    { name: 'maker', type: 'address' },
    { name: 'signer', type: 'address' },
    { name: 'taker', type: 'address' },
    { name: 'tokenId', type: 'uint256' },
    { name: 'makerAmount', type: 'uint256' },
    { name: 'takerAmount', type: 'uint256' },
    { name: 'expiration', type: 'uint256' },
    { name: 'nonce', type: 'uint256' },
    { name: 'feeRateBps', type: 'uint256' },
    { name: 'side', type: 'uint8' },
    { name: 'signatureType', type: 'uint8' },
  ],
} as const;

Signing Logic

const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';

export async function createSignedOrder(params: {
  market: ClobMarket;
  side: 0 | 1;           // 0 = BUY, 1 = SELL
  outcome: 'yes' | 'no'; // tokens.yes = YES, tokens.no = NO
  priceCents: number;    // e.g. 65 = $0.65
  shares: number;       // number of shares
  orderType: 'GTC' | 'FOK';
  nonce: number;
}): Promise<{ order: Record<string, unknown>; signature: string }> {
  const { market, side, outcome, priceCents, shares, nonce } = params;
  const privateKey = process.env.PRIVATE_KEY as `0x${string}`;
  if (!privateKey) throw new Error('PRIVATE_KEY required');

  const account = privateKeyToAccount(privateKey);
  const walletClient = createWalletClient({
    account,
    chain: base,
    transport: http(),
  });

  const tokenId = outcome === 'yes' ? market.tokens.yes : market.tokens.no;
  const price = priceCents / 100;
  const sharesScaled = parseUnits(shares.toString(), 6);
  const makerAmount = side === 0
    ? parseUnits((price * shares).toFixed(6), 6)  // BUY: offer USDC
    : sharesScaled;                                 // SELL: offer shares
  const takerAmount = side === 0
    ? sharesScaled                                  // BUY: want shares
    : parseUnits((price * shares).toFixed(6), 6);   // SELL: want USDC

  const salt = BigInt(Date.now());
  const expiration = 0n;

  const order = {
    salt,
    maker: account.address,
    signer: account.address,
    taker: ZERO_ADDRESS,
    tokenId: BigInt(tokenId),
    makerAmount,
    takerAmount,
    expiration,
    nonce: BigInt(nonce),
    feeRateBps: 0n,
    side: params.side as 0 | 1,
    signatureType: 0,  // EOA
  };

  const domain = {
    name: 'Limitless CTF Exchange',
    version: '1',
    chainId,
    verifyingContract: market.venue.exchange as `0x${string}`,
  };

  const signature = await walletClient.signTypedData({
    domain,
    types: orderType,
    primaryType: 'Order',
    message: order,
  });

  return {
    // Send large integer fields as decimal strings to preserve precision
    // (salt can exceed JS's safe-integer range; Number() would corrupt it).
    order: {
      salt: order.salt.toString(),
      maker: account.address,
      signer: account.address,
      taker: ZERO_ADDRESS,
      tokenId: tokenId,
      makerAmount: order.makerAmount.toString(),
      takerAmount: order.takerAmount.toString(),
      expiration: '0',
      nonce: order.nonce.toString(),
      feeRateBps: 0,
      side: params.side,
      signatureType: 0,
      signature,
    },
    signature,
  };
}
USDC uses 6 decimals. Use parseUnits(value, 6) for amounts. Prices are in dollars; multiply price * shares before scaling.

4. Submitting Orders

Submit the signed order to POST /orders with order, orderType, marketSlug, and ownerId.
interface CreateOrderPayload {
  order: Record<string, unknown>;
  orderType: 'GTC' | 'FOK';
  marketSlug: string;
  ownerId: number;
}

export async function submitOrder(payload: CreateOrderPayload) {
  return apiPost<{ order: unknown; makerMatches?: unknown[] }>('/orders', payload);
}

// Example usage
const market = await getMarket('btc-100k-weekly');
const { order } = await createSignedOrder({
  market,
  side: 0,           // BUY
  outcome: 'yes',
  priceCents: 65,
  shares: 10,
  orderType: 'GTC',
  nonce: 0,
});

const result = await submitOrder({
  order,
  orderType: 'GTC',
  marketSlug: market.slug,
  ownerId: Number(process.env.OWNER_ID),
});

5. WebSocket Subscription for Real-Time Data

Connect to wss://ws.limitless.exchange with namespace /markets using socket.io-client. Emit subscribe_market_prices with marketAddresses and/or marketSlugs.
Subscriptions replace previous ones. To subscribe to both AMM (by address) and CLOB (by slug), send both marketAddresses and marketSlugs in a single subscribe_market_prices call.
import { io } from 'socket.io-client';

const WS_URL = 'wss://ws.limitless.exchange';
const NAMESPACE = '/markets';

// Market price/orderbook streams are public — no authentication required.
const socket = io(`${WS_URL}${NAMESPACE}`, {
  transports: ['websocket'],
});

socket.on('connect', () => {
  socket.emit('subscribe_market_prices', {
    marketAddresses: ['0x1234...'],   // AMM markets
    marketSlugs: ['btc-100k-weekly'], // CLOB markets
  });
});

socket.on('newPriceData', (data: { marketAddress: string; updatedPrices: { yes: string; no: string } }) => {
  console.log('AMM price update:', data.marketAddress, data.updatedPrices);
});

socket.on('orderbookUpdate', (data: { marketSlug: string; orderbook: unknown }) => {
  console.log('CLOB orderbook update:', data.marketSlug);
});

socket.on('disconnect', (reason) => {
  console.log('Disconnected:', reason);
});
EventDescription
newPriceDataAMM market price update
orderbookUpdateCLOB orderbook update

Complete End-to-End Example

import { getMarket } from './market';
import { createSignedOrder } from './orders';
import { submitOrder } from './submit';
import { io } from 'socket.io-client';

const WS_URL = 'wss://ws.limitless.exchange';

async function main() {
const marketSlug = process.argv[2] ?? 'btc-100k-weekly';
const market = await getMarket(marketSlug);
console.log('Market:', market.slug, 'Venue:', market.venue.exchange);

const { order } = await createSignedOrder({
  market,
  side: 0,
  outcome: 'yes',
  priceCents: 65,
  shares: 5,
  orderType: 'GTC',
  nonce: 0,
});

const result = await submitOrder({
  order,
  orderType: 'GTC',
  marketSlug: market.slug,
  ownerId: Number(process.env.OWNER_ID),
});
console.log('Order submitted:', result);

// Public market stream — no authentication required.
const socket = io(`${WS_URL}/markets`, {
  transports: ['websocket'],
});

socket.on('connect', () => {
  socket.emit('subscribe_market_prices', { marketSlugs: [marketSlug] });
});

socket.on('orderbookUpdate', (data) => {
  console.log('Orderbook:', data);
});
}

main().catch(console.error);

Reference Summary

ItemValue
API base URLhttps://api.limitless.exchange
WebSocket URLwss://ws.limitless.exchange
Namespace/markets
ChainBase (chainId: 8453)
EIP-712 domain nameLimitless CTF Exchange
EIP-712 version1
USDC decimals6
Side0 = BUY, 1 = SELL
signatureType0 = EOA
Order typesGTC, FOK

Next Steps

EIP-712 Signing

Deep dive into order structure and signing.

Venue System

Token approvals and venue addresses.

WebSocket Events

Full event reference for real-time data.

API Reference

Complete endpoint documentation.