acta-maker-sdk 0.5.5

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

acta-maker-sdk

Rust SDK for Acta's Solana options RFQ venue. It supports makers that receive RFQs and send signed quotes, plus vault operators that quote, check served transactions, and run capital operations. Features are opt-in; the default set is empty.

Install and credentials

Registration is not self-service. Acta must register the maker owner and its quote-signing key on chain. Until then the connection fails with maker_not_registered.

  • The owner holds funds and signs maker transactions such as DepositPremium, WithdrawPremium, and DepositFundsToPosition.
  • The registered quote-signing key authenticates and signs quotes. It can be the owner key.

Deposit a premium balance with DepositPremium before quoting. A fill debits its premium from that balance.

Rust 1.89 or later is required.

[dependencies]
acta-maker-sdk = { version = "0.5.4", features = ["ws-client"] }

For registration and environment setup, use the Rust maker guide.

Quick start: managed maker

From this checkout, point the examples at a 64-byte Solana keypair JSON file:

export ACTA_KEYPAIR_PATH=/absolute/path/to/quote-signing-key.json
export ACTA_MAKER_OWNER=<owner base58>
cargo run --locked --features ws-client --example hello_auth
cargo run --locked --features ws-client,http --example managed_quote

ACTA_WS_URL defaults to wss://devnet-api.acta.markets. DEVNET_WS, MAINNET_WS, DEVNET_API, and MAINNET_API name the public origins.

The quote examples fetch GET /api/v1/venue and call .venue(&venue). Without http, call .welcome(&welcome) after the session welcome. The examples send real signed quotes at a fixed 1_000_000_000 price, so use a test maker on devnet. They do not price, hedge, or fund.

The managed quote loop connects, authenticates, recovers state, subscribes to RFQs, signs a quote, and sends it in the recovered epoch:

use acta_maker_sdk::*;
use acta_maker_sdk::ws::{maker::MakerQuoteClient, managed::*, types::*};
use std::{sync::Arc, time::Duration};
# async fn run(keypair: [u8; 64], owner: String, min_validity: Duration) -> Result<(), Box<dyn std::error::Error>> {
static NONCES: AtomicNonceGenerator = AtomicNonceGenerator::new();

let signer = Arc::new(BytesSigner::from_keypair(&keypair)?);
let maker_owner = decode_base58_32(&owner)?;
let config = ManagedWsConfig::new(
    DEVNET_WS,
    HelloData {
        protocol_version: WS_PROTOCOL_VERSION.to_string(),
        features: vec![],
        client_name: Some("my-bot".to_string()),
        client_version: None,
    },
    signer.clone(),
    owner,
)
.low_latency()
.with_initial_subscribe(SubscribeData {
    request_id: uuid::Uuid::new_v4(),
    channels: vec![WsChannel::Rfqs],
    underlying_mints: None,
    quote_mints: None,
});

let client = MakerQuoteClient::spawn(config)?;
let mut messages = client.subscribe_messages();
let epoch = client.wait_until_ready().await?;

while let Ok(msg) = messages.recv().await {
    match msg.message() {
        ServerMessage::RfqBroadcast(rfq) => {
            let min_valid_until = QuoteExpiry::after(min_validity).ok_or("clock")?;
            let valid_until =
                QuoteExpiry::after(min_validity + Duration::from_secs(60)).ok_or("clock")?;
            let quote = match RfqBinding::from_broadcast(rfq)?
                .quote()
                .maker_owner(maker_owner)
                .price(Price::new(1_000_000_000))
                .valid_until(valid_until)
                .min_valid_until(min_valid_until)
                .nonce(Nonce::new(NONCES.next_u64()?))
                .sign(signer.as_ref())
            {
                Ok(quote) => quote,
                Err(QuoteBuildError::ValidUntilAfterMarketExpiry { .. }) => continue,
                Err(error) => return Err(error.into()),
            };
            client.quote_in_epoch(quote, epoch).await?;
        }
        ServerMessage::QuoteAcknowledged(ack) => println!("accepted {:?}", ack.order_id),
        ServerMessage::QuoteOutbid(outbid) => println!("outbid on {}", outbid.rfq_id),
        ServerMessage::QuoteRejected(rejected) => println!("rejected {:?}", rejected.reason),
        ServerMessage::QuoteFilled(fill) => {
            println!("filled on {}, position {}", fill.market_pda, fill.position_pda)
        }
        ServerMessage::RfqClosed(closed) => println!("closed {:?}", closed.reason),
        _ => {}
    }
}
# Ok(())
# }

