> 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/monorepo-infrastructure/monorepo/11-primitive-packages.md).

# Primitive Packages

> **Part of**: [Monorepo Infrastructure PRD](/docs/monorepo-infrastructure/monorepo.md) | **Last Updated**: 2026-02-21
>
> Specifies the 6 foundational packages that sit below all existing packages in the dependency graph. These were extracted from the Privy PoC (`tmp_repos/privy-poc`) and provide the shared building blocks for wallet key management, on-chain math, policy construction, and testing infrastructure.

***

## Overview

Six new packages form a primitive layer below `@gotts.ai/safe`, `@gotts.ai/vault`, and `@gotts.ai/portal`:

```
@gotts.ai/core         (zero workspace deps)
       │
       ├──────────────────────────┐
       ▼                          ▼
@gotts.ai/chain        @gotts.ai/crypto
(zero workspace deps)  (zero workspace deps)
       │                          │
       ▼                          │
@gotts.ai/policy       ──────────┘
(depends on chain)
       │
       ▼
@gotts.ai/wallet       (depends on core, chain, crypto, policy)
       │
       ├─────────────────┬─────────────────┐
       ▼                 ▼                 ▼
@gotts.ai/safe   @gotts.ai/vault   @gotts.ai/portal
```

**`@gotts.ai/test-utils`** is a shared dev dependency used by all packages in tests.

***

## Package Specifications

### `packages/core` → `@gotts.ai/core`

**Type**: Published (npm `@gotts.ai` scope) **Runtime dependencies**: None (Node.js built-ins only)

#### Purpose

Provides config I/O, error types, and constants shared across all Gotts packages. The only package that knows where the Gotts config file lives and how to read/write it.

#### API Shape

```typescript
// Config I/O
export function loadConfig(): GottsConfig | null;
export function saveConfig(config: GottsConfig): void;
export function configPath(): string;
export function configExists(): boolean;

// Config schema
export interface GottsConfig {
  mode: "privy" | "local";
  walletId?: string; // Privy wallet ID (mode: 'privy' only)
  walletAddress?: string; // Ethereum address
  authPrivateKey?: string; // P-256 auth key, base64 DER (mode: 'privy' only)
  authPublicKey?: string; // P-256 public key, base64 SPKI (for CLI pairing)
  allowedAddresses?: string[]; // Policy allowlist
  webAppUrl?: string; // Portal server URL for keychain/proxy mode
  privyAppId?: string; // Public Privy app ID
  createdAt?: string; // ISO 8601 timestamp
  agentId?: string; // ERC-8004 agent ID (persisted after registration)
}

// Constants
export const PRIVY_API_BASE = "https://api.privy.io";
export const CONFIG_DIR = "~/.gotts";
export const CONFIG_FILE = "~/.gotts/config.json";

// Error classes
export class GottsError extends Error {
  code: string;
}
export class ConfigError extends GottsError {}
export class AuthError extends GottsError {}

// Utility
export function fetchPrivyAppId(webAppUrl: string): Promise<string>;
```

**Config file**: `~/.gotts/config.json` with `0o600` permissions (owner read/write only).

#### Tests

* Config read/write round-trip (file mode)
* `loadConfig()` returns null when file does not exist
* `saveConfig()` creates parent directory if needed
* `configPath()` expands `~` correctly on all platforms
* Config validation rejects unknown `mode` values
* `ConfigError` and `AuthError` extend `GottsError`
* `fetchPrivyAppId()` returns appId from `/api/info` endpoint (MSW mocked)

***

### `packages/chain` → `@gotts.ai/chain`

**Type**: Internal (no npm publish; TypeScript source via `exports`) **Runtime dependencies**: None (pure TypeScript, no external packages)

#### Purpose

ETH math (pure BigInt, no floats), address utilities, a lightweight JSON-RPC client, and the canonical chain registry. Used by `@gotts.ai/wallet`, `@gotts.ai/safe`, and `@gotts.ai/vault`.

