> 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/14-deployment.md).

# Production Deployment

> **Document Type**: OPS (informative) | **Package**: `packages/safe/` | **Prerequisites**: [10-wallets.md](/docs/gotts-safe-mcp-server/mcp-server/10-wallets.md), [11-config.md](/docs/gotts-safe-mcp-server/mcp-server/11-config.md) | **Credential Architecture**: [shared/credential-architecture.md](/docs/prd-shared/credential-architecture.md)
>
> How to deploy Gotts Safe to production: platform selection, setup automation, secret management, authentication, monitoring, and security hardening. Covers the full path from `pnpm build` to a running server that agents can connect to over the network. This is an operational document, not a product requirement -- see [shared/doc-standards.md](/docs/prd-shared/doc-standards.md) for document type definitions.

***

## Why This Matters

Gotts Safe custodies wallet credentials and signs transactions. It is the **prerequisite** for any agent to participate in vaults, execute trades, manage liquidity, or interact with the protocol. Before an agent can call `vault_deposit` or `execute_swap`, Gotts Safe must be deployed, configured with a wallet, and accessible over the network (or locally via stdio for development).

The goal: **an operator goes from zero to a production MCP server in under 10 minutes, spending under $5/month.**

***

## Deployment Architecture

### Three-Tier Model

```
┌──────────────────────────────────────────────────────┐
│                   Transport Layer                     │
│  Streamable HTTP (production) | stdio (development)   │
│  TLS termination, CORS, bearer token / OAuth 2.1      │
└────────────────────┬─────────────────────────────────┘
                     │
┌────────────────────▼─────────────────────────────────┐
│                  Application Layer                     │
│  MCP protocol handling, tool dispatch, safety          │
│  middleware, caching, rate limiting, spending limits    │
└────────────────────┬─────────────────────────────────┘
                     │
┌────────────────────▼─────────────────────────────────┐
│                   Signing Layer                        │
│  Privy TEE / Local key                                 │
│  Policy engine evaluation, transaction broadcast       │
└──────────────────────────────────────────────────────┘
```

### Transport Selection

| Transport                         | Use Case                                 | Session Management        | Multi-Client |
| --------------------------------- | ---------------------------------------- | ------------------------- | ------------ |
| **stdio**                         | Local development, Claude Desktop config | Implicit (single process) | No           |
| **Streamable HTTP** (recommended) | Production remote access                 | `Mcp-Session-Id` header   | Yes          |
| **WebSocket**                     | Legacy compatibility                     | Per-connection            | Yes          |

**Streamable HTTP** is the production default. It replaced HTTP+SSE in the March 2025 MCP spec revision. The server exposes a single endpoint (e.g., `https://your-server.fly.dev/mcp`) that accepts POST requests with JSON-RPC payloads. Clients include Claude.ai, Claude Code, Cursor, VS Code, and any MCP-compatible agent framework.

**stdio** remains the default for local development. Claude Desktop discovers stdio servers via `claude_desktop_config.json`. No networking, no auth, no deployment required.

***

## Platform Comparison

| Criteria               | **Fly.io** (recommended) | Railway            | Cloudflare Workers   | Docker (self-hosted) | Local dev   |
| ---------------------- | ------------------------ | ------------------ | -------------------- | -------------------- | ----------- |
| **Monthly cost**       | \~$3-5 (auto-stop)       | Usage-based (\~$5) | Free tier available  | Infrastructure cost  | Free        |
| **Setup time**         | \~2 minutes              | \~3 minutes        | \~5 minutes          | \~10 minutes         | Instant     |
| **Memory limit**       | Configurable (256MB+)    | Configurable       | 128MB                | Unlimited            | Unlimited   |
| **Full Node.js**       | Yes                      | Yes                | No (Workers runtime) | Yes                  | Yes         |
| **Native MCP support** | `fly mcp launch`         | No                 | `MCPAgent` class     | No                   | N/A         |
| **Secret management**  | `fly secrets set`        | Dashboard + CLI    | `wrangler secret`    | `.env` file          | `.env` file |
| **Auto-scaling**       | Auto-stop/start          | Auto-sleep         | Edge auto-scale      | Manual               | N/A         |
| **Bearer token auth**  | Auto-configured          | Manual             | Manual               | Manual               | N/A         |
| **CPU time limit**     | None                     | None               | 30s default          | None                 | None        |
| **Best for**           | Production agents        | Cost-sensitive     | Read-only data tier  | Maximum control      | Development |

