@bskts/sdk

中文文档
0.9.2发布于 2026/11/03MITTypeScriptES Module构建来源已验证近 30 天下载 2010

Typed, non-custodial client for the bskts API: tokenized index baskets on Robinhood Chain. Reads, unsigned transaction builders, dry runs and guard rails for wallets and agents.

原始使用文档
文档目录9 节
  1. The loop
  2. Orders
  3. Errors
  4. Units
  5. Earning fees
  6. Delegated agents (bskts accounts)
  7. Weekend Cover (SDK 0.6.0+)
  8. Delegated limit buys
  9. Delegated exits, buy-above limits and previews

@bskts/sdk

Typed, non-custodial client for the bskts API: tokenized index baskets of Stock Tokens and crypto on Robinhood Chain (chainId 4663).

  • Reads: live NAV, 24h change and buy cost for every basket, wallet balances, positions with PnL, open orders, NAV history or just its summary, and every public job executable right now.
  • Unsigned plans: build* methods return { approval, tx, simulation }. You sign with your own wallet through viem. The SDK never holds keys or funds.
  • Safe by default for agents: the API dry-runs every plan as your address and execute() refuses one that would revert; an optional policy caps trade size, baskets, cost and slippage before anything is built; errors carry a stable code.
  • Typed from Zod schemas: every response is validated at runtime. On-chain amounts are bigint, addresses and calldata viem's Address / Hex.
pnpm add @bskts/sdk viem

ESM only, Node 20.19+, browsers, Workers, Deno and Bun. viem is a peer dependency.

The loop

import { BsktsClient, robinhood, viemSender } from "@bskts/sdk";
import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount(process.env.AGENT_KEY as `0x${string}`);
const sender = viemSender(
  createWalletClient({ account, chain: robinhood, transport: http() }),
  createPublicClient({ chain: robinhood, transport: http() }),
);
const me = account.address;

const bskts = new BsktsClient({
  policy: { maxUsdPerTrade: 250, allowedTickers: ["DIGI64", "MAG6"], maxBuyCostBps: 50 },
});

// 1. pick
const { markets } = await bskts.markets(); // ticker, nav, change24h, buyCostBps
const { summary } = await bskts.navSummary("DIGI64", { days: 30 });

// 2. check the wallet: USDG to spend, ETH for gas
const wallet = await bskts.wallet(me);
if (!wallet.gasOk) throw new Error(wallet.note);

// 3. buy $100: approval first if needed (waited on), then the buy
await bskts.execute(
  await bskts.buildBuy({ ticker: "DIGI64", amountUsdg: 100, address: me }),
  sender,
  { wait: true },
);

// 4. hold
const { positions } = await bskts.positions(me); // value, avgEntry, unrealizedPnl, walletSharesWei (bigint)

// 5. sell half, then the rest
await bskts.execute(
  await bskts.buildSell({ ticker: "DIGI64", address: me, fraction: 0.5 }),
  sender,
  { wait: true },
);
await bskts.execute(await bskts.buildSell({ ticker: "DIGI64", address: me }), sender, {
  wait: true,
});

In a browser, build the wallet client over the injected provider: createWalletClient({ account, chain: robinhood, transport: custom(window.ethereum) }). For another signer library, implement Sender (send(tx) and wait(hash)).

Orders

With policy.maxBuyCostBps set, market buys and limit buys (wallet and delegated) refuse a current buy cost above the cap or an unknown cost with POLICY. For a resting limit, this check applies when the order is placed; its on-chain slippage floor governs the later fill. Sells, exits and cancellations do not require a buy-cost estimate.

// stop-loss + take-profit, one cancels the other
await bskts.execute(
  await bskts.buildOrder({
    kind: "exit",
    ticker: "INDEX2",
    address: me,
    shares: positions[0].walletSharesWei,
    stopNav: 92,
    tpNav: 112,
  }),
  sender,
);

// a resting limit buy: fills when the 10-minute average NAV drops to the trigger
await bskts.execute(
  await bskts.buildOrder({
    kind: "limit",
    ticker: "MAG6",
    address: me,
    amountUsdg: 250,
    triggerNav: 98,
  }),
  sender,
);

// cancel: pass the order's escrow too. Every escrow numbers its orders from 1, so one
// id can be open on several, and without escrow the API refuses with ORDER_AMBIGUOUS
const { orders } = await bskts.orders(me);
const [o] = orders;
await bskts.execute(
  await bskts.buildOrder({ kind: "cancel", orderId: o.id, escrow: o.escrow, address: me }),
  sender,
);

Errors

Every failure is a BsktsError with code, retryable and detail:

