Skip to main content

Constructor

import { GauntletClient } from '@gauntlet-xyz/sdk'
import { createPublicClient, createWalletClient, http } from 'viem'
import { base } from 'viem/chains'

const client = new GauntletClient({
  evmClients: {
    [base.id]: createPublicClient({ chain: base, transport: http(process.env.RPC_URL_BASE!) }),
  },
  wallet: createWalletClient({
    account,
    chain: base,
    transport: http(process.env.RPC_URL_BASE!),
  }),
})
ParameterTypeRequiredDescription
evmClientsRecord<number | string, PublicClient>For transaction methodsChain ID to viem PublicClient map
walletWalletClientFor transaction methodsAny viem-compatible WalletClient — used only to read the sender address. The SDK never signs.
apiKeystringNoPartner API key from the Developer Portal
attributionModeAttributionModeNoDefaults to AttributionMode.PUBLIC
builderCodestringNoBuilder identifier for attribution — must be requested from Gauntlet, not self-serve. NOT the same as API key. Without it, transactions are unattributed.

Discover Vaults

import { getVaults } from '@gauntlet-xyz/sdk/evm'
// also available from the root: import { getVaults } from '@gauntlet-xyz/sdk'
import { base } from 'viem/chains'

const candidates = await getVaults(client, { chainId: base.id })
// returns:
// [
//   {
//     vaultId: "baseUsdcPrime",
//     name: "...",
//     protocol: "morpho",
//     deployments: [{ chainId: 8453, supplyToken: [{ symbol: "USDC", ... }], ... }]
//   },
//   ...
// ]

Transaction Functions

Import from @gauntlet-xyz/sdk/evm. These communicate on-chain via your RPC URLs. Require wallet in the client config — the SDK reads the account address from wallet.account.

getDepositTx

The vaultId string resolves to a VaultDeployment from the manifest. This is how the SDK knows which token to approve (supplyToken[0].address), which contract to call (vaultAddress or provisionerAddress), and whether the vault supports sync or async deposits.
import { getDepositTx, VaultId } from '@gauntlet-xyz/sdk/evm'
// also available from the root: import { getDepositTx, VaultId } from '@gauntlet-xyz/sdk'

const steps = await getDepositTx(client, {
  vaultId: VaultId.AeraUsdAlpha,
  amount: 1_000_000n,
  receiver: '0xReceiver', // optional, defaults to wallet account
})
ParameterTypeRequiredDescription
vaultIdstringYesVault identifier — resolves token, contract, and deposit mode from the manifest
amountbigintYesAmount in token base units
chainIdnumberFor multi-chain vaultsDefaults to Base
assetSymbolstringNoRequired for multi-asset vaults to select the asset token
depositModestringNoOverride deposit mode: 'async' (queued) or 'sync' (instant). When omitted, uses the vault’s native mode — async for Aera, sync for Morpho. Vaults with depositMode: 'both' default to async.
receiverAddressNoDefaults to wallet.account. Aera vaults require receiver to equal the signer — providing a different address throws an error.
slippageBpsnumberNoSlippage tolerance in basis points (e.g. 100 = 1%). Defaults to 100. Must be an integer between 0 and 10000.

getWithdrawTx

import { getWithdrawTx, VaultId } from '@gauntlet-xyz/sdk/evm'
// also available from the root: import { getWithdrawTx, VaultId } from '@gauntlet-xyz/sdk'

// Withdraw all shares
const steps = await getWithdrawTx(client, {
  vaultId: VaultId.AeraUsdAlpha,
  entireAmount: true,
  receiver: '0xReceiver', // optional
})

// Withdraw by shares
const steps = await getWithdrawTx(client, {
  vaultId: VaultId.AeraUsdAlpha,
  shares: 500_000000000000000000n,
})

