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):
API_KEY=lmts_your_api_key_here
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

All REST requests require the X-API-Key header. WebSocket connections pass the same key during the handshake.
const API_URL = process.env.API_URL ?? 'https://api.limitless.exchange';
const API_KEY = process.env.API_KEY;

if (!API_KEY?.startsWith('lmts_')) {
throw new Error('Invalid or missing API_KEY. Get one at limitless.exchange → Api keys');
}

export async function apiGet<T>(path: string): Promise<T> {
const res = await fetch(`${API_URL}${path}`, {
  headers: { 'X-API-Key': API_KEY },
});
if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`);
return res.json();
}

export async function apiPost<T>(path: string, body: unknown): Promise<T> {
const res = await fetch(`${API_URL}${path}`, {
  method: 'POST',
  headers: {
    'X-API-Key': API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`);
return res.json();
}

2. Fetching Market Data and Caching Venue Info

Fetch market details via GET /markets/:slug. The response includes venue (exchange, adapter) and positionIds for CLOB markets.
1

Fetch market by slug

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

Extract venue and position IDs

Use venue.exchange as the EIP-712 verifyingContract. Use positionIds[0] for YES and positionIds[1] 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;
}

interface ClobMarket {
  slug: string;
  venue: Venue;
  positionIds: [string, string];  // [YES, NO]
}

export async function getMarket(slug: string): Promise<ClobMarket> {
  const data = await apiGet<ClobMarket & { position_ids?: string[] }>(`/markets/${slug}`);
  const positionIds = (data.positionIds ?? data.position_ids ?? []) as [string, string];
  if (!data.venue?.exchange || positionIds.length < 2) {
    throw new Error(`Market ${slug} is not a CLOB market or missing venue data`);
  }
  return { ...data, positionIds };
}
For markets that use position_ids (snake_case) in the API response, map to positionIds in your code. The first element is YES, the second is NO.

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'; // positionIds[0]=YES, positionIds[1]=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.positionIds[0] : market.positionIds[1];
  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 {
    order: {
      salt: Number(order.salt),
      maker: account.address,
      signer: account.address,
      taker: ZERO_ADDRESS,
      tokenId: tokenId,
      makerAmount: Number(order.makerAmount),
      takerAmount: Number(order.takerAmount),
      expiration: 0,
      nonce: Number(order.nonce),
      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';

const socket = io(`${WS_URL}${NAMESPACE}`, {
  transports: ['websocket'],
  extraHeaders: {
    'X-API-Key': process.env.API_KEY ?? '',
  },
});

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 API_KEY = process.env.API_KEY!;
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);

const socket = io(`${WS_URL}/markets`, {
  transports: ['websocket'],
  extraHeaders: { 'X-API-Key': API_KEY },
});

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