JavaScript SDK

Official TypeScript client for the PropDAO trading API. One file, zero dependencies, Node 18+ and browsers.

Live environment — every account trades on live Hyperliquid prices with simulated capital. No sandbox needed: an evaluation account is the sandbox.
API in testing — the v1 API and SDKs are in open beta. Endpoints, fields and error shapes can change without notice, and downtime is possible. Don't rely on it for anything you can't re-run.

Installation REQUIRED

The SDK is a single TypeScript file with zero dependencies. Runs in Node 18+, Bun, Deno and browsers (uses fetch).

Option A — copy-paste. Save the source below as propdao.ts (or strip the types for propdao.js).

Option B — npm (package publishing soon):

npm install @propdao/sdk

Environment

export PROPDAO_API_KEY=pd_live_...

In the browser pass the key to the constructor instead — and never ship a live key in client code.

SDK source COPY & PASTE

Save as propdao.ts. Typed OrderOpts, Market and Risk, throws PropDAOError, auto intentId per order, waits out 409 races.

/**
 * PropDAO agent SDK. Zero dependencies, works in Node 18+ and browsers.
 *
 *   import { PropDAO } from "@propdao/sdk";
 *   const pd = new PropDAO();                       // reads PROPDAO_API_KEY
 *   const [acct] = await pd.getAccounts();
 *   const { roomUsd } = await pd.getRisk(acct.account_id);
 *   await pd.marketBuy(acct.account_id, "BTCUSDC", 0.01, { leverage: 2, sl: 60000 });
 */

export type Side = "BUY" | "SELL";
export type OrderType = "market" | "limit" | "stop_market" | "stop_limit" | "take_market" | "take_limit" | "scale" | "twap";

export interface OrderOpts {
  orderType?: OrderType;
  leverage?: number;
  limitPrice?: number;
  triggerPrice?: number;
  sl?: number;
  tp?: number;
  /** "gtc" (default) | "ioc" | "alo" (post-only) */
  tif?: "gtc" | "ioc" | "alo";
  reduceOnly?: boolean;
  /** Idempotency key; auto-generated if omitted so a retry never double-fills. */
  intentId?: string;
  twapMs?: number; twapMin?: number; twapMax?: number; twapTrigger?: number;
  twapRandomize?: boolean; twapMaxSlippagePct?: number;
  scaleStart?: number; scaleEnd?: number; scaleCount?: number; scaleDist?: string;
}

export interface Market {
  symbol: string;
  coin: string;
  dex: string | null;
  maxLeverage: number;
  lotStep: number;
  szDecimals: number;
}

export interface Risk {
  equity: number;
  balance: number;
  floor: number;
  floorKind: "max" | "daily";
  maxFloor: number;
  dailyFloor: number;
  roomUsd: number;
  roomPct: number;
  breached: boolean;
  openPositions: number;
}

export class PropDAOError extends Error {
  constructor(public status: number, message: string) {
    super(`${status}: ${message}`);
  }
}

export class PropDAO {
  private key: string;
  private base: string;
  private timeoutMs: number;

  constructor(apiKey?: string, baseUrl = "https://app.propdao.finance/api/v1", timeoutMs = 15_000) {
    const k = apiKey ?? (globalThis as any).process?.env?.PROPDAO_API_KEY;
    if (!k) throw new Error("apiKey required (or set PROPDAO_API_KEY)");
    this.key = k;
    this.base = baseUrl.replace(/\/$/, "");
    this.timeoutMs = timeoutMs;
  }

  private async req<T = any>(method: string, path: string, body?: unknown, retried = false): Promise<T> {
    const res = await fetch(this.base + path, {
      method,
      signal: AbortSignal.timeout(this.timeoutMs),
      headers: { Authorization: `Bearer ${this.key}`, "Content-Type": "application/json" },
      body: body == null ? undefined : JSON.stringify(body),
    }).catch((e) => { throw new PropDAOError(0, `network error: ${e?.message ?? e}`); });
    // 429 on a read: wait out Retry-After (capped) once, then surface it.
    if (res.status === 429 && method === "GET" && !retried) {
      const wait = Math.min(5, Number(res.headers.get("retry-after")) || 1);
      await new Promise((r) => setTimeout(r, wait * 1000));
      return this.req(method, path, body, true);
    }
    const data: any = await res.json().catch(() => ({}));
    if (!res.ok) throw new PropDAOError(res.status, data?.error ?? res.statusText);
    return data as T;
  }

