Examples
The dex-sdk-examples repository is a small Cargo workspace of runnable programs built on top of the Perpl SDK (perpl-sdk). It shows how to build an exchange snapshot, stream on-chain events, keep a local cache current, and submit orders — the same building blocks you would use in a production trading bot.
There are two packages:
perpl_market_making_bot
A configurable trading bot with three interchangeable strategies (best bid and offer, spread, taker)
perpl_market_making_bot
perpl_utilities
Read-only tools that stream and pretty-print live exchange state
print_book, print_trades, perpl_test_exchange
Note: The examples workspace pins
alloy = "1.4.0", while theperpl-sdkcrate itself usesalloy = "2.0.4". If you copy code from the examples into a project that also depends on a newer SDK build, align thealloyversion to avoid duplicate-crate type mismatches.
The examples depend on the SDK by path (perpl-sdk = { path = "../dex-sdk/crates/sdk" }), so they expect the dex-sdk repository to be checked out as a sibling directory:
parent/
├── dex-sdk/ # the perpl-sdk crate
└── dex-sdk-examples/ # this repositoryPrerequisites
Rust 1.85.0 or newer (the SDK uses edition 2024).
Foundry
anvil— only needed for the local test exchange (perpl_test_exchange); not required to run against testnet.A checkout of the
dex-sdkrepository as a sibling ofdex-sdk-examples(see above).
Clone and build
git clone https://github.com/PerplFoundation/dex-sdk-examples.git
cd dex-sdk-examples
cargo buildAll commands below are run from the workspace root. cargo run --bin <name> builds and runs a single binary.
The market-making bot
perpl_market_making_bot is one bot that can run any of three strategies. Connection and account settings come from the environment (loaded from a .env file); the strategy and its parameters come from the command line.
Configuration
The bot reads a PerplConfig from environment variables via envy + dotenvy. Create a .env file in the workspace root:
The example above targets testnet (BTC is perpetual 16). The values are:
Variable
Meaning
Testnet value (from Chain::testnet())
CHAIN_ID
Chain ID
10143
COLLATERAL_TOKEN_ADDRESS
Collateral token (ERC-20) address
0xa9012a055bd4e0eDfF8Ce09f960291C09D5322dC
ADDRESS
Exchange contract address
0x1964C32f0bE608E7D29302AFF5E61268E72080cc
DEPLOYED_AT_BLOCK
Block the exchange was deployed at (snapshot lower bound)
62953
PERPETUAL_ID
Perpetual market to trade
16 (BTC)
NODE_RPC_URL
Monad JSON-RPC endpoint
https://testnet-rpc.monad.xyz
TIMEOUT_SECONDS
Fallback strategy-run interval when no events arrive
30 (default)
PRIVATE_KEY
Private key for the trading account
your key
Note: The
.envshipped in the repository points at a local Anvil instance (CHAIN_ID=1337,NODE_RPC_URL="http://localhost:52778/") and ships a well-known Anvil development key. Use those defaults only against the local test exchange (perpl_test_exchange); replace every value with your own for testnet or mainnet, and never commit a real private key.
The bot builds a chain config from these values with Chain::custom(...):
Run a strategy
The bot takes a strategy subcommand plus that strategy's flags:
Strategy flags:
bbo
--order-size
yes
Size of each quote
spread
--orders-per-side
yes
Number of orders to place on each side
spread
--order-size
yes
Size of each order
spread
--max-matches
no
Max matches per order (max_matches)
spread
--leverage
no
Leverage for each order (default 1)
taker
--order-size
yes
Maximum order size (actual size is randomized up to this)
taker
--leverage
no
Leverage for each order (default 1)
Set RUST_LOG to control log verbosity (the bot defaults to info if unset):
How the bot loop works
PerplMarketMakingBot::try_new(...) wires up an alloy provider with your wallet and an ExchangeInstance, then run() executes this loop (market-making/src/lib.rs):
Select
A tokio::select! reacts to whichever fires first:
a new stream event →
exchange.apply_events(...)updates the cache, thenstrategy.execute(...)runs on the resulting state events;an error reported back from a previous submission → re-run
execute;a timeout tick (every
TIMEOUT_SECONDS) → runexecuteanyway, in case the market is quiet.
Note:
stream::rawandstream::tradeare not cancellation-safe — do not drop them across anawaitin aselect!arm without pinning, as the example does withpin!(...).
How orders are submitted
Every strategy expresses intent as an OrderRequest, calls .prepare(&exchange) to scale human-readable decimals into the contract's fixed-point OrderDesc, then submits a batch:
execOrders(orderDescs, revertOnFail) takes the prepared order descriptions and a revertOnFail flag — when true, the whole batch reverts together if any order fails.
An OrderRequest is constructed positionally. The BBO strategy's place_order shows the shape:
RequestType values used by the strategies:
RequestType
Effect
OpenLong
Open/add to a long (a bid)
OpenShort
Open/add to a short (an ask)
CloseLong
Reduce/close a long (reduce-only ask)
CloseShort
Reduce/close a short (reduce-only bid)
Cancel
Cancel a resting order (order_id required)
Change
Amend a resting order's price/size (order_id required)
Strategy: BBO
File: market-making/src/strategies/bbo.rs — best bid and offer (BBO).
Keeps exactly one bid and one ask quoting at the current top of book.
Initialize: requires exactly one account in the snapshot (errors otherwise), stores the account ID, then cancels all existing orders in one atomic batch.
Execute: acts only when a fill event is present in the block's state events. On a fill it reads the current best bid and best ask from the level-3 (L3) book:
if there is no resting bid, place a
post_onlyOpenLongat the best bid; if there is one and it is below the best bid,Changeit up to the best bid;symmetrically for the ask side with
OpenShort.
Orders use leverage
1and are submitted atomically. The receipt is awaited on a spawned task so the loop keeps consuming events; errors flow back through theerror_txchannel.
Strategy: Spread
File: market-making/src/strategies/spread.rs
Maintains a ladder of orders_per_side quotes on each side, stepped away from the mark price.
Target prices: for
iin1..=orders_per_side, the offset isi / 500(≈ 0.2% per step). Bid prices aremark * (1 - offset), ask prices aremark * (1 + offset).Reconciliation (
create_target_order_changes): for each target price it keeps an existing order if the size already matches,Changes it if the size differs, reuses a spare order at a new price, or places a newpost_onlyorder for anything left over. This minimizes churn versus cancel-and-replace.Ordering: whether bids or asks are submitted first depends on the mark-price direction (
bids_firstwhen the new mark is at or below the previous mark), so the side moving toward the market is refreshed first.Orders honor the optional
--leverageand--max-matchesflags and are submitted non-atomically (atomic = false).
Strategy: Taker
File: market-making/src/strategies/taker.rs
A liquidity-taking stress/demo strategy that repeatedly crosses the book.
Side: chosen randomly each run via a
Bernoulli(0.5)distribution (long or short).Size: a random fraction in
(0, 1](OpenClosed01) times--order-size.Position handling: if a position exists on the opposite side, it is closed first (
CloseLong/CloseShortfor the full position size), then a new position is opened.Crossing price: opening orders are
immediate_or_cancel(IoC) with a price ofUD64::MAXfor a buy andUD64::ZEROfor a sell, so they always cross whatever is resting. Submitted non-atomically.
Add your own strategy
All three implement the Strategy trait (market-making/src/strategies/mod.rs):
To add a strategy: implement the trait for a new struct, add a variant to the StrategyType enum (which fans method calls out to each concrete strategy), and add a clap subcommand in main.rs that constructs it.
The utilities
The perpl_utilities package holds read-only tools. They are the fastest way to confirm your RPC endpoint and market are live and to see the SDK's state types in action.
print_book
Streams a single market's order book and reprints it whenever state changes. Exercises the Perpetual accessors and the OrderBook level-2 (L2), level-3 (L3), and compact renderers.
Flags (utilities/src/print-book.rs):
-c, --chain
testnet
Chain to connect to. Only testnet is supported.
-m, --market
— (required)
Perpetual market ID (e.g. 16 for BTC on testnet)
-r, --rpc-url
— (required)
Monad JSON-RPC URL
-d, --depth
10
Price levels to display (0 = all)
-p, --poll-interval
500
RPC poll interval in milliseconds
--mode
l3
l2 (aggregated levels), l3 (individual orders), or compact
--orders-per-level
5
Max orders shown per level in L3/compact (0 = all)
It first prints a market-info block (name, last/mark/oracle price, funding rate, open interest, fees, margins, paused flag) and the initial book, then uses a retry-backoff RPC client and stream::raw to reprint on every block that produces state events. The market ID must be one of the chain's perpetuals or the tool exits.
print_trades
Streams and prints normalized trades from testnet. It has no command-line flags — the RPC endpoint (https://testnet-rpc.monad.xyz) and Chain::testnet() are hard-coded, and it starts from the current block.
It pipes stream::raw into stream::trade, which aggregates maker and taker fills into per-taker Trades. For each block it prints the taker (account, side, total size, average price, perpetual, fee) and every maker fill (maker_account_id, maker_order_id, size, price, fee) — a good template for downstream trade analytics.
perpl_test_exchange
Starts a local Anvil instance with the Perpl exchange deployed and seeded test accounts, using the SDK's testing::TestExchange helper (requires the SDK testing feature). It creates two maker accounts (IDs 0 and 1) and one taker (ID 2), each funded with 1,000,000 units of collateral, plus a BTC perpetual market, then logs the exchange address and RPC URL and stays running.
Point the market-making bot's .env at the address, RPC URL, and chain ID this prints to drive strategies against a fully local exchange — no testnet funds or connectivity required.
Next steps
Chain configs, market IDs, and endpoints for every network: Networks.
Generate the full SDK API docs locally:
cargo doc -p perpl-sdk --no-deps --open.For ad-hoc live inspection without writing code, use the
perpl-clitool shipped in thedex-sdkrepository.
Last updated