@acta-markets/ts-sdk

中文文档
0.4.2发布于 2026/11/04MITTypeScriptESM + CJS近 30 天下载 2391

TypeScript SDK for Acta options: RFQ WebSocket client, order signing, and Solana instructions

原始使用文档
文档目录12 节
  1. Install
  2. Taker: request a quote
  3. Buying options
  4. Maker: quote an RFQ
  5. HTTP reads
  6. Chain
  7. Migrating to 0.4.2
  8. Migrating to 0.4.0
  9. Recovery
  10. List paging
  11. Documentation
  12. License

Acta TypeScript SDK

TypeScript SDK for Acta options on Solana: WebSocket RFQ clients for takers, makers and vault operators, order signing, checked signing of served transactions, HTTP reads, and Solana instruction builders.

Version 0.4.x speaks WS protocol 1.0.0. Read the changelog before upgrading.

Install

npm install @acta-markets/ts-sdk

The package ships ESM and CommonJS builds.

ImportContents
@acta-markets/ts-sdk/wsWebSocket client, wallet auth, quote building, checked taker signing
@acta-markets/ts-sdk/chainAccount fetchers, PDAs, order preimages, instruction builders, vault planners
@acta-markets/ts-sdk/httpVenue, token, market and vault reads
@acta-markets/ts-sdk/ws/keypairKeypairAuthProvider for Node
@acta-markets/ts-sdk/typesShared types

Browser apps should import /ws. It does not load the chain client or the IDL.

Taker: request a quote

import {
  ActaWsClient,
  ALL_MINTS,
  ALL_POSITIONS,
  DEVNET_WS,
  WalletAuthProvider,
  type WalletLike,
} from "@acta-markets/ts-sdk/ws";
import type { Address } from "@solana/kit";

export function connectTaker(wallet: WalletLike, market: Address) {
  const client = new ActaWsClient({
    url: DEVNET_WS,
    role: "taker",
    onDisconnect: "keep_quotes",
    products: ["covered_call", "cash_secured_put", "call_spread", "put_spread", "cash_put"],
    clientName: "my-app",
    clientVersion: "1.0.0",
  });

  client.on("error", console.error);
  client.on("quoteReceived", (quote) => console.log(quote.maker, quote.price));
  client.on("authenticated", () => {
    client.getMarketDescriptors();
    client.getPositions(ALL_POSITIONS);
    client.subscribe(["chain_events"], ALL_MINTS);
  });
  client.on("marketDescriptors", async () => {
    await client.createRfq({
      market,
      terms: { type: "cash_secured_put", strike: 136_000_000_000 },
      side: "sell",
      quantity: 20_000_000,
      timeoutSeconds: 30,
      requestId: crypto.randomUUID(),
    });
  });

  client.connectAndAuthenticate(new WalletAuthProvider(wallet));
  return client;
}

url is the API origin; the client appends /taker, /maker, /maker/data or /vault. DEVNET_WS and MAINNET_WS are exported. Register handlers before connecting and wait for authenticated before trading. createRfq needs the market's descriptor, so wait for marketDescriptors first.

price and strike are 1e9 fixed-point values per underlying unit. quantity is in underlying-mint atoms. createRfq, acceptQuote and submitSignedSponsoredTx resolve when the server accepts the command, not when the transaction lands.

After acceptQuote(rfqId, maker, orderIdHex), the server sends sponsoredTxToSign. Sign it only with verifyAndSignTakerFill, which rebuilds the expected fill and refuses anything else. Keep accepted order IDs across reconnects; after a timeout or an unknown delivery, call getOrderStatus(orderIdHex) before retrying. The web client guide has the full signing and recovery flow.

Buying options

A taker buys with side: "buy" in the RFQ. A maker writes the option from its Maker PDA custody, so only makers subscribed to taker_buy_rfqs with enough custody see the RFQ. A vault whose delegate holds SIGN_WRITER_QUOTES also quotes buys and writes from its collateral leg. The best buy quote is the lowest price. If no maker can receive the RFQ, the server closes it at once with reason no_eligible_makers instead of waiting for the timeout.

await client.createRfq({
  market,
  terms: { type: "covered_call", strike: 150_000_000_000 },
  side: "buy",
  quantity: 10_000_000,
  timeoutSeconds: 30,
  requestId: crypto.randomUUID(),
});

Accept and sign exactly as for a sell: verifyAndSignTakerFill reads the side from the RFQ and refuses a fill whose side, terms or amounts differ. The buyer pays the gross premium in the quote mint and becomes the position holder; the maker or vault is the writer. For a vault quote pass the decoded vault as makerVault: the buyer then checks the served OpenPositionForVaultWriter with holder byte 1, in which the buyer's wallet is the signer and the vault's delegate signed only the order id.

A physical buy (covered_call, cash_secured_put) is exercised by depositing the settlement amount strictly before expiryTs; the program rejects a later deposit:

