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

# Identity & Wallets

> How Mouth handles user identity via X login and embedded wallets via Privy.

## Overview

Mouth uses **Privy** for authentication and wallet management. Users sign in with their X (Twitter) account, and Privy automatically creates an **embedded wallet** for them on Base.

This eliminates the need for users to:

* Install MetaMask or any browser extension
* Manage private keys or seed phrases
* Bridge funds from other chains manually

## Authentication Flow

```mermaid theme={null}
sequenceDiagram
    participant U as User
    participant App as Mouth Web App
    participant Privy as Privy SDK
    participant X as X (Twitter) OAuth
    participant Base as Base Network

    U->>App: Clicks "Login with X"
    App->>Privy: Initiate X OAuth
    Privy->>X: OAuth redirect
    X-->>Privy: Auth token + X profile data
    Privy-->>Privy: Create/retrieve embedded wallet
    Privy-->>App: Session created
    App-->>U: Logged in, wallet ready
```

## Identity Mapping

When a user logs in for the first time, Mouth stores:

| Field           | Source  | Description                               |
| --------------- | ------- | ----------------------------------------- |
| `userId`        | Privy   | Unique Privy user ID                      |
| `xHandle`       | X OAuth | The user's X handle (e.g., `@cryptoguru`) |
| `xId`           | X OAuth | The user's immutable X numeric ID         |
| `walletAddress` | Privy   | The embedded wallet address on Base       |
| `createdAt`     | System  | Registration timestamp                    |

<Note>
  We store the X **numeric ID** (not just the handle) because users can change their handle. The numeric ID is immutable and ensures we always map to the correct X account.
</Note>

## Pre-Generated Wallets (Unregistered Opponents)

When a Challenger creates a bet against an X handle that hasn't signed up on Mouth yet, the backend uses **Privy's server-side SDK** to pre-generate an embedded wallet for that user.

### How It Works

```mermaid theme={null}
sequenceDiagram
    participant C as Challenger
    participant BE as Backend
    participant Privy as Privy Server SDK
    participant SC as Smart Contract

    C->>BE: Create bet against @cryptoguru
    BE->>BE: Check DB — @cryptoguru not registered
    BE->>Privy: getByTwitterUsername("cryptoguru")
    Privy-->>BE: User not found
    BE->>Privy: Create user (username: cryptoguru) + pre-generate wallet
    Privy-->>BE: User created, wallet: 0xABC...
    BE->>BE: Store user in DB (pre_registered = true)
    BE->>SC: factory.createBet(..., opponent: 0xABC..., ...)
    Note over Privy: Days later...
    Note over Privy: @cryptoguru logs in with X
    Privy-->>Privy: Recognizes X ID → assigns same wallet 0xABC...
    Note over SC: @cryptoguru can now interact with the bet contract
```

### Server-Side Code

Using the `@privy-io/node` SDK, the backend first checks if the user already exists in Privy before creating:

```typescript theme={null}
// 1. Check if user exists in Privy
let privyUser;
try {
  privyUser = await privy.users().getByTwitterUsername({ username: 'cryptoguru' });
} catch {
  // User doesn't exist in Privy
}

// 2. If not found, create with pre-generated wallet
if (!privyUser) {
  privyUser = await privy.users().create({
    linked_accounts: [
      {
        type: 'twitter_oauth',
        subject: 'cryptoguru',   // placeholder — ideally the real X numeric ID
        username: 'cryptoguru',  // X handle (without @)
        name: 'cryptoguru'       // Display name
      }
    ],
    wallets: [{ chain_type: 'ethereum' }]
  });
}

// 3. Extract wallet address
const walletAddress = privyUser.linked_accounts
  .find(a => a.type === 'wallet' || a.type === 'smart_wallet')?.address;
```

When `@cryptoguru` eventually logs in via X OAuth on Mouth, Privy recognizes their X ID and assigns the **same wallet** that was pre-generated. The wallet and any assets in it (or bets referencing it) are immediately accessible.

### Rate Limits

Privy's user creation endpoint has a rate limit of **240 users per minute**. Implement exponential backoff starting at 1 second if you receive HTTP 429.

## Embedded Wallet

Each user gets a **Privy embedded wallet** — a non-custodial wallet managed by Privy's infrastructure:

* **Chain**: Base (Coinbase L2)
* **Currency**: USDC
* **Key management**: Privy's secure enclave (user doesn't see private keys)
* **Transaction execution**: All transactions (deposits, withdrawals, claims) are executed **server-side** by the Mouth backend using Privy's wallet API with gas sponsorship — users never sign transactions in the browser

### Server-Side Sponsored Transactions

Mouth uses a **server-side relay model** for all on-chain operations. The backend sends transactions on behalf of users via `privy.wallets().ethereum().sendTransaction()` with `sponsor: true`.

This means:

* **Users pay zero gas fees** — all transactions are sponsored by Mouth via Privy's gas sponsorship infrastructure
* **No wallet popups** — users never see MetaMask-style signing prompts
* **Backend controls execution** — the server encodes the transaction calldata, specifies the target contract, and sends it from the user's embedded wallet using their `privyWalletId`

**Supported operations:**

| Operation      | What happens                                                                                      |
| -------------- | ------------------------------------------------------------------------------------------------- |
| Create bet     | Backend approves USDC to factory (if needed), then calls `factory.createBet()` from user's wallet |
| Accept bet     | Backend approves USDC to bet contract, then calls `bet.deposit()` from user's wallet              |
| Cancel bet     | Backend calls `bet.withdraw()` from user's wallet                                                 |
| Claim winnings | Backend calls `bet.claim()` from user's wallet                                                    |
| Withdraw USDC  | Backend calls `usdc.transfer()` from user's wallet to external address                            |

### Authorization: How the Server Signs for User Wallets

Since embedded wallets are **owned by the user** (not the server), Privy requires explicit authorization for the backend to send transactions on their behalf. Mouth uses the **authorization key + session signer** pattern:

```mermaid theme={null}
sequenceDiagram
    participant FE as Frontend
    participant Privy as Privy SDK
    participant BE as Backend
    participant Chain as Base Network

    Note over FE,Privy: On login (once per session)
    FE->>Privy: addSessionSigners(walletAddress, quorumId)
    Privy-->>FE: Signer registered for this session

    Note over FE,Chain: When user initiates an action (e.g. withdraw)
    FE->>BE: POST /auth/withdraw (Bearer token)
    BE->>BE: Encode calldata (e.g. usdc.transfer)
    BE->>Privy: sendTransaction(walletId, tx, authorization_context)
    Note over BE,Privy: authorization_context contains<br/>the P256 authorization private key
    Privy->>Privy: Verify key is a registered signer for this wallet
    Privy->>Chain: Submit sponsored tx (gas paid by Mouth)
    Chain-->>Privy: Transaction receipt
    Privy-->>BE: { hash: "0x..." }
    BE-->>FE: { txHash: "0x..." }
```

**Setup (one-time):**

1. Generate a **P256 key pair** (ECDSA on prime256v1)
2. Register the **public key** as a **key quorum** in the [Privy Dashboard](https://dashboard.privy.io) → Authorization Keys → "Register key quorum" (threshold: 1 of 1)
3. Store the **private key** (base64-encoded PKCS8) in the backend env as `PRIVY_AUTHORIZATION_PRIVATE_KEY`
4. Store the **quorum ID** in the frontend env as `NEXT_PUBLIC_SIGNER_QUORUM_ID`

**Frontend (every session):**
The `AuthSync` component calls `addSessionSigners()` after login to grant the server's key quorum permission to sign for the user's embedded wallet during this session:

```typescript theme={null}
const { addSessionSigners } = useSessionSigners();

addSessionSigners({
  address: user.wallet.address,
  signers: [{ signerId: SIGNER_QUORUM_ID }],
});
```

**Backend (every transaction):**
The `sendSponsored()` function passes the authorization private key in the `authorization_context`:

```typescript theme={null}
const result = await privy.wallets().ethereum().sendTransaction(walletId, {
  caip2: "eip155:8453",
  params: { transaction: { to, data, value: "0x0", chain_id: 8453 } },
  sponsor: true,
  authorization_context: {
    authorization_private_keys: [env.PRIVY_AUTHORIZATION_PRIVATE_KEY],
  },
});
```

<Note>
  The `user_jwts` authorization method does **not** work when Privy is the auth provider (it's designed for external providers like Auth0/Firebase that register a JWKS endpoint). The authorization key + session signer approach is the correct pattern for Privy-authenticated users.
</Note>

### Resolve & Finalize (Admin Operations)

Bet resolution and finalization are **not** user-initiated — they're performed by the Mouth resolver using a dedicated backend wallet:

| Operation    | Signer                                    | Method                                                        |
| ------------ | ----------------------------------------- | ------------------------------------------------------------- |
| Resolve bet  | Resolver account (`RESOLVER_PRIVATE_KEY`) | Direct Viem `writeContract()` — pays gas from resolver wallet |
| Finalize bet | Resolver account (`RESOLVER_PRIVATE_KEY`) | Direct Viem `writeContract()` — pays gas from resolver wallet |

These are admin-only operations protected by a wallet address check in the route handlers.

### Funding the Wallet

Users deposit USDC into their embedded wallet via:

1. **Direct transfer**: Send USDC on Base to their embedded wallet address
2. **On-ramp**: Buy USDC directly with a credit/debit card via Privy's built-in fiat on-ramp (Coinbase Pay, MoonPay)
3. **Bridge**: Move USDC from any chain to Base via the integrated LI.FI bridge widget

### Withdrawing from the Wallet

Users can withdraw USDC from their embedded wallet to any external address at any time (for funds not locked in active bets). Withdrawals are executed server-side via `POST /auth/withdraw`.

### USDC Approval to Factory

The first time a user creates a bet, the backend grants an **infinite USDC approval** (`type(uint256).max`) from the user's embedded wallet to the MouthBetFactory contract. This is a one-time gasless transaction that avoids repeated approval prompts. The `factory_approved` field in the database tracks whether this approval has been granted.

For individual bet contracts (when accepting bets), the backend grants approval to the specific bet contract address before each deposit.

## Why Privy + Embedded Wallets?

### For the User

* **Zero friction**: Login with X, no wallet setup
* **Familiar**: Uses their existing X identity
* **Safe**: No seed phrases to lose, Privy handles key management

### For the Protocol

* **Identity guarantee**: We know the X account behind every wallet, because Privy verified the OAuth
* **PvP matching**: When user A challenges `@cryptoguru`, we can pre-generate a wallet and verify that the person accepting is actually `@cryptoguru`
* **Social features**: Profiles, leaderboards, and X integration all rely on verified X identity
* **Compliance-ready**: KYC can be layered on top of Privy if needed in the future

## Security Considerations

* Privy embedded wallets are **non-custodial** — Mouth never has access to user private keys
* X OAuth tokens are handled by Privy and never stored by Mouth
* Smart contracts verify wallet addresses, not X handles (handles can change, wallet addresses are on-chain)
* The mapping between X identity and wallet is maintained in the Mouth database, with Privy as the source of truth
