Docs

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

MethodReturnsDescription
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

MethodReturnsDescription
address()AddressSigner's wallet address
deployments()&DeploymentsContract deployment addresses
provider()&RootProvider<Ethereum>Underlying Alloy provider
wallet()&EthereumWalletSigning wallet
transport()&HftTransportTransport 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 ID
  • usd_delta: i128 -- USD amount to add (positive) or remove (negative)
  • perp_limit: u128 -- Slippage limit
  • urgency: 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 ID
  • margin_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.

MethodDescription
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

FunctionReturnsDescription
math::entry_price(entry_perp_delta, entry_usd_delta)f64Entry price from raw deltas
math::position_size(entry_perp_delta)f64Position size in base asset
math::position_value(entry_perp_delta, mark_price)f64Notional value at mark price
math::leverage(position_value, effective_margin)f64Current leverage ratio
math::liquidation_price(entry_price, perp_delta, margin, liq_ratio, is_long)f64Liquidation price

Conversions

FunctionReturnsDescription
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)f64Scale from 6-decimal format

Liquidity

FunctionReturnsDescription
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.

LevelBase Fee MultiplierPriority Fee MultiplierUse Case
Low1x1xNon-urgent reads/approvals
Normal2x1xStandard trading (default)
High3x2xTime-sensitive trades
Critical4x5xLiquidation avoidance

Gas Limits

Pre-computed gas limits (no estimateGas RPC call on the hot path).

OperationGas Limit
APPROVE60,000
OPEN_TAKER700,000
OPEN_MAKER800,000
CLOSE_POSITION600,000
ADJUST_NOTIONAL350,000
ADJUST_MARGIN250,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()?;
StrategyDescription
RoundRobinCycle through endpoints
LatencyBasedRoute 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()?;
ParameterDefaultDescription
failure_threshold3Consecutive failures before circuit opens
recovery_timeout30sTime in open state before a half-open probe
half_open_max_requests1Max 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);
    }
});
MethodReturnsDescription
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()&RootProviderAccess 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):

  1. Stop-loss (highest)
  2. Take-profit
  3. Trailing stop
MethodReturnsDescription
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}"),
}
ErrorDescription
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)