For the complete documentation index, see llms.txt. This page is also available as Markdown.

Quickstart

The Perpl Rust SDK (perpl-sdk) gives you a convenient in-memory cache of on-chain exchange state. The pattern is always the same: capture an initial snapshot, then stream on-chain events to keep that snapshot current, and read prices, order books, positions, and trades straight out of the cache. When you want to trade, you build an OrderRequest, prepare it against the cached state, and submit it to the Exchange contract.

This page walks through a runnable example end to end. Every type, method, and value shown below comes from the SDK source and the shipped examples.

SDK (software development kit); RPC (remote procedure call); L2/L3 refer to level-2 (aggregated price levels) and level-3 (individual resting orders) views of an order book; bps means basis points.

Prerequisites

Requirement
Value

Rust toolchain

1.85.0 or newer (the SDK uses Rust edition 2024)

Local testing (optional)

Foundry anvil — required only if you use the SDK's testing module against a local node

API docs

cargo doc -p perpl-sdk --no-deps --open

The SDK crate is perpl-sdk version 0.2.0, part of the perpl-sdk workspace (see perpl.xyz).

Add the SDK to your project

The SDK is consumed as a path dependency — point your Cargo.toml at the crates/sdk directory of a local dex-sdk checkout. This is exactly how the shipped examples depend on it:

[package]
name = "my-perpl-app"
edition = "2024"
version = "0.1.0"

[dependencies]
# Adjust the relative path to wherever you checked out dex-sdk.
perpl-sdk = { path = "../dex-sdk/crates/sdk" }

alloy    = { version = "2.0.4", features = ["full"] }
fastnum  = "0.7.4"           # fixed-point decimals used for prices/sizes
futures  = "0.3.31"          # Stream combinators (StreamExt)
tokio    = { version = "1.49", features = ["full"] }

The SDK exposes two Cargo features, both on by default: display (adds std::fmt::Display implementations for the state types, so you can println!("{}", price)) and testing (adds a local testing environment with the collateral token and Exchange contracts deployed).

The core workflow

  1. Choose a Chain (mainnet, testnet, or a custom deployment).

  2. Build an Exchange snapshot with SnapshotBuilder.

  3. Open a stream::raw event stream and call exchange.apply_events(...) for each block to keep the cache current.

  4. Read state (mark_price, l3_book, positions, …) from the cache, or run stream::trade for a normalized trade feed.

  5. To trade, build an OrderRequest, prepare it, and submit via the Exchange contract.

1. Pick a chain

The Chain type carries the full per-network configuration — chain ID, collateral token, Exchange contract address, the block the Exchange was deployed at, and the list of listed perpetual (market) IDs. Use a built-in constructor:

Constructor
Chain ID
Exchange contract
Collateral token
Deploy block
Perpetual IDs

Chain::mainnet()

143

0x34B6552d57a35a1D042CcAe1951BD1C370112a6F

0x00000000eFE302BEAA2b3e6e1b18d08D69a9012a

54773010

[1, 10, 20, 31, 40, 50]

Chain::testnet()

10143

0x1964C32f0bE608E7D29302AFF5E61268E72080cc

0xa9012a055bd4e0eDfF8Ce09f960291C09D5322dC

62953

[16, 32, 48, 64, 256]

On mainnet, SOL is perpetual ID 31 (not 30). Perpetual IDs are network-specific — the same asset has a different ID on testnet. See Networks & Configuration for the full market tables.

Read individual fields with the getters:

For a non-standard deployment (for example a local anvil node), build one explicitly:

2. Build a state snapshot

SnapshotBuilder fetches the current on-chain state for the markets you ask for and returns an Exchange — the in-memory cache. You give it a &Chain and an alloy provider:

Builder options:

Method
Purpose

.at_block(BlockId)

Snapshot state as of a specific block instead of the latest.

.with_perpetuals(Vec<PerpetualId>)

Restrict the snapshot to these markets.

.with_accounts(Vec<AccountId>)

Load only these accounts' positions. Mutually exclusive with .with_all_positions().

.with_all_positions()

Load positions for every account. Mutually exclusive with .with_accounts(...).

.with_orders_per_batch(n) / .with_positions_per_batch(n)

Tune multicall batch sizes (default 1000).