codemeaning
POLICYyour client's policy refused it; nothing was requested or sent
SIMULATION_FAILEDthe API's dry run reverted (detail.reason); execute(plan, sender, { force: true }) sends anyway
INSUFFICIENT_SHARESselling more than the wallet holds (escrowed shares must be cancelled out of their order first)
NO_ROUTEa constituent has no route right now: that basket can't be bought or sold through the API
NO_REFERENCElimit / exit orders unavailable for this basket (market buys and sells work)
ORDER_AMBIGUOUSa cancel's orderId is open on more than one escrow: pass escrow from orders()
UNKNOWN_BASKET, BAD_REQUEST, ORDER_NOT_OPEN, NOT_FOUNDfix the request
PAUSED, UPSTREAM, SERVER, NETWORKretryable: true: try again later (except NETWORK from sessionRelayer: see UNCERTAIN)
FEE_REQUIREDthe relayer quoted detail.requiredFee; executeSession re-signs the same action within your cap
RATE_LIMITEDretryable: true: the relayer sent nothing; resubmit the same signed action later
REFUSEDthe relayer refused before sending (reason in the message); retryable only for its 503
UNCERTAINthe relayer failed and may have sent it: check the action's nonce on chain before anything else
TX_FAILEDmined and reverted (usually the market moved past minShares / minOut): rebuild and retry
BAD_RESPONSEthe API answered with a shape this SDK version doesn't know: upgrade

Units

  • amountUsdg, NAVs, value, PnL: plain dollars (number).
  • On-chain amounts (*Wei, shares, minShares, expectedUsdg, minOutUsdg, feeUsdg): bigint in base units (shares 1e18, USDG 1e6). Pass walletSharesWei back as-is.
  • change24h and NAV summaries: fractions (0.012 = +1.2%). buyCostBps, slippageBps: basis points.

Earning fees

Resting orders and scheduled buys are public jobs: jobs() lists every one executable right now, with calldata and the fee it pays whoever sends it. Simulate first; each fill is held to the order's on-chain floor.

MIT · API guide for agents · bskts docs

Delegated agents (bskts accounts)

Use this path for autonomous trades after a one-time owner approval. Direct wallet buildBuy/buildSell plans are a different path and need the wallet's signer and ETH. A delegated agent trades from the owner's bskts account, never withdraws, and pays network fees from account USDG.

  1. Create an agent signing key in your host wallet or secure signer. Share only its public address. The SDK and MCP never accept private keys.
  2. Read client.agentSession({owner, key}). It resolves the owner's account. If inactive, send the owner to the returned approvalUrl; in Portfolio they connect the public key and review/sign its limits. Fund that account with USDG.
  3. Build client.buildAgentTrade({owner,key,ticker,side:"buy",amountUsdg:6}) or {owner,key,ticker,side:"sell",shares} (exact bigint base units).
  4. Give executeSession the host's signer callbacks and an explicit network fee ceiling. There is no owner popup per trade or owner/agent ETH requirement.
import { BsktsClient, executeSession, sessionRelayer } from "@bskts/sdk";
import type { SessionSender } from "@bskts/sdk";

// hostSigner is supplied by your secure host wallet, already configured to
// use the agent key. Never embed a private key in source, prompts or MCP calls.
const client = new BsktsClient();
const key = hostSigner.address;
const permission = await client.agentSession({ owner, key });
if (!permission.active) throw new Error("Owner approval required: " + permission.approvalUrl);

const sender: SessionSender = {
  key,
  sign: (typedData) => hostSigner.signTypedData(typedData),
  submit: sessionRelayer(),
};
const plan = await client.buildAgentTrade({
  owner,
  key,
  ticker: "INDEX2",
  side: "buy",
  amountUsdg: 6,
  slippageBps: 50,
});
const receipt = await executeSession(plan, sender, {
  maxNetworkFeeUsdg: 250_000n, // host explicitly allows at most $0.25 network fee
});

Suggested owner limits: 10% of available account USDG (at least $10) capped at $100 of daily turnover, 0.5% all-in module slippage, seven days. The approved dollar limit stays fixed after deposits. Only the owner can change/renew it. Some baskets need more slippage; request owner approval rather than silently widening it. The relayer needs $5 of basket value after fees/slippage for buys (so a $5 cash budget can be too small), or the entire sell position.

Session plans are unsigned, so simulation.ok is null. The relayer simulates the signed Action before broadcasting. A 402 requiredFee is re-signed only within the host's fee cap, retaining the same nonce, deadline and trade terms.

Delegated plans bind their basket ticker to the reviewed vault addresses in src/agent-vaults.json. The client rejects a mismatched API response, and executeSession checks again before signing, including plans supplied directly or changed after parsing. Unknown or redeployed baskets require an SDK upgrade; addresses never change automatically from a trade response. Maintainers update the pins from /v1/baskets only after comparing them with the main repository's canonical src/lib/contracts.ts, and ship the reviewed change with a changeset.

