JavaScript / TypeScript SDK

Official JS/TS client for the Propr trading API

Node 18+TypeScriptPackage: propr-sdk
●
Production Environment —For actual trading. Trades execute on real Hyperliquid markets with real funds.

Installation

Required
npm install ulid
# or
yarn add ulid
# or
pnpm add ulid

Create a .env file in your project root:

PROPR_API_KEY=pk_live_your_api_key_here
PROPR_API_URL=https://api.propr.xyz/v1
PROPR_WS_URL=wss://api.propr.xyz/ws

Get your API key from Settings.

SDK Source

Copy & Paste

Save this as propr-sdk.ts in your project. Works in Node.js 18+ (native fetch) and modern browsers. For older Node versions, install node-fetch.

/**
 * Propr TypeScript/JavaScript SDK
 * Official client for the Propr trading API.
 *
 * Usage:
 *   import { ProprClient } from './propr-sdk';
 *
 *   const client = new ProprClient();
 *   await client.setup();
 *   console.log(await client.getPositions());
 */

import { ulid } from 'ulid';

const DEFAULT_BASE_URL = 'https://api.propr.xyz/v1';

// ── Types ──

export interface ProprClientOptions {
  apiKey?: string;
  baseUrl?: string;
  timeout?: number;
}

export interface Order {
  orderId: string;
  intentId: string;
  orderGroupId: string | null;
  exchangeOrderId: string | null;
  userId: string;
  accountId: string;
  positionId: string | null;
  exchange: string;
  productType: string;
  asset: string;
  base: string;
  quote: string;
  type: string;
  side: string;
  positionSide: string;
  timeInForce: string;
  quantity: string;
  price: string | null;
  triggerPrice: string | null;
  closePosition: boolean;
  reduceOnly: boolean;
  cumulativeQuantity: string;
  cumulativeQuote: string;
  averageFillPrice: string | null;
  cumulativeTradingFees: string;
  tradingFeeRate: string;
  expiresAt: string | null;
  filledAt: string | null;
  cancelledAt: string | null;
  status: string;
  createdAt: string;
  updatedAt: string;
}

export interface Position {
  positionId: string;
  userId: string;
  accountId: string;
  exchange: string;
  productType: string;
  status: string;
  asset: string;
  base: string;
  quote: string;
  positionSide: string;
  leverage: string;
  marginMode: string;
  quantity: string;
  entryPrice: string;
  breakEvenPrice: string;
  markPrice: string;
  liquidationPrice: string;
  unrealizedPnl: string;
  realizedPnl: string;
  marginUsed: string;
  notionalValue: string;
  cumulativeFunding: string;
  cumulativeTradingFees: string;
  tradingFeeRate: string;
  returnOnEquity: string;
  createdAt: string;
  updatedAt: string;
  closedAt: string | null;
}

export interface Trade {
  tradeId: string;
  userId: string;
  accountId: string;
  orderId: string;
  positionId: string;
  exchangeTradeId: string | null;
  transactionHash: string | null;
  exchange: string;
  productType: string;
  type: string;
  liquidityType: string;
  asset: string;
  base: string;
  quote: string;
  side: string;
  positionSide: string;
  quantity: string;
  price: string;
  quoteQuantity: string;
  fee: string;
  feeAsset: string;
  feeRate: string;
  leverage: string;
  marginMode: string;
  realizedPnl: string;
  positionSizeBefore: string;
  slippage: string;
  markPriceAtOrder: string;
  isLiquidation: boolean;
  executedAt: string;
  createdAt: string;
}

export interface MarginConfig {
  configId: string;
  accountId: string;
  exchange: string;
  asset: string;
  marginMode: string;
  leverage: string;
  createdAt: string;
  updatedAt: string;
}

export interface LeverageLimits {
  defaultMax: number;
  overrides: Record<string, number>;
}

export type ChallengeAttemptStatus = 'active' | 'passed' | 'failed';
export type ChallengeFailureReason =
  | 'max_drawdown_exceeded'
  | 'max_daily_loss_exceeded'
  | 'profit_target_not_met';
export type PhaseAttemptStatus = 'active' | 'not_started' | 'passed' | 'failed';

