> ## Documentation Index
> Fetch the complete documentation index at: https://starkware-9575960b-starkzapv4.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Privy Integration

> Server-side key management and wallet infrastructure for Starknet applications

<img src="https://mintcdn.com/starkware-9575960b-starkzapv4/Q0Mo-yiQRCwOcerg/assets/starkzap/privy-integration.png?fit=max&auto=format&n=Q0Mo-yiQRCwOcerg&q=85&s=9da5c79e4dc9a6258ba54ca407006c21" alt="Privy integration hero" width="1024" height="576" data-path="assets/starkzap/privy-integration.png" />

## Overview

[Privy](https://privy.io) provides secure, server-side key management for Starknet wallets. With Privy, you can:

* Create and manage Starknet wallets for users
* Sign transactions server-side without exposing private keys
* Support social login and email-based authentication
* Integrate with existing Privy authentication flows

Privy supports Starknet as a [Tier 2 chain](https://docs.privy.io/recipes/use-tier-2#starknet), allowing you to use Privy's raw sign functionality for transaction signing.

## Why Use Privy?

* ✅ **Security**: Private keys never leave Privy's secure infrastructure
* ✅ **User Experience**: Social login and email-based authentication
* ✅ **Server-Side Signing**: Sign transactions on your backend
* ✅ **Multi-Chain**: Same infrastructure for multiple blockchains
* ✅ **Gasless Transactions**: Configure AVNU Paymaster for sponsored transactions (see [AVNU Paymaster Integration](/build/starkzap/integrations/avnu-paymaster))

## Wallet ownership model

Privy supports two ways to use wallets. Choosing the wrong one can lead to JWT or auth errors, so use this as a guide:

| Wallet type | Requires user JWT? | Best for |
| - | - | - |
| **Server-managed** | No | Backend signing, custodial flows |
| **User-owned** | Yes | End-user auth, user-linked wallets |

* **Server-managed**: Create wallets without an owner (omit `owner`). Your backend signs with `PrivyClient` (app credentials) and no end-user JWT is required. Create with just `{ chain_type: "starknet" }`. Best for custodial / backend-only flows.
* **User-owned**: Create wallets with `owner: { user_id }` (the user's DID) so the wallet is tied to a Privy user; access and signing then require that user's JWT. **This is the model the examples below use** — the client authenticates with a Privy client SDK, and the backend verifies the token before creating/using the wallet.

Note: `owner: { user_id }` takes the user's DID (`did:privy:...`). The separate `owner_id` field expects a **cuid2 key-quorum id**, not a user DID — passing a DID there fails with an `Invalid cuid2` error.

## Setup

### 1. Install Privy

```bash theme={null}
npm install @privy-io/node
```

### 2. Initialize Privy Client

```typescript theme={null}
import { PrivyClient } from "@privy-io/node";

const privy = new PrivyClient({
  appId: process.env.PRIVY_APP_ID!,
  appSecret: process.env.PRIVY_APP_SECRET!,
});
```

### 3. Create a Starknet Wallet

```typescript theme={null}
// Server-managed wallet (app-owned) — no end-user JWT required to sign.
const wallet = await privy.wallets().create({ chain_type: "starknet" });

// Or tie the wallet to a Privy user (user-owned). Note: it's `owner: { user_id }`
// with the user's DID — not a top-level `user_id`, and not `owner_id` (that field
// is a cuid2 key-quorum id, not a user DID). Signing then requires the user's JWT.
// const wallet = await privy.wallets().create({
//   chain_type: "starknet",
//   owner: { user_id: "did:privy:..." },
// });

console.log("Wallet ID:", wallet.id);
console.log("Wallet Address:", wallet.address);
console.log("Public Key:", wallet.public_key);
```

## Integration with Starkzap

### Server-Side Signing Endpoint

Create an endpoint that signs transaction hashes using Privy:

```typescript theme={null}
import express from "express";

app.post("/api/wallet/sign", async (req, res) => {
  const { walletId, hash } = req.body;
  
  if (!walletId || !hash) {
    return res.status(400).json({ error: "walletId and hash required" });
  }

  try {
    const result = await privy.wallets().rawSign(walletId, {
      params: { hash },
    });
    
    res.json({ signature: result.signature });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});
```

### Client-Side Integration

> The access token comes from a Privy **client** SDK — `@privy-io/js-sdk-core`
> (vanilla JS/Svelte/Vue), `@privy-io/react-auth` (React), or `@privy-io/expo`
> (React Native) — after the user logs in. The `@privy-io/node` `PrivyClient`
> shown above is **server-only** and has no `getAccessToken()`. Below, `privy`
> refers to the initialized client SDK instance.

Use Privy with Starkzap:

```typescript theme={null}
import { StarkZap, PrivySigner, OnboardStrategy, accountPresets } from "starkzap";

const sdk = new StarkZap({ network: "sepolia" });
// `privy` here is your Privy client SDK instance (see note above).
const accessToken = await privy.getAccessToken();

// Option 1: Using onboard API (recommended)
const onboard = await sdk.onboard({
  strategy: OnboardStrategy.Privy,
  accountPreset: accountPresets.argentXV050,
  privy: {
    resolve: async () => {
      // Get Privy signer context (walletId + publicKey) from your backend
      const walletRes = await fetch("https://your-api.example/api/wallet/starknet", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${accessToken}`,
        },
      });
      const walletData = await walletRes.json();

      return {
        walletId: walletData.wallet.id,
        publicKey: walletData.wallet.publicKey,
        serverUrl: "https://your-api.example/api/wallet/sign",
      };
    },
  },
  deploy: "if_needed",
});

