Skip to main content

Documentation Index

Fetch the complete documentation index at: https://developers.paxoslabs.com/llms.txt

Use this file to discover all available pages before exploring further.

@paxoslabs/amplify-sdk@1.0.0 is wallet-agnostic. Every prepare / cancel method returns
import type { Address, Hex } from 'viem'

{
  transaction: {
    to: Address
    data: Hex
    value: string // decimal, usually "0"
    // optional, only with responseFormat: 'full' | 'structured'
    abi?: unknown
    functionName?: string
    args?: unknown[]
  }
}
The SDK does not bundle, sign, or submit anything. It just produces calldata. That means you can submit Amplify transactions from any wallet that can issue a contract call — EOAs via wagmi/viem, smart-contract accounts via Privy or Dynamic, ERC-4337 bundlers via Alchemy or Pimlico, and server-side walletClient signers.
If you arrived from the v0.5.x docs, the SDK no longer exposes smart-wallet-specific helpers, auth-method enums, or type guards. The flow is now: call permit.authorize, branch on auth.method, then submit prepared.transaction. See Migrating from 0.5 for the full mapping.

What the SDK gives you

prepared.transaction is the same shape regardless of vault, chain, or wallet:
import { AmplifyClient } from '@paxoslabs/amplify-sdk'

const client = new AmplifyClient({
  apiKey: process.env.PAXOS_LABS_API_KEY!,
})

const prepared = await client.deposit.prepareDeposit({
  vaultAddress: '0xbbbb000000000000000000000000000000000001',
  depositAsset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
  depositAmount: '1000000',
  userAddress: '0xUserOrSmartAccount',
  chainId: 1,
})

// prepared.transaction.{to,data,value} — submit from any wallet
Your job is to route { to, data, value } to whichever sender the connected wallet exposes. The rest of this guide shows that routing for the wallet types Amplify integrators most often use.

userAddress vs. to

Two address fields appear on most prepare requests:
FieldMeaning
userAddressThe account that submits the transaction. For EOAs, the signer. For AA, the smart-account address.
to(Optional) The address that receives the resulting shares (deposits) or assets (withdrawals settlement).
Set to explicitly when a session key submits the transaction but the smart account should own the shares:
const prepared = await client.deposit.prepareDeposit({
  vaultAddress,
  depositAsset,
  depositAmount,
  userAddress: sessionKeyAddress, // submits the tx
  to: smartAccountAddress,        // owns the shares
  chainId,
})
Withdrawals expose the same idea via intendedDepositor, receiver, and refundReceiver — pass each explicitly when the submitter and the share/asset owner differ. See the Withdrawals guide.

Permit vs. approval on smart wallets

For deposits you first call client.permit.authorize(...). It returns one of three shapes:
import type { Hex } from 'viem'

type PermitResponseDto =
  | { method: 'permit'; permitData: { domain; types; value; deadline } }
  | { method: 'approval'; approvalTransaction: { encoded: Hex; /* … */ } }
  | { method: 'already_approved' }
  • Smart wallets that expose signTypedData (EIP-712) — most Privy embedded smart wallets, Dynamic embedded wallets, and Safe — can sign EIP-2612 permits. Treat method: 'permit' identically to an EOA: sign the typed data, pass permitSignature + permitDeadline to prepareDeposit, and submit one transaction.
  • Smart wallets without typed-data signing (some session-key flows, restricted policy-engine accounts) cannot produce a permit. Fall back to the approval path: submit approvalTransaction.encoded to the deposit asset, wait for confirmation, then call prepareDeposit without permitSignature / permitDeadline.
A defensive client handles all three branches:
const auth = await client.permit.authorize({
  vaultAddress,
  tokenAddress: depositAsset,
  amount: depositAmount,
  userAddress,
  chainId,
})

let permitSignature: Hex | undefined
let permitDeadline: number | undefined

