Skip to main content
import { GauntletClient, getUserCurrentBalance } from '@gauntlet-xyz/sdk'
import { getDepositTx, getWithdrawTx, VaultId } from '@gauntlet-xyz/sdk/evm'
import { createPublicClient, createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { base } from 'viem/chains'


const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)

const walletClient = createWalletClient({
  account,
  chain: base,
  transport: http(process.env.RPC_URL_BASE!),
})

const client = new GauntletClient({
  evmClients: {
    [base.id]: createPublicClient({ chain: base, transport: http(process.env.RPC_URL_BASE!) }),
  },
  wallet: walletClient,
  builderCode: 'your-builder-code', // request from Gauntlet — must be registered with the indexer for attribution to be counted
})

const steps = await getDepositTx(client, {
  vaultId: VaultId.AeraUsdAlpha,
  amount: 1_000_000n, // 1 USDC (6 decimals)
})

Two Ways to Submit

Each step in steps exposes two surfaces. Pick the one that fits your stack:

step.payload — scripts and embedded wallets

Attribution is pre-baked into payload.data. Because the calldata is fully formed, you can simulate the exact bytes that will hit the chain before spending gas, then wait for each receipt before moving to the next step.
import { createPublicClient, http } from 'viem'
import { base } from 'viem/chains'

const publicClient = createPublicClient({ chain: base, transport: http(process.env.RPC_URL_BASE!) })

// steps may be [approve, requestDeposit] — execute in order
for (const step of steps) {
  // Estimate gas before sending — simulates the exact calldata that will be broadcast.
  // Throws if the call would revert (e.g. insufficient balance, wrong allowance) so you
  // catch failures before spending gas. Returns the gas units needed for the tx.
  const gas = await publicClient.estimateGas({
    to: step.payload.to,
    data: step.payload.data,
    account: step.payload.account,
  })

  // Send with the estimated gas limit — prevents out-of-gas failures on-chain.
  const { type: txType, account: _ignoredAccount, ...txPayload } = step.payload
  const hash = await walletClient.sendTransaction({ ...txPayload, account, gas })

  // Wait for the receipt before proceeding to the next step.
  // Each step depends on the previous one being confirmed on-chain — the deposit will
  // revert if the approval hasn't landed yet.
  const receipt = await publicClient.waitForTransactionReceipt({ hash, confirmations: 2 })

  if (receipt.status !== 'success') {
    throw new Error(`Transaction reverted: ${step.payload.type}`)
  }
}

step.tx — browser wallets (wagmi)

Use the structured ABI fields with writeContract. You must pass step.tx.attribution as dataSuffix — without it the transaction goes through but volume is not attributed.
// wagmi — MetaMask, Coinbase Wallet, WalletConnect, etc.
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
  })
}

Check Balance

// balance will be in pendingDeposit until the vault solver settles it (~2 hours)
const balances = await getUserCurrentBalance(client, {
  vaultId: VaultId.AeraUsdAlpha,
  address: '0xUser',
})

console.log(balances)

Withdraw

const withdrawSteps = await getWithdrawTx(client, {
  vaultId: VaultId.AeraUsdAlpha,
  entireAmount: true,
})

// same two paths apply — use whichever matches your stack
for (const step of withdrawSteps) {
  const gas = await publicClient.estimateGas({
    to: step.payload.to,
    data: step.payload.data,
    account: step.payload.account,
  })

  const hash = await walletClient.sendTransaction({ ...step.payload, gas })

  const receipt = await publicClient.waitForTransactionReceipt({ hash, confirmations: 2 })

  if (receipt.status !== 'success') {
    throw new Error(`Transaction reverted: ${step.payload.type}`)
  }
}