Recipes
Copy-pasteable, task-oriented snippets for the most common Perpl integration jobs: placing and cancelling an order, streaming your own fills, reading live positions and balances, subscribing to the order book, and attaching a take-profit / stop-loss (TP/SL) to a position.
Each recipe shows the direct-API approach first (REST over HTTPS and the WebSocket streams) and then notes the equivalent in the Rust SDK (software development kit, the perpl-sdk crate) where one exists. Acronyms used throughout: REST (Representational State Transfer), WSS (WebSocket Secure), API (application programming interface), RPC (remote procedure call), IOC (immediate-or-cancel), FOK (fill-or-kill), GTC (good-till-cancel), bps (basis points), PnL (profit and loss), FIFO (first-in-first-out).
Note: These recipes assume you have already created an API key and set your environment variables. If not, start with the Quickstart and the Networks & Configuration reference.
Before you start
Two things every recipe reuses: environment configuration and a request signer.
Environment
API keys are Ed25519 key pairs (a modern elliptic-curve signature scheme). The server stores only your public key; the private key never leaves your machine, and every request is signed — there is no bearer token or session cookie.
// Mainnet defaults; set the testnet values to target testnet.
const API_URL = process.env.PERPL_API_URL || 'https://app.perpl.xyz/api'; // REST base URL — includes /api
const WS_URL = process.env.PERPL_WS_URL || 'wss://app.perpl.xyz'; // WebSocket base URL — NO /api prefix
const CHAIN_ID = Number(process.env.PERPL_CHAIN_ID) || 143; // 143 mainnet, 10143 testnet
// Enrolled key (see the Quickstart):
const API_KEY = process.env.PERPL_API_KEY!; // opaque X-API-Key token
const privateKey = Buffer.from(
(process.env.PERPL_API_KEY_SECRET ?? '').replace(/^0x/, ''),
'hex',
); // 32-byte Ed25519 private key
// Market IDs are network-specific — never hard-code across networks.
// Mainnet: BTC=1, MON=10, ETH=20, SOL=31, HYPE=40, ZEC=50
// Testnet: BTC=16, ETH=32, SOL=48, MON=64, ZEC=256
const MARKETS = { BTC: 1, MON: 10, ETH: 20, SOL: 31, HYPE: 40, ZEC: 50 } as const;The signedFetch helper (REST)
Every REST call is signed over a canonical string of six fields joined by newlines: <chain_id>, <HTTP_METHOD>, <request-target> (path + query string exactly as sent), <timestamp_ms>, <nonce> (client-random, base64url, no padding), <sha256(body) hex>. The signature and three companion headers are sent as X-API-*.
The request-target must match byte-for-byte what the server receives — include the query string exactly as sent, and sign that exact string. The timestamp must be within ±30 seconds of server time, and each nonce is single-use. See Authentication for the full signing spec.
Opening the trading WebSocket
Orders, fills, positions, and balances all flow over the authenticated trading WebSocket at /ws/v1/trading. The first frame after the socket opens must be a signed ApiKeySignIn frame (message type mt: 29). Placing orders requires a trade-scoped key; a read-scoped key still receives snapshots and updates but its order frames are rejected with 403.
On close code 3401 (authentication failure), reconnect and re-send a freshly signed mt: 29 frame (new timestamp + nonce). Full connection, snapshot, sequence-tracking, and reconnection semantics are in the WebSocket reference.
SDK equivalent. The Rust SDK does not use the WebSocket API. It maintains an in-memory cache of on-chain exchange state: build a snapshot with state::SnapshotBuilder, then keep it current from a per-block event stream (stream::raw) fed into Exchange::apply_events. See SDK Concepts.
Place and cancel an order
Orders are submitted as OrderRequest frames (mt: 22) on the trading WebSocket.
Key fields
rq
Request ID — per-account idempotency key, strictly increasing. The server guarantees at-most-once execution per rq.
mkt / acc
Market ID and your account ID.
t
Order type: 1 OpenLong, 2 OpenShort, 3 CloseLong, 4 CloseShort, 5 Cancel, 6 IncreasePositionCollateral, 7 Change.
p
Limit price (scaled). 0 = market order.
s
Size (scaled by the market's size_decimals).
ms
Maximum market-order price slippage, in bps (recommended for market orders).
fl
Flags: 0 GTC, 1 PostOnly, 2 FOK, 4 IOC.
lv
Leverage in hundredths (1000 = 10x).
lb
Last execution block — the last Monad block at which the order is valid.
oid
Order ID — required for Cancel / Change.
rq must be strictly increasing per account. Seed it from the account's last-forwarded request ID (lfr, delivered in the WalletSnapshot and AccountUpdate frames): rq = max(localCounter, account.lfr) + 1. Submitting rq <= lfr fails with reject reason sr: 32 (OrderDescIdTooLow) — retry once with a fresh rq.
Place a limit order (open long)
Prices and sizes are scaled integers. On mainnet BTC, price_decimals = 1 (so $95,000 → 950000) and size_decimals = 5 (so 0.1 BTC → 10000). Read the per-market decimals from GET /api/v1/pub/context.
Place a market order
Set p: 0, use the IOC flag, and bound your slippage with ms:
Cancel an order
Cancel by order ID (oid) with order type 5:
Recommended validation before sending (see the WebSocket reference): size > 0; leverage within the market's limits (MarketConfig.initial_margin); marketId present in /api/v1/pub/context; price > 0 for limit orders and price = 0 for market; lb no more than head_block + market.order_ttl_blocks; and the socket is open. Order-status updates arrive as OrdersUpdate (mt: 24) — orders carrying r: true should be removed from your open-orders view.
SDK equivalent. Build a types::OrderRequest, call .prepare(&exchange) to scale the decimal fields into the on-chain OrderDesc, and submit through the generated exchange binding:
The SDK works in human-readable leverage (not hundredths) and scales prices/sizes via the perpetual's converters. Note the order-type numbering differs between the two layers: the WebSocket API's t field is 1-indexed (1 OpenLong … 7 Change, as in the table above), while the SDK's RequestType enum is 0-indexed (0 OpenLong … 6 Change) — the same operation, offset by one. See SDK Concepts → Building and sending orders.
Stream your own fills
After the trading socket authenticates, fills stream in as FillsUpdate (mt: 25). Each Fill carries the market (mkt), order (oid), order type (t), liquidity side (l: 1 Maker, 2 Taker), fill price (p, scaled), size (s, scaled), and fee/rebate (f).
For historical fills, page through the signed REST endpoint GET /api/v1/trading/fills (newest→oldest; count max 100; follow the np cursor):
The history endpoints do not support server-side filtering by market or date — filter client-side. See the REST API reference.
SDK equivalent. Layer the normalized trade stream stream::trade on top of stream::raw. It aggregates all maker fills belonging to one taker into a single Trade and normalizes the fixed-point values to decimals. Each Trade exposes taker_account_id, taker_side, total_size(), avg_price(), perpetual_id, taker_fee, and maker_fills (each with maker_account_id, maker_order_id, size, price, fee). See SDK Concepts → the normalized trade stream.
Fetch positions and balances
Live positions and balances come from the trading WebSocket snapshots delivered right after authentication, then stay current via updates:
Balances — WalletSnapshot (
mt: 19) and AccountUpdate (mt: 21). The account object carriesb(balance),lb(locked balance), andlfr(last forwarded request ID — also yourrqseed).Positions — PositionsSnapshot (
mt: 26) then PositionsUpdate (mt: 27).
For historical positions and account events, use the signed REST endpoints GET /api/v1/trading/position-history and GET /api/v1/trading/account-history (same { d, np } paginated shape as fills). Account events are typed by et (AccountEventType), e.g. 1 Deposit, 2 Withdrawal, 4 Settlement, 5 Liquidation, 8 Funding. Balances and amounts are decimal strings; fees are in micros (10^-6).
There is no REST endpoint for current positions — the live view is the WebSocket PositionsSnapshot/PositionsUpdate stream. REST position-history returns historical position records. The Position and Account type fields are documented in Types & Errors.
SDK equivalent. Snapshot the accounts you care about and read state directly off the cache:
.with_accounts(...) fetches the accounts' balances and positions; alternatively .with_all_positions() fetches every position (the two are mutually exclusive). See SDK Concepts → the snapshot workflow.
Subscribe to the order book
The order book is public — connect to the market-data WebSocket at /ws/v1/market-data (no authentication) and subscribe to order-book@<market_id> with a SubscriptionRequest (mt: 5). You receive an L2BookSnapshot (mt: 15) then incremental L2BookUpdate (mt: 16) messages. L2 = level 2 (aggregated by price level). Each price level is { p, s, o } (price, size, number of orders); a level with o: 0 has been removed.
Prices and sizes are scaled by the market's price_decimals / size_decimals (from GET /api/v1/pub/context). Other market-data streams use the same mt: 5 subscribe shape: trades@<market_id>, candles@<market_id>*<resolution>, market-state@<chain_id>, funding@<chain_id>, gas-stats@<chain_id>, and heartbeat@<chain_id>. See the WebSocket reference.
SDK equivalent. The SDK maintains a full L3 (level 3, per-order) book: snapshot a perpetual, stream events, and read perp.l3_book() alongside perp.mark_price(), perp.last_price(), and perp.oracle_price():
The command-line tool prints the same book without writing code: perpl-cli show book --perp <id>. See SDK Concepts and the CLI reference.
Set a take-profit / stop-loss (trigger orders)
A take-profit / stop-loss (TP/SL) is a trigger order: a normal OrderRequest (mt: 22) that carries a trigger price and condition, and is not posted to the book until the market crosses that price. Trigger-specific fields:
tp
Trigger price (scaled).
tpc
Trigger condition: 1 GTELast, 2 LTELast, 3 GTEMark, 4 LTEMark.
lp
Linked position ID — the trigger is cancelled when that position closes or inverts.
tr
Linked request ID — activate on the linked request's fill, cancel on its failure.
lb
Must be 0 for trigger orders (no expiry block; the server manages the lifecycle).
For a long position you protect it with two reduce-only CloseLong (t: 3) triggers linked to the position via lp:
Stop-loss — close when price falls to/below your stop → condition
LTELast(2) orLTEMark(4).Take-profit — close when price rises to/above your target → condition
GTELast(1) orGTEMark(3).
For a short position, mirror the logic with CloseShort (t: 4): the stop-loss fires on a GTE condition (price rising against you) and the take-profit on an LTE condition (price falling in your favor).
The number of resting trigger orders per account is capped by max_account_trigger_orders from the ProtocolInstance in GET /api/v1/pub/context. Trigger-order lifecycle, the tr request-linking behavior, and order-status transitions (8 Untriggered → 9 Triggered) are described in the WebSocket reference.
SDK equivalent. Not documented. The OrderRequest::new constructor in the current SDK sources exposes order-lifecycle fields (price, size, expiry, post-only / FOK / IOC, leverage, collateral amount) but no trigger-price / trigger-condition / linked-position fields, so trigger orders are placed over the WebSocket API shown above.
TODO(author): Confirm whether
perpl-sdkgained a trigger-order path (e.g. additionalOrderRequestfields or a dedicated request type) in a version newer than the sources reviewed here; if so, document the SDK equivalent for TP/SL.
Handling reconnects and rate limits
WebSocket sequence gaps — track
sn; seed it from the WalletSnapshot and require each Heartbeat (mt: 100) to besn + 1. On a gap, reconnect and re-subscribe / re-authenticate to get a fresh snapshot.WebSocket auth failure — close code
3401; reconnect and re-send a freshly signedmt: 29frame.Rate limits — approximate: REST public ~100 req/min, REST authenticated ~60 req/min, WS ~50 msg/sec per connection, ~5 connections per IP. On HTTP
429, back off exponentially (1s / 2s / 4s).
Full reconnection and error-handling patterns are in the WebSocket reference and Types & Errors.
See also
Quickstart — create a key and make your first signed call.
Networks & Configuration — endpoints, addresses, market IDs.
Authentication — REST and WebSocket signing spec.
REST API reference — every endpoint, pagination, response shapes.
WebSocket reference — message types, streams, order semantics.
Types & Errors — enums,
Position/Accountfields, reject reasons.SDK Concepts — the Rust
perpl-sdksnapshot/stream/order model.
TODO(author): Confirm the final GitBook navigation slugs for the cross-links above once the site structure is published (the API pages may live under a
direct-api/section rather thanapi/); adjust the relative paths to match.
Last updated