if (auth.method === 'permit' && supportsTypedData(wallet)) {
  permitSignature = await wallet.signTypedData({
    account: userAddress,
    domain: auth.permitData.domain,
    types: auth.permitData.types,
    primaryType: 'Permit',
    message: auth.permitData.value,
  })
  permitDeadline = Number(auth.permitData.deadline)
} else if (auth.method === 'approval' || auth.method === 'permit') {
  // Submit approval — either because the token requires it, or because
  // this wallet can't sign EIP-712.
  const approvalTx =
    auth.method === 'approval'
      ? auth.approvalTransaction
      : await fallbackApprovalFor(depositAsset, vaultAddress, depositAmount)
  const hash = await wallet.sendTransaction({
    to: depositAsset,
    data: approvalTx.encoded,
    chainId,
  })
  await publicClient.waitForTransactionReceipt({ hash })
}
// method === 'already_approved' → nothing to do

const prepared = await client.deposit.prepareDeposit({
  vaultAddress,
  depositAsset,
  depositAmount,
  userAddress,
  chainId,
  ...(permitSignature ? { permitSignature, permitDeadline } : {}),
})
The same auth.method === 'approval' path applies to withdrawals — except tokenAddress is the share token (vaultAddress), and the only valid response shapes are approval or already_approved.

Pattern 1 — Privy / Dynamic smart wallets (UserOps under the hood)

Both Privy’s useSmartWallets and Dynamic’s embedded smart-account adapter expose a sendTransaction-style API that internally builds a UserOperation, sends it to a bundler, and resolves with a hash. From the SDK’s perspective they are identical — just pass prepared.transaction.{to,data,value} through.
// pseudo-code; consult Privy / Dynamic docs for the exact hook shape
import { useSmartWallets } from '@privy-io/react-auth/smart-wallets'
import { AmplifyClient } from '@paxoslabs/amplify-sdk'
import type { Address, Hex } from 'viem'

const client = new AmplifyClient({ apiKey })

function useAmplifyDeposit() {
  const { client: smartWallet } = useSmartWallets()

  return async function deposit(params: {
    vaultAddress: Address
    depositAsset: Address
    depositAmount: string
    smartAccountAddress: Address
    chainId: number
  }) {
    const auth = await client.permit.authorize({
      vaultAddress: params.vaultAddress,
      tokenAddress: params.depositAsset,
      amount: params.depositAmount,
      userAddress: params.smartAccountAddress,
      chainId: params.chainId,
    })

    const calls: { to: Address; data: Hex; value?: bigint }[] = []

    if (auth.method === 'approval') {
      calls.push({
        to: params.depositAsset,
        data: auth.approvalTransaction.encoded as Hex,
      })
    }

    const prepared = await client.deposit.prepareDeposit({
      vaultAddress: params.vaultAddress,
      depositAsset: params.depositAsset,
      depositAmount: params.depositAmount,
      userAddress: params.smartAccountAddress,
      chainId: params.chainId,
      // No permitSignature — smart wallet is using the approval path.
    })

    calls.push({
      to: prepared.transaction.to as Address,
      data: prepared.transaction.data as Hex,
      value: BigInt(prepared.transaction.value),
    })

    // One user confirmation, one UserOperation — both calls bundle together.
    return smartWallet.sendTransaction({ calls })
  }
}
The hook signatures, sendTransaction parameters, and how gas sponsorship is configured are dictated by Privy and Dynamic — not by the Amplify SDK. The code above is illustrative; consult the wallet provider’s docs for the authoritative API.
If the asset supports permit and the smart wallet supports signTypedData, you can collapse to a single call by signing the permit first and skipping the approval step entirely.

Pattern 2 — ERC-4337 bundlers (Alchemy, Pimlico, Biconomy)

Native ERC-4337 SDKs expose batched submission as sendTransactions({ requests }) or sendUserOperation. The response is a userOpHash, not a transaction hash:
import type { Address, Hex } from 'viem'

const userOpHash = await smartAccountClient.sendTransactions({
  requests: [
    auth.method === 'approval' && {
      to: depositAsset,
      data: auth.approvalTransaction.encoded as Hex,
    },
    {
      to: prepared.transaction.to as Address,
      data: prepared.transaction.data as Hex,
      value: BigInt(prepared.transaction.value),
    },
  ].filter(Boolean) as { to: Address; data: Hex; value?: bigint }[],
})

