# Ekx Privy

> Wallet and auth layer for Ekinoxis dApps using Privy — embedded wallets, email/social/wallet login, server-side token verification, and wagmi integration. Use when adding login to a dApp, creating embedded wallets for users, verifying a Privy access token in an API route or server action, gating a page or contract call behind authentication, or debugging "user is not authenticated"/wallet-not-found issues. Covers @privy-io/react-auth, @privy-io/server-auth and @privy-io/wagmi.

- Skill: `ekinoxis-evm/ekx-privy` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ekinoxis-evm/ekx-privy`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ekinoxis-evm/ekx-privy/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: Ekinoxis-evm (https://skillmd.com/u/ekinoxis-evm)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/ekinoxis-evm/ekx-privy

---


# Privy

The default wallet + auth layer for Ekinoxis user-facing dApps.

Authoritative docs: the **`privy-docs` MCP server**. Query it before answering from
memory — Privy's API moves fast.
Web: https://docs.privy.io · Dashboard: https://dashboard.privy.io

---

## Why Privy and not RainbowKit

Privy gives us **embedded wallets** — a user logs in with email or Google and gets a
real EVM wallet without ever seeing a seed phrase. That is the whole reason it won:
our users (LATAM real-estate buyers, gamers, renters) are not crypto-native and will
not install MetaMask.

Use **RainbowKit/wagmi alone** only when the audience is definitionally crypto-native.
Use **Coinbase CDP** instead when an *agent*, not a person, holds the key — see
[`../ekx-coinbase-cdp/SKILL.md`](../ekx-coinbase-cdp/SKILL.md).

---

## Environment

```bash
NEXT_PUBLIC_PRIVY_APP_ID=          # public — safe in the bundle
NEXT_PUBLIC_PRIVY_CLIENT_ID=       # public
PRIVY_APP_SECRET=                  # SECRET — server only
PRIVY_VERIFICATION_KEY=            # SECRET — verifies access tokens offline
```

---

## Client setup

The provider, as it actually stands in production:

```tsx
"use client";
import { PrivyProvider } from "@privy-io/react-auth";

export default function Providers({ children }: { children: React.ReactNode }) {
  return (
    <PrivyProvider
      appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID!}
      config={{
        loginMethods: ["email", "wallet", "google"],
        appearance: { theme: "dark", accentColor: "#ff7a45" },
        embeddedWallets: { ethereum: { createOnLogin: "users-without-wallets" } },
      }}
    >
      {children}
    </PrivyProvider>
  );
}
```

`createOnLogin: "users-without-wallets"` is the setting that matters — it mints an
embedded wallet for email/Google users while letting existing wallet users bring their own.

### Reading auth state

```tsx
const { ready, authenticated, user, login, logout } = usePrivy();

// ALWAYS gate on `ready` first. Before it flips true, `authenticated` is false
// even for a logged-in user — this is the #1 source of login-flicker bugs.
if (!ready) return <Skeleton />;
if (!authenticated) return <button onClick={login}>Entrar</button>;
```

### Getting the wallet

```tsx
const { wallets } = useWallets();
const wallet = wallets[0];              // embedded wallet is index 0
await wallet.switchChain(84532);        // Base Sepolia — do this before any tx
const provider = await wallet.getEthereumProvider();
```

---

## wagmi integration

Use `@privy-io/wagmi` so that wagmi hooks (`useReadContract`, `useWriteContract`)
work against the Privy wallet.

```tsx
import { WagmiProvider, createConfig } from "@privy-io/wagmi";
import { base, baseSepolia } from "viem/chains";
import { http } from "wagmi";

export const wagmiConfig = createConfig({
  chains: [baseSepolia],
  transports: { [baseSepolia.id]: http(process.env.NEXT_PUBLIC_BASE_SEPOLIA_RPC_URL) },
});
```

Nesting order is **PrivyProvider → QueryClientProvider → WagmiProvider**. Getting this
wrong produces "WagmiProvider not found" at runtime even though it is in the tree.

---

## Server-side verification

Never trust a wallet address sent from the browser. Verify the access token.

```ts
import { PrivyClient } from "@privy-io/server-auth";

const privy = new PrivyClient(
  process.env.NEXT_PUBLIC_PRIVY_APP_ID!,
  process.env.PRIVY_APP_SECRET!,
);

export async function requireUser(req: Request) {
  const token = req.headers.get("authorization")?.replace("Bearer ", "");
  if (!token) throw new Error("unauthenticated");

  const claims = await privy.verifyAuthToken(token, process.env.PRIVY_VERIFICATION_KEY);
  const user = await privy.getUser(claims.userId);
  return user;
}
```

On the client, get the token with `const token = await getAccessToken()` from
`usePrivy()` and send it as `Authorization: Bearer …`.

---

## Gotchas we have hit

1. **`ready` before `authenticated`.** Always. See above.
2. **Embedded wallets do not auto-switch chains.** Call `switchChain` before every write, or the tx silently lands on the wrong network.
3. **`user.wallet.address` can be undefined** right after first login while the embedded wallet is still being provisioned. Poll `useWallets()` instead.
4. **The verification key is not the app secret.** `verifyAuthToken` with the wrong one fails with a confusing signature error.
5. **Allowed domains.** Every deploy URL — including Vercel preview URLs — must be added in the Privy dashboard, or login fails silently in preview. Add the wildcard `*.vercel.app` for the project.
6. **Server-side `getUser` costs a network round-trip.** `verifyAuthToken` alone is offline and enough for authorization; only call `getUser` when you need the linked accounts.

