Skip to main content
This walkthrough bootstraps a Vite + React project that authenticates users with Privy, initializes the Amplify SDK, and wires up deposit plus withdrawal flows using the unified deposit API. Follow the steps end-to-end or cherry-pick the sections that fit your stack.
The snippets use the latest Privy React SDK (@privy-io/react-auth) APIs. Wait for ready from usePrivy and useWallets before inspecting user or wallet state to avoid race conditions.

1. Scaffold the Project

pnpm create vite amplify-earn-starter --template react-ts
cd amplify-earn-starter
pnpm install
Install the runtime dependencies:
pnpm add @paxoslabs/amplify-sdk viem @privy-io/react-auth @tanstack/react-query
Install type support if you use tests:
pnpm add -D @types/node @types/react @types/react-dom

2. Configure Environment Variables

Create .env.local (ignored by Vite by default):
VITE_PRIVY_APP_ID=your-privy-app-id
VITE_AMPLIFY_API_KEY=your-amplify-api-key
VITE_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/your-key
Never commit this file. Provide an .env.example without secrets for teammates.

3. Set Up Global Providers

Create src/app/providers.tsx:
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { PrivyProvider } from "@privy-io/react-auth";
import type { ReactNode } from "react";

const queryClient = new QueryClient();

export function AmplifyProviders({ children }: { children: ReactNode }) {
  if (!import.meta.env.VITE_PRIVY_APP_ID) {
    throw new Error("Missing VITE_PRIVY_APP_ID env variable");
  }

  return (
    <PrivyProvider appId={import.meta.env.VITE_PRIVY_APP_ID}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </PrivyProvider>
  );
}
Wire the providers in src/main.tsx:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { AmplifyProviders } from "./providers.tsx";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <AmplifyProviders>
      <App />
    </AmplifyProviders>
  </StrictMode>,
);

4. Initialize the Amplify SDK

Create src/hooks/useAmplify.ts:
import { initAmplifySDK, LogLevel } from "@paxoslabs/amplify-sdk";
import { useEffect } from "react";

let initialized = false;

export function useAmplify() {
  useEffect(() => {
    if (initialized) return;

    const apiKey = import.meta.env.VITE_AMPLIFY_API_KEY;
    if (!apiKey) {
      throw new Error("Missing VITE_AMPLIFY_API_KEY env variable");
    }

    // Initialize with optional configuration
    initAmplifySDK(apiKey, {
      logLevel: LogLevel.ERROR, // Set to DEBUG for development
      telemetry: true, // Enable error tracking
    });

    initialized = true;
  }, []);
}
Call the hook near the root of your app (inside App or a layout component). For advanced configuration options, see SDK Initialization.

5. Build Wallet & Session State

Create src/hooks/useWalletSession.ts:
import { usePrivy, useWallets } from "@privy-io/react-auth";
import { useMemo } from "react";

export function useWalletSession() {
  const { ready: privyReady, authenticated, login, logout } = usePrivy();
  const { ready: walletsReady, wallets } = useWallets();

  const wallet = useMemo(
    () =>
      wallets.find((item) => item.walletClientType === "privy") ?? wallets[0],
    [wallets],
  );

  return {
    ready: privyReady && walletsReady,
    authenticated,
    login,
    logout,
    wallet,
  };
}
The usePrivy and useWallets hooks come directly from the official Privy React SDK. Wait for ready before inspecting wallets.

6. Create the App with Unified Deposit API

Add src/app/App.tsx:
import {
  DepositAuthMethod,
  fetchSupportedAssets,
  prepareApproveWithdrawTxData,
  prepareDepositAuthorization,
  prepareDepositTxData,
  prepareDepositWithPermitTxData,
  prepareWithdrawTxData,
  YieldType,
  type SupportedAsset as AmplifySupportedAsset,
} from "@paxoslabs/amplify-sdk";
import { usePrivy } from "@privy-io/react-auth";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { encodeFunctionData, isAddress, type Address } from "viem";
import { mainnet } from "viem/chains";
import "./App.css";
import { useAmplify } from "./hooks/use-amplify";
import { useWalletSession } from "./hooks/use-wallet-session";