// Withdraw by asset amount
const steps = await getWithdrawTx(client, {
  vaultId: VaultId.AeraUsdAlpha,
  amount: 1_000_000n,
})
ParameterTypeRequiredDescription
vaultIdstringYesVault identifier
shares | amount | entireAmountYes (one of)Exact shares, exact asset amount, or full position
chainIdnumberFor multi-chain vaultsDefaults to Base
assetSymbolstringNoRequired for multi-asset vaults to select the withdraw token
depositModestringNoOverride withdraw mode: 'async' (queued) or 'sync' (instant). When omitted, uses the vault’s native mode — async for Aera, sync for Morpho. Vaults with depositMode: 'both' default to async.
receiverAddressNoDefaults to wallet.account. Aera vaults require receiver to equal the signer — providing a different address throws an error.
slippageBpsnumberNoSlippage tolerance in basis points (e.g. 100 = 1%). Defaults to 100. Must be an integer between 0 and 10000.

User Vault Balance

Balance lifecycle

An async deposit or withdrawal passes through a pending state while the vault solver queues and processes the operation:
  • pendingDeposit — funds are locked in the provisioner contract. They are not yet earning yield. Once the solver settles the request, they move to balance and begin earning.
  • pendingWithdraw — vault shares have been redeemed but the underlying assets have not yet been transferred. They are no longer earning yield. Once the solver settles the request, they arrive as ERC-20 tokens in the receiver wallet.
The solver typically processes requests within 2 hours; the maximum window is 12 hours. Funds are safe in both pending states — the delay is operational, not a risk. How an amount moves through the three states depends on whether the user chose sync or async:
PathWhere the balance landsNotes
Async depositpendingDeposit for ~2–12 hours (usually ~2), then moves to balanceNot earning during pending; best execution price once settled
Sync depositDirectly into balanceSlightly worse price; no waiting
Sync withdrawRemoved from balance immediately; claimable as ERC-20 in the receiver walletSlightly worse price; no waiting
Async withdrawMoves from balance to pendingWithdraw for ~2–12 hours (usually ~2), then claimable as ERC-20 in the receiver walletNo longer earning during pending; best execution price once settled

getUserCurrentBalance

import { getUserCurrentBalance, VaultId } from '@gauntlet-xyz/sdk'
// VaultId also available from: import { VaultId } from '@gauntlet-xyz/sdk/evm'

const balance = await getUserCurrentBalance(client, {
  vaultId: VaultId.AeraUsdAlpha,
  address: '0xUser',
})
ParameterTypeRequiredDescription
vaultIdstringYesMust resolve to an Aera multi-depositor vault — throws UnsupportedProtocolError otherwise
addressAddressYesAccount to query
chainIdnumberNoWhen omitted, returns one entry per chain the vault is deployed on. When provided, returns only that chain — throws ChainMismatchError if not deployed there.
Returns Promise<UserCurrentBalance[]> — one entry per chain the vault is deployed on:
type UserCurrentBalance = {
  chain: string           // chain identifier, e.g. "base" — included for non-EVM compatibility
  token: Address          // token address
  decimals: number        // token decimals
  pendingDeposit: bigint  // assets locked in provisioner after async deposit — not yet earning yield; 0n if none
  balance: bigint         // assets actively earning in the vault; 0n if no position
  pendingWithdraw: bigint // assets redeemed but not yet claimable after async withdraw — no longer earning; 0n if none
}
All numeric fields are always present. If the account has no position on a given chain, all three bigint fields are 0n — this is not an error.

Result Shape

Both getDepositTx and getWithdrawTx return Promise<PreparedTx[]>.
type PreparedTx = {
  payload: {
    type: string    // 'approve' | 'deposit' | 'requestDeposit' | 'redeem' | 'requestRedeem' | 'withdraw'
    to: Address     // contract to call
    data: Hex       // ABI-encoded calldata with attribution suffix already concatenated
    account?: Address
  }
  tx: EvmTxStep    // structured ABI fields + raw attribution bytes — use with writeContract
}

type EvmTxStep = {
  type: 'approve' | 'deposit' | 'requestDeposit' | 'redeem' | 'requestRedeem' | 'withdraw'
  address: Address        // contract to call
  abi: Abi                // ABI fragment for this call
  functionName: string
  args: readonly unknown[]
  account: Address        // sender address
  attribution?: Hex       // raw attribution bytes — must pass as dataSuffix to writeContract
}
Steps must be executed in order. An approve step, when present, always comes first. Each step exposes two submission paths with different trade-offs:

