# tcloud

TypeScript SDK and CLI for [Tangle Router](https://router.tangle.tools): model routing, sandbox-backed agent calls, operator routing, and anonymous payments via ShieldedCredits.

Zero framework dependencies. Pure `fetch` + SSE. Works in Node.js, Deno, Bun, and edge runtimes.

## Table of Contents

- [Installation](#installation)
- [SDK](#sdk)
  - [Quick Start](#quick-start)
  - [Model Selection](#model-selection)
  - [Streaming](#streaming)
  - [Full Chat Completion](#full-chat-completion)
  - [Private Inference](#private-inference)
  - [Embeddings](#embeddings)
  - [Web Search](#web-search)
  - [Video & Avatar Generation](#video--avatar-generation)
  - [Async Jobs](#async-jobs)
  - [Operator Routing](#operator-routing)
  - [Models and Operators](#models-and-operators)
  - [API Key Management](#api-key-management)
  - [Cost Estimation](#cost-estimation)
  - [Spending Limits](#spending-limits)
- [CLI](#cli)
  - [Authentication](#authentication)
  - [Chat](#chat)
  - [Browse](#browse)
  - [Credits and Keys](#credits-and-keys)
  - [Wallet Management](#wallet-management)
- [Configuration](#configuration)
- [OpenAI SDK Compatibility](#openai-sdk-compatibility)
- [Vercel AI SDK Compatibility](#vercel-ai-sdk-compatibility)
- [Examples](#examples)
- [License](#license)

## Installation

```bash
npm install @tangle-network/tcloud
```

Or run the CLI directly:

```bash
npx @tangle-network/tcloud chat "What is Tangle?"
```

Do not install unscoped `tcloud`. It is unrelated to Tangle.

## SDK

### Quick Start

```ts
import { TCloud } from '@tangle-network/tcloud'

// Set model at client creation — explicit and consistent.
const client = new TCloud({
  apiKey: 'sk-tan-...',
  model: 'gpt-4o-mini',
})

const answer = await client.ask('What is Tangle Network?') // string
console.log(answer)
```

### Quick Start — direct cli-bridge (subscription-backed coding harness)

When you have your own [cli-bridge](https://github.com/drewstone/cli-bridge) running (locally or on your own VPS), point the SDK straight at it. cli-bridge is OpenAI-compatible (`/v1/chat/completions`) so chat / ask / askStream work unchanged — and your CLI subscriptions on the bridge box pay for the LLM tokens directly (no router, no per-token billing).

```bash
# 1. clone + run cli-bridge (one terminal)
gh repo clone drewstone/cli-bridge && cd cli-bridge
echo "BRIDGE_BEARER=$(openssl rand -hex 32)" >> .env.local
echo "BRIDGE_BACKENDS=claude,passthrough"     >> .env.local
export $(grep -v '^#' .env.local | xargs) && pnpm exec tsx src/server.ts

# 2. authenticate your harness CLI
claude /login    # or: kimi login, codex login, opencode auth login
```

```ts
import { TCloudClient } from '@tangle-network/tcloud'

const client = TCloudClient.fromCliBridge({
  url: 'http://127.0.0.1:3344',           // or any reachable URL
  bearer: process.env.CLI_BRIDGE_BEARER!, // value of BRIDGE_BEARER from .env.local
})

// model id = `<harness>/<model>` — no `bridge/` prefix in direct mode
const reply = await client.ask('reply with OK', 'claude-code/sonnet')

// streaming + full chat work the same
for await (const chunk of client.askStream('write a haiku', 'kimi-code/kimi-for-coding')) {
  process.stdout.write(chunk)
}
```

See [`examples/14-direct-cli-bridge.ts`](./examples/14-direct-cli-bridge.ts) for the full pattern. Direct cli-bridge clients also support `client.bridge({ harness, model, resume })`; in direct mode the SDK sends `<harness>/<model>` and maps `resume` to cli-bridge's `session_id`.

### Model Selection

Model is set at client creation. Override per-request when needed:

```ts
// Default model for all requests
const client = new TCloud({ apiKey: '...', model: 'gpt-4o-mini' })
await client.ask('Hello')  // returns string

// Full control per-request (OpenAI-compatible)
const completion = await client.chat({
  model: 'claude-sonnet-4-6',
  messages: [{ role: 'user', content: 'Hello' }],
  temperature: 0.5,
  maxTokens: 100,
}) // returns ChatCompletion { id, model, choices: [{ index, message, finish_reason }], usage? }

// Get full response with usage stats
const full = await client.askFull('Hello') // returns ChatCompletion
console.log(full.model, full.usage?.total_tokens)

// Search available models
const llamas = await client.searchModels('llama') // returns Model[]
```

### Streaming

```ts
for await (const chunk of client.askStream('Explain zero-knowledge proofs')) {
  process.stdout.write(chunk)
}

// With model override
for await (const chunk of client.askStream('Hello', 'claude-sonnet-4-6')) {
  process.stdout.write(chunk)
}
```

### Full Chat Completion

OpenAI-compatible request/response format:

```ts
const completion = await client.chat({
  model: 'meta-llama/llama-4-maverick',
  messages: [
    { role: 'system', content: 'You are a helpful assistant.' },
    { role: 'user', content: 'Hello' },
  ],
  temperature: 0.7,
  maxTokens: 1024,
}) // returns ChatCompletion

console.log(completion.choices[0].message.content)
// completion.usage => { prompt_tokens, completion_tokens, total_tokens }
```

### Private Inference

Anonymous inference with no API key. Uses EIP-712 SpendAuth signatures — the operator verifies payment without learning your identity.

```ts
import { TCloud } from '@tangle-network/tcloud'

const client = TCloud.shielded()
const answer = await client.ask('Hello from the shadows')
```

Under the hood: generates an ephemeral wallet, signs a SpendAuth payload, and sends it as an `X-Payment-Signature` header. The operator validates the cryptographic proof and serves inference without knowing who you are.

### Embeddings

```ts
const response = await client.embeddings({
  model: 'text-embedding-3-small',
  input: 'What is Tangle?',
}) // returns EmbeddingResponse
// EmbeddingResponse: { object, data: [{ object, embedding: number[], index }], model, usage }
console.log(response.data[0].embedding.length) // 1536
```

### Web Search

```ts
const results = await client.search({
  query: 'latest Tangle docs',
  provider: 'exa',
  maxResults: 5,
})

console.log(results.data[0].title)
console.log(results.citations)
```

Use search inside chat with either the typed gateway shorthand or OpenRouter-compatible plugins:

```ts
await client.ask('What changed in Tangle this week?', {
  webSearch: { provider: 'exa', maxResults: 5, searchRecency: 'week' },
})

await client.chat({
  messages: [{ role: 'user', content: 'Summarize recent AI video model releases.' }],
  plugins: [{ id: 'web', engine: 'parallel', max_results: 5 }],
})
```

### Video & Avatar Generation

```ts
// Generate a video from a text prompt
const video = await client.videoGenerate({
  prompt: 'A sunset over mountains',
  duration: 5,
}) // returns VideoResponse { id, status, url?, error? }

// Generate a talking-head avatar video
const avatar = await client.avatarGenerate({
  audio_url: 'https://example.com/narration.mp3',
  image_url: 'https://example.com/face.jpg',
}) // returns AvatarGenerateResponse { job_id, status, result?, error? }
```

### Async Jobs

Avatar and video generation are asynchronous. Use `watchJob()` for real-time SSE streaming of job progress, or poll with `avatarJobStatus()`.

```ts
// Submit an avatar job
const job = await client.avatarGenerate({
  audio_url: 'https://...',
  image_url: 'https://...',
})
console.log(job.job_id) // 'job-abc123'
console.log(job.status) // 'queued'

// Watch until complete (SSE streaming)
const result = await client.watchJob(job.job_id, {
  onEvent: (e) => console.log(`${e.status} ${e.progress ?? ''}%`),
}) // returns JobEvent { status, progress?, result?, error?, timestamp }
console.log(result.result) // { video_url: 'https://...' }

// Or just poll
const status = await client.avatarJobStatus(job.job_id)
// returns AvatarJobStatus { job_id, status, result?, error? }
```

### Operator Routing

Route requests to specific operators or use strategy-based selection:

```ts
const client = new TCloud({
  apiKey: 'sk-tan-...',
  routing: {
    prefer: 'operator-slug',    // specific operator
    strategy: 'lowest-latency', // or 'lowest-price', 'highest-reputation'
    region: 'us-east',          // geographic preference
  },
})
```

The gateway selects the best operator based on a composite score (reputation 40%, latency 30%, price 30%) and returns the selection in response headers:

```ts
// After a request, check which operator served it:
// X-Tangle-Operator: <slug>
// X-Tangle-Price-Input: <per-token>
// X-Tangle-Price-Output: <per-token>
```

### Models and Operators

```ts
// List all available models
const models = await client.models() // returns Model[]
// Model: { id, name, context_length, pricing: { prompt, completion }, _provider? }
models.forEach(m => console.log(m.id, m._provider))

// Search models by name, provider, or capability
const llamas = await client.searchModels('llama') // returns Model[]
const anthropic = await client.searchModels('anthropic')

// List active operators with stats
const { operators, stats } = await client.operators()
// returns { operators: Operator[], stats: any }
// Operator: { id, slug, name, status, endpointUrl, reputationScore, avgLatencyMs, models, ... }
console.log(`${stats.activeOperators} operators serving ${stats.totalModels} models`)

// Check credit balance
const credits = await client.credits() // returns CreditBalance
// CreditBalance: { balance: number, transactions: [{ id, amount, type, description, createdAt }] }
console.log(`Balance: $${credits.balance}`)
```

### API Key Management

Create, list, and revoke API keys programmatically:

```ts
// Create a new key
const { key, id } = await client.createKey('my-app')
console.log(key) // sk-tan-... (shown once, store it)

// List all keys
const keys = await client.keys()
keys.forEach(k => console.log(k.name, k.prefix, k.lastUsedAt))

// Revoke a key
await client.revokeKey(id)
```

### Cost Estimation

Preview cost before sending a request:

```ts
const cost = await client.estimateCost({
  model: 'gpt-4o',
  inputTokens: 1000,
  outputTokens: 500,
}) // returns { inputCost: number, outputCost: number, total: number }
console.log(`Estimated: $${cost.total.toFixed(6)}`)
// { inputCost: 0.005, outputCost: 0.0075, total: 0.0125 }
```

### Spending Limits

Prevent runaway costs with per-request and total budget caps:

```ts
const client = new TCloud({
  apiKey: 'sk-tan-...',
  model: 'gpt-4o-mini',
  limits: {
    maxCostPerRequest: 0.01,     // $0.01 max per request
    maxTotalSpend: 1.00,         // $1.00 lifetime budget
    maxRequests: 100,            // hard request cap
    onLimitWarning: (info) => {
      console.warn(`${info.type} at ${info.current}/${info.limit}`)
    },
  },
})

// Requests that would exceed limits are blocked with TCloudError (429)
await client.ask('Hello')

// Check metering
const { totalSpent, requestCount } = client.usage
```

## CLI

### Authentication

```bash
tcloud auth login              # Browser-based device flow
tcloud auth set-key sk-tan-... # Set key directly
tcloud auth status             # Check current auth
```

### Chat

```bash
npx @tangle-network/tcloud chat "Explain zero-knowledge proofs"
npx @tangle-network/tcloud chat -m meta-llama/llama-4-maverick "Hello"
npx @tangle-network/tcloud chat --private "Anonymous request"  # ShieldedCredits mode
npx @tangle-network/tcloud chat                                # Interactive mode
```

### Browse

```bash
tcloud models                  # List available models
tcloud models -s llama         # Search models
tcloud search "Tangle docs" --provider exa --max-results 5
tcloud operators               # List active operators
```

### Credits and Keys

```bash
tcloud credits balance         # Check balance
tcloud credits add 10          # Add credits
tcloud keys create my-app      # Create a new API key
tcloud keys list               # List keys
```

### Wallet Management

For private inference, manage ephemeral wallets:

```bash
tcloud wallet generate                    # Create wallet
tcloud wallet generate -l "research"      # With label
tcloud wallet list                        # List wallets
```

Wallets use BIP-39 mnemonics with BIP-44 derivation. Private keys are encrypted at rest with AES-256-GCM (PBKDF2 210K iterations).

## Configuration

Config stored in `~/.tcloud/config.json`:

```bash
tcloud config --api-url https://router.tangle.tools
tcloud config --model gpt-4o-mini
```

Environment variables:
- `TANGLE_API_KEY` — API key (primary). One key for router + sandbox + all Tangle products.
- `TCLOUD_API_KEY` — Deprecated alias, still honored for backwards compatibility.
- `TANGLE_ROUTER_URL` — Override CLI router URL.
- `TCLOUD_API_URL` — Deprecated CLI router URL alias.

## OpenAI SDK Compatibility

tcloud's API is OpenAI-compatible. You can use the OpenAI SDK directly:

```ts
import OpenAI from 'openai'

const client = new OpenAI({
  apiKey: 'sk-tan-...',
  baseURL: 'https://router.tangle.tools/v1',
})

const completion = await client.chat.completions.create({
  model: 'meta-llama/llama-4-maverick',
  messages: [{ role: 'user', content: 'Hello' }],
})
```

## Vercel AI SDK Compatibility

Use with the Vercel AI SDK's OpenAI provider:

```ts
import { createOpenAI } from '@ai-sdk/openai'
import { generateText } from 'ai'

const tangle = createOpenAI({
  apiKey: 'sk-tan-...',
  baseURL: 'https://router.tangle.tools/v1',
})

const { text } = await generateText({
  model: tangle('meta-llama/llama-4-maverick'),
  prompt: 'What is decentralized AI?',
})
```

## Examples

See the [`examples/`](./examples/) directory — each is a self-contained script:

| # | Example | What it shows |
|---|---------|---------------|
| 01 | [Quick Start](./examples/01-quick-start.ts) | Minimum viable setup |
| 02 | [Model Selection](./examples/02-model-selection.ts) | Three ways to pick a model, search, browse |
| 03 | [Streaming](./examples/03-streaming.ts) | Real-time SSE output, text + chunk modes |
| 04 | [Private Inference](./examples/04-private-inference.ts) | ShieldedCredits, ephemeral wallets, auto-replenish |
| 05 | [Operator Routing](./examples/05-operator-routing.ts) | Prefer operator, strategy, region, list operators |
| 06 | [Cost & Usage](./examples/06-cost-and-usage.ts) | Estimate cost, track tokens, check balance |
| 07 | [API Keys](./examples/07-api-keys.ts) | Create, list, revoke keys programmatically |
| 08 | [OpenAI SDK](./examples/08-openai-compat.ts) | Drop-in replacement via baseURL |
| 09 | [Vercel AI SDK](./examples/09-vercel-ai-sdk.ts) | generateText + streamText with Tangle |
| 10 | [Spending Limits](./examples/10-spending-limits.ts) | Budget caps, request limits, warning callbacks |
| 11 | [Inference Strategies](./examples/11-inference-strategies.ts) | Min-exposure + operator rotation patterns |
| 12 | [Bridge Sessions](./examples/12-bridge-sessions.ts) | Persistent bridge sessions for harness routing |
| 13 | [Bridge Chat App](./examples/13-bridge-chat-app.ts) | Multi-turn bridge application |
| 14 | [Direct cli-bridge](./examples/14-direct-cli-bridge.ts) | Local cli-bridge, no router |
| 15 | [Sandbox Agents](./examples/15-sandbox-agents.ts) | Sandbox agents + inline AgentProfile |
| 16 | [Tangle Intelligence Hook](./examples/16-tangle-intelligence-hook.ts) | Trace inference into hosted intelligence (one OTLP block) |

Run any example:
```bash
TANGLE_API_KEY=sk-tan-... npx tsx examples/01-quick-start.ts
```

## License

[Apache-2.0](./LICENSE)