**Why Fly.io is the default recommendation**: Gotts Safe depends on heavy Node.js packages (viem, Uniswap SDKs, zod) that require full Node.js runtime -- Cloudflare Workers' 128MB memory limit and incomplete Node.js API compatibility are disqualifying for the full server. Fly.io's native `fly mcp launch` command handles deployment, auth, and client configuration in a single step. Auto-stop/start means the Machine only runs (and costs money) when an agent is actively connected.

**When to use Cloudflare Workers instead**: If deploying a read-only data tier (price feeds, pool info, token search) without wallet or signing capabilities, Workers' edge deployment and free tier are attractive. The server can be split: Workers for read tools, Fly.io for write tools.

***

## Recommended Deployment: Fly.io

### One-Command Deploy

```bash
# Install flyctl if not present
curl -L https://fly.io/install.sh | sh

# Deploy Gotts Safe with secrets
fly mcp launch "node dist/index.js" \
  --claude --cursor --server uniswap-agent \
  --secret WALLET_TYPE=privy \
  --secret PRIVY_APP_ID=your-app-id \
  --secret PRIVY_APP_SECRET=your-app-secret \
  --secret PRIVY_WALLET_ID=your-wallet-id \
  --secret ALCHEMY_API_KEY=your-alchemy-key
```

This single command:

1. Creates a Fly Machine running Gotts Safe
2. Injects secrets as environment variables (never stored in plaintext)
3. Configures bearer token authentication automatically
4. Outputs client configuration JSON for Claude Code and Cursor
5. Enables auto-stop (Machine suspends after inactivity, restarts on next request)

### Connecting Clients

**Claude Code**:

```bash
claude mcp add --transport http uniswap https://uniswap-agent.fly.dev/mcp \
  --header "Authorization: Bearer <token-from-fly-mcp-launch>"
```

**Cursor** (add to `.cursor/mcp.json`):

```json
{
  "mcpServers": {
    "uniswap": {
      "url": "https://uniswap-agent.fly.dev/mcp",
      "headers": {
        "Authorization": "Bearer <token-from-fly-mcp-launch>"
      }
    }
  }
}
```

**Claude Desktop**: Use Settings > Connectors to add the remote server URL. Claude Desktop does not support remote servers via `claude_desktop_config.json` (that file only handles local stdio servers).

### Cost Optimization

* **Auto-stop**: Enabled by default. The Machine suspends after 5 minutes of inactivity and restarts in \~300ms on next request. Cost is zero while suspended.
* **Minimal Machine**: 256MB RAM, shared-cpu-1x is sufficient for a single-agent deployment.
* **Estimated cost**: $3-5/month for a lightly used agent (a few hundred tool calls/day).

### Multi-User Isolation

For serving multiple agents/users, Fly.io recommends the **single-tenant pattern**: one Machine per user, each with its own secrets. This provides process-level isolation with no cross-tenant data leakage.

```bash
# Create per-user apps
fly mcp launch "node dist/index.js" \
  --server uniswap-agent-alice \
  --secret PRIVY_WALLET_ID=alice-wallet-id \
  # ... alice's secrets

fly mcp launch "node dist/index.js" \
  --server uniswap-agent-bob \
  --secret PRIVY_WALLET_ID=bob-wallet-id \
  # ... bob's secrets
```

The `fly-replay` header routes requests to the correct Machine based on user identity.

***

## Alternative: Railway

Railway provides usage-based pricing with no idle charges. Simpler than Fly.io but lacks native MCP tooling.

```bash
# Install Railway CLI
npm install -g @railway/cli

# Deploy from the repo root
railway up --service gotts-safe

# Set environment variables
railway variables set WALLET_TYPE=privy
railway variables set PRIVY_APP_ID=your-app-id
# ... remaining secrets
```

Railway auto-detects the Dockerfile and builds. Generate a public domain via `railway domain`. Connect clients using the generated URL with manual bearer token configuration.