This sketch quotes only in its first epoch. managed_quote waits for each new epoch and applies recovery before it quotes again. A socket write is not an accepted quote; never replay an old signed quote after a reconnect.

QuoteFilled means the position opened on chain. Fund its settlement leg with DepositFundsToPosition (or fund_position) if required. Acta's keeper settles an in-the-money funded position to the owner's ATA; no SDK settlement call is needed.

Features and examples

Feature Enables
ws-client MakerQuoteClient, MakerDataClient, ManagedWsHandle, and raw Tokio WsClient
vault VaultOperatorClient, checked vault transaction signing, and projection feed; includes ws-client and chain
chain Builders, derived addresses, checked account decoders, VaultManager, price routes, and served-transaction verification
chain-rpc ChainClient over Solana RPC; extends chain
http Read-only HttpClient using reqwest 0.13 and rustls with the platform verifier
decimal Exact rust_decimal views for Price, Strike, and Quantity
test-helpers ManagedWsHandle::test_handle and message injection
Example Features Purpose
hello_auth ws-client Authenticate, print success, close
managed_quote ws-client, http Recover state and send epoch-bound quotes
quote ws-client, http Raw /maker handshake and signed quote
discovery ws-client Read quotes, positions, markets, and summary on /maker/data
indicative ws-client Answer unsigned indicative-price requests
loopback_latency ws-client Local managed-client latency sample
vault_operator vault, http Vault session, checked fill and swap signing
vault_capital chain-rpc Capital-cycle crank
curator_pt chain-rpc, http PT route planning and simulation

Maker examples read ACTA_KEYPAIR_PATH, ACTA_MAKER_OWNER, and ACTA_WS_URL. vault_operator also needs ACTA_VAULT_PDA. vault_capital reads ACTA_KEYPAIR_PATH, ACTA_VAULT_PDA, and ACTA_RPC_URL (devnet by default).

ACTA_VAULT_PDA=<vault> cargo run --locked --features vault,http --example vault_operator
ACTA_VAULT_PDA=<vault> ACTA_RPC_URL=<rpc> \
  cargo run --locked --features chain-rpc --example vault_capital

Maker sessions and recovery

Use MakerQuoteClient on /maker for quotes, replacements, cancellations, and subscriptions. Use MakerDataClient on /maker/data for private reads. The data endpoint supplies GetActiveRfqs, GetMyQuotes, GetOrderStatus, GetMakerPositions, GetMarketsForMaker, GetMyTrades, and GetMmSummary.

MakerDataClient also receives the owner's PositionUpdated, TradeExecuted, and MakerCapsUpdated pushes after summary recovery. No subscription is needed for those pushes. all_maker_positions(filter, timeout) pages GetMakerPositions when MmSummary.positions_has_more is set.

use acta_maker_sdk::*;
use acta_maker_sdk::ws::{managed::*, maker::MakerDataClient, types::*};
use std::sync::Arc;
# async fn run(hello: HelloData) -> Result<(), Box<dyn std::error::Error>> {
# let signer = Arc::new(BytesSigner::from_secret([1u8; 32]));
# let owner = signer.pubkey_base58();
let config = ManagedWsConfig::new("wss://devnet-api.acta.markets", hello, signer, owner)
    .with_endpoint(MakerWsEndpoint::Data);
let data = MakerDataClient::spawn(config)?;
data.wait_until_ready().await?;
let summary = data
    .request(
        GetMmSummaryMessage { request_id: uuid::Uuid::new_v4() },
        std::time::Duration::from_secs(2),
    )
    .await?;
# let _ = summary;
# Ok(())
# }