#### API Shape

```typescript
// ETH Math (pure BigInt — no floats, no rounding errors)
export function parseEtherToHex(eth: string): Hex; // "1.5" → "0x14d1120d7b160000"
export function formatWeiToEth(wei: bigint): string; // 1500000000000000000n → "1.5"
export function parseHexToWei(hex: Hex): bigint; // "0x14d1120d7b160000" → 1500000000000000000n
export function formatHexToEth(hex: Hex): string; // "0x14d1120d7b160000" → "1.5"

// Address utils
export function isValidAddress(address: string): address is Address;
export function normalizeAddress(address: string): Address; // lowercases, validates
export function truncateAddress(address: Address): string; // "0x1234...5678" (6+4)

// RPC client (JSON-RPC 2.0, no SDK dependency)
export class RpcClient {
  constructor(rpcUrl: string);
  getBalance(address: Address): Promise<bigint>;
  estimateGas(tx: TransactionRequest): Promise<bigint>;
  call(tx: TransactionRequest): Promise<Hex>;
}

// Chain registry (all 11 Uniswap-supported chains)
export const chains: Record<ChainId, Chain>;
export const BASE_CHAIN_ID = 8453;

// Types
export type Address = `0x${string}`;
export type Hex = `0x${string}`;
export type ChainId =
  | 1
  | 10
  | 56
  | 137
  | 324
  | 8453
  | 42161
  | 42220
  | 43114
  | 81457
  | 130;
export interface Chain {
  id: ChainId;
  name: string;
  rpcUrl: string;
  blockExplorer: string;
}
export interface TransactionRequest {
  to: Address;
  value?: Hex;
  data?: Hex;
  from?: Address;
}
```

#### Tests

* `parseEtherToHex` / `formatWeiToEth` round-trip for 0, 1, 1.5, max safe value
* `isValidAddress` accepts valid checksummed and lowercase addresses; rejects invalid
* `truncateAddress` produces correct 6+4 format
* `RpcClient.getBalance` parses hex balance (MSW mocked JSON-RPC)
* `RpcClient.estimateGas` returns bigint (MSW mocked)
* `chains` contains all 11 Uniswap chain IDs

***

### `packages/crypto` → `@gotts.ai/crypto`

**Type**: Internal (no npm publish; TypeScript source via `exports`) **Runtime dependencies**: `@noble/curves` (P-256), `@noble/hashes` (SHA-256), `canonicalize`

#### Purpose

P-256 key generation, canonical JSON signing (RFC 8785), DER encoding/decoding, Privy-compatible auth payload construction, and optional OS keychain storage. The cryptographic foundation for Privy authorization key management.

#### API Shape

```typescript
// Key generation
export function generateKeyPair(): {
  publicKeyB64: string;
  privateKeyB64: string;
};
// Returns SPKI DER (public) and PKCS8 DER (private), both base64-encoded

// Signing protocol
export function buildSigningPayload(data: unknown): string; // RFC 8785 canonical JSON
export function signPayload(payload: string, privateKeyB64: string): string; // base64 signature
export function verifyPayload(
  payload: string,
  signature: string,
  publicKeyB64: string,
): boolean;

// Privy-specific
export function buildPrivySigningPayload(
  method: string,
  params: unknown,
): string;
// Wraps payload in Privy's auth header format

// DER encoding utilities (for interop with OpenSSL, Web Crypto API)
export function encodePrivateKeyToPkcs8(scalar: Uint8Array): Uint8Array;
export function encodePublicKeyToSpki(point: Uint8Array): Uint8Array;
export function extractPrivateScalar(pkcs8Der: Uint8Array): Uint8Array;
export function derivePublicKeyFromPrivate(privateKeyB64: string): string; // → publicKeyB64

// OS Keychain (optional — requires `keytar` peer dependency)
export function storePrivyCredentials(
  service: string,
  creds: PrivyCredentials,
): Promise<void>;
export function loadPrivyCredentials(
  service: string,
): Promise<PrivyCredentials | null>;

export interface PrivyCredentials {
  appId: string;
  appSecret: string;
  authPrivateKey: string;
  walletId: string;
}
```

