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:
"dependencies": { "@aqueduct/sdk": "workspace:*" }It brings @anchor-lang/core, @solana/web3.js (v1) and @solana/spl-token with it.
| Import | Contents |
|---|---|
@aqueduct/sdk | AqueductClient, PDA helpers, constants, deployments, and everything in the registry |
@aqueduct/sdk/registry | Tiers, strategies and the fee schedule, without Anchor or web3.js. Use it where bundle size matters |
@aqueduct/sdk/admin | Deployment setup and fee sync helpers used by the admin scripts |
@aqueduct/sdk/deployments/devnet.json | Raw 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.
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`);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 USDCEvery 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#
| Field | Meaning |
|---|---|
tier, name, address | bedrock or rapids, its display name and the vault account |
totalAssets, idle | USDC in the vault, and the part sitting in the idle account |
sharePrice, totalShares | USDC per share, and shares outstanding including pending fee shares |
apy, grossApy | Allocation-weighted APY of the strategies, after and before the performance fee |
perfFeeBps, feeBps, highWaterMark | Fee settings and the share price the performance fee was last charged at |
depositCap | Maximum total assets, USDC; 0 means no cap |
strategies | Per strategy: id, name, venue, chain, riskScore, maxAllocBps, enabled, allocated, apy, weight, reserve |
lifetimeDeposits, lifetimeWithdrawals | Running totals, USDC |
Deposit and withdraw#
From a script with a keypair:
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:
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#
| Member | Returns |
|---|---|
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) / faucetIx | Devnet 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 |
program | The 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.
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.4It 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:
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:
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.
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.
| Command | What it does |
|---|---|
pnpm --filter @aqueduct/sdk setup | Creates the test mint, config, both vaults, every reserve and strategy, then seeds starter deposits. Idempotent |
pnpm --filter @aqueduct/sdk sync-fees | Sets each vault's fees to the registry schedule, migrating old vault layouts first |
pnpm --filter @aqueduct/sdk set-treasury | Points the config at a new treasury |
pnpm --filter @aqueduct/sdk sync-idl | Copies the IDL and types from an anchor build into the SDK |
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.