export interface ChallengeAttemptPhase {
  attemptPhaseId: string;
  attemptId: string;
  phaseId: string;
  order: number;
  status: PhaseAttemptStatus;
  startingBalance: string;
  endingBalance: string | null;
  failureReason: ChallengeFailureReason | null;
  failureDetails: Record<string, any> | null;
  startedAt: string | null;
  endedAt: string | null;
  createdAt: string;
  updatedAt: string;
}

export interface ChallengeAttempt {
  attemptId: string;
  userId: string;
  purchaseId: string;
  challengeId: string;
  accountId: string;
  currentPhaseId: string | null;
  status: ChallengeAttemptStatus;
  failureReason: ChallengeFailureReason | null;
  failureDetails: Record<string, any> | null;
  startedAt: string | null;
  endedAt: string | null;
  createdAt: string;
  updatedAt: string;
  phases?: ChallengeAttemptPhase[];
  currentPhase?: ChallengeAttemptPhase;
}

export interface CreateOrderParams {
  side: 'buy' | 'sell';
  positionSide: 'long' | 'short';
  orderType: 'market' | 'limit' | 'stop_market' | 'stop_limit' | 'take_profit_market' | 'take_profit_limit';
  asset: string;
  base: string;
  quote: string;
  quantity: string;
  price?: string;
  triggerPrice?: string;
  timeInForce?: 'GTC' | 'IOC' | 'FOK' | 'GTX';
  reduceOnly?: boolean;
  closePosition?: boolean;
}

// ── Error ──

export class ProprAPIError extends Error {
  statusCode: number;
  code: number | null;

  constructor(statusCode: number, code: number | null, message: string) {
    super(`[${statusCode}] ${code}: ${message}`);
    this.name = 'ProprAPIError';
    this.statusCode = statusCode;
    this.code = code;
  }
}

// ── Client ──

export class ProprClient {
  private apiKey: string;
  private baseUrl: string;
  private timeout: number;
  public accountId: string | null = null;

  constructor(options: ProprClientOptions = {}) {
    this.apiKey =
      options.apiKey ||
      process.env.PROPR_API_KEY ||
      '';
    this.baseUrl =
      options.baseUrl ||
      process.env.PROPR_API_URL ||
      DEFAULT_BASE_URL;
    this.timeout = options.timeout || 30_000;

    if (!this.apiKey) {
      throw new Error(
        'API key required. Set PROPR_API_KEY env var or pass apiKey option.\n' +
        'Get your key at https://app.propr.xyz/settings'
      );
    }
  }

  // ── Internal ──

  private async request<T = any>(
    method: string,
    path: string,
    options: { params?: Record<string, any>; body?: any } = {},
  ): Promise<T> {
    let url = `${this.baseUrl}${path}`;

    if (options.params) {
      const searchParams = new URLSearchParams();
      for (const [key, value] of Object.entries(options.params)) {
        if (value !== undefined && value !== null) {
          searchParams.set(key, String(value));
        }
      }
      const qs = searchParams.toString();
      if (qs) url += `?${qs}`;
    }

    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);

