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
[]
= { = "0.8.0", = ["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:
ACTA_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 *;
use ;
use ;
# async
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