Developers

Complete API reference and integration guides. Trade every PropDAO account from Python, TypeScript, cURL or an AI agent.

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.

Authentication API KEY

Get your API key

  1. Open the terminal at app.propdao.finance and sign in
  2. Avatar → Settings → Developers
  3. Click Generate key. It starts with pd_live_ and is shown once

Send it as a bearer token on every authenticated request:

curl -H "Authorization: Bearer $PROPDAO_API_KEY" \
  https://app.propdao.finance/api/v1/me
HeaderAuthorization: Bearer pd_live_…
Base URLhttps://app.propdao.finance/api/v1
Rate limit300 reads/min · 60 orders/min, per key, separate buckets
Keys per login1 — regenerating revokes the previous one
ScopeSame access as your login, every account you own
SandboxNone needed — evaluation accounts are simulated money on live prices
Keep the key secret. Store it in an environment variable. If it leaks, regenerate it in Settings → Developers.

Quickstart 5 MIN

cURL

export PROPDAO_API_KEY=pd_live_...

# which accounts can I trade?
curl -H "Authorization: Bearer $PROPDAO_API_KEY" https://app.propdao.finance/api/v1/accounts

# how much can this one lose before it is closed?
curl -H "Authorization: Bearer $PROPDAO_API_KEY" https://app.propdao.finance/api/v1/accounts/prop-abc123/risk

# buy 0.01 BTC at market, 2x, with a stop
curl -X POST -H "Authorization: Bearer $PROPDAO_API_KEY" -H "Content-Type: application/json" \
  -d '{"symbol":"BTCUSDC","side":"BUY","qty":0.01,"leverage":2,"sl":60000}' \
  https://app.propdao.finance/api/v1/accounts/prop-abc123/orders

Python

Copy propdao.py into your project (zero dependencies; PyPI package coming). Full reference: Python SDK.

from propdao import PropDAO

pd = PropDAO()                                   # reads PROPDAO_API_KEY
acct = pd.get_accounts()[0]["account_id"]

risk = pd.get_risk(acct)
print(risk["roomUsd"], risk["floorKind"])        # e.g. 3120.5 'max'

r = pd.market_buy(acct, "BTCUSDC", 0.01, leverage=2, sl=60000)
print(r["status"])                               # MARKET BUY executed @ 64210.5000 (BTCUSDC, 2×). Fee -$0.29.

for p in pd.get_open_positions(acct):
    print(p["symbol"], p["side"], p["qty"], p["entry"], p["mark"], p["unrealizedPnl"])

pd.twap(acct, "BTCUSDC", "BUY", 0.5, minutes=30, max_price=66000)   # work size in gradually
pd.close_all(acct)

JavaScript / TypeScript

Copy propdao.ts into your project (zero dependencies, Node 18+ or browser; npm package coming). Full reference: JavaScript SDK.

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

const pd = new PropDAO();                        // reads PROPDAO_API_KEY
const [{ account_id }] = await pd.getAccounts();
const { roomUsd } = await pd.getRisk(account_id);
if (roomUsd > 500) await pd.marketBuy(account_id, "ETHUSDC", 0.5, { leverage: 2, sl: 3000 });

AI agent (MCP)

Add to Claude Desktop's claude_desktop_config.json, Cursor's MCP settings, or any MCP client:

{ "mcpServers": { "propdao": {
  "command": "node", "args": ["/path/to/propdao-sdk/mcp/dist/index.js"],
  "env": { "PROPDAO_API_KEY": "pd_live_…" } } } }

Then: "Check my PropDAO risk and buy 0.01 BTC if there's room."

Health & identity NO AUTH

GET/healthno auth
{ "status": "ok", "services": { "db": "ok" }, "time": "2026-09-17T12:00:00.000Z" }

status is degraded if any service is error. Always check before trading.

GET/me
{ "userId": "…", "loginKind": "supabase", "apiKeyId": "…" }

Transparency stats NO AUTH

The numbers on the Transparency page come from a public, aggregates-only Postgres function. Call it yourself — no PropDAO key, just the project's publishable key:

