Skip to main content

APIError Class

All HTTP errors from the SDK are thrown as APIError instances. Inspect the status, message, and data properties to determine what went wrong.
import { APIError } from '@limitless-exchange/sdk';

try {
  await orderClient.createOrder({ /* ... */ });
} catch (error) {
  if (error instanceof APIError) {
    console.error('Status:', error.status);   // HTTP status code
    console.error('Message:', error.message);  // Human-readable error
    console.error('Data:', error.data);        // Raw response body (if any)
  }
}

APIError Properties

PropertyTypeDescription
statusnumberHTTP status code (400, 401, 429, 500, etc.)
messagestringError description from the server
dataunknownRaw response body, useful for debugging

Common Status Codes

CodeMeaningAction
400Bad request (invalid parameters)Fix request parameters
401Unauthorized (invalid or missing API key)Check your LIMITLESS_API_KEY
403Forbidden (insufficient permissions)Verify account permissions
404Not found (invalid slug or resource)Check market slug or resource ID
425Too Early — receive-window check failed (POST /orders only)Re-stamp timestamp and resubmit. See Receive Window.
429Rate limitedBack off and retry with delay
500Internal server errorRetry with exponential backoff
502Bad gatewayRetry with exponential backoff
503Service unavailableRetry with exponential backoff
504Gateway timeoutRetry with exponential backoff

withRetry Wrapper

The SDK provides a withRetry utility function that wraps any async operation with configurable retry logic.
import { withRetry, APIError } from '@limitless-exchange/sdk';

const result = await withRetry(
  () => orderClient.createOrder({
    marketSlug: 'btc-100k-weekly',
    tokenId: market.tokens.yes,
    side: 'BUY',
    price: 0.65,
    size: 100,
    orderType: 'GTC',
  }),
  {
    statusCodes: [429, 500, 502, 503, 504],
    maxRetries: 3,
    delays: [1, 2, 4], // Seconds between retries
    onRetry: (attempt, error, delay) => {
      console.warn(`Retry ${attempt} in ${delay}s: ${error.message}`);
    },
  }
);

withRetry Options

OptionTypeDefaultDescription
statusCodesnumber[][429, 500, 502, 503, 504]HTTP status codes that trigger a retry
maxRetriesnumber3Maximum number of retry attempts
delaysnumber[]Explicit delays in seconds for each attempt. If omitted, exponential backoff is used. If fewer delays than retries, the last is reused.
exponentialBasenumber2Base for exponential backoff when delays is omitted (base^attempt seconds)
maxDelaynumber60Maximum backoff delay in seconds
onRetry(attempt: number, error: Error, delay: number) => voidCallback invoked before each retry
Only APIError instances with a matching status code trigger retries. Other errors (network failures, timeouts) are thrown immediately.

@retryOnErrors Decorator

For class-based architectures, use the @retryOnErrors decorator to add retry logic to individual methods.
import { retryOnErrors } from '@limitless-exchange/sdk';

class TradingBot {
  private orderClient: OrderClient;

  constructor(orderClient: OrderClient) {
    this.orderClient = orderClient;
  }

  @retryOnErrors({
    statusCodes: [429, 500, 502, 503, 504],
    maxRetries: 5,
    exponentialBase: 2,
    maxDelay: 60,
  })
  async placeOrder(params: CreateOrderParams) {
    return this.orderClient.createOrder(params);
  }
}

Decorator Options

@retryOnErrors takes the same RetryConfigOptions as withRetry.
OptionTypeDefaultDescription
statusCodesnumber[][429, 500, 502, 503, 504]HTTP status codes that trigger a retry
maxRetriesnumber3Maximum number of retry attempts
delaysnumber[]Explicit per-attempt delays in seconds (overrides exponential backoff)
exponentialBasenumber2Base for exponential backoff when delays is omitted (base^attempt seconds)
maxDelaynumber60Maximum backoff delay in seconds
onRetry(attempt: number, error: Error, delay: number) => voidCallback invoked before each retry
When delays is omitted, the delay for each retry is calculated (in seconds) as:
delay = min(exponentialBase ^ attempt, maxDelay)
For example, with exponentialBase: 2 and maxDelay: 60:
AttemptDelay
01 s
12 s
24 s
38 s
416 s
532 s
660 s (capped)