  // -- read ---------------------------------------------------------------
  health(): Promise<{ status: "ok" | "degraded"; services: { db: "ok" | "error" }; time: string }> {
    return this.req("GET", "/health");
  }
  me(): Promise<{ userId: string; loginKind: string; apiKeyId: string }> {
    return this.req("GET", "/me");
  }
  /** Every tradable symbol with maxLeverage, lotStep, and the fee schedule. */
  markets(): Promise<{ fees: { taker: number; maker: number }; data: Market[]; total: number }> {
    return this.req("GET", "/markets");
  }
  async challenges(): Promise<any[]> {
    return (await this.req("GET", "/challenges")).data;
  }
  async getAccounts(): Promise<any[]> {
    return (await this.req("GET", "/accounts")).accounts ?? [];
  }
  getAccount(accountId: string): Promise<any> {
    return this.req("GET", `/accounts/${accountId}`);
  }
  async getOpenPositions(accountId: string): Promise<any[]> {
    return (await this.req("GET", `/accounts/${accountId}/positions?limit=500`)).data;
  }
  async getOpenOrders(accountId: string): Promise<any[]> {
    return (await this.req("GET", `/accounts/${accountId}/orders?limit=500`)).data;
  }
  async getTrades(accountId: string, limit = 100, offset = 0): Promise<any[]> {
    return (await this.req("GET", `/accounts/${accountId}/trades?limit=${limit}&offset=${offset}`)).data;
  }
  /** Dollars you can lose before breach. Size off roomUsd. */
  getRisk(accountId: string): Promise<Risk> {
    return this.req("GET", `/accounts/${accountId}/risk`);
  }