**Keychain support**: Uses `keytar` (optional peer dependency) for OS keychain access:

* macOS: Keychain
* Linux: Secret Service (via libsecret)
* Windows: Credential Manager

If `keytar` is not installed, `storePrivyCredentials` / `loadPrivyCredentials` throw a `ConfigError` with an install hint.

#### Tests

* Key generation produces valid P-256 keypair (DER round-trip)
* `signPayload` → `verifyPayload` round-trip (positive and negative cases)
* `buildSigningPayload` produces canonicalized JSON (property order, unicode normalization)
* `extractPrivateScalar` + `derivePublicKeyFromPrivate` are inverses
* `buildPrivySigningPayload` matches Privy's documented auth format
* Keychain store/load round-trip (keytar mocked)

***

### `packages/policy` → `@gotts.ai/policy`

**Type**: Internal (no npm publish; TypeScript source via `exports`) **Runtime dependencies**: `@gotts.ai/chain` (address validation only)

#### Purpose

Privy policy DSL — types, fluent builder, quick builders, resolvers, and validators. Eliminates manual JSON policy authoring. Every policy used in the Gotts ecosystem should be created via this package.

#### API Shape

```typescript
// Privy policy types (matches Privy API v1)
export type ConditionOperator = "eq" | "in" | "lte" | "gte";
export type FieldSource = "ethereum_transaction" | "ethereum_calldata";
export type PolicyAction = "ALLOW" | "DENY";

export interface Condition {
  field_source: FieldSource;
  field: string;
  operator: ConditionOperator;
  value: string | string[];
  abi?: object[];
}

export interface Rule {
  name: string;
  method: "eth_sendTransaction" | "personal_sign" | "eth_signTypedData" | "*";
  action: PolicyAction;
  conditions: Condition[];
}

export interface Policy {
  version: "1.0";
  name: string;
  chain_type: "ethereum";
  rules: Rule[];
  owner?: { public_key: string };
}

// Fluent builder
export class PolicyBuilder {
  allowTo(address: string): this;
  allowToMany(addresses: string[]): this;
  allowMethod(contractAddress: string, methodName: string, abi: object[]): this;
  allowValue(lteWei: string): this;
  deny(description?: string): this;
  withOwner(publicKeyB64: string): this;
  build(): Policy;
}

// Quick builders
export function buildSendOnlyPolicy(allowedAddresses: string[]): Policy;
// Handles eq vs in automatically (eq for single address, in for multiple)

// Resolvers
export function extractAllowedAddresses(policy: Policy): string[];
export function extractAllowedAddressesFromRaw(raw: unknown): string[];
// Parse policy object back to recipient list

// Validators
export interface ValidationResult {
  valid: boolean;
  errors?: string[];
}
export function validatePolicy(policy: Policy): ValidationResult;
export function wouldAllow(
  policy: Policy,
  tx: { to: string; value?: string; data?: string },
): boolean;
// Simulate policy decision without calling Privy API
```

#### Tests

* `PolicyBuilder` produces valid `Policy` object for all combinations
* `buildSendOnlyPolicy(["0x..."])` uses `eq` operator
* `buildSendOnlyPolicy(["0x...", "0x..."])` uses `in` operator
* `extractAllowedAddresses` recovers addresses from both `eq` and `in` rules
* `validatePolicy` rejects unknown operators, empty rules array, missing version
* `wouldAllow` returns true for matching tx, false for non-matching tx and DENY rules

***

### `packages/wallet` → `@gotts.ai/wallet`

**Type**: Published (npm `@gotts.ai` scope) **Runtime dependencies**: `@gotts.ai/core`, `@gotts.ai/chain`, `@gotts.ai/crypto`, `@gotts.ai/policy` **Optional peer dependencies**: `agent0-sdk` (>=1.5.0) — used by `registerAgent()` for Tier 1/2 registration. Falls back to direct contract calls if not installed.

