Developers
Complete API reference and integration guides. Trade every PropDAO account from Python, TypeScript, cURL or an AI agent.
Authentication API KEY
Get your API key
- Open the terminal at app.propdao.finance and sign in
- Avatar → Settings → Developers
- 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
| Header | Authorization: Bearer pd_live_… |
| Base URL | https://app.propdao.finance/api/v1 |
| Rate limit | 300 reads/min · 60 orders/min, per key, separate buckets |
| Keys per login | 1 — regenerating revokes the previous one |
| Scope | Same access as your login, every account you own |
| Sandbox | None needed — evaluation accounts are simulated money on live prices |
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/ordersPython
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
/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.
/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
/marketsno authEvery 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 }/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
/accounts{ "accounts": [ { "account_id": "prop-abc123", "status": "active", "challenge_id": "…", "purchase_date": "…", "challenge": { } } ] }status: active (evaluation) · funded · failed · passed · expired.
/accounts/:idFull live state, including the entire trade history — heavy. In a loop, use /risk, /positions and /orders instead. Commonly used fields:
| Field | Meaning |
|---|---|
balance | Cash after realised PnL, fees and margin currently locked in positions |
startingBalance | Account size |
maxDrawdownLimitPct | Static drawdown, % below starting balance (5) |
dailyDrawdownLimitPct | Daily loss limit, % (1, 2 or 3) |
dailyAnchorEquity | Balance the daily floor hangs off, set at 00:00 UTC |
profitTargetPct | Target to pass the evaluation, % (10) |
stage | Evaluation or Funded |
drawdownBreachedAt | Timestamp if the account failed, else null |
openPositions[] | see Positions |
pendingOrders[] | see Orders |
twapOrders[] | { id, symbol, side, totalSize, filledSize, state } |
tradeHistory[] | Closed trades, oldest first |
equity field on the raw state — use GET /risk, which prices it live.Risk READ FIRST
/accounts/:id/riskRead 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
/accounts/:id/ordersResting 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 }/accounts/:id/orders| Field | Type | Notes |
|---|---|---|
symbol | string | Required. e.g. BTCUSDC |
side | string | Required. BUY or SELL |
qty | number | Required. Units of the asset, not dollars |
orderType | string | market (default) · limit · stop_market · stop_limit · take_market · take_limit · scale · twap |
intentId | string | Idempotency key. Same intentId → same response, never a second fill (x-idempotent-replay: true). SDKs set it automatically |
leverage | number | Default 1. Clamped to the symbol max |
tif | string | gtc (default) · ioc immediate-or-cancel · alo post-only |
reduceOnly | boolean | Only reduce an existing position |
limitPrice | number | limit / stop_limit / take_limit |
triggerPrice | number | stop_* / take_* |
sl, tp | number | Stop-loss / take-profit price attached to the resulting position |
twapMs | number | twap: total duration in ms. Minimum notional $100 |
twapMin, twapMax | number | twap: price bounds; slices outside are skipped |
twapTrigger | number | twap: start only once price crosses this |
twapRandomize | boolean | twap: jitter slice sizes |
twapMaxSlippagePct | number | twap: skip a slice if impact exceeds this |
scaleStart, scaleEnd | number | scale: price range for the ladder |
scaleCount | number | scale: rungs, default 5 |
scaleDist | string | scale: 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.
/accounts/:id/orders/:orderIdCancel a resting order. For a scale ladder, cancelling any rung cancels the ladder.
/accounts/:id/twaps/:twapIdStop a running TWAP. Filled slices stay open as a position.
Positions API KEY
/accounts/:id/positionsOpen 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 }/accounts/:id/positions/:pidSet 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.
/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
| Type | Behaviour | Fills | sl/tp at submit |
|---|---|---|---|
market | Walks the book now, bounded by the slippage cap | Immediately | Yes |
limit | Walks now if it crosses, else rests and fills at its own price (maker) | When price reaches limitPrice | Yes |
stop_market | Armed; walks the book when triggerPrice is hit | Trigger | No |
stop_limit | Armed; rests at limitPrice once triggerPrice is hit | Trigger, then limit | No |
take_market | Same as stop_market, opposite trigger direction | Trigger | No |
take_limit | Same as stop_limit, opposite trigger direction | Trigger, then limit | No |
scale | scaleCount independent limits spread from scaleStart to scaleEnd. Cancelling any rung cancels the ladder | Each rung at its price | No |
twap | Market 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 TWAP | On schedule | No |
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
| Value | Meaning |
|---|---|
gtc | Default. Rests until filled or cancelled |
ioc | Immediate-or-cancel. Market orders only — a non-market order with ioc is rejected with 400 |
alo | Add-liquidity-only (post-only). Rests as maker; if it would cross the book on arrival it is rejected instead of taking |
Execution timing
| Rule | Detail |
|---|---|
| Minimum hold | A position must be open for 1 second before a manual close (POST /positions/:pid/close, or a reduce-only market order) |
| Between executions | User-initiated executions — market opens and manual closes — must be at least 0.5 seconds apart, per account |
| Not gated | Resting orders, triggers and TWAP slices are engine-initiated and bypass both rules; the 60 orders/min key limit still applies |
| Offline execution | Nothing 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 violation | 400 with "Hold positions 1s · max 1 execution every 0.5s". Nothing is executed — wait the remainder and resend with the same intentId |
Order statuses
| Status | Meaning |
|---|---|
PENDING | Resting or armed, nothing filled |
TRIGGERED | Stop/take trigger hit; the child limit is now resting (or the market walk is executing) |
PARTIALLY_FILLED | Some quantity filled, remainder still resting |
FILLED | Done; it leaves GET /orders and shows in the position / trades |
CANCELLED | Cancelled by you, by a ladder cancel, or by the engine (breach) |
TWAP states
| State | Meaning |
|---|---|
AWAITING_TRIGGER | Waiting for twapTrigger before the schedule starts |
RUNNING | Slicing |
TERMINATED | Price crossed twapMin/twapMax; remaining slices cancelled. Filled slices stay open |
COMPLETED | Ran to expiry (may be short if slices were skipped) |
CANCELLED | Stopped 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
| Topic | Rule |
|---|---|
| Margin mode | Isolated 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 side | Merges into one position at a weighted-average entry; leverage is the latest order's |
| Same symbol, opposite side | Opens 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 triggers | Do not reserve margin, so protective stops never eat your buying power |
| Liquidation vs breach | Liquidation 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 position | Stored as gross-$ PnL at trigger (sl, tp) and exposed as prices (slPrice, tpPrice). Set by price via PATCH |
Close reasons
tradeHistory[].reason / GET /trades:
| Reason | When |
|---|---|
Manual Close | POST /positions/:pid/close with percent = 1 |
Partial Close | … with percent < 1, or a reduce-only market order |
Stop Loss / Take Profit | Position bracket fired |
Liquidation | Isolated bust price reached |
Breach | Account floor touched; all positions closed, account over |
Copy Close | Closed 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.
| What | Funded |
|---|---|
GET /accounts → status | funded (was active) |
GET /accounts/:id → stage | Funded |
| Profit target | None. profitTargetPct is still reported but nothing happens at it |
| Drawdown | Unchanged: 5% static floor below the starting balance + your daily limit. Withdrawing profit lowers balance toward the start; the floor does not move |
| Payouts | 80% 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 else | Same 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
| Field | Values |
|---|---|
side | BUY · SELL |
orderType | market · limit · stop_market · stop_limit · take_market · take_limit · scale · twap |
tif | gtc · ioc · alo |
scaleDist | flat · weighted |
Order status | PENDING · TRIGGERED · PARTIALLY_FILLED · FILLED · CANCELLED |
TWAP state | AWAITING_TRIGGER · RUNNING · TERMINATED · COMPLETED · CANCELLED |
Account status | active · funded · passed · failed · expired |
stage | Evaluation · Funded |
floorKind | max (static 5%) · daily |
Trade reason | Manual Close · Partial Close · Stop Loss · Take Profit · Liquidation · Breach · Copy Close |
exitLiquidity | taker · maker |
Trades API KEY
/accounts/:id/tradesClosed 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.
| Market | Max leverage |
|---|---|
| Crypto with market cap ≥ $1B | 2x |
| Equities, indices, commodities, FX, pre-IPO | 1.5x |
| Crypto under $1B market cap | 1x |
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.
| Status | Meaning |
|---|---|
| 400 | Bad input — the message says what |
| 401 | Missing, invalid or revoked key |
| 403 | Not your account, or it is breached (Max drawdown breached — new orders are disabled.) |
| 404 | Position / order not found |
| 429 | Rate limited. Back off per Retry-After |
| 5xx | Engine 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
| Liquidity | Rate | When |
|---|---|---|
| Maker | 0.015% | Limit orders that rest and fill later (tif: alo guarantees maker or reject) |
| Taker | 0.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
| Symptom | Cause / fix |
|---|---|
Hold positions 1s · max 1 execution every 0.5s | You 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 number | qty missing or 0. Send units of the asset, rounded to lotStep |
| Position much smaller than expected | Leverage defaults to 1×. Send leverage |
| Bought instead of closing | You sent the opposite side. Use POST /positions/:pid/close |
| Two fills from one intended order | Retry without intentId. Always send one |
TWAP total size must be at least $100 | Increase qty or leverage |
SL gross price PnL must be <= current | Stop is on the wrong side of the mark |
429 | Over the per-key limit. Back off per Retry-After |
403 Max drawdown breached | The account is closed. Buy a new challenge |
| Symbol rejected | Not in GET /markets. Format is <COIN>USDC |