@note-systems/sdk

One call in. Six things happen before it reaches the chain.

A typed TypeScript client over viem. Reads batch through Multicall3, writes simulate before they sign, gas is estimated with the vault's fee accrual forced to run, and every result is read from the contract's own events. Every revert comes back with its name.

  1. TypedFunction name and arguments are checked against the contract ABI when your code compiles.0 RPC calls
  2. Chain agreementThe RPC, your wallet and the configured chain must agree, or nothing is sent.ChainMismatchError
  3. SimulatedAn eth_call against the latest block. A revert is decoded by name and never signed.reverts named
  4. Estimated twicePlainly, and with the fee accrual forced to run. The larger figure carries a 30% margin and a 120,000 floor.max(plain, accrual)
  5. Sent and awaitedSigned by your wallet client, then the receipt. A revert after inclusion is replayed against the prior block to recover its name.1.8 s on testnet
  6. DecodedShares and the attribution flag come from the vault's own events, filtered by emitter.{ shares, tagged }

01 / Install

One package, one peer

npm install viem https://note.systems/sdk/note-systems-sdk-0.1.1.tgz
  • Node 20 or later. viem 2.21 or later as a peer, nothing else at runtime.
  • Releases are served from note.systems/sdk/: latest.json names the version, tarball and sha512, and each tarball sits next to its .sha512 file. npm pins the integrity in your lockfile.
  • ESM with full TypeScript types. ABIs exported as const so calls are typed end to end.
  • The vault creation code the factory pins ships in the package; factory.codeMatches() compares hashes before you deploy.

02 / Reads

Batched, bounded, verified

  • summary, holding, exposure and verifyBinding batch through Multicall3: one round trip each.
  • The factory registry is the source of the mandate and adapter for a listed vault; summary refuses an unlisted address with UnlistedVaultError.
  • Every list is paged: factory.list(start, count), requests(owner, start, count). Nothing grows without bound and no indexer is required.

03 / Writes

Simulate, estimate, send, decode

  • Every write returns { hash, receipt, gasUsed, result }. The result is parsed from the receipt's events, filtered to the contract you called.
  • A transaction that reverts after inclusion throws TransactionRevertedError with the hash, the receipt, an outOfGas flag and the revert name recovered by replay.
  • Writes are typed against the ABI: a wrong function name or argument shape does not compile.

04 / Errors

Named, from every layer

  • NoteError.decoded carries { name, args, message } for errors from the vault, the mandate, the adapter, NoteCore and RollPolicy, including errors bubbled through factory.create.
  • ValidationError names the field path that failed, before any RPC call.
  • RPC URLs are redacted from every message, so logs never leak a provider key.

05 / Safety

Bounds checked before you sign

  • verifyBinding confirms the factory lists the vault and that registry, vault, mandate and adapter all point at each other.
  • MANDATE_BOUNDS mirror the contract: up to 16 underlyings and 32 partners, management fee to 2%, performance fee to 30%, partner share to 50%, exit lock 1 hour to 7 days, mandate delay 1 to 30 days.
  • Repeated partner ids, a zero payee, or a payee equal to the vault are refused client side with the field named.

06 / Gas

Estimated with the accrual forced

Every vault write accrues the management fee once per block. An estimate taken against a block that has already accrued comes out low; the mined transaction, a block later, accrues and would run out of gas. The SDK estimates a second time with lastAccrual overridden and keeps the larger figure.

accrual forced · limit sent 234,260 · measured on testnet

Quick start

From install to a tagged deposit

  1. ClientsA viem public client for reads and a wallet client for writes. The SDK never holds a key.
  2. DiscoverThe first listed vault, then verifyBinding before anything is trusted.
  3. Quotesummary for the mandate and previewDeposit for the share count, both before signing.
  4. DepositApprove, then deposit with your partner id. The id follows the shares; tagged tells you it stuck.
  5. HandleCatch NoteError and branch on decoded.name.
import { createPublicClient, createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { NoteError, createNoteClient, partnerId, robinhoodTestnet } from '@note-systems/sdk';

// 1. clients
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const publicClient = createPublicClient({ chain: robinhoodTestnet, transport: http() });
const walletClient = createWalletClient({ chain: robinhoodTestnet, transport: http(), account });
const note = createNoteClient({ publicClient, walletClient });

// 2. discover and verify
const [vault] = await note.factory.list(0n, 20n);
if (!vault || !(await note.vaults.verifyBinding(vault))) throw new Error('no bound vault');

// 3. quote
const summary = await note.vaults.summary(vault);
const assets = 100_000_000n;                          // 100 USDG, six decimals
const quoted = await note.vaults.previewDeposit(vault, assets);

// 4. deposit with attribution
try {
  await note.vaults.approve(vault, assets);
  const tx = await note.vaults.deposit({ vault, assets, receiver: account.address, partnerId: partnerId('wallet.example') });
  console.log(summary.name, quoted, tx.result.shares, tx.result.tagged, tx.gasUsed);
} catch (e) {
  // 5. handle by name
  if (e instanceof NoteError && e.decoded?.name === 'Locked') { /* show the unlock time */ }
  else throw e;
}
Measured on Robinhood Chain testnet, 27 September 2026. Gas is the receipt's gasUsed; time is wall clock from call to decoded result over the public RPC, including receipt polling.
OperationGas usedTime
factory.create vault, mandate, adapter, 1 partner, 8 bands13,039,7321.9 s
deposit tagged, first into a fresh vault337,8511.8 s
mint243,1641.8 s
allocate open a position in a series684,3161.3 s
withdraw instant path300,9501.4 s
requestRedeem partial fill at request435,0521.3 s
claimRedeem60,3141.8 s
exitInKind424,1771.3 s
accrueFees accrual minted, 2 positions175,8693.1 s
Reads 67 calls, mediannone170 ms
  • 0.1.1package version
  • 69tests in 8 files, every ABI hash pinned
  • 43files packed, 81 kB
  • 0dependency advisories
  • 113live steps on testnet, 23 refusals decoded by name