Keep a data session for monitoring, funding, or hot standby. On restart, use maker_open_positions(owner) to recover open and funded positions from chain. Open /maker only when taking over: a second quote session replaces the first and may cancel its quotes if cancel-on-disconnect is enabled. Data pushes are not replayed, so reconcile after a gap or reconnect. Up to four data sessions may share one owner; a fifth gets session_limit_exceeded.

Ready means required SDK recovery reads arrived. It does not mean a strategy applied them, and recovery is not one atomic snapshot. Apply recovery and lifecycle messages before trading. Commands are tied to the current epoch; quote_in_epoch, replace_in_epoch, and batch_quotes_in_epoch return NotReady once the session advances.

Subscriptions survive resume and fresh authentication. The first local receiver gets a bounded since-spawn backlog, so subscribe before waiting for traffic. See the maker API and WS conventions for delivery, close codes, subscriptions, rate limits, and error variants.

Queries, sends, and cancellation

GetMyQuotes with Live returns the complete current owner set without pagination. History is persisted and paged. GetOrderStatus returns Pending, Confirmed with a position address, or Unknown. An empty Live set, timeout, or Unknown does not prove an interrupted order has no exposure.

send confirms a socket write. send_await waits for a correlated response, and try_send returns a SendTicket whose drop before dequeue cancels that send. Quote, replacement, batch, and cancellation messages share a FIFO lane; a full lane returns QueueFull. CancelQuoteAck and CancelAllQuotesAck name the IDs actually cancelled; a QuoteCancelled lifecycle event is not a cancellation receipt.

Quote outcomes

Subscribe before sending. Create QuoteOutcomeTracker::new(rfq_id, order_id, epoch, deadline). Pass each ManagedInbound to observe, and call check with the current epoch and monotonic time on transitions and at the deadline.

outcome reports Pending, Acked, Selected, terminal rejection, fill, expiry, cancellation, loss, or Unknown. A selected order can remain unresolved after RfqClosed until matching status proves a loss. A stream gap, closure, epoch change, or deadline makes an unresolved order Unknown, never a proven failure. Reconcile it through GetMyQuotes or GetOrderStatus; do not reuse its order ID.

Quote construction and numeric units

RfqBinding::from_broadcast validates the market PDA, mints, expiry, and option type under ACTA_PROGRAM_ID before signing. For another deployment, use from_broadcast_for_program with a locally configured program ID. Never let an RFQ choose the program ID.

QuoteBuilder requires maker_owner, price, validity bounds, and nonce. Read the minimum validity and settlement buffer from HttpClient::venue or WelcomeData; the builder rejects an impossible local validity window. Quote expiry is whole Unix seconds. The full signing, validation, and replacement rules are in the Rust maker guide and maker API.

Price and Strike use the 1e9 PRICE_SCALE per underlying token, independent of mint decimals. Quantity is underlying atoms. Without decimal, use Price::as_decimal, Strike::as_decimal, Quantity::to_units, and total_premium_quote_atoms. With decimal, use Price::to_decimal, Strike::to_decimal, Price::from_decimal_floor, Quantity::to_decimal, and Quantity::from_decimal_floor. The floor conversions return None for negative or out-of-range values.

Chain helpers and funding batches

The chain feature provides offline builders and checked account decoders. With chain-rpc, maker_snapshot reads requested positions, premium ATAs, and unique markets in batches of at most 100 keys. Missing positions and a missing premium ATA are observations, not proof of closure or a failed RPC read.

Use maker_open_positions(owner) at startup or manual recovery, not in a tick loop. PositionSnapshot::fund_instructions and required_settlement are pure. For native settlement legs, prepend wsol::build_wrap_sol_ixs and aggregate the deficit across the batch; budget the wallet balance, rent, and fees. NativeSolFunding::with_default_reserve retains the single-position convenience.

deposit_premium, withdraw_premium, fund_position, and send_instructions_with_options succeed only after confirmation at the client's commitment. send_instructions_with_blockhash is submit-only and accepts one shared blockhash for a batch.