curl -X POST https://jnjlpcsoxvgeiqfozlqf.supabase.co/rest/v1/rpc/transparency_stats \
  -H "apikey: sb_publishable_autaw6At8QPIzBjo3Swr2A_RWhwWmHY" -H "Content-Type: application/json" -d '{}'
{ "paid_to_traders": 1797.11, "fees_collected": 528, "payouts_requested": 6, "registered_users": 681,
  "active_accounts": 220, "funded_accounts": 21, "trades_closed": 2856, "notional_traded": 69898488,
  "markets_traded": 137, "generated_at": "2026-09-20T20:27:59Z" }

It is SECURITY DEFINER so it can count across row-level-secured tables, and it returns totals and counts only — nothing that identifies an account. Payouts settle on-chain in USDC and are independently verifiable.

Markets & challenges NO AUTH

GET/marketsno auth

Every tradable symbol with its leverage cap and lot step, plus the fee schedule. Cache it.

{ "fees": { "taker": 0.00045, "maker": 0.00015 }, "marginMode": "isolated",
  "data": [ { "symbol": "BTCUSDC", "coin": "BTC", "dex": null, "maxLeverage": 2, "lotStep": 0.00001, "szDecimals": 5 } ],
  "total": 155 }
GET/challengesno auth
{ "data": [ { "id": "prop-25k-…", "name": "$25K Pro", "priceUsd": 149, "description": "…",
    "rules": { "account_size": 25000, "profit_target": 10, "max_drawdown": 5, "daily_loss": 2, "profit_split": 80 } } ],
  "total": 15, "purchaseUrl": "https://app.propdao.finance" }

Accounts API KEY

GET/accounts
{ "accounts": [ { "account_id": "prop-abc123", "status": "active", "challenge_id": "…", "purchase_date": "…", "challenge": { } } ] }

status: active (evaluation) · funded · failed · passed · expired.

GET/accounts/:id

Full live state, including the entire trade history — heavy. In a loop, use /risk, /positions and /orders instead. Commonly used fields:

FieldMeaning
balanceCash after realised PnL, fees and margin currently locked in positions
startingBalanceAccount size
maxDrawdownLimitPctStatic drawdown, % below starting balance (5)
dailyDrawdownLimitPctDaily loss limit, % (1, 2 or 3)
dailyAnchorEquityBalance the daily floor hangs off, set at 00:00 UTC
profitTargetPctTarget to pass the evaluation, % (10)
stageEvaluation or Funded
drawdownBreachedAtTimestamp if the account failed, else null
openPositions[]see Positions
pendingOrders[]see Orders
twapOrders[]{ id, symbol, side, totalSize, filledSize, state }
tradeHistory[]Closed trades, oldest first
There is no equity field on the raw state — use GET /risk, which prices it live.

Risk READ FIRST

GET/accounts/:id/risk

Read this before sizing anything. The account is closed the instant equity touches a floor — by a millisecond risk engine on live Hyperliquid prices, not a periodic sweep. roomUsd is computed by the same math that enforces the floor.

{
  "equity": 99120.5,        // balance + locked margin + unrealised PnL at live marks
  "balance": 98900.0,
  "floor": 96000.0,         // the binding drawdown floor right now
  "floorKind": "max",       // "max" (static 5%) or "daily" — whichever is higher binds
  "maxFloor": 95000.0,
  "dailyFloor": 96000.0,
  "roomUsd": 3120.5,        // equity - floor. Your whole risk budget.
  "roomPct": 3.15,
  "breached": false,
  "openPositions": 1
}
roomUsd = equity − floor
floor   = max(maxFloor, dailyFloor)

A long's worst case is (entry − stopLoss) × qty. Keep that well inside roomUsd; fees land on top.

Orders API KEY

GET/accounts/:id/orders

Resting orders and live TWAPs (TWAPs carry orderType: "twap"). ?limit= (default 100, max 500), ?offset=.

{ "data": [ { "id": "ord-…", "symbol": "BTCUSDC", "side": "BUY", "qty": 0.01, "orderType": "limit", "limitPx": 60000,
    "triggerPrice": null, "leverage": 2, "sl": null, "tp": null, "status": "PENDING", "createdAt": 1789688521621 } ],
  "total": 1 }
