Perp CityDocs

Installation

How to add the PerpCity Rust SDK to your project.

Prerequisites

  • Rust 1.91+ (2024 edition)
  • A wallet private key (or an AWS KMS key -- see below)
  • Access to an Arbitrum RPC endpoint
  • Tokio async runtime

Add as a Dependency

Add to your Cargo.toml:

[dependencies]
perpcity-sdk = "0.3"
alloy = "1"
tokio = { version = "1", features = ["full"] }

To sign with an AWS KMS key that never leaves AWS instead of a local private key, enable the aws feature:

perpcity-sdk = { version = "0.3", features = ["aws"] }

Environment Configuration

Create a .env file in your project root:

RPC_URL=https://your-rpc-endpoint.example/v2/YOUR_KEY
READ_RPC_URL=https://arbitrum-one-rpc.publicnode.com  # optional, for read/write split
PRIVATE_KEY=0xYOUR_PRIVATE_KEY
PERP_ADDRESS=0xPERP          # the market's Perp contract

Never commit your .env file to version control. Add it to .gitignore to protect your private key.

Each market is its own Perp contract, identified by its address -- a PerpClient is constructed per market. The USDC and Uniswap V4 PoolManager addresses are provided as constants (ARBITRUM_USDC, ARBITRUM_POOL_MANAGER, and their ARBITRUM_SEPOLIA_* counterparts).

Basic Setup

use alloy::primitives::{Address, U256};
use alloy::signers::local::PrivateKeySigner;
use perpcity_sdk::prelude::*;

#[tokio::main]
async fn main() -> perpcity_sdk::Result<()> {
    let rpc_url = std::env::var("RPC_URL").expect("RPC_URL required");
    let private_key = std::env::var("PRIVATE_KEY").expect("PRIVATE_KEY required");
    let perp = std::env::var("PERP_ADDRESS").expect("PERP_ADDRESS required");

    let signer: PrivateKeySigner = private_key.parse()?;

    let transport = HftTransport::new(
        TransportConfig::builder()
            .shared_endpoint(&rpc_url)
            .build()?,
    )?;

    let deployments = Deployments {
        perp: perp.parse::<Address>()?,
        usdc: ARBITRUM_USDC,
        pool_manager: ARBITRUM_POOL_MANAGER,
    };

    let client = PerpClient::new_arbitrum(transport, signer, deployments)?;

    // Required: initialize nonce and gas before trading
    client.sync_nonce().await?;
    client.refresh_gas().await?;
    client.ensure_approval(U256::MAX).await?;

    // Verify connection
    let (config, snapshot) = client.get_perp_snapshot().await?;
    println!("Mark price: {:.2}", snapshot.mark_price);

    Ok(())
}

Use PerpClient::new_arbitrum_sepolia for testnet, or PerpClient::new(transport, signer, deployments, chain_id) for an explicit chain ID. The client accepts any alloy transaction signer (TxSigner), not just PrivateKeySigner -- with the aws feature, pass an alloy::signers::aws::AwsSigner for KMS signing (see examples/aws_kms_signer.rs in the repository).

Always call sync_nonce(), refresh_gas(), and ensure_approval() before your first transaction. These initialize the lock-free nonce tracker, fee cache, and USDC spending approval.

Endpoint Pools

The transport organizes endpoints into three pools: shared, read, and write. Every RPC request is automatically classified as a read (eth_call, eth_getBalance, etc.) or write (eth_sendRawTransaction) and routed to the appropriate pool.

Read request:  read pool → shared pool (fallback)
Write request: write pool → shared pool (fallback)

Each pool is an independent unit with its own endpoints, circuit breakers, latency tracking, and round-robin counter. If all endpoints in the dedicated pool are unhealthy (circuit breakers tripped), requests automatically fall back to the shared pool. When the dedicated pool recovers (circuit breaker probes succeed after the recovery timeout), requests route back to it.

let transport = HftTransport::new(
    TransportConfig::builder()
        .shared_endpoint("https://arb-mainnet.g.alchemy.com/v2/KEY")  // writes + read fallback
        .read_endpoint("https://arbitrum-one-rpc.publicnode.com")      // dedicated reads (free)
        .build()?,
)?;
PoolBuilder methodPurpose
Shared.shared_endpoint()Handles any request type, serves as fallback for both read and write pools
Read.read_endpoint()Dedicated to read operations. Use free/public RPCs here to reduce costs
Write.write_endpoint()Dedicated to write operations. Use reliable paid RPCs for transaction broadcast

This design enables routing reads to free public RPCs while reserving paid endpoints for writes -- the primary mechanism for reducing RPC costs. Each endpoint only needs to appear in one pool (no duplication).

If you only use .shared_endpoint(), all requests go through the shared pool -- the same behavior as a single-pool setup. The read and write pools are optional.

Multi-Endpoint Transport

For production setups, configure multiple endpoints per pool with automatic failover:

use perpcity_sdk::transport::config::Strategy;
use std::time::Duration;

let transport = HftTransport::new(
    TransportConfig::builder()
        .shared_endpoint("https://primary-rpc.com")
        .shared_endpoint("https://fallback-rpc.com")
        .strategy(Strategy::LatencyBased)
        .request_timeout(Duration::from_secs(2))
        .build()?,
)?;

Available strategies:

StrategyDescription
RoundRobinCycle through endpoints sequentially
LatencyBasedRoute to the lowest-latency healthy endpoint (default)
Hedged { fan_out }Fan out to N endpoints, take the fastest response

Release Profile

For production builds, add to your Cargo.toml:

[profile.release]
lto = "fat"
codegen-units = 1
panic = "abort"
strip = "symbols"

Build Commands

cargo build                # Debug build
cargo build --release      # Optimized release build
cargo test                 # Run tests
cargo run --example quickstart  # Run quickstart example

Next Steps

  • Browse the API Reference for all types, functions, and HFT infrastructure
  • Check the examples for full trading workflows