    try {
      const response = await fetch(url, {
        method,
        headers: {
          'Content-Type': 'application/json',
          'X-API-Key': this.apiKey,
        },
        body: options.body ? JSON.stringify(options.body) : undefined,
        signal: controller.signal,
      });

      if (!response.ok) {
        let code: number | null = null;
        let message = 'unknown_error';
        try {
          const body = await response.json();
          code = body.code ?? null;
          message = body.message ?? message;
        } catch {}
        throw new ProprAPIError(response.status, code, message);
      }

      return response.json();
    } finally {
      clearTimeout(timeoutId);
    }
  }

  private get<T = any>(path: string, params?: Record<string, any>): Promise<T> {
    return this.request<T>('GET', path, { params });
  }

  private post<T = any>(path: string, body?: any): Promise<T> {
    return this.request<T>('POST', path, { body });
  }

  private put<T = any>(path: string, body?: any): Promise<T> {
    return this.request<T>('PUT', path, { body });
  }

  private accountPath(suffix: string): string {
    if (!this.accountId) {
      throw new Error(
        'accountId not set. Call client.setup() first or set client.accountId manually.'
      );
    }
    return `/accounts/${this.accountId}${suffix}`;
  }

  // ── Setup ──

  async setup(accountId?: string): Promise<string> {
    if (accountId) {
      this.accountId = accountId;
      return this.accountId;
    }

    const attempts = await this.getChallengeAttempts({ status: 'active' });
    if (!attempts.length) {
      throw new Error(
        'No active challenge found. Purchase a challenge at ' +
        'https://app.propr.xyz/dashboard first.'
      );
    }
    this.accountId = attempts[0].accountId;
    return this.accountId;
  }

  // ── Health ──

  async health(): Promise<{ status: string }> {
    return this.get('/health');
  }

  async healthServices(): Promise<{ core: string }> {
    return this.get('/health/services');
  }

  // ── User ──

  async getUser(): Promise<any> {
    return this.get('/users/me');
  }

  // ── Challenges ──

  async getChallenges(params: {
    challengeId?: string;
    productId?: string;
    currency?: string;
    exchange?: string;
    limit?: number;
    offset?: number;
  } = {}): Promise<any[]> {
    const res = await this.get<{ data: any[] }>('/challenges', {
      limit: 20,
      offset: 0,
      ...params,
    });
    return res.data ?? [];
  }

  // ── Challenge Attempts ──

  async getChallengeAttempts(params: {
    attemptId?: string;
    challengeId?: string;
    status?: ChallengeAttemptStatus;
    limit?: number;
    offset?: number;
  } = {}): Promise<ChallengeAttempt[]> {
    const res = await this.get<{ data: ChallengeAttempt[] }>('/challenge-attempts', {
      limit: 20,
      offset: 0,
      ...params,
    });
    return res.data ?? [];
  }

  async getChallengeAttempt(attemptId: string): Promise<ChallengeAttempt> {
    return this.get<ChallengeAttempt>(`/challenge-attempts/${attemptId}`);
  }

  // Order list filters (exact enum values — invalid values return HTTP 400):
  // side: buy | sell
  // positionSide: long | short
  // type: market | limit | stop_market | stop_limit | take_profit_market | take_profit_limit
  // status: pending | open | partially_filled | filled | cancelled | rejected | expired
  // (not "active" or "triggered" — those are other APIs / WS events)

  async getOrders(params: {
    orderId?: string;
    tradeId?: string;
    positionId?: string;
    base?: string;
    quote?: string;
    side?: string;
    positionSide?: string;
    type?: string;
    status?: string;
    limit?: number;
    offset?: number;
  } = {}): Promise<Order[]> {
    const res = await this.get<{ data: Order[] }>(
      this.accountPath('/orders'),
      { limit: 20, offset: 0, ...params },
    );
    return res.data ?? [];
  }

  async createOrder(params: CreateOrderParams): Promise<Order[]> {
    const order: Record<string, any> = {
      accountId: this.accountId,
      intentId: ulid(),
      exchange: 'hyperliquid',
      type: params.orderType,
      side: params.side,
      positionSide: params.positionSide,
      productType: 'perp',
      timeInForce: params.timeInForce ?? (params.orderType === 'market' ? 'IOC' : 'GTC'),
      asset: params.asset,
      base: params.base,
      quote: params.quote,
      quantity: params.quantity,
      reduceOnly: params.reduceOnly ?? false,
      closePosition: params.closePosition ?? false,
    };
    if (params.price !== undefined) order.price = params.price;
    if (params.triggerPrice !== undefined) order.triggerPrice = params.triggerPrice;

    const res = await this.post<{ data: Order[] }>(
      this.accountPath('/orders'),
      { orders: [order] },
    );
    return res.data ?? [];
  }

  async createOrders(orders: Record<string, any>[]): Promise<Order[]> {
    for (const order of orders) {
      if (!order.intentId) order.intentId = ulid();
      if (!order.accountId) order.accountId = this.accountId;
    }

    const res = await this.post<{ data: Order[] }>(
      this.accountPath('/orders'),
      { orders },
    );
    return res.data ?? [];
  }

  async cancelOrder(orderId: string): Promise<Order | null> {
    try {
      return await this.post<Order>(this.accountPath(`/orders/${orderId}/cancel`));
    } catch (err) {
      if (err instanceof ProprAPIError && err.statusCode === 400) {
        return null; // Already filled or cancelled
      }
      throw err;
    }
  }

  async cancelAllOrders(base?: string): Promise<Order[]> {
    const params: Record<string, any> = { status: 'open' };
    if (base) params.base = base;

    const openOrders = await this.getOrders(params);
    const cancelled: Order[] = [];

    for (const order of openOrders) {
      const result = await this.cancelOrder(order.orderId);
      if (result) cancelled.push(result);
    }
    return cancelled;
  }

  // ── Positions ──

  async getPositions(params: {
    positionId?: string;
    asset?: string;
    base?: string;
    quote?: string;
    positionSide?: string;
    status?: string;
    limit?: number;
    offset?: number;
    excludeZero?: boolean;
  } = {}): Promise<Position[]> {
    const { excludeZero = true, ...queryParams } = params;

    const res = await this.get<{ data: Position[] }>(
      this.accountPath('/positions'),
      { limit: 20, offset: 0, ...queryParams },
    );

    let positions = res.data ?? [];
    if (excludeZero) {
      positions = positions.filter((p) => parseFloat(p.quantity) > 0);
    }
    return positions;
  }

  async getOpenPositions(base?: string): Promise<Position[]> {
    return this.getPositions({ base, status: 'open', excludeZero: true });
  }

  // ── Trades ──

  async getTrades(params: {
    tradeId?: string;
    positionId?: string;
    orderId?: string;
    base?: string;
    quote?: string;
    side?: string;
    limit?: number;
    offset?: number;
  } = {}): Promise<Trade[]> {
    const res = await this.get<{ data: Trade[] }>(
      this.accountPath('/trades'),
      { limit: 20, offset: 0, ...params },
    );
    return res.data ?? [];
  }

  // ── Margin Configuration ──

  async getMarginConfig(asset: string): Promise<MarginConfig> {
    return this.get<MarginConfig>(this.accountPath(`/margin-config/${asset}`));
  }

  async updateMarginConfig(
    configId: string,
    asset: string,
    leverage: number,
    marginMode: string = 'cross',
  ): Promise<MarginConfig> {
    return this.put<MarginConfig>(
      this.accountPath(`/margin-config/${configId}`),
      {
        exchange: 'hyperliquid',
        asset,
        marginMode,
        leverage,
      },
    );
  }

  // ── Leverage Limits ──

  async getLeverageLimits(): Promise<LeverageLimits> {
    return this.get<LeverageLimits>('/leverage-limits/effective');
  }

  async maxLeverage(asset: string): Promise<number> {
    const limits = await this.getLeverageLimits();
    return limits.overrides[asset] ?? limits.defaultMax;
  }

  // ── Convenience Methods ──

  async marketBuy(base: string, quantity: string, quote = 'USDC'): Promise<Order[]> {
    return this.createOrder({
      side: 'buy',
      positionSide: 'long',
      orderType: 'market',
      asset: base,
      base,
      quote,
      quantity,
    });
  }

  async marketSell(
    base: string,
    quantity: string,
    quote = 'USDC',
    reduceOnly = true,
  ): Promise<Order[]> {
    return this.createOrder({
      side: 'sell',
      positionSide: 'long',
      orderType: 'market',
      asset: base,
      base,
      quote,
      quantity,
      reduceOnly,
    });
  }

  async limitBuy(
    base: string,
    quantity: string,
    price: string,
    quote = 'USDC',
  ): Promise<Order[]> {
    return this.createOrder({
      side: 'buy',
      positionSide: 'long',
      orderType: 'limit',
      asset: base,
      base,
      quote,
      quantity,
      price,
    });
  }

  async limitSell(
    base: string,
    quantity: string,
    price: string,
    quote = 'USDC',
    reduceOnly = true,
  ): Promise<Order[]> {
    return this.createOrder({
      side: 'sell',
      positionSide: 'long',
      orderType: 'limit',
      asset: base,
      base,
      quote,
      quantity,
      price,
      reduceOnly,
    });
  }

  async closePosition(base: string, quote = 'USDC'): Promise<Order[]> {
    const positions = await this.getOpenPositions(base);
    if (!positions.length) return [];

    const pos = positions[0];
    const closeSide = pos.positionSide === 'long' ? 'sell' : 'buy';

    return this.createOrder({
      side: closeSide as 'buy' | 'sell',
      positionSide: pos.positionSide as 'long' | 'short',
      orderType: 'market',
      asset: base,
      base,
      quote,
      quantity: pos.quantity,
      reduceOnly: true,
      closePosition: true,
    });
  }

  async setLeverage(
    asset: string,
    leverage: number,
    marginMode = 'cross',
  ): Promise<MarginConfig> {
    const config = await this.getMarginConfig(asset);
    return this.updateMarginConfig(config.configId, asset, leverage, marginMode);
  }
}

