Skip to content
Open app

TypeScript SDK

@aqueduct/sdk reads both vaults, values positions and builds deposit, withdraw, faucet and keeper transactions for the Aqueduct program. The keeper and the web app both use it.

On this page

Install#

The SDK is a private workspace package in the Aqueduct monorepo, not published to npm. Its sources are consumed directly as TypeScript, so any runtime or bundler that understands TS (Next.js, tsx, Vite) can import it without a build step. Inside the monorepo:

package.json
"dependencies": { "@aqueduct/sdk": "workspace:*" }

It brings @anchor-lang/core, @solana/web3.js (v1) and @solana/spl-token with it.

ImportContents
@aqueduct/sdkAqueductClient, PDA helpers, constants, deployments, and everything in the registry
@aqueduct/sdk/registryTiers, strategies and the fee schedule, without Anchor or web3.js. Use it where bundle size matters
@aqueduct/sdk/adminDeployment setup and fee sync helpers used by the admin scripts
@aqueduct/sdk/deployments/devnet.jsonRaw devnet addresses

Read the vaults#

A client needs a connection and a deployment. Without a wallet it is read-only; every builder below returns an unsigned transaction either way.

read.ts
import { AqueductClient, DEPLOYMENTS } from "@aqueduct/sdk";
import { Connection, PublicKey } from "@solana/web3.js";

const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const client = new AqueductClient(connection, DEPLOYMENTS.devnet!);

for (const v of await client.fetchVaults()) {
  console.log(`${v.name}: ${v.totalAssets.toFixed(2)} USDC, ${v.idle.toFixed(2)} idle`);
  console.log(`  share price ${v.sharePrice.toFixed(6)}, net APY ${v.apy.toFixed(2)}% (gross ${v.grossApy.toFixed(2)}%)`);
  for (const s of v.strategies) {
    console.log(`  #${s.index} ${s.name} on ${s.chain}: ${(s.weight * 100).toFixed(1)}% of the vault at ${s.apy}% (cap ${s.maxAllocBps / 100}%)`);
  }
}

// the devnet admin, which holds the seed deposits
const owner = new PublicKey("DDVtiUetxxks7LfFUxngLj1jD5huPxmJ9urNCVUKQpvq");
const usdc = await client.fetchAssetBalance(owner);
const positions = await client.fetchPositions(owner);
console.log(`wallet holds ${usdc} test USDC`);
for (const p of positions) console.log(`${p.tier}: ${p.shares} shares worth ${p.value.toFixed(2)} USDC`);
Output on 8 October 2026
Bedrock: 250066.39 USDC, 12502.95 idle
  share price 1.000251, net APY 3.81% (gross 4.01%)
  #0 Kamino Lend on solana: 20.0% of the vault at 4.34% (cap 40%)
  #1 Jupiter Lend on solana: 35.0% of the vault at 4.1% (cap 40%)
  …
Rapids: 100058.14 USDC, 25043.62 idle
  …
wallet holds 0 test USDC
bedrock: 250000.007372 shares worth 250062.67 USDC
rapids: 100000.00292 shares worth 100052.12 USDC

Every number in a VaultView is in UI units (USDC and shares as decimals, APY in percent, caps in basis points). Reserve interest and the pending performance fee are projected to the moment of the fetch, so sharePrice is exactly what a deposit or withdrawal would get right now.

VaultView#

FieldMeaning
tier, name, addressbedrock or rapids, its display name and the vault account
totalAssets, idleUSDC in the vault, and the part sitting in the idle account
sharePrice, totalSharesUSDC per share, and shares outstanding including pending fee shares
apy, grossApyAllocation-weighted APY of the strategies, after and before the performance fee
perfFeeBps, feeBps, highWaterMarkFee settings and the share price the performance fee was last charged at
depositCapMaximum total assets, USDC; 0 means no cap
strategiesPer strategy: id, name, venue, chain, riskScore, maxAllocBps, enabled, allocated, apy, weight, reserve
lifetimeDeposits, lifetimeWithdrawalsRunning totals, USDC

Deposit and withdraw#

From a script with a keypair:

deposit.ts
import { AqueductClient, DEPLOYMENTS } from "@aqueduct/sdk";
import { Connection, Keypair, sendAndConfirmTransaction } from "@solana/web3.js";

const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const client = new AqueductClient(connection, DEPLOYMENTS.devnet!);
const user = Keypair.generate(); // fund with devnet SOL before running

// 1. mint 1,000 test USDC (devnet only, at most 10,000 per call)
await sendAndConfirmTransaction(connection, await client.buildFaucetTx(user.publicKey, 1_000), [user]);

// 2. deposit 250 USDC into Bedrock
const vault = client.vaultAddress("bedrock");
await sendAndConfirmTransaction(connection, await client.buildDepositTx(user.publicKey, vault, 250), [user]);

// 3. withdraw every share (Infinity = the whole balance, read on-chain)
await sendAndConfirmTransaction(connection, await client.buildWithdrawTx(user.publicKey, vault, Infinity), [user]);

In a browser, hand the transaction to the connected wallet instead. Builders set a compute budget, the fee payer and a recent blockhash, so the wallet only has to sign:

with a wallet
const client = new AqueductClient(connection, DEPLOYMENTS.devnet!);
const tx = await client.buildDepositTx(wallet.publicKey, client.vaultAddress("rapids"), 100);
const signature = await wallet.sendTransaction(tx, connection);
await connection.confirmTransaction(
  { signature, blockhash: tx.recentBlockhash!, lastValidBlockHeight: (await connection.getLatestBlockhash()).lastValidBlockHeight },
  "confirmed",
);