const wallet = onboard.wallet;

// Option 2: Using PrivySigner directly (reuse accessToken)
const walletRes = await fetch("https://your-api.example/api/wallet/starknet", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${accessToken}`,
  },
});
const { wallet: privyWallet } = await walletRes.json();

const signer = new PrivySigner({
  walletId: privyWallet.id,
  publicKey: privyWallet.publicKey,
  serverUrl: "https://your-api.example/api/wallet/sign",
});

const walletFromSigner = await sdk.connectWallet({
  account: { signer, accountClass: accountPresets.argentXV050 },
});
```

## Complete Example

### Backend (Express.js)

```typescript theme={null}
import express from "express";
import { PrivyClient } from "@privy-io/node";
import cors from "cors";

const privy = new PrivyClient({
  appId: process.env.PRIVY_APP_ID!,
  appSecret: process.env.PRIVY_APP_SECRET!,
});

const app = express();
app.use(cors());
app.use(express.json());

// Verify the Privy access token → the authenticated user's DID.
async function auth(req, res, next) {
  const token = req.headers.authorization?.replace("Bearer ", "");
  if (!token) return res.status(401).json({ error: "Missing token" });
  try {
    const claims = await privy.utils().auth().verifyAccessToken(token);
    req.userId = claims.user_id; // e.g. "did:privy:..."
    next();
  } catch {
    res.status(401).json({ error: "Invalid token" });
  }
}

// Create or get the user's Starknet wallet.
// In production, cache the wallet per user (e.g. in a database) so you don't
// create a new one on every call.
app.post("/api/wallet/starknet", auth, async (req, res) => {
  try {
    const wallet = await privy.wallets().create({
      chain_type: "starknet",
      owner: { user_id: req.userId }, // user's DID from the verified token
    });

    res.json({
      wallet: {
        id: wallet.id,
        address: wallet.address,
        publicKey: wallet.public_key,
      },
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Sign transaction hash
app.post("/api/wallet/sign", async (req, res) => {
  const { walletId, hash } = req.body;
  
  try {
    const result = await privy.wallets().rawSign(walletId, {
      params: { hash },
    });
    
    res.json({ signature: result.signature });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(3001);
```

### Frontend

```typescript theme={null}
import { StarkZap, OnboardStrategy, accountPresets } from "starkzap";

const sdk = new StarkZap({ network: "sepolia" });
// `privy` is your Privy client SDK instance (js-sdk-core / react-auth / expo),
// not the server-only @privy-io/node PrivyClient.
const accessToken = await privy.getAccessToken();

// Connect with SDK
const onboard = await sdk.onboard({
  strategy: OnboardStrategy.Privy,
  accountPreset: accountPresets.argentXV050,
  privy: {
    resolve: async () => {
      const walletRes = await fetch("http://localhost:3001/api/wallet/starknet", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${accessToken}`,
        },
      });
      const { wallet } = await walletRes.json();

      return {
        walletId: wallet.id,
        publicKey: wallet.publicKey,
        serverUrl: "http://localhost:3001/api/wallet/sign",
      };
    },
  },
  deploy: "if_needed",
});

const connectedWallet = onboard.wallet;

// Use the wallet
const balance = await connectedWallet.balanceOf(STRK);
console.log(balance.toFormatted());
```

## React Native Integration

For React Native applications, use `@privy-io/expo`:

```typescript theme={null}
import { PrivyProvider } from "@privy-io/expo";
import { StarkZap, OnboardStrategy } from "starkzap";

// Wrap your app with PrivyProvider (expo needs both appId and clientId)
<PrivyProvider appId={PRIVY_APP_ID} clientId={PRIVY_CLIENT_ID}>
  <App />
</PrivyProvider>

// In your component
const privyClient = usePrivy();
const accessToken = await privyClient.getAccessToken();
const walletRes = await fetch("https://your-api.example/api/wallet/starknet", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${accessToken}`,
  },
});
const { wallet: walletData } = await walletRes.json();

const onboard = await sdk.onboard({
  strategy: OnboardStrategy.Privy,
  deploy: "never",
  privy: {
    resolve: async () => ({
      walletId: walletData.id,
      publicKey: walletData.publicKey,
      serverUrl: "https://your-api.example/api/wallet/sign",
    }),
  },
});
```

## Resources

* [Privy Documentation](https://docs.privy.io)
* [Starknet Tier 2 Support](https://docs.privy.io/recipes/use-tier-2#starknet)
* [Privy Node SDK](https://docs.privy.io/reference/server-sdk-node)
* [Privy Expo SDK](https://docs.privy.io/reference/react-native-sdk)

## Best Practices

1. **Never expose private keys** - Always use server-side signing
2. **Authenticate requests** - Verify user identity before signing
3. **Use HTTPS** - Always use secure connections for signing endpoints
4. **Handle errors gracefully** - Provide user-friendly error messages
5. **Monitor usage** - Track wallet creation and signing operations


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.