Quick Start

5 min

Get trading in a few lines:

import { ProprClient } from './propr-sdk';

const client = new ProprClient(); // reads PROPR_API_KEY from env
await client.setup();             // finds your active challenge account

// Check your positions
const positions = await client.getOpenPositions();
for (const p of positions) {
  console.log(`${p.positionSide} ${p.quantity} ${p.base} @ ${p.entryPrice}`);
}

// Place a market buy
const orders = await client.marketBuy('BTC', '0.001');
console.log(`Order placed: ${orders[0].orderId}`);

CommonJS (Node.js without ESM)

If you're not using ES modules, use require:

// propr-sdk.js (rename .ts to .js and remove type annotations)
// Or compile with: npx tsc propr-sdk.ts --target es2020 --module commonjs

const { ProprClient } = require('./propr-sdk');

async function main() {
  const client = new ProprClient();
  await client.setup();

  const positions = await client.getOpenPositions();
  console.log(positions);
}

main().catch(console.error);

API Reference

Constructor

const client = new ProprClient({
  apiKey: 'pk_live_...',            // or set PROPR_API_KEY env var
  baseUrl: 'https://...',           // or set PROPR_API_URL env var
  timeout: 30_000,                  // request timeout in ms (default 30s)
});

Setup

MethodDescription
await client.setup()Auto-detect account ID from active challenge
await client.setup(accountId)Use a specific account ID