executeSession remembers the fee each kind of trade on each basket last needed (the relayer quotes on success too) and signs the next one with it, so a repeat trade is one signature and one relayer round trip. A plan built with feeUsdg keeps that fee, and a remembered fee above your cap is not used. The memory lives in this process for six hours; pass fees: new Map() or your own { get, set } store to keep it elsewhere (across workers or restarts), or fees: false to always start from the 402 quote.

The SDK does not automatically retry a timeout: check BsktsSessionModule.nonceUsed(account,nonce) and the Executed event before resubmitting. Never rebuild a possibly submitted trade with a new nonce.

For a process that can restart, use executeSessionOnce (SDK 0.5.0+) and one durable store per logical trade. It reserves the unsigned plan before signing, retains the original nonce across fee quotes, and records the successful receipt. Concurrent starts can only reserve one plan. A restart never signs or submits that operation again: it returns the recorded outcome or reconciles its nonce. Keys and signatures are never written to this store.

import { executeSessionOnce, sessionActionState, robinhood } from "@bskts/sdk";
import { fileSessionStore } from "@bskts/sdk/node"; // Node only; no fs in the root SDK
import { createPublicClient, http } from "viem";

const chain = createPublicClient({ chain: robinhood, transport: http("https://bskts.xyz/rpc") });
const result = await executeSessionOnce({
  store: fileSessionStore(".bskts-agent/strategy-operation-001.json"),
  sender, // same secure host callbacks as above
  maxNetworkFeeUsdg: 250_000n,
  build: () => client.buildAgentTrade({ owner, key, ticker: "INDEX2", side: "buy", amountUsdg: 6 }),
  reconcile: (plan) => sessionActionState(chain, plan.action),
});

Retain the journal and its .result file, including after success. Ignore .bskts-agent/ in Git. Custom stores must make reserve an exclusive durable create; an ordinary overwrite does not prevent competing processes from trading. File storage is for processes sharing that filesystem; independent hosts need a shared store with the same guarantee. Partial/corrupt records fail closed.

sessionActionState reads the pinned module at one RPC block on chain 4663. An unused nonce remains unresolved at or before its deadline, because a request may still be in flight. Read failures leave the record intact. A consumed nonce means no further submission is needed; it does not recover a transaction hash. An expired unused action is retained as a terminal outcome and is never replaced automatically. Reconcile the previous operation before choosing a new store ID for a deliberate new trade. Do not delete records to restart after an error.

When delegated slippage is omitted, the API resolves it to the lower of 50 bps and the owner's grant maximum. The client checks that resolved value against policy.maxSlippageBps; explicit values over policy are refused before fetching.

Daily turnover includes buys and sells, resets at 00:00 UTC, is separate for each key and excludes network fees (module hard ceiling $10 per Action). Revoking blocks new requests once confirmed; existing orders/schedules can still execute and need separate cancellation. Use a fresh key to reconnect.

The MCP exposes bskts_agent_session and bskts_agent_trade for the same flow. It stays unsigned: your agent host must supply the signer and submitter.

Agent keys authorise account trading, including Weekend Cover when it is enabled. Cover spending is counted separately against the same daily limit; underwriting additionally requires the account owner’s allowance. Individual key revocation leaves existing Cover listings open: withdraw them in My cover. The API’s moduleEnabled/coverEnabled fields report the current account setup.

Weekend Cover (SDK 0.6.0+)

Agents can buy Weekend Cover on a basket the account holds, from the same key, once the owner has turned Cover on (coverEnabled). Cover pays the weekend drop between fromBps and toBps (300-1000: from -3%, in full at -10%). A session pays at most 3% of the cover (premium + fee), and cover counts against its own daily limit, apart from trades. Agents never underwrite.

import { coverActionState, coverRelayer, executeCoverOnce, type CoverPending } from "@bskts/sdk";
import { fileSessionStore } from "@bskts/sdk/node";

const book = await client.coverBook("DIGI64", { coverUsd: 1000, fromBps: 300, toBps: 1000 });
// book.ranges[0].quote.cost, and history.fairPer1k: what the range paid on an average weekend
const result = await executeCoverOnce({
  store: fileSessionStore<CoverPending>(".bskts-agent/cover-digi64-001.json"),
  sender: { key, sign: (td) => signer.signTypedData(td), submit: coverRelayer() },
  maxNetworkFeeUsdg: 250_000n,
  build: () =>
    client.buildAgentCover({
      owner,
      key,
      action: "take",
      ticker: "DIGI64",
      fromBps: 300,
      toBps: 1000,
      coverUsd: 1000,
    }),
  reconcile: (plan) => coverActionState(chain, plan.action),
});

