> 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/prd-shared/testnet-faucet.md).

# Testnet Faucet

> **Type**: SPEC (normative) | **Last Updated**: 2026-02-21
>
> Minimal API spec for the Gotts testnet faucet service. This is a standalone micro-service (part of `api.gotts.ai`, not the MCP server) that the install wizard calls during testnet setup to eliminate funding friction for identity registration.

***

## Purpose

When an operator runs `npx @gotts.ai setup` targeting Sepolia or Base Sepolia, the wizard automatically requests testnet ETH so ERC-8004 identity registration can proceed without manual faucet hunting. The faucet is a convenience layer -- not a critical path. If it fails, the wizard falls back to showing the wallet address and asking the operator to fund manually.

***

## API Spec

Base URL: `https://api.gotts.ai`

### `POST /v1/faucet/request`

Request testnet ETH for a wallet address.

**Authentication**: Bearer token containing the operator's Privy App ID. The faucet verifies the App ID by calling Privy's public config API (`GET https://auth.privy.io/api/v1/apps/{appId}/config`). Valid response = legitimate operator. No separate faucet API key needed.

**Request**:

```json
{
  "address": "0x1a2B3c4D5e6F7a8B9c0D1e2F3a4B5c6D7e8F9a0B",
  "chainId": 84532,
  "privyAppId": "clx1234567890abcdef"
}
```

| Field        | Type     | Required | Description                                                          |
| ------------ | -------- | -------- | -------------------------------------------------------------------- |
| `address`    | `string` | Yes      | Recipient wallet address (0x-prefixed, checksummed)                  |
| `chainId`    | `number` | Yes      | Target chain ID. Must be 11155111 (Sepolia) or 84532 (Base Sepolia). |
| `privyAppId` | `string` | Yes      | Operator's Privy App ID for abuse prevention                         |

**Success Response (200)**:

```json
{
  "txHash": "0xabc123def456789...",
  "amount": "0.01",
  "chainId": 84532
}
```

**Already Claimed (409)**:

```json
{
  "error": "ALREADY_CLAIMED",
  "message": "This wallet has already received testnet ETH",
  "claimedAt": "2026-02-20T10:00:00Z"
}
```

**Rate Limited (429)**:

```json
{
  "error": "RATE_LIMITED",
  "message": "Too many requests. Try again in 60 seconds.",
  "retryAfter": 60
}
```

**Invalid Chain (400)**:

```json
{
  "error": "INVALID_CHAIN",
  "message": "Faucet only supports Sepolia (11155111) and Base Sepolia (84532). Received: 8453"
}
```

**Faucet Paused (503)**:

```json
{
  "error": "FAUCET_PAUSED",
  "message": "Faucet is temporarily paused due to low balance. Try again later."
}
```

### `GET /v1/faucet/status`

Health check and remaining balance. No authentication required.

**Response (200)**:

```json
{
  "status": "active",
  "chains": {
    "11155111": { "balance": "4.23", "claimsToday": 42 },
    "84532": { "balance": "8.91", "claimsToday": 17 }
  }
}
```

If balance is below threshold:

```json
{
  "status": "paused",
  "reason": "Low balance on Base Sepolia",
  "chains": {
    "11155111": { "balance": "2.10", "claimsToday": 35 },
    "84532": { "balance": "0.05", "claimsToday": 89 }
  }
}
```

***

## Anti-Abuse Measures

| Measure                       | Implementation                                                                         |
| ----------------------------- | -------------------------------------------------------------------------------------- |
| **One-time per wallet**       | Store `(address, chainId)` pairs in SQLite. Reject duplicates with 409.                |
| **One-time per Privy App**    | Store `(privyAppId, chainId)` pairs. Each app gets one claim per chain.                |
| **Rate limiting**             | 5 requests per IP per minute (regardless of success/failure).                          |
| **Amount cap**                | Fixed 0.01 ETH per claim. Non-configurable.                                            |
| **Chain restriction**         | Only Sepolia (11155111) and Base Sepolia (84532). Rejects mainnet chain IDs with 400.  |
| **Balance monitoring**        | Alert when faucet wallet balance < 1 ETH. Auto-pause when < 0.1 ETH.                   |
| **Privy App ID verification** | Validates App ID against Privy's public API before processing. Invalid IDs return 401. |

***

## Supported Chains

| Chain            | Chain ID | Faucet Available | Notes                                                                                                            |
| ---------------- | -------- | ---------------- | ---------------------------------------------------------------------------------------------------------------- |
| Sepolia          | 11155111 | Yes              | Ethereum testnet                                                                                                 |
| Base Sepolia     | 84532    | Yes              | Base testnet                                                                                                     |
| Base mainnet     | 8453     | No               | Use paymaster-sponsored UserOp (see [erc8004-integration.md](/docs/prd-shared/erc8004-integration.md) Section 6) |
| Ethereum mainnet | 1        | No               | Requires real ETH                                                                                                |

***

## Implementation

Minimal Node.js service (\~100 LOC for the faucet routes). Part of the `api.gotts.ai` service alongside the IPFS proxy (see [erc8004-integration.md](/docs/prd-shared/erc8004-integration.md) Section 8).

**Dependencies**: `express`, `viem`, `better-sqlite3`

**Storage**: Single SQLite table:

```sql
CREATE TABLE claims (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  address TEXT NOT NULL,
  chain_id INTEGER NOT NULL,
  privy_app_id TEXT NOT NULL,
  tx_hash TEXT NOT NULL,
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
  UNIQUE(address, chain_id),
  UNIQUE(privy_app_id, chain_id)
);
```

**Deployment**: Fly.io (same app as IPFS proxy). Faucet wallet private keys stored as Fly.io secrets (`FAUCET_PRIVATE_KEY_SEPOLIA`, `FAUCET_PRIVATE_KEY_BASE_SEPOLIA`). Each chain has a dedicated faucet wallet.

**Monitoring**: The `/v1/faucet/status` endpoint exposes current balance. Alerting via Fly.io metrics when balance < 1 ETH. Auto-pause (return 503) when balance < 0.1 ETH.

***

## Wizard Integration

The install wizard detects testnet from the `--chains` flag or RPC URL. When testnet is detected and wallet balance is zero, it automatically calls the faucet before attempting identity registration. No prompt needed:

```
[3/6] Register agent identity (recommended)
      Requesting testnet ETH from Gotts faucet... 0.01 ETH received
      Registering on Base Sepolia via Agent0 SDK...
      Agent ID: 42 · Tier: Unverified
```

If the faucet fails (already claimed, rate limited, or paused):

```
[3/6] Register agent identity (recommended)
      Faucet unavailable (already claimed for this wallet)
      Send ~0.005 ETH to 0x1a2B...3c4D on Base Sepolia
      Waiting for funds... (60s timeout, press Enter to skip)
```

See [mcp-server/15-install-wizard.md](/docs/gotts-safe-mcp-server/mcp-server/15-install-wizard.md) Step 3/6 for the full wizard integration flow.

***

## Cross-References

| Document                                                                                       | Reference                                                     |
| ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| [erc8004-integration.md](/docs/prd-shared/erc8004-integration.md)                              | Gas strategy Tier 2 references this faucet spec               |
| [mcp-server/15-install-wizard.md](/docs/gotts-safe-mcp-server/mcp-server/15-install-wizard.md) | Wizard Step 3/6 calls the faucet before identity registration |