Health

MethodAuthReturns
await client.health()No{ status: "OK" }
await client.healthServices()No{ core: "OK" | "ERROR" }

User

MethodAuthReturns
await client.getUser()YesUser profile object

Challenges

MethodAuthReturns
await client.getChallenges()NoChallenge[]
await client.getChallengeAttempts({ status: "active" })YesAttempt[]
await client.getChallengeAttempt(attemptId)YesAttempt

Orders

MethodDescription
await client.getOrders({ status: "open" })List orders with filters
await client.createOrder({ side, positionSide, orderType, ... })Place a single order
await client.createOrders([{...}, {...}])Place multiple orders
await client.cancelOrder(orderId)Cancel an order (null if already done)
await client.cancelAllOrders("BTC")Cancel all open orders

Convenience Order Methods

MethodDescription
await client.marketBuy("BTC", "0.001")Market buy (long)
await client.marketSell("BTC", "0.001")Market sell (close long, reduceOnly)
await client.limitBuy("BTC", "0.001", "90000")Limit buy (long)
await client.limitSell("BTC", "0.001", "100000")Limit sell (close, reduceOnly)
await client.closePosition("BTC")Close entire position (auto-detects side)

Positions

MethodDescription
await client.getPositions({ base: "BTC", status: "open" })List positions with filters
await client.getOpenPositions()Get all open non-zero positions
await client.getOpenPositions("ETH")Get open positions for a specific asset

Trades

MethodDescription
await client.getTrades({ base: "BTC" })List trade executions with filters

Margin & Leverage

MethodDescription
await client.getMarginConfig("BTC")Get margin config for an asset
await client.setLeverage("BTC", 5)Set leverage (creates/updates config)
await client.getLeverageLimits()Get max leverage limits for all assets
await client.maxLeverage("BTC")Get max leverage for a specific asset

TypeScript Types

Typed

The SDK exports full TypeScript interfaces for all response types:

import type {
  Order,
  Position,
  Trade,
  MarginConfig,
  LeverageLimits,
  CreateOrderParams,
  ProprClientOptions,
} from './propr-sdk';

All monetary values are returned as strings to preserve decimal precision. Use a library like decimal.js or bignumber.js for arithmetic — never use native Number for money calculations.

Examples

Recipes

Grid Bot

Place a grid of limit orders around the current price:

import { ProprClient } from './propr-sdk';

const client = new ProprClient();
await client.setup();

// Set 3x leverage on BTC
await client.setLeverage('BTC', 3);

// Get reference price from existing position
const positions = await client.getOpenPositions('BTC');
const midPrice = positions.length
  ? parseFloat(positions[0].markPrice)
  : 95_000;

// Place grid: 5 buy orders below, 5 sell orders above
const gridSpacing = 500;
const quantity = '0.001';

for (let i = 1; i <= 5; i++) {
  const buyPrice = String(midPrice - gridSpacing * i);
  const sellPrice = String(midPrice + gridSpacing * i);

  await client.limitBuy('BTC', quantity, buyPrice);
  await client.limitSell('BTC', quantity, sellPrice, 'USDC', false);
}

