> ## Documentation Index
> Fetch the complete documentation index at: https://docs.chipipay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# useChipiSession

> Unified session key management for gasless UX - create, register, execute, and revoke sessions with a single hook.

## Overview

`useChipiSession` provides a unified API for managing session keys, enabling gasless transactions without requiring the owner's signature for each operation.

This hook combines all session-related operations:

* **Session creation** (local keypair generation)
* **Session registration** (on-chain)
* **Transaction execution** (using session key)
* **Session revocation** (on-chain)
* **Status checking** (remaining calls, expiration)

<Warning>
  Session keys only work with **CHIPI wallets**. READY wallets do not support session keys.
</Warning>

## Prerequisites

Before using `useChipiSession`, you need:

1. A **CHIPI wallet** (check `wallet.walletType === "CHIPI"` or `wallet.supportsSessionKeys`)
2. The user's **encryption key (PIN)**
3. An **authentication token** (from Clerk, Firebase, etc.)

## Session Lifecycle

```mermaid theme={null}
stateDiagram-v2
    [*] --> none: No session
    none --> created: createSession()
    created --> active: registerSession()
    active --> active: executeWithSession()
    active --> revoked: revokeSession()
    active --> expired: Time/calls exhausted
```

| State     | Description                                          |
| --------- | ---------------------------------------------------- |
| `none`    | No session exists                                    |
| `created` | Session created locally, not yet registered on-chain |
| `active`  | Session registered and usable                        |
| `expired` | Session has expired (time or call limit)             |
| `revoked` | Session has been revoked on-chain                    |

## Quick Start

```tsx theme={null}
import { useAuth } from "@clerk/nextjs";
import { useChipiWallet, useChipiSession } from "@chipi-stack/nextjs";

function SessionComponent() {
  const { userId, getToken } = useAuth();
  const [pin, setPin] = useState("");
  
  const { wallet } = useChipiWallet({
    externalUserId: userId,
    getBearerToken: getToken,
  });
  
  const {
    session,
    sessionState,
    hasActiveSession,
    remainingCalls,
    createSession,
    registerSession,
    executeWithSession,
    isCreating,
    isRegistering,
    isExecuting,
  } = useChipiSession({
    wallet,
    encryptKey: pin,
    getBearerToken: getToken,
  });

  // Setup session (create + register)
  const handleSetup = async () => {
    await createSession();
    await registerSession();
  };

  // Execute a transfer using the session
  const handleTransfer = async () => {
    await executeWithSession([{
      contractAddress: "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb",
      entrypoint: "transfer",
      calldata: [recipientAddress, amount, "0x0"],
    }]);
  };

  return (
    <div>
      <p>Session: {sessionState}</p>
      {hasActiveSession && <p>Calls remaining: {remainingCalls}</p>}
      
      {!hasActiveSession ? (
        <button onClick={handleSetup} disabled={isCreating || isRegistering}>
          Setup Session
        </button>
      ) : (
        <button onClick={handleTransfer} disabled={isExecuting}>
          Transfer (Gasless)
        </button>
      )}
    </div>
  );
}
```

## Configuration Options

```typescript theme={null}
interface UseChipiSessionConfig {
  // Required
  wallet: SessionWallet | null | undefined;
  encryptKey: string;
  getBearerToken: () => Promise<string | null | undefined>;
  
  // Optional
  storedSession?: SessionKeyData | null;
  onSessionCreated?: (session: SessionKeyData) => void | Promise<void>;
  defaultDurationSeconds?: number;  // Default: 21600 (6 hours)
  defaultMaxCalls?: number;         // Default: 1000
  autoCheckStatus?: boolean;        // Default: true
}
```