  // -- trade --------------------------------------------------------------
  async placeOrder(accountId: string, symbol: string, side: Side, qty: number, opts: OrderOpts = {}) {
    const intentId = opts.intentId ?? (globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`);
    for (let attempt = 0; ; attempt++) {
      try {
        return await this.req("POST", `/accounts/${accountId}/orders`, { symbol, side, qty, orderType: "market", ...opts, intentId });
      } catch (e) {
        // 409 = same intentId still executing (a retried request raced itself); wait for its result.
        if (!(e instanceof PropDAOError) || e.status !== 409 || attempt >= 4) throw e;
        await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
      }
    }
  }
  marketBuy(accountId: string, symbol: string, qty: number, opts?: OrderOpts) {
    return this.placeOrder(accountId, symbol, "BUY", qty, opts);
  }
  marketSell(accountId: string, symbol: string, qty: number, opts?: OrderOpts) {
    return this.placeOrder(accountId, symbol, "SELL", qty, opts);
  }
  limitBuy(accountId: string, symbol: string, qty: number, price: number, opts?: OrderOpts) {
    return this.placeOrder(accountId, symbol, "BUY", qty, { ...opts, orderType: "limit", limitPrice: price });
  }
  limitSell(accountId: string, symbol: string, qty: number, price: number, opts?: OrderOpts) {
    return this.placeOrder(accountId, symbol, "SELL", qty, { ...opts, orderType: "limit", limitPrice: price });
  }
  /** Work qty into the market over `minutes`. */
  twap(accountId: string, symbol: string, side: Side, qty: number, minutes: number, opts: OrderOpts = {}) {
    return this.placeOrder(accountId, symbol, side, qty, { ...opts, orderType: "twap", twapMs: Math.round(minutes * 60_000) });
  }
  /** Set or clear stop-loss / take-profit on an open position (null clears). */
  setRisk(accountId: string, positionId: string, risk: { sl?: number | null; tp?: number | null }) {
    return this.req("PATCH", `/accounts/${accountId}/positions/${positionId}`, risk);
  }
  /** Cancel every resting order and TWAP. Cancelling one rung of a scale ladder
   *  cancels its siblings, so a 404 on a later rung is expected and skipped. */
  async cancelAllOrders(accountId: string) {
    const out = [];
    for (const o of await this.getOpenOrders(accountId)) {
      try {
        out.push(o.orderType === "twap" ? await this.cancelTwap(accountId, o.id) : await this.cancelOrder(accountId, o.id));
      } catch (e) {
        if (!(e instanceof PropDAOError) || e.status !== 404) throw e;
      }
    }
    return out;
  }
  cancelOrder(accountId: string, orderId: string) {
    return this.req("DELETE", `/accounts/${accountId}/orders/${orderId}`);
  }
  cancelTwap(accountId: string, twapId: string) {
    return this.req("DELETE", `/accounts/${accountId}/twaps/${twapId}`);
  }
  closePosition(accountId: string, positionId: string, percent = 1) {
    return this.req("POST", `/accounts/${accountId}/positions/${positionId}/close`, { percent });
  }
  async closeAll(accountId: string) {
    const out = [];
    for (const p of await this.getOpenPositions(accountId)) out.push(await this.closePosition(accountId, p.id));
    return out;
  }
}

Quick start 5 MIN

import { PropDAO } from "./propdao";        // or "@propdao/sdk"

const pd = new PropDAO();                        // reads PROPDAO_API_KEY
const [{ account_id }] = await pd.getAccounts();

const risk = await pd.getRisk(account_id);       // ALWAYS before sizing
console.log(`room to breach: $${risk.roomUsd.toFixed(2)} (${risk.floorKind} floor)`);

const r = await pd.marketBuy(account_id, "BTCUSDC", 0.01, { leverage: 2, sl: 60000 });
console.log(r.status);

API reference REFERENCE

Constructor

const pd = new PropDAO(
  apiKey?: string,                                  // default: process.env.PROPDAO_API_KEY
  baseUrl = "https://app.propdao.finance/api/v1",
  timeoutMs = 15_000,
);

Read

MethodAuthReturns
pd.health()No{status, time}
pd.markets()No{fees, data: Market[], total}
pd.challenges()NoAccount sizes, prices and rules
pd.me()Yes{userId, loginKind, apiKeyId}
pd.getAccounts()YesAccounts you own
pd.getAccount(id)YesFull live state incl. the whole trade history — heavy. Prefer getRisk / getOpenPositions in loops
pd.getRisk(id)YesRisk — equity, floor, roomUsd, breached
pd.getOpenPositions(id)YesOpen positions at live marks
pd.getOpenOrders(id)YesResting orders and live TWAPs
pd.getTrades(id, limit = 100, offset = 0)YesClosed trades, newest first

Trade

MethodDescription
pd.placeOrder(id, symbol, side, qty, opts?)Any order type via opts.orderType
pd.marketBuy(id, symbol, qty, opts?)Market long
pd.marketSell(id, symbol, qty, opts?)Market short. Against an open long this opens a hedge leg — pass { reduceOnly: true } or use closePosition
pd.limitBuy(id, symbol, qty, price, opts?)Resting limit buy
pd.limitSell(id, symbol, qty, price, opts?)Resting limit sell
pd.twap(id, symbol, side, qty, minutes, opts?)Work size in over time. Min $100 notional
pd.setRisk(id, positionId, { sl?, tp? })Set SL/TP by price. null clears
pd.closePosition(id, positionId, percent = 1)Close all or part at mark
pd.closeAll(id)Flatten every position
pd.cancelOrder(id, orderId)Cancel a resting order
pd.cancelTwap(id, twapId)Stop a running TWAP
pd.cancelAllOrders(id)Cancel every resting order and TWAP. Ladder siblings already gone (404) are skipped

OrderOpts

interface OrderOpts {
  orderType?: "market" | "limit" | "stop_market" | "stop_limit" | "take_market" | "take_limit" | "scale" | "twap";
  leverage?: number;            // default 1, clamped to the symbol cap
  limitPrice?: number;          // limit / stop_limit / take_limit
  triggerPrice?: number;        // stop_* / take_*
  sl?: number; tp?: number;     // stop-loss / take-profit PRICE; market & limit only, else use setRisk()
  tif?: "gtc" | "ioc" | "alo";  // ioc: market only; alo: post-only, rejected if it would cross
  reduceOnly?: boolean;
  intentId?: string;            // auto-generated if omitted
  twapMs?: number; twapMin?: number; twapMax?: number; twapTrigger?: number;
  twapRandomize?: boolean; twapMaxSlippagePct?: number;
  scaleStart?: number; scaleEnd?: number; scaleCount?: number; scaleDist?: string;
}

Recipes EXAMPLES

Risk-sized entry

const SYMBOL = "ETHUSDC", STOP_PCT = 0.02, RISK_FRACTION = 0.25;

const risk = await pd.getRisk(account_id);
const mkt = (await pd.markets()).data.find((m) => m.symbol === SYMBOL)!;
const px = (await pd.getOpenPositions(account_id)).find((p) => p.symbol === SYMBOL)?.mark ?? 2600;

const budget = risk.roomUsd * RISK_FRACTION;           // dollars we accept losing
let qty = budget / (px * STOP_PCT);
qty = Math.round(qty / mkt.lotStep) * mkt.lotStep;

await pd.marketBuy(account_id, SYMBOL, qty, { leverage: Math.min(2, mkt.maxLeverage), sl: px * (1 - STOP_PCT) });

Grid bot

The engine never nets, so a filled sell above opens a short leg beside your long. Use closePosition to actually reduce.

const SYMBOL = "BTCUSDC", MID = 81000, STEP = 500, QTY = 0.001;
for (let i = 1; i <= 5; i++) {
  await pd.limitBuy(account_id, SYMBOL, QTY, MID - STEP * i, { leverage: 2 });
  await pd.limitSell(account_id, SYMBOL, QTY, MID + STEP * i, { leverage: 2 });
}
console.log((await pd.getOpenOrders(account_id)).length, "orders resting");

Risk guard

const MIN_ROOM_PCT = 1;
setInterval(async () => {
  const r = await pd.getRisk(account_id);
  console.log(`equity ${r.equity.toFixed(2)} floor ${r.floor.toFixed(2)} room ${r.roomPct.toFixed(2)}%`);
  if (r.openPositions && r.roomPct < MIN_ROOM_PCT) {
    console.log("room too thin - flattening");
    await pd.cancelAllOrders(account_id);
    await pd.closeAll(account_id);
  }
}, 3000);

Stop-loss + take-profit

await pd.marketBuy(account_id, "ETHUSDC", 0.1, { leverage: 2, sl: 2500, tp: 2900 });

const [pos] = await pd.getOpenPositions(account_id);
await pd.setRisk(account_id, pos.id, { sl: 2550 });     // move the stop up
await pd.setRisk(account_id, pos.id, { tp: null });     // clear the take-profit

TWAP

const state = await pd.twap(account_id, "BTCUSDC", "BUY", 0.5, 30, { leverage: 2, twapMax: 82000 });
const twapId = state.twapOrders.at(-1).id;
// ...later
await pd.cancelTwap(account_id, twapId);

Portfolio monitor

const r = await pd.getRisk(account_id);
console.log(`equity $${r.equity.toFixed(2)}  floor $${r.floor.toFixed(2)}  room $${r.roomUsd.toFixed(2)} (${r.roomPct.toFixed(2)}%)`);
console.table((await pd.getOpenPositions(account_id)).map((p) => ({
  symbol: p.symbol, side: p.side, qty: p.qty, entry: p.entry, mark: p.mark, uPnL: p.unrealizedPnl,
})));

Error handling REFERENCE

Every failure throws PropDAOError with .status and .message. Status 0 is a network error with no response — the intentId makes a retry safe. Reads that hit 429 are retried once after Retry-After; writes are not.

import { PropDAO, PropDAOError } from "./propdao";

try {
  await pd.marketBuy(account_id, "BTCUSDC", 0.01, { leverage: 2 });
} catch (e) {
  if (!(e instanceof PropDAOError)) throw e;
  if (e.status === 429) await new Promise((r) => setTimeout(r, 2000));
  else if (e.status === 403 && e.message.includes("breached")) process.exit(1);
  else if (e.status === 0) { /* network blip - retry with the same intentId */ }
  else if (e.status === 400 && e.message.startsWith("Hold positions")) await new Promise((r) => setTimeout(r, 1000)); // 1 s min hold / 0.5 s spacing
  else console.error("API error", e.status, e.message);
}
StatusMeaningAction
400Bad input, or Hold positions 1s · max 1 execution every 0.5sRead message; for the timing error wait and resend the same intentId
401Invalid or revoked keyCheck PROPDAO_API_KEY
403Not your account / breachedStop trading that account
404Order or position not foundRefresh state
409Same intentId still executingSDK waits and retries for you
429Rate limitedBack off; 300 reads / 60 orders per min
0Network error, no responseRetry — the intentId makes it safe
5xxEngine errorRetry reads; check state before retrying an order