import { fetchMarket, fetchPosition } from "@acta-markets/ts-sdk";
import { flows } from "@acta-markets/ts-sdk/chain";

const { ixs } = await flows.buildDepositFundsIxs({
  programId,
  payer: buyer.address,
  owner: buyer,
  position: (await fetchPosition(rpc, positionPda)).data,
  market: (await fetchMarket(rpc, marketPda)).data,
});

Spreads and cash puts take no deposit and settle in cash. Outcomes arrive as the position state:

  • settled with settlement.type physical (deposited, or out of the money), cash (holder_payoff is yours), or not_exercised (in the money without a deposit: all collateral returns to the writer).
  • terminal_pending { slot, outcome }: the chain has settled the position and the server has not sealed that slot yet. Show it, but do not count it.

Maker: quote an RFQ

import {
  ActaWsClient,
  buildSignedQuoteFromRfq,
  generateNonce,
  MAINNET_WS,
  type AuthProvider,
  type RfqBroadcastMessage,
} from "@acta-markets/ts-sdk/ws";
import type { SignerLike } from "@acta-markets/ts-sdk/chain";
import type { Address } from "@solana/kit";

export function runMaker(
  auth: AuthProvider,
  makerOwner: Address,
  makerSigner: SignerLike,
  priceFor: (rfq: RfqBroadcastMessage) => number,
) {
  const quotes = new ActaWsClient({
    url: MAINNET_WS,
    role: "maker",
    onDisconnect: "cancel_quotes",
    products: ["covered_call", "cash_secured_put"],
    clientName: "my-maker",
    clientVersion: "1.0.0",
  });
  let timing = { minValidity: 0, buffer: 0, clockOffsetMs: 0 };
  quotes.on("welcome", (welcome) => {
    timing = {
      minValidity: welcome.min_quote_validity_seconds,
      buffer: welcome.settlement_buffer_seconds,
      clockOffsetMs: welcome.server_time_unix_ms - Date.now(),
    };
  });
  quotes.on("rfqBroadcast", async (rfq) => {
    const now = Math.floor((Date.now() + timing.clockOffsetMs) / 1000);
    const validUntil = Math.max(
      now + timing.minValidity,
      rfq.expires_at + Math.max(timing.minValidity, timing.buffer),
    );
    if (validUntil > rfq.market.expiry_ts) return;
    const quote = await buildSignedQuoteFromRfq({
      rfq,
      terms: rfq.order_options[0],
      price: priceFor(rfq),
      validUntil,
      nonce: generateNonce(),
      makerOwner,
      makerSigner,
    });
    quotes.submitQuote(quote);
  });
  quotes.connectAndAuthenticate(auth);
  return quotes;
}

Quote only terms listed in rfq.order_options. buildSignedQuoteFromRfq checks the terms and integer ranges, not venue timing. A quote must stay valid past the RFQ expiry by the larger of min_quote_validity_seconds and settlement_buffer_seconds from Welcome. Use a second client with makerEndpoint: "data" for reads and owner pushes (positionUpdated, makerCapsUpdated, tradeExecuted) so the quote socket only quotes. The maker API reference covers the quote lifecycle.

HTTP reads

import { ActaHttpClient } from "@acta-markets/ts-sdk/http";
import { MAINNET_API } from "@acta-markets/ts-sdk/ws";

const http = new ActaHttpClient({ baseUrl: MAINNET_API });
const [venue, tokens, markets, vaults] = await Promise.all([
  http.venue(new AbortController().signal),
  http.tokens(),
  http.markets({ active: true }),
  http.vaults(),
]);

tokens() returns a Map keyed by mint. Responses are revalidated with ETags.

Chain

/chain exports account fetchers, PDA helpers, order preimages and signing, the generated instruction builders, and flows:

  • flows.buildOpenPositionFlowIxs and buildOpenSpreadFlowIxs return the maker Ed25519 verification and open instructions.
  • flows.buildDepositFundsIxs funds an Open position; a taker-buy buyer signs it strictly before expiryTs.
  • buildSettlePositionIx settles physical options and spreads. An unfunded taker-buy returns all collateral to the writer, also when ITM.
  • flows.VaultManagerPlanner builds vault capital, trading, spread, swap and governance instructions. trading.openPositionAsWriter and spreads.openSpreadAsWriter sell to a maker (holder byte 0); openPositionForWalletHolder and openSpreadForWalletHolder build the open a wallet buys from the vault (holder byte 1).
  • decodeActaEventData (root export) reads event version 2: one PositionOpened per open (writer, holder, flags, terms, gross and net premium, collateral) and one VaultCycleOpened per vault side. Version 1 events from before the upgrade return null.

Vault operators connect with role: "vault" and a vaultPda. Sign served vault transactions only with verifyAndSignSponsoredTx and verifyAndSignSwap from /ws; they sign only an exact match of the expected intent and throw flows.VaultIntentError (exported from /chain) otherwise.