| Option                   | Type                    | Default  | Description                      |
| ------------------------ | ----------------------- | -------- | -------------------------------- |
| `wallet`                 | `SessionWallet`         | Required | Wallet data (must be CHIPI type) |
| `encryptKey`             | `string`                | Required | User's PIN for signing           |
| `getBearerToken`         | `() => Promise<string>` | Required | Auth token function              |
| `storedSession`          | `SessionKeyData`        | `null`   | Previously stored session        |
| `onSessionCreated`       | `(session) => void`     | -        | Callback to persist new session  |
| `defaultDurationSeconds` | `number`                | `21600`  | Session duration (6 hours)       |
| `defaultMaxCalls`        | `number`                | `1000`   | Max calls per session            |
| `autoCheckStatus`        | `boolean`               | `true`   | Auto-fetch on-chain status       |

## Return Values

### Session Data

| Property           | Type                     | Description                      |
| ------------------ | ------------------------ | -------------------------------- |
| `session`          | `SessionKeyData \| null` | Current session data             |
| `sessionStatus`    | `SessionDataResponse`    | On-chain session status          |
| `sessionState`     | `SessionState`           | Lifecycle state                  |
| `hasActiveSession` | `boolean`                | Whether session is usable        |
| `isSessionExpired` | `boolean`                | Whether session has expired      |
| `remainingCalls`   | `number \| undefined`    | Calls left in session            |
| `supportsSession`  | `boolean`                | Whether wallet supports sessions |

### Actions

| Method                      | Returns                   | Description                        |
| --------------------------- | ------------------------- | ---------------------------------- |
| `createSession(config?)`    | `Promise<SessionKeyData>` | Create new session locally         |
| `registerSession(config?)`  | `Promise<string>`         | Register on-chain (returns txHash) |
| `revokeSession()`           | `Promise<string>`         | Revoke on-chain (returns txHash)   |
| `executeWithSession(calls)` | `Promise<string>`         | Execute calls (returns txHash)     |
| `clearSession()`            | `void`                    | Clear local session state          |
| `refetchStatus()`           | `Promise<void>`           | Refresh on-chain status            |

### Loading States

| Property          | Description                  |
| ----------------- | ---------------------------- |
| `isCreating`      | Creating session locally     |
| `isRegistering`   | Registering session on-chain |
| `isRevoking`      | Revoking session on-chain    |
| `isExecuting`     | Executing transaction        |
| `isLoadingStatus` | Fetching on-chain status     |

## Persisting Sessions

`SessionKeyData` contains an `encryptedPrivateKey`. Where you store it matters — the SDK does not persist sessions itself; this is your responsibility.

<Warning>
  **Do not store `SessionKeyData` in Clerk `unsafeMetadata`.** `unsafeMetadata` is client-writable and embedded in the Clerk session JWT, which means the encrypted session key is exposed to the browser on every session refresh. Combined with a low-entropy user PIN (e.g. 6 digits = 10⁶ combinations), the ciphertext is brute-forceable offline by anyone who obtains a valid session token.

  Use a server-side route that writes to Clerk **`privateMetadata`** (server-only, not in the JWT), or store the session in your own database. See the recommended pattern below.
</Warning>

### Recommended: server-side route + `privateMetadata`

Persist `SessionKeyData` from a server route after verifying the user's auth token. Chipi's backend SDK ships JWKS verification (`iss` + `aud` validation) for Clerk, Firebase, BetterAuth, and generic providers — use it to authenticate the route. See the [Gasless setup guides](/sdk/nextjs/gasless-clerk-setup) for JWKS configuration.

**1. Server route — `app/api/chipi/session/route.ts`:**

```ts theme={null}
import { auth, clerkClient } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
import type { SessionKeyData } from "@chipi-stack/types";

// GET — return the stored session (server-only, never exposed in JWT)
export async function GET() {
  const { userId } = await auth();
  if (!userId) return NextResponse.json({ error: "unauthorized" }, { status: 401 });

  const client = await clerkClient();
  const user = await client.users.getUser(userId);
  const session = user.privateMetadata?.chipiSession as SessionKeyData | undefined;
  return NextResponse.json({ session: session ?? null });
}

// POST — persist a newly created session
export async function POST(req: Request) {
  const { userId } = await auth();
  if (!userId) return NextResponse.json({ error: "unauthorized" }, { status: 401 });

  const body = await req.json().catch(() => null);
  if (!body || typeof body !== "object" || !("session" in body)) {
    return NextResponse.json({ error: "invalid_session_payload" }, { status: 400 });
  }
  const { session } = body as { session: SessionKeyData };

  // updateUserMetadata performs a deep merge — using updateUser() here would
  // replace the entire privateMetadata object and clobber any other keys.
  const client = await clerkClient();
  await client.users.updateUserMetadata(userId, {
    privateMetadata: { chipiSession: session },
  });
  return NextResponse.json({ ok: true });
}
```

