Expand description
§acta-maker-sdk
Rust SDK for Acta’s Solana options RFQ venue. Makers use it to receive RFQs and send signed quotes. Vault operators use it to quote for a vault, check served transactions before signing, and run capital operations.
§Install
[dependencies]
acta-maker-sdk = { version = "0.8.0", features = ["ws-client"] }Rust 1.89 or later. The default feature set is empty.
| Feature | Enables |
|---|---|
ws-client | MakerQuoteClient, MakerDataClient, ManagedWsHandle and the raw Tokio WsClient |
http | Read-only HttpClient (reqwest 0.13 with rustls) |
chain | Instruction builders, derived addresses, checked account decoders, VaultManager, served-transaction verification |
chain-rpc | ChainClient over Solana RPC; includes chain |
vault | VaultOperatorClient and checked vault transaction signing; includes ws-client and chain |
decimal | rust_decimal conversions for Price, Strike and Quantity |
test-helpers | ManagedWsHandle::test_handle and message injection |
This release speaks WS protocol 1.1.0 (WS_PROTOCOL_VERSION). See
CHANGELOG.md for what changed.
§Before you connect
Maker registration is not self-service. Acta registers the maker owner and its
quote-signing key on chain; until then authentication fails with
maker_not_registered.
- The owner holds funds and signs maker transactions such as
MakerDepositandDepositFundsToPosition. - The quote-signing key authenticates the session and signs quotes. It can be the owner key.
- A fill debits its premium from the owner’s premium balance, so deposit with
MakerDepositbefore quoting.
The Rust maker guide covers registration and environment setup.
§Quick start
The examples read 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_quoteACTA_WS_URL defaults to DEVNET_WS. The quote examples send real signed
quotes at a fixed price of 1_000_000_000, so use a test maker on devnet. They do
not price, hedge or fund.
A managed quote loop connects, authenticates, recovers state, subscribes to RFQs and quotes in the recovered epoch:
use acta_maker_sdk::*;
use acta_maker_sdk::ws::{maker::MakerQuoteClient, managed::*, types::*};
use std::{sync::Arc, time::Duration};
static NONCES: AtomicNonceGenerator = AtomicNonceGenerator::new();
let signer = Arc::new(BytesSigner::from_keypair(&keypair)?);
let maker_owner = Address32::from_base58(&owner)?;
let config = ManagedWsConfig::new(
DEVNET_WS,
HelloData {
protocol_version: WS_PROTOCOL_VERSION.to_string(),
on_disconnect: OnDisconnect::CancelQuotes,
client_name: "my-bot".to_string(),
client_version: env!("CARGO_PKG_VERSION").to_string(),
},
signer.clone(),
maker_owner,
)
.low_latency()
.with_initial_subscribe(SubscribeData {
products: ProductSet::ALL,
request_id: uuid::Uuid::new_v4(),
channels: vec![WsChannel::Rfqs],
underlying_mints: Vec::new(),
quote_mints: Vec::new(),
});
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),
_ => {}
}
}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 if the product requires it. Acta’s keeper settles an
in-the-money funded position to the owner’s ATA; no SDK call is needed.
§Examples
| Example | Features | Purpose |
|---|---|---|
hello_auth | ws-client | Authenticate and close |
managed_quote | ws-client, http | Recover state and send epoch-bound quotes |
taker_buy_quote | ws-client, http, chain-rpc | Write taker buys from Maker PDA custody and keep it funded |
quote | ws-client, http | Raw /maker handshake and one signed quote |
spread_quote | ws-client, http | Price and sign call spreads, put spreads and cash puts |
discovery | ws-client | Read quotes, positions, markets and summary on /maker/data |
indicative | ws-client | Answer unsigned indicative-price requests |
loopback_latency | ws-client | Measure local managed-client latency |
vault_operator | vault, http | Vault session quoting both sides, with checked fill and swap signing |
vault_capital | chain-rpc | Capital-cycle crank |
curator_pt | chain-rpc, http | Plan a PT route and simulate it; never sends |
Maker examples read ACTA_KEYPAIR_PATH, ACTA_MAKER_OWNER and ACTA_WS_URL.
taker_buy_quote also reads ACTA_OWNER_KEYPAIR_PATH, ACTA_CUSTODY_MINT,
ACTA_CUSTODY_TOKEN_PROGRAM, ACTA_CUSTODY_TARGET (atoms) and ACTA_RPC_URL.
vault_operator also reads ACTA_VAULT_PDA. vault_capital reads
ACTA_KEYPAIR_PATH, ACTA_VAULT_PDA and ACTA_RPC_URL (devnet by default).
curator_pt reads RPC_URL, JUPITER_API_KEY, CURATOR_KEYPAIR, VAULT and
AMOUNT_ATOMS, and takes buy, sell or redeem as its argument.
§Writing taker buys
A buy RFQ (side: buy) asks you to write the option the taker holds: the taker is
the holder, you are the writer. Subscribe to WsChannel::TakerBuyRfqs; the channel
is separate from Rfqs, so a maker that only sells never receives buys.
- Custody. The collateral leaves your Maker PDA custody at open, not your
wallet: the underlying for a call, the quote mint for a put, a spread or a cash
put. The net premium lands in custody in the same transaction. Fund custody with
ChainClient::maker_deposit(MakerDeposit) from your wallet ATA; a wSOL underlying must already be wrapped in that ATA. For native SOL,chain::build_maker_deposit_native_sol_ixswraps and deposits in one go andchain::build_maker_withdraw_native_sol_ixswithdraws and unwraps. - Admission. The server checks custody before it sends you the RFQ. A short
custody turns into
RfqSkippedwithmaker_insufficient_balanceand the RFQ never reaches quoting, so top up when you see the skip, after every fill, or on a timer. - Price. The taker pays your price; quote an ask. The order id signs the side, so a buy quote cannot fill a sell.
- Settlement. The holder deposits strictly before expiry to exercise a physical
buy. An unfunded in-the-money buy returns all collateral to you and settles as
not_exercised; spreads and cash puts settle in cash with no deposit.PositionInfonames the roleswriter(you) andholder(the buyer).
See examples/taker_buy_quote.rs.
§Rules that matter
- Epochs.
Readymeans the SDK’s recovery reads arrived, not that your strategy applied them.quote_in_epoch,replace_in_epochandbatch_quotes_in_epochreturnNotReadyonce the session moves to a new epoch. - Subscribe first. The first receiver gets a bounded backlog since spawn; later receivers start at subscription time.
- Unknown is not failure. A timeout, gap, reconnect or
Unknownorder status does not prove an order has no exposure. Reconcile withGetMyQuotesorGetOrderStatusand never reuse its order ID. - Program ID.
RfqBinding::from_broadcastchecks the market againstACTA_PROGRAM_ID. For another deployment usefrom_broadcast_for_programwith a locally configured ID; never take it from the RFQ. - Units.
PriceandStrikeuse the 1e9PRICE_SCALEper underlying token.Quantityis underlying atoms. Signed quote expiry is whole Unix seconds;*_unix_msfields are milliseconds. - Closed wire. Every field is present on every frame and absent facts are
null. Decoding rejects unknown message types, enum values, error codes and missing fields. A rejected frame afterReadykeeps the session up and reaches you asManagedWsEvent::UnreadableFrameand a receiver gap: reconcile as after any gap. Match error codes, not messages. - Side. Every RFQ, quote, position and trade carries
terms: OptionTermsandside. Makers subscribe toWsChannel::TakerBuyRfqsto receivebuyRFQs, physical or spread, and quote them from custody. A vault sees both sides on itsvaultschannel: it bids on a sell and holds, and withSIGN_WRITER_QUOTESit asks on a buy and writes from its collateral leg. The buyer’s wallet signs that open (VaultOpenIntent::Buy); the vault signs only the quote. A vault never requests a buy as taker (taker_buy_unsupported).
§Where to start in the API
| Task | Start with |
|---|---|
| Quote, replace, cancel | MakerQuoteClient, RfqBinding, QuoteBuilder |
| Private reads and owner pushes | MakerDataClient (/maker/data), all_pages |
| Track one quote to a result | ws::maker::QuoteOutcomeTracker |
| Spreads and cash puts | OptionTerms, spreads::SpreadTerms, build_spread_order_preimage |
| Premium and fee split | physical_premium, SpreadTerms::premium |
| Recover positions from chain | ChainClient::maker_open_positions, maker_snapshot |
| Fund and confirm | PositionSnapshot::fund_instructions, confirm_signatures |
| Vault owner actions | chain::vault::VaultManager, ChainClient::vault_manager |
| Vault operator session | VaultOperatorClient, verify_and_sign_sponsored_tx, verify_and_sign_swap |
| Buy from a vault (wallet side) | VaultManager::open_position_for_wallet_holder, verify_vault_open with VaultOpenIntent::Buy |
| Decode position events | chain::events::decode_position_event (event version 2) |
| Vault capital cycle | ChainClient::unfunded_positions, pending_capital_requests |
| PT routing | chain::vault::curator |
| Venue limits, tokens, markets, vaults | HttpClient |
Read venue limits such as minimum quote validity from HttpClient::venue or
WelcomeData rather than hardcoding them.
§Latency sample
Release build, loopback peer, low_latency, one Apple silicon machine. This is
SDK overhead only; network and server time are extra.
| 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 |
§List paging
List calls collect every page for you. MakerDataClient::request_all and the
HttpClient catalog methods go through paging::fetch_all. For a “load more” view
use request_page and keep the returned cursor as an opaque value.
On the wire, WS lists take limit (at most 200) and Cursor::Start {} or
Cursor::After { id }, and return next_cursor, null on the last page. REST lists
take an optional limit (default 50, at most 200, 100 for vault requests) and an
optional cursor: omit it for the first page, then pass next_cursor as is.
§Documentation
- Rust maker guide
- Maker API reference
- WebSocket conventions
- HTTP API
- Vault API, SDKs and swaps
- API reference on docs.rs
§License
MIT
Re-exports§
pub use error::ActaSdkError;pub use network::DEVNET_API;pub use network::DEVNET_WS;pub use network::MAINNET_API;pub use network::MAINNET_WS;pub use quote::QuoteBuildError;pub use quote::QuoteBuilder;pub use quote::RfqBinding;pub use types::PRICE_SCALE;pub use types::QuoteExpiry;pub use types::ids::Balance;pub use types::ids::ChainId;pub use types::ids::ChainPositionType;pub use types::ids::ChainPositionTypeParseError;pub use types::ids::ChainTerms;pub use types::ids::Decimals;pub use types::ids::DurationSeconds;pub use types::ids::MarketId;pub use types::ids::Nonce;pub use types::ids::OptionTerms;pub use types::ids::OrderId;pub use types::ids::OrderVersion;pub use types::ids::PositionRevision;pub use types::ids::PositionType;pub use types::ids::Price;pub use types::ids::Quantity;pub use types::ids::QuoteCount;pub use types::ids::RfqVersion;pub use types::ids::Slot;pub use types::ids::Strike;pub use types::ids::TakerSide;pub use types::ids::TimeoutSeconds;pub use types::ids::TradeCount;pub use types::ids::UserId;pub use types::ids::Volume;pub use types::invite::REFERRAL_CODE_MAX_LEN;pub use types::invite::REFERRAL_CODE_MIN_LEN;pub use types::invite::RESERVED_CODES;pub use types::invite::ReferralCode;pub use types::invite::ReferralCodeFormatError;pub use types::invite::TakerStatus;pub use types::invite::is_reserved;pub use types::vault::VaultDelegatePermissions;pub use types::vault::VaultLeg;pub use types::vault::VaultOperatorKind;pub use types::vault::VaultStagingStep;pub use types::vault::VaultSwapDirection;pub use types::vault::VaultSwapMode;pub use types::Address32;pub use types::AddressError;pub use types::PhysicalPremium;pub use ws::client::PreparedClientMessage;pub use ws::client::ServerClose;pub use ws::client::WsClient;pub use ws::client::WsTransportConfig;pub use ws::error::WsClientError;pub use ws::error::WsResult;pub use ws::error::WsTransportConfigError;pub use ws::maker::MakerClientError;pub use ws::maker::MakerDataClient;pub use ws::maker::MakerDataRequest;pub use ws::maker::MakerDataRequestError;pub use ws::maker::MakerQuoteClient;pub use ws::maker::MakerResponse;pub use ws::maker::PagedRequest;pub use ws::managed::GapPolicy;pub use ws::managed::MakerWsEndpoint;pub use ws::managed::ManagedInbound;pub use ws::managed::ManagedMessageReceiver;pub use ws::managed::ManagedReceiveError;pub use ws::managed::ManagedWsConfig;pub use ws::managed::ManagedWsConfigError;pub use ws::managed::ManagedWsError;pub use ws::managed::ManagedWsEvent;pub use ws::managed::ManagedWsHandle;pub use ws::managed::ManagedWsState;pub use ws::managed::ManagedWsTerminationReason;pub use ws::managed::OutboundMessageError;pub use ws::managed::SendAwaitError;pub use ws::managed::SendTicket;pub use ws::managed::WaitUntilReadyError;pub use ws::managed::normalize_maker_data_ws_url;pub use ws::managed::normalize_maker_ws_url;pub use ws::managed::normalize_maker_ws_url_for_endpoint;pub use ws::managed::spawn_managed_ws;pub use ws::market_decimals::MarketDecimals;pub use ws::market_decimals::TokenDecimals;pub use ws::types::ClientMessage;pub use ws::types::ServerMessage;pub use ws::vault::FleetListing;pub use ws::vault::VaultFleet;pub use ws::vault::VaultFleetConfig;pub use ws::vault::SaleDesk;pub use ws::vault::SaleError;pub use ws::vault::SaleOutcome;pub use ws::vault::SaleStage;pub use ws::vault::SignedSale;pub use ws::vault::VaultSale;pub use ws::vault::SendGate;pub use ws::vault::Ungated;pub use ws::vault::VaultClientError;pub use ws::vault::VaultOperatorClient;pub use ws::vault::VaultOperatorConfig;pub use ws::vault::VaultProjection;pub use ws::vault::VaultProjectionReceiver;pub use ws::vault::VaultRequestError;pub use ws::vault::VaultTxError;pub use nonce::AtomicNonceGenerator;pub use nonce::NonceError;pub use nonce::NonceGenerator;pub use orders::BytesSigner;pub use orders::ORDER_DOMAIN_TAG;pub use orders::ORDER_ID_LEN;pub use orders::ORDER_PREIMAGE_LEN;pub use orders::OrderError;pub use orders::OrderPreimageArgs;pub use orders::SignerLike;pub use orders::build_order_preimage;pub use orders::compute_order_id;pub use orders::hash_order_preimage;pub use orders::order_id_hex;pub use orders::order_preimage_hex;pub use orders::sign_order_id_base58;pub use orders::sign_order_id_bytes;pub use orders::sign_order_id_from_base58_keypair;pub use orders::sign_order_id_with_async_signer;pub use orders::sign_order_id_with_signer;pub use orders::sign_order_id_with_signer_base58;pub use orders::verify_order_id_signature_base58;pub use orders::verify_order_id_signature_bytes;pub use signing::AsyncSignerLike;pub use signing::SigningError;pub use signing::SigningFuture;pub use wire::WireError;pub use wire::decode_base58_32;pub use wire::decode_base58_64;pub use wire::decode_hex_32;pub use wire::encode_base58;pub use wire::encode_hex;pub use types::InvalidProductSet;pub use types::ProductSet;
Modules§
- chain
- error
- http
- Read-only client for the public Acta HTTP API.
- io_
observer - Optional process-local observations at transport boundaries.
- network
- Public Acta API origins. Append the WebSocket path (
/maker,/maker/data,/vault) or pass the origin tonormalize_maker_ws_url. - nonce
- orders
- paging
- Shared continuation handling for HTTP and WebSocket lists.
- quote
- Quote assembly:
RfqBindingdecodes identities and resolves offered terms;QuoteBuilderderives the signed preimage andQuoteMessagefrom the same terms, price, expiry and nonce. - signing
- spreads
- Cash spreads use quote atoms for collateral and payoff; prices use 1e9.
- types
- Domain values shared by WebSocket and chain APIs.
- wire
- ws
Constants§
- ACTA_
PROGRAM_ ID - The same on devnet and mainnet;
/api/v1/venueprogram_idis authoritative. - WS_
PROTOCOL_ VERSION