**Cost**: Pay only for active compute. A lightly used agent costs \~$3-5/month. No charges when the service is idle (auto-sleep after 10 minutes of inactivity).

***

## Alternative: Docker (Self-Hosted)

For maximum control, self-host using the existing Docker infrastructure.

### Build and Run

```bash
# Build the production image
docker compose build gotts-safe-http

# Run with HTTP transport on port 3000
docker compose up gotts-safe-http
```

The `docker-compose.yml` already defines two services:

* `gotts-safe`: stdio mode (for local development)
* `gotts-safe-http`: HTTP mode on port 3000

### Exposing to the Internet

For remote access from Claude Code or Cursor, expose the server securely:

**Option A: Cloudflare Tunnel** (recommended for self-hosted):

```bash
# Install cloudflared
brew install cloudflared

# Create a tunnel to expose port 3000
cloudflared tunnel --url http://localhost:3000
```

This provides a public HTTPS URL with no port forwarding, no static IP, and automatic TLS. Free tier available.

**Option B: Caddy reverse proxy** (for VPS/bare metal):

```
# Caddyfile
gotts-safe.yourdomain.com {
    reverse_proxy localhost:3000
}
```

Caddy handles automatic TLS via Let's Encrypt.

***

## Cloudflare Workers: Programmatic Deployment

Cloudflare Workers is the recommended platform for the **read-only data tier** (the hosted `https://mcp.agenticvaults.xyz/mcp` endpoint). Workers has fundamental constraints that make it unsuitable for the full server (128MB memory limit, no full Node.js APIs), but these constraints are non-issues for read-only pool data, price feeds, and token search.

### Key Properties

* **Cold start**: Sub-5ms (V8 isolates, pre-warmed during TLS handshake — far faster than Lambda's 100ms-1s)
* **Pricing**: Free tier includes 100,000 requests/day. Paid plan: $5/month for 10M requests, then $0.30/million. No egress fees.
* **Durable Objects**: Available on free tier. Enable stateful sessions for agents that maintain context between MCP tool calls.
* **Secrets**: Encrypted at rest, never returned by any API after creation. Injected as environment variables at runtime.

### TypeScript SDK Deployment (Recommended for Install Wizard)

**Do not use wrangler CLI for programmatic deployment.** Wrangler's `unstable_startWorker()` and `getPlatformProxy()` APIs are limited to local dev. For CI/CD and the install wizard's Cloudflare deploy path, use the official TypeScript SDK:

```typescript
import Cloudflare from "cloudflare";
import { toFile } from "cloudflare/index";
import { readFileSync } from "fs";

const client = new Cloudflare({ apiToken: process.env.CLOUDFLARE_API_TOKEN });
const ACCOUNT_ID = process.env.CLOUDFLARE_ACCOUNT_ID;
const SCRIPT_NAME = "gotts-safe-data";

// 1. Deploy the Worker script
const workerCode = readFileSync("./dist/worker.mjs", "utf-8");

await client.workers.scripts.update(SCRIPT_NAME, {
  account_id: ACCOUNT_ID,
  metadata: {
    main_module: "worker.mjs",
    compatibility_date: "2024-09-01",
    compatibility_flags: ["nodejs_compat"],
    bindings: [
      { type: "plain_text", name: "ENVIRONMENT", text: "production" },
      { type: "plain_text", name: "UNISWAP_MCP_PROFILE", text: "data" },
    ],
  },
  files: {
    "worker.mjs": await toFile(Buffer.from(workerCode), "worker.mjs", {
      type: "application/javascript+module",
    }),
  },
});

// 2. Set secrets (values are encrypted at rest, never returned by any API)
async function setSecrets(scriptName: string, secrets: Record<string, string>) {
  for (const [name, text] of Object.entries(secrets)) {
    await fetch(
      `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/workers/scripts/${scriptName}/secrets`,
      {
        method: "PUT",
        headers: {
          Authorization: `Bearer ${process.env.CLOUDFLARE_API_TOKEN}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ name, text, type: "secret_text" }),
      },
    );
  }
}

