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
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.positionIds[0],
    side: 'BUY',
    price: 0.65,
    size: 100,
    orderType: 'GTC',
  }),
  {
    statusCodes: [429, 500, 502, 503, 504],
    maxRetries: 3,
    delays: [1000, 2000, 4000], // Milliseconds between retries
    onRetry: (error, attempt) => {
      console.warn(`Retry ${attempt}: ${error.message}`);
    },
  }
);

withRetry Options

OptionTypeDefaultDescription
statusCodesnumber[][429, 500, 502, 503, 504]HTTP status codes that trigger a retry
maxRetriesnumber3Maximum number of retry attempts
delaysnumber[][1000, 2000, 4000]Delay in ms before each retry. If fewer delays than retries, the last delay is reused.
onRetry(error: ApiError, attempt: 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: 30_000,
  })
  async placeOrder(params: CreateOrderParams) {
    return this.orderClient.createOrder(params);
  }
}

Decorator Options

OptionTypeDefaultDescription
statusCodesnumber[][429, 500, 502, 503, 504]HTTP status codes that trigger a retry
maxRetriesnumber3Maximum number of retry attempts
exponentialBasenumber2Base for exponential backoff calculation (base^attempt * 1000 ms)
maxDelaynumber30000Maximum delay in milliseconds between retries
The delay for each retry is calculated as:
delay = min(exponentialBase ^ attempt * 1000, maxDelay)
For example, with exponentialBase: 2 and maxDelay: 30000:
AttemptDelay
12,000 ms
24,000 ms
38,000 ms
416,000 ms
530,000 ms (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.positionIds[0],
    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.