Protocol Flow

Acta is an RFQ options venue on Solana. Positions are fully collateralized against on-chain escrow accounts and settle physically after expiry. There is no continuous margin engine, no leverage, and no early exercise.

API payloads are in Maker API, Taker API, and WS common conventions.

Actors

ActorRoleAccess
TakerWrites the option (covered call or cash-secured put), posts collateral, opens the RFQ.Open. Any wallet; no registration, no KYC. Only rate-limited (active RFQ count).
MakerBuys the option, pays premium, may fund the settlement leg.Permissioned. On-chain RegisterMaker is admin-gated and timelocked. Subject to caps.
Keeper (hot authority)Relays fills on-chain, co-signs OpenPosition, calls FinalizeMarket.Protocol-operated.
LiquidatorCloses ITM positions the maker never funded; fronts the settlement to the taker.Permissionless. Anyone can run it.
Oracle / adminPublishes the scalar settlement price after expiry.Protocol-operated; settlement/liquidation that consume it are permissionless.

The maker is the option buyer and the taker is the writer. The is_taker_buy field in the order-id preimage is fixed at 0; a taker-buys-the-option market type is reserved but not implemented.

What an RFQ does

There is no order book. The taker drives a sealed auction, makers stream signed quotes, and the taker picks the winner.

  • The taker sends RfqRequest (market, position type, strike, quantity). The backend broadcasts it to eligible makers.
  • Makers reply with signed Quotes. Each quote is a (strike, price, valid_until) against the fixed request — makers cannot quote partial size or change the side.
  • The taker selects the winner by sending AcceptQuote { maker, order_id }. Whatever order_id the taker names wins. The server's "best price" (QuoteBestStatus / QuoteOutbid) is advisory only and never selects the winner.
  • Winner-take-all: one quote is locked for the full RFQ quantity. No partial fills, no multiple winners. Quotes reserve nothing until a fill consumes caps and open interest.
  • One quote per (maker, strike): a maker's new quote on the same strike replaces the prior one. A maker may quote several distinct strikes.
  • The advisory "best" ranking (used only for QuoteBestStatus / best_price) is: higher premium wins, ties broken by earlier received_at, then by smaller order_id.

Indicative pricing: after connecting, makers may receive IndicativePricesRequest and reply with non-binding reference prices per strike. Takers see these while browsing, before opening an RFQ.

Trade lifecycle

market traded
  -> RFQ active            (taker requests quotes)
  -> quotes streaming      (makers submit signed quotes)
  -> quote accepted        (taker picks order_id; sponsored tx built)
  -> position open         (atomic risk transfer on-chain)
  -> position funded?       optional (maker deposits settlement leg)
  -> market finalized      (oracle settlement price written after expiry)
  -> settled OTM / settled ITM funded / liquidated ITM unfunded

1. Market

A market carries: underlying mint, quote mint, expiry timestamp, option side (covered call or cash-secured put), strike, oracle setup. It is tradeable until market.expiry_ts. After expiry, oracle finalization is required before any settlement or liquidation.

2. RFQ

The taker sends RfqRequest with market, position type, strike, and quantity. quantity is always in underlying atomic units. Before broadcasting, the backend checks: market is active, taker can trade, trading is not paused, size and tick rules pass, caps do not block. A maker blocked by caps may receive RfqSkipped instead of RfqBroadcast.

3. Quote

Makers receive RfqBroadcast, choose a premium, build a 32-byte order_id, sign it, and send Quote. The quoted price is gross premium per 1 underlying unit, scaled by 1e9. The order_id commits to market, position type, strike, quantity, gross price, validity, maker, taker, and nonce. Backend checks: order-id and maker signature match, quote still valid, maker registered, maker has enough deposited quote balance for the premium, maker and platform caps pass.

4. Accept and open

The taker sends AcceptQuote { maker, order_id }. The backend returns a sponsored transaction; the taker signs it and returns it with SubmitSignedSponsoredTx. On chain, OpenPosition requires the taker's signature, the maker's Ed25519 signature over order_id, and the keeper co-signature. If those pass, the position opens atomically; risk transfers here (see Risk model).

