API Reference
Complete reference for the PerpCity Rust SDK -- client, trading, math, and HFT infrastructure.
Complete reference for the PerpCity Rust SDK.
PerpClient
The main client for interacting with the PerpCity protocol.
use perpcity_sdk::{PerpClient, Deployments};
use perpcity_sdk::transport::TransportConfig;
let transport = TransportConfig::new(vec!["https://base-rpc-url.com".into()])
.build()?;
let deployments = Deployments {
perp_manager: "0x...".parse()?,
usdc: "0x...".parse()?,
..Default::default()
};
let mut client = PerpClient::new_base_mainnet(transport, signer, deployments)?;
client.sync_nonce().await?;
client.refresh_gas().await?;Initialization
| Method | Returns | Description |
|---|---|---|
new(transport, signer, deployments, chain_id) | Result<Self> | Create client with explicit chain ID |
new_base_mainnet(transport, signer, deployments) | Result<Self> | Create client for Base L2 mainnet |
sync_nonce() | Result<()> | Initialize lock-free nonce tracking from on-chain state |
refresh_gas() | Result<()> | Update EIP-1559 gas cache from latest block header |
ensure_approval(min_amount) | Result<Option<B256>> | Check/set USDC spending approval |
Accessors
| Method | Returns | Description |
|---|---|---|
address() | Address | Signer's wallet address |
deployments() | &Deployments | Contract deployment addresses |
provider() | &RootProvider<Ethereum> | Underlying Alloy provider |
wallet() | &EthereumWallet | Signing wallet |
transport() | &HftTransport | Transport for health diagnostics |
Trading Functions
open_taker
Opens a leveraged long or short position.
use perpcity_sdk::{OpenTakerParams, Urgency};
let pos_id = client.open_taker(perp_id, OpenTakerParams {
is_long: true,
margin: 100.0, // 100 USDC
leverage: 10.0, // 10x
unspecified_amount_limit: 0,
}, Urgency::Normal).await?;Returns: Result<U256> -- Position ID
open_maker
Opens a liquidity provider (maker) position. Price range is aligned to tick spacing automatically.
use perpcity_sdk::OpenMakerParams;
let pos_id = client.open_maker(perp_id, OpenMakerParams {
margin: 10000.0,
price_lower: 2900.0,
price_upper: 3100.0,
liquidity: 1_000_000,
max_amt0_in: 1_000_000,
max_amt1_in: 1_000_000,
}, Urgency::Normal).await?;Maker positions are subject to a lockup period (7 days on testnet). You cannot close a maker position before the lockup expires.
close_position
Closes any position (taker or maker).
use perpcity_sdk::CloseParams;
let result = client.close_position(pos_id, CloseParams {
min_amt0_out: 0,
min_amt1_out: 0,
max_amt1_in: 1_000_000,
}, Urgency::Normal).await?;
println!("tx: {:?}", result.tx_hash);
if let Some(remaining) = result.remaining_position_id {
println!("Partial close, remaining: {remaining}");
}Returns: Result<CloseResult> -- Transaction hash and optional remaining position ID (for partial closes)
adjust_notional
Increase or decrease position exposure.
client.adjust_notional(pos_id, 5000, 0, Urgency::Normal).await?;Parameters:
pos_id: U256-- Position IDusd_delta: i128-- USD amount to add (positive) or remove (negative)perp_limit: u128-- Slippage limiturgency: Urgency-- Gas priority level
adjust_margin
Add or remove collateral from a position.
client.adjust_margin(pos_id, 1000, Urgency::Normal).await?;Parameters:
pos_id: U256-- Position IDmargin_delta: i128-- Margin to add (positive) or remove (negative)urgency: Urgency-- Gas priority level
Market Data
get_perp_config
Fetches full perp market configuration including mark price, fees, and bounds.
let config = client.get_perp_config(perp_id).await?;
println!("Mark: {:.2}", config.mark);
println!("Max leverage: {:.0}x", config.bounds.max_taker_leverage);
println!("Tick spacing: {}", config.tick_spacing);Returns: Result<PerpData>
get_perp_data
Fetches beacon address, tick spacing, and mark price.
let (beacon, tick_spacing, mark) = client.get_perp_data(perp_id).await?;Returns: Result<(Address, i32, f64)>
get_mark_price
Current mark price from 1-second TWAP.
let mark = client.get_mark_price(perp_id).await?;get_funding_rate
Daily funding rate for a perp market.
let rate = client.get_funding_rate(perp_id).await?;get_open_interest
Long and short open interest.
let oi = client.get_open_interest(perp_id).await?;
println!("Long OI: {:.2}, Short OI: {:.2}", oi.long_oi, oi.short_oi);get_position
Raw on-chain position struct.
let position = client.get_position(pos_id).await?;get_live_details
Live PnL, funding, effective margin, and liquidation status.
let details = client.get_live_details(pos_id).await?;
println!("PnL: {:.2}", details.pnl);
println!("Funding: {:.2}", details.funding_payment);
println!("Effective margin: {:.2}", details.effective_margin);
println!("Liquidatable: {}", details.is_liquidatable);get_usdc_balance
Wallet USDC balance.
let balance = client.get_usdc_balance().await?;Simulations (Quotes)
Read-only simulation of trades without submitting transactions.
quote_open_taker
Simulate a taker position opening.
let quote = client.quote_open_taker(perp_id, OpenTakerParams {
is_long: true,
margin: 100.0,
leverage: 10.0,
unspecified_amount_limit: 0,
}).await?;
println!("Perp delta: {:.6}, USD delta: {:.2}", quote.perp_delta, quote.usd_delta);quote_open_maker
Simulate a maker position opening.
let quote = client.quote_open_maker(perp_id, OpenMakerParams {
margin: 10000.0,
price_lower: 2900.0,
price_upper: 3100.0,
liquidity: 1_000_000,
max_amt0_in: 1_000_000,
max_amt1_in: 1_000_000,
}).await?;quote_swap
Simulate a raw pool swap.
let quote = client.quote_swap(
perp_id,
true, // zero_for_one
true, // is_exact_in
1_000_000, // amount (6-decimal)
None, // sqrt_price_limit (optional)
).await?;Cache Management
The SDK maintains a 2-tier TTL cache tuned for Base L2 block times.
| Method | Description |
|---|---|
invalidate_fast_cache() | Clear prices, funding, balance (2s TTL layer) |
invalidate_all_cache() | Clear all cached state |
confirm_tx(tx_hash) | Remove transaction from in-flight tracking |
fail_tx(tx_hash) | Release nonce on transaction failure |
in_flight_count() | Number of unconfirmed transactions |
Calculation Functions
Pure math -- no network required. Available via the math module.
Position Math
| Function | Returns | Description |
|---|---|---|
math::entry_price(entry_perp_delta, entry_usd_delta) | f64 | Entry price from raw deltas |
math::position_size(entry_perp_delta) | f64 | Position size in base asset |
math::position_value(entry_perp_delta, mark_price) | f64 | Notional value at mark price |
math::leverage(position_value, effective_margin) | f64 | Current leverage ratio |
math::liquidation_price(entry_price, perp_delta, margin, liq_ratio, is_long) | f64 | Liquidation price |
Conversions
| Function | Returns | Description |
|---|---|---|
convert::price_to_sqrt_price_x96(price) | Result<U256> | Price to Uniswap V4 sqrtPriceX96 |
convert::sqrt_price_x96_to_price(sqrt) | Result<f64> | sqrtPriceX96 to decimal price |
math::price_to_tick(price) | Result<i32> | Price to Uniswap tick |
math::tick_to_price(tick) | Result<f64> | Tick to price (1.0001^tick) |
convert::scale_to_6dec(amount) | Result<i128> | Scale f64 to 6-decimal USDC format |
convert::scale_from_6dec(value) | f64 | Scale from 6-decimal format |
Liquidity
| Function | Returns | Description |
|---|---|---|
math::get_sqrt_ratio_at_tick(tick) | Result<U256> | sqrtPriceX96 for a tick |
math::estimate_liquidity(tick_lower, tick_upper, margin_scaled) | Result<u128> | Liquidity estimate for USD amount |
HFT Infrastructure
Urgency Levels
Controls EIP-1559 fee scaling for transaction priority.
| Level | Base Fee Multiplier | Priority Fee Multiplier | Use Case |
|---|---|---|---|
Low | 1x | 1x | Non-urgent reads/approvals |
Normal | 2x | 1x | Standard trading (default) |
High | 3x | 2x | Time-sensitive trades |
Critical | 4x | 5x | Liquidation avoidance |
Gas Limits
Pre-computed gas limits (no estimateGas RPC call on the hot path).
| Operation | Gas Limit |
|---|---|
APPROVE | 60,000 |
OPEN_TAKER | 700,000 |
OPEN_MAKER | 800,000 |
CLOSE_POSITION | 600,000 |
ADJUST_NOTIONAL | 350,000 |
ADJUST_MARGIN | 250,000 |
Transport Configuration
use perpcity_sdk::transport::{TransportConfig, Strategy};
use std::time::Duration;
let transport = TransportConfig::new(vec![
"https://primary-rpc.com".into(),
"https://fallback-rpc.com".into(),
])
.strategy(Strategy::Hedged { fan_out: 2 })
.request_timeout(Duration::from_secs(2))
.build()?;| Strategy | Description |
|---|---|
RoundRobin | Cycle through endpoints |
LatencyBased | Route to lowest-latency healthy endpoint |
Hedged { fan_out } | Fan out to N endpoints, take fastest response |
Circuit Breaker
Each endpoint has an independent circuit breaker that removes unhealthy RPCs from rotation.
use perpcity_sdk::transport::CircuitBreakerConfig;
use std::time::Duration;
let transport = TransportConfig::new(vec![
"https://primary-rpc.com".into(),
"https://fallback-rpc.com".into(),
])
.circuit_breaker(CircuitBreakerConfig {
failure_threshold: 3, // consecutive failures before opening
recovery_timeout: Duration::from_secs(30), // wait before probing again
half_open_max_requests: 1, // concurrent probes in half-open state
})
.build()?;| Parameter | Default | Description |
|---|---|---|
failure_threshold | 3 | Consecutive failures before circuit opens |
recovery_timeout | 30s | Time in open state before a half-open probe |
half_open_max_requests | 1 | Max concurrent requests during half-open probing |
Retry Policy
Reads are retried with exponential backoff. Writes (transactions) are never retried.
use perpcity_sdk::transport::RetryConfig;
let transport = TransportConfig::new(vec!["https://rpc.com".into()])
.retry(RetryConfig {
max_retries: 2, // retry attempts for reads
base_delay: Duration::from_millis(100), // delay scales by 2^attempt
})
.build()?;WebSocket Subscriptions
WebSocket support is not yet live on PerpCity. The WsManager API is available in the SDK and ready to use once WebSocket endpoints are enabled.
The WsManager provides WebSocket subscriptions for real-time block headers and contract events, with automatic reconnection on disconnect.
use perpcity_sdk::transport::ws::{WsManager, ReconnectConfig};
use std::time::Duration;
let ws = WsManager::connect(
"wss://base-ws-url.com",
ReconnectConfig {
initial_backoff: Duration::from_millis(500),
max_backoff: Duration::from_secs(30),
backoff_multiplier: 2,
max_attempts: 0, // 0 = unlimited
},
).await?;
// Subscribe to new block headers
let mut blocks = ws.subscribe_blocks().await?;
tokio::spawn(async move {
while let Some(header) = blocks.recv().await {
println!("New block: {}", header.number);
}
});
// Subscribe to contract event logs
let mut logs = ws.subscribe_logs(filter).await?;
tokio::spawn(async move {
while let Some(log) = logs.recv().await {
println!("Event: {:?}", log);
}
});| Method | Returns | Description |
|---|---|---|
connect(url, config) | Result<WsManager> | Establish WebSocket connection |
subscribe_blocks() | Result<Receiver<Header>> | Stream new block headers |
subscribe_logs(filter) | Result<Receiver<Log>> | Stream contract events matching a filter |
reconnect() | Result<()> | Reconnect with exponential backoff |
provider() | &RootProvider | Access underlying provider for direct RPC calls |
Position Manager
Track positions with automated stop-loss, take-profit, and trailing stop triggers. Evaluate all positions against current prices in a single call.
use perpcity_sdk::hft::position_manager::PositionManager;
let mut pm = PositionManager::new();
pm.track(ManagedPosition {
perp_id,
position_id: 42,
is_long: true,
entry_price: 3000.0,
margin: 1000.0,
stop_loss: Some(2800.0),
take_profit: Some(3500.0),
trailing_stop_pct: Some(0.05), // 5% trailing stop
trailing_stop_anchor: None, // auto-set on first check
});
// In your trading loop, check all triggers against current prices
let actions = pm.check_triggers(current_price);
for action in actions {
match action.trigger {
TriggerType::StopLoss => { /* close position */ }
TriggerType::TakeProfit => { /* close position */ }
TriggerType::TrailingStop => { /* close position */ }
}
}Trailing stop behavior:
- Longs: Anchor tracks highest price seen. Fires when
price <= anchor * (1 - pct) - Shorts: Anchor tracks lowest price seen. Fires when
price >= anchor * (1 + pct) - Anchor only moves in the favorable direction
Trigger priority (when multiple fire simultaneously):
- Stop-loss (highest)
- Take-profit
- Trailing stop
| Method | Returns | Description |
|---|---|---|
track(position) | () | Start tracking a position |
untrack(position_id) | () | Stop tracking a position |
check_triggers(price) | Vec<TriggerAction> | Evaluate all positions, return fired triggers |
check_triggers_into(price, buf) | () | Zero-allocation version (appends to pre-allocated Vec) |
get(position_id) | Option<&ManagedPosition> | Access a tracked position |
get_mut(position_id) | Option<&mut ManagedPosition> | Mutably access a tracked position |
Types
Core
pub struct Deployments {
pub perp_manager: Address,
pub usdc: Address,
pub fees_module: Option<Address>,
pub margin_ratios_module: Option<Address>,
pub lockup_period_module: Option<Address>,
pub sqrt_price_impact_limit_module: Option<Address>,
}Market Data
pub struct PerpData {
pub id: B256,
pub tick_spacing: i32,
pub mark: f64,
pub beacon: Address,
pub bounds: Bounds,
pub fees: Fees,
}
pub struct Bounds {
pub min_margin: f64,
pub min_taker_leverage: f64,
pub max_taker_leverage: f64,
pub liquidation_taker_ratio: f64,
}
pub struct Fees {
pub creator_fee: f64,
pub insurance_fee: f64,
pub lp_fee: f64,
pub liquidation_fee: f64,
}
pub struct OpenInterest {
pub long_oi: f64,
pub short_oi: f64,
}Position
pub struct LiveDetails {
pub pnl: f64,
pub funding_payment: f64,
pub effective_margin: f64,
pub is_liquidatable: bool,
}
pub struct CloseResult {
pub tx_hash: B256,
pub remaining_position_id: Option<U256>,
}Quote Results
pub struct OpenTakerQuote {
pub perp_delta: f64,
pub usd_delta: f64,
}
pub struct OpenMakerQuote {
pub perp_delta: f64,
pub usd_delta: f64,
}
pub struct SwapQuote {
pub perp_delta: f64,
pub usd_delta: f64,
}Position Manager
pub struct ManagedPosition {
pub perp_id: [u8; 32],
pub position_id: u64,
pub is_long: bool,
pub entry_price: f64,
pub margin: f64,
pub stop_loss: Option<f64>,
pub take_profit: Option<f64>,
pub trailing_stop_pct: Option<f64>,
pub trailing_stop_anchor: Option<f64>,
}
pub enum TriggerType {
StopLoss,
TakeProfit,
TrailingStop,
}WebSocket
pub struct ReconnectConfig {
pub initial_backoff: Duration, // default: 500ms
pub max_backoff: Duration, // default: 30s
pub backoff_multiplier: u32, // default: 2
pub max_attempts: u32, // default: 0 (unlimited)
}Parameters
pub struct OpenTakerParams {
pub is_long: bool,
pub margin: f64,
pub leverage: f64,
pub unspecified_amount_limit: u128,
}
pub struct OpenMakerParams {
pub margin: f64,
pub price_lower: f64,
pub price_upper: f64,
pub liquidity: u128,
pub max_amt0_in: u128,
pub max_amt1_in: u128,
}
pub struct CloseParams {
pub min_amt0_out: u128,
pub min_amt1_out: u128,
pub max_amt1_in: u128,
}Error Handling
All errors use the PerpCityError enum with thiserror:
use perpcity_sdk::PerpCityError;
match client.open_taker(perp_id, params, Urgency::Normal).await {
Ok(pos_id) => println!("Opened position: {pos_id}"),
Err(PerpCityError::InvalidMargin { reason }) => eprintln!("Bad margin: {reason}"),
Err(PerpCityError::TxReverted { reason }) => eprintln!("Reverted: {reason}"),
Err(PerpCityError::TooManyInFlight { count, max }) => {
eprintln!("Too many in-flight txs: {count}/{max}");
}
Err(e) => eprintln!("Error: {e}"),
}| Error | Description |
|---|---|
InvalidPrice { reason } | Invalid price value |
InvalidMargin { reason } | Margin validation failure |
InvalidLeverage { reason } | Leverage out of bounds |
InvalidTickRange { lower, upper } | Lower tick >= upper tick |
InvalidConfig { reason } | Configuration error |
Overflow { context } | Arithmetic overflow |
TxReverted { reason } | On-chain transaction reverted |
EventNotFound { event_name } | Expected event not emitted |
GasPriceUnavailable { reason } | Gas cache not initialized |
TooManyInFlight { count, max } | Nonce exhaustion |
PerpNotFound { perp_id } | Unknown perp market |
PositionNotFound { pos_id } | Unknown position |
ModuleNotRegistered { module } | Required module address not set in Deployments |
AlloyContract(...) | Contract interaction error (from Alloy) |
AlloyTransport(...) | RPC transport error (from Alloy) |