Path 1 — step.payload + sendTransaction

Use for: backend scripts, embedded wallets (Privy, Dynamic), server-side signing, EVM pre-simulation (eth_call on the exact bytes to be broadcast). Attribution is pre-concatenated into payload.data — it cannot be lost regardless of wallet or provider.
for (const step of steps) {
  await walletClient.sendTransaction(step.payload)
}

Path 2 — step.tx + writeContract

Use for: browser wallets via wagmi (MetaMask, Coinbase Wallet, WalletConnect), or when you need wagmi simulation hooks. Pass step.tx.attribution as dataSuffix — wagmi appends it to the ABI-encoded calldata before sending. If dataSuffix is omitted or the EIP-1193 provider strips it, the transaction succeeds but volume is not attributed.
for (const step of steps) {
  await walletClient.writeContract({
    address: step.tx.address,
    abi: step.tx.abi,
    functionName: step.tx.functionName,
    args: step.tx.args,
    account: step.tx.account,
    dataSuffix: step.tx.attribution, // required — omitting silently drops attribution
  })
}

Attribution

How ERC-8021 builder codes work, why dataSuffix matters, and how to verify attribution is tracked.

Types

VaultInfo

type VaultInfo = {
  vaultId: string
  name: string
  protocol: 'aera' | 'morpho'
  strategy: string
  deployments: VaultDeployment[]
}

VaultDeployment

The object vaultId resolves to. Carries all metadata the SDK needs to construct deposit and withdraw transactions — you never supply these directly.
type VaultDeployment = {
  chain: 'evm'
  chainId: number
  vaultAddress: Address           // ERC4626 vault contract
  provisionerAddress?: Address    // multi-depositor vaults: deposit routes through this instead
  vaultType: 'single-depositor' | 'multi-depositor'
  depositMode: 'sync' | 'async' | 'both'  // validates the depositMode param in getDepositTx / getWithdrawTx
  supplyToken: TokenInfo[]        // tokens accepted by this vault; provides address and decimals
}

TokenInfo

type TokenInfo = {
  symbol: string
  address: Address
  decimals: number
}

VaultFilter

type VaultFilter = {
  chainId?: number
  protocol?: string
}

AttributionMode

enum AttributionMode {
  PUBLIC  = 'public',
  ENCODED = 'encoded',  // not yet implemented
  PRIVATE = 'private',  // not yet implemented
}

Errors

All errors extend GauntletSDKError, which extends Error.
ErrorDescription
VaultNotFoundErrorVault ID doesn’t exist or isn’t deployed on the requested chain. Has .vaultId and optional .chainId properties.
UnsupportedAssetErrorToken not accepted by this vault. Has .asset and .vaultId properties.
ChainMismatchErrorChain parameter doesn’t match vault deployment. Has .expected and .received properties.
UnsupportedDepositModeErrorRequested sync/async mode not supported by this vault. Has .vaultId, .requested, and .available properties.
RpcNotConfiguredErrorNo evmClients entry provided for the required chain ID. Has .chainId property.
AccountRequiredErrorNo wallet provided in client config.
UnsupportedProtocolErrorVault protocol is not supported by this method (e.g. getUserCurrentBalance only supports Aera multi-depositor). Has .protocol property.
InvalidWithdrawParamsErrorgetWithdrawTx called without exactly one of shares, amount, or entireAmount.
InvalidSlippageBPSErrorslippageBps is not an integer in the range 0–10000. Has .slippage property.
UnimplementedFeatureErrorFeature exists in the API but is not yet implemented (e.g. AttributionMode.ENCODED). Has .feature property.
UnsupportedFeatureErrorFeature is not supported at all (distinct from unimplemented). Has .feature property.
UnitConversionErrorFailed to convert token units for a vault — fee calculator unavailable on-chain. Has .vaultAddress property.

Go Deeper

Examples

End-to-end code for deposits, withdrawals, and error handling.

API Reference

Use the raw API directly if you need more control than the SDK provides.