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 bulk-withdraw flows. 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 AppProviders({ 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 React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./app/App";
import { AppProviders } from "./app/providers";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <AppProviders>
      <App />
    </AppProviders>
  </React.StrictMode>
);

4. Initialize the Amplify SDK Once

Create src/hooks/useAmplify.ts:
import { initAmplifySDK } 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");
    }
    initAmplifySDK(apiKey);
    initialized = true;
  }, []);
}
Call the hook near the root of your app (inside App or a layout component).

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 a Starter App

Add src/app/App.tsx:
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { encodeFunctionData } from "viem";
import { mainnet } from "viem/chains";
import {
  fetchVaults,
  prepareApproveDepositTokenTxData,
  prepareApproveWithdrawTxData,
  prepareWithdrawTxData,
  prepareDepositPermitSignature,
  prepareDepositTxData,
  prepareDepositWithPermitTxData,
  YieldType,
} from "@paxoslabs/amplify-sdk";
import { usePrivy } from "@privy-io/react-auth";
import { useAmplify } from "../hooks/useAmplify";
import { useWalletSession } from "../hooks/useWalletSession";

export function App() {
  useAmplify();
  const { ready, authenticated, login, logout, wallet } = useWalletSession();
  const { sendTransaction } = usePrivy();
  const [status, setStatus] = useState<string | null>(null);

  const { data: vaults } = useQuery({
    queryKey: ["core-vaults", mainnet.id],
    enabled: authenticated,
    queryFn: () =>
      fetchVaults({
        chainId: mainnet.id,
        yieldType: YieldType.CORE,
      }),
  });
  const coreVault = vaults?.[0];

  if (!ready) {
    return <p>Loading Privy…</p>;
  }

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

  if (!coreVault) {
    return <p>Loading vault metadata…</p>;
  }

  const owner = wallet.address as `0x${string}`;
  const depositToken = coreVault.depositTokenAddressId as `0x${string}`;
  const chainId = coreVault.chainId;

  async function approveAndDeposit() {
    setStatus("Preparing approval…");
    const approval = await prepareApproveDepositTokenTxData({
      chainId,
      depositAmount: "100",
      depositToken,
      yieldType: coreVault.yieldType,
    });

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

    setStatus("Submitting deposit…");
    const deposit = await prepareDepositTxData({
      chainId,
      depositAmount: "100",
      depositToken,
      recipientAddress: owner,
      yieldType: coreVault.yieldType,
    });

    await sendTransaction({
      chainId: deposit.chainId,
      to: deposit.address,
      data: encodeFunctionData({
        abi: deposit.abi,
        functionName: deposit.functionName,
        args: deposit.args,
      }),
    });
    setStatus("Deposit sent");
  }

  async function depositWithPermit() {
    setStatus("Creating permit signature…");
    const permit = await prepareDepositPermitSignature({
      yieldType: coreVault.yieldType,
      depositToken,
      depositAmount: "100",
      recipientAddress: owner,
      chainId,
    });

    const provider = await wallet.getEthereumProvider();
    const signature = (await provider.request({
      method: "eth_signTypedData_v4",
      params: [
        owner,
        JSON.stringify({
          domain: permit.domain,
          types: permit.types,
          primaryType: permit.primaryType,
          message: permit.message,
        }),
      ],
    })) as `0x${string}`;

    setStatus("Submitting depositWithPermit…");
    const tx = await prepareDepositWithPermitTxData({
      yieldType: coreVault.yieldType,
      recipientAddress: owner,
      depositToken,
      depositAmount: "100",
      chainId,
      signature,
      deadline: permit.message.deadline,
    });

    await sendTransaction({
      chainId: tx.chainId,
      to: tx.address,
      data: encodeFunctionData({
        abi: tx.abi,
        functionName: tx.functionName,
        args: tx.args,
      }),
    });
    setStatus("Permit deposit sent");
  }

  async function approveAndWithdraw() {
    setStatus("Preparing withdrawal…");

    const approval = await prepareApproveWithdrawTxData({
      chainId,
      depositAmount: "100",
      depositToken,
      yieldType: coreVault.yieldType,
    });

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

    const bulk = await prepareWithdrawTxData({
      yieldType: coreVault.yieldType,
      shareAmount: "1.0",
      wantAssetAddress: depositToken,
      chainId,
      userAddress: owner,
      slippage: 75,
    });

    await sendTransaction({
      chainId: bulk.chainId,
      to: bulk.address,
      data: encodeFunctionData({
        abi: bulk.abi,
        functionName: bulk.functionName,
        args: bulk.args,
      }),
    });
    setStatus("Withdrawal sent");
  }

  return (
    <main>
      <h1>Amplify Earn Starter</h1>
      <p>{owner}</p>
      {status && <p>{status}</p>}
      <div>
        <button onClick={approveAndDeposit}>Approve & Deposit</button>
        <button onClick={depositWithPermit}>Deposit with Permit</button>
        <button onClick={approveAndWithdraw}>Withdraw</button>
      </div>
      <button onClick={logout}>Disconnect</button>
    </main>
  );
}
  • Replace hard-coded addresses with vault selections sourced from the API.
  • Add React Query mutations around each helper to display optimistic states and capture errors.
  • Surface APIError payloads in telemetry and show a fallback UI for unsupported assets.
  • Extract transaction 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.***