Skip to main content

Using Privy Wallets

If you use Privy to manage wallets for your users, you can connect those wallets to Legend to access DeFi yield, swaps, and transfers — without changing your key management setup. This guide covers the integration pattern: how a Privy embedded wallet becomes a Legend sub-account signer, and how to sign Legend plans using Privy’s secp256k1_sign method.

How it fits together

Privy handles wallet creation and key management. Legend handles DeFi operations. The two connect through the wallet’s EOA address. Legend never sees the private key. Privy holds it. Your app passes the plan’s EIP-712 digest to Privy for signing, then relays the signature back to Legend for execution.

Prerequisites

  • A Legend Prime Account with a query key (contact us to get set up)
  • A Privy app with embedded wallets enabled
  • legend-prime and @privy-io/react-auth installed
npm install legend-prime @privy-io/react-auth

Step 1 — Get the Privy wallet address

When a user authenticates with Privy, they get an embedded wallet. Use its address as the Legend signer.
import { useWallets } from "@privy-io/react-auth";

const { wallets } = useWallets();
const embeddedWallet = wallets.find((w) => w.walletClientType === "privy");
const signerAddress = embeddedWallet.address;
This is a standard Ethereum EOA address. Legend doesn’t need to know that Privy manages the key behind it.

Step 2 — Create a Legend sub-account

Pass the wallet address to Legend as an eoa signer. This is typically done from your backend.
import { LegendPrime } from "legend-prime";

const client = new LegendPrime({
  queryKey: process.env.LEGEND_QUERY_KEY,
});

const account = await client.accounts.create({
  signerType: "eoa",
  signerAddress: signerAddress, // The Privy wallet address
});

// Save these — you'll need them
const accountId = account.account_id;
const walletAddress = account.legend_wallet_address;
Legend creates an on-chain smart wallet (Quark Wallet) for the account. The legend_wallet_address is where funds live.
Fund the Legend wallet, not the Privy EOA. Send assets to legend_wallet_address on a supported network (Base, Ethereum Mainnet, Arbitrum, or Optimism). The Privy wallet address is the signer, not the asset holder.

Step 3 — Create a plan

When your user wants to earn yield (or swap, transfer, etc.), create a plan through Legend. The plan response includes an EIP-712 digest — a hash that needs to be signed by the wallet’s key.
const plan = await client.plan.earn(accountId, {
  amount: "1000000", // 1 USDC (6 decimals)
  asset: "USDC",
  network: "base",
  protocol: "compound",
});

const digest = plan.details.eip712_data.digest; // "0x8a3f...b7c2"
Plans expire after 2 minutes. Sign and execute before expires_at or create a new plan.

Step 4 — Sign the digest with Privy

This is where the two systems meet. Pass the EIP-712 digest to Privy’s secp256k1_sign method, which performs raw curve-level signing over the hash — exactly what Legend expects.
const provider = await embeddedWallet.getEthereumProvider();

const signature = await provider.request({
  method: "secp256k1_sign",
  params: [digest],
});
secp256k1_sign signs the hash directly without any prefix or transformation. This is the same operation that viem’s signer.sign({ hash }) performs — just routed through Privy’s key management instead of a local private key.
Don’t use personal_sign or eth_signTypedData_v4 here. personal_sign adds an Ethereum message prefix that produces the wrong signature. eth_signTypedData_v4 requires the full EIP-712 typed data object and recomputes the digest internally — unnecessary since Legend already provides the digest.

Step 5 — Execute the plan

Send the signature back to Legend. This triggers the on-chain transaction.
const result = await client.plan.execute(accountId, {
  planId: plan.plan_id,
  signature,
});

console.log(result.status); // "executing"

Step 6 — Monitor

Poll activities or use long-polling events to confirm the transaction.
const { activities } = await client.accounts.activities(accountId);
console.log(activities[0].activity_status); // "pending" → "confirmed"

Architecture patterns

Privy + Legend integrations typically follow one of two patterns:

User-facing app

Your frontend handles Privy auth and signing. Your backend handles Legend API calls. Users see an “Earn Yield” button, click it, approve the signing request, and funds are deployed.
LayerResponsibility
FrontendPrivy auth, wallet access, secp256k1_sign
BackendLegend SDK, plan creation, plan execution

Automated backend

Your backend manages Privy wallets via the server SDK and signs on behalf of users (with their consent). No user interaction needed for signing.
import Privy from "@privy-io/node";

const privy = new Privy({ appId: "...", appSecret: "..." });

// Sign the digest server-side
const { signature } = await privy
  .wallets()
  .ethereum()
  .secp256k1Sign(walletId, {
    params: { hash: digest },
  });
This pattern works well for omnibus accounts or automated strategies where your server controls the signing flow.

Full example

Here’s a complete React component that creates an earn plan and signs it with Privy:
import { useWallets } from "@privy-io/react-auth";
import { LegendPrime } from "legend-prime";

const client = new LegendPrime({
  queryKey: process.env.NEXT_PUBLIC_LEGEND_QUERY_KEY,
});

function EarnButton({ accountId }: { accountId: string }) {
  const { wallets } = useWallets();

  async function handleEarn() {
    const wallet = wallets.find((w) => w.walletClientType === "privy");
    const provider = await wallet.getEthereumProvider();

    // 1. Create the plan
    const plan = await client.plan.earn(accountId, {
      amount: "1000000",
      asset: "USDC",
      network: "base",
      protocol: "compound",
    });

    // 2. Sign the EIP-712 digest with Privy
    const signature = await provider.request({
      method: "secp256k1_sign",
      params: [plan.details.eip712_data.digest],
    });

    // 3. Execute
    await client.plan.execute(accountId, {
      planId: plan.plan_id,
      signature,
    });
  }

  return <button onClick={handleEarn}>Earn 1 USDC on Compound</button>;
}

Next steps