JavaScript / TypeScript SDK
Official JS/TS client for the Propr trading API
propr-sdkInstallation
Requirednpm 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 & PasteSave 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 minGet 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
| Method | Description |
|---|---|
| await client.setup() | Auto-detect account ID from active challenge |
| await client.setup(accountId) | Use a specific account ID |
Health
| Method | Auth | Returns |
|---|---|---|
| await client.health() | No | { status: "OK" } |
| await client.healthServices() | No | { core: "OK" | "ERROR" } |
User
| Method | Auth | Returns |
|---|---|---|
| await client.getUser() | Yes | User profile object |
Challenges
| Method | Auth | Returns |
|---|---|---|
| await client.getChallenges() | No | Challenge[] |
| await client.getChallengeAttempts({ status: "active" }) | Yes | Attempt[] |
| await client.getChallengeAttempt(attemptId) | Yes | Attempt |
Orders
| Method | Description |
|---|---|
| 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
| Method | Description |
|---|---|
| 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
| Method | Description |
|---|---|
| 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
| Method | Description |
|---|---|
| await client.getTrades({ base: "BTC" }) | List trade executions with filters |
Margin & Leverage
| Method | Description |
|---|---|
| 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
TypedThe 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
RecipesGrid 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-TimeFor 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);
}
}| Status | Meaning | Action |
|---|---|---|
| 400 | Bad request / validation | Check parameters |
| 401 | Invalid API key | Check PROPR_API_KEY |
| 403 | Forbidden | Check account ownership |
| 404 | Not found | Check resource ID |
| 429 | Rate limited | Slow down (1200 req/min) |
| 500 | Server error | Retry or check health |