buildAgentCover also takes action: "request" (with pricePer1k, locking the premium until an underwriter fills it) and action: "cancel" (with listingId, free). coverPositions(account) shows the cover held and its payouts, which the keeper makes automatically.

Delegated limit buys

Available since 0.7.2.

Use buildAgentOrder({ kind: "limit", owner, key, ticker, amountUsdg, triggerNav, slippageBps, expiryDays? }) for an unsigned resting buy. The full budget is escrowed and counts against today's session turnover. The trigger must be positive and uses ten-minute pool-average NAV at or below the limit. Expiry is capped to the session. slippageBps is required: an integer from 50 to 1000, at most the session's maxSlippageBps and at least the basket's limitMinSlippageBps from basket() or markets({ fields: [..., "limitMinSlippageBps"] }) (the bskts app uses 300). buyCostBps + 20 understates a yield basket, whose CLM reserve sets its minimum. Out-of-range values are refused with BAD_REQUEST before fetching; the API answers SESSION_LIMIT (403) above the grant and BAD_REQUEST naming the minimum below the basket's fill cost. Grants below 50 bps cannot use the limit escrow.

Use orders(session.account) to list open orders and buildAgentOrder({ kind: "cancel", owner, key, orderId }) to cancel an account-owned limit, including an expired one. Cancellation refunds the same account, uses no turnover and does not replenish the daily cap. It may require a network fee. Both plans work with executeSessionOnce and a durable store, just like market trades. Never rebuild a timed-out placement with a new ID.

decodeAgentOrder(plan) reads the signed terms; limitOrderId(receipt.logs) reads a placement ID from the pinned escrow's event without waiting for indexing. Creation is not a fill. The open-order list is not fill history. Revocation does not cancel existing orders; cancel before revoking or use the owner's wallet afterwards.

Delegated exits, buy-above limits and previews

Available since 0.8.0.

  • buildAgentOrder({ kind: "exit", owner, key, ticker, shares, stopNav?, tpNav?, slippageBps, expiryDays? }): an unsigned stop-loss (sells when the ten-minute pool-average NAV is at or below stopNav), take-profit (at or above tpNav) or bracket (both; stopNav < tpNav, one fill closes it). shares is a bigint of 1e18 units or "max" (every share the account holds outside orders). The shares are escrowed and count against today's turnover at their value (the higher of spot and ten-minute NAV). A stop at or above the current NAV, or a take-profit at or below it, is refused: it would sell at once. slippageBps is required: 50-1000, within the grant, at least the basket's exitMinSlippageBps. The network fee comes from the account's USDG.
  • buildAgentOrder({ kind: "cancel-exit", owner, key, orderId }) returns an exit's shares. Limit and exit IDs are numbered apart and can be equal: use the kind orders() reports. exitOrderId(receipt.logs) reads a new exit's ID.
  • triggerBelow: false on a limit buys at or above triggerNav instead of at or below it.
  • Every order plan has preview (AgentOrderPreview), computed by the API so the SDK, MCP and skill show the same numbers: navNow (the ten-minute NAV the triggers compare with), each trigger's fromNowPct; for an exit the account's avgEntry and, per edge, priceReturnPct, estNetReturnPct, worstNetReturnPct (after the 0.20% sell fee, the filler fee and maxSlippageBps, which includes the sell fee), proceeds and breakEvenNav; for a limit worstPricePerShare and fillsNow. describePreview(plan.preview) renders it as lines. Triggers are on NAV: a take-profit 5% over NAV is a 5% price move, not 5% net profit. The worst* figures are at the trigger, not bounds: the escrows price the floor from the NAV when the order fills, so a stop after a gap down sells for less and a buy-above after a gap up pays more; the preview's basis says so. Returns are NAV-based against avgEntry and leave out buy-side costs and network fees. Show the preview before signing.

版本与发布记录

@bskts/sdk 共发布 16 个版本,首次发布于 2026/09/30;过去 12 个月发布了 16 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.9.2latest2026/11/036 天前
0.9.12026/10/2713 天前
0.9.02026/10/2713 天前
0.8.12026/10/2713 天前
0.8.02026/10/2614 天前
0.7.22026/10/2416 天前

关于 @bskts/sdk 的常见问题

@bskts/sdk 是什么?

Typed, non-custodial client for the bskts API: tokenized index baskets on Robinhood Chain. Reads, unsigned transaction builders, dry runs and guard rails for wallets and agents.

如何安装 @bskts/sdk?

在项目目录运行 npm install @bskts/sdk,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:viem(^2.55.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@bskts/sdk 支持 TypeScript 吗?

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

@bskts/sdk 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@bskts/sdk 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20.19。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@bskts/sdk 可以商用吗?

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

@bskts/sdk 的下载量有多少?

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

@bskts/sdk 还在维护吗?

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