acta-maker-sdk 0.4.0

Rust SDK for Acta options market makers — WebSocket client, order signing, message types
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
# acta-maker-sdk

Rust SDK for quoting Acta options: managed WebSocket connections, RFQ-bound
order signing, typed protocol messages, and Solana funding instructions.

| API | Use it for |
| --- | --- |
| `MakerQuoteClient` | Quotes, replacements and cancellations on `/maker` |
| `MakerDataClient` | Private reads and execution lookups on `/maker/data` |
| `ManagedWsHandle` | Correlated requests and explicit connection epochs |
| `WsClient` | Raw transport with application-managed authentication and recovery |

## Install

Requires Rust 1.89 or later. Default features are empty.

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

| Feature | What it enables |
|---|---|
| `ws-client` | Typed maker clients, `ManagedWsHandle`, and raw `WsClient`; 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) |

## Onboarding

Acta's admin registers the maker owner and any delegated auth or quote-signing
keys on-chain through governance. Request onboarding through the
[integration documentation](https://docs.acta.markets/quickstart/maker-rust-sdk)
before connecting. Registration is permissioned; `maker_not_registered` ends
the connection attempt without retries.

## Connect and subscribe

```rust,no_run
use acta_maker_sdk::*;
use acta_maker_sdk::ws::{managed::*, maker::MakerQuoteClient, types::*};
use std::sync::Arc;
# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let signer = Arc::new(BytesSigner::from_secret([1u8; 32]));

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,
)
.with_cancel_on_disconnect(true)
.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 connection_epoch = client.wait_until_ready().await?;
# let _ = (messages, connection_epoch);
# Ok(())
# }
```

RFQ broadcasts require the `Rfqs` server subscription. `with_initial_subscribe`
includes it in SDK readiness; `subscribe_messages()` only attaches a local
receiver. Apply recovery messages to your strategy before starting to quote,
even after `wait_until_ready()` returns.

### Separate quote and data connections

`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:

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

Use the data endpoint (`/maker/data`) for
`GetActiveRfqs`, `GetMyQuotes`, `GetOrderStatus`, `GetMakerPositions`,
`GetMarketsForMaker`, `GetMyTrades`, and `GetMmSummary`. `GetMyQuotes.scope`
is required: `Live` returns the complete current owner set without pagination;
`History` pages persisted records with `limit` and the `cursor`/`cursor_id`
pair (`created_at` plus `order_id`). Positions and trades remain paginated by
their existing keys. A Live response always has `has_more: false`.
`MakerDataClient::request()` moves the typed payload out of the received
message, so reading it is infallible; `into_payload()` takes it by value.

## Readiness and reconnect

Both endpoints advance `Connected → Authenticated → Reconciled → Ready` after
receiving `MmSummary`, `ActiveRfqs` and `GetMyQuotes { scope: Live }`.
Quote connections restore subscriptions first. The first subscriber can read a
bounded since-spawn backlog; subscribe early to avoid overflowing it.

Use `connect_timeout` and `max_reconnect_attempts` to bound connection failures;
`0` retries forever. `reconnect_jitter_ratio` spreads reconnect attempts.

After an authenticated disconnect, the managed client first resumes with the
server-issued session ID. Only `session_expired` falls back to a full challenge
authentication on the same socket; other resume errors remain failures for that
attempt. `session_replaced`, protocol-version mismatch, rejected credentials, and
an exhausted reconnect limit are terminal states, visible through
`ManagedWsState::Closed` and `ManagedWsEvent::Terminated`. The one exception is
`invalid_signature`: it retries with the normal backoff and only goes terminal
on the third rejection with no successful authentication in between, so a
one-off signer or server glitch does not stop an unattended maker.

Dropping the last `ManagedWsHandle` also stops the connection task. Call
`close().await` when you need to wait until shutdown has completed.

Typed clients reject commands before readiness with `NotReady`.
Use `client.raw()` for protocol operations not exposed by the typed API.

## Delivery and cancellations

Once a session is `Ready`, the session task reads and correlates responses while
one writer owns the socket sink. Quotes, batches, replacements and cancellations
share a FIFO lane. Ping, authentication and close use the control lane. The
backend also applies quote and cancellation requests in their connection order.
Concurrent producers are ordered by actor receipt.
A full writer lane reports `QueueFull`. `write_timeout` limits each socket write;
it does not bound the time a cancellation spends behind earlier quotes.

`send()` confirms a socket write, not a server ACK. `send_await()` waits for a
correlated response. `try_send()` returns a `SendTicket`; `wait()` reports its
socket-write result.

To await cancellation on the server, use `client.raw().send_await(ClientMessage::CancelQuote(request), timeout)`
and match `CancelQuoteAck` or `RequestError`. The ACK contains the request ID,
RFQ ID and actual cancelled order IDs. `CancelAllQuotesAck` contains the actual
aggregate. `QuoteCancelled` is a lifecycle event and never completes an awaited
cancellation. Losing the connection after a write but before an ACK leaves the
outcome unknown; the SDK does not replay trading commands. An empty
`QuoteCancelled.order_ids` removes nothing. Lifecycle RFQ/order versions and
cancellation order IDs are required fields; missing values are rejected during
decoding rather than replaced with zero or an empty set.

`CancelRfq` has no correlated success receipt. Use `send()` and process
`RfqClosed` as lifecycle state; it can also result from expiry or a fill.
`send_await(CancelRfq)` returns `NoCorrelationKey` without writing the command.
A rejected cancellation arrives as `RequestError` with its `request_id`.

### Connection epochs and application recovery

Trading commands, including those sent through the raw managed handle, require
Ready. All commands submitted from Ready, including reads, are bound to that
connection epoch and cannot be sent by a later connection. Typed data requests
check readiness and capture that epoch together before enqueueing. The low-level `WsClient` remains
application-managed.

Receive and apply recovery responses and lifecycle messages before restarting
your strategy. `Ready` confirms that the SDK received the required responses;
it does not confirm that the strategy applied them, and the reads are not an
atomic snapshot. `GetMyQuotes { scope: Live }` contains every Core-held
unfinished quote for the owner: selectable `Active { rank }`, non-selectable
`Retained` (including locked non-winners and frozen refresh quotes),
`AwaitingSignature`, and `Executing`. `History` is a separate source and must
not overwrite this live state.

Keep submitted `OrderId` intent until an exact cancellation/replacement ID, a
confirmed lifecycle fact, or a positive execution lookup resolves it.
`GetOrderStatus` is available through `MakerDataClient::request()` and the
ready quote handle's raw correlated request path; it returns `Pending`,
`Confirmed { position_pda }`, or
`Unknown`. `Unknown`, a timeout, and an empty Live result do not prove that a
maker is flat.
When application recovery is tracked separately from SDK readiness, use
`ManagedWsHandle::send_in_epoch(message, connection_epoch)` with the epoch of
the recovery snapshot the application applied. A stale or non-ready epoch is
rejected with `NotReady` before enqueueing. The queued command keeps that epoch
and cannot be sent by a later connection; disconnect/write errors still apply.

### Slow consumers

Each inbound message carries a connection epoch and a connection-local sequence.
A lagging receiver reports `Gap`. `GapPolicy::EndpointDefault` reconnects quote
sessions and only surfaces gaps on data sessions. With reconnect enabled, once
the session actor processes the signal, it stops
sending and reconnects through recovery. A frame already handed to the writer
may have reached the server. Explicit `GapPolicy::Surface` reports the gap
without reconnecting. Lag is detected when the receiver next calls `recv()`.

### Cancel-on-disconnect (COD)

Managed quote connections request `cancel_on_disconnect` by default. The server
must include it in `Welcome.enabled_features`, otherwise the SDK terminates with
`FeatureUnsupported`. To disable COD, set `with_cancel_on_disconnect(false)`
and omit `cancel_on_disconnect` from `HelloData.features`. The setter does not
remove manually supplied features.
Data connections do not request cancellation for the quote session.

COD removes the session's Active quotes and retained non-selectable quotes after
the server detects the connection loss and Core processes it.
CancelAll removes the same set, optionally filtered by market, and its ACK lists
the removed IDs. Those quotes cannot return through rollback. Selected/in-flight
orders remain governed by their lifecycle, including when their maker cancels
other strikes. Single-RFQ CancelQuote rejects a locked RFQ as a whole.
With `cancel_on_disconnect` enabled, the same cleanup applies when a `Gap`
causes a reconnect.

For an application-requested stop, `close()` uses the control lane; an ongoing
socket write can still delay it. A slowly draining quote queue does not by itself
force a disconnect. COD therefore supplies a disconnect fallback, not a bounded
cancellation time.

## Subscriptions

Dynamic subscriptions survive resume and fresh authentication, including removals and empty mint filters.
Only one subscription change may await acknowledgement at a time; another
returns `SubscriptionPending`. Trading and cancellation remain available.
The acknowledgement timeout starts when the subscription frame is written.

`initial_subscribe` seeds the desired target; its `request_id` is not sent on
the wire. Recovery generates fresh request IDs. A new handle starts with empty
desired channels/scopes when no initial target is supplied. It does not adopt
server-persisted mint filters: recovery sends both mint lists explicitly and
clears a persisted scope when its desired list is empty. Supply the intended
filters even when resuming an existing server session.

## Latency and limits

### Client profile

`ManagedWsConfig::low_latency()` sets ping every 2 s with a 2 s pong deadline, a 1 s write
timeout, 1024 queued commands, a 4096-slot inbound ring
and `GapPolicy::Reconnect`. The defaults stay conservative for tooling that
polls occasionally. The heartbeat settings detect a dead socket in about 4 s
with `low_latency()` and up to 40 s with defaults, subject to task scheduling.

### Server limits

Default limits are 32 KiB per client message, 50 quotes per batch, 50 quotes/s
with a burst of 100 per wallet, and 30 messages/s with a burst of 60 per
connection. Critical pushes (`RfqBroadcast`, ACKs, fills) retry for 250 ms twice;
if delivery still fails, the server closes the connection for recovery.

### SDK loopback measurements

Measured by `examples/loopback_latency.rs` on one machine, release build,
`low_latency()` profile, 20 000 samples per leg (Apple silicon, Tokio
multi-thread). These are local measurements, not a cancellation-latency guarantee;
network and server time come on top.

| Leg | p50 | p90 | p99 |
|-----|-----|-----|-----|
| `try_send` → frame on the peer's socket (one frame in flight) | 20 µs | 27 µs | 60 µs |
| peer send → message parsed by the session task (paced 1/ms) | 150 µs | 281 µs | 424 µs |
| session task → subscriber `recv()` | 19 µs | 39 µs | 70 µs |

The outbound leg is the command channel, lane routing, serialization and the
socket write. The inbound leg includes the peer's own wake-up after pacing, so
it overstates the SDK's share. Under a burst the same legs turn into queueing
time, which is what `QueueFull` and `Gap` are for.

## Raw WebSocket access

```rust,no_run
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.

## Quoting an RFQ

A quote signs the 32-byte order ID: SHA-256 of a fixed 182-byte preimage.
`RfqBinding` builds the preimage and `QuoteMessage` from the same strike, price,
expiry and nonce, avoiding `order_id preimage mismatch` errors.

```rust,no_run
use acta_maker_sdk::*;
use acta_maker_sdk::ws::types::RfqBroadcastMessage;
use std::time::Duration;
# fn run(rfq: &RfqBroadcastMessage) -> Result<(), Box<dyn std::error::Error>> {
# let signer = BytesSigner::from_secret([1u8; 32]);
# let nonce = 42u64;
let quote = RfqBinding::from_broadcast(rfq)?
    .quote()
    .price(Price::new(2_000_000_000))
    .valid_until(QuoteExpiry::after(Duration::from_secs(350)).ok_or("clock before epoch")?)
    .nonce(Nonce::new(nonce))
    .sign(&signer)?;
# let _ = quote;
# Ok(()) }
```

`RfqBinding::from_broadcast` decodes the RFQ's program, market and taker keys
once; reuse one binding for every quote and replacement on that RFQ.
`sign_with_async_signer` covers keys held in an HSM or a remote service.
Either way, `quote.into_replacement(old_order_id)` turns the signed quote into a
`ReplaceQuoteMessage`.

By default the builder quotes the RFQ's requested strike. `offered_strikes()`
lists every strike the RFQ will accept — its requested one plus its
`order_options` — and `.strike(...)` selects among them. A strike outside that
set is refused locally instead of after a round trip.

`valid_until` is a `QuoteExpiry` of whole Unix seconds, because that is what the
preimage signs and what the wire carries. Converting from a `SystemTime`
truncates; a sub-second fraction cannot survive to be rounded into a different
second than the one that was signed.

`Price` and `Strike` are 1e9 fixed-point per one unit of the underlying,
independent of mint decimals (`PRICE_SCALE` is that factor). `Quantity` is in
the underlying mint's atomic units. `Price::new(2_000_000_000)` quotes a gross
premium of 2 quote tokens per underlying unit; the SDK cannot detect a price
supplied in the wrong units.

### Signing the preimage yourself

`preimage_args(maker_pubkey)` returns the exact `OrderPreimageArgs` the server
will reconstruct, for inspection or for signing out of band:

```rust,no_run
use acta_maker_sdk::*;
use acta_maker_sdk::ws::types::RfqBroadcastMessage;
# fn run(rfq: &RfqBroadcastMessage) -> Result<(), Box<dyn std::error::Error>> {
# let signer = BytesSigner::from_secret([1u8; 32]);
let binding = RfqBinding::from_broadcast(rfq)?;
let builder = binding
    .quote()
    .price(Price::new(2_000_000_000))
    .valid_until(QuoteExpiry::from_unix_seconds(1_725_000_000))
    .nonce(Nonce::new(42));

let args = builder.preimage_args(signer.pubkey_bytes())?;
let order_id = compute_order_id(&args);
# let _ = order_id;
# Ok(()) }
```

`BytesSigner` wraps an Ed25519 keypair with `Zeroize` on drop. `SignerLike` is the
synchronous 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`
also covers signing, so timed-out signer futures must be cancellation-safe.

### Delegated signing keys

A maker registration can delegate signing: an auth key that opens sessions and a
quote key that signs orders, both distinct from the maker owner. The pubkey you
authenticate as is always the *owner* — the server looks the registration up by
it and verifies the challenge against the registered auth signing key — and the
order preimage is always reconstructed with the owner's key, so both sides must
be told the owner when the keys differ:

```rust,no_run
use acta_maker_sdk::*;
use acta_maker_sdk::ws::types::RfqBroadcastMessage;
# fn run(rfq: &RfqBroadcastMessage, owner: [u8; 32]) -> Result<(), Box<dyn std::error::Error>> {
# let quote_signer = BytesSigner::from_secret([1u8; 32]);
let quote = RfqBinding::from_broadcast(rfq)?
    .quote()
    .price(Price::new(2_000_000_000))
    .valid_until(QuoteExpiry::from_unix_seconds(1_725_000_000))
    .nonce(Nonce::new(42))
    .maker_owner(owner)
    .sign(&quote_signer)?;
# let _ = quote;
# Ok(()) }
```

On the connection side, both `ManagedWsConfig::new` and `new_async` default
to the signer's own key; override it with `with_auth_pubkey(owner)`. Without
`maker_owner` every quote from a delegated key is rejected as
`order_id preimage mismatch`; without the owner auth pubkey the session fails as
`maker_not_registered`.

## 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:

```rust,no_run
# #[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 pre-sign check includes the wrap amount, rent for token
accounts created by funding, the assembled transaction's RPC fee, and the configured
reserve. With an external fee payer, the maker budget includes only the wrap and
reserve. This is a snapshot, not an on-chain lock: another 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.

## Settlement

Settlement is a permissionless on-chain crank, normally run by Acta's keeper
once the market is finalized. Proceeds are paid directly into the maker owner's
associated token account for the payout mint — the keeper's transaction creates
that account if it is missing — so a settled position requires no maker action
and no SDK call. `build_withdraw_premium_ixs` moves earned premium out of the
premium vault; it is not part of settlement.

## Protocol types

The current WS contract requires `Welcome.server_time_unix_ms`,
`AuthSuccess.expires_at`, `RfqBroadcast.sent_at_unix_ms`, and `instruction_index`
on all six known chain events. Missing or null values are rejected. The two
`*_unix_ms` fields remain milliseconds; credential expiry remains Unix seconds.
Credential expiry does not itself define the lifetime of an authenticated socket.
Chain events are identified by `(signature, instruction_index)`, never by
signature alone.

The SDK provides enums for known string values and tagged state objects:

- `PositionType`: `"covered_call"` | `"cash_secured_put"`
- `MakerQuoteState`: `active { rank, rfq_version }`, `retained { rfq_version }`, `awaiting_signature { rfq_version }`, `executing { rfq_version }`, or `historical { status }`; `QuoteRank` is `"best"` | `"outbid"` and `HistoricalQuoteStatus` is `"submitted"` | `"filled"` | `"cancelled"` | `"replaced"` | `"expired"` | `"lost"`.
- `OrderExecutionState`: `pending`, `confirmed { position_pda }`, or `unknown`.
- `QuoteCancelReason`: `"requested"` | `"risk_check"` | `"rfq_accepted"` | `"maker_disconnected"`
- `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 use SDK enums. Open-ended fields, such as
diagnostic reason text, remain strings.

## Examples

The programs in the
[`examples/` directory](https://github.com/acta-markets/rust-maker-sdk/tree/main/examples)
cover raw authentication, discovery, quoting, indicative pricing, and the
managed quote client.

```bash
cargo run --example managed_quote --features ws-client
```

## Documentation

See the [Rust API reference](https://docs.rs/acta-maker-sdk/0.4.0) and the
[maker integration guide](https://docs.acta.markets/quickstart/maker-rust-sdk).
README examples are compiled as `no_run` doctests without opening live connections.

## License

MIT