With chain-rpc plus ws-client, call confirm_signatures once for the batch. It uses signatureSubscribe, then one chunked status read for unresolved signatures on a deadline or WebSocket failure. signature_statuses is available with chain-rpc alone. ConfirmationStatus::Pending is unknown, not a reason to send a new payment.

Capital-cycle reads

For a vault crank, ChainClient::unfunded_positions reads held positions that still need their settlement leg. pending_capital_requests reads queued deposits and withdrawals from program accounts. VaultManager builds the capital-cycle instructions, and fund_maker_position moves the required leg into a position the vault holds. These are chain observations and builders; select a cadence and authority policy in the operator.

Vault operators

VaultOperatorClient authenticates an operator for one vault. The connection grant is current-connection state; it disappears on disconnect or invalidation. Check permissions and Ready before trading. The vault operator guide owns the role, lifecycle, funding, and protocol detail.

verify_and_sign_sponsored_tx and verify_and_sign_swap rebuild the transaction from the operator's intent and sign only an exact match. Supply the vault account read before the request, bounded compute budget, lookup tables, trade terms or PairPriceRoutes, accepted staging routes, oracle shards, and an independent minimum-output floor. verify_vault_open and verify_vault_swap expose the same checks outside the client.

Checked signing verifies the fee payer, required signers, program IDs, account privileges, instruction data, ATA setup, quote signature, price section, and allowed swap hops. It checks deadlines but not the blockhash. A received ACK only proves enqueueing; use swap_status or SwapSettled and refresh the vault for execution state.

VaultProjectionAdvanced is a versioned projection cursor, not an execution result. Ignore duplicates and older versions, then reload HTTP vault detail on a newer version or a gap. VaultProjection::follows checks adjacency.

For a staged swap, request the same swap again only after the stage confirms. The unstage must spend the whole staged amount at the open generation. A recovery stage has its own VaultSwapKind::RecoveryStage shape; the checked signer rebuilds it before signing.

Price routes, venues, and staging

PriceRoute is the on-chain valuation source for a mint. For a served swap, PairPriceRoutes supplies the vault's main and second routes, accepted staging routes, and permitted push-oracle shards. The deployment's venue registry controls the allowed hop programs. Read the detailed route, brick, and checked-signing requirements in the vault operator guide.

For direct PT routing with chain-rpc and http, call curator::Config::discover once, then use Curator::plan and curator::execute. Re-discover after a route dependency or second-leg roll. A staged plan must refresh the slot and re-quote before its unstage; Plan::Staged::unstage is a fit preview, not a transaction to cache and send.

The curator_pt example simulates only; it never broadcasts a mainnet transaction:

RPC_URL=... JUPITER_API_KEY=... CURATOR_KEYPAIR=/path/to/keypair.json \
VAULT=... AMOUNT_ATOMS=1000000 \
cargo +stable run --example curator_pt --features chain-rpc,http -- buy

Use sell for PT input before maturity or redeem after maturity. A staged simulation covers only the first half; the second needs confirmed staging state.

HTTP, wire, and upgrades

HttpClient reads venue limits, tokens, markets, vaults, and vault positions. Venue, token, and market responses use ETag revalidation; vault reads are not cached. Read current limits from the service rather than hardcoding them.

Wire enum values are extensible. Match error codes, not messages, and include fallback arms for non_exhaustive enums. *_unix_ms fields are milliseconds; signed quote expiry is Unix seconds. Chain events are identified by (signature, instruction_index).

Use WS_PROTOCOL_VERSION instead of hardcoding the protocol version.

Version 0.5.4 adds WelcomeData validity-window fields, the decimal feature, maker-data owner pushes, QuoteOutcomeTracker, batch confirmation, PriceRoute swaps, and staged recovery. See CHANGELOG.md for exact 0.5.4 and earlier migration notes, especially explicit maker owners and epoch-bound quote methods.

Latency sample

Release loopback sample with low_latency on one Apple silicon machine:

Leg p50 p90 p99
try_send to peer frame 20 µs 27 µs 60 µs
peer send to session parse 150 µs 281 µs 424 µs
session task to subscriber recv 19 µs 39 µs 70 µs

This measures SDK overhead only; network and server time are additional.

Documentation

License

MIT