Amounts are in USDC for deposits and in shares for withdrawals. buildWithdrawTx with Infinity reads your share balance on-chain and burns all of it, so no dust is left behind. The program creates the user's USDC and share token accounts if they do not exist.

Deposit and withdraw instructions need every strategy of the vault as remaining accounts, in order. The builders fetch and append them for you with strategyMetas(vault); if you build instructions by hand, do the same or the program rejects the transaction with StrategyAccountsMismatch.

Client reference#

MemberReturns
new AqueductClient(connection, deployment, provider?)A client bound to the deployment's program ID. Pass an AnchorProvider to sign through Anchor
vaultAddress(tier) / tierOf(vault)A tier's vault PublicKey, and the reverse
fetchVault(tier) / fetchVaults()VaultView for one or both tiers
fetchPositions(owner, vaults?)Shares and USDC value per tier for a wallet
fetchAssetBalance(owner)The wallet's test USDC balance
buildDepositTx(owner, vault, amountUi)Unsigned deposit transaction
buildWithdrawTx(owner, vault, sharesUi)Unsigned withdraw transaction; Infinity burns every share
buildFaucetTx(owner, amountUi) / faucetIxDevnet faucet, up to 10,000 USDC per call
strategyMetas(vault)The [strategy, reserve, reserve token] account triplets every vault instruction needs
keeperMoveIx(kind, keeper, vault, index, amount)allocate or deallocate instruction (keeper only; amount in base units)
collectFeesIxs(payer, tier)Permissionless fee sweep, creating the treasury's share account if needed
setReserveApyIx(keeper, reserve, apyBps)Devnet APY mirror (keeper only)
txFor(payer, ixs, cu?)Wrap instructions into a transaction with a compute budget and blockhash
programThe underlying Anchor Program, for anything not wrapped, such as program.account.vault.fetch

Registry#

The registry is the single source of truth for tiers and strategies. The setup script creates one on-chain strategy per entry, the keeper prices them, and the app and these docs label them.

TypeScript
import { PERF_FEE_BPS, STRATEGIES, TIERS, netApy, strategiesFor } from "@aqueduct/sdk/registry";

for (const s of strategiesFor("rapids")) console.log(s.index, s.id, s.name, s.chain, s.riskScore, s.maxAllocBps);
console.log(TIERS.bedrock.targetApy, STRATEGIES.length);
console.log(netApy(6, PERF_FEE_BPS.rapids)); // 5.4

It also exports MGMT_FEE_BPS, IDLE_BUFFER_BPS, TIER_INDEX and the Tier, Chain and StrategySpec types. The tables on Tiers and venues are generated from it.

Units and PDAs#

The program works in integer base units; USDC and every share mint have 6 decimals (DECIMALS, UNIT = 1_000_000). Builders and views take and return UI numbers. Convert with toBaseUnits(1.5) (returns 1_500_000n) and fromBaseUnits(1_500_000n) (returns 1.5). Reserve exchange rates are fixed point, scaled by RATE_SCALE = 1018.

Every program account address can be derived without a network call:

TypeScript
import { DEPLOYMENTS, TIER_INDEX, idlePda, sharesPda, strategyPda, vaultPda } from "@aqueduct/sdk";
import { PublicKey } from "@solana/web3.js";

const devnet = DEPLOYMENTS.devnet!;
const programId = new PublicKey(devnet.programId);
const mint = new PublicKey(devnet.assetMint);
const vault = vaultPda(mint, TIER_INDEX.bedrock, programId); // equals devnet.vaults.bedrock
const shareMint = sharesPda(vault, programId);
const idle = idlePda(vault, programId);
const firstStrategy = strategyPda(vault, 0, programId);

The full list of seeds is in the program reference.

Errors#

Program errors arrive as custom program error: 0x… in the transaction logs. The client's IDL maps the code to a name and a message:

TypeScript
try {
  await sendAndConfirmTransaction(connection, tx, [user]);
} catch (err) {
  const hex = /custom program error: 0x([0-9a-f]+)/i.exec(String(err))?.[1];
  const known = hex ? client.program.idl.errors?.find((e) => e.code === parseInt(hex, 16)) : undefined;
  console.log(known?.name, known?.msg); // e.g. DepositCapExceeded "Deposit would exceed the vault cap"
}

The full table is under Errors in the program reference.

Coming with the next SDK release

The next SDK release adds @aqueduct/sdk/errors (decodeError and isAqueductError, with friendly messages for wallet, RPC and program errors), @aqueduct/sdk/math (share, fee and unit math in bigint, including exact parsing of typed amounts), and read helpers such as previewDeposit, previewWithdraw, fetchPosition and getDeployment. Everything on this page keeps working unchanged.

Admin scripts#

The SDK package also carries the scripts that create and maintain a deployment. They need the admin keypair.

CommandWhat it does
pnpm --filter @aqueduct/sdk setupCreates the test mint, config, both vaults, every reserve and strategy, then seeds starter deposits. Idempotent
pnpm --filter @aqueduct/sdk sync-feesSets each vault's fees to the registry schedule, migrating old vault layouts first
pnpm --filter @aqueduct/sdk set-treasuryPoints the config at a new treasury
pnpm --filter @aqueduct/sdk sync-idlCopies the IDL and types from an anchor build into the SDK
Written against packages/sdk/src/client.ts, packages/sdk/src/index.ts, packages/sdk/src/registry.ts and packages/sdk/package.json. If the code and this page disagree, the code wins.