**2. Client — load and persist via the route:**

```tsx theme={null}
"use client";

import { useEffect, useState } from "react";
import { useChipiWallet, useChipiSession } from "@chipi-stack/nextjs";
import type { SessionKeyData } from "@chipi-stack/types";

function PersistentSession() {
  const { wallet } = useChipiWallet({ ... });
  const [storedSession, setStoredSession] = useState<SessionKeyData | null>(null);

  // Hydrate from server on mount
  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const r = await fetch("/api/chipi/session");
        if (!r.ok) return;
        const d = (await r.json()) as { session?: SessionKeyData | null };
        if (!cancelled) setStoredSession(d.session ?? null);
      } catch {
        if (!cancelled) setStoredSession(null);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, []);

  const { session, hasActiveSession, createSession, registerSession } = useChipiSession({
    wallet,
    encryptKey: pin,
    getBearerToken: getToken,
    storedSession,
    onSessionCreated: async (newSession) => {
      const r = await fetch("/api/chipi/session", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ session: newSession }),
      });
      if (!r.ok) throw new Error(`Failed to persist session: ${r.status}`);
    },
  });

  // Session is restored on mount — and never leaks via the JWT
}
```

### Reduce the brute-force surface

Even with server-side storage, the encryption strength of `encryptedPrivateKey` depends on the encryption key:

* **PIN-only wallets** — a 6-digit PIN has \~20 bits of entropy; an attacker who obtains the ciphertext can crack it offline in seconds. Treat the encrypted session key as sensitive even at rest.
* **Passkey wallets (recommended)** — the encryption key is hardware-backed via WebAuthn PRF and never leaves the device's secure element. This is the production-grade default. See the [PIN → passkey migration guide](/sdk/guides/pin-to-passkey-migration).

### Minimal session whitelist

When registering a session, restrict `allowedEntrypoints` to the smallest set your flow actually needs. Avoid including broad approval entrypoints (`approve`, `set_approval_for_all`) unless required — a session with these permissions can drain tokens within the validity window if compromised.

## Executing Transactions

The `executeWithSession` method accepts an array of Starknet calls:

```typescript theme={null}
// Single transfer
await executeWithSession([{
  contractAddress: USDC_CONTRACT,
  entrypoint: "transfer",
  calldata: [recipient, amount, "0x0"],
}]);

// Batch multiple calls
await executeWithSession([
  {
    contractAddress: USDC_CONTRACT,
    entrypoint: "approve",
    calldata: [spender, amount, "0x0"],
  },
  {
    contractAddress: DEX_CONTRACT,
    entrypoint: "swap",
    calldata: [...],
  },
]);
```

## Custom Session Configuration

### Custom Duration

```typescript theme={null}
// Create session valid for 1 hour
await createSession({ durationSeconds: 3600 });
```

### Custom Max Calls

```typescript theme={null}
// Register with 500 max calls
await registerSession({ maxCalls: 500 });
```

### Allowed Entrypoints

Restrict which contract methods the session can call:

```typescript theme={null}
await registerSession({
  allowedEntrypoints: [
    "0x..." // Only allow specific function selectors
  ],
});
```

## Error Handling

```tsx theme={null}
const { error, session } = useChipiSession({...});

// Check for errors
if (error) {
  console.error("Session error:", error.message);
}

// Handle specific errors
const handleSetup = async () => {
  try {
    await createSession();
    await registerSession();
  } catch (err) {
    if (err.message.includes("does not support sessions")) {
      alert("Please use a CHIPI wallet for sessions");
    } else if (err.message.includes("expired")) {
      alert("Session has expired, please create a new one");
    } else {
      alert(`Error: ${err.message}`);
    }
  }
};
```