Rate Limiting with OrderQueue

For high-frequency trading scenarios, implement an OrderQueue to throttle outgoing requests and avoid 429 errors:
import { APIError } from '@limitless-exchange/sdk';

class OrderQueue {
  private queue: Array<() => Promise<void>> = [];
  private processing = false;
  private minDelayMs: number;

  constructor(minDelayMs = 200) {
    this.minDelayMs = minDelayMs;
  }

  async enqueue<T>(fn: () => Promise<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      this.queue.push(async () => {
        try {
          resolve(await fn());
        } catch (error) {
          reject(error);
        }
      });
      this.processQueue();
    });
  }

  private async processQueue() {
    if (this.processing) return;
    this.processing = true;

    while (this.queue.length > 0) {
      const task = this.queue.shift()!;
      await task();
      await new Promise((r) => setTimeout(r, this.minDelayMs));
    }

    this.processing = false;
  }
}

// Usage
const orderQueue = new OrderQueue(250); // 250ms between requests

const result = await orderQueue.enqueue(() =>
  orderClient.createOrder({
    marketSlug: 'btc-100k-weekly',
    tokenId: market.tokens.yes,
    side: 'BUY',
    price: 0.65,
    size: 100,
    orderType: 'GTC',
  })
);

Best Practices

Catch APIError separately from other errors. Network failures, JSON parse errors, and timeouts are not APIError instances and should be handled differently.
try {
  await orderClient.createOrder({ /* ... */ });
} catch (error) {
  if (error instanceof APIError) {
    // Server responded with an error status
    handleApiError(error);
  } else if (error instanceof TypeError) {
    // Network error (DNS, connection refused, etc.)
    console.error('Network error:', error.message);
  } else {
    // Unexpected error
    throw error;
  }
}
Client errors (400, 401, 403) indicate a problem with your request or credentials. Retrying them wastes time and API quota. Only retry transient server errors (429, 5xx).
When rate limited, the server may include a Retry-After header. If available, respect it. Otherwise, use exponential backoff starting at 1 second.
For the most robust setup, use OrderQueue to throttle request rate and withRetry or @retryOnErrors to handle transient failures:
const result = await orderQueue.enqueue(() =>
  withRetry(
    () => orderClient.createOrder({ /* ... */ }),
    { statusCodes: [429, 500, 502, 503, 504], maxRetries: 3 }
  )
);

Logging

The SDK provides optional logging through a simple ILogger interface. Logging is completely opt-in with zero overhead by default.

Quick Start

import { HttpClient, ConsoleLogger } from '@limitless-exchange/sdk';

const logger = new ConsoleLogger('info');

const httpClient = new HttpClient({
  baseURL: 'https://api.limitless.exchange',
  apiKey: process.env.LIMITLESS_API_KEY,
  logger,
});

Log Levels

LevelWhat’s Logged
debugRequest headers (API key redacted), request/response bodies, WebSocket events
infoAPI requests (method + URL), successful responses, order creation/cancellation
warnWarnings only
errorAPI errors (with status code), network errors, WebSocket connection errors

Custom Logger for Production

Implement the ILogger interface to integrate with your own logging infrastructure:
import { ILogger } from '@limitless-exchange/sdk';

class MyLogger implements ILogger {
  debug(message: string, meta?: Record<string, any>): void { /* ... */ }
  info(message: string, meta?: Record<string, any>): void { /* ... */ }
  warn(message: string, meta?: Record<string, any>): void { /* ... */ }
  error(message: string, error?: Error, meta?: Record<string, any>): void { /* ... */ }
}

const httpClient = new HttpClient({
  baseURL: 'https://api.limitless.exchange',
  apiKey: process.env.LIMITLESS_API_KEY,
  logger: new MyLogger(),
});

Passing Logger to SDK Components

All SDK components accept an optional logger:
const logger = new ConsoleLogger('info');

const httpClient = new HttpClient({ baseURL: '...', logger });
const marketFetcher = new MarketFetcher(httpClient, logger);
const orderClient = new OrderClient({ httpClient, wallet, logger });
The SDK automatically redacts API keys in logs (shown as ***). Private keys are never logged.