#### Purpose

The primary wallet abstraction for agents. Two modes — Privy TEE (production) and local private key (dev/testing). Provides `getBalance`, `send`, `estimateGas`, and `getInfo` with a consistent interface regardless of mode. Also provides Privy API helpers for programmatic wallet and policy management.

#### API Shape

```typescript
export interface WalletConfig {
  mode: "privy" | "local";
  // Privy mode
  appId?: string;
  appSecret?: string;
  walletId?: string;
  authPrivateKey?: string; // P-256 DER private key, base64
  privyMode?: "proxy" | "self-hosted";
  webAppUrl?: string; // Required for privyMode: 'proxy'
  // Local mode (dev/testing only)
  privateKey?: string; // Raw Ethereum private key (0x-prefixed)
  chainId?: number; // Default: 8453 (Base)
}

export class GottsWallet {
  constructor(config: WalletConfig);

  // Core operations
  getBalance(): Promise<string>; // Returns formatted ETH string
  send(to: string, amountEth: string): Promise<string>; // Returns tx hash
  estimateGas(to: string, amountEth: string): Promise<string>; // Returns formatted ETH string
  getInfo(): WalletInfo;

  // Privy API helpers (Privy mode only)
  createPolicy(policy: Policy): Promise<string>; // Returns policy ID
  createWallet(policyId?: string): Promise<{ id: string; address: string }>;
  listWallets(): Promise<Array<{ id: string; address: string }>>;
}

export interface WalletInfo {
  address: string;
  mode: "privy" | "local";
  walletId?: string; // Privy mode only
  chainId: number;
}

// Error types
export class WalletError extends Error {
  code: string;
}
export class AddressNotAllowedError extends WalletError {
  address: string;
}
export class CredentialsError extends WalletError {}

// EIP-1193 provider bridge (for Agent0 SDK integration)
// See prd/shared/erc8004-integration.md for the full spec.
export interface GottsEIP1193Provider {
  request(args: {
    method: string;
    params?: unknown[] | Record<string, unknown>;
  }): Promise<unknown>;
}

export function createEIP1193Provider(
  wallet: GottsWallet,
): GottsEIP1193Provider;
// Returns an EIP-1193 provider that routes signing to the wallet's backend
// and proxies all other RPC calls to the configured chain RPC URL.
// Compatible with agent0-sdk's `walletProvider` config option (v1.5.0+).

// Agent identity registration (wraps Agent0 SDK with three-tier fallback)
export interface AgentRegistrationConfig {
  name?: string; // Default: "agent-<address-prefix>"
  description?: string; // Default: "Gotts agent on <chain>"
  role?: "vault_participant" | "vault_manager" | "vault_creator";
  mcpEndpoint?: string; // Auto-detected from transport config
  a2aEndpoint?: string; // Optional
  ipfsMode?: "gotts" | "pinata" | "inline"; // Default: 'gotts'
  pinataJwt?: string; // Required only when ipfsMode='pinata'
}

export interface RegistrationResult {
  agentId: string; // ERC-8004 token ID
  agentURI?: string; // IPFS or inline URI
  txHash: string;
  registrationMethod: "agent0-ipfs" | "agent0-inline" | "direct-contract";
}

export function registerAgent(
  wallet: GottsWallet,
  config?: AgentRegistrationConfig,
): Promise<RegistrationResult>;
// Three-tier fallback: (1) Agent0 SDK + Gotts IPFS proxy, (2) Agent0 SDK + inline URI,
// (3) direct contract call via viem. Falls back automatically if agent0-sdk is not installed.
```

**Privy sub-modes:**

| Sub-mode      | Description                                                                             | When to Use                             |
| ------------- | --------------------------------------------------------------------------------------- | --------------------------------------- |
| `proxy`       | Routes signing through Portal server (`webAppUrl`) — private key never on agent machine | Agent machines without Privy App Secret |
| `self-hosted` | Signs directly via Privy API using `appId` + `appSecret` + `authPrivateKey`             | Operators who self-host                 |

