Acta Maker Quickstart
JSON-layer path for a maker connection: auth, subscribe, quote, handle fills, reconnect. Rust integrations should start with Rust maker SDK. Payload examples are in Maker wire examples; onboarding is in Sandbox and devnet; units and envelopes are in WebSocket conventions; the message catalogue is in Maker API reference.
Connection and authentication
The first message from the client is Hello. Version compatibility is semver-based: the server accepts any client protocol_version that is >= min_supported_version, otherwise it replies with VersionMismatch and closes the connection. features is an opt-in list. The quote_expired feature, when enabled, causes the server to emit explicit QuoteExpired events in place of silent expiry.
{
"type": "Hello",
"data": {
"protocol_version": "1.0.0",
"features": [],
"client_name": "maker-bot",
"client_version": "0.1.0"
}
}
The server responds with Welcome (server_time_unix_ms is for clock sync), then AuthRequest with a challenge string. Sign the UTF-8 challenge bytes with quote_signing, base58-encode the signature, and reply with AuthChallenge:
{
"type": "AuthChallenge",
"data": {
"challenge": "<from AuthRequest>",
"signature": "<base58 ed25519 signature>",
"pubkey": "<maker_owner pubkey, base58>"
}
}
pubkey is the maker_owner wallet registered on-chain, not the quote_signing key. The server looks up the registered signing key from maker_owner and verifies against it. Auth must finish within 15 seconds of the challenge; after three failed attempts the server closes the connection.
Subscription
Subscriptions do not persist across disconnects. The client must reissue Subscribe after each successful authentication. A null mints filter selects all mints.
{
"type": "Subscribe",
"data": {
"request_id": "<uuid>",
"channels": ["rfqs", "chain_events"],
"underlying_mints": null,
"quote_mints": null
}
}
request_id (UUID) is required; the server echoes it in SubscribeAck.
AddMints, RemoveMints, AddChannels, and RemoveChannels mutate subscriptions incrementally. Each mutation returns SubscriptionUpdated with the current subscription state.
Quoting
On RfqBroadcast, pick a strike from rfq.strike or rfq.order_options, compute the premium, set valid_until >= now + 310s, build the 182-byte order preimage from Maker API reference (Quote rules), hash it to order_id, sign the 32-byte hash with quote_signing, and send Quote:
{
"type": "Quote",
"data": {
"rfq_id": "...",
"strike": 160000000000,
"price": 50000000,
"valid_until": 1710000310,
"nonce": 42,
"order_id": "0x<64 hex>",
"signature": "<base58 of ed25519(order_id)>"
}
}
When an RFQ permits multiple strikes, each strike requires a distinct Quote (or several can be batched in a single BatchQuotes). Each quote requires its own order_id and nonce. A new quote on the same (rfq_id, strike) replaces the prior quote for that pair.
Use ReplaceQuote for repricing: the swap is atomic and single-RTT. CancelQuote followed by Quote introduces a gap in the book during the cancellation window and incurs an additional round-trip.
is_taker_buy in the order-id preimage is fixed at 0; the taker is always the option writer. Submitting 1 causes preimage validation to fail. Implementations whose preimage builders default this field to true must override it explicitly.
QuoteRejected carries reason. Fix the cause before retrying; the same payload will fail again.
Lifecycle events
Lifecycle events are keyed by order_id. RfqClosed is the terminal event for an RFQ; QuoteFilled is a fill-details event and is followed by RfqClosed.
| Event | Description |
|---|---|
QuoteAcknowledged | Server accepted the quote. On a replace, includes replaced_order_id. |
QuoteRejected | Server refused the quote; see reason. |
QuoteBestStatus | Quote is currently the best in the book. |
QuoteOutbid | Quote has been displaced by a better one. |
QuoteRefreshRequested | Settlement-buffer cutoff is approaching; resubmit with valid_until ≥ min_valid_until before the cutoff. |
QuoteSelected | Taker selected this quote; sponsored-tx settlement is in flight. |
QuoteFilled | Position opened on-chain. Carries position_pda and tx_signature. |
QuoteCancelled | Terminal. reason ∈ {requested, risk_check, rfq_accepted}. |
QuoteExpired | Emitted only when quote_expired was enabled in Hello. |
RfqAvailableAgain | Settlement reverted; the auction has reopened. The maker may re-quote with a fresh order_id. |
RfqClosed | Terminal RFQ event. Drop per-RFQ state. |
On-chain responsibilities
Quoting and the lifecycle above are entirely off-chain (WebSocket). Your only on-chain actions as a maker are funding-related:
DepositPremium— deposit program quote balance; required before quoting (the fill's premium debit draws from it).WithdrawPremiumretrieves idle balance.DepositFundsToPosition— optional, after a fill: fund the settlement leg (open→funded) to avoid the ITM-unfunded liquidation loss.
You do not settle or liquidate. Publishing the settlement price and finalizing markets is operator-side; settlement is keeper-driven; liquidating ITM-unfunded positions is permissionless. Your downside is bounded — fund the settlement leg, or a third party liquidates and fronts the taker payout. See Protocol flow for the risk model.
Indicative pricing
If the account is enrolled in pre-trade pricing, the server periodically emits IndicativePricesRequest for reference prices to be displayed in the taker UI. Indicative quotes are non-binding and operate under a tighter latency budget than auction quotes. The response is correlated by request_id:
{
"type": "IndicativePricesResponse",
"data": {
"request_id": "<from request>",
"market": "<market PDA, base58>",
"position_type": "covered_call",
"prices": [
{ "strike": 150000000000, "price": 45000000 },
{ "strike": 160000000000, "price": 50000000 }
]
}
}
Reconnection
Quotes survive disconnects until valid_until or RfqClosed. Subscriptions do not; resend them after auth. The server does not replay events missed during the disconnect window. Events already in flight can arrive again after recovery, so process lifecycle events idempotently by order_id.
After reauthentication, use /maker/data for recovery reads and /maker for
quote-plane subscription state:
{ "type": "GetMyQuotes", "data": { "request_id": "...", "active_only": true } }
{ "type": "GetActiveRfqs", "data": { "request_id": "..." } }
{ "type": "GetMakerPositions", "data": { "request_id": "..." } }
{ "type": "GetMyTrades", "data": { "request_id": "..." } }
GetMyQuotes with active_only: true returns live quotes. With active_only: false, the backend also appends historical quotes from DB; pass limit to cap the historical slice. GetMyTrades supports keyset pagination via cursor and cursor_id, and a market filter; see Maker API reference.
For MM dashboard bootstrap, send GetMmSummary on /maker/data after auth or reconnect. Do not poll it; use manual refresh or drift recovery if a full snapshot is needed later.
If many maker workers can reconnect at once, add a small random delay before expensive recovery
reads so they do not all hit the data plane in the same second.
Discovery
Fetch static metadata on /maker/data at startup and refresh it when markets or tokens change. Do not refresh these requests on a fixed interval.
{ "type": "GetMarketDescriptors", "data": { "request_id": "...", "active_only": true } }
{ "type": "GetExpiries", "data": { "request_id": "..." } }
{ "type": "GetTokens", "data": { "request_id": "...", "active_only": true } }
{ "type": "GetMarketsForMaker", "data": { "request_id": "..." } }
active_only: true filters to tradable markets (non-finalized, non-disabled, before the pre-expiry trading cutoff); false returns settled/expired markets in addition.
Operational defaults
| Topic | Recommendation |
|---|---|
| Application Ping | Approximately every 30 seconds. Each Pong carries an updated server_time_unix_ms. |
| Reconnect backoff | Exponential with jitter, e.g. 250 ms initial, 5 s cap, ±20%. |
valid_until margin | now + 320..360s. Values below 310s are rejected; values significantly above 360s increase the maker's exposure window without functional benefit. |
| Clock skew | Track offset = server_time − local_time from Welcome and Pong. Apply when computing valid_until. |
| Quote concurrency | One active quote per (rfq_id, strike). Repricing via ReplaceQuote. |
| Message rate | 30 msg/s sustained, 60 burst per WebSocket connection. |
| Query rate | 20 query tokens/s sustained, 40 burst per WebSocket connection. |
| Message size | 32 KiB inbound WS message limit. |
| Batch quotes | Hard max 50 quote elements; cost is max(1, quotes.length) quote tokens. |
The server sends WebSocket protocol pings every 30 seconds and closes idle connections after 90 seconds. WebSocket libraries that do not answer protocol pings will disconnect. If a correct client still drops, check proxies or firewalls that close idle TCP connections.