acta-maker-sdk 0.3.0

Rust SDK for Acta options market makers — WebSocket client, order signing, message types
Documentation

acta-maker-sdk

Rust SDK for Acta options market makers. Handles WebSocket connectivity, authentication, order signing, and message serialization.

Two API levels:

  • ManagedWs — reconnects and authenticates automatically, and exposes send_await() for correlated responses.
  • WsClient — raw WebSocket client. You manage the connection yourself. Good for scripts and testing.

No features enabled by default — lightweight core with opt-in WS and Solana support.

WebSocket snippets are compiled with cargo test --doc --features ws-client; chain snippets are also compiled with cargo test --doc --features ws-client,chain-rpc. They are marked no_run: they type-check but do not open a live connection.

Install

[dependencies]
acta-maker-sdk = { version = "0.2.0", features = ["ws-client"] }
Feature What it enables
ws-client WebSocket client (WsClient, ManagedWs), requires tokio
chain Solana instruction builders (DepositPremium, WithdrawPremium, FundPosition)
chain-rpc Solana RPC queries (extends chain)
test-helpers Test utilities (ManagedWsHandle::test_handle, message injection)

Quick start (ManagedWs)

use acta_maker_sdk::*;
use acta_maker_sdk::ws::{managed::*, types::*};
use std::sync::Arc;
# fn run() -> Result<(), Box<dyn std::error::Error>> {
let signer = BytesSigner::from_secret([1u8; 32]);
let signer_for_auth = signer.clone();

let config = ManagedWsConfig::new(
    "wss://devnet-api.acta.markets/maker",
    HelloData {
        protocol_version: WS_PROTOCOL_VERSION.to_string(),
        features: vec![],
        client_name: Some("my-bot".to_string()),
        client_version: Some("0.1.0".to_string()),
    },
    signer.pubkey_base58(),
    Arc::new(move |challenge: &str| {
        Ok(signer_for_auth.sign_message_base58(challenge.as_bytes()))
    }),
);

let handle = spawn_managed_ws(config)?;
// handle.send(), handle.subscribe_messages(), handle.send_await()
# let _ = handle;
# Ok(())
# }

ManagedWsConfig defaults to the maker quote endpoint (/maker). Keep latency-sensitive commands there: Quote, BatchQuotes, ReplaceQuote, CancelQuote, CancelAllQuotes, and subscriptions. For private maker reads and recovery queries, open a separate data-plane connection:

use acta_maker_sdk::*;
use acta_maker_sdk::ws::{managed::*, types::*};
use std::sync::Arc;
# fn run() {
# let signer = BytesSigner::from_secret([1u8; 32]);
# let signer_for_auth = signer.clone();
# let hello = HelloData {
#     protocol_version: WS_PROTOCOL_VERSION.to_string(),
#     features: vec![],
#     client_name: None,
#     client_version: None,
# };
# let challenge_signer = Arc::new(move |challenge: &str| {
#     Ok(signer_for_auth.sign_message_base58(challenge.as_bytes()))
# });
let data_config = ManagedWsConfig::new(
    "wss://devnet-api.acta.markets",
    hello,
    signer.pubkey_base58(),
    challenge_signer,
)
.with_endpoint(MakerWsEndpoint::Data);
# let _ = data_config;
# }

The data endpoint normalizes to /maker/data and is the preferred place for GetActiveRfqs, GetMyQuotes, GetMakerPositions, GetMarketsForMaker, GetMyTrades, and GetMmSummary. Automatic reconcile reads are enabled on the data endpoint and disabled on the quote endpoint by default.

send() confirms that the frame was written to the current WebSocket. It is not a server ACK. Use send_await() when the command has a correlated server response. Commands submitted while reconnecting fail with Disconnected; the SDK never replays a quote behind the strategy's back. try_send() returns a SendTicket, whose wait() method reports the eventual socket-write result.

Each inbound message carries a connection epoch and a connection-local sequence number. ManagedMessageReceiver::recv() returns Gap if its bounded broadcast buffer overflows. Treat that as a reason to refresh state through the data connection.

Quick start (WsClient)

use acta_maker_sdk::*;
use acta_maker_sdk::ws::types::*;
# async fn run() -> Result<(), Box<dyn std::error::Error>> {
# let signer = BytesSigner::from_secret([1u8; 32]);
let mut client = WsClient::connect("wss://devnet-api.acta.markets/maker").await?;

client.send_hello(HelloData {
    protocol_version: WS_PROTOCOL_VERSION.to_string(),
    features: vec!["quote_expired".to_string()],
    client_name: Some("maker-bot".to_string()),
    client_version: Some("0.1.0".to_string()),
}).await?;

while let Some(msg) = client.next().await {
    match msg? {
        ServerMessage::AuthRequest(data) => {
            client.auth_challenge(AuthChallengeData {
                challenge: data.challenge.clone(),
                signature: signer.sign_message_base58(data.challenge.as_bytes()),
                pubkey: signer.pubkey_base58(),
            }).await?;
        }
        ServerMessage::AuthSuccess(data) => {
            println!("authenticated: {}", data.session_id);
        }
        other => println!("server: {other:?}"),
    }
}
# Ok(()) }

WsClient enables TCP_NODELAY and applies bounded frame, message, and write buffers. For a message that will be sent more than once, build a PreparedClientMessage and call send_prepared() to reuse its serialized bytes.

Order signing