**Local mode warning**: The `local` mode uses `viem`'s `privateKeyToAccount`. It provides no TEE isolation, no policy enforcement, and no key rotation. Use only for local dev, CI testing, and prototyping. Never deploy to production. Never commit `.env` files containing raw private keys.

#### Tests

* `GottsWallet` (Privy mode, self-hosted): `getBalance`, `send`, `estimateGas` via MSW-mocked Privy API
* `GottsWallet` (Privy mode, proxy): routes requests through `webAppUrl/api/send` (MSW mocked)
* `GottsWallet` (local mode): `getBalance` via MSW-mocked RPC, `send` signs locally
* `AddressNotAllowedError` thrown when policy rejects recipient (MSW simulates 403)
* `CredentialsError` thrown on invalid credentials (MSW simulates 401)
* `createPolicy` → `createWallet` flow (full Privy API mock)
* `listWallets` pagination handling
* `createEIP1193Provider` routes `eth_accounts` locally and `eth_sendTransaction` to wallet backend
* `createEIP1193Provider` proxies `eth_call` and `eth_getBalance` to RPC (MSW mocked)
* `registerAgent` Tier 1: Agent0 SDK + IPFS proxy (MSW mocked SDK + proxy)
* `registerAgent` Tier 2: Agent0 SDK + inline URI (MSW mocked SDK)
* `registerAgent` Tier 3: direct contract call when agent0-sdk not installed (MSW mocked RPC)
* `registerAgent` fallback chain: Tier 1 IPFS failure falls back to Tier 2

***

### `packages/test-utils` → `@gotts.ai/test-utils`

**Type**: Internal dev dependency (not published; not included in production builds) **Runtime dependencies**: `msw`, `@testing-library/react`, `happy-dom`

#### Purpose

Shared MSW (Mock Service Worker) handlers and test fixtures used across all packages. Prevents duplication of Privy API mocks, RPC mocks, and React test wrappers across the monorepo.

#### API Shape

```typescript
// MSW handlers for Privy API
export const privyHandlers: RequestHandler[];
// Mocks: GET /v1/wallets, POST /v1/wallets, POST /v1/wallets/{id}/rpc,
//         POST /v1/policies, GET /v1/policies/{id}

// MSW handlers for JSON-RPC
export const rpcHandlers: RequestHandler[];
// Mocks: eth_getBalance, eth_estimateGas, eth_call, eth_sendRawTransaction

// MSW handlers for Portal API
export const portalHandlers: RequestHandler[];
// Mocks: GET /api/info, POST /api/create-wallet, POST /api/send,
//         GET /api/wallets, POST /api/cli-session, GET /api/cli-session/{id}

// MSW handlers for Agent0 SDK and ERC-8004
export const agent0Handlers: RequestHandler[];
// Mocks: Agent0 SDK registration calls, IPFS upload (returns deterministic CID)

// MSW handlers for ERC-8004 Identity Registry contract (direct contract fallback)
export const identityRegistryHandlers: RequestHandler[];
// Mocks: register() call simulation, event emission

// MSW handlers for Gotts API (IPFS proxy + faucet)
export const faucetHandlers: RequestHandler[];
// Mocks: POST /v1/faucet/request (success + 409 already claimed),
//         GET /v1/faucet/status, POST /v1/ipfs/pin

// Composite server factory
export function createTestServer(...handlers: RequestHandler[][]): SetupServer;

// Default combined server (all handlers)
export const testServer: SetupServer;

// React test utilities
export { render, screen, waitFor, userEvent } from "@testing-library/react";
export function renderWithProviders(ui: React.ReactElement): RenderResult;

// Test fixtures
export const TEST_WALLET_ADDRESS = "0x1234567890abcdef1234567890abcdef12345678";
export const TEST_WALLET_ID = "wlt_test_abc123";
export const TEST_POLICY_ID = "pol_test_xyz789";
export const TEST_PRIVATE_KEY =
  "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
// (Anvil default key #0 — safe to use in tests, never use in production)
```