Timing and deadlines

Defaults below; all are configurable.

ParameterDefaultMeaning
RFQ windowtaker-chosenexpires_at = created_at + request.timeout. The auction deadline.
settlement_buffer300 sThe trailing window reserved for settlement confirmation. A quote's effective trading cutoff is valid_until - 300s; quotes with valid_until < now + 310s are rejected.
quote_refresh_margin10 sAt effective_expiry - 10s the server fires QuoteRefreshRequested and freezes the quote (unacceptable until re-quoted).
signature_timeout30 sMax time the taker has to sign the sponsored tx after AcceptQuote. The signature deadline is the minimum of now + 30s, the quote's effective expiry, and the RFQ's expires_at.
submitted_watchdog_timeout120 sIf the keeper does not confirm a submitted order within this window, it is failed/reverted.
closed_rfq_ttl300 sHow long a closed RFQ is retained before purge.
max_quotes_per_rfq50Cap on quotes per RFQ across all makers.
max_active_rfqs_per_taker / _total10 / 1000Concurrency limits.

rfq.expires_at is the auction deadline; quote.valid_until is the on-chain order validity. They are distinct clocks.

On a signature timeout or tx-build failure, the locked (winning) quote is discarded and only the still-valid losing quotes are restored; the RFQ reverts to active if it has not yet expired.

Economics

Units

PRICE_SCALE = 1_000_000_000.

FieldMeaning
priceGross premium per 1 underlying unit, 1e9 scale
strikeQuote per 1 underlying unit, 1e9 scale
quantityUnderlying atomic units
total_premiumNet premium paid to taker, quote atomic units

Quote-side atomic amount:

scaled_quote_amount(x, quantity) =
  floor(x * quantity * 10^quote_decimals / (PRICE_SCALE * 10^underlying_decimals))

Used for both scaled_quote_amount(price, quantity) = gross premium and scaled_quote_amount(strike, quantity) = notional / put collateral / call settlement amount.

Cash flow at open

On a successful OpenPosition:

  1. Maker PDA pays net premium to the taker in the quote token.
  2. Maker PDA pays the protocol fee to the protocol fee account.
  3. Taker collateral moves into the position escrow.
  4. Position status becomes open.

Collateral

Position typeTaker collateral (locked at open)Maker settlement asset (if ITM)
Covered callquantity underlying atomic unitsscaled_quote_amount(strike, quantity) quote atomic units
Cash-secured putscaled_quote_amount(strike, quantity) quote atomic unitsquantity underlying atomic units

Taker collateral is fully locked at open. The maker settlement leg is not locked at open: the maker pays premium at open and may deposit the settlement asset later with DepositFundsToPosition (status openfunded). The maker also needs a deposited program quote balance (via DepositPremium) for the premium debit; idle balance is retrieved with WithdrawPremium.

Fees

The maker signs the gross price. The contract nets fees during open:

gross_premium = scaled_quote_amount(price, quantity)

price_after_premium_fee_bps =
  price - floor(price * protocol_fee_bps_premium / 10_000)
premium_fee =
  gross_premium - scaled_quote_amount(price_after_premium_fee_bps, quantity)

notional_quote = scaled_quote_amount(strike, quantity)
volume_fee = floor(notional_quote * protocol_fee_bps_volume / 10_000)

fee_total   = min(premium_fee, volume_fee)
net_premium = gross_premium - fee_total

The taker receives net_premium; Position.total_premium stores the same net value. protocol_fee_bps_premium and protocol_fee_bps_volume are on-chain config set by governance (each ≤ 10000 bps). net_price in WS payloads is a display estimate only; for accounting use the formula above or the on-chain total_premium.

Settlement and payoff

Settlement is physical: it moves tokens at the strike. After expiry the hot authority publishes scalar prices to the Acta oracle accounts (configured via OracleSource) and calls FinalizeMarket:

settlement_price = floor(underlying_price * PRICE_SCALE / quote_price)