use acta_maker_sdk::*;
# fn run() {
# let signer = BytesSigner::from_secret([1u8; 32]);
// Field values come from the `RfqBroadcast` you are quoting.
let args = OrderPreimageArgs {
    chain_id: 0,
    program_id: [0u8; 32],
    is_taker_buy: false,
    position_type: PositionType::CoveredCall,
    market: [0u8; 32],
    strike: 100_000_000_000,
    quantity: 1,
    gross_price: 2_000_000_000,
    valid_until: 1_725_000_000,
    maker: signer.pubkey_bytes(),
    taker: [0u8; 32],
    nonce: 42,
};
let order_id = compute_order_id(&args);
let signature = sign_order_id_with_signer(&order_id, &signer);
# let _ = signature;
# }

BytesSigner wraps an Ed25519 keypair with Zeroize on drop. SignerLike is the synchronous hot-path interface for local keys. Use AsyncSignerLike when signing may wait for an external process or device. The SDK verifies every returned signature against the requested message and pubkey_bytes. ManagedWsConfig::auth_timeout bounds the complete authentication flow, including signing; timed-out signer futures are dropped and must therefore be cancellation-safe.

Funding positions with native SOL

With the chain-rpc feature, FundPositionArgs can wrap native SOL when the position settles in wSOL. Wrapping is opt-in:

# #[cfg(feature = "chain-rpc")]
# {
use acta_maker_sdk::chain::{FundPositionArgs, NativeSolFunding};
# use solana_sdk::pubkey::Pubkey;
# fn args(maker_owner: Pubkey, position_pda: Pubkey) {
let fund = FundPositionArgs {
    maker_owner,
    position_pda,
    create_atas: true,
    native_sol: NativeSolFunding::with_default_reserve(),
};
# let _ = fund;
# }
# }

The SDK validates the position, market, and existing wSOL account. When the maker is also the fee payer, the final pre-sign check includes the wrap amount, rent for missing token accounts created by funding, the RPC fee for the assembled transaction, and the configured reserve. With an external fee payer, the maker budget includes only the wrap and reserve. This is a snapshot guarantee, not an on-chain lock: another concurrent wallet transaction can still spend SOL after the check. RPC and account-validation errors fail closed; they are not treated as a zero token balance. Use NativeSolFunding::Disabled when wrapping is handled by the caller.

Value sets (string enums)

These are String on the wire; the SDK provides enums for known value sets:

  • PositionType: "covered_call" | "cash_secured_put"
  • QuoteStatus: "pending" | "best" | "outbid" | "filled" | "expired"
  • QuoteCancelReason: "requested" | "risk_check" | "rfq_accepted"
  • RfqAvailableAgainReason: "signature_timeout" | "tx_failed" | "tx_build_failed"
  • RfqCloseReason: "expired" | "taker_cancelled" | "filled" | "market_expired" | "ladder_timeout"
  • QuoteFinalStatus: "expired" | "outbid" | "cancelled" | "filled"
  • PositionUpdateType: "created" | "funded" | "liquidated" | "settled"

Known order and position statuses are represented by SDK enums. Truly open-ended backend fields, such as diagnostic reason text, remain strings.

Examples

Examples require the ws-client feature:

cargo run --example hello_auth --features ws-client

Subscribe to RFQs + chain events

use acta_maker_sdk::*;
use acta_maker_sdk::ws::types::*;
# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut client = WsClient::connect("wss://devnet-api.acta.markets/maker").await?;
client
    .subscribe(SubscribeData {
        request_id: Default::default(),
        channels: vec![WsChannel::Rfqs, WsChannel::ChainEvents],
        underlying_mints: None,
        quote_mints: None,
    })
    .await?;
# Ok(()) }

Send a quote

use acta_maker_sdk::*;
use acta_maker_sdk::ws::types::*;
use uuid::Uuid;
# async fn run() -> Result<(), Box<dyn std::error::Error>> {
# let mut client = WsClient::connect("wss://devnet-api.acta.markets/maker").await?;
client
    .quote(QuoteMessage {
        rfq_id: Uuid::new_v4(),
        strike: Strike::new(100_000_000_000),
        price: Price::new(2_000_000_000),
        valid_until: std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_725_000_000),
        nonce: Nonce::new(42),
        order_id: OrderId::from_hex(
            "0000000000000000000000000000000000000000000000000000000000000001",
        )
        .unwrap(),
        signature: "base58-signature".to_string(),
    })
    .await?;
# Ok(()) }

Discovery APIs

use acta_maker_sdk::*;
use acta_maker_sdk::ws::{managed::normalize_maker_data_ws_url, types::*};
# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let url = normalize_maker_data_ws_url("wss://devnet-api.acta.markets");
let mut client = WsClient::connect(&url).await?;

client
    .get_my_quotes(GetMyQuotesMessage {
        request_id: Default::default(),
        active_only: true,
        limit: None,
    })
    .await?;

client
    .get_maker_positions(GetMakerPositionsMessage {
        request_id: Default::default(),
        market: None,
        underlying_mint: None,
        status: Some(vec!["open".to_string(), "funded".to_string()]),
        min_expiry_ts: None,
        limit: None,
    })
    .await?;
# Ok(()) }

Cancel quote(s)

use acta_maker_sdk::*;
use uuid::Uuid;
# async fn run() -> Result<(), Box<dyn std::error::Error>> {
# let mut client = WsClient::connect("wss://devnet-api.acta.markets/maker").await?;
client
    .cancel_quote(Uuid::new_v4())
    .await?;
client
    .cancel_all_quotes(Some("market_pda_base58".to_string()))
    .await?;
# Ok(()) }

Documentation

Full documentation is available at docs.acta.markets:

  • Rust SDK integration guide
  • Wire examples (JSON and Rust)
  • Maker API reference (all WS messages)
  • Sandbox / devnet setup
  • FAQ and troubleshooting

License

MIT