Under the hood, build() normalizes the target block to a concrete number, probes the Exchange for getPerpetualInfoV2 support (falling back to the V0 layout, defaulting the V2-only fundingSumScalingExp / priceResiduePNSQ16 fields to 0), fetches global and per-perpetual parameters, fees, and margins, then reads resting orders (order-ID bitmap → batched getOrder multicalls, preserving first-in-first-out (FIFO) order) and positions. The default batch size of 1000 is tuned for Monad's per-slot gas cost and the 30M-gas eth_call limit.

The returned Exchange exposes the cached state:

3. Stream events to stay current

A snapshot is a point-in-time view. To keep it live, open a stream::raw event stream starting just after the snapshot block and feed every block into exchange.apply_events(...):

stream::raw polls get_logs per block from your starting StateInstant. On Monad it gates on the safe block tag, because the latest tag returns proposed (non-final) blocks.

apply_events returns:

Return
Meaning

Ok(Some(state_events))

The block's events were applied; state_events describes what changed.

Ok(None)

The block was already applied — safe to skip.

Err(_)

The block could not be applied.

Full example: live order book

This is a complete, runnable main.rs modeled on the SDK's print_book utility. It builds a testnet snapshot for one market, prints the market info and the top of book, then streams events and reprints whenever the book changes.

The OrderBook type also supports full L2 and L3 rendering. Iterate aggregated levels with book.bids() / book.asks() (each maps a price to a level with .size() and .num_orders()), and individual resting orders with book.bid_orders() / book.ask_orders(). Each BookOrder exposes order_id(), account_id(), size(), price(), leverage(), expiry_block(), and r#type() (an OrderType).

Reading recent trades

For a normalized trade feed, wrap a stream::raw stream with stream::trade. It aggregates the raw maker/taker fill events into one Trade per taker, with the maker side broken out into individual fills. This is the print_trades utility, in full:

A Trade carries taker_account_id, taker_side, total_size(), avg_price() (an Option), perpetual_id, and taker_fee; its maker_fills is a Vec<MakerFill>, each with maker_account_id, maker_order_id, size, price, and fee.

Posting an order (outline)

Reading state needs only a read-only provider. To trade, you need a provider configured with a wallet (signer), then you build an OrderRequest, prepare it against the cached Exchange, and submit it to the Exchange contract.

Choose a request type. RequestType is a u8 enum:

Value
Variant
Side
Notes

0

OpenLong

Bid

1

OpenShort

Ask

2

CloseLong

Ask

reduce-only

3

CloseShort

Bid

reduce-only

4

Cancel

cancel a resting order

5

IncreasePositionCollateral

6

Change

amend a resting order

Build and prepare the request. OrderRequest::new takes the market, the request type, price/size as fixed-point decimals (fastnum UD64), leverage, and the order flags. prepare(&Exchange) scales the human-readable decimals to the on-chain fixed-point representation and returns an OrderDesc ready to send:

Submit to the Exchange contract. The canonical entry point documented by the SDK is Exchange::ExchangeInstance::execOrders. The shipped examples submit through execOrders, passing the prepared order descriptors and a revertOnFail flag, then await the receipt. Your provider must be a wallet-enabled alloy provider (a DynProvider built with .wallet(wallet)):

Full signer wiring — loading a private key, building the wallet-enabled DynProvider, and driving order submission in a loop — is shown in the market-making example (dex-sdk-examples/market-making), which implements best-bid-offer (BBO), spread, and taker strategies on top of exactly this flow.

Inspecting state without writing code

For quick, one-off inspection you do not need to write a program — the SDK ships the perpl-cli binary, which uses the same snapshot/stream machinery:

perpl-cli defaults to mainnet; pass --testnet for testnet, or --rpc <URL> / --exchange <ADDRESS> for a custom deployment. See the CLI reference for the full command set (snapshot, trace, show account, show book, show trades, block <n>, tx <hash>).

Limitations & follow-ups

The SDK documents these current limitations, worth knowing before you build on it:

  • Funding-event processing is not yet implemented — funding data is a planned follow-up.

  • Event streaming uses log polling. Future versions may lower indexing latency with WebSocket subscriptions and/or Monad execution events. Because it polls, always run behind a retry/backoff layer.

  • Test coverage is limited. Treat the SDK as early-stage and validate against testnet before going to mainnet.

Next steps

  • Networks & Configuration — every endpoint, contract address, chain ID, and market ID for both networks.

  • Generate the full API reference locally: cargo doc -p perpl-sdk --no-deps --open.

  • Explore the dex-sdk-examples workspace: utilities (print_book, print_trades) and market-making (BBO, spread, and taker strategies).

Last updated