function App() {
  useAmplify();
  const { ready, authenticated, login, logout, wallet } = useWalletSession();
  const { sendTransaction } = usePrivy();
  const [status, setStatus] = useState<string | null>(null);
  const [selectedAsset, setSelectedAsset] =
    useState<AmplifySupportedAsset | null>(null);
  const {
    data: supportedAssets,
    isLoading: isLoadingSupportedAssets,
    isError: isErrorSupportedAssets,
  } = useQuery({
    queryKey: ["amplify-vaults", mainnet.id],
    enabled: authenticated,
    queryFn: () =>
      fetchSupportedAssets({
        yieldType: YieldType.PRIME,
      }),
  });
  console.log(supportedAssets);
  if (!ready) {
    return <p>Loading Privy…</p>;
  }

  if (!authenticated || !wallet) {
    return <button onClick={login}>Connect with Privy</button>;
  }

  if (isLoadingSupportedAssets) {
    return <p>Loading assets...</p>;
  }

  if (isErrorSupportedAssets) {
    return <p>Error loading assets</p>;
  }

  const owner = wallet.address as `0x${string}`;
  const chainId = mainnet.id;

  const handleAssetChange = (address: Address) => {
    if (!isAddress(address)) {
      console.error("Invalid asset address");
      return;
    }
    setSelectedAsset(
      supportedAssets?.find((asset) => asset.address === address) ?? null,
    );
  };

  async function handleDeposit() {
    const params = {
      yieldType: YieldType.PRIME,
      depositToken: selectedAsset?.address as `0x${string}`,
      depositAmount: "100",
      recipientAddress: owner,
      chainId,
    };

    // Step 1: Get authorization method
    setStatus("Checking authorization...");
    const auth = await prepareDepositAuthorization(params);

    // Step 2: Handle based on method
    switch (auth.method) {
      case DepositAuthMethod.PERMIT: {
        setStatus("Please sign permit...");
        const provider = await wallet.getEthereumProvider();
        const signature = (await provider.request({
          method: "eth_signTypedData_v4",
          params: [
            owner,
            JSON.stringify({
              domain: auth.permitData.domain,
              types: auth.permitData.types,
              primaryType: auth.permitData.primaryType,
              message: auth.permitData.message,
            }),
          ],
        })) as `0x${string}`;

        setStatus("Submitting deposit...");
        const permitTx = await prepareDepositWithPermitTxData({
          ...params,
          signature,
          deadline: BigInt(auth.permitData.message.deadline),
        });

        // Extract and encode the permit deposit data
        const {
          abi: permitAbi,
          functionName: permitFn,
          args: permitArgs,
        } = permitTx.data;
        await sendTransaction({
          chainId: permitTx.chainId,
          to: permitTx.address,
          data: encodeFunctionData({
            abi: permitAbi,
            functionName: permitFn,
            args: permitArgs,
          }),
        });
        setStatus("Deposit complete!");
        break;
      }

      case DepositAuthMethod.APPROVAL: {
        setStatus("Approving token spend...");
        const {
          abi: approvalAbi,
          functionName: approvalFn,
          args: approvalArgs,
        } = auth.txData;
        await sendTransaction({
          chainId: chainId,
          to: auth.txData.address,
          data: encodeFunctionData({
            abi: approvalAbi,
            functionName: approvalFn,
            args: approvalArgs,
          }),
        });

        setStatus("Submitting deposit...");
        const depositTx = await prepareDepositTxData(params);
        const {
          abi: depositAbi,
          functionName: depositFn,
          args: depositArgs,
        } = depositTx;
        await sendTransaction({
          chainId: depositTx.chainId,
          to: depositTx.address,
          data: encodeFunctionData({
            abi: depositAbi,
            functionName: depositFn,
            args: depositArgs,
          }),
        });
        setStatus("Deposit complete!");
        break;
      }

      case DepositAuthMethod.ALREADY_APPROVED: {
        setStatus("Submitting deposit...");
        const directTx = await prepareDepositTxData(params);
        const { abi, functionName, args } = directTx;
        await sendTransaction({
          chainId: directTx.chainId,
          to: directTx.address,
          data: encodeFunctionData({ abi, functionName, args }),
        });
        setStatus("Deposit complete!");
        break;
      }
    }
  }

  async function handleWithdraw() {
    setStatus("Preparing withdrawal...");

    const approval = await prepareApproveWithdrawTxData({
      chainId,
      wantAssetAddress: selectedAsset?.address as `0x${string}`,
      yieldType: YieldType.PRIME,
    });

    setStatus("Approving withdrawal...");
    await sendTransaction({
      chainId,
      to: approval.address,
      data: encodeFunctionData({
        abi: approval.abi,
        functionName: approval.functionName,
        args: approval.args,
      }),
    });

    const withdraw = await prepareWithdrawTxData({
      yieldType: YieldType.PRIME,
      wantAssetAddress: selectedAsset?.address as `0x${string}`,
      offerAmount: "1",
      chainId,
    });

    setStatus("Submitting withdrawal...");
    const {
      abi: withdrawAbi,
      functionName: withdrawFn,
      args: withdrawArgs,
    } = withdraw;
    await sendTransaction({
      chainId: withdraw.chainId,
      to: withdraw.address,
      data: encodeFunctionData({
        abi: withdrawAbi,
        functionName: withdrawFn,
        args: withdrawArgs,
      }),
    });
    setStatus("Withdrawal complete!");
  }

  return (
    <main>
      <h1>Amplify Earn Starter</h1>
      <p>{wallet.address}</p>
      {status && <p>{status}</p>}
      <div>
        <select
          value={selectedAsset?.symbol ?? ""}
          onChange={(e) => {
            handleAssetChange(e.target.value as Address);
          }}
        >
          {supportedAssets?.map((asset) => (
            <option key={asset.address} value={asset.address}>
              {asset.symbol}
            </option>
          ))}
        </select>
      </div>
      <div>
        <div>
          <button onClick={handleDeposit}>Deposit</button>
          <button onClick={handleWithdraw}>Withdraw</button>
        </div>
      </div>
      <button onClick={logout}>Disconnect</button>
    </main>
  );
}

export default App;
  • Replace hard-coded amounts with user inputs and vault selections sourced from the API.
  • Add React Query mutations around each flow to display optimistic states and capture errors.
  • Surface APIError payloads in telemetry and show a fallback UI for unsupported assets.
  • Extract deposit and withdraw logic into reusable hooks (useDeposit, useWithdraw).
  • Add analytics around wallet connection and transaction success rates.

8. Starter Repository Layout

amplify-earn-starter/
├─ src/
│  ├─ app/
│  │  ├─ App.tsx
│  │  └─ providers.tsx
│  ├─ hooks/
│  │  ├─ useAmplify.ts
│  │  └─ useWalletSession.ts
│  └─ main.tsx
├─ .env.example
└─ README.md
Clone this structure in your org repo and extend it with routing, feature flags, and analytics as needed. Tie everything back to the Developer Guide for deployment practices and keep Functions handy as you add more flows.