Mint-aware builders need rpc or an explicit token program; Token-2022 mints need their actual token program. Pass a programId per flow when you do not use the default program. The vault operator guide covers vault sessions, served swaps and staging.

Migrating to 0.4.2

Requires the vault-writer program upgrade. Opcodes, account layouts and seeds are unchanged.

0.4.10.4.2
OpenPositionForVaultAsTaker, OpenSpreadForVaultAsTakerOpenPositionForVaultWriter, OpenSpreadForVaultWriter, plus holderKind
OpenPositionForVaultAsMaker, OpenSpreadForVaultAsMakerOpenPositionForVaultHolder, OpenSpreadForVaultHolder
VaultFundMakerPosition, fundMakerPositionFundPositionForVaultHolder, fundHolderPosition
openPositionAsTaker, openSpreadAsTakeropenPositionAsWriter, openSpreadAsWriter
openPositionAsMaker, openSpreadAsMakeropenPositionAsHolder, openSpreadAsHolder
vault writer slots actor, maker, makerCustodyAtaparticipant, counterparty, premiumSource
vault holder slots taker, takerUnderlyingAta, takerQuoteAtawriter, writerUnderlyingAta, writerQuoteAta
OpenPosition slots user, userUnderlyingAta, userQuoteAtataker, writerUnderlyingAta, takerQuoteAta
deposit makerPda; maker transfer makerOwnerPremiumAtaholderMakerPda; makerOwnerAta
OPEN_TAKER, SIGN_MAKER_QUOTES, FUND_MAKEROPEN_AS_WRITER, SIGN_HOLDER_QUOTES, FUND_HOLDER; new SIGN_WRITER_QUOTES
events OpenPosition, OpenSpread, OpenTakerBuy, OpenTakerBuySpreadPositionOpened
events VaultPositionOpened, VaultSpreadOpenedVaultCycleOpened
VaultMakerPositionFunded; TakerBuyFunded.userVaultHolderPositionFunded; TakerBuyFunded.holder
holder mismatch on deposit: MakerNotOwnerNotPositionHolder (1111)

Migrating to 0.4.0

Position roles are writer and holder; bytes and offsets are unchanged.

0.3.x0.4.0
Position.takerOwner, makerOwnerwriter, holder
POSITION_OFFSET_TAKER_OWNER, _MAKER_OWNERPOSITION_OFFSET_WRITER, _HOLDER
deposit makerOwner, makerFundingAtaholder, holderFundingAta
settle and liquidate userDestAta, makerDestAtawriterDestAta, holderDestAta
events user, makerOwner, takerholder, writer
makerPayoff, takerReturn; WS maker_payoff, taker_returnholderPayoff, writerReturn; holder_payoff, writer_return
fetchPositionsByMaker, ByTakerfetchPositionsByHolder, ByWriter
buildDepositPremiumIx, DepositPremiumbuildMakerDepositIx, MakerDeposit

See the CHANGELOG for the full list.

Recovery

The client reconnects and restores subscriptions. With autoReconcile, the default, it then re-reads your RFQs and positions. A frame it cannot read does not drop the session: you get unknownMessage or an invalid_frame error, and the same reads run at most once every 5 seconds, because the skipped frame may have carried a fill or a position change.

List paging

List methods collect every page for you through fetchAllPages. For a "load more" view use a page method such as getPositionsPageAsync, or pass a page query to an HTTP history method, and keep the returned cursor as an opaque value.

On the wire, WS lists take limit (at most 200) and a tagged cursor, 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. See the paging contract.

Documentation

License

MIT

版本与发布记录

@acta-markets/ts-sdk 共发布 48 个版本,首次发布于 2026/02/16;过去 12 个月发布了 48 个版本。

版本发布日期距今
0.4.2latest2026/11/043 天前
0.4.12026/11/044 天前
0.4.02026/11/044 天前
0.3.22026/11/025 天前
0.3.12026/11/026 天前
0.3.02026/10/308 天前

其他发布通道:beta → 0.0.25-beta,vaults → 0.1.6-vaults.4。

关于 @acta-markets/ts-sdk 的常见问题

@acta-markets/ts-sdk 是什么?

TypeScript SDK for Acta options: RFQ WebSocket client, order signing, and Solana instructions

如何安装 @acta-markets/ts-sdk?

在项目目录运行 npm install @acta-markets/ts-sdk,或使用 pnpm add / yarn add / bun add。

@acta-markets/ts-sdk 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@acta-markets/ts-sdk 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@acta-markets/ts-sdk 可以商用吗?

@acta-markets/ts-sdk 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@acta-markets/ts-sdk 的下载量有多少?

根据 npm 官方下载统计,@acta-markets/ts-sdk 在 2026/10/05 – 2026/11/03 这 30 天内被下载 2,391 次,日均约 80 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@acta-markets/ts-sdk 还在维护吗?

最新版本 0.4.2 发布于 2026/11/04(3 天前);至今共发布 48 个版本,其中过去 12 个月发布了 48 个;首次发布于 2026/02/16。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。