POST/accounts/:id/orders
FieldTypeNotes
symbolstringRequired. e.g. BTCUSDC
sidestringRequired. BUY or SELL
qtynumberRequired. Units of the asset, not dollars
orderTypestringmarket (default) · limit · stop_market · stop_limit · take_market · take_limit · scale · twap
intentIdstringIdempotency key. Same intentId → same response, never a second fill (x-idempotent-replay: true). SDKs set it automatically
leveragenumberDefault 1. Clamped to the symbol max
tifstringgtc (default) · ioc immediate-or-cancel · alo post-only
reduceOnlybooleanOnly reduce an existing position
limitPricenumberlimit / stop_limit / take_limit
triggerPricenumberstop_* / take_*
sl, tpnumberStop-loss / take-profit price attached to the resulting position
twapMsnumbertwap: total duration in ms. Minimum notional $100
twapMin, twapMaxnumbertwap: price bounds; slices outside are skipped
twapTriggernumbertwap: start only once price crosses this
twapRandomizebooleantwap: jitter slice sizes
twapMaxSlippagePctnumbertwap: skip a slice if impact exceeds this
scaleStart, scaleEndnumberscale: price range for the ladder
scaleCountnumberscale: rungs, default 5
scaleDiststringscale: flat (default) or weighted

Market orders fill immediately against the live book and return the updated account state plus a status line, e.g. MARKET BUY executed @ 64210.5000 (BTCUSDC, 2×). Fee -$0.29. Everything else rests and appears in pendingOrders (or twapOrders). Same-symbol, same-side positions merge at a weighted average entry.

DELETE/accounts/:id/orders/:orderId

Cancel a resting order. For a scale ladder, cancelling any rung cancels the ladder.

DELETE/accounts/:id/twaps/:twapId

Stop a running TWAP. Filled slices stay open as a position.

Positions API KEY

GET/accounts/:id/positions

Open positions priced at live marks.

{ "data": [ { "id": "pos-…", "symbol": "BTCUSDC", "side": "BUY", "qty": 0.01, "entry": 64210.5, "leverage": 2,
    "notional": 642.1, "marginAllocated": 321.0, "openedAt": 1789688515998,
    "mark": 64890.0, "unrealizedPnl": 6.79,
    "sl": -50.0, "tp": 120.0,               // engine form: gross PnL in dollars at the trigger
    "slPrice": 59210.5, "tpPrice": 76210.5  // the same, as prices
  } ], "total": 1 }
PATCH/accounts/:id/positions/:pid

Set or clear stop-loss / take-profit by price. Either or both; null clears.

{ "sl": 60000, "tp": null }

A stop must be on the losing side of the current mark and a take-profit on the winning side; otherwise 400 with the reason.

POST/accounts/:id/positions/:pid/close
{ "percent": 0.5 }   // optional, 0–1, default 1 (close all)

Closes at the current mark. To close, call this — the engine never nets: sending the opposite side opens a second, hedged position on the same symbol. reduceOnly: true on a market order is routed here as a partial close.

Order lifecycle REFERENCE

Nine order types, one fill primitive: every order ends up walking the same book. What differs is when the walk happens and what price bound it carries.

Order types

TypeBehaviourFillssl/tp at submit
marketWalks the book now, bounded by the slippage capImmediatelyYes
limitWalks now if it crosses, else rests and fills at its own price (maker)When price reaches limitPriceYes
stop_marketArmed; walks the book when triggerPrice is hitTriggerNo
stop_limitArmed; rests at limitPrice once triggerPrice is hitTrigger, then limitNo
take_marketSame as stop_market, opposite trigger directionTriggerNo
take_limitSame as stop_limit, opposite trigger directionTrigger, then limitNo
scalescaleCount independent limits spread from scaleStart to scaleEnd. Cancelling any rung cancels the ladderEach rung at its priceNo
twapMarket slices every ≥30 s across twapMs (5 min – 7 days). Min $100 notional. twapMin/twapMax skip out-of-range slices; crossing a bound terminates the TWAPOn scheduleNo
Only market and limit accept sl/tp at submission, because only they know their entry price. For every other type, set the bracket on the position with PATCH /positions/:pid once it exists. A trigger on the wrong side of the mark is rejected at submission rather than firing on the next tick.

Time in force

ValueMeaning
gtcDefault. Rests until filled or cancelled
iocImmediate-or-cancel. Market orders only — a non-market order with ioc is rejected with 400
aloAdd-liquidity-only (post-only). Rests as maker; if it would cross the book on arrival it is rejected instead of taking

Execution timing

RuleDetail
Minimum holdA position must be open for 1 second before a manual close (POST /positions/:pid/close, or a reduce-only market order)
Between executionsUser-initiated executions — market opens and manual closes — must be at least 0.5 seconds apart, per account
Not gatedResting orders, triggers and TWAP slices are engine-initiated and bypass both rules; the 60 orders/min key limit still applies
Offline executionNothing needs the terminal open. Market orders fill in the request. Resting orders, SL/TP and TWAP slices are settled by a server cron every minute (candle-scanned, so wicks are not missed) and by the live risk daemon for breaches — expect up to ~60 s between a trigger being hit and the fill appearing in GET /trades
On violation400 with "Hold positions 1s · max 1 execution every 0.5s". Nothing is executed — wait the remainder and resend with the same intentId

Order statuses

StatusMeaning
PENDINGResting or armed, nothing filled
TRIGGEREDStop/take trigger hit; the child limit is now resting (or the market walk is executing)
PARTIALLY_FILLEDSome quantity filled, remainder still resting
FILLEDDone; it leaves GET /orders and shows in the position / trades
CANCELLEDCancelled by you, by a ladder cancel, or by the engine (breach)

TWAP states

StateMeaning
AWAITING_TRIGGERWaiting for twapTrigger before the schedule starts
RUNNINGSlicing
TERMINATEDPrice crossed twapMin/twapMax; remaining slices cancelled. Filled slices stay open
COMPLETEDRan to expiry (may be short if slices were skipped)
CANCELLEDStopped via DELETE /twaps/:id

Slice cadence is approximate: slices fire on the engine tick, late but never early, and a catch-up rule absorbs drift.

Positions & margin REFERENCE

TopicRule
Margin modeIsolated on every API position. Liquidation when price moves against you by 100 / leverage % (2x → 50%, 1x → never from price alone). Cross margin exists in the terminal UI but is not exposed to keys
Same symbol, same sideMerges into one position at a weighted-average entry; leverage is the latest order's
Same symbol, opposite sideOpens a hedge leg — two positions, no netting. To exit, use POST /positions/:pid/close, or send a market order with reduceOnly: true, which the API routes to a partial close of the opposing position
Unfired triggersDo not reserve margin, so protective stops never eat your buying power
Liquidation vs breachLiquidation closes one position at its bust price. A breach (equity touches the static or daily floor) closes every position and ends the account — watch GET /risk, not the liquidation price
SL / TP on the positionStored as gross-$ PnL at trigger (sl, tp) and exposed as prices (slPrice, tpPrice). Set by price via PATCH

Close reasons

tradeHistory[].reason / GET /trades:

ReasonWhen
Manual ClosePOST /positions/:pid/close with percent = 1
Partial Close… with percent < 1, or a reduce-only market order
Stop Loss / Take ProfitPosition bracket fired
LiquidationIsolated bust price reached
BreachAccount floor touched; all positions closed, account over
Copy CloseClosed by a copy-trading leader

Funded accounts API KEY

Passing the evaluation flips the same account_id to the funded stage — nothing to re-create, the key keeps working.

WhatFunded
GET /accounts → statusfunded (was active)
GET /accounts/:id → stageFunded
Profit targetNone. profitTargetPct is still reported but nothing happens at it
DrawdownUnchanged: 5% static floor below the starting balance + your daily limit. Withdrawing profit lowers balance toward the start; the floor does not move
Payouts80% of any amount you withdraw, $20 minimum, on-chain USDC. Payout is UI-only for now — there is no POST /payouts. Close positions, then withdraw from the terminal
Everything elseSame endpoints, same limits, same fees

Find the funded account

acct = next(a["account_id"] for a in pd.get_accounts() if a["status"] == "funded")

An agent should treat failed / expired accounts as dead and skip them — orders return 403 Max drawdown breached.

Enums REFERENCE

FieldValues
sideBUY · SELL
orderTypemarket · limit · stop_market · stop_limit · take_market · take_limit · scale · twap
tifgtc · ioc · alo
scaleDistflat · weighted
Order statusPENDING · TRIGGERED · PARTIALLY_FILLED · FILLED · CANCELLED
TWAP stateAWAITING_TRIGGER · RUNNING · TERMINATED · COMPLETED · CANCELLED
Account statusactive · funded · passed · failed · expired
stageEvaluation · Funded
floorKindmax (static 5%) · daily
Trade reasonManual Close · Partial Close · Stop Loss · Take Profit · Liquidation · Breach · Copy Close
exitLiquiditytaker · maker

Trades API KEY

GET/accounts/:id/trades