console.log(`Grid placed: 10 orders around ${midPrice}`);

Portfolio Monitor

import { ProprClient } from './propr-sdk';

const client = new ProprClient();
await client.setup();

const positions = await client.getOpenPositions();

let totalMargin = 0;
let totalUpnl = 0;

console.log('Asset        Side   Qty          Entry        Mark         uPnL');
console.log('-'.repeat(72));

for (const p of positions) {
  const margin = parseFloat(p.marginUsed);
  const upnl = parseFloat(p.unrealizedPnl);
  totalMargin += margin;
  totalUpnl += upnl;

  console.log(
    `${p.base.padEnd(12)} ${p.positionSide.padEnd(6)} ${p.quantity.padEnd(12)} ` +
    `${p.entryPrice.padEnd(12)} ${p.markPrice.padEnd(12)} ${upnl.toFixed(2)}`
  );
}

console.log('-'.repeat(72));
console.log(`Total margin: ${totalMargin.toFixed(2)} USDC`);
console.log(`Total uPnL:   ${totalUpnl.toFixed(2)} USDC`);

Stop Loss + Take Profit

import { ProprClient } from './propr-sdk';

const client = new ProprClient();
await client.setup();

// Open long position
await client.marketBuy('ETH', '0.1');

// Set stop loss at 2900
await client.createOrder({
  side: 'sell',
  positionSide: 'long',
  orderType: 'stop_market',
  asset: 'ETH',
  base: 'ETH',
  quote: 'USDC',
  quantity: '0.1',
  triggerPrice: '2900',
  reduceOnly: true,
});

// Set take profit at 3150
await client.createOrder({
  side: 'sell',
  positionSide: 'long',
  orderType: 'take_profit_market',
  asset: 'ETH',
  base: 'ETH',
  quote: 'USDC',
  quantity: '0.1',
  triggerPrice: '3150',
  reduceOnly: true,
});

console.log('Position opened with SL and TP');

WebSocket Client

Real-Time

For real-time events, use the native WebSocket API (browser) or ws package (Node.js):

// Node.js: npm install ws
import WebSocket from 'ws';

const WS_URL = process.env.PROPR_WS_URL || 'wss://api.propr.xyz/ws';
const API_KEY = process.env.PROPR_API_KEY;

function connect() {
  const ws = new WebSocket(WS_URL, {
    headers: { 'X-API-Key': API_KEY },
  });

  ws.on('open', () => console.log('Connected'));

  ws.on('message', (raw) => {
    const msg = JSON.parse(raw.toString());
    const { type, data } = msg;

    switch (type) {
      case 'connected':
        console.log(`Authenticated: ${data.userId}`);
        break;
      case 'position.updated':
        console.log(`Position: ${data.base} uPnL=${data.unrealizedPnl}`);
        break;
      case 'order.filled':
        console.log(`Filled: ${data.side} ${data.base} @ ${data.averageFillPrice}`);
        break;
      case 'trade.created':
        console.log(`Trade: ${data.type} ${data.quantity} ${data.base}`);
        break;
      default:
        console.log(`Event: ${type}`);
    }
  });

  ws.on('close', () => {
    console.log('Disconnected, reconnecting in 5s...');
    setTimeout(connect, 5000);
  });

  ws.on('error', (err) => console.error('WS error:', err.message));
}

connect();

Error Handling

The SDK throws ProprAPIError on API errors:

import { ProprClient, ProprAPIError } from './propr-sdk';

const client = new ProprClient();
await client.setup();

try {
  const orders = await client.marketBuy('BTC', '0.001');
} catch (err) {
  if (err instanceof ProprAPIError) {
    console.log(`API Error: status=${err.statusCode} code=${err.code}`);

    if (err.statusCode === 429) console.log('Rate limited - slow down');
    if (err.statusCode === 400) console.log('Bad request - check params');
    if (err.statusCode === 401) console.log('Invalid API key');
  } else {
    console.error('Unexpected error:', err);
  }
}
StatusMeaningAction
400Bad request / validationCheck parameters
401Invalid API keyCheck PROPR_API_KEY
403ForbiddenCheck account ownership
404Not foundCheck resource ID
429Rate limitedSlow down (1200 req/min)
500Server errorRetry or check health
Developer Docs | Trading API | Propr