> For the complete documentation index, see [llms.txt](https://gotts.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://gotts.gitbook.io/docs/gotts-safe-mcp-server/mcp-server/10-wallets.md).

# Wallet Support

> **Package**: `packages/safe/` | **Prerequisites**: [09-safety.md](/docs/gotts-safe-mcp-server/mcp-server/09-safety.md) | **Credential Architecture**: [shared/credential-architecture.md](/docs/prd-shared/credential-architecture.md)

***

## Overview

### Operator vs Agent Responsibilities

Gotts Safe process holds wallet credentials, not the LLM agent. The agent calls MCP tools; the server uses the configured wallet provider to sign transactions. If the agent crashes or restarts, the wallet and funds are unaffected. For the full credential model, see [shared/credential-architecture.md](/docs/prd-shared/credential-architecture.md).

```
Operator (human)              Agent (LLM)              MCP Server Process
    │                              │                         │
    ├─ Provisions wallet           │                         │
    ├─ Gets API credentials        │                         │
    ├─ Writes .env / config ──────────────────────────────> reads at startup
    ├─ Sets wallet policy          │                         │
    ├─ Funds wallet                │                         │
    │                              │                         │
    │                              ├─ Calls MCP tools ─────> receives tool calls
    │                              │                         ├─ Sends signing request to TEE
    │                              │                         ├─ TEE evaluates policy
    │                              │                         ├─ TEE signs (or rejects)
    │                              │                         ├─ Broadcasts to chain
    │                              │ <─ Returns result ──────┤
```

### Architecture

All wallet types are normalized to viem's `Account` interface. Gotts Safe does not implement signing logic; it delegates to the configured wallet provider. **The agent never sees or holds any private key.**

```
┌──────────────────────────────────────────────┐
│              Wallet Abstraction               │
│                                               │
│  interface WalletProvider {                   │
│    getAddress(): Address                      │
│    signTransaction(tx): Promise<Hex>          │
│    signTypedData(data): Promise<Hex>          │
│    getChainId(): number                       │
│  }                                            │
└──────┬────────┬────────┬────────┬────────────┘
       │        │        │        │
  ┌────┴───┐┌───┴───┐┌───┴───┐┌──┴──────┐
  │ Local  ││ Privy ││ZeroDev││  Safe   │
  │ Key    ││Server ││Kernel ││1-of-1   │
  │(dev)   ││Wallet ││       ││+ Guards │
  └────────┘└───────┘└───────┘└─────────┘
       │        │        │        │
  ┌────┴───┐┌───┴───┐┌───┴───┐┌──┴──────┐
  │GottsWlt││Lit    ││Generic││         │
  │(mode:  ││Proto. ││viem   ││         │
  │local)  ││Vincent││Account││         │
  └────────┘└───────┘└───────┘└─────────┘
```

> **Implemented via `@gotts.ai/wallet`**: The `GottsWallet` class from `packages/wallet` wraps both Privy (TEE mode) and local private key (dev mode). For ZeroDev, Safe, and custom viem accounts, use the `GottsWallet` with a generic viem `Account` passed directly. See [prd/monorepo/11-primitive-packages.md](/docs/monorepo-infrastructure/monorepo/11-primitive-packages.md) for the `GottsWallet` API.

### Wallet Type: Local Private Key

**Use case**: Development, testing, personal bots on trusted hardware.

**How it works**: A raw private key is loaded from an environment variable. viem's `privateKeyToAccount` creates a local signer.

**Security**: Lowest security. Key is in memory. Acceptable only for development or small-value operations on trusted machines.

**Config**:

```bash
WALLET_TYPE=local
PRIVATE_KEY=0x...
```

### Wallet Type: Privy Server Wallet (Recommended Default)

**Use case**: Production agents that need arbitrary transaction signing with granular policy enforcement. The default recommendation for the Gotts Vaults.

**How it works**: The server calls Privy's Wallet API to sign transactions. Keys are held in Privy's infrastructure (TEE-backed with Shamir's Secret Sharing). Privy's policy engine enforces granular rules: contract allowlists, method restrictions, transfer limits, time-based controls, recipient restrictions, and chain restrictions. Two wallet models: agent-controlled (developer-owned) for fully autonomous agents, or user-owned with agent signers for supervised patterns.

**Why it's the default**: Privy's policy engine is the most granular of all providers -- per-contract method restrictions map directly to the vault protocol's role-based policies (participant vs manager vs creator). Official OpenClaw integration via `privy-agentic-wallets-skill`. Multi-chain support (EVM + Solana + Tier 2 chains). Authorization key quorums for multi-party approval. Transaction and balance webhooks for monitoring. See [shared/credential-architecture.md](/docs/prd-shared/credential-architecture.md) Section 4 for the full comparison.

**Security**: Keys never leave Privy's TEE. Shamir's Secret Sharing splits the key into enclave share + auth share. Policy engine evaluated inside TEE before any signing. Stripe-backed infrastructure (Privy acquired by Stripe, June 2025).

**Pricing**: 50,000 free signatures/month included on all plans. Scale plan: up to 9,999 MAU at $499/month. Overage signatures: $0.01 each on self-serve (as low as $0.001/signature on enterprise). Each server wallet API action (including signing) counts as one signature.

**Config** (two sub-modes — see [Privy Transaction Routing Modes](#privy-transaction-routing-modes) below):

Self-hosted (MCP server calls Privy directly):

```json
{
  "mode": "privy",
  "privyAppId": "app_...",
  "privyAppSecretStorage": "keychain",
  "walletId": "wallet_...",
  "walletAddress": "0x...",
  "authPrivateKey": "MIGHAg..."
}
```

App Secret is stored in OS keychain (`@gotts.ai/crypto` → keytar) and loaded at runtime. The MCP server holds all 4 credentials and calls Privy directly.

Proxy (MCP server signs locally, Portal forwards to Privy):

```json
{
  "mode": "privy",
  "webAppUrl": "https://portal.example.com",
  "walletId": "wallet_...",
  "walletAddress": "0x...",
  "authPrivateKey": "MIGHAg..."
}
```

App Secret lives on the Portal server only. The MCP server signs with its P-256 auth key and sends `{walletId, rpcBody, signature}` to `webAppUrl/api/send`. Portal adds Basic Auth and forwards to Privy.

Legacy env-var format (still supported):

```bash
UNISWAP_MCP_PRIVY_APP_ID=...
UNISWAP_MCP_PRIVY_APP_SECRET=...
UNISWAP_MCP_PRIVY_WALLET_ID=...
UNISWAP_MCP_PRIVY_AUTH_PRIVATE_KEY=...  # P-256 authorization key
```

#### Privy Policy Engine

The policy engine is the defining capability of Privy for DeFi agents. Policies are evaluated **inside the TEE before any key material is assembled** — a compromised server cannot bypass them.

**Policy data model:**

```typescript
interface PrivyPolicy {
  version: "1.0";
  name: string; // 1-50 characters
  chain_type: "ethereum"; // or 'solana', 'tron', 'sui'
  rules: Array<{
    name: string;
    method: "eth_sendTransaction" | "personal_sign" | "eth_signTypedData" | "*";
    action: "ALLOW" | "DENY";
    conditions: Array<{
      field_source: "ethereum_transaction" | "ethereum_calldata";
      field: "to" | "value" | "function_name" | string; // 'function_name' requires abi
      operator: "eq" | "in" | "lte" | "gte";
      value: string | string[];
      abi?: object[]; // Required for all ethereum_calldata conditions
    }>;
  }>;
  owner?: { public_key: string }; // P-256 DER-encoded public key
}
```

**DENY evaluation order**: DENY rules take absolute precedence. The engine evaluates all DENY rules first; if any matches, the transaction is rejected regardless of ALLOW rules. Only after all DENY rules pass does it check ALLOW rules. If no ALLOW rule matches, the transaction is implicitly denied.

This means a policy with only ALLOW rules creates an **implicit deny-all** — any contract interaction not explicitly allowed is rejected. This is the recommended posture for production DeFi agents.

**"DeFi Safe Defaults" policy — complete example** (for Base mainnet):

```typescript
import { PrivyClient, generateP256KeyPair } from "@privy-io/node";

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

// Generate P-256 authorization key (stored in .env, never leaves your infra)
const { privateKey, publicKey } = await generateP256KeyPair();

const policy = await privy.policies().create({
  name: "Uniswap DeFi Safe Defaults — Base",
  version: "1.0",
  chain_type: "ethereum",
  rules: [
    // Allow Uniswap V3 SwapRouter02 on Base
    {
      name: "Allow V3 SwapRouter02",
      method: "eth_sendTransaction",
      action: "ALLOW",
      conditions: [
        {
          field_source: "ethereum_transaction",
          field: "to",
          operator: "eq",
          value: "0x2626664c2603336E57B271c5C0b26F421741e481",
        },
      ],
    },
    // Allow Uniswap V4 Universal Router on Base
    {
      name: "Allow V4 Universal Router",
      method: "eth_sendTransaction",
      action: "ALLOW",
      conditions: [
        {
          field_source: "ethereum_transaction",
          field: "to",
          operator: "eq",
          value: "0x6fF5693b99212Da76ad316178A184AB56D299b43",
        },
      ],
    },
    // Allow Uniswap V4 PoolManager on Base
    {
      name: "Allow V4 PoolManager",
      method: "eth_sendTransaction",
      action: "ALLOW",
      conditions: [
        {
          field_source: "ethereum_transaction",
          field: "to",
          operator: "eq",
          value: "0x498581ff718922c3f8e6a244956af099b2652b2b",
        },
      ],
    },
    // Allow Permit2 (required for Uniswap V3/V4 token approvals)
    {
      name: "Allow Permit2",
      method: "eth_sendTransaction",
      action: "ALLOW",
      conditions: [
        {
          field_source: "ethereum_transaction",
          field: "to",
          operator: "eq",
          value: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
        },
      ],
    },
    // Allow vault contract (ERC-4626) — only deposit and withdraw functions
    {
      name: "Allow vault deposit",
      method: "eth_sendTransaction",
      action: "ALLOW",
      conditions: [
        {
          field_source: "ethereum_transaction",
          field: "to",
          operator: "eq",
          value: "0xYOUR_VAULT_CONTRACT",
        },
        {
          field_source: "ethereum_calldata",
          field: "function_name",
          operator: "in",
          value: ["deposit", "withdraw", "redeem"],
          abi: [
            {
              name: "deposit",
              type: "function",
              inputs: [
                { name: "assets", type: "uint256" },
                { name: "receiver", type: "address" },
              ],
              outputs: [{ name: "shares", type: "uint256" }],
            },
            {
              name: "withdraw",
              type: "function",
              inputs: [
                { name: "assets", type: "uint256" },
                { name: "receiver", type: "address" },
                { name: "owner", type: "address" },
              ],
              outputs: [{ name: "shares", type: "uint256" }],
            },
            {
              name: "redeem",
              type: "function",
              inputs: [
                { name: "shares", type: "uint256" },
                { name: "receiver", type: "address" },
                { name: "owner", type: "address" },
              ],
              outputs: [{ name: "assets", type: "uint256" }],
            },
          ],
        },
      ],
    },
    // Allow emergency withdrawal to cold wallet only
    {
      name: "Allow cold wallet withdrawal",
      method: "eth_sendTransaction",
      action: "ALLOW",
      conditions: [
        {
          field_source: "ethereum_transaction",
          field: "to",
          operator: "eq",
          value: "0xYOUR_COLD_WALLET_ADDRESS",
        },
      ],
    },
    // DENY private key export — explicit block even if auth key is compromised
    {
      name: "Block private key export",
      method: "exportPrivateKey",
      conditions: [],
      action: "DENY",
    },
  ],
  owner: { public_key: publicKey },
});
```

**Function-level calldata restrictions**: To restrict a contract to specific functions, use `ethereum_calldata` conditions with the `abi` field. The `abi` field is **required** for any calldata condition — Privy cannot decode the calldata without it:

```typescript
// Only allow the deposit() function with a maximum deposit cap
{
  name: 'Allow vault deposit with cap',
  method: 'eth_sendTransaction',
  action: 'ALLOW',
  conditions: [
    { field_source: 'ethereum_transaction', field: 'to', operator: 'eq', value: '0xVAULT' },
    { field_source: 'ethereum_calldata', field: 'function_name', operator: 'eq', value: 'deposit', abi: [...] },
    // Cap individual deposit at 10,000 USDC (6 decimals)
    { field_source: 'ethereum_calldata', field: 'deposit.assets', operator: 'lte', value: '10000000000' }
  ]
}
```

**Full wallet creation + policy attachment flow**:

```typescript
// 1. Create policy (shown above) → policy.id

// 2. Create server wallet with policy attached
const wallet = await privy.wallets().create({
  chain_type: "ethereum",
  owner: { public_key: publicKey }, // Links to your P-256 auth key
  policy_ids: [policy.id], // Policy enforced in TEE before all signing
});
// Returns: { id, address, chain_type }

// 3. Sign a transaction (policy checked inside TEE)
const tx = await privy
  .wallets()
  .ethereum()
  .sendTransaction(wallet.id, {
    caip2: "eip155:8453", // Base mainnet
    transaction: {
      to: "0x2626664c2603336E57B271c5C0b26F421741e481",
      value: "0x0",
      data: "0x...",
    },
    authorization_context: { authorization_private_keys: [privateKey] },
  });

// 4. Or use viem integration for familiar DX
import { createViemAccount } from "@privy-io/node/viem";
import { createWalletClient, http } from "viem";
import { base } from "viem/chains";

const account = await createViemAccount(privy, {
  walletId: wallet.id,
  address: wallet.address as `0x${string}`,
  authorizationContext: { authorizationPrivateKeys: [privateKey] },
});

const walletClient = createWalletClient({
  account,
  chain: base,
  transport: http(),
});
const hash = await walletClient.sendTransaction({
  to: "0x...",
  value: 0n,
  data: "0x...",
});
```

#### Privy Authorization Key Mechanics

Authorization keys are **P-256 (secp256r1)** keypairs. The private key never leaves your infrastructure; only the public key is registered with Privy. Every signing request must include an ECDSA signature in the `privy-authorization-signature` header, which the TEE verifies before reassembling the wallet key.

```typescript
// Generate via SDK (recommended)
import { generateP256KeyPair } from "@privy-io/node";
const { privateKey, publicKey } = await generateP256KeyPair();
// Both are base64 DER-encoded strings

// Or via OpenSSL:
// openssl ecparam -name prime256v1 -genkey -noout -out private.pem
// openssl ec -in private.pem -pubout -outform DER | base64
```

**Best practices:**

* Maintain **separate keys for transaction signing and policy management**. The transaction-signing key lives in an env var on your server; the policy-management key stays offline or in a KMS.
* This separation means even if your backend is compromised, the attacker cannot modify policies.
* **Rotation cadence**: Every 90–180 days. Add the new key to the wallet's auth key quorum before removing the old one — never swap directly (would cause a lockout window).
* Store `UNISWAP_MCP_PRIVY_AUTH_PRIVATE_KEY` in a secrets manager (AWS Secrets Manager, HashiCorp Vault) for remote deployments. Never commit to version control.

To attach a policy to an existing wallet:

```bash
curl -X PATCH https://api.privy.io/v1/wallets/{wallet_id} \
  -H "Content-Type: application/json" \
  -d '{"policy_ids": ["policy-id"]}'
```

#### Privy Transaction Routing Modes

The Privy wallet integration supports two transaction routing sub-modes. The choice determines where the Privy App Secret lives and how signing requests reach Privy's TEE.

**Sub-mode detection**: `GottsWallet.fromConfig()` (in `packages/wallet/src/wallet.ts`) reads `GottsConfig` and defaults to proxy mode. Self-hosted mode is selected when `privyAppSecretStorage` is set (indicating the App Secret is available locally via keychain). Proxy mode is selected when `webAppUrl` is present and `privyAppSecretStorage` is absent.

```
                    Self-Hosted                              Proxy
                    ───────────                              ─────
MCP Server Process                           MCP Server Process
    │                                            │
    ├─ Has: App ID, App Secret,                  ├─ Has: P-256 auth key,
    │       Wallet ID, P-256 auth key            │       Wallet ID, webAppUrl
    │                                            │
    ├─ buildPrivySigningPayload()                ├─ buildPrivySigningPayload()
    ├─ signPayload() with P-256 key              ├─ signPayload() with P-256 key
    │                                            │
    ├─ sendDirectToPrivy()                       ├─ sendViaProxy()
    │   POST /v1/wallets/{id}/rpc                │   POST webAppUrl/api/send
    │   + Basic Auth (App ID:Secret)             │   body: {walletId, rpcBody, signature}
    │   + privy-authorization-signature          │
    │                                            │
    v                                            v
Privy TEE                                   Portal Server
    │                                            │
    ├─ Verifies P-256 signature                  ├─ Reads App ID + App Secret from env
    ├─ Evaluates policy                          ├─ Adds Basic Auth header
    ├─ Signs tx inside enclave                   ├─ Adds privy-authorization-signature
    ├─ Returns signed tx                         │   (forwarded from request)
                                                 ├─ POST /v1/wallets/{id}/rpc → Privy TEE
                                                 ├─ Returns {hash} to MCP server
```

**Who holds what:**

| Credential              | Self-Hosted                        | Proxy                                                   |
| ----------------------- | ---------------------------------- | ------------------------------------------------------- |
| Privy App ID            | MCP server (`.env` or config)      | Portal server (`env`) + optionally cached on MCP server |
| Privy App Secret        | MCP server (OS keychain or `.env`) | Portal server (`env`) only — **never on agent machine** |
| P-256 Authorization Key | MCP server (config or keychain)    | MCP server (config or keychain)                         |
| Wallet ID               | MCP server (config)                | MCP server (config)                                     |
| Wallet Address          | MCP server (config)                | MCP server (config)                                     |

**Security trade-offs:**

| Property                        | Self-Hosted                           | Proxy                                                              |
| ------------------------------- | ------------------------------------- | ------------------------------------------------------------------ |
| Credential compartmentalization | All credentials on one machine        | App Secret isolated on Portal server                               |
| Network hops                    | 1 (MCP → Privy)                       | 2 (MCP → Portal → Privy)                                           |
| Availability dependency         | Privy API only                        | Privy API + Portal server                                          |
| Agent machine compromise impact | Full signing access within policy     | Can sign only if Portal is reachable; App Secret safe              |
| Setup complexity                | Operator manages all 4 creds on agent | Operator deploys Portal once, agents pair via `gotts setup --pair` |
| Multi-agent scaling             | Each agent needs App Secret           | All agents share one Portal with App Secret                        |

**When to use which:**

* **Self-hosted**: Single agent, operator controls the machine, simplest possible setup.
* **Proxy**: Multiple agents sharing one Portal, browser-managed wallet creation via pairing flow, or when the App Secret must not touch agent machines.

**Portal `/api/send` endpoint spec:**

```
POST /api/send
Content-Type: application/json

Request:
{
  "walletId": "wallet_abc123",
  "rpcBody": { "method": "eth_sendTransaction", "params": [...] },
  "signature": "base64-encoded-p256-ecdsa-signature"
}

Response (200):
{ "hash": "0xabc..." }

Response (4xx/5xx):
{ "error": "Human-readable error message" }
```

The signature is content-bound: it covers `SHA-256(appId + walletId + JSON(rpcBody))`. Portal cannot forge or modify the transaction — it can only forward or refuse. If Portal is compromised, the attacker gains the App Secret but cannot sign arbitrary transactions without also compromising the P-256 auth key on the agent machine.

**Portal `/api/info` endpoint spec:**

```
GET /api/info

Response (200):
{ "privyAppId": "app_abc123" }
```

Returns the Privy App ID from Portal's environment. Not a secret (already exposed as `NEXT_PUBLIC_PRIVY_APP_ID` in the browser). Used by the MCP server to construct the signing payload when `privyAppId` is not in local config.

**Implementation references:**

| Component                           | File                                     | Status                  |
| ----------------------------------- | ---------------------------------------- | ----------------------- |
| Sub-mode routing                    | `packages/wallet/src/wallet.ts:86-121`   | Complete                |
| `sendViaProxy()`                    | `packages/wallet/src/proxy.ts`           | Complete                |
| `sendDirectToPrivy()`               | `packages/wallet/src/privy-api.ts:18-52` | Complete                |
| `PrivySubMode` type                 | `packages/wallet/src/types.ts:4`         | Complete                |
| `fetchPrivyAppId()`                 | `packages/core/src/config.ts:112-125`    | Complete                |
| `GottsConfig.webAppUrl`             | `packages/core/src/config.ts:16`         | Complete                |
| `GottsConfig.privyAppSecretStorage` | `packages/core/src/config.ts:22`         | Complete                |
| Portal `/api/send` endpoint         | `packages/portal/src/api/send.ts`        | **Not yet implemented** |
| Portal `/api/info` endpoint         | `packages/portal/src/api/info.ts`        | **Not yet implemented** |

#### Verified Base Mainnet Contract Addresses

For policy allowlists on Base (chain ID 8453). Cross-verified against Uniswap official docs and BaseScan:

| Contract                        | Address                                      | Required For                                |
| ------------------------------- | -------------------------------------------- | ------------------------------------------- |
| **SwapRouter02 (V3)**           | `0x2626664c2603336E57B271c5C0b26F421741e481` | V3 swaps                                    |
| **QuoterV2 (V3)**               | `0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a` | V3 quotes (view, usually no signing needed) |
| V3 Factory                      | `0x33128a8fC17869897dcE68Ed026d694621f6FDfD` | Pool discovery                              |
| V3 NonfungiblePositionManager   | `0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1` | LP positions                                |
| **PoolManager (V4)**            | `0x498581ff718922c3f8e6a244956af099b2652b2b` | V4 pool interactions                        |
| **Universal Router (V2/V3/V4)** | `0x6fF5693b99212Da76ad316178A184AB56D299b43` | All versions, universal routing             |
| V4 PositionManager              | `0x7c5f5a4bbd8fd63184577525326123b519429bdc` | V4 LP positions                             |
| V4 Quoter                       | `0x0d5e0f971ed27fbff6c2837bf31316121532048d` | V4 quotes                                   |
| V4 StateView                    | `0xa3c0c9b65bad0b08107aa264b0f3db444b867a71` | V4 pool state reads                         |
| **Permit2**                     | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | Token approvals (all versions)              |
| WETH                            | `0x4200000000000000000000000000000000000006` | Wrapped ETH on Base                         |

**Minimum allowlist for a basic Uniswap policy** (5 addresses):

1. SwapRouter02 (V3 swaps)
2. Universal Router (V4 + multi-version)
3. PoolManager (V4 direct)
4. Permit2 (approvals)
5. Your backup cold wallet (emergency withdrawal)

#### ERC-4626 Function Selectors

The four state-changing functions for vault interactions, for use in policy calldata conditions:

| Function   | Selector     | Signature                                                   |
| ---------- | ------------ | ----------------------------------------------------------- |
| `deposit`  | `0x6e553f65` | `deposit(uint256 assets, address receiver)`                 |
| `withdraw` | `0xb460af94` | `withdraw(uint256 assets, address receiver, address owner)` |
| `mint`     | `0x94bf804d` | `mint(uint256 shares, address receiver)`                    |
| `redeem`   | `0xba087652` | `redeem(uint256 shares, address receiver, address owner)`   |

ERC-4626 extends ERC-20, so the vault share token also inherits `transfer`, `approve`, and `transferFrom`. If your policy restricts by function name, explicitly ALLOW these ERC-20 methods on the vault contract address as well.

### Wallet Type: Safe Smart Account

**Use case**: Maximum on-chain safety. The agent is the sole signer on a 1-of-1 Safe, but Transaction Guards enforce rules at the smart contract level.

**How it works**: The agent signs transactions as the sole owner of a Safe smart account. Transaction Guards (on-chain contracts) validate every transaction before execution. Even if the agent's LLM hallucinates, the Guard reverts transactions that violate configured rules (spending limits, token allowlists, contract address restrictions). This is the model used by Olas (3.5M+ autonomous transactions).

**Security**: Highest. On-chain guards are tamper-proof and do not depend on the server's safety middleware. Defense in depth: server-side safety + on-chain guards.

**Config**:

```bash
WALLET_TYPE=safe
SAFE_ADDRESS=0x...
SAFE_OWNER_PRIVATE_KEY=0x...
# OR use Privy as the Safe owner:
SAFE_OWNER_WALLET_TYPE=privy
SAFE_OWNER_PRIVY_WALLET_ID=...
```

### Wallet Type: ZeroDev Kernel

**Use case**: Agents needing ERC-4337 smart account with session keys for granular, time-bounded permissions.

**How it works**: ZeroDev Kernel (6M+ smart accounts, 50+ networks) provides ERC-7579 modular smart accounts with session keys that have granular policies (call policies, spending limits, time bounds). Session keys can be lazily enabled without a setup transaction. Ideal for agents that need scoped, temporary permissions for specific operations.

**Security**: Session keys limit agent authority to specific contracts, functions, and amounts. Keys expire automatically. No need to expose the account owner key to the agent.

**Config**:

```bash
WALLET_TYPE=zerodev
ZERODEV_PROJECT_ID=...
ZERODEV_SESSION_KEY=...
```

**Note**: **EIP-7702** (Pectra upgrade, May 2025) enables existing EOAs to upgrade into smart accounts. This simplifies migration for agents currently using local private keys -- they can upgrade to smart account features (session keys, spending limits, multi-sig) without deploying a new account.

### Wallet Type: Generic viem Account

**Use case**: Any custom wallet provider that implements viem's `Account` interface.

**How it works**: The operator provides a custom `Account` object via the programmatic API (not env-based config). This supports any wallet provider: Lit Protocol PKPs, Dynamic, Para, or custom implementations.

**Config**: Programmatic only (TypeScript API, not env vars):

```typescript
import { createGottsSafeServer } from "@gotts.ai/safe";
import { myCustomAccount } from "./my-wallet";

const server = createGottsSafeServer({
  wallet: { account: myCustomAccount },
  // ... other config
});
```

### Wallet Architecture Summary

| Provider               | Key Storage       | Arbitrary Signing | Signing Latency | Policy Granularity                                    | Best For                              |
| ---------------------- | ----------------- | ----------------- | --------------- | ----------------------------------------------------- | ------------------------------------- |
| **Privy** (default)    | TEE + Shamir      | Yes               | 100-200ms       | Contract + method + amount + time + chain + recipient | **Production agents** (all use cases) |
| ZeroDev                | Smart Account     | Yes               | 200-500ms       | Session key policies                                  | Scoped permissions, ERC-4337          |
| Safe                   | Smart Account     | Yes               | 500ms-2s        | On-chain guards                                       | Maximum on-chain security             |
| Lit Protocol (Vincent) | Distributed (MPC) | Yes               | 200-500ms       | On-chain + off-chain                                  | Censorship resistance                 |
| Local Key              | In-memory (viem)  | Yes               | < 1ms           | None                                                  | **Development/testing only**          |
| Custom viem Account    | Varies            | Varies            | Varies          | Varies                                                | Custom integrations                   |

### Identity NFT Custody Capability

Not all wallet types are suitable for holding ERC-8004 identity NFTs. Identity NFTs should reside in wallets that support **key rotation without address change**, enabling the agent to recover from key compromise without losing its identity or reputation.

| Provider            | Supports Key Rotation | Identity NFT Custody             | Recommendation                                              |
| ------------------- | --------------------- | -------------------------------- | ----------------------------------------------------------- |
| Local Key (dev)     | No                    | Not recommended                  | Identity NFT on EOA has no transfer protection              |
| **Privy** (default) | Via ERC-4337 layer    | Recommended (with smart account) | Layer ZeroDev Kernel on top of Privy signer                 |
| ZeroDev             | Yes (native)          | Recommended                      | ERC-4337 with sudo + session key separation                 |
| Safe                | Yes (native)          | Recommended                      | `swapOwner()` + Transaction Guards block identity transfers |
| Custom viem Account | Varies                | Depends on implementation        | Must support address-stable key rotation                    |

**Critical**: Agents using Local Key (EOA) wallets should migrate their identity NFTs to a smart contract wallet before operating in production. EOA keys cannot be rotated without moving the NFT, which triggers a 30-day reputation decay. See [shared/onboarding-workflow.md](/docs/prd-shared/onboarding-workflow.md) Path C for the migration guide.

***

### Credential Lifecycle

Credentials are not static -- they need rotation, backup, and revocation procedures. For the full rotation playbook, see [shared/credential-architecture.md](/docs/prd-shared/credential-architecture.md) Section 6.

| Provider        | Credential                | How to Rotate                                               | Impact on Wallet                                                                                  |
| --------------- | ------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| Privy           | App Secret                | Regenerate via [console.privy.io](https://console.privy.io) | Wallet unaffected. Update `.env`, restart.                                                        |
| Privy           | Authorization key (P-256) | Add new key to quorum, remove old one                       | Wallet unaffected. Threshold prevents lockout.                                                    |
| Safe            | Owner key                 | `swapOwner()` on-chain (requires threshold signatures)      | Wallet address unchanged. Identity preserved.                                                     |
| Local Key (dev) | Private key               | **Cannot rotate without changing address**                  | New address. Identity NFT must transfer (30-day decay). Never use local keys for real identities. |

**Critical**: Store a backup of API credentials outside the agent machine. If the machine is destroyed, credentials from the provider dashboard let you re-provision a new agent with the same wallet. The wallet's funds and on-chain state survive independently.

***

### Agent Onboarding

For the complete end-to-end workflow for setting up an agent -- from wallet provisioning through ERC-8004 registration, guardian configuration, funding, and vault participation -- see [**shared/onboarding-workflow.md**](/docs/prd-shared/onboarding-workflow.md).

The onboarding guide covers three paths:

* **Path A (Automated)**: Single-command setup via `self-funding-setup` skill or `OnboardRouter.sol`
* **Path B (Manual)**: Step-by-step for operators who want full control
* **Path C (Migration)**: For agents with existing ERC-8004 identities that need to upgrade to guardian-protected smart wallet custody

***