const receipt = await smartAccountClient.waitForUserOperationReceipt({
  hash: userOpHash,
})
const txHash = receipt.receipt.transactionHash
Two things to remember:
  1. Do not use wagmi’s useWaitForTransactionReceipt with a userOpHash — poll the bundler instead (waitForUserOperationReceipt).
  2. Gas sponsorship is configured on the smart-account client (paymaster), not on the prepared transaction. The SDK never sets gasPrice or maxFeePerGas.

Pattern 3 — viem walletClient (EOA or AA exposed as JSON-RPC)

For non-AA wallets and for AA wallets exposed through a viem walletClient (Safe via safe-apps-sdk, MetaMask Smart Account, etc.), submit directly:
import type { Address, Hex } from 'viem'
import { createWalletClient, custom, parseEther } from 'viem'
import { mainnet } from 'viem/chains'

const walletClient = createWalletClient({
  account: userAddress,
  chain: mainnet,
  transport: custom(window.ethereum),
})

const prepared = await client.deposit.prepareDeposit({ /* ... */ })

const hash = await walletClient.sendTransaction({
  to: prepared.transaction.to as Address,
  data: prepared.transaction.data as Hex,
  value: BigInt(prepared.transaction.value),
  chain: mainnet,
})
Permit signing uses the same client:
const signature = await walletClient.signTypedData({
  account: userAddress,
  domain: auth.permitData.domain,
  types: auth.permitData.types,
  primaryType: 'Permit',
  message: auth.permitData.value,
})

Pattern 4 — Server-side signers

When the signer lives on your backend (custodial flows, automation), the same walletClient works with an in-memory account:
import type { Address, Hex } from 'viem'
import { createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { mainnet } from 'viem/chains'
import { AmplifyClient } from '@paxoslabs/amplify-sdk'

const account = privateKeyToAccount(process.env.SIGNER_PRIVATE_KEY as Hex)
const walletClient = createWalletClient({
  account,
  chain: mainnet,
  transport: http(process.env.RPC_URL),
})

const client = new AmplifyClient({
  apiKey: process.env.PAXOS_LABS_API_KEY!,
})

const prepared = await client.deposit.prepareDeposit({
  vaultAddress,
  depositAsset,
  depositAmount,
  userAddress: account.address,
  chainId: mainnet.id,
})

const hash = await walletClient.sendTransaction({
  to: prepared.transaction.to as Address,
  data: prepared.transaction.data as Hex,
  value: BigInt(prepared.transaction.value),
})
Server-side keys must never be sent to a browser. Keep the signer behind a trusted endpoint and surface only prepared.transaction to clients that need to display previews.

Error handling

Smart-wallet submission can fail in three distinct layers — handle each:
import { AmplifyError, AmplifyTimeoutError } from '@paxoslabs/amplify-sdk'

try {
  const prepared = await client.deposit.prepareDeposit({ /* ... */ })
  await smartWallet.sendTransaction({ /* prepared.transaction */ })
} catch (err) {
  if (err instanceof AmplifyTimeoutError) {
    // SDK couldn't reach the backend in time. Retry or surface a UI hint.
  } else if (err instanceof AmplifyError) {
    // err.statusCode  — backend HTTP status
    // err.body        — parsed error body (when JSON)
    // err.message     — human-readable summary
    // err.rawResponse — raw fetch Response, for debugging
  } else {
    // Wallet / bundler error — UserOp rejected, paymaster denied, etc.
  }
}

Checklist

Use the permit path. One UserOp / one transaction for the whole deposit.
Use the approval path. Batch approve + deposit calls together so the user signs once. If the wallet cannot batch (rare for modern AA), submit sequentially and wait for the approval receipt before preparing the deposit.
Pass the session-key address as userAddress, and the smart account as to (deposits) or intendedDepositor / receiver / refundReceiver (withdrawals). The session key submits the tx; the smart account owns the shares.
Use a viem walletClient with a privateKeyToAccount. Keep the key out of the browser; expose only the prepared transaction shape to clients.