#### Usage Pattern

```typescript
// In any package's test file:
import {
  createTestServer,
  privyHandlers,
  rpcHandlers,
} from "@gotts.ai/test-utils";

const server = createTestServer(privyHandlers, rpcHandlers);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
```

**Used by**: `packages/core`, `packages/chain`, `packages/crypto`, `packages/policy`, `packages/wallet`, `packages/safe`, `packages/vault`, `packages/portal`

***

## Dependency Graph (Full Primitive Layer)

```
@gotts.ai/core         (published)
    │  Config I/O, error types, constants
    │  Zero workspace deps
    │
    ├──────────────────────────────────┐
    ▼                                  ▼
@gotts.ai/chain        @gotts.ai/crypto
    │  ETH math, address utils, RPC       P-256, signing, DER, keychain
    │  Zero workspace deps                Zero workspace deps
    │
    └──────────────────┐
                       ▼
@gotts.ai/policy       (depends on chain for address validation)
    │  Privy policy DSL, builders, validators
    │
    └──────────────────────────────────────────┐
                                               ▼
@gotts.ai/wallet       (published)
    │  Depends on: core, chain, crypto, policy
    │  Optional peer: agent0-sdk (>=1.5.0)
    │  GottsWallet, createEIP1193Provider, registerAgent
    │
    ├─────────────────┬─────────────────┐
    ▼                 ▼                 ▼
@gotts.ai/safe   @gotts.ai/vault   @gotts.ai/portal

@gotts.ai/test-utils   (internal dev dep, not in production graph)
    │  MSW handlers, test fixtures
    │  Used by all packages in tests
```

***

## Package Metadata

| Package               | npm Scope              | Publish | Node min | Vitest | tsdown |
| --------------------- | ---------------------- | ------- | -------- | ------ | ------ |
| `packages/core`       | `@gotts.ai/core`       | Yes     | 20+      | Yes    | Yes    |
| `packages/chain`      | `@gotts.ai/chain`      | No      | 20+      | Yes    | No     |
| `packages/crypto`     | `@gotts.ai/crypto`     | No      | 20+      | Yes    | No     |
| `packages/policy`     | `@gotts.ai/policy`     | No      | 20+      | Yes    | No     |
| `packages/wallet`     | `@gotts.ai/wallet`     | Yes     | 20+      | Yes    | Yes    |
| `packages/test-utils` | `@gotts.ai/test-utils` | No      | 20+      | N/A    | No     |

**Versioning**: `@gotts.ai/core` and `@gotts.ai/wallet` follow semver and are managed via Changesets alongside the other published packages. Internal packages do not have published versions.

***

## Design Decisions

| Decision                                 | Rationale                                                                                                                                                       |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `core` and `chain` are separate packages | `core` is config/auth/errors (may be used in browser). `chain` is RPC/ETH math (Node.js only). Keeps browser-safe code separate from Node-only code.            |
| `crypto` has zero workspace deps         | Cryptographic primitives must be auditable in isolation. No accidental transitive dependencies from the DeFi layer.                                             |
| `wallet` is the only published primitive | Agents and portal apps import `GottsWallet`. Internal packages (`chain`, `crypto`, `policy`) are implementation details of `wallet`.                            |
| Local mode warning is prominent          | Raw private keys have no place in production. The mode name `local` and the explicit `CredentialsError` on misconfiguration make this clear.                    |
| `test-utils` not in production graph     | Dev-only. Never imported in `src/` — only in `test/`. Enforced by ESLint rule (`no-restricted-imports` for `@gotts.ai/test-utils` outside `test/` directories). |
| MSW over nock/vitest mocks               | MSW intercepts at the network layer, making tests more realistic (catches serialization bugs). All packages can share the same handlers.                        |