## Complete Example

```tsx theme={null}
"use client";

import { useState, useEffect } from "react";
import { useAuth } from "@clerk/nextjs";
import { useChipiWallet, useChipiSession } from "@chipi-stack/nextjs";
import type { SessionKeyData } from "@chipi-stack/types";

export function GaslessTransfers() {
  const { getToken, userId } = useAuth();
  const [pin, setPin] = useState("");
  const [recipient, setRecipient] = useState("");
  const [amount, setAmount] = useState("");

  const { wallet, formattedBalance } = useChipiWallet({
    externalUserId: userId,
    getBearerToken: getToken,
  });

  // Hydrate session from a server-side route that reads Clerk privateMetadata
  // (NOT unsafeMetadata — see the "Persisting Sessions" section above).
  const [storedSession, setStoredSession] = useState<SessionKeyData | null>(null);
  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const r = await fetch("/api/chipi/session");
        if (!r.ok) return;
        const d = (await r.json()) as { session?: SessionKeyData | null };
        if (!cancelled) setStoredSession(d.session ?? null);
      } catch {
        if (!cancelled) setStoredSession(null);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, []);

  const {
    session,
    sessionState,
    hasActiveSession,
    remainingCalls,
    supportsSession,
    createSession,
    registerSession,
    executeWithSession,
    revokeSession,
    isCreating,
    isRegistering,
    isExecuting,
    isRevoking,
    error,
  } = useChipiSession({
    wallet,
    encryptKey: pin,
    getBearerToken: getToken,
    storedSession,
    onSessionCreated: async (newSession) => {
      const r = await fetch("/api/chipi/session", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ session: newSession }),
      });
      if (!r.ok) throw new Error(`Failed to persist session: ${r.status}`);
    },
  });

  if (!wallet?.supportsSessionKeys) {
    return <p>Sessions require a CHIPI wallet</p>;
  }

  return (
    <div className="space-y-6">
      {/* PIN Input */}
      <input
        type="password"
        placeholder="Enter PIN"
        value={pin}
        onChange={(e) => setPin(e.target.value)}
        maxLength={4}
      />

      {/* Session Status */}
      <div className="p-4 bg-gray-100 rounded">
        <p>State: <strong>{sessionState}</strong></p>
        {hasActiveSession && (
          <p>Remaining calls: {remainingCalls}</p>
        )}
      </div>

      {/* Session Setup */}
      {!hasActiveSession && (
        <button
          onClick={async () => {
            await createSession();
            await registerSession();
          }}
          disabled={isCreating || isRegistering || !pin}
        >
          {isCreating ? "Creating..." : isRegistering ? "Registering..." : "Setup Session"}
        </button>
      )}

      {/* Transfer Form */}
      {hasActiveSession && (
        <div className="space-y-4">
          <input
            placeholder="Recipient address"
            value={recipient}
            onChange={(e) => setRecipient(e.target.value)}
          />
          <input
            type="number"
            placeholder="Amount"
            value={amount}
            onChange={(e) => setAmount(e.target.value)}
          />
          <button
            onClick={async () => {
              await executeWithSession([{
                contractAddress: "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb",
                entrypoint: "transfer",
                calldata: [recipient, amount, "0x0"],
              }]);
            }}
            disabled={isExecuting}
          >
            {isExecuting ? "Sending..." : "Send USDC (Gasless)"}
          </button>
        </div>
      )}

      {/* Revoke Session */}
      {hasActiveSession && (
        <button
          onClick={revokeSession}
          disabled={isRevoking}
          className="text-red-600"
        >
          {isRevoking ? "Revoking..." : "Revoke Session"}
        </button>
      )}

      {/* Error Display */}
      {error && (
        <p className="text-red-500">{error.message}</p>
      )}
    </div>
  );
}
```