Price publication has two modes, selected by GlobalConfig.settlement_attestor:

  • Direct (no attestor configured): UpdateOraclePrice writes the backend-published scalar.
  • Attested (attestor configured): the hot authority must submit UpdateOraclePriceAttested, immediately preceded in the same transaction by an Ed25519 signature from the configured attestor key over a domain-separated message binding the program, config, oracle, price, expiry and a short validity window; the on-chain program verifies the binding. The published price is computed off-chain from Pyth Benchmarks history under a fixed canonical TWAP policy.

Neither mode verifies a Pyth price-update account on-chain; settlement trusts the Acta oracle (plus, in attested mode, the independent second signature), not an on-chain Pyth proof.

At-the-money counts as OTM.

Position typeOTMITM
Covered callsettlement_price <= strikesettlement_price > strike
Cash-secured putsettlement_price >= strikesettlement_price < strike

Outcomes by status and moneyness:

StatusMoneynessToken movement
openOTMTaker gets collateral back.
fundedOTMTaker gets collateral back; maker gets settlement deposit back.
fundedITMTaker collateral goes to maker; maker settlement deposit goes to taker (a swap at the strike).
openITMNormal settlement fails; the position must be liquidated.

Who holds and receives what:

Covered callCash-secured put
Taker posts (collateral)UnderlyingQuote
Taker receives (premium)Quote (net)Quote (net)
Maker pays (premium)QuoteQuote
Maker funds (settlement, if ITM)QuoteUnderlying
ITM: taker receivesQuote (maker's settlement)Underlying (maker's settlement)
ITM: maker receivesTaker's underlying collateralTaker's quote collateral
OTM: each side keepsTaker keeps collateral; maker keeps settlement depositSame

Risk model

Every position is fully collateralized at open in its own escrow. There is no leverage, no maintenance margin, no mark-to-market, no margin call, and no early exercise. Positions open before expiry and resolve only after market finalization.

Custody is per-position. Taker collateral and the maker's settlement deposit each sit in escrow accounts owned by the position PDA; the maker's premium balance sits in the maker PDA. The protocol never holds principal risk; it custodies escrow and routes settlement. The counterparty is a specific maker, not a pool or the protocol: each position names one taker and one maker.

The taker carries no credit risk. Collateral is locked at open, and the taker is made whole on every path: the maker funds the ITM settlement, or a permissionless liquidator fronts it.

The maker is exposed only by leaving an ITM position unfunded. The settlement leg is not pre-funded at open. If the option expires ITM and the maker never called DepositFundsToPosition, normal settlement fails and the position stays open; a liquidator then pays the taker the settlement asset, takes the taker's collateral, and closes the position as liquidated. The maker forfeits the premium already paid, and the protocol enforces no further debt. Maximum loss on either side is bounded by escrowed funds.

Liquidation is post-expiry only: an ITM, unfunded position after finalization, not a price-threshold or maintenance call. It is permissionless but not automatic; the protocol exposes the path and does not run the liquidator. The taker is paid when the liquidation transaction lands.

Backend caps are a separate risk control layered on top: platform caps (token open interest per underlying, quote notional per quote mint, market open interest) apply to everyone; maker caps (open position count, notional per underlying, deposited premium balance) apply to makers. See Capacity limits.

State machine and events

Terminal states:

  • RFQ: expired, cancelled, or filled.
  • Position: settled or liquidated.

Events worth tracking end to end:

EventWhen
QuoteFilled / OrderConfirmedPosition opened on-chain; carries position_pda and tx_signature.
RfqClosedAuction closed; on a fill, winner = { maker, price, tx_signature }.
ChainEvent(position_opened)Position opened, via chain events.
MarketFinalizedMarket expired, oracle settlement price set.
ChainEvent(position_settled)Assets distributed.
ChainEvent(position_liquidated)Unfunded ITM position liquidated.

RegisterMaker, DepositPremium, WithdrawPremium, and DepositFundsToPosition are Solana on-chain instructions, not WebSocket messages; build and submit them as transactions signed by the relevant owner wallet. Contact the Acta team for the program IDL and account layouts.