await setSecrets(SCRIPT_NAME, {
  UNISWAP_MCP_SUBGRAPH_API_KEY: process.env.SUBGRAPH_API_KEY!,
});
// Note: to the Worker runtime, secrets and plain-text bindings are identical
// (both accessed via env.VARIABLE_NAME). Distinction is only about storage security.
```

### Stateless vs Stateful Workers

**Stateless** (recommended for read-only data tier): No Durable Objects needed. Each request gets a fresh context. Ideal for price queries, pool lookups, and token search where no session state is needed.

```typescript
// Stateless: single handler, no Durable Objects
import { createMcpHandler } from "@modelcontextprotocol/sdk/server/cloudflare.js";
import { registerDataTools } from "./tools/index.js";

const handler = createMcpHandler((server) => {
  registerDataTools(server); // Only data profile tools
});

export default { fetch: handler };
```

**Stateful** (for agents that maintain context between calls): Uses Durable Objects to maintain per-session state. Enables `GET /mcp` SSE stream for server-initiated push:

```typescript
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

export class UniswapDataMCP extends McpAgent<{}, Env> {
  server = new McpServer({ name: "uniswap-data", version: "1.0.0" });

  async init() {
    // Register tools on init — called once per Durable Object instance
    registerDataTools(this.server);
  }
}

// wrangler.toml:
// [[durable_objects.bindings]]
// name = "MCP_AGENT"
// class_name = "UniswapDataMCP"
```

Durable Objects hibernate when inactive (billing only for CPU time) and resume on next request. This makes stateful sessions free during idle periods.

### Cloudflare Access for Machine-to-Machine Auth

For protecting the hosted endpoint from unauthorized access (without requiring OAuth on every client):

```typescript
// 1. Create a service token (one-time via Cloudflare dashboard or API)
// Returns: service_token_id + CF-Access-Client-Id + CF-Access-Client-Secret

// 2. Client sets headers on every request:
const response = await fetch("https://mcp.agenticvaults.xyz/mcp", {
  method: "POST",
  headers: {
    "CF-Access-Client-Id": process.env.CF_ACCESS_CLIENT_ID!,
    "CF-Access-Client-Secret": process.env.CF_ACCESS_CLIENT_SECRET!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(mcpRequest),
});

// 3. Worker validates the Cf-Access-Jwt-Assertion header automatically
// (Cloudflare Access injects this after validating the service token)
// Always verify the JWT against your team's public keys as a second layer:
// https://<team>.cloudflareaccess.com/cdn-cgi/access/certs
```

This enables M2M authentication without OAuth flows — appropriate for agent-to-server connections where the agent is a fixed service (not a user).

### API Token Minimum Permissions

The Cloudflare API token used for deployment needs:

* **Workers Scripts (Edit)** — at the account level
* **Workers Routes (Edit)** — at the zone level (only if configuring custom routes)

Tokens can be created programmatically via `POST /user/tokens`, but require a bootstrap token with `API Tokens::Edit` permission (created once in the Cloudflare dashboard).

***

## Setup Automation CLI

### `scripts/setup-production.ts`

A single interactive script that orchestrates the entire setup:

```bash
pnpm setup:production
```

### What It Does

1. **Check prerequisites**: Node.js >= 20, required CLI tools installed
2. **Choose wallet provider**: Interactive prompt (Privy recommended, or local key for dev)
3. **Create wallet** (if Privy): Calls `@privy-io/node` SDK to create a wallet programmatically -- no dashboard visit needed for the wallet itself
4. **Generate `.env`**: Writes all required environment variables to `packages/safe/.env`
5. **Apply wallet policy**: Selects and applies a policy template based on intended role (vault participant, trader, etc.)
6. **Choose deployment target**: Interactive prompt (Fly.io, Railway, Docker, local)
7. **Deploy**: Dispatches the appropriate deployment command
8. **Health check**: Runs `check_setup_health` via MCP Inspector to verify everything works
9. **Register identity** (optional): Prompts to register ERC-8004 identity via `register_agent`
10. **Output client config**: Prints JSON configuration for Claude Code, Cursor, and Claude Desktop

### Non-Interactive Mode

For CI/CD or agent-driven setup:

```bash
pnpm setup:production \
  --wallet-provider privy \
  --privy-app-id $PRIVY_APP_ID \
  --privy-app-secret $PRIVY_APP_SECRET \
  --rpc-provider alchemy \
  --alchemy-api-key $ALCHEMY_API_KEY \
  --deploy-target fly \
  --skip-identity \
  --non-interactive
```

### Agent-Driven Setup

Claude or OpenClaw can run the setup autonomously if the operator has pre-configured the Privy App ID and Secret:

1. Operator sets `PRIVY_APP_ID` and `PRIVY_APP_SECRET` in the environment
2. Agent calls the `provision_wallet` MCP tool to create a wallet
3. Agent calls `check_setup_health` to verify readiness
4. Agent calls `register_agent` to mint ERC-8004 identity
5. Agent calls `vault_deposit` to begin vault participation

Steps 2-5 require no human interaction. The operator's only prerequisite is a one-time Privy app creation at [console.privy.io](https://console.privy.io).

### Programmatic Wallet Creation

The setup script uses the `@privy-io/node` SDK:

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

const privy = new PrivyClient(appId, appSecret);

// Create agent-controlled wallet (Model 1: developer-owned)
const wallet = await privy.wallets().create({
  chainType: "ethereum",
});

console.log(`Wallet created: ${wallet.address}`);
console.log(`Wallet ID: ${wallet.id}`);
```

The wallet address is written to `.env` automatically.

***

## Secret Management

### Development

Environment variables in `.env` files. Acceptable for local development only.

```bash
# packages/safe/.env
WALLET_TYPE=local
PRIVATE_KEY=0xdev-key-never-use-in-production
ALCHEMY_API_KEY=your-alchemy-key
```

### Staging / Small Production

**Fly.io secrets** (encrypted at rest, injected as env vars at runtime):

```bash
fly secrets set PRIVY_APP_SECRET=your-secret --app uniswap-agent
fly secrets list --app uniswap-agent  # verify, values are never displayed
```

**Railway variables** (encrypted, injected at runtime):

```bash
railway variables set PRIVY_APP_SECRET=your-secret
```

### Production

**Doppler** (\~$12/user/month): Best developer experience. Git-style activity logs, sync to 20+ platforms, auto-rotation, rollback.

```bash
# Install and authenticate
doppler setup

# Secrets are injected at runtime, never touch disk
doppler run -- node dist/index.js
```

**AWS Secrets Manager** (\~$0.40/secret/month): Deep AWS integration, Lambda-based auto-rotation, CloudTrail audit logging.

### Non-Negotiable for Production

* **Privy authorization keys**: Must be enabled. Without them, a leaked `PRIVY_APP_SECRET` grants unrestricted wallet access. With authorization keys, each signing request must be cryptographically signed by a P-256 key that the operator holds.
* **No raw private keys in environment**: Use Privy -- never `WALLET_TYPE=local` with a real-value `PRIVATE_KEY`.
* **Secret redaction in logs**: Pino with redaction rules (see Monitoring section).

***

## Authentication

### Three-Tier API Keys

In addition to the single bearer token model below, Gotts Safe supports a three-tier API key model for fine-grained access control. Three keys are generated at server startup (or via the install wizard) and written to `.env`:

| Key Tier     | Prefix            | Scope                                                                           | Primary Consumer                        |
| ------------ | ----------------- | ------------------------------------------------------------------------------- | --------------------------------------- |
| **Read**     | `gotts_read_`     | All query tools (`get_*`, `search_*`, `list_*`, `query_*`)                      | Monitoring dashboards, read-only Portal |
| **Feedback** | `gotts_feedback_` | Read + `submit_operator_feedback`, `manage_insight`, `update_memory_confidence` | Portal operator (recommended default)   |
| **Write**    | `gotts_write_`    | Full access to all tools including transaction execution                        | The agent itself                        |

The MCP server includes authorization middleware that checks the key prefix to determine the allowed tool set. Unauthorized tool calls receive a structured `INSUFFICIENT_KEY_TIER` error.

**Key generation**: Keys are 256-bit, cryptographically random, base64url-encoded. Generated at server startup if not present in environment, or via `npx @gotts.ai setup`.

**Transport security**: HTTPS required for non-localhost connections carrying API keys. Keys are transmitted as `Authorization: Bearer` headers and never logged.

For the full specification, see [prd/website/portal/03-api-keys.md](/docs/website/website/portal/03-api-keys.md).

### Stage 2: Bearer Token (Default)

Fly.io auto-configures bearer token auth. For other platforms, generate a token and require it on all requests:

```bash
# Generate a random bearer token
export MCP_AUTH_TOKEN=$(openssl rand -hex 32)

# Server reads from environment
MCP_AUTH_TOKEN=$MCP_AUTH_TOKEN node dist/index.js
```

The server validates the `Authorization: Bearer <token>` header on every request. Requests without a valid token receive `401 Unauthorized`.

### Stage 3: SIWE + JWT (D-085)

For crypto-native agents connecting to a remote MCP server. SIWE (Sign-In with Ethereum, EIP-4361) is used as the authorization grant, producing a scoped JWT:

1. Agent connects, receives `401` with SIWE challenge nonce
2. Agent signs SIWE message via its wallet (Privy enclave or other EIP-191 signer)
3. Agent sends `POST /auth/siwe` with `{ message, signature }`
4. Server verifies signature, checks ERC-8004 Identity Registry for agent registration and role metadata
5. Server issues JWT with claims: `{ agentId, address, role, tier, scopes, exp }`
6. All subsequent requests include `Authorization: Bearer <JWT>` and `Mcp-Session-Id`

Endpoints exposed for SIWE auth:

* `GET /.well-known/oauth-protected-resource` -- MCP-standard discovery
* `POST /auth/siwe` -- SIWE challenge/response authentication
* `POST /mcp` -- MCP Streamable HTTP endpoint (requires `Authorization: Bearer <JWT>`)
* `GET /.well-known/agent.json` -- A2A Agent Card (public, no auth required)

### Stage 4: Full OAuth 2.1

For non-crypto multi-user deployments, implement the MCP spec's OAuth 2.1 flow:

1. Server acts as an **OAuth 2.0 Resource Server** (validates tokens)
2. External **Authorization Server** (Auth0, Keycloak) handles authentication and token issuance
3. Discovery via RFC 9728 Protected Resource Metadata
4. PKCE mandatory for all clients per OAuth 2.1

SIWE (Stage 3) and OAuth 2.1 (Stage 4) can coexist -- the server accepts both JWT types and maps them to the same internal authorization model.

### MCP Server Identity Registration (D-084)

As part of production deployment, register Gotts Safe as an ERC-8004 entity:

1. Create a Privy wallet for the server (or use existing wallet from env vars)
2. Register via Agent0 SDK with `role: "infrastructure"`, `serviceType: "mcp_server"` metadata
3. Upload registration file to IPFS with MCP, A2A, and web service endpoints
4. Set up automated infrastructure feedback monitoring (uptime, response time, success rate)

This is a Phase 5 deployment task. The server identity enables trust chain verification and infrastructure discovery.

### Per-Tool Authorization Scopes

Map OAuth scopes to tool categories:

| Scope             | Tools                                                                     | Description               |
| ----------------- | ------------------------------------------------------------------------- | ------------------------- |
| `read:market`     | `get_token_price`, `get_pool_info`, `search_tokens`, etc.                 | Read-only market data     |
| `read:portfolio`  | `get_agent_balance`, `get_positions_by_owner`, `get_position`             | Wallet and position reads |
| `write:trade`     | `execute_swap`, `submit_uniswapx_order`, `submit_cross_chain_intent`      | Trading operations        |
| `write:liquidity` | `add_liquidity`, `remove_liquidity`, `collect_fees`, `rebalance_position` | LP management             |
| `write:vault`     | `vault_deposit`, `vault_withdraw`, `vault_rebalance`                      | Vault operations          |
| `admin:wallet`    | `provision_wallet`, `approve_token`, `check_safety_status`                | Wallet administration     |

Bearer token auth (Stage 2) grants all scopes. OAuth 2.1 (Stage 3+) enables fine-grained per-scope control.

***

## Monitoring and Observability

### Structured Logging

Use **Pino** with aggressive secret redaction:

```typescript
import pino from "pino";

const logger = pino({
  level: process.env.LOG_LEVEL || "info",
  redact: {
    paths: [
      "*.privateKey",
      "*.apiSecret",
      "*.seedPhrase",
      "req.headers.authorization",
      "*.privyAppSecret",
    ],
    remove: true,
  },
});
```

**What to log**: Every tool invocation (tool name, parameters minus secrets, duration, success/failure), every transaction intent (before execution), every transaction submission (tx hash, chain, gas estimate), every confirmation/failure (block number, status, gas used).

**What never to log**: Private keys, API secrets, seed phrases, full authorization headers, transaction signing data.

### OpenTelemetry

Instrument three layers:

| Layer              | Metrics                                                                                | Purpose       |
| ------------------ | -------------------------------------------------------------------------------------- | ------------- |
| **Infrastructure** | Connection count, memory, CPU, request throughput, HTTP error rates                    | Standard APM  |
| **Tool execution** | Per-tool latency (p50/p95/p99), success/failure rates, error rates by tool             | MCP-specific  |
| **Transaction**    | Submission success rate, confirmation time, gas cost, revert rate, slippage vs. quoted | DeFi-specific |

### Alerting

Configure alerts for:

* Unusual transaction volume (spike vs. rolling baseline)
* Large withdrawals exceeding configured thresholds
* Failed transaction clusters (gas estimation issues, contract reverts)
* Tool calls to new/unknown recipient addresses
* Rate limit exhaustion
* Spending limit approaching daily cap (80% threshold)

***

## Security Hardening Checklist

Before going live with real funds, verify every item:

**Wallet Security**:

* [ ] `WALLET_TYPE` is `privy` (never `local` in production)
* [ ] Privy authorization keys are enabled (P-256 asymmetric key quorum)
* [ ] Wallet policy is configured: contract allowlist, method restrictions, transfer limits
* [ ] Policy denies all operations not explicitly allowed (catch-all reject rule)

**Transport Security**:

* [ ] Streamable HTTP transport with TLS 1.3 (auto via Fly.io, Caddy, or Cloudflare)
* [ ] Bearer token or OAuth 2.1 authentication enabled
* [ ] CORS restricted to known client origins
* [ ] Rate limiting active (per-IP and per-token)

**Secret Management**:

* [ ] No raw private keys in environment variables
* [ ] Secrets stored in Fly.io secrets, Doppler, or AWS Secrets Manager (not `.env` files)
* [ ] Pino log redaction rules cover all secret paths
* [ ] `NODE_ENV=production` set (prevents Express stack trace exposure)
* [ ] Core dumps disabled (`ulimit -c 0` in Dockerfile)

**Safety Middleware**:

* [ ] Spending limits configured (per-tx, per-session, daily)
* [ ] Token allowlist enabled (strict mode recommended)
* [ ] Balance circuit breaker threshold set
* [ ] Nonce manager active
* [ ] Pre-flight simulation enabled for all write operations

**Operational**:

* [ ] Health check endpoint responds (`check_setup_health` tool)
* [ ] Structured logging with correlation IDs
* [ ] Monitoring alerts configured for transaction anomalies
* [ ] Credential rotation playbook documented (see [credential-architecture.md](/docs/prd-shared/credential-architecture.md) Section 6)

***

## Deployment Progression

For teams building incrementally, follow this staged approach:

### Stage 0: Zero-Dependency Evaluation (Minute 1)

The fastest path. No accounts, no API keys, no external services. For operators who want to evaluate Gotts Safe before committing to provider accounts.

```bash
npx @gotts.ai setup --zero
```

This generates a local private key (via `viem/accounts`), starts the MCP server with the `data` profile (28 read-only tools), and connects to public RPCs. Zero questions, zero cost, <30 seconds to first tool call.

**What you get:** Read-only access to pool info, token prices, trade history, token search, and portfolio views across all 11 chains.

**What you don't get:** Write operations (trading, LP, vault deposits) require a TEE-backed wallet from Stage 1+.

**Upgrade:** Re-run `npx @gotts.ai setup` to upgrade to a full setup with Privy wallet and write capabilities. See [15-install-wizard.md](/docs/gotts-safe-mcp-server/mcp-server/15-install-wizard.md) for the zero-dependency specification.

### Stage 1: Local Development (Day 1)

```bash
# Clone and install
git clone https://github.com/uniswap/uni-ai && cd uni-ai
pnpm install && pnpm build

# Run as stdio server (Claude Desktop / Claude Code local)
cd packages/safe
cp .env.example .env  # Edit with your ALCHEMY_API_KEY
pnpm dev
```

Secrets in `.env`. Test against Base Sepolia or Anvil fork. Zero networking, zero auth.

### Stage 2: Remote Prototype (Week 1)

```bash
# Deploy to Fly.io with bearer token auth
pnpm build
fly mcp launch "node dist/index.js" \
  --claude --cursor --server uniswap-agent \
  --secret PRIVY_APP_ID=your-id \
  --secret PRIVY_APP_SECRET=your-secret \
  --secret ALCHEMY_API_KEY=your-key
```

Working remote MCP server with minimal effort. Connect Claude Code via `claude mcp add --transport http`.

### Stage 3: Production Hardening (Weeks 2-4)

* Enable Privy authorization keys and policy engine
* Move secrets to Doppler or Fly.io secrets (not `.env`)
* Add Pino structured logging with redaction
* Configure per-tool rate limiting
* Add OpenTelemetry instrumentation
* Enable CORS restrictions
* Test with adversarial inputs (wrong addresses, excessive amounts)

### Stage 4: Enterprise Grade (Month 2+)

* OAuth 2.1 via Auth0 or Keycloak
* Per-tool authorization scopes
* Kubernetes deployment with 3+ replicas and HPA
* Redis for distributed session management
* SIEM integration for audit logging
* Anomaly detection on transaction patterns
* SOC 2 compliance documentation (if needed)

***

## Cost Analysis

Minimum viable production deployment:

| Service     | Plan                                 | Monthly Cost   |
| ----------- | ------------------------------------ | -------------- |
| **Privy**   | Free tier (50K signatures/month)     | $0             |
| **Alchemy** | Free tier (300M compute units/month) | $0             |
| **Fly.io**  | Auto-stop Machine, 256MB RAM         | \~$3-5         |
| **Total**   |                                      | **\~$5/month** |

This gets a production MCP server with TEE-backed wallet signing, multi-chain RPC access, and remote connectivity from any MCP client.

**Scaling costs**: Fly.io charges \~$0.0070/hr for a shared-cpu-1x, 256MB Machine. With auto-stop, a lightly used agent (a few hundred calls/day) costs $3-5/month. Heavy usage (always-on) costs \~$5/month. Additional Machines for multi-user isolation add \~$3-5/month each.

***

## End-to-End Flow: Zero to Vault Agent

The complete operator experience from nothing to a vault-participating agent. This describes the full production setup path. For alternative entry points — zero-dependency evaluation (`--zero`), browser wizard (`--ui`), headless agent-driven setup, or chat-based setup — see [15-install-wizard.md](/docs/gotts-safe-mcp-server/mcp-server/15-install-wizard.md).

```
1. Create Privy app at console.privy.io          (~2 min, one-time)
2. Create Alchemy app at alchemy.com/dashboard    (~1 min, one-time)
3. Run: pnpm setup:production                      (~3 min, interactive)
   ├─ Creates Privy wallet via SDK
   ├─ Generates .env
   ├─ Applies vault-participant policy
   ├─ Deploys to Fly.io
   ├─ Outputs client config JSON
   └─ Runs health check
4. Add config to Claude Code / Cursor             (~30 sec, paste JSON)
5. Agent autonomously:                             (fully automated)
   ├─ Calls check_setup_health
   ├─ Calls register_agent (ERC-8004 identity)
   ├─ Calls vault_simulate_deposit
   └─ Calls vault_deposit (first deposit)
```

**Total operator time**: \~7 minutes. **Total cost**: \~$5/month. **External accounts needed**: Privy (free), Alchemy (free), Fly.io (credit card for billing).

Steps 1-2 are one-time prerequisites. Step 3 is the single automated command. Step 4 is a copy-paste. Step 5 is fully autonomous -- the agent handles identity registration and vault participation without human intervention.

***