Closed trades, newest first. ?limit=, ?offset=.

{ "data": [ { "id": "hist-…", "symbol": "BTCUSDC", "side": "BUY", "qty": 0.0014, "entry": 76405.14, "exit": 76408,
    "grossPnl": 0.004, "pnl": -0.1, "fee": 0.1, "openFee": 0.05, "closeFee": 0.05, "fundingPaid": 0,
    "leverage": 2, "openedAt": 1789688515998, "closedAt": 1789688606610, "reason": "Manual Close",
    "exitLiquidity": "taker", "slippagePct": 0 } ], "total": 1 }

Symbols & leverage REFERENCE

Symbols are <COIN>USDC: BTCUSDC, ETHUSDC, SOLUSDC. Equities, indices, commodities and FX use the same form — NVDAUSDC, GOLDUSDC, SP500USDC. The authoritative list with caps and lot steps is GET /markets; the human version is the Assets page.

MarketMax leverage
Crypto with market cap ≥ $1B2x
Equities, indices, commodities, FX, pre-IPO1.5x
Crypto under $1B market cap1x

Sending more than the cap is clamped, not rejected. All positions are isolated margin.

Errors & limits REFERENCE

Errors are JSON { "error": "human readable message" } with a conventional status.

StatusMeaning
400Bad input — the message says what
401Missing, invalid or revoked key
403Not your account, or it is breached (Max drawdown breached — new orders are disabled.)
404Position / order not found
429Rate limited. Back off per Retry-After
5xxEngine error. Safe to retry reads; check state before retrying an order

Limits per key: 300 reads/min, 60 orders/min. Poll every 2–3 s at most.

Fees REFERENCE

LiquidityRateWhen
Maker0.015%Limit orders that rest and fill later (tif: alo guarantees maker or reject)
Taker0.045%Market orders, IOC, limit orders that cross

Charged on the notional of every fill, open and close, on every market at every leverage. Reported per trade as openFee, closeFee, fee.

Rules for AI agents PROMPT

Paste this into your agent's system prompt, CLAUDE.md, or .cursorrules.

You trade a PropDAO prop-firm account through its API (or the propdao SDK / MCP tools).

1. Call GET /accounts/:id/risk before every trade. roomUsd is the whole budget;
   size each position so its stop-loss loss is a fraction of it (a quarter is sane).
2. qty is in units of the asset (0.01 BTC), never dollars. Get lotStep and
   maxLeverage from GET /markets and round qty to lotStep.
3. Symbols are <COIN>USDC: BTCUSDC, ETHUSDC, SOLUSDC.
4. To close, call POST /positions/:pid/close. Do not send the opposite side.
5. Send an intentId on every order (the SDKs do it for you). A retry with the
   same intentId is safe.
6. Market orders fill in the response; read `status`. Everything else rests —
   check GET /orders.
7. TWAPs need at least $100 notional.
8. Hold a position at least 1 s before closing it, and leave 0.5 s between
   market executions. A 400 "Hold positions 1s" means wait, then resend.
9. Poll no faster than every 2-3 s. The engine reacts to prices itself.
10. 403 "Max drawdown breached" means the account is over. Stop.
11. Keep the key in an environment variable. Never in source.

Tool descriptions that work

If you expose the API as tools yourself, put the risk rule in the tool description — models read tool descriptions at the moment of choosing:

get_risk: "How much the account can lose before it is breached and closed.
           ALWAYS call before sizing: keep worst-case loss well under roomUsd."

A complete 60-line agent, a DCA bot and a risk guard are on the Python SDK page.

Pitfalls REFERENCE

SymptomCause / fix
Hold positions 1s · max 1 execution every 0.5sYou opened and closed inside 1 s, or fired two market executions inside 0.5 s. Wait and resend with the same intentId
Lot size must be a positive numberqty missing or 0. Send units of the asset, rounded to lotStep
Position much smaller than expectedLeverage defaults to 1×. Send leverage
Bought instead of closingYou sent the opposite side. Use POST /positions/:pid/close
Two fills from one intended orderRetry without intentId. Always send one
TWAP total size must be at least $100Increase qty or leverage
SL gross price PnL must be <= currentStop is on the wrong side of the mark
429Over the per-key limit. Back off per Retry-After
403 Max drawdown breachedThe account is closed. Buy a new challenge
Symbol rejectedNot in GET /markets. Format is <COIN>USDC