# Gauntlet SDK — CONTEXT.md
## Package
- Name: `@gauntlet-xyz/sdk`
- Install: `npm install @gauntlet-xyz/sdk` (peer dep: `viem >=2.0.0`)
- Import paths:
- `@gauntlet-xyz/sdk` — root: GauntletClient, AttributionMode, all functions, types, errors
- `@gauntlet-xyz/sdk/evm` — EVM subpath: tx-building functions + types (preferred for tree-shaking)
- `@gauntlet-xyz/sdk/api` — Gaia REST API client (`GauntletApi`) + data helpers, usable standalone
- `@gauntlet-xyz/sdk/privy` — Privy wallet integration helpers
---
## GauntletClient
Main entry point. Pass one instance to every SDK function.
```typescript
import { GauntletClient, AttributionMode } from '@gauntlet-xyz/sdk'
const client = new GauntletClient({
evmClients: { [chainId]: publicClient }, // viem PublicClient per chain
wallet: walletClient, // viem WalletClient — SDK reads account address only, never signs
apiKey: '<YOUR_API_KEY>', // optional — sent as x-api-key on client.api requests
apiUrl: 'https://api.gauntlet.xyz', // optional — override Gaia API origin (relative path OK in browser)
builderCode: 'your-code', // optional — MUST be issued by Gauntlet, not self-serve
attributionMode: AttributionMode.PUBLIC, // optional — default PUBLIC
})
client.api // typed Gaia REST API client — see "Gaia API" section
```
| Param | Type | Required | Notes |
|-------|------|----------|-------|
| `evmClients` | `Record<number\|string, PublicClient>` | For tx methods | chainId → viem PublicClient |
| `wallet` | `WalletClient` | For tx methods | Only reads `wallet.account.address` |
| `apiKey` | `string` | No | Gaia API key, sent as `x-api-key`. Anonymous access is rate-limited. |
| `apiUrl` | `string` | No | Gaia API origin override. Defaults to `https://api.gauntlet.xyz`. In a browser it may be a relative path (e.g. a Next.js rewrite like `/gauntlet-api`). |
| `builderCode` | `string` | No | Must be issued by Gauntlet. Without it, transactions are unattributed. |
| `attributionMode` | `AttributionMode` | No | `PUBLIC` (default). `ENCODED`/`PRIVATE` throw `UnimplementedFeatureError`. |
Other members: `client.manifest` (Promise of the bundled vault manifest), `client.setManifest(manifest)` (override it), `client.getPublicClient(chainId?)` (defaults to Base; throws `RpcNotConfiguredError`).
---
## EVM Functions
### getDepositTx(client, params) → Promise<PreparedTx[]>
Builds ordered EVM transactions to deposit into a vault. Returns 1–2 steps: optional ERC-20 `approve` (only if allowance insufficient) + `deposit` or `requestDeposit`.
```typescript
import { getDepositTx, VaultId } from '@gauntlet-xyz/sdk/evm'
const steps = await getDepositTx(client, {
vaultId: VaultId.AeraUsdAlpha, // or any VaultId value or raw string
amount: 1_000_000n, // token base units
chainId: 8453, // optional; defaults to Base (8453) for multi-chain vaults
assetSymbol: 'USDC', // optional; required for multi-asset vaults
depositMode: 'async', // optional; 'async' | 'sync'. Availability read from live vault config.
receiver: '0x...', // optional; defaults to wallet.account. Aera V1: MUST equal signer.
slippageBps: 100, // optional; integer 0–10000. Default 100 (1%).
solverTip: 0n, // optional; tip for async Aera provisioner requests. Default 0.
maxPriceAge: 864_000n, // optional; max price age (seconds) for async Aera requests. Default 10 days.
})
```
Throws: `VaultNotFoundError`, `AccountRequiredError`, `UnsupportedDepositModeError`,
`UnsupportedAssetError`, `InvalidSlippageBPSError`, `InvalidSolverTipError`, `RpcNotConfiguredError`
---
### getWithdrawTx(client, params) → Promise<PreparedTx[]>
Builds ordered EVM transactions to withdraw from a vault. Exactly one of `shares`, `amount`, or `entireAmount` is required.
```typescript
import { getWithdrawTx, VaultId } from '@gauntlet-xyz/sdk/evm'
const steps = await getWithdrawTx(client, {
vaultId: VaultId.AeraUsdAlpha,
entireAmount: true, // OR: shares: 500_000000000000000000n OR: amount: 1_000_000n
chainId: 8453, // optional; defaults to Base
assetSymbol: 'USDC', // optional; required for multi-asset vaults
depositMode: 'async', // optional; 'async' | 'sync'. Aera: withdrawals blocked for 1 hour after a sync deposit.
receiver: '0x...', // optional; receives the withdrawn assets (shares always burn from the signer). Defaults to wallet.account
slippageBps: 100, // optional; integer 0–10000. Default 100 (1%).
solverTip: 0n, // optional; default 0
maxPriceAge: 864_000n, // optional; default 10 days
})
```
Throws: `VaultNotFoundError`, `AccountRequiredError`, `UnsupportedDepositModeError`,
`UnsupportedAssetError`, `InvalidSlippageBPSError`, `InvalidSolverTipError`, `InvalidWithdrawParamsError`, `RpcNotConfiguredError`
---
### getDepositReceiverApprovalTx(client, params) → Promise<PreparedTx>
Aera V2 only. Builds the receiver-side approval transaction required before a depositor
can make **sync** deposits to a separate receiver. The client's wallet account is the
receiver and must sign this transaction.
```typescript
import { getDepositReceiverApprovalTx } from '@gauntlet-xyz/sdk/evm'
const step = await getDepositReceiverApprovalTx(client, {
vaultId: 'gtusda',
depositor: '0xDepositor', // the address that will deposit on the receiver's behalf
approved: true, // optional; default true. false revokes.
chainId: 8453, // optional
})
```
Throws: `VaultNotFoundError`, `AccountRequiredError`, `UnsupportedFeatureError` (on V1 vaults), `RpcNotConfiguredError`
---
### getVaults(client, filter?) → Promise<VaultInfo[]>
Returns vaults from bundled manifest. No network request.
```typescript
import { getVaults } from '@gauntlet-xyz/sdk/evm'
const vaults = await getVaults(client, { chainId: 8453, protocol: 'aera' })
// filter is optional — omit to return all vaults
```
---
### getUserCurrentBalance(client, params) → Promise<UserCurrentBalance[]>
Returns balance breakdown for a user across all deployments of a vault. Only works for Aera multi-depositor vaults. Makes on-chain RPC calls (scans ~3 days of provisioner events for pending requests).
```typescript
import { getUserCurrentBalance } from '@gauntlet-xyz/sdk'
const balances = await getUserCurrentBalance(client, {
vaultId: 'gtusda',
address: '0xUser',
chainId: 8453, // optional; omit to return one entry per chain the vault is deployed on
})
```
Throws: `VaultNotFoundError`, `UnsupportedProtocolError`, `ChainMismatchError`, `RpcNotConfiguredError`
---
## Gaia API — client.api
Typed client for the Gaia REST API (`api.gauntlet.xyz`): indexed vaults, user positions
with PnL, activity logs, TVL, and token prices. Available as `client.api` on a configured
`GauntletClient`, or standalone:
```typescript
import { GauntletApi } from '@gauntlet-xyz/sdk/api'
const api = new GauntletApi({ apiKey: process.env.GAUNTLET_API_KEY })
```
### Conventions
- **Vault ids are CAIP-10-style**: `"{chainId}:{address}"` with a lowercase address
(e.g. `"8453:0xabc…"`) — NOT manifest vault ids like `'gtusda'`. Convert with
`apiVaultIdFromVaultId(client, 'gtusda')` / `vaultIdFromApiVaultId(client, apiId)`.
- **Amounts are human-unit decimal strings** (e.g. `"1250.5"`), not base-unit bigints.
Convert with `decimalToBigInt(value, decimals)` / `sharesToBigInt(value)` (shares are
always 18 decimals — `SHARE_DECIMALS`). Conversion is exact: excess precision throws
`DecimalPrecisionError` instead of rounding.
- **Pagination is cursor-based**: pass `{ next: previousResponse.meta.next_cursor }`.
Page size is capped at 1000 rows.
- Response types are generated from the service's OpenAPI spec, so they cannot drift
from the server models.
### Endpoints
```typescript
api.health() // GET /health — liveness (version + uptime)
api.chainSyncStatus() // GET /health/chains — per-chain indexer freshness
api.vaults(page?) // GET /v1/vaults — all indexed vaults with live metrics (TVL, APY, unit price)
api.vault(apiVaultId) // GET /v1/vaults/{vault_id}
api.vaultDefinition(apiVaultId) // GET /v1/vaults/{vault_id}/definition — raw indexed definition
api.vaultTimeseries(apiVaultId, window?) // GET /v1/vaults/{vault_id}/timeseries — TVL / unit-price / APY history
api.positions(wallet, page?) // GET /v1/users/{wallet}/positions — all positions with PnL
api.position(wallet, apiVaultId) // GET /v1/users/{wallet}/positions/{vault_id} — one position with PnL breakdown
api.positionTimeseries(wallet, apiVaultId, window?) // value / cost-basis / PnL / ROI history
api.activity(wallet, options?) // GET /v1/users/{wallet}/activity — one page of the immutable event log
api.activityRows(wallet, options?) // async generator — auto-follows next_cursor across pages
api.tvl({ includeBreakdown? }) // GET /v1/tvl — aggregate Gauntlet TVL
api.latestPrice({ address, chainId, at? }) // GET /v1/prices — latest (or point-in-time) USD token price
api.priceTimeseries({ address, chainId, start, end, granularity?, limit? }) // USD price history
```
Options types: `PageOptions { next?, limit?, order? }`,
`TimeWindowOptions extends PageOptions { start?, end?, granularity? }`
(`granularity`: `'hour' | 'day' | 'week' | 'month'`; dates are ISO 8601 / RFC 3339),
`ActivityOptions extends PageOptions { vaultId? }` (CAIP-10; omit for wallet-wide activity).
Failed requests throw `GauntletApiError` with `.status`, `.path`, and optional `.code`.
### Activity flows (high-level helper)
The raw activity log is an immutable row stream; Aera async lifecycles span multiple rows
(`deposit_pending` → `deposit`/`deposit_refunded`) paired by `request_hash`.
`getActivityFlows` fetches and stitches them into one flow per user action — the
replacement for scanning vault event logs over RPC.
```typescript
import { getActivityFlows, waitForRequestSettlement } from '@gauntlet-xyz/sdk/api'
const flows = await getActivityFlows(client.api, wallet, { vaultId?, maxRows? }) // newest-first
const open = flows.filter((f) => f.status === 'pending')
// After submitting a requestDeposit/requestRedeem tx, poll until it settles or refunds:
const settled = await waitForRequestSettlement(client.api, wallet, requestHash, {
vaultId?, pollIntervalMs?: 5_000, timeoutMs?: 600_000, // throws SettlementTimeoutError on expiry
})
```
```typescript
type ActivityFlow = {
kind: 'deposit' | 'withdraw' | 'transfer_in' | 'transfer_out'
status: 'settled' | 'pending' | 'refunded'
vaultId: string // CAIP-10
requestHash: string | null // Aera async correlation hash; null for sync flows
requestedAt: Date | null
settledAt: Date | null // null while pending
assets: { decimal: string; raw: bigint | null; token: TokenRef | null }
shares: bigint // 18-decimal base units
txHashes: string[]
}
```
`stitchActivityFlows(rows)` is the pure stitcher if you already have rows.
### Position history (high-level helper)
Replays one vault's activity into a chronological position timeline — running share
balance, escrowed pending amounts, cumulative net asset flows.
```typescript
import { getPositionHistory } from '@gauntlet-xyz/sdk/api'
const history = await getPositionHistory(client.api, wallet, apiVaultId)
// { vaultId, token, points: PositionHistoryPoint[] } — oldest-first
// Each point: { timestamp, txHash, type, sharesDelta, assetsDelta, sharesBalance,
// pendingDepositAssets, pendingRedeemShares, netAssetsIn }
// A wallet that never touched the vault gets an empty timeline, not an error.
```
`buildPositionHistory(rows)` is the pure builder.
---
## Privy — @gauntlet-xyz/sdk/privy
Helpers for Privy embedded/connected wallets. No dependency on any `@privy-io` package.
```typescript
import { createGauntletClientFromPrivy, walletClientFromPrivy } from '@gauntlet-xyz/sdk/privy'
import { base } from 'viem/chains'
// One-call setup: builds public clients per chain + a signing wallet from the Privy provider
const { wallets } = useWallets() // @privy-io/react-auth
const client = await createGauntletClientFromPrivy({
wallet: wallets[0],
chains: [base], // first chain is the wallet's signing chain
transports: { ... }, // optional per-chain viem Transport; defaults to public RPC http()
apiKey, apiUrl, attributionMode, builderCode, // optional, same as GauntletClient
})
// Or just wrap the Privy wallet in a viem WalletClient:
const walletClient = await walletClientFromPrivy(wallets[0], base)
```
---
## Transaction Submission
`getDepositTx` and `getWithdrawTx` return `PreparedTx[]`. Steps MUST be executed in order.
An `approve` step, when present, always comes first.
### PreparedTx type
```typescript
type PreparedTx = {
payload: {
type: string // 'approve' | 'deposit' | 'requestDeposit' | 'redeem' | 'requestRedeem' | 'withdraw' | 'setDepositReceiverApproval'
to: Address // contract address
data: Hex // ABI-encoded calldata + attribution suffix pre-concatenated
account?: Address
}
tx: EvmTxStep // structured ABI fields + raw attribution bytes
}
type EvmTxStep = {
type: 'approve' | 'deposit' | 'requestDeposit' | 'redeem' | 'requestRedeem' | 'withdraw' | 'setDepositReceiverApproval'
address: Address
abi: Abi
functionName: string
args: readonly unknown[]
account: Address
attribution?: Hex // raw ERC-8021 suffix bytes only — NOT the full calldata
}
```
---
### Path 1: step.payload + sendTransaction
**Use for:** backend scripts, embedded wallets (Privy, Dynamic), server-side signing, EVM simulation.
Attribution is baked into `payload.data` — cannot be dropped regardless of wallet or provider.
```typescript
for (const step of steps) {
// estimateGas simulates exact calldata before broadcast — catches reverts before spending gas
const gas = await publicClient.estimateGas({
to: step.payload.to,
data: step.payload.data,
account: step.payload.account,
})
// gas limit prevents out-of-gas failures on-chain
const hash = await walletClient.sendTransaction({ ...step.payload, gas })
// wait for confirmation before next step — deposit reverts if preceding approve is not mined
const receipt = await publicClient.waitForTransactionReceipt({ hash, confirmations: 2 })
if (receipt.status !== 'success') throw new Error(`Reverted: ${step.payload.type}`)
}
```
---
### Path 2: step.tx + writeContract
**Use for:** browser wallets via wagmi (MetaMask, Coinbase Wallet, WalletConnect).
CRITICAL: `dataSuffix` MUST be `step.tx.attribution`. Omitting it silently drops attribution —
the transaction succeeds on-chain but volume is NOT tracked.
```typescript
import { useWriteContract } from 'wagmi'
const { writeContractAsync } = useWriteContract()
for (const step of steps) {
await writeContractAsync({
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
- Format: ERC-8021 — `0x8021` + UTF-8 hex of builderCode, e.g. `"acme"` → `0x802161636d65`
- Appended as suffix to calldata. Does not affect contract execution.
- `builderCode` MUST be issued by Gauntlet. Unregistered codes append bytes but are not counted.
- Without `builderCode`, no bytes are appended and transactions are unattributed.
- `AttributionMode.ENCODED` and `AttributionMode.PRIVATE` throw `UnimplementedFeatureError`.
---
## VaultId Enum
Well-known vault ids. The bundled manifest contains more vaults than this enum — any
manifest `vaultId` string is accepted wherever `VaultId` is; use `getVaults(client)` to
enumerate everything. The Gaia API indexes even more vaults than the manifest lists.
```typescript
enum VaultId {
BaseUsdcPrime = 'baseUsdcPrime',
EthUsdcPrime = 'ethUsdcPrime',
AeraUsdAlpha = 'gtusda', // gtUSDa — multi-chain USDC, Aera async
EthUsdcPrimeV2 = 'ethUsdcPrimeV2',
AeraUsdAlphaStaging = 'stgusda',
AeraUsdAlphaDev = 'devusda',
AeraUsdAlphaDevDeux = 'devusda2',
AeraLeveredFalconX = 'gpaafalconx',
AeraLeveredFalconXStaging = 'pytstg',
AeraSyrupUsdc = 'gpsyrupusdc',
AeraSyrupUsdcStaging = 'syruppytstg',
AeraBtcYield = 'gtbtc',
AeraBtcYieldStaging = 'gtbtcstaging',
AeraLeveredUsccStaging = 'gtusccstg',
AeraLeveredUscc = 'gtuscc',
AeraLend = 'gtlend',
AeraKastEth = 'kasteth',
}
```
---
## Types
```typescript
type VaultInfo = {
vaultId: string
name: string
protocol: 'aera' | 'morpho'
strategy: string
deployments: VaultDeployment[]
}
type VaultDeployment = { // currently always EvmVaultDeployment; narrow on `chain`
chain: 'evm'
chainId: number
vaultAddress: Address
vaultType: 'single-depositor' | 'multi-depositor'
supplyToken: TokenInfo[]
expirationDays?: number // days before an async request deadline expires; default 3
}
// NOTE: provisioner/feeCalculator addresses and sync/async support are NOT in the
// manifest — they are resolved from live chain state (see Key Constraints).
type TokenInfo = {
symbol: string
address: Address
decimals: number
}
type UserCurrentBalance = {
chain: string // 'base' | 'ethereum' | 'arbitrum' | 'optimism'
token: Address
decimals: number
pendingDeposit: bigint // assets locked in provisioner after async deposit — awaiting solver, 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 — awaiting solver, no longer earning; 0n if none
}
// All three bigint fields are always present. 0n means no position — not an error.
type VaultFilter = {
chainId?: number
protocol?: string
}
enum AttributionMode {
PUBLIC = 'public', // ERC-8021 builder code — default
ENCODED = 'encoded', // NOT IMPLEMENTED — throws UnimplementedFeatureError
PRIVATE = 'private', // NOT IMPLEMENTED — throws UnimplementedFeatureError
}
const ContractVersion = { V1: 'v1', V2: 'v2' } // Aera provisioner/feeCalculator generations
```
---
## Balance Lifecycle
Pending states represent funds in transit — locked in the provisioner contract and awaiting the vault solver. During `pendingDeposit`, assets are not yet earning yield. During `pendingWithdraw`, vault shares have been redeemed and assets are no longer earning yield but have not yet been transferred. Funds are safe in both pending states; the delay (~2h, up to 12h) is operational, not a risk.
| Event | Effect on balance fields |
|-------|--------------------------|
| Async deposit submitted | Amount enters `pendingDeposit` — locked in provisioner, not yet earning |
| Solver processes deposit (~2h, up to 12h) | `pendingDeposit` → `balance` — now earning yield |
| Sync deposit | Goes directly to `balance` — earning immediately |
| Async withdraw submitted | `balance` → `pendingWithdraw` — shares redeemed, no longer earning, not yet claimable |
| Solver processes withdraw (~2h, up to 12h) | Leaves `pendingWithdraw`; claimable as ERC-20 in receiver wallet |
| Sync withdraw | Removed from `balance` immediately; claimable in receiver wallet. Aera vaults: blocked for 1 hour after the user's most recent sync deposit |
---
## Errors
All extend `GauntletSDKError extends Error`.
| Class | Properties | Thrown when |
|-------|------------|-------------|
| `VaultNotFoundError` | `.vaultId`, `.chainId?` | Vault ID not in manifest or not on requested chain |
| `UnsupportedAssetError` | `.asset`, `.vaultId` | Token not accepted by vault |
| `ChainMismatchError` | `.expected`, `.received` | chainId param doesn't match deployment |
| `UnsupportedDepositModeError` | `.vaultId`, `.requested`, `.available` | Requested sync/async not supported |
| `RpcNotConfiguredError` | `.chainId` | No evmClients entry for required chain |
| `AccountRequiredError` | — | No wallet in client config |
| `UnsupportedProtocolError` | `.protocol` | getUserCurrentBalance called on non-Aera or single-depositor vault |
| `InvalidWithdrawParamsError` | — | Not exactly one of shares/amount/entireAmount provided |
| `InvalidSlippageBPSError` | `.slippage` | slippageBps not an integer in 0–10000 |
| `InvalidSolverTipError` | `.solverTip`, `.availableAmount` | solverTip ≥ available token amount |
| `StalePriceError` | `.blockTimestamp`, `.maxPriceAge`, `.priceTimestamp` | Vault's on-chain price older than its max age |
| `UnimplementedFeatureError` | `.feature` | ENCODED/PRIVATE attribution modes, getSourceId |
| `UnsupportedFeatureError` | `.feature` | Feature not supported (e.g. separate receiver / receiver approval on Aera V1) |
| `UnitConversionError` | `.vaultAddress` | Fee calculator unavailable on-chain |
| `GauntletApiError` | `.status`, `.path`, `.code?` | Gaia API request failed (non-2xx) |
| `InvalidDecimalError` | `.value` | String is not a valid decimal |
| `DecimalPrecisionError` | `.value`, `.decimals` | Conversion would lose precision (never rounds) |
| `InvalidCaipIdError` | `.id` | Not a valid `"{chainId}:{address}"` vault id |
| `SettlementTimeoutError` | `.requestHash`, `.timeoutMs` | waitForRequestSettlement deadline expired |
---
## Key Constraints
- The SDK never signs. `wallet` is read-only for `wallet.account.address`.
- `getDepositTx`/`getWithdrawTx` require `evmClients` + `wallet` (check allowance on-chain).
- `getVaults` reads bundled manifest — no network call required.
- **Aera runtime is resolved from live chain state**, not the manifest: provisioner and
fee-calculator addresses are read from the vault contract, and V1 vs V2 is detected
on-chain (`resolveAeraRuntimeContracts` / `resolveContractVersion`, exported for advanced use
alongside price/unit utilities `convertTokenToUnits`, `convertUnitsToToken`, `getVaultState`, `isVaultPaused`).
- **Deposit mode:** sync/async availability is read from live vault configuration. When
both are available and `depositMode` is omitted, async is used. Morpho vaults are sync-only.
- **Aera receiver rules:** on V1 vaults, `receiver` MUST equal the signer — a different
address throws `UnsupportedFeatureError`. V2 vaults support a separate receiver; for
**sync** deposits to a separate receiver, the receiver must first sign
`getDepositReceiverApprovalTx` approving the depositor. On withdrawals, `receiver` is
where the assets are sent; shares always burn from the signer.
- **Unit lock after sync deposit:** on Aera vaults, a sync deposit locks all of the
user's vault units for the vault's deposit refund timeout, currently 1 hour. Any
withdraw (sync or async) or unit transfer within that window reverts on-chain with
`Aera__UnitsLocked`. Async deposits do not trigger the lock.
- `getUserCurrentBalance` scans ~3 days of provisioner events — requires `evmClients` for each chain queried.
- **Multi-chain vaults** (e.g. gtUSDa): `chainId` defaults to Base (8453) when omitted.
- **Single-chain vaults:** provide `chainId` matching the deployment, or omit it. A mismatched chainId throws `ChainMismatchError`.
- Steps from `getDepositTx`/`getWithdrawTx` must be sent in order. Deposit reverts if preceding approve is not mined first.
- Using Path 2 (writeContract): `dataSuffix` must be `step.tx.attribution` or attribution is silently lost.
- **Gaia API:** vault ids are CAIP-10 (`"{chainId}:{address}"`, lowercase) — convert with
`apiVaultIdFromVaultId`; amounts are human-unit decimal strings — convert with
`decimalToBigInt`/`sharesToBigInt` (shares are always 18 decimals); pages cap at 1000 rows
(cursor via `meta.next_cursor`); anonymous access is rate-limited.
Agents
CONTEXT.md
Copy this into your LLM context window for accurate, grounded answers about the Gauntlet SDK.
Copy the block below into your system prompt or context window. It gives an LLM everything it needs to answer questions about the SDK, generate correct integration code, and avoid common mistakes.