# Channels Source: https://docs.voight.xyz/agents/channels Where your agent shows up — a built-in web chat and API, plus Telegram, GitHub and LinkedIn you connect in a click. X is days away. A channel is a place your agent works. Every agent ships with two built in; three more you connect in a click; X is days away. ## Built in A live chat with your agent right in the dashboard — the fastest way to talk to it and watch it work. Every agent is queryable programmatically, so you can wire it into your own product or workflow. ## Connect in a click Create a bot with [@BotFather](https://t.me/BotFather), paste its token, and your agent starts replying in your Telegram chats. The token is stored in Secret Manager — never in our database. Install the Voight GitHub App on a repo and your agent can read issues and open pull requests. Connect or disconnect per agent, any time. One-click OAuth from **Settings → Channels**. Your agent drafts posts in your voice and publishes on your behalf once you approve. Great with the [Social template](/agents/templates#social). ## Launching next Post and reply on X. Built and in final rollout: launching in the coming days. One agent, many channels. Connecting Telegram, GitHub or LinkedIn doesn't spin up a new agent — it's the *same* agent, with the same memory and persona, showing up in another place. ## Next * [Quickstart](/agents/quickstart#step-5-put-it-to-work) — connect Telegram and GitHub step by step * [What are Voight Agents?](/agents/overview) — the concept # Frameworks Source: https://docs.voight.xyz/agents/frameworks The runtime engine your agent runs on. Hermes is live today, ZeroClaw is in preview; OpenClaw and ElizaOS are next. A **framework** is the engine your agent runs on — the loop that turns the model's decisions into real tool calls, memory, and channel activity. You pick one when you deploy. Voight hosts and runs it for you on private infrastructure; you never manage a server. ## Available now **NousResearch · recommended.** The full agentic loop — tool use, persistent memory and built-in web search. Every Hermes agent runs on its own **private Cloud Run** service, isolated per agent, and scales to zero when idle so it stays cheap to keep online. Or on a dedicated [Nosana GPU](/agents/gpu-hosting), running its model locally. Hermes is the default. It's a mature, tool-calling runtime — the model reasons, calls tools, reads and writes its memory, and acts across your connected channels. ## In preview **Rust · OpenClaw-inspired, ultra-light.** A runtime that idles at \~19 MiB of RAM, with streaming chat, server-side session threads, and a text-format tool parser that works even with models that lack native tool calling. ZeroClaw agents run **GLM 4.6 through the platform** on the same private, per-agent Cloud Run isolation as Hermes, and speak every core channel — dashboard chat (streamed), Telegram, and scheduled tasks. The runtime is version-pinned to an audited upstream release. ZeroClaw is fully wired end to end and rolling out: the tile already shows in the deploy flow and opens to everyone shortly. In this first version it runs on the platform's inference (no BYOK yet) and on Voight Cloud only (no GPU hosting variant). ## Coming soon These appear in the deploy flow and can be selected, but don't provision a live agent yet — they're on the roadmap. A **code-first agent runtime** — geared toward agents that live in a codebase and ship work. On the way. A **multi-agent OS** — for orchestrating fleets of agents that coordinate. On the way. More frameworks are on the way. If you pick one that isn't live yet, your choice is saved, but the agent won't run until that framework ships — deploy on **Hermes** for a working agent today. ## Why it's a choice Different frameworks suit different jobs: a single autonomous worker (Hermes), an ultra-light runtime that squeezes the most out of every megabyte (ZeroClaw), an agent that ships code (OpenClaw), or a coordinated fleet (ElizaOS). Making the framework a first-class choice at deploy time means your agent isn't locked to one philosophy — as the lineup grows, you pick the engine that fits the work. ## Next * [Models](/agents/models) — the LLM that does the thinking inside the framework * [Channels](/agents/channels) — where the running agent shows up * [Quickstart](/agents/quickstart) — deploy on Hermes now # GPU hosting on Nosana Source: https://docs.voight.xyz/agents/gpu-hosting Run your agent on a dedicated GPU from Nosana's decentralized network, with a local model and no inference key anywhere: verifiable on-chain, woken on demand, renewed while in use. When you deploy an agent you choose **where it runs**. The default is **Voight Cloud**: private, scale-to-zero infrastructure with sponsored models. The alternative is **Nosana GPU**: a dedicated GPU rented from [Nosana](https://nosana.com)'s decentralized network, running a model **inside the job itself**. One agent, one GPU job. Nobody shares it with you. The model runs locally on that GPU. There is no inference API key in the container to leak. Every GPU job is a real Solana account. The Overview links straight to it on Nosana's explorer. ## Deploying on a GPU In the deploy flow, the **Hosting** step offers the two hosts. Pick **Nosana GPU** and a GPU market: | Market | Status | | - | - | | NVIDIA RTX 3060 | Live | | NVIDIA RTX 3090 | Coming soon | | NVIDIA RTX 4090 | Coming soon | A GPU agent takes **a few minutes** to come up (the node claims the job, pulls the image, and loads the model) instead of the \~60 seconds of Voight Cloud. You'll see **Deploying** until the agent's gateway answers, then **Live**. Everything else works the same: chat, Telegram, GitHub, scheduled tasks, memory. The model is fixed per host. GPU agents run **Hermes 3 8B** locally (see [Models](/agents/models)); the sponsored GLM / MiMo lineup belongs to Voight Cloud. ## The hourly lifecycle GPU time on Nosana is rented in **hour blocks**. Rather than hide that, the platform treats the hour as a first-class part of the agent's life: An agent that has been active recently gets its job **extended before the hour lapses**. Same job, same GPU, no restart: a conversation simply continues across the boundary. If nobody used the agent for a while, the job ends at its hour and the Overview shows **GPU stopped**. Your agent is not gone: its identity, channels, and memory stay; only the GPU is released. Press **Wake up** on the Overview, or just **message it** (in the dashboard or on Telegram): the message starts the GPU back up. Scheduled tasks wake their agent too, and retry once it's live. A wake takes a few minutes, like a deploy. Automatic renewals are capped per day so a forgotten agent can't run forever; you can always wake it yourself. ## Memory across GPU sessions Each GPU job is ephemeral, but your agent's **curated memory is not**: it is restored when the GPU comes up and saved back periodically and at shutdown. An agent that stops and wakes picks up where it left off. The storage layout is the same one Voight Cloud agents use, so memory is portable across hosts. ## Model and privacy GPU agents run **Hermes 3 8B**, NousResearch's fine-tune of Llama 3.1 8B trained for the Hermes runtime's tool-calling format, served locally on the job's GPU with a 64K context window. Because inference happens on the GPU you rented, **no model provider ever sees your conversations**, and no inference key exists in the container. Two honest caveats: * **The node operator can inspect the container.** Nosana is a decentralized network: the machine running your job belongs to someone else. Treat the agent's memory as visible to that host. For sensitive work, prefer Voight Cloud. * **8B is not a frontier model.** It is fast, private, and capable for everyday tasks and web research. For heavy reasoning, Voight Cloud's GLM lineup is the stronger pick. What stays protected regardless: the keys inside the container are **derived per agent and revocable**, so a key lifted from one job never opens another agent's gateway or memory. ## Verify it yourself The agent's Overview shows **Nosana GPU** as its runtime, linked to the **current on-chain job** on [dashboard.nosana.com](https://dashboard.nosana.com): market, node, and timings, verifiable by anyone. The container image and deployment tooling are open source at [github.com/Voightxyz/nosana-agents](https://github.com/Voightxyz/nosana-agents), with a build log of every live measurement. ## Cost A GPU agent costs the same **\$15 per 30 days** as any agent (see [Pricing](/agents/pricing)). GPU time is **currently included**; per-hour metering is planned and we'll announce the rate before it turns on. You can also top up your balance in **NOS**, Nosana's token, alongside USDC and SOL. ## Next * [Quickstart](/agents/quickstart): deploy an agent and pick its host * [Models](/agents/models): what runs where * [Nosana integration](/integrations/nosana): observing agents that run on Nosana, the other direction # Models Source: https://docs.voight.xyz/agents/models The LLM behind your agent. GLM 4.6 by default, the new GLM 5.3 and 5.3 Flash, the MiMo family, and bring-your-own-key via OpenRouter or direct provider keys. The **model** is the brain doing the thinking inside your [framework](/agents/frameworks). You pick one at deploy time and can change it later. Agents need **tool-calling**. A model that can't call tools can't run the agent loop, so the picker only offers models verified to work end to end. ## The lineup Inference is routed through **Voight's own key** — you don't bring one. Two families are available: The GLM lineup, 4.5 through **5.3 and 5.3 Flash**. Strong general reasoning and tool use. Sponsored by Z.ai. Fast, inexpensive, tool-capable, with a very large context window. The **default is GLM 4.6**: balanced, proven in production, and strong at tool use. MiMo stays available as a low-cost option. **New: GLM 5.3 and GLM 5.3 Flash.** The newest Z.ai flagship and its fast variant, both with a 1M-token context window and full tool-calling. GLM 5.3 is the strongest reasoner in the lineup; 5.3 Flash is fast and inexpensive. Both are available everywhere a model can be picked: on Voight's key, on your own OpenRouter key, or on your own Z.ai key. Every new account starts with a **welcome inference credit**, and usage past it draws from your prepaid credit balance: you always see what a model costs before you pick it. ## On a Nosana GPU: local, private Agents hosted on a [Nosana GPU](/agents/gpu-hosting) don't use the sponsored lineup. Hermes agents run **Hermes 3 8B** (NousResearch's Llama 3.1 8B fine-tune, trained for the Hermes runtime's tool-calling format) and ZeroClaw agents run **Qwen2.5 7B Instruct**, in both cases **locally on the job's own GPU**. Inference never leaves the GPU and no provider key exists in the container. It is the private, verifiable option; for heavy reasoning, the GLM lineup on Voight Cloud is stronger. ## Bring your own key Open the model choice wide, with inference billed to your own provider account. Both modes live in the agent's Settings: * **BYOK · OpenRouter** — paste your OpenRouter key and pick *any* model in the OpenRouter catalog, GLM 5.3 and 5.3 Flash included. * **BYOK · Direct** — connect a provider key directly: **OpenAI, Anthropic, Gemini, Qwen, Z.ai, or Xiaomi**. Calls go straight to the vendor (GLM 5.3 and 5.3 Flash via your Z.ai key). ## Choosing | You want… | Pick | | - | - | | The proven, balanced default | **GLM 4.6** (default) | | The strongest reasoning, 1M context | **GLM 5.3** | | Fast and cheap with 1M context | **GLM 5.3 Flash** | | A low-cost alternative family | **MiMo v2.5** | | Inference that never leaves a GPU you rent | **Nosana GPU** hosting (Hermes 3 8B, local) | | A specific frontier model | Wait for **BYOK**, or tell us what you need | ## Next * [Frameworks](/agents/frameworks) — the engine the model runs inside * [Channels](/agents/channels) — where the agent works * [Pricing](/agents/pricing) — what a running agent costs # What are Voight Agents? Source: https://docs.voight.xyz/agents/overview Deployable, always-on worker agents you own — one click to live, working across chat, GitHub and an API around the clock. **Voight Agents** are autonomous worker agents you deploy in one click and *own*. Give one a role, pick its brains, and it runs 24/7 — answering on Telegram, acting on GitHub, chatting on the web, and reachable over an API — without a server to babysit. This is a separate product from the [observability platform](/introduction), which is what Voight is: the platform **watches** the AI apps and agents *you* build. **Voight Agents** exists for when you don't have agents yet: Voight **hosts and runs them for you**, and you can point the platform at your Voight Agent to observe it like any other. Voight Agents is built for the web3 market: each agent gets an on-chain identity on Solana and can run on decentralized GPU. ## The idea Your next hire doesn't sleep. It sells, supports and gets work done, around the clock — and the agent (its identity, memory and channels) belongs to your account, not a rented seat. * **One click, \~60 seconds to live.** No infra, no Docker, no keys to wire. Pick a name, a framework, a model and a role — deploy. * **Always on.** Each agent runs on isolated, private cloud infrastructure (or a dedicated [Nosana GPU](/agents/gpu-hosting) with a local model) and keeps its own persistent memory between runs. * **Multi-channel.** The same agent replies on Telegram, works a GitHub repo, chats in the dashboard, and answers over an API. * **You own it.** Rename it, re-brief it, connect or disconnect channels, pause or delete — it's yours. ## How it fits together | Piece | What it is | Page | | - | - | - | | **Framework** | The runtime engine your agent runs on (Hermes today, ZeroClaw in preview) | [Frameworks](/agents/frameworks) | | **Model** | The LLM that does the thinking — GLM (up to 5.3 and 5.3 Flash), MiMo, or bring your own key | [Models](/agents/models) | | **Channels** | Where the agent shows up — web, API, Telegram, GitHub, LinkedIn | [Channels](/agents/channels) | | **Template** | A starting persona + toolset (General, Sales, Social, Prediction Markets) | [Templates](/agents/templates) | | **Hosting** | Where it runs: Voight Cloud, or a dedicated Nosana GPU with a local model | [GPU hosting](/agents/gpu-hosting) | | **Memory** | Per-agent, persistent Markdown memory that survives restarts | — | ## What one agent can do today * **Reason and act with tools** — it decides what to do and uses its tools instead of guessing. * **Search the web** — built in, no setup. * **Remember** — persistent memory per agent, carried across conversations and runs. * **Work channels** — reply on Telegram, open PRs and review issues on GitHub, publish on LinkedIn with your approval, chat in the dashboard, or be driven over the API. * **Run on a schedule** — recurring tasks so it keeps working while you're away. ## Deploy one Sign in with a wallet or email and deploy your first agent. **The first 130 agents are free** — one per person. ## Next * [Quickstart](/agents/quickstart) — deploy your first agent in four steps * [Templates](/agents/templates) — pick a lane: General, Sales, Social, or Prediction Markets * [Frameworks](/agents/frameworks) — Hermes today, ZeroClaw in preview; OpenClaw and ElizaOS next * [Models](/agents/models) — the sponsored lineup and bring-your-own-key * [GPU hosting](/agents/gpu-hosting): a dedicated Nosana GPU with a local model, verifiable on-chain * [Channels](/agents/channels) — connect Telegram, GitHub and LinkedIn * [Pricing](/agents/pricing) — \$15/mo per agent, first 130 free * [Deploy agents from Claude Code with MCP](https://voight.xyz/guides/deploy-ai-agents-with-mcp): create, chat with and schedule agents without leaving your editor # Pricing Source: https://docs.voight.xyz/agents/pricing Simple per-agent pricing. The first 130 agents are free at launch; after that it's $15/mo per agent, paid in crypto. One price per agent. No seats, no per-message metering at launch — you pay to deploy an agent and to keep it running. **\$0** at launch * One free agent per person * Full framework + channels * First in, first served **\$15 / month** * Deploy + monthly renewal * Runs 24/7 on private infra * Paid in crypto (no card) * Agent registry included ## How it works * **Deploy** an agent — the first 130 (one per person) are free during launch; after that a deploy costs **\$15**. * **Renewal** — a running agent renews monthly at **\$15** to stay live. Let it lapse and it's paused, not deleted. * **Pay in crypto** — top up in **USDC, SOL, or NOS** (Nosana's token) from your account. Email sign-ups get an embedded wallet, so there's no card and no external checkout. * **GPU hosting**: an agent on a [Nosana GPU](/agents/gpu-hosting) costs the same \$15 per 30 days. GPU time is currently included; per-hour metering is planned and the rate will be announced before it turns on. * **One free grant per person** — a person can claim exactly one free agent, whether through the public pool or a community code. Everything after that is paid. ## Scaling (coming soon) Multi-agent plans that drop the per-agent price as you add agents are on the roadmap: **\$40 / mo** · 3 agents — coming soon **\$65 / mo** · 5 agents — coming soon ## Community claims Partner communities receive a batch of reserved free agents. If you're part of one, you'll get a claim link from your community — redeem it and your first agent is free (subject to the one-free-per-person rule). ## Next * [Quickstart](/agents/quickstart) — deploy your first agent * [Frameworks](/agents/frameworks) · [Models](/agents/models) · [Channels](/agents/channels) # Deploy your first agent Source: https://docs.voight.xyz/agents/quickstart From sign-up to a live, always-on agent in about a minute — four steps, no infrastructure. Everything happens in the agents app at [agent.voight.xyz](https://agent.voight.xyz). No CLI, no server, no keys to wire. ## Step 1 — Register Sign in with a **Solana wallet or an email**. An embedded wallet is created for you if you use email, so you can pay for renewals in crypto later without leaving the app. No setup required. ## Step 2 — Choose a framework The engine your agent runs on. **Hermes** is live and recommended; more are on the way — see [Frameworks](/agents/frameworks). Recommended · live. Tools, memory, web search on private Cloud Run. Code-first runtime · coming soon. Multi-agent OS · coming soon. ## Step 3 — Customize Give the agent an identity and pick its brain: * **Name** — becomes its handle (e.g. `aria` → `agent.voight.xyz/aria`). * **Template** — a starting focus: **General**, **Sales**, **Social**, or **Prediction Markets**. It seeds the persona *and* the agent's toolset; see [Templates](/agents/templates). * **Model** — the LLM that does the thinking. The default is **GLM 4.6** (balanced, proven, tool-capable). Full list on [Models](/agents/models). * **Role & tone** — a one-line description of the job and a tone (Friendly / Professional / Direct / Sharp), folded into the agent's persona. * **Hosting**: **Voight Cloud** (default, live in about a minute) or **Nosana GPU**: a dedicated GPU running a local model, a few minutes to come up, verifiable on-chain. See [GPU hosting](/agents/gpu-hosting). You can also paste a full custom persona instead of using a template — anything up to \~8,000 characters becomes the agent's brief. ## Step 4 — Deploy One click. Your agent is provisioned on its own private runtime and goes live in **about 60 seconds** (a few minutes on a Nosana GPU, while the node loads the model). You'll land in its workspace with a live chat, its status, and its channels. ## Step 5 — Put it to work * **Chat** with it right there in the dashboard. * **Connect Telegram** — paste a bot token from [@BotFather](https://t.me/BotFather) and it starts replying in your chats. See [Channels](/agents/channels). * **Connect GitHub** — install the Voight GitHub App on a repo and it can read issues and open PRs. * **Connect LinkedIn** — one-click OAuth in Settings → Channels; it drafts posts and, once you approve, publishes on your behalf. * **Reach it over the API** — every agent is queryable programmatically. * **Schedule tasks** — set recurring work so it keeps going 24/7. ## Cost The **first 130 agents are free** (one per person, at launch). After that it's **\$15/mo per agent** — deploy + renewal, paid in crypto. Full breakdown on [Pricing](/agents/pricing). ## Next * [Templates](/agents/templates) · [Frameworks](/agents/frameworks) · [Models](/agents/models) · [Channels](/agents/channels) * [What are Voight Agents?](/agents/overview) — the concept and what one agent can do # Templates Source: https://docs.voight.xyz/agents/templates The starting persona and toolset for your agent: General, Sales, Social, or Prediction Markets. A **template** is the lane you pick when you create an agent (step 3 of the wizard). One choice seeds two things at once: * **The persona.** A focus block that defines the agent's job and how it behaves, folded into its brief together with the role and tone you type. * **The toolset.** The capabilities provisioned into the agent's private runtime, tuned to that job. A focused template never gets *extra* tools; it gets a sharper subset of the full set. The template is a **create-time choice**: it is baked into the agent when it's provisioned, and an existing agent can't be switched to a different template later. To change lanes, deploy a new agent. (The persona *text* itself stays editable in Settings at any time.) ## The four templates The default. No lane: the full capability set and a neutral brief. Best when the job is unique, or when you plan to write your own persona. Prospects, qualifies, and reaches out. Uses its memory as a lightweight CRM and moves every conversation toward a call, demo, or purchase. Plans, writes, and publishes content in your voice, and keeps up with mentions across your connected channels. A prediction-markets analyst. Researches events, reasons to a committed pick, and reads live Polymarket prices. ## Capabilities by template Every template ships with web search, files and a terminal, persistent memory, skills, and scheduled tasks. The differences: | Capability | General | Sales | Social | Prediction | | - | :-: | :-: | :-: | :-: | | Web search & research | ✓ | ✓ | ✓ | ✓ | | Persistent memory | ✓ | ✓ | ✓ | ✓ | | Scheduled tasks | ✓ | ✓ | ✓ | ✓ | | Vision (understands images) | ✓ | ✗ | ✓ | ✗ | | Browse the skill hub | ✓ | ✗ | ✗ | ✗ | Focused templates deliberately drop what their job doesn't need: a sales agent has no reason to browse the skill hub, and vision stays where images actually matter (social content, and the general all-rounder). ## Sales Built to fill a pipeline, not to spam one. * **Prospects and qualifies.** Researches a lead or company before reaching out and notes *why* they fit. * **Reaches out for real.** With LinkedIn [connected](/agents/channels) (X is days away), it drafts personalized outreach and follow-ups and sends them once you approve. Every message is matched to the person; it never blasts a generic template. * **Memory as CRM.** Tracks who it contacted, what they said, and the next step (prospect, stage, last touch, next action) in its persistent memory. * **Follow-up cadences.** Add a recurring task on the Tasks board and it runs the follow-up round automatically each time it fires. Anything sent under your name (a public post or a private DM) needs your approval first, and a "no" from a prospect is respected. ## Social A social media manager that keeps you present without sounding like a bot wrote it. * **Creates content.** Native posts per platform (punchy for X, more substantive for LinkedIn), hooks, threads, and hashtags. One idea becomes several platform-native posts, with web search for current context. * **Publishes.** With LinkedIn connected (X is days away), it posts once you approve; drafts always land with you first. Put publishing on a calendar with a recurring task. * **Engages.** Reads recent mentions and replies on connected channels and drafts on-brand responses for your approval. * **Stays consistent.** Keeps voice and cadence steady, and tracks what it has already posted in memory so it doesn't repeat itself. * **Sees images.** Vision is included, since visuals are core to social content. ## Prediction Markets An analyst identity, not a general assistant with market data bolted on. It thinks in odds, edges, and catalysts. * **Research first.** Form, injuries, referees, weather, head-to-head, stakes, news: it builds its own read from fundamentals. Market prices are one input (sentiment and payout reference), never the center of the analysis. * **Commits to a pick.** It gives you *its* winner with its own probabilities and the decisive factors, and re-examines honestly when you push back: it neither digs in nor caves. * **Reads live markets.** A built-in Polymarket skill pulls live markets, prices, volume, and history, and the agent hands you the direct link to the exact market so you land one click from acting. * **Odds cards in chat.** Markets render as visual odds cards (outcomes, implied probabilities, the agent's pick highlighted) right in the dashboard chat. * **Monitors over time.** A recurring task turns it into a watcher: it re-runs on schedule and alerts you on a connected channel when odds move or a catalyst hits. Prediction agents give **research, not financial advice**. They never take custody of funds, sign transactions, or place positions. The agent surfaces the opportunity; you decide and execute. ## Templates and your own brief A template is a seed, not a cage: * The **role and tone** you type in the wizard are folded into the template's brief. * Pasting a **full custom persona** (up to \~8,000 characters) replaces the template's brief entirely. The template still decides the toolset. * After deploy, the persona stays editable in the agent's Settings. ## Choosing | You want… | Pick | | - | - | | An all-rounder, or a fully custom persona | **General** (default) | | Outreach, qualification, and follow-ups | **Sales** | | Content, publishing, and engagement | **Social** | | Odds, edges, and committed picks | **Prediction Markets** | ## Next * [Models](/agents/models): the brain that runs the template * [Channels](/agents/channels): connect Telegram, GitHub and LinkedIn (X is days away) * [Quickstart](/agents/quickstart): deploy one in about a minute # Anthropic SDK Source: https://docs.voight.xyz/ai-apps/anthropic Wrap your Anthropic client and capture every Messages call without changing app code. `@voightxyz/anthropic` instruments the official Anthropic Node SDK. Wrap your client once and every `messages.create` call — non-streaming or streaming — lands in Voight with prompts, tokens, cache reads, cache creations, tool use, latency, and errors. Same backend, same dashboard as [`@voightxyz/openai`](/ai-apps/openai) and [library mode](/sdk/library-mode) — events from all three land side-by-side under the same agent. ## Quick setup (recommended) From the root of your app: ```bash theme={null} npx -y @voightxyz/sdk init ``` The [wizard](/ai-apps/wizard) detects `@anthropic-ai/sdk` in your `package.json`, prompts for your Voight key + privacy level, validates the key, and writes a ready-to-import `src/lib/voight.ts` with the wrapped client. 30 seconds, zero copy-paste. Continue below if you'd rather wire it manually. ## Install ```bash theme={null} npm install @anthropic-ai/sdk @voightxyz/anthropic ``` Requirements: * Node.js 18+ (uses global `fetch`) * `@anthropic-ai/sdk` 0.30.0+ ## Quick start ```ts theme={null} import Anthropic from '@anthropic-ai/sdk' import { wrapAnthropic } from '@voightxyz/anthropic' const client = wrapAnthropic(new Anthropic(), { voightApiKey: process.env.VOIGHT_KEY, agent: 'my-prod-agent', }) const response = await client.messages.create({ model: 'claude-haiku-4-5', max_tokens: 1024, messages: [{ role: 'user', content: 'Hello' }], }) ``` That's it. Every call is captured automatically. Visit your [dashboard](https://voight.xyz/dashboard/ai-apps) to see them in the **AI Apps** section. ## Tracing & per-user tags For production apps, wrap each request boundary with `withTrace` to group every LLM call inside one request into one trace, and to attribute cost per end-user with one line of code: ```ts theme={null} import Anthropic from '@anthropic-ai/sdk' import { wrapAnthropic, withTrace, log } from '@voightxyz/anthropic' const anthropic = wrapAnthropic(new Anthropic(), { agent: 'production-chat-api' }) app.post('/api/chat', async (req, res) => { await withTrace( async () => { log('chat request received') const reply = await anthropic.messages.create({ model: 'claude-haiku-4-5', max_tokens: 1024, messages: [{ role: 'user', content: req.body.prompt }], }) res.json({ reply }) }, { routeTag: 'POST /api/chat', tags: { userId: req.user.id, plan: req.user.plan }, }, ) }) ``` Every wrapped LLM call inside the `withTrace` block gets stamped with `metadata.tags = { userId, plan, ... }` automatically. The dashboard's [AI Apps section](/ai-apps/overview) then surfaces: * **Users sub-tab** — per-user spend, traces, tokens (driven by `tags.userId`) * **Trace cards** — every `withTrace` block as one drillable card with cost, latency, and event timeline * **User filter pill** — narrow Overview / Models / Tools to one user Full surface in [Tracing](/ai-apps/tracing). The per-user pattern in depth — with examples for Auth0, Clerk, NextAuth, custom JWT, and anonymous flows — lives at [per-user spend](/concepts/per-user-spend). ## Options | Option | Type | Default | Purpose | | - | - | - | - | | `voightApiKey` | string | env `VOIGHT_KEY` | Your Voight key from the dashboard | | `agent` | string | env `VOIGHT_AGENT` → `HOSTNAME` → `'unknown-agent'` | Stable identifier surfaced in the dashboard | | `apiBase` | string | `https://api.voight.xyz` | Override for self-hosted deployments | | `privacy` | `'minimal' \| 'standard' \| 'full'` | `'standard'` | Capture aggressiveness | | `sessionId` | string | auto UUID v4 | Trace grouping. Stable across calls of one wrapper instance — events sharing a `sessionId` render as a single trace in the dashboard | | `enabled` | boolean | `true` | Kill switch — when false, returns the original client untouched | | `otel` | boolean | `false` | Emit captured calls as OpenTelemetry spans alongside the direct ingest. See [OpenTelemetry side-channel](#opentelemetry-side-channel) below. | A missing or empty API key is non-fatal: the wrapper prints a one-line warning and returns the original client. Production keeps running. ## What's captured | Signal | Field on the event | | - | - | | Model id (with version suffix, e.g. `claude-haiku-4-5-20251001`) | `model` | | Prompt messages | `input.messages` | | Response text (aggregated from `content[].text` blocks) | `metadata.responseText` | | Token counts (input / output / total) | `metadata.tokens` | | Cache reads (`cache_read_input_tokens`) | `metadata.tokens.cache_read` | | Cache creations (`cache_creation_input_tokens`) | `metadata.tokens.cache_creation` | | Tool use (full array) | `metadata.toolCalls` | | First tool's name (audit-log compat) | `toolExecuted` | | Streaming flag | `metadata.streaming` | | Stop reason | `metadata.finishReason` | | Trace grouping | `metadata.sessionId` | | Trace ID (when inside `withTrace`) | `metadata.traceId` | | Route tag (when inside `withTrace`) | `metadata.routeTag` | | User / plan / org tags (when inside `withTrace`) | `metadata.tags` | | Capture level used | `metadata.privacyLevel` | | Latency in milliseconds | `durationMs` | | Errors (re-thrown to the caller, recorded with `outcome: 'failed'`) | `errorMessage`, `outcome` | ## Streaming Streaming works without setup. Anthropic emits a typed event sequence (`message_start`, `content_block_start`, `content_block_delta`, `content_block_stop`, `message_delta`, `message_stop`); the wrapper's state machine reacts to the events that drive capture and passes the rest through unchanged. ```ts theme={null} const stream = await client.messages.create({ model: 'claude-haiku-4-5', max_tokens: 256, stream: true, messages: [{ role: 'user', content: 'count to five' }], }) for await (const event of stream) { if ( event.type === 'content_block_delta' && event.delta.type === 'text_delta' ) { process.stdout.write(event.delta.text) } } ``` Initial usage from `message_start` carries `input_tokens` plus the two cache fields. Final `output_tokens` lands on `message_delta` and merges into the emitted event so the full breakdown is captured. ## Tool use ```ts theme={null} const response = await client.messages.create({ model: 'claude-haiku-4-5', max_tokens: 256, tools: [{ name: 'get_weather', description: 'Get weather for a city', input_schema: { type: 'object', properties: { location: { type: 'string' } }, required: ['location'], }, }], messages: [{ role: 'user', content: "what's the weather in Tokyo?" }], }) ``` Tool calls live as `tool_use` blocks inside `response.content[]`. The wrapper flattens them into the same `{ id, name, arguments }` shape `@voightxyz/openai` produces — dashboards render tool calls from either provider identically. Anthropic's tool input is a parsed object on the wire; we JSON-stringify it so the captured `arguments` field is a string (matching the openai package). On the captured event: * `toolExecuted: 'get_weather'` — first tool's name * `metadata.toolCalls: [{ id, name, arguments }]` — full array Streaming tool use works the same — `input_json_delta.partial_json` fragments across `content_block_delta` events are concatenated per `index`. ## Path-A cache pricing Anthropic distinguishes two cache operations: * **Cache creation** (`cache_creation_input_tokens`) — writing context to the cache. Billed at 1.25× input rate. * **Cache read** (`cache_read_input_tokens`) — reading cached context on subsequent turns. Billed at 0.10× input rate. Both fields are surfaced on `metadata.tokens` only when strictly positive. The Voight backend pricing engine applies the multipliers automatically — your spend reflects the real cost, not a flat-rate over-estimate. ## OpenTelemetry side-channel By default the wrapper POSTs each captured call directly to `api.voight.xyz`. Set `otel: true` to **additionally** emit each call as an OpenTelemetry span — useful when the host process already runs an OTel pipeline (Langfuse, Phoenix, Datadog, Sentry, or [`@voightxyz/vercel-ai`](/ai-apps/vercel-ai)) and you want Voight events to appear there too. ```ts theme={null} const client = wrapAnthropic(new Anthropic(), { agent: 'my-app', otel: true }) ``` Each span is named `voight.anthropic.messages` and carries the standard `gen_ai.*` semantic-convention attributes (`gen_ai.system` resolves to `'anthropic'`, plus `gen_ai.request.model`, `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`, `gen_ai.usage.cache_read_input_tokens`, `gen_ai.response.finish_reasons`) plus the parallel Vercel-style `ai.*` namespace. The direct ingest path is unchanged — `otel: true` is purely additive. ### Dedup marker Every emitted span carries `voight.source: 'wrapper'`. If you also use [`@voightxyz/vercel-ai`](/ai-apps/vercel-ai) ≥ 0.1.1 in the same process, that exporter skips wrapper-emitted spans automatically — no duplicate events in your dashboard. ### Optional peer dependency `@opentelemetry/api` is now an optional peer dependency. If you never set `otel: true`, nothing changes. If you set `otel: true` but the package isn't installed, the wrapper logs a single warning and falls back to direct ingest only. ## Privacy Three levels apply to prompts, response text, and tool-call arguments. The function name in `toolExecuted` is treated as a tag (not user content) and survives all levels. | Level | Prompts | Response text | Tool arguments | `toolExecuted` (name) | | - | - | - | - | - | | `minimal` | dropped | dropped | dropped | kept | | `standard` | scrubbed | scrubbed | scrubbed | kept | | `full` | verbatim | verbatim | verbatim | kept | Standard scrubs 12 patterns: PEM private keys, JWTs, Anthropic / OpenAI / Stripe live / GitHub / AWS / Slack / Voight API keys, emails, E.164 phones, and Luhn-validated credit cards. See [PII patterns](/privacy/pii-patterns) for the full catalogue. ## How it compares | Use case | Reach for | | - | - | | Coding agent (Claude Code, Cursor, Codex) capturing your dev sessions | [Hooks-based SDK](/quickstart) | | Autonomous TS/JS bot you wrote yourself emitting custom events | [Library mode](/sdk/library-mode) | | Production app calling Anthropic in user-facing flows | This package | | Production app calling OpenAI | [`@voightxyz/openai`](/ai-apps/openai) | | Per-user / per-tenant cost attribution in any of the above | [Per-user spend](/concepts/per-user-spend) | | Anything else (Python, Go, Rust) | [HTTP API](/sdk/http-api) | The packages coexist — wrap your Anthropic client AND call `voight.log()` for your own domain events under the same agent. Adding [`withTrace`](/ai-apps/tracing) on top groups them all per-request. ## Source * [github.com/Voightxyz/voight-anthropic](https://github.com/Voightxyz/voight-anthropic) * [npmjs.com/package/@voightxyz/anthropic](https://www.npmjs.com/package/@voightxyz/anthropic) ## Roadmap * Bedrock and Vertex Anthropic clients * Batch API (when GA) See the [changelog](/changelog) for shipped releases. # GLM (Z.ai) Source: https://docs.voight.xyz/ai-apps/glm Observability for GLM models from Z.ai (formerly Zhipu AI) — three setup paths, full pricing coverage. [Z.ai](https://z.ai) (rebranded from Zhipu AI in 2025) publishes the GLM model family — GLM-5, GLM-4.7, GLM-4.5, plus vision variants and OCR. Voight ships pricing entries for every model in the [official Z.ai catalogue](https://docs.z.ai/guides/overview/pricing), so events from GLM calls land in your dashboard with the same cost / token / latency capture as any other provider. The Z.ai API at `https://api.z.ai/api/paas/v4` is [officially OpenAI-compatible](https://docs.z.ai), which means the same Voight SDK family that wraps OpenAI / Anthropic also covers GLM — no separate package needed. ## Three setup paths Pick the one that matches how your app calls GLM: | Your stack | Path | | - | - | | Express / vanilla Node calling Z.ai via the OpenAI SDK | [Direct wrapper](#direct-wrapper-voightxyzopenai) | | Next.js / any app using the Vercel AI SDK + `zhipu-ai-provider` | [Vercel AI SDK exporter](#vercel-ai-sdk-voightxyzvercel-ai) | | Autonomous bot / library code emitting events directly | [Library mode](#library-mode-voightxyzsdk) | ## Direct wrapper (`@voightxyz/openai`) Because Z.ai is OpenAI-compatible, [`@voightxyz/openai`](/ai-apps/openai) works zero-code — just point the OpenAI client at Z.ai's base URL: ```ts theme={null} import OpenAI from 'openai' import { wrapOpenAI } from '@voightxyz/openai' const glm = wrapOpenAI( new OpenAI({ baseURL: 'https://api.z.ai/api/paas/v4', apiKey: process.env.ZHIPU_API_KEY, }), { voightApiKey: process.env.VOIGHT_KEY, agent: 'production-chat-api', privacy: 'standard', }, ) const result = await glm.chat.completions.create({ model: 'glm-4.5', messages: [{ role: 'user', content: 'Hello' }], }) ``` Every call captures prompts, tokens, response text, tool calls, finish reason, and latency. The cost is computed from the Voight backend's `MODEL_PRICING` table using the GLM rates listed below. ## Vercel AI SDK (`@voightxyz/vercel-ai`) If your app uses the Vercel AI SDK with the community [`zhipu-ai-provider`](https://github.com/Xiang-CH/zhipu-ai-provider), [`@voightxyz/vercel-ai`](/ai-apps/vercel-ai) captures every `streamText` / `generateText` / `streamObject` / `generateObject` call automatically via OpenTelemetry — same as for OpenAI or Anthropic targets. ```ts filename="instrumentation.ts" theme={null} import { registerOTel } from '@vercel/otel' import { VoightExporter } from '@voightxyz/vercel-ai' export function register() { registerOTel({ serviceName: 'my-app', traceExporter: new VoightExporter({ agent: 'production-chat-api', privacy: 'standard', }), }) } ``` ```ts filename="app/api/chat/route.ts" theme={null} import { streamText } from 'ai' import { zhipu } from 'zhipu-ai-provider' export async function POST(req: Request) { const result = streamText({ model: zhipu('glm-4.7'), prompt: (await req.json()).prompt, experimental_telemetry: { isEnabled: true, metadata: { userId: 'user_123', plan: 'pro' }, }, }) return result.toAIStreamResponse() } ``` The `metadata.userId` lifts onto `metadata.tags.userId` in Voight and powers the dashboard's [per-user spend](/concepts/per-user-spend) sub-tab. ## Library mode (`@voightxyz/sdk`) For autonomous agents or library callers that don't use either of the above SDKs, the library-mode client is provider-agnostic — call GLM however you like and emit a Voight event manually: ```ts theme={null} import { Voight } from '@voightxyz/sdk' const voight = new Voight({ apiKey: process.env.VOIGHT_KEY, agentId: 'my-glm-bot', }) const t0 = Date.now() const res = await fetch('https://api.z.ai/api/paas/v4/chat/completions', { method: 'POST', headers: { 'authorization': `Bearer ${process.env.ZHIPU_API_KEY}`, 'content-type': 'application/json', }, body: JSON.stringify({ model: 'glm-4.5', messages: [{ role: 'user', content: 'Hello' }], }), }) const data = await res.json() voight.log({ type: 'reasoning', model: 'glm-4.5', durationMs: Date.now() - t0, outcome: 'success', metadata: { source: 'custom', provider: 'zhipu', tokens: { input: data.usage.prompt_tokens, output: data.usage.completion_tokens, }, responseText: data.choices[0].message.content, }, }) ``` The cost matcher resolves `glm-4.5` to the table entry below; the family tag `zhipu` keeps the event grouped with other GLM traffic in the dashboard. ## Pricing coverage Verified against the official Z.ai pricing page (USD per 1M tokens). ### Text models | Model | Input | Output | | - | - | - | | GLM-5.1 | \$1.40 | \$4.40 | | GLM-5 | \$1.00 | \$3.20 | | GLM-5-Turbo | \$1.20 | \$4.00 | | GLM-4.7 | \$0.60 | \$2.20 | | GLM-4.7-FlashX | \$0.07 | \$0.40 | | GLM-4.7-Flash | Free | Free | | GLM-4.6 | \$0.60 | \$2.20 | | GLM-4.5 | \$0.60 | \$2.20 | | GLM-4.5-X | \$2.20 | \$8.90 | | GLM-4.5-Air | \$0.20 | \$1.10 | | GLM-4.5-AirX | \$1.10 | \$4.50 | | GLM-4.5-Flash | Free | Free | | GLM-4-32B-0414-128K | \$0.10 | \$0.10 | ### Vision models | Model | Input | Output | | - | - | - | | GLM-5V-Turbo | \$1.20 | \$4.00 | | GLM-4.5V | \$0.60 | \$1.80 | | GLM-4.6V | \$0.30 | \$0.90 | | GLM-4.6V-FlashX | \$0.04 | \$0.40 | | GLM-4.6V-Flash | Free | Free | | GLM-OCR | \$0.03 | \$0.03 | Longest-prefix lookup handles overlapping family names — e.g. an event tagged `model: 'glm-4.5-airx-20251130'` matches `glm-4.5-airx` (not `glm-4.5-air` or `glm-4.5`) and bills at the correct rate. ## Resources * [Z.ai pricing](https://docs.z.ai/guides/overview/pricing) — source of truth for the table above * [Z.ai API reference](https://docs.z.ai) * [`zhipu-ai-provider`](https://github.com/Xiang-CH/zhipu-ai-provider) — community Vercel AI SDK provider * [@voightxyz/openai](https://www.npmjs.com/package/@voightxyz/openai) on npm * [@voightxyz/vercel-ai](https://www.npmjs.com/package/@voightxyz/vercel-ai) on npm # OpenAI SDK Source: https://docs.voight.xyz/ai-apps/openai Wrap your OpenAI client and capture every model call without changing app code. `@voightxyz/openai` instruments the official OpenAI Node SDK. Wrap your client once and every `chat.completions.create` or `responses.create` call — non-streaming or streaming — lands in Voight with prompts, tokens, cache reads, tool calls, latency, and errors. This is the same SDK-instrumentation model Sentry / Vercel AI / LangChain use. Same backend, same dashboard as [`@voightxyz/anthropic`](/ai-apps/anthropic) and [library mode](/sdk/library-mode) — events from all three land side-by-side under the same agent. ## Quick setup (recommended) From the root of your app: ```bash theme={null} npx -y @voightxyz/sdk init ``` The [wizard](/ai-apps/wizard) detects `openai` in your `package.json`, prompts for your Voight key + privacy level, validates the key, and writes a ready-to-import `src/lib/voight.ts` with the wrapped client. 30 seconds, zero copy-paste. Continue below if you'd rather wire it manually. ## Install ```bash theme={null} npm install openai @voightxyz/openai ``` Requirements: * Node.js 18+ (uses global `fetch`) * `openai` SDK 4.0.0+ ## Quick start ```ts theme={null} import OpenAI from 'openai' import { wrapOpenAI } from '@voightxyz/openai' const client = wrapOpenAI(new OpenAI(), { voightApiKey: process.env.VOIGHT_KEY, agent: 'my-prod-agent', }) const response = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'Hello' }], }) ``` That's it. Every call is captured automatically. Visit your [dashboard](https://voight.xyz/dashboard/ai-apps) to see them in the **AI Apps** section. ## Tracing & per-user tags For production apps, wrap each request boundary with `withTrace` to group every LLM call inside one request into one trace, and to attribute cost per end-user with one line of code: ```ts theme={null} import OpenAI from 'openai' import { wrapOpenAI, withTrace, log } from '@voightxyz/openai' const openai = wrapOpenAI(new OpenAI(), { agent: 'production-chat-api' }) app.post('/api/chat', async (req, res) => { await withTrace( async () => { log('chat request received') const reply = await openai.chat.completions.create({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: req.body.prompt }], }) res.json({ reply }) }, { routeTag: 'POST /api/chat', tags: { userId: req.user.id, plan: req.user.plan }, }, ) }) ``` Every wrapped LLM call inside the `withTrace` block gets stamped with `metadata.tags = { userId, plan, ... }` automatically. The dashboard's [AI Apps section](/ai-apps/overview) then surfaces: * **Users sub-tab** — per-user spend, traces, tokens (driven by `tags.userId`) * **Trace cards** — every `withTrace` block as one drillable card with cost, latency, and event timeline * **User filter pill** — narrow Overview / Models / Tools to one user Full surface in [Tracing](/ai-apps/tracing). The per-user pattern in depth — with examples for Auth0, Clerk, NextAuth, custom JWT, and anonymous flows — lives at [per-user spend](/concepts/per-user-spend). ## Options | Option | Type | Default | Purpose | | - | - | - | - | | `voightApiKey` | string | env `VOIGHT_KEY` | Your Voight key from the dashboard | | `agent` | string | env `VOIGHT_AGENT` → `HOSTNAME` → `'unknown-agent'` | Stable identifier surfaced in the dashboard | | `apiBase` | string | `https://api.voight.xyz` | Override for self-hosted deployments | | `privacy` | `'minimal' \| 'standard' \| 'full'` | `'standard'` | Capture aggressiveness | | `sessionId` | string | auto UUID v4 | Trace grouping. Stable across calls of one wrapper instance — events sharing a `sessionId` render as a single trace in the dashboard | | `enabled` | boolean | `true` | Kill switch — when false, returns the original client untouched (zero overhead) | | `otel` | boolean | `false` | Emit captured calls as OpenTelemetry spans alongside the direct ingest. See [OpenTelemetry side-channel](#opentelemetry-side-channel) below. | A missing or empty API key is non-fatal: the wrapper prints a one-line warning and returns the original client. Production keeps running. ## What's captured | Signal | Field on the event | | - | - | | Model id (with version suffix) | `model` | | API surface used (chat completions vs responses) | `metadata.api` (`'responses'` on responses events; omitted on chat) | | Prompt messages (chat) | `input.messages` | | Input payload (responses) | `input.input` | | Response text | `metadata.responseText` | | Token counts (input / output / total) | `metadata.tokens` | | Cache reads (`prompt_tokens_details.cached_tokens`) | `metadata.tokens.cache_read` | | Reasoning tokens (Responses API, o1 / o3 / future reasoning models) | `metadata.tokens.reasoning` | | Tool / function calls (full array) | `metadata.toolCalls` | | First tool's name (audit-log compat) | `toolExecuted` | | Streaming flag | `metadata.streaming` | | Finish reason / response status | `metadata.finishReason` | | Trace grouping | `metadata.sessionId` | | Trace ID (when inside `withTrace`) | `metadata.traceId` | | Route tag (when inside `withTrace`) | `metadata.routeTag` | | User / plan / org tags (when inside `withTrace`) | `metadata.tags` | | Capture level used | `metadata.privacyLevel` | | Latency in milliseconds | `durationMs` | | Errors (re-thrown to the caller, recorded with `outcome: 'failed'`) | `errorMessage`, `outcome` | ## Supported endpoints The wrapper intercepts two paths: * `client.chat.completions.create` — Chat Completions (non-streaming + streaming, tool calling) * `client.responses.create` — Responses API (non-streaming + streaming, function calling, reasoning models) Everything else on the OpenAI client passes through untouched. Embeddings, images, audio, and the Azure OpenAI client are on the 0.2.0 roadmap. ## Chat Completions ```ts theme={null} const response = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'Hi' }], }) ``` Streaming works without setup. The wrapper auto-injects `stream_options.include_usage: true` so the final chunk carries the token count, and a per-index aggregator reassembles tool-call argument fragments. ```ts theme={null} const stream = await client.chat.completions.create({ model: 'gpt-4o-mini', stream: true, messages: [{ role: 'user', content: 'count to five' }], }) for await (const chunk of stream) { process.stdout.write(chunk.choices[0]?.delta?.content ?? '') } ``` An explicit `stream_options: { include_usage: false }` from the caller is preserved — you opt out of token capture for streaming events but everything else is still captured. ## Responses API The Responses API is OpenAI's surface for apps built after Mar 2025: typed output items, stateful conversations via `previous_response_id`, built-in tools, and explicit `reasoning_tokens` for o1 / o3 models. ```ts theme={null} const response = await client.responses.create({ model: 'gpt-4o-mini', input: 'Reply with: pong', }) console.log(response.output_text) ``` Events from this surface carry `metadata.api: 'responses'` so dashboards can distinguish call sites from Chat Completions. Streaming is a typed event sequence (`response.created`, `response.output_text.delta`, `response.output_item.added`, `response.function_call_arguments.delta`, `response.completed`); the wrapper's state machine reacts to the critical events and passes the rest through unchanged. ```ts theme={null} const stream = await client.responses.create({ model: 'gpt-4o-mini', input: 'Count to five', stream: true, }) for await (const event of stream) { if (event.type === 'response.output_text.delta') { process.stdout.write(event.delta) } } ``` For reasoning models, `metadata.tokens.reasoning` separates the "thinking" overhead from the visible answer so cost analysis stays accurate. ## Tool / function calling Works the same for both endpoints: ```ts theme={null} const response = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: "what's the weather in Tokyo?" }], tools: [{ type: 'function', function: { name: 'get_weather', description: 'Get weather for a city', parameters: { type: 'object', properties: { location: { type: 'string' } }, required: ['location'], }, }, }], }) ``` On the captured event: * `toolExecuted: 'get_weather'` — first tool's name. Renders in the audit-log DETAIL column with the same shape as Bash/Edit hook events. * `metadata.toolCalls: [{ id, name, arguments }]` — full array. `arguments` is the raw JSON string the model produced (we don't parse it — invalid JSON is a real failure mode you need to debug). Streaming function calls work the same — fragment deltas across chunks (Chat Completions) or `function_call_arguments.delta` events (Responses) are concatenated per tool index. ## OpenTelemetry side-channel By default the wrapper POSTs each captured call directly to `api.voight.xyz`. Set `otel: true` to **additionally** emit each call as an OpenTelemetry span — useful when the host process already runs an OTel pipeline (Langfuse, Phoenix, Datadog, Sentry, or [`@voightxyz/vercel-ai`](/ai-apps/vercel-ai)) and you want Voight events to appear there too. ```ts theme={null} const client = wrapOpenAI(new OpenAI(), { agent: 'my-app', otel: true }) ``` Each span is named `voight.openai.chat` (or `voight.openai.responses`) and carries the standard `gen_ai.*` semantic-convention attributes (`gen_ai.system`, `gen_ai.request.model`, `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`, `gen_ai.usage.cache_read_input_tokens`, `gen_ai.response.finish_reasons`) plus the parallel Vercel-style `ai.*` namespace. The direct ingest path is unchanged — `otel: true` is purely additive. ### Dedup marker Every emitted span carries `voight.source: 'wrapper'`. If you also use [`@voightxyz/vercel-ai`](/ai-apps/vercel-ai) ≥ 0.1.1 in the same process, that exporter skips wrapper-emitted spans automatically — no duplicate events in your dashboard. ### Optional peer dependency `@opentelemetry/api` is now an optional peer dependency. If you never set `otel: true`, nothing changes. If you set `otel: true` but the package isn't installed, the wrapper logs a single warning and falls back to direct ingest only. ## Privacy Three levels apply to prompts, response text, and tool-call arguments. The function name in `toolExecuted` is treated as a tag (not user content) and survives all levels. | Level | Prompts | Response text | Tool arguments | `toolExecuted` (name) | | - | - | - | - | - | | `minimal` | dropped | dropped | dropped | kept | | `standard` | scrubbed | scrubbed | scrubbed | kept | | `full` | verbatim | verbatim | verbatim | kept | Standard scrubs 12 patterns: PEM private keys, JWTs, Anthropic / OpenAI / Stripe live / GitHub / AWS / Slack / Voight API keys, emails, E.164 phones, and Luhn-validated credit cards. Token counts, model ids, and timing are NEVER scrubbed — they're numeric or tags, no PII risk. See [PII patterns](/privacy/pii-patterns) for the full catalogue. ## How it compares | Use case | Reach for | | - | - | | Coding agent (Claude Code, Cursor, Codex) capturing your dev sessions | [Hooks-based SDK](/quickstart) | | Autonomous TS/JS bot you wrote yourself emitting custom events | [Library mode](/sdk/library-mode) | | Production app calling OpenAI in user-facing flows | This package | | Production app calling Anthropic | [`@voightxyz/anthropic`](/ai-apps/anthropic) | | Per-user / per-tenant cost attribution in any of the above | [Per-user spend](/concepts/per-user-spend) | | Anything else (Python, Go, Rust) | [HTTP API](/sdk/http-api) | The packages coexist — wrap your OpenAI client AND call `voight.log()` for your own domain events under the same agent. Adding [`withTrace`](/ai-apps/tracing) on top groups them all per-request. ## Source * [github.com/Voightxyz/voight-openai](https://github.com/Voightxyz/voight-openai) * [npmjs.com/package/@voightxyz/openai](https://www.npmjs.com/package/@voightxyz/openai) ## Roadmap * Embeddings (`embeddings.create`) * Image generation (`images.generate`) * Audio (Whisper / TTS) * Azure OpenAI client See the [changelog](/changelog) for shipped releases. # AI Apps overview Source: https://docs.voight.xyz/ai-apps/overview The dashboard section for production LLM apps — cost, traces, models, tools, and per-user spend in one place. **AI Apps** is the dedicated dashboard section for production apps that call OpenAI or Anthropic at runtime — your customer-facing copilots, your agentic features, your AI-powered endpoints. It lives at [voight.xyz/dashboard/ai-apps](https://voight.xyz/dashboard/ai-apps) and ships once you wrap your provider client with [`@voightxyz/openai`](/ai-apps/openai) or [`@voightxyz/anthropic`](/ai-apps/anthropic). The section is separate from the coding-agent dashboard on purpose. Your team's Claude Code / Cursor / Codex telemetry and your production LLM app's user-facing telemetry are different operational questions — different cadences, different SLOs, different people read them. Keeping them in their own surface lets each one stay opinionated about what it shows. ## What you get out of the box Wrap your client once, deploy, and the dashboard populates within seconds. Five sub-tabs aggregate the same event stream from different angles: | Sub-tab | The question it answers | | - | - | | **Overview** | "How is this app doing right now?" — cost, traffic, error rate, latency, all with prior-window deltas | | **Traces** | "What happened in this specific request?" — every `withTrace` block as a card, drillable to individual events | | **Models** | "Which model is eating my budget?" — cost & token mix by model, ranked | | **Tools** | "Which functions does the model call most?" — tool-use frequency, success rate, p50/p95 duration | | **Users** | "Which end-user costs me money?" — per-user spend, traces, tokens (powered by [per-user tags](/concepts/per-user-spend)) | ## The data model behind the section Every event in AI Apps comes from one of two sources: * **Wrapped LLM calls** — captured automatically by `@voightxyz/openai` or `@voightxyz/anthropic` when you wrap your client. One event per `chat.completions.create`, `responses.create`, or `messages.create` call. Carries model, tokens (input/output/cache reads/cache creations), tool calls, latency, finish reason, and (if you call `withTrace`) your `tags` map. * **`log()` events inside `withTrace`** — when you call `log()` from inside a `withTrace` block to mark a domain event (`"user submitted form"`, `"retrieval returned 0 results"`). Stamped with the same `tags` as the surrounding LLM calls, but `type: 'log'` so they're distinguishable. Both flow through the same ingestion pipeline as the rest of Voight. The AI Apps section adds a server-side filter: only agents whose `framework` is `openai` or `anthropic` are included, so your coding-agent traffic never bleeds in. ## Filter pills Three filters live in the top bar, all composable: * **Agent** — narrow to a single wrapped agent (e.g. `production-chat-api` vs `internal-summarizer-bot`). All agents using `@voightxyz/openai` or `@voightxyz/anthropic` appear here. * **Provider** — narrow to OpenAI or Anthropic only. Useful when one wrapper agent calls both providers (it happens — multi-provider routers, fallback chains) and you want to isolate one. * **User** — narrow to a single `tags.userId` value. Drives the Users sub-tab but is also available globally so you can see Models / Tools / Traces scoped to one customer. The filter state is reflected in the URL — share a link to `/dashboard/ai-apps?provider=anthropic&tag.plan=enterprise` and the recipient lands on the same view. ### Provider filter: per-event, not per-agent A subtle bug we hit early: a single agent that calls both OpenAI **and** Anthropic (failover, A/B routing) gets `agent.framework` pinned to whichever wrapper initialized first. If you filtered by `agent.framework`, choosing "OpenAI" would still show Claude models from that same agent. The fix shipped in May 2026: provider filtering uses **`metadata.source`** (per-event, set by each wrapper) instead of `agent.framework` (per-agent, first-seen). Multi-provider agents now filter cleanly — choose OpenAI and you see only events where the wrapper that emitted them was `@voightxyz/openai`. ## Overview sub-tab The default landing tab. Three regions: 1. **KPI strip** — cost USD, total calls, error rate, p50 latency, p95 latency. Each KPI carries a delta vs the prior identical window (`28d vs prior 28d` by default; toggleable to 7d / 1d). 2. **Activity pulse** — stacked-area chart of traffic over time, coloured by provider. The pulse refreshes every 15s so you can watch live traffic during a deploy. 3. **Top models / Top tools / Top users** — three compact leaderboards as entry points to the deeper sub-tabs. Everything respects the active filters. Set the Provider pill to OpenAI and the KPIs, the pulse, and the leaderboards all narrow. ## Traces sub-tab A `withTrace` block becomes one trace card. The card shows: * Route tag (`POST /api/chat`) * Total duration of the block * Cost USD * Number of LLM calls, tool calls, errors * User tag (if set) — clickable to filter the whole section to that user Click a card and you get the full timeline: every `chat.completions.create`, every tool call, every `log()` event, in order, with prompts and responses revealed by an eye toggle (masked by default, same UX as the coding-agent traces). Derived child spans: if a model response includes `tool_calls`, the dashboard renders each one as a synthetic child span under the parent LLM call — even though they're not separate events on the wire. Mirror what most engineers expect from APM tools. ## Models sub-tab One row per `(provider, model)` pair active in the time window: * Cost USD (sum) * Calls (count) * Tokens (input + output + cache reads + cache creations, decomposed) * p50 / p95 latency * Share of overall cost as a sparkline Sorted by cost descending. Click any row to filter Traces and Overview to that specific model. ## Tools sub-tab One row per unique `tool.name` the model invoked: * Invocations (count) * Success rate (`outcome === 'success'` / total) * p50 / p95 duration of the tool execution itself * Last seen timestamp * Top users for this tool (mini-leaderboard inline) Includes both function-calling (OpenAI / Anthropic `tool_use`) and any explicit `log({ type: 'tool', toolExecuted })` calls inside `withTrace`. ## Users sub-tab Powered by [per-user tags](/concepts/per-user-spend). See that page for the 1-line code pattern that makes this tab populate. Key columns: * User (the `tags.userId` value, verbatim) * Spend (USD) * Traces (`withTrace` block count) * Tokens (total) * Last seen * Top model Sorted by spend desc. Empty state if you haven't wired `tags.userId` yet, with a deep link to the per-user-spend setup guide. ## Privacy in AI Apps The wrapper SDKs ship with the same three privacy levels as the rest of Voight — Minimal, Standard, Full. Set on wrap: ```ts theme={null} wrapOpenAI(new OpenAI(), { privacy: 'standard', agent: '...' }) ``` | Level | Prompts & responses | Tool arguments | Tokens & cost | Tags | | - | - | - | - | - | | Minimal | dropped | dropped | kept | kept | | Standard ★ | scrubbed via [`scrubPii()`](/privacy/pii-patterns) | scrubbed | kept | kept | | Full | verbatim | verbatim | kept | kept | Tags are treated as operational identifiers (not user content) and pass through every level. Numbers always pass through. The AI Apps section works in all three modes — Minimal still gives you cost, model, latency, and user attribution; you lose only the prompt/response previews. ## Time window The window selector in the top-right controls every panel in the section. Defaults to 28 days. Available windows: 1 day, 7 days, 14 days, 28 days, 90 days. All deltas (the green/red percentage chips on KPIs) compare to the prior identical window — `28d vs prior 28d`, `7d vs prior 7d`, etc. Retention cap is governed by your [pricing tier](/pricing) — events older than your retention window are purged. ## How AI Apps relates to the rest of the dashboard | Dashboard surface | What it shows | When you reach for it | | - | - | - | | **AI Apps** (this page) | Production LLM apps using the wrapper SDKs | Customer-facing copilots, agentic features, B2B AI products | | **Overview** (`/dashboard`) | Coding-agent sessions (Claude Code, Cursor, Codex) + library-mode bots | Your team's dev workflow telemetry | | **Audit log** | Every event across every surface, filterable | "Show me what happened at 3:42pm" forensics | | **Sessions** | Per-process timelines | Long-running autonomous agents, multi-hour Claude Code runs | | **Traces** | Per-prompt timelines | One agent turn from prompt to final response | | **Explorer** (`/explore`) | Public Solana agent registry | On-chain identity, reputation, x402 | Same backend, same event schema, same privacy model. The AI Apps section is the **opinionated view** for production LLM-app telemetry — built so a product engineer can answer "is this app healthy?" without learning the rest of the dashboard. ## API access Every panel is backed by a public endpoint under `/v1/me/ai-apps/*`: ```bash theme={null} curl -H "authorization: Bearer $PRIVY_JWT" \ "https://api.voight.xyz/v1/me/ai-apps/overview?windowDays=28&provider=anthropic" curl -H "authorization: Bearer $PRIVY_JWT" \ "https://api.voight.xyz/v1/me/ai-apps/users?windowDays=7&tag.plan=enterprise" ``` Build your own internal admin UIs, weekly cost reports, customer-facing per-tenant dashboards — same data the AI Apps section consumes. ## Next * [Per-user spend](/concepts/per-user-spend) — the killer feature; one line of code to populate the Users tab * [Tracing](/ai-apps/tracing) — `withTrace` + `log` API in full * [OpenAI SDK](/ai-apps/openai) — wrap your OpenAI client * [Anthropic SDK](/ai-apps/anthropic) — wrap your Anthropic client * [Python (bitfrost)](/ai-apps/python) — one line auto-instruments openai, anthropic, litellm, and smolagents * [How to debug AI agents in production](https://voight.xyz/guides/debug-ai-agents-in-production): the failure modes worth watching and how to catch them # Python (bitfrost) Source: https://docs.voight.xyz/ai-apps/python One line of Python auto-instruments openai, anthropic, litellm, and smolagents, and streams every LLM call to Voight over OpenTelemetry. [`bitfrost`](https://github.com/Voightxyz/bitfrost) is Voight's OpenTelemetry bridge for Python. It turns the OTel GenAI spans your LLM libraries already emit into Voight events: model calls, prompts, tokens, cost, latency, errors, and the trace tree that links them. It is also a standalone tool: without a Voight key it captures to your terminal, a JSONL file, or SQLite, with zero network calls. Point it at Voight when you want the dashboard. ## Install ```bash theme={null} pip install bitfrost ``` Python 3.10+. ## Quick start One line at your app's entrypoint: ```python theme={null} import bitfrost from bitfrost.backends.voight import VoightBackend bitfrost.quickstart(agent="my-app", backend=VoightBackend()) ``` `quickstart` auto-detects and instruments every supported library it finds: `openai`, `anthropic`, `litellm`, and `smolagents`. Your normal LLM calls start streaming to the dashboard, no other code changes. `VoightBackend` reads your API key from the `VOIGHT_KEY` environment variable: ```bash theme={null} export VOIGHT_KEY="vk_..." ``` Create the key in the dashboard under Settings, or from the "Add app" guide. ## What lands in Voight Each LLM call becomes one event carrying: * the model and provider, resolved from the span's `gen_ai.*` attributes (both the v1.27 and v1.32+ semantic-convention generations are understood) * prompt messages and the response text, subject to the privacy level * token counts (input, output, total): the dashboard's token and cost KPIs read these directly * duration, outcome, and the error message when a call fails * the span's own timestamp, so batched or delayed sends keep an honest timeline * the OpenTelemetry trace context (`traceId`, `spanId`, `parentSpanId`), so multi-step requests arrive as a tree, not a flat list ## Privacy Three levels, applied before anything leaves the process: `standard` (default), `minimal` (drops prompt and response content), and `off`. PII scrubbing (12 patterns plus Luhn) runs on outbound content. ```python theme={null} bitfrost.quickstart(agent="my-app", backend=VoightBackend(), privacy="minimal") ``` ## No supported library? If none of the four libraries is installed, `quickstart` still installs the tracer and warns you. Any manually created OpenTelemetry span with `gen_ai.*` attributes is captured, so custom stacks and other instrumentations keep working. ## Next * [AI Apps overview](/ai-apps/overview): what the dashboard shows * [Tracing](/ai-apps/tracing): how events group into traces * [bitfrost on GitHub](https://github.com/Voightxyz/bitfrost): full README, CLI, TUI, and the local offline dashboard # Tracing Source: https://docs.voight.xyz/ai-apps/tracing Group every LLM call inside a request into one trace — withTrace + log + tags, shared by both wrappers. `withTrace` is the request-boundary primitive shared by [`@voightxyz/openai`](/ai-apps/openai) and [`@voightxyz/anthropic`](/ai-apps/anthropic). It opens a logical span — typically your HTTP handler — and every wrapped LLM call inside that span gets grouped under one trace in the dashboard, automatically. It also carries **tags** (the foundation of [per-user spend tracking](/concepts/per-user-spend)) and a **routeTag** (so traces are attributable to which endpoint produced them). Under the hood it's `AsyncLocalStorage` — Node's async-context primitive — so there's nothing to thread through your function signatures. ## Why grouping matters A single user request often makes multiple LLM calls: a planner, a retrieval reranker, a final answer, maybe a moderation check on the output. Without grouping, those are five disconnected events in the audit log. With `withTrace`, they're one trace card on the dashboard with one total cost, one total latency, and a drillable timeline. The same shape industry tools (Datadog APM, Sentry, OpenTelemetry) use for HTTP request tracing — Voight applies it to LLM workflows. ## The API surface Two functions, exported identically from both wrappers: ```ts theme={null} import { withTrace, log } from '@voightxyz/openai' // — or — import { withTrace, log } from '@voightxyz/anthropic' ``` Use whichever you imported the wrapper from. Both share the same backing implementation; you don't need to import from both if your app uses both providers. ### `withTrace(fn, options)` ```ts theme={null} withTrace( fn: () => Promise, options?: { routeTag?: string // e.g. 'POST /api/chat' tags?: Record // e.g. { userId, plan, org } }, ): Promise ``` Opens a trace, runs `fn`, returns whatever `fn` returned. Every wrapped LLM call that happens inside `fn` (including in helper functions, await points, callbacks — anything reachable through async context) gets stamped with the trace's `routeTag` and `tags`. The trace closes automatically when `fn` resolves or rejects. Errors propagate normally — `withTrace` doesn't swallow. ### `log(message, extra?)` ```ts theme={null} log( message: string, extra?: Record, ): void ``` Emits a free-form event inside the current trace. Doesn't require an awaitable — synchronous, fire-and-forget. Common uses: * `log('cache hit')` — mark a code path * `log('retrieval returned 0 results', { query })` — capture a domain signal * `log('fallback to gpt-4o')` — annotate a routing decision `log()` events appear in the Traces timeline interleaved with LLM calls, carry the same `tags`, and show up in the audit log. Calling `log()` **outside** a `withTrace` block is a no-op (with a one-line console warning) — the event has nowhere to belong. ## Minimal example ```ts theme={null} import OpenAI from 'openai' import { wrapOpenAI, withTrace, log } from '@voightxyz/openai' const openai = wrapOpenAI(new OpenAI(), { agent: 'production-chat-api', privacy: 'standard', }) app.post('/api/chat', async (req, res) => { await withTrace( async () => { log('chat request received', { userId: req.user.id }) const reply = await openai.chat.completions.create({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: req.body.prompt }], }) log('reply generated', { tokens: reply.usage?.total_tokens }) res.json({ reply: reply.choices[0].message.content }) }, { routeTag: 'POST /api/chat', tags: { userId: req.user.id, plan: req.user.plan }, }, ) }) ``` In the dashboard this lands as **one trace card** with two `log` events bookending the LLM call, all tagged with the user. ## Composing across helpers You don't need to pass anything explicitly through function signatures — `AsyncLocalStorage` carries the trace context through any awaited call: ```ts theme={null} async function summarize(text: string) { log('summarize:start', { length: text.length }) const result = await openai.chat.completions.create({ ... }) log('summarize:done') return result } async function moderate(text: string) { log('moderate:start') const result = await openai.chat.completions.create({ ... }) log('moderate:done') return result } app.post('/api/process', async (req, res) => { await withTrace( async () => { const summary = await summarize(req.body.text) const flagged = await moderate(summary.choices[0].message.content!) res.json({ summary, flagged }) }, { routeTag: 'POST /api/process', tags: { userId: req.user.id }, }, ) }) ``` Two LLM calls, four `log` events, one trace. The helpers don't know they're being traced — they just call `log()` and use the wrapped client. ## Mixing providers in one trace If you use both OpenAI and Anthropic in the same request (router pattern, fallback chain, A/B test), wrap both clients and import `withTrace` from either package: ```ts theme={null} import { wrapOpenAI, withTrace } from '@voightxyz/openai' import { wrapAnthropic } from '@voightxyz/anthropic' const openai = wrapOpenAI(new OpenAI(), { agent: 'router' }) const anthropic = wrapAnthropic(new Anthropic(), { agent: 'router' }) await withTrace( async () => { const openaiReply = await openai.chat.completions.create({ ... }) const anthropicReply = await anthropic.messages.create({ ... }) // both LLM calls land under the same trace, tagged identically }, { tags: { userId, ab: 'arm-b' } }, ) ``` Both packages share a global async-context store under the hood — there's no fight about who "owns" the trace. ## Tags propagate everywhere Anything you set in `withTrace({ tags })` lands on `metadata.tags` of every event produced inside. The dashboard surfaces: * `tags.userId` → the Users sub-tab + the global User filter pill * All other tags → queryable via `GET /v1/me/ai-apps/*?tag.=` (e.g. `?tag.plan=pro`) Reserved keys (Voight uses them for product features): `userId`, `plan`, `org`, `feature`. You can still use other names freely; these four just have dedicated dashboard surfaces. See [per-user spend](/concepts/per-user-spend) for the full conventions. ## Route tagging `routeTag` is a freeform string that becomes the trace card's headline label. Conventions that work well: * HTTP method + path: `'POST /api/chat'` * gRPC: `'ChatService.GenerateReply'` * Job names: `'cron:daily-summary'` * Background workers: `'queue:embed-doc'` If you don't pass one, the trace card falls back to `'untagged'`. Nothing breaks, but you lose the ability to slice metrics by endpoint. ## Nested `withTrace` calls If you call `withTrace` inside an already-open trace, the inner call **inherits** the outer trace's context — same `routeTag`, same `tags`, same trace ID. The inner block doesn't open a new trace. This is intentional: nesting is common in middleware (a logger middleware wraps every handler in `withTrace`, then a handler-specific wrapper does the same), and we don't want to fragment one logical request into multiple traces. If you genuinely need a separate trace inside the same async stack (rare), end the outer one explicitly and start a new one after. ## Errors ```ts theme={null} await withTrace( async () => { await openai.chat.completions.create({ ... }) throw new Error('downstream failed') // ← rethrows after the LLM call is captured }, { routeTag: 'POST /api/risky' }, ) // Caller sees the original error. // Trace card on the dashboard shows: 1 LLM call captured + 1 errored trace. ``` The LLM call's event is recorded normally. The trace card surfaces the error state so you can find failed requests fast in the dashboard. ## Performance * `AsyncLocalStorage` is part of Node's built-in `async_hooks`. Overhead is sub-microsecond per await — negligible compared to any network call. * `log()` events are buffered in memory and flushed when the trace closes. One HTTP request out per trace, not per `log()` call. * `withTrace` is safe in serverless (Vercel Functions, AWS Lambda, Cloudflare Workers — wherever Node 18+ runs). ## Comparing to library-mode `voight.log()` If you're using the [library mode SDK](/sdk/library-mode) for autonomous bots, you're already familiar with `voight.log()`. The two are siblings: | | Library mode `voight.log()` | Wrapper `log()` | | - | - | - | | Where it lives | `@voightxyz/sdk` | `@voightxyz/openai` and `@voightxyz/anthropic` | | Needs an open trace? | No — every call is its own event | Yes — must be inside `withTrace` | | Carries `tags`? | Pass as `metadata` per call | Inherited from `withTrace({ tags })` automatically | | Returns a promise? | Yes — `{ ok, error? }` shape | No — fire-and-forget | | Use case | Autonomous loops, agent decisions | Request-boundary instrumentation in apps | Same backend, same event ingestion, same dashboard. The two coexist — a hybrid app can call `voight.log()` from a background worker AND `withTrace` / `log` from request handlers, under the same agent. ## FAQ `AsyncLocalStorage` is preserved across native `async`/`await`, `Promise.then`, Node's stream events, and most modern frameworks (Express, Fastify, Hono, Koa, Next.js Route Handlers). A few cases lose it: explicit thread-pool workarounds (`worker_threads`), some legacy callback-based libraries, and certain promise libraries that drop `domain` context. If you use `withTrace` and the inner LLM call doesn't get tagged, the async context was probably dropped somewhere on the path. Workaround: pass the tags explicitly to a fresh `withTrace` call deeper in the stack. Yes — `withTrace` is just a function. Call it from your queue handler the same way you'd call it from an HTTP handler: ```ts theme={null} queue.process('embed-doc', async (job) => { await withTrace( async () => { /* your handler */ }, { routeTag: 'queue:embed-doc', tags: { jobId: job.id } }, ) }) ``` The trace lifecycle matches the job lifecycle. Not via `tags`. The wrapper accepts a top-level `agent` and `privacy` option, but per-request tagging is scoped to `withTrace`. This is deliberate — global mutable tags on a wrapper instance would be racy across concurrent requests. If you really need static tags (e.g. `env: 'production'` everywhere), you can wrap a single `withTrace` at process boot and run your whole app inside it, but the typical pattern is per-request `withTrace`. Tags are flattened into `metadata.tags` on every event. Practical limit is what Postgres / your JSON column can store comfortably — keep tag values short strings (under 256 chars). The dashboard truncates display at 64 chars per value. For longer attributes (full user records, structured payloads), use `metadata.detail` on individual events via the [library-mode SDK](/sdk/library-mode) — that's the right slot. ## Next * [Per-user spend](/concepts/per-user-spend) — the killer use case for `withTrace({ tags })` * [OpenAI SDK](/ai-apps/openai) — `wrapOpenAI` + everything captured per LLM call * [Anthropic SDK](/ai-apps/anthropic) — `wrapAnthropic` + everything captured per LLM call * [AI Apps overview](/ai-apps/overview) — what the dashboard renders from these traces # Vercel AI SDK Source: https://docs.voight.xyz/ai-apps/vercel-ai OpenTelemetry SpanExporter for the Vercel AI SDK — capture every streamText / generateText / streamObject / generateObject call without touching your app code. `@voightxyz/vercel-ai` is an OpenTelemetry `SpanExporter` that consumes the `experimental_telemetry` spans the [Vercel AI SDK](https://sdk.vercel.ai) emits natively. Register it once next to `@vercel/otel`, set `experimental_telemetry: { isEnabled: true }` on your `streamText` / `generateText` / `streamObject` / `generateObject` calls, and every call lands in your Voight dashboard with prompts, tokens, tool calls, cache reads, latency, and errors. Same backend and dashboard as [`@voightxyz/openai`](/ai-apps/openai) + [`@voightxyz/anthropic`](/ai-apps/anthropic). Events from all three packages land side-by-side under the same agent. ## Quick setup with the wizard If your app already imports `ai`, the lowest-friction install is the wizard from the main SDK: ```bash theme={null} cd your-app npx -y @voightxyz/sdk init ``` It detects `ai` in your `package.json`, prompts for a privacy level + Voight key + agent name, validates the key, and writes a ready-to-load `instrumentation.ts` that registers `VoightExporter` with `@vercel/otel`. 30 seconds, zero copy-paste. Full walkthrough at [docs.voight.xyz/ai-apps/wizard](/ai-apps/wizard). Continue below if you'd rather wire it manually. ## Why an OTel SpanExporter The Vercel AI SDK emits OpenTelemetry spans natively when `experimental_telemetry` is enabled — the same wire format every other LLM-observability tool listed in the [Vercel AI SDK docs](https://sdk.vercel.ai/providers/observability) consumes (Langfuse, Helicone, Phoenix, Braintrust, Datadog, Sentry, Weights & Biases). We follow the same contract so you can: * Wire Voight alongside one of those tools via `MultiSpanProcessor`, or * Drop in Voight as the sole observability provider, with the same code path either way. No vendor lock, no custom middleware. ## Install ```bash theme={null} npm install ai @ai-sdk/openai @vercel/otel @voightxyz/vercel-ai ``` `@voightxyz/vercel-ai` declares `@opentelemetry/api` and `@opentelemetry/sdk-trace-base` as peer dependencies, and `ai` as an optional peer. Bring your own provider package (`@ai-sdk/openai`, `@ai-sdk/anthropic`, …). Requirements: * Node.js 18+ * Vercel AI SDK 6.0.0+ * A Voight API key (`vk_…`) — generate one at [voight.xyz/dashboard/settings](https://voight.xyz/dashboard/settings) ## Quick start ### 1. Register the exporter via `instrumentation.ts` ```ts theme={null} // instrumentation.ts (Next.js App Router) or any OTel bootstrap. import { registerOTel } from '@vercel/otel' import { VoightExporter } from '@voightxyz/vercel-ai' export function register() { registerOTel({ serviceName: 'my-app', traceExporter: new VoightExporter({ agent: 'production-chat-api', privacy: 'standard', }), }) } ``` ### 2. Enable telemetry on your LLM calls ```ts theme={null} import { openai } from '@ai-sdk/openai' import { streamText } from 'ai' const result = streamText({ model: openai('gpt-4o-mini'), prompt: req.body.prompt, experimental_telemetry: { isEnabled: true, functionId: 'stream-text', }, }) ``` ### 3. Set `VOIGHT_KEY` ```bash theme={null} # .env.local VOIGHT_KEY=vk_your_key_here ``` That's it — every wrapped `streamText` / `generateText` / `streamObject` / `generateObject` call is captured automatically. Visit your [Voight dashboard](https://voight.xyz/dashboard/ai-apps) to see them in real time. ## Per-user attribution For production apps where you want to attribute cost per end-user with one line of code, pass `metadata` on `experimental_telemetry`: ```ts theme={null} const result = streamText({ model: openai('gpt-4o-mini'), prompt: req.body.prompt, experimental_telemetry: { isEnabled: true, functionId: 'stream-text', metadata: { userId: session.user.id, plan: session.user.plan, org: session.user.org, }, }, }) ``` The exporter lifts every `ai.telemetry.metadata.` span attribute into the Voight event's `metadata.tags.` — which is exactly the shape that drives the [per-user spend](/concepts/per-user-spend) sub-tab and the per-tag filter pills. The `userId` value is the only one Voight treats specially (it powers the **Users sub-tab**). Everything else is filter-only. ## Options | Option | Type | Default | Description | | - | - | - | - | | `agent` | `string` | `process.env.VOIGHT_AGENT` → `process.env.HOSTNAME` → `'unknown-agent'` | Stable agent identifier surfaced in the dashboard. | | `voightApiKey` | `string` | `process.env.VOIGHT_KEY` | API key for the Voight ingest endpoint. | | `apiBase` | `string` | `https://api.voight.xyz` | Override for self-hosted Voight deployments. | | `privacy` | `'minimal' \| 'standard' \| 'full'` | `'standard'` | See [Privacy](#privacy). | | `sessionId` | `string` | Auto UUID v4 per exporter instance | Stamps `metadata.sessionId` on every event the exporter emits. Use to scope a session yourself (per-user, per-conversation). | | `fetch` | `typeof fetch` | `globalThis.fetch` | Override for tests / custom transports. | | `onError` | `(err: unknown) => void` | no-op | Receives ingest network errors. Helpful during development. | ## What's captured | Signal | Where it lands | | - | - | | Model id (with version suffix) | `model` | | Base provider (`openai`, `anthropic`, …) | `metadata.provider` | | Raw provider surface (e.g. `openai.responses`) | `metadata.providerSurface` | | Prompt messages | `input.messages` (post-privacy filter) | | Response text | `metadata.responseText` | | Token counts (input / output) | `metadata.tokens` | | Cache reads (`gen_ai.usage.cache_read_input_tokens` / `ai.usage.cachedInputTokens`) | `metadata.tokens.cache_read` | | Cache creation (Anthropic Path-A) | `metadata.tokens.cache_creation` | | Tool / function calls (normalised to `{id, name, arguments}`) | `metadata.toolCalls` + `toolExecuted` | | Streaming flag (derived from `ai.streamText.doStream` vs `ai.generateText.doGenerate`) | `metadata.streaming` | | API marker | `metadata.api: 'vercel-ai'` | | Source marker | `metadata.source: 'vercel-ai-sdk'` | | Trace grouping (auto UUID or explicit) | `metadata.sessionId` | | User-supplied telemetry metadata | `metadata.tags.*` | | Finish reason | `metadata.finishReason` | | Latency (ms) | `durationMs` | | Errors | `errorMessage` + `outcome: 'failed'` | ## Privacy Three levels control how aggressively prompts, responses, and tool arguments are scrubbed before they leave the process. The PII catalogue is the same as `@voightxyz/openai` and `@voightxyz/anthropic`. | Level | Prompts & response text | Tool arguments | Tokens / model / cost / latency | | - | - | - | - | | `minimal` | dropped | dropped (names kept as tags) | always captured | | `standard` (default) | PII redacted (emails, phone, credit cards, JWTs, OpenAI / Anthropic / Stripe / GitHub / AWS / Slack keys) | PII redacted | always captured | | `full` | raw | raw | always captured | For most apps `'standard'` is the right balance — useful debugging signal without leaking secrets. ## Pairing with another OTel exporter The exporter coexists with any other `SpanExporter` via OTel's `MultiSpanProcessor` — common when you already ship to Datadog / Honeycomb / Grafana Tempo and want to add Voight as a second sink. ```ts theme={null} import { registerOTel } from '@vercel/otel' import { VoightExporter } from '@voightxyz/vercel-ai' import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http' registerOTel({ serviceName: 'my-app', traceExporter: [ new VoightExporter({ agent: 'production-chat-api' }), new OTLPTraceExporter({ url: process.env.OTLP_ENDPOINT }), ], }) ``` Each exporter sees the same span batch independently — Voight only consumes spans that carry the GenAI semconv (`gen_ai.*`) or Vercel (`ai.*`) attributes, so non-LLM spans (HTTP, DB) are silently skipped from the Voight stream while still reaching your other exporter. ## Pairing with Voight direct wrappers If your app also uses [`@voightxyz/openai`](/ai-apps/openai) or [`@voightxyz/anthropic`](/ai-apps/anthropic) with `otel: true`, both packages emit OpenTelemetry spans for every LLM call. Since `VoightExporter` is registered as an OTel exporter, those wrapper-emitted spans would normally hit the Voight backend twice — once via the wrapper's direct POST, once via this exporter. Starting in `0.1.1`, the exporter recognises the `voight.source: 'wrapper'` attribute the wrappers stamp on those spans and skips them cleanly — no POST, callback still SUCCESS. Other OTel exporters in the same process (Langfuse, Datadog, Sentry) still see the spans and forward them normally. The dedup is scoped to the Voight-to-Voight loop. Spans without that marker — the canonical `streamText` / `generateText` / `streamObject` / `generateObject` spans the Vercel AI SDK emits — are unaffected. ## Out of scope (deferred) * **Direct middleware** (`voightMiddleware()`) for users who prefer a zero-OTel-setup wrap — planned for `0.2`. * **`withTrace` / `log` async-context helpers** — OTel context propagation already provides equivalent semantics. The helpers may return in `0.2` if real usage shows a gap. * **Batched / buffered ingest** — the per-event POST is sufficient for the workloads we expect at this scale. Batching arrives with real-world failure-mode data to design against. * **Bedrock / Vertex provider-specific paths** — the `gen_ai.*` normalisation is expected to cover them; will revisit if a user reports a missing attribute. ## Source [github.com/Voightxyz/voight-vercel-ai](https://github.com/Voightxyz/voight-vercel-ai) — Apache 2.0. # Wizard Source: https://docs.voight.xyz/ai-apps/wizard npx @voightxyz/sdk init — scaffold Voight into a production app in 30 seconds. `init` is the wizard sub-command in `@voightxyz/sdk` for apps that already call an LLM in production. It detects which SDK family you use — direct provider wrappers ([`@voightxyz/openai`](/ai-apps/openai), [`@voightxyz/anthropic`](/ai-apps/anthropic)) or the [Vercel AI SDK](/ai-apps/vercel-ai) — and scaffolds the matching path so you don't have to copy snippets from the docs. Run it from the root of your app: ```bash theme={null} cd your-app npx -y @voightxyz/sdk init ``` That's the entire setup. ## What the wizard does The flow is single-pass — no daemon, no background services, no follow-up commands: 1. **Detects providers**. Reads `package.json` (both `dependencies` and `devDependencies`) for three keys: `openai`, `@anthropic-ai/sdk`, and `ai` (Vercel AI SDK). If none are found, the wizard aborts cleanly and explains what to install. 2. **Picks the path**. If `ai` is present, the wizard takes the **Vercel AI SDK path** — it writes a root `instrumentation.ts` registering [`@voightxyz/vercel-ai`](/ai-apps/vercel-ai) with `@vercel/otel`. Otherwise it takes the **direct-wrapper path** — it writes `src/lib/voight.ts` with the wrapped OpenAI / Anthropic clients. Mixed projects (both `ai` and a direct provider) take the Vercel path with an explicit message, since the OTel exporter covers every underlying provider. 3. **Detects package manager**. Looks for `pnpm-lock.yaml` → `yarn.lock` → `bun.lockb` → falls back to npm. The install command printed at the end matches what you actually use. 4. **Detects framework**. Looks for `next` in deps. If present, the usage snippet at the end uses Next.js Route Handler shape (`export async function POST`); otherwise Express style (`app.post('/api/chat', ...)`). 5. **Prompts for privacy level**. Three options: * **Minimal** — metadata only (tokens, latency, cost). No prompts or responses captured. * **Standard ★** — full content with [12-pattern PII scrubbing](/privacy/pii-patterns). Required for customer-facing apps where end-user PII flows through prompts. * **Full** — raw, no scrubbing. Debugging only. 6. **Prompts for the Voight API key** (`vk_...`). Validates it against `api.voight.xyz` before writing anything — typo'd keys get caught here, not in production. 7. **Prompts for an agent name**. Defaults to your `package.json` name; press enter to accept or type a custom one. 8. **Writes the target module**: * **Direct-wrapper path** → `src/lib/voight.ts` (or `lib/voight.ts` if your project doesn't have `src/`). Contains the wrapped clients for the providers detected, plus a header comment explaining how to swap if your provider keys come from AWS Secrets Manager / Vault / Doppler instead of env. * **Vercel AI SDK path** → `instrumentation.ts` at the project root (or `src/instrumentation.ts` if your project uses `src/`). Registers `@vercel/otel` with `VoightExporter` as the trace exporter and includes a header comment explaining how to activate per-user attribution via `experimental_telemetry.metadata`. 9. **Appends `VOIGHT_KEY` to `.env.local`**. Creates the file if absent. Existing `VOIGHT_KEY` lines are left alone. 10. **Prints the install command + usage snippet**. The install command uses your package manager and is tailored to the path: * Direct wrappers → `pnpm add @voightxyz/openai @voightxyz/anthropic` * Vercel AI SDK → `pnpm add @voightxyz/vercel-ai @vercel/otel` ## What it does NOT do * ❌ **Never touches `~/.claude`, `~/.cursor`, `~/.codex`** or any other path outside your project's `cwd`. This is the key difference from `setup` (which wires IDE hooks globally). * ❌ **Never asks for or stores your provider keys** (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`). Those belong to your app and stay there. The Voight wrapper instruments the requests but never reads the auth header. * ❌ **Never runs `npm install` for you**. We print the command so you can run it in your terminal — that way you stay in control of your lockfile (and we don't accidentally pin a version you didn't approve). * ❌ **Never overrides an existing `src/lib/voight.ts` or `instrumentation.ts`**. If the target file already exists, the wizard aborts and asks you to move it aside (relevant if you already wire OpenTelemetry yourself — see the [MultiSpanProcessor pairing](/ai-apps/vercel-ai#pairing-with-another-otel-exporter) docs). ## Example session ``` $ cd ~/projects/customer-support-saas $ npx -y @voightxyz/sdk init Voight · init → /Users/jane/projects/customer-support-saas ✓ Detected providers: openai (^4.79.2) + @anthropic-ai/sdk (^0.96.0) ✓ Detected package manager: pnpm ✓ Detected framework: Next.js Privacy level for this project? 1) Minimal · metadata only (tokens, latency, cost) 2) Standard ★ Required for customer-facing apps — PII scrubbing on 3) Full · raw, no scrubbing (debugging only) > 2 ✓ Privacy: standard Now we need your Voight API key: 1. Open → https://voight.xyz/dashboard 2. Sign in (Google / X / wallet) 3. Settings → Generate key → copy the vk_… secret Paste it here: vk_aBcD… ✓ Validating key with api.voight.xyz… ✓ Key valid Agent name (defaults to "customer-support-saas")? production-chat-api ✓ Agent: production-chat-api ✓ Wrote src/lib/voight.ts ✓ Created .env.local with VOIGHT_KEY Use it in your handlers: // app/api/chat/route.ts import { openai, withTrace } from '@/lib/voight' export async function POST(req: Request) { return withTrace( async () => { const r = await openai.chat.completions.create({ ... }) return Response.json({ reply: r.choices[0].message }) }, { routeTag: 'POST /api/chat', tags: { userId: 'user_123', plan: 'pro' }, }, ) } One more step — install the wrappers: pnpm add @voightxyz/openai @voightxyz/anthropic Then open https://voight.xyz/dashboard/ai-apps ``` ## Example session — Vercel AI SDK path When the wizard detects `ai` in `package.json`, it takes the Vercel path. The prompts are identical (privacy + key + agent name); only the written file and the install command differ: ``` $ cd ~/projects/ai-chatbot $ npx -y @voightxyz/sdk init Voight · init → /Users/jane/projects/ai-chatbot ✓ Detected providers: ai (^6.0.0) + openai (^4.79.2) (using the Vercel AI SDK path — covers both) ✓ Detected package manager: pnpm ✓ Detected framework: Next.js ✓ Privacy: standard ✓ Key valid ✓ Agent: production-chat-api ✓ Wrote instrumentation.ts ✓ Created .env.local with VOIGHT_KEY Activate telemetry on your existing calls: // app/api/chat/route.ts import { openai } from '@ai-sdk/openai' import { streamText } from 'ai' export async function POST(req: Request) { const result = streamText({ model: openai('gpt-4o-mini'), prompt: (await req.json()).prompt, experimental_telemetry: { isEnabled: true, metadata: { userId: 'user_123', plan: 'pro' }, }, }) return result.toAIStreamResponse() } Per-user attribution: anything you put in `metadata` shows up under `metadata.tags.*` in Voight — `userId` powers the Users sub-tab. One more step — install the Voight exporter: pnpm add @voightxyz/vercel-ai @vercel/otel Then open https://voight.xyz/dashboard/ai-apps ``` ## Generated file ### Direct-wrapper path `src/lib/voight.ts` after a dual-provider install with `agent: 'production-chat-api'`, `privacy: 'standard'`: ```ts theme={null} // ────────────────────────────────────────────────────────────────── // Voight observability — generated by `npx @voightxyz/sdk init` // // Both wrapped clients use the standard `new OpenAI()` / // `new Anthropic()` constructors, which read OPENAI_API_KEY and // ANTHROPIC_API_KEY from process.env. This is the convention for // most apps (the values typically live in .env / .env.local). // // If your provider keys come from elsewhere (AWS Secrets Manager, // HashiCorp Vault, Doppler, a database, etc.), adjust the // constructor calls below to pass the key explicitly, e.g.: // // const openai = wrapOpenAI( // new OpenAI({ apiKey: await loadFromVault('openai') }), // { agent: 'production-chat-api', privacy: 'standard' } // ) // // The Voight wrapper never sees the provider key — it wraps // whatever client you construct. // ────────────────────────────────────────────────────────────────── import OpenAI from 'openai' import Anthropic from '@anthropic-ai/sdk' import { wrapOpenAI } from '@voightxyz/openai' import { wrapAnthropic } from '@voightxyz/anthropic' export const openai = wrapOpenAI(new OpenAI(), { agent: 'production-chat-api', privacy: 'standard', }) export const anthropic = wrapAnthropic(new Anthropic(), { agent: 'production-chat-api', privacy: 'standard', }) // Re-exports — import everything from one place. export { withTrace, log } from '@voightxyz/openai' ``` If only one provider is detected, only that provider's block is emitted. Single import path either way: `import { openai, anthropic, withTrace, log } from '@/lib/voight'`. ### Vercel AI SDK path `instrumentation.ts` after a Vercel install with `agent: 'production-chat-api'`, `privacy: 'standard'`: ```ts theme={null} // ────────────────────────────────────────────────────────────────── // Voight observability — generated by `npx @voightxyz/sdk init` // // Registers the Voight OpenTelemetry exporter for every Vercel // AI SDK call (streamText / generateText / streamObject / // generateObject). To opt a call into observability, flip // `experimental_telemetry: { isEnabled: true }` on it. // // For per-user attribution, pass a metadata bag: // // experimental_telemetry: { // isEnabled: true, // metadata: { userId: session.user.id, plan: session.user.plan }, // } // // The Voight exporter lifts `ai.telemetry.metadata.*` attributes // onto `metadata.tags.*` so the dashboard Users sub-tab + per-tag // filter pills populate automatically. // ────────────────────────────────────────────────────────────────── import { registerOTel } from '@vercel/otel' import { VoightExporter } from '@voightxyz/vercel-ai' export function register() { registerOTel({ serviceName: 'production-chat-api', traceExporter: new VoightExporter({ agent: 'production-chat-api', privacy: 'standard', }), }) } ``` The file lives at `instrumentation.ts` in the project root (Next.js convention) — or `src/instrumentation.ts` if your project already uses `src/`. See the [@voightxyz/vercel-ai docs](/ai-apps/vercel-ai) for the full capture matrix. ## Non-interactive flags If you're scripting the install (CI, Dockerfile, dotfile bootstrap), pass everything as flags: ```bash theme={null} npx -y @voightxyz/sdk init \ --privacy=standard \ --key=vk_aBcD... \ --agent=production-chat-api ``` Reads `VOIGHT_KEY` and `VOIGHT_PRIVACY` from env too if you'd rather not pass them inline. Refuses to write anything if the key validation fails — same safety as the interactive flow. ## Manual install (skip the wizard) The wizard is convenience, not requirement. If you'd rather wire things up by hand, the [OpenAI SDK](/ai-apps/openai) and [Anthropic SDK](/ai-apps/anthropic) pages walk through the same install — npm install, wrap your client, set `VOIGHT_KEY`. Three commands, no wizard. ## Troubleshooting The wizard only operates on projects that already use one of those SDKs. If you're in a monorepo, `cd` into the app folder first (e.g. `cd apps/web`). If your project doesn't use either yet, install the provider SDK first: ```bash theme={null} npm install openai npm install @anthropic-ai/sdk ``` Then re-run the wizard. The key you pasted isn't recognised by `api.voight.xyz`. Common causes: * Typo in the paste — Voight keys start with `vk_` and are about 50 characters * The key was revoked — check [dashboard/settings](https://voight.xyz/dashboard/settings) and generate a new one * Wrong key entirely (e.g. you pasted an OpenAI key by mistake) Fix and re-run — no files were written. The wizard refuses to overwrite an existing file. Either move it aside (`mv src/lib/voight.ts src/lib/voight.ts.bak`) or delete it, then re-run. Not an error — the wizard leaves your existing `VOIGHT_KEY` alone instead of overwriting it. If you genuinely want to rotate the key, edit `.env.local` manually. The wizard picks the package manager whose lockfile it sees first (pnpm → yarn → bun → npm). If you have stale lockfiles from a previous tool, delete them. The wizard never runs the install itself — you can always run `pnpm add ...` / `yarn add ...` / `bun add ...` regardless of what the wizard printed. ## Safety guarantees `init` is implemented in `src/init.ts` of `@voightxyz/sdk` with **zero edits** to the existing `setup`, `hook`, or any other module. It cannot affect users running `setup` or `hook` because: * The CLI dispatches by sub-command name (`setup` / `init` / `hook`) — the `setup` branch runs first and returns before reaching `init`. * `init.ts` has no imports from `setup.ts`. There's no shared mutable state, no shared global config. * `init` writes only inside `process.cwd()` — no `homedir()` calls, no `~/.config` paths. The shape is enforced by the test suite (256 tests, including a structural assertion that `init` never produces a write path outside cwd). ## Next * [OpenAI SDK](/ai-apps/openai) — what the wrapped OpenAI client captures * [Anthropic SDK](/ai-apps/anthropic) — what the wrapped Anthropic client captures * [Tracing](/ai-apps/tracing) — `withTrace` and `log` in depth * [Per-user spend](/concepts/per-user-spend) — the `tags.userId` pattern the wizard's snippet demonstrates * [AI Apps overview](/ai-apps/overview) — the dashboard surface the wizard targets # Public agent endpoints Source: https://docs.voight.xyz/api-reference/agents Read-only endpoints for the public Explorer — no auth required. These endpoints serve the public [Explorer](https://voight.xyz/explore). No auth — anyone can read them, used to render agent listings, profiles, and cross-chain payment data. ## GET /v1/stats Global aggregate stats across all agents. ```bash theme={null} curl https://api.voight.xyz/v1/stats ``` Response: ```json theme={null} { "agents": { "total": 1452, "identified": 944, "unknown": 508 }, "events24h": 48355, "lastUpdated": "2026-05-12T14:02:54.084Z" } ``` ## GET /v1/explorer The full Explorer homepage payload — top agents, collections, recent activity feed, x402 stats. ```bash theme={null} curl https://api.voight.xyz/v1/explorer ``` Returns a large composite object. See the [Explorer page](https://voight.xyz/explore) for what each field renders. ## GET /v1/agents Paginated list of agents marked `isPublic: true`. Used by the Explorer's main grid. ```bash theme={null} curl "https://api.voight.xyz/v1/agents?limit=20&offset=0&search=eliza" ``` | Query param | Type | Default | Notes | | - | - | - | - | | `limit` | int | 20 | Max 100 | | `offset` | int | 0 | For pagination | | `search` | string | — | Free-text match on agent name, snsName, or description | | `filter` | `all \| identified \| unknown` | `all` | Identified = has `enriched.profiled = true` | Response: ```json theme={null} { "agents": [ { "id": "cmoq...", "snsName": "trading-bot.sol", "displayName": "Voight dev agent", "registryId": "...", // Metaplex Core asset pubkey if on-chain "reputationScore": 0.94, "source": "agent-registry", "metadata": { "agentUri": "...", "feedback_count": 0 }, "enriched": { "name": "Voight dev agent", "description": "...", "image": "https://...", "capabilities": ["..."], "profiled": true }, "lastSeenAt": "2026-05-12T13:55:21Z" } ], "total": 1452, "offset": 0 } ``` ## GET /v1/agents/:identifier Full public profile for a single agent. ```bash theme={null} curl https://api.voight.xyz/v1/agents/trading-bot.sol curl https://api.voight.xyz/v1/agents/cmoqgt7k8014msovgm35r2hwa ``` `identifier` can be: * A SNS domain (`trading-bot.sol`) * A Voight cuid (`cmoq...`) * A Metaplex Core asset pubkey if registered on-chain Response includes the full agent record plus aggregated public KPIs (events count, last activity, reputation) — without exposing your event content if you're the agent owner. Private fields stay private. ## GET /v1/x402 Proxy to x402scan data. Returns the live payment graph across Solana + Base. ```bash theme={null} curl https://api.voight.xyz/v1/x402 ``` Returns top sellers, recent transfers, and Coinbase Bazaar facilitator info. Used by the Explorer's x402 section. ## POST /v1/waitlist Email capture from the landing page. Rate-limited 5/h per IP. ```bash theme={null} curl -X POST https://api.voight.xyz/v1/waitlist \ -H "content-type: application/json" \ -d '{"email":"you@example.com","source":"landing"}' ``` | Field | Type | Required | Notes | | - | - | - | - | | `email` | string | Yes | Validated by Zod | | `source` | string | No | Free-form (`landing`, `docs`, etc.) — for analytics | | `metadata` | object | No | Anything else you want to attach | Response: `200 OK` with `{ ok: true }` if accepted, `429` if rate-limited. ## GET /health Liveness probe — for monitoring tools or uptime checks. ```bash theme={null} curl https://api.voight.xyz/health ``` Response: ```json theme={null} { "status": "ok", "service": "voight-api", "uptime": 8234.5, "version": "0.1.0" } ``` ## Next * [`/v1/me/*`](/api-reference/me) — authenticated dashboard endpoints: events, sessions, traces, errors, alerts, keys # POST /v1/events Source: https://docs.voight.xyz/api-reference/events Ingest an agent event. The single entry point for agent telemetry. Authenticated via API key. ## Endpoint ``` POST https://api.voight.xyz/v1/events ``` ## Headers ``` Authorization: Bearer vk_YOUR_API_KEY Content-Type: application/json ``` ## Request body ```json theme={null} { "agentId": "string", "timestamp": "string | number (optional)", "type": "reasoning | tool | tx | decision | action | error (optional)", "input": { "prompt": "string", "context": {} }, "reasoning": "string (optional)", "toolsConsidered": ["string"], "toolExecuted": "string (optional)", "transaction": "string (optional, transaction signature or reference)", "amount": { "token": "string", "value": "number" }, "outcome": "pending | success | failed (optional)", "durationMs": "number (optional)", "errorMessage": "string (optional)", "model": "string (optional)", "metadata": { /* free-form */ } } ``` | Field | Type | Required | Notes | | - | - | - | - | | `agentId` | string (1–120 chars) | Sometimes | Required unless your API key is pre-bound to an agent. Can be a SNS domain, a CUID, or a free-form label. | | `timestamp` | ISO 8601 string or unix ms number | No | Defaults to server-received time. | | `type` | enum | No | Defaults to `decision`. | | `input` | object | No | `{ prompt, context }` shape. Stored verbatim in DB after scrubbing. | | `reasoning` | string | No | Free-form human-readable trace. | | `toolsConsidered` | string\[] | No | Alternative tools the agent rejected. | | `toolExecuted` | string | No | Name of the tool that ran. | | `transaction` | string or null | No | Transaction signature or reference, for agents that move funds. | | `amount` | object | No | `{ token, value }`. For swaps / transfers / payments. | | `outcome` | enum | No | One of `pending`, `success`, `failed`. | | `durationMs` | non-negative integer | No | Tool runtime. | | `errorMessage` | string | No | When `outcome: 'failed'`. | | `model` | string | No | LLM model used (e.g. `claude-opus-4-7`). | | `metadata` | object | No | Free-form JSON. Voight reads specific keys (`tokensBreakdown`, `traceId`, `sessionId`, `cwd`, `git`, `privacyLevel`, `denial`, `wakeup`) but accepts any custom keys. | ## Validation The payload is validated with [Zod](https://zod.dev) on the server. Failed validation returns `400 Bad Request` with details. Unknown top-level keys are rejected; unknown keys inside `metadata` are accepted. ## Identity resolution The server resolves `agentId` to an internal cuid in this order: 1. **CUID match** — if `agentId` matches `/^c[a-z0-9]{24}$/`, look up by primary key directly. Fastest path. Used by SDK 0.3.4+ after the first event populates `.voight-agent-id`. 2. **`snsName` match** — for SNS domains like `trading-bot.sol`. 3. **`displayName` match** — for renamed agents. 4. **Create new** — if nothing matched, create a new Agent row. The response always includes the resolved cuid as `agentId`, regardless of what you sent. Persist it locally to skip resolution on subsequent calls. ## Response ### 202 Accepted ```json theme={null} { "id": "evt_clz3...", "accepted": true, "agentId": "cmoq...", "eventId": "evt_clz3..." } ``` `agentId` in the response is the resolved cuid. `eventId` is the new event's id. ### 400 Bad Request ```json theme={null} { "error": "validation_failed", "details": [ { "path": ["amount", "value"], "message": "Expected number, received string" } ] } ``` ### 401 Unauthorized ```json theme={null} { "error": "unauthorized", "reason": "missing_bearer | invalid_key | revoked_key" } ``` ### 410 Gone ```json theme={null} { "error": "agent_deleted" } ``` The agent has been soft-deleted from the dashboard. Ingestion is blocked. Subsequent requests for the same `agentId` will also return 410. The SDK can react by surfacing a warning to the user. ### 429 Too Many Requests ```json theme={null} { "error": "rate_limited" } ``` Headers include `Retry-After`. The SDK respects this automatically with exponential backoff. ### 500 / 502 / 503 Backend error. Retry with backoff. The SDK does this automatically; for direct HTTP callers, implement reasonable retries. ## Side effects When an event is accepted, the server also: * Updates `agent.lastSeenAt` to now (fire-and-forget) * Lazy-backfills `agent.framework` if the event has `metadata.tool` and the field was null * May trigger anomaly evaluation if this event crosses an alert threshold (next scheduler tick at most 5 minutes later) * Stores a `contentHash` (SHA-256 of the normalised payload): the basis of the tamper-evident audit trail ## Pre-bound vs free API keys When you create an API key in the dashboard: * **Default**: not bound to an agent. Caller specifies `agentId` per event. * **Bound to an agent**: caller can omit `agentId`; the server uses the bound one. Useful for autonomous bots where the key is dedicated. Bound keys can't be used to ingest events for other agents — Voight rejects mismatched `agentId` values. ## Idempotency Ingestion is **not idempotent** by default. Each call creates a new row. If you need dedup (e.g. retrying after timeout), generate a stable `metadata.dedupKey` on your side and reconcile downstream. ## Rate limits | Tier | Events / month | Burst limit | | - | - | - | | Free | 10,000 | 60 events / minute | | Pro | 300,000 | 600 events / minute | | Enterprise | Unlimited | Custom | Burst limits are enforced today; monthly quotas ship server-side in v1.0 alongside Stripe billing. ## Next * [`GET /v1/agents`](/api-reference/agents) — public agent listing for the Explorer * [`/v1/me/*`](/api-reference/me) — authenticated dashboard endpoints # /v1/me/* (authenticated) Source: https://docs.voight.xyz/api-reference/me The dashboard endpoints — your agents, events, sessions, traces, alerts, keys. All `/me/*` endpoints require a Privy JWT (the dashboard auth token). The frontend handles this automatically — these docs are for external tools talking to your own Voight account. ## Authentication ``` Authorization: Bearer ``` Get the JWT from your browser's dev tools while logged into the dashboard: ```js theme={null} // In the browser console at voight.xyz/dashboard const { getAccessToken } = window.__privy console.log(await getAccessToken()) ``` JWTs expire in \~1 hour. Long-running scripts should re-fetch periodically. For programmatic / long-lived access, use an [API key](/api-reference/events) on `/v1/events` for ingestion or build a separate backend that signs Privy JWTs server-side. ## Identity + profile | Method | Path | Purpose | | - | - | - | | `GET` | `/me` | Current user (id, privyId, email, wallet, handle) | | `PATCH` | `/me` | Update handle | ```bash theme={null} curl -H "Authorization: Bearer $JWT" https://api.voight.xyz/v1/me ``` ## Agents | Method | Path | Purpose | | - | - | - | | `GET` | `/me/agents` | List all your agents | | `GET` | `/me/agents/:id` | Full agent profile + 30-day calendar + KPIs + recent sessions / errors | | `PATCH` | `/me/agents/:id` | Edit displayName | | `DELETE` | `/me/agents/:id` | Soft-delete agent + block ingestion (returns 410 Gone going forward) | ```bash theme={null} curl -H "Authorization: Bearer $JWT" \ https://api.voight.xyz/v1/me/agents ``` ## Events | Method | Path | Purpose | | - | - | - | | `GET` | `/me/events?limit=N&since=24h\|7d\|30d\|all&agentId=cuid` | Stream of recent events (cap 1000) | Query params (all optional): | Param | Default | Notes | | - | - | - | | `limit` | 50 | Clamped 1..1000 | | `since` | (none) | `24h`, `7d`, `30d`, `all`. Invalid values silently dropped. | | `agentId` | (none) | Filter by one agent. Must be a CUID. Non-CUID values silently dropped. | ```bash theme={null} curl -H "Authorization: Bearer $JWT" \ "https://api.voight.xyz/v1/me/events?limit=100&since=24h" ``` ## Sessions | Method | Path | Purpose | | - | - | - | | `GET` | `/me/sessions?limit=N` | Aggregated sessions (grouped by `sessionId`) | | `GET` | `/me/sessions/:id` | One session detail with all events | ## Traces | Method | Path | Purpose | | - | - | - | | `GET` | `/me/traces?since=X` | Traces (grouped by `metadata.traceId`) | | `GET` | `/me/traces/:traceId` | One trace with full timeline + git context + privacy chips | ## Errors | Method | Path | Purpose | | - | - | - | | `GET` | `/me/errors?since=X` | Error clusters (Sentry-style, grouped by fingerprint) | | `POST` | `/me/errors/:fingerprint/resolve` | Mark cluster resolved | | `DELETE` | `/me/errors/:fingerprint/resolve` | Re-open cluster | Clusters auto-reopen if a new event matches the fingerprint with `timestamp > resolvedAt`. ## Alerts | Method | Path | Purpose | | - | - | - | | `GET` | `/me/alerts?status=X&count=Y` | Active / acknowledged / all alerts | | `POST` | `/me/alerts/:id/acknowledge` | Ack alert | | `POST` | `/me/alerts/:id/snooze` | Snooze N hours (`?hours=24`) | | `POST` | `/me/alerts/:id/unacknowledge` | Re-open alert | ## API keys | Method | Path | Purpose | | - | - | - | | `GET` | `/me/keys` | Your API keys (prefix + label + lastUsedAt) | | `POST` | `/me/keys` | Create a new key. Returns the plaintext **once**. | | `DELETE` | `/me/keys/:id` | Revoke a key | ```bash theme={null} # Create curl -X POST -H "Authorization: Bearer $JWT" \ -H "content-type: application/json" \ -d '{"label":"My production bot"}' \ https://api.voight.xyz/v1/me/keys # Response (only time you'll see the secret) # { "id": "...", "keyPrefix": "vk_abc12345", "secret": "vk_abc12345...full key...", "label": "My production bot" } ``` ## Spend summary | Method | Path | Purpose | | - | - | - | | `GET` | `/me/spend-summary` | Aggregated cost across all your agents (used by Overview KPI) | Returns `{ total, by_agent: [...], by_model: [...], by_day: [...] }`. ## Test event | Method | Path | Purpose | | - | - | - | | `POST` | `/me/test-event` | Create a synthetic event for onboarding/testing. Creates a placeholder agent if needed. | ```bash theme={null} curl -X POST -H "Authorization: Bearer $JWT" \ https://api.voight.xyz/v1/me/test-event ``` The synthetic agent has `isPublic: false` and a recognizable `ownerWallet` (`voight:test:`) so it won't appear on the public Explorer. ## Rate limits Per-user, per-endpoint. Reasonable defaults — if you hit a 429, back off. If you need higher limits for a specific use case, [reach out](mailto:support@voight.xyz). ## Notes * All `/me/*` endpoints filter by `userId` server-side. Cross-user reads are impossible from the API. * Soft-deleted agents are filtered out of all reads (`deletedAt: null` filter). The 9 read queries that touch `Agent` enforce this; the mutation queries (alert ack/snooze) intentionally keep the filter absent so you can dismiss alerts for deleted agents. * Endpoint behavior is documented in source at [`apps/api/src/routes/me.ts`](https://github.com/Voightxyz/voight/blob/main/apps/api/src/routes/me.ts). ## Next * [`POST /v1/events`](/api-reference/events) — event ingestion (API key auth, not JWT) * [Public agents](/api-reference/agents) — read-only Explorer endpoints (no auth) # Changelog Source: https://docs.voight.xyz/changelog What shipped, when. SDK releases follow [semver](https://semver.org/). Backend and dashboard ship continuously from `main` without version tags. ## Week of 2026-08-30 — Codex verified through native OpenTelemetry, SDK 0.7.0 Codex is a first-class target again, and this time on solid ground. Instead of fighting Codex Desktop's sandbox with a hooks plugin, Voight now speaks the protocol Codex itself ships: its native OpenTelemetry export, pointed at a new OTLP receiver (`POST /v1/otel/logs`). One config block, and every Codex session lands as a full trace. [Read the guide →](/sdk/codex) ### New * **`@voightxyz/sdk` 0.7.0** — `setup --target=codex` writes the `[otel]` block into `~/.codex/config.toml` (with a one-time backup, and cleanup of anything the old plugin approach left behind). Re-runs are idempotent; a foreign `[otel]` exporter is detected and never overwritten. * **OTLP/JSON logs receiver** on the API. Maps Codex's telemetry (prompts, tool calls with durations, per-turn token usage, permission decisions, API failures) onto Voight's event taxonomy; `conversation.id` becomes the trace id, so [Smart Traces](/concepts/traces) work on Codex sessions out of the box. * **Privacy at the receiver.** Codex attaches your OpenAI account id and email to every record; Voight strips them (plus all `auth.*` attributes) before storage. ### Changed * The dead marketplace-plugin machinery for Codex was removed from the SDK, and the install-target table now reads what it should have all along: verified, against codex-cli 0.146. ## Week of 2026-08-17 — Voight Agents on Nosana GPUs Voight Agents gained a second home: a **dedicated GPU from [Nosana](https://nosana.com)'s decentralized network**, running the model locally inside the job. Pick **Nosana GPU** in the deploy flow and you get an agent whose inference never leaves the GPU you rented, with no inference key in the container, and whose compute is a real on-chain job anyone can verify. [Read the guide →](/agents/gpu-hosting) ### New * **GPU hosting** in the deploy flow (NVIDIA 3060 market live; 3090 and 4090 coming soon). GPU agents run **Hermes 3 8B** locally with a 64K context window. * **An honest hourly lifecycle.** GPU time is rented by the hour, so the platform treats it as such: an agent in use is **renewed in place** before the hour lapses (same job, no restart), an idle one shows **GPU stopped**, and a stopped one **wakes on demand**: from a button, by messaging it on web or Telegram, or by one of its scheduled tasks. * **Memory that survives GPU sessions.** Curated memory is restored when the GPU comes up and saved back periodically and at shutdown. * **NOS deposits.** Top up your credits in Nosana's token, alongside USDC and SOL. * The container image and deployment tooling are open source: [Voightxyz/nosana-agents](https://github.com/Voightxyz/nosana-agents). ## Week of 2026-06-08 — Bitfrost (Python) 0.1.0 Python joins the family. [`bitfrost`](/python/bitfrost) is a drop-in OpenTelemetry exporter for Python LLM apps — the Python counterpart to `@voightxyz/vercel-ai`. Same backend, same dashboard, different runtime; events from both land side-by-side under the same agent. ### New * **`bitfrost` 0.1.0** — `pip install bitfrost`. One line (`bitfrost.quickstart()`) auto-detects and instruments openai / anthropic / litellm / smolagents. Built on the OpenTelemetry GenAI semantic conventions (v1.27 and v1.32+), so it works with the instrumentation libraries you already use. [Get started →](/python/bitfrost) * **Standalone-first.** Capture to your terminal, a JSONL file, or SQLite with zero network calls — Voight is one optional backend, never a requirement. Ships five backends, four auto-instrument helpers, a rich CLI, an interactive TUI, and an offline local web dashboard (`bitfrost serve`). * **Private by default.** Three privacy levels with in-process PII scrubbing (12 patterns + credit-card Luhn) applied before any event leaves the process. Bitfrost is open source (MIT) on [GitHub](https://github.com/Voightxyz/bitfrost) and works entirely on its own. Point it at Voight only when you want hosted dashboards and per-user spend. ## Week of 2026-05-18 — Vercel AI SDK support, install wizard, GLM provider A short recap of what shipped this week. Per-package version notes live in the sections below. ### New * **`@voightxyz/vercel-ai` 0.1.0** — first stable release of the [Vercel AI SDK exporter](/ai-apps/vercel-ai). Register one OpenTelemetry exporter next to `@vercel/otel` and every `streamText` / `generateText` / `streamObject` / `generateObject` call lands in your dashboard with accurate token counts, tool calls, and per-user attribution. Validated against the `vercel/ai-chatbot` reference app. * **`@voightxyz/sdk init` now detects the Vercel AI SDK** — running `npx -y @voightxyz/sdk init` in a project that uses `ai` writes an `instrumentation.ts` that wires up the exporter automatically. Direct-wrapper projects (OpenAI / Anthropic only) get the same files as before. See the [install wizard guide](/ai-apps/wizard). * **GLM (Z.ai / Zhipu) provider page** — new [GLM provider doc](/ai-apps/glm) walks through using GLM models behind the OpenAI wrapper, with pricing and routing notes. ### Updates * **One-line per-user attribution** — both `@voightxyz/openai` and `@voightxyz/anthropic` now accept a `tags` map on `withTrace`. Pass `{ userId, plan, ... }` once at the request boundary and every LLM call inside the block is stamped automatically. Lights up the dashboard's [Users sub-tab](/ai-apps/overview#users-sub-tab) and [per-user spend](/concepts/per-user-spend) view. * **Request-boundary trace grouping** — `withTrace(fn, { routeTag })` and `log(message, extra?)` are now exported from both App SDKs. Group every LLM call from one request under a single trace card, interleaved with your own log lines. Mixing OpenAI and Anthropic calls in the same trace works without extra setup. * **`@voightxyz/anthropic` peer range loosened to `>=0.27.0`** — projects pinning older Anthropic SDK releases no longer hit `ERESOLVE` on install. Runtime verified against `0.29.2` and `0.96.0`. ### Fixes * **Vercel AI exporter token accuracy** — outer wrapper spans are filtered out so token counts match the underlying provider's billing, and `durationMs` is rounded to an integer. Already on the OpenAI or Anthropic wrapper? Adding per-user attribution is a one-line change — see the [per-user spend guide](/concepts/per-user-spend). Four SDKs ship from separate packages: * **`@voightxyz/sdk`** — coding-agent hooks + library mode (autonomous bots) * **`@voightxyz/openai`** — OpenAI Node SDK wrapper for production apps * **`@voightxyz/anthropic`** — Anthropic Node SDK wrapper for production apps * **`@voightxyz/vercel-ai`** — OpenTelemetry SpanExporter for the Vercel AI SDK ## `@voightxyz/vercel-ai` changelog ### 0.1.1 — 2026-05-22 **Dedup filter for wrapper-emitted spans.** When [`@voightxyz/openai`](https://www.npmjs.com/package/@voightxyz/openai) or [`@voightxyz/anthropic`](https://www.npmjs.com/package/@voightxyz/anthropic) is wrapped with `otel: true`, every captured call would otherwise land twice in the Voight backend — once via the wrapper's direct POST, once via this exporter. 0.1.1 teaches `VoightExporter.export()` to recognise the `voight.source: 'wrapper'` attribute the wrappers stamp on those spans, and skip them cleanly. Other OTel exporters in the same process (Langfuse, Datadog, Sentry) still see the spans — the dedup is scoped to the Voight-to-Voight loop. Spans without that marker (the canonical `streamText` / `generateText` / `streamObject` / `generateObject` spans the Vercel AI SDK emits) are unaffected. ### 0.1.0 — 2026-05-21 **First stable release.** `@voightxyz/vercel-ai` is an OpenTelemetry `SpanExporter` for the [Vercel AI SDK](https://sdk.vercel.ai). Register it next to `@vercel/otel`, flip `experimental_telemetry: { isEnabled: true }` on your `streamText` / `generateText` / `streamObject` / `generateObject` calls, and every call lands in your Voight dashboard. ```ts theme={null} import { registerOTel } from '@vercel/otel' import { VoightExporter } from '@voightxyz/vercel-ai' registerOTel({ serviceName: 'my-app', traceExporter: new VoightExporter({ agent: 'production-chat-api' }), }) ``` **What lands today:** * `streamText` / `generateText` / `streamObject` / `generateObject` capture (one event per LLM call — outer wrappers filtered out so token counts stay accurate) * OpenAI + Anthropic provider attribution (via `gen_ai.system` / `ai.model.provider`) * Tool calls (normalised to `{id, name, arguments}` JSON-string), token counts (input / output / cache\_read / cache\_creation), finish reason, streaming flag, sessionId * **Per-user attribution** via `experimental_telemetry.metadata` — passing `{ metadata: { userId, plan, ... } }` activates the dashboard Users sub-tab + per-tag filter pills automatically * Three privacy levels (`'minimal'` / `'standard'` / `'full'`) sharing the same 12-pattern PII catalogue as the other wrappers * Coexists with other observability tools via OTel's `MultiSpanProcessor` Validated end-to-end against the [`vercel/ai-chatbot`](https://github.com/vercel/ai-chatbot) reference app. Full setup at [ai-apps/vercel-ai](/ai-apps/vercel-ai). ## `@voightxyz/openai` changelog ### 0.1.7 — 2026-05-22 **`{ otel: true }` opt-in — OpenTelemetry side-channel.** When set, every captured call is *also* emitted as an OpenTelemetry span with `gen_ai.*` semantic-convention attributes (plus the parallel `ai.*` namespace for Vercel cross-compatibility). The wrapper picks up whichever `TracerProvider` is registered in the host process, so you can wire it inside an OTel-mandated stack (Langfuse, Phoenix, Datadog, Sentry, [`@voightxyz/vercel-ai`](/ai-apps/vercel-ai)) without losing the direct ingestion to `api.voight.xyz`. ```ts theme={null} const client = wrapOpenAI(new OpenAI(), { agent: 'my-app', otel: true }) ``` Span name: `voight.openai.chat` (or `voight.openai.responses`). Each span carries `voight.source: 'wrapper'` so `@voightxyz/vercel-ai` ≥ 0.1.1 deduplicates them automatically. `@opentelemetry/api` is now an **optional** peer dependency. Default `otel: false` ships byte-identical behaviour to 0.1.6 — no extra install, no runtime cost. ### 0.1.6 — 2026-05-17 **`withTrace({ tags })` — per-user / per-tenant attribution.** `withTrace` now accepts a `tags` map alongside `routeTag`. Every wrapped LLM call inside the block gets stamped with `metadata.tags` automatically via `AsyncLocalStorage`. This is the one-line code change that lights up the dashboard's [Users sub-tab](/ai-apps/overview#users-sub-tab) and powers [per-user spend](/concepts/per-user-spend) attribution. ```ts theme={null} await withTrace( async () => { /* handler */ }, { tags: { userId: req.user.id, plan: req.user.plan } }, ) ``` Tags propagate to both Chat Completions and Responses events. Missing or empty tags are a no-op — no behaviour change for callers not using the feature. ### 0.1.5 — 2026-05-17 **`withTrace` + `log` — request-boundary trace grouping.** New exports: * `withTrace(fn, { routeTag })` opens a logical trace; every wrapped LLM call inside the block is grouped under one trace card in the dashboard. * `log(message, extra?)` emits a free-form event inside the current trace — interleaved with LLM calls in the timeline. Backed by Node's `AsyncLocalStorage` — no signature threading, no middleware required. Mixes cleanly with `@voightxyz/anthropic` (same backing store across both packages). ### 0.1.0 — 2026-05-15 **Initial release.** OpenAI Node SDK wrapper. Captures: * Chat Completions (`client.chat.completions.create`) — non-streaming + streaming, tool calling * Responses API (`client.responses.create`) — non-streaming + streaming, function calling, reasoning models * Token breakdown (input / output / cache reads / reasoning) * Tool calls (full array per event) * Latency, finish reason, error capture Three privacy levels (Minimal / Standard / Full) with local PII scrubbing on Standard. ## `@voightxyz/anthropic` changelog ### 0.1.8 — 2026-05-22 **`{ otel: true }` opt-in — OpenTelemetry side-channel.** Mirror of [`@voightxyz/openai@0.1.7`](#017--2026-05-22) — same flag, same `gen_ai.*` + `ai.*` attribute shape, same `voight.source: 'wrapper'` dedup marker, same optional peer on `@opentelemetry/api`. Span name: `voight.anthropic.messages`. `gen_ai.system` resolves to `'anthropic'`. Default `otel: false` ships byte-identical behaviour to 0.1.7. ### 0.1.7 — 2026-05-17 **Loosen `@anthropic-ai/sdk` peer-dependency from `>=0.30.0` to `>=0.27.0`.** Real-world projects on older SDK versions hit `ERESOLVE` on install (Anthropic's own `customer-support-agent` quickstart pins `^0.27.1`; `financial-data-analyst` pinned `^0.29.0` — both rejected `0.1.6`). The `0.30.0` floor was overly conservative — the wrapper consumes the SDK purely structurally (no type imports, only runtime field reads with defensive fallbacks), so older releases work fine. Runtime verified against `@anthropic-ai/sdk` 0.29.2 and 0.96.0. 92/92 tests still green. No code changes — peer-dependency metadata only. ### 0.1.6 — 2026-05-17 **`withTrace({ tags })` — per-user / per-tenant attribution.** Mirror of `@voightxyz/openai` 0.1.6. Every wrapped `messages.create` call inside a `withTrace` block gets `metadata.tags` stamped automatically. Powers the dashboard's [Users sub-tab](/ai-apps/overview#users-sub-tab) and [per-user spend](/concepts/per-user-spend) attribution. ```ts theme={null} await withTrace( async () => { /* handler */ }, { tags: { userId: req.user.id, plan: req.user.plan } }, ) ``` ### 0.1.5 — 2026-05-17 **`withTrace` + `log` — request-boundary trace grouping.** Mirror of `@voightxyz/openai` 0.1.5. Same async-context store under the hood — import `withTrace` from either package; mixing providers in one trace works without setup. ### 0.1.0 — 2026-05-15 **Initial release.** Anthropic Node SDK wrapper. Captures: * `messages.create` — non-streaming + streaming, tool use * Token breakdown (input / output / cache reads / cache creations) * Path-A cache pricing (1.25× creation, 0.10× read applied server-side) * Tool calls (flattened to `{ id, name, arguments }` matching the OpenAI wrapper) * Latency, stop reason, error capture Three privacy levels (Minimal / Standard / Full) with local PII scrubbing on Standard. ## `@voightxyz/sdk` changelog ### 0.6.5 — 2026-05-21 **`init` wizard now detects the Vercel AI SDK.** If your `package.json` lists `ai` (Vercel AI SDK), the wizard takes a new third path: instead of writing `src/lib/voight.ts` with wrapped clients, it writes an `instrumentation.ts` that registers [`@voightxyz/vercel-ai`](/ai-apps/vercel-ai) with `@vercel/otel`. The Vercel AI SDK emits OpenTelemetry spans natively, so every `streamText` / `generateText` / `streamObject` / `generateObject` call carrying `experimental_telemetry: { isEnabled: true }` lands in your Voight dashboard with the same shape as direct-wrapper events. Mixed projects (both `ai` and a direct provider in deps) take the Vercel path with an explicit message — the OTel exporter covers every underlying provider via `gen_ai.*` semconv normalisation. Strictly additive: zero edits to the `setup` flow (Claude Code / Cursor / Codex hooks) or to the direct-wrapper path. A project that has only `openai` or `@anthropic-ai/sdk` (no `ai`) gets exactly the same files the 0.6.4 wizard produced — verified byte-for-byte via the registry-level smoke test before `@latest` promotion. Same install command (`npx -y @voightxyz/sdk init`), same prompts (privacy + key + agent), framework-aware usage snippet, pm-tailored install command (`pnpm add @voightxyz/vercel-ai @vercel/otel` for the Vercel path). ### 0.6.4 — 2026-05-17 **New `init` sub-command — scaffold Voight into a production app with one wizard.** For projects that already use the OpenAI or Anthropic Node SDK in production, `init` automates the install: ```bash theme={null} cd your-app npx -y @voightxyz/sdk init ``` The wizard: 1. Reads `package.json` and detects `openai` and/or `@anthropic-ai/sdk` already in your deps. Aborts cleanly if neither is present. 2. Detects your package manager (pnpm / yarn / bun / npm) and your framework (Next.js or vanilla) to tailor the install command + usage snippet at the end. 3. Prompts for privacy level (Standard recommended for customer-facing apps), Voight API key, and agent name. 4. Validates the key against `api.voight.xyz` before writing anything — typo'd keys get caught now instead of in production. 5. Generates `src/lib/voight.ts` with the wrapped clients for the providers it detected, plus a header comment explaining how to adjust if your provider keys come from AWS Secrets Manager / Vault / Doppler instead of env vars. 6. Appends `VOIGHT_KEY` to `.env.local`. Provider keys (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`) are never asked for — they belong to your app, the Voight wrapper never sees them. 7. Prints the right install command for your package manager + a usage snippet (Next.js Route Handler or Express, depending on what's in your deps). ### Safety guarantees `init` lives in a new file `src/init.ts` with **zero edits to `setup.ts`, `hook.ts`, or any other module**. It never touches `~/.claude`, `~/.cursor`, or `~/.codex` — only writes inside `process.cwd()`. Users of `setup` get byte-identical behaviour to 0.6.3; 256 tests green (was 214 + 42 new for `init`). If you'd rather wire things up manually (no wizard), the package pages for [OpenAI](/ai-apps/openai) and [Anthropic](/ai-apps/anthropic) walk through the same install in three commands. ### 0.6.3 — 2026-05-14 **Fix: Codex plugin install lands in the right cache dir + valid auth enum.** Two last-mile Codex setup bugs. The plugin now installs into the location Codex actually loads from, and the authentication value matches Codex's accepted enum so the wrapper no longer rejects it at startup. ### 0.6.2 — 2026-05-14 **Fix: install the SDK locally so hooks fire inside the Codex sandbox.** Codex runs hooks inside a sandboxed environment that can't reach globally-installed packages. Setup now drops the SDK into the project so the hook script resolves correctly on first run. ### 0.6.1 — 2026-05-14 **Fix: `VOIGHT_SOURCE` disambiguates Codex from Claude Code.** Both adapters share a hook entry point. The wrapper now sets `VOIGHT_SOURCE` so events route through the correct mapper instead of being misattributed. ### 0.6.0 — 2026-05-14 **Codex support (verified target).** Voight now wires into [Codex](/quickstart) the same way it wires into Claude Code and Cursor — one `setup` command drops a local marketplace plugin that registers the hooks. Events show up in the live timeline with the same shape as other targets. See the [quickstart](/quickstart) for the install steps. ### 0.5.2 — 2026-05-14 **Fix: route Cursor response and thought text through metadata.** Cursor's response and reasoning fields are now carried in `metadata` instead of top-level event fields. The dashboard masks them by default with the same eye-toggle reveal used for Claude Code transcripts. ### 0.5.1 — 2026-05-14 **Fix: normalise Cursor `model: 'default'` to `cursor-auto`.** Cursor reports `default` for its auto-routing mode, which doesn't tell you which model actually ran the turn. The SDK now rewrites it to `cursor-auto` so the dashboard's model column is honest about what happened. ### 0.5.0 — 2026-05-14 **Cursor support + framework-agnostic setup.** Voight is no longer Claude-Code-only. Highlights: * **Cursor adapter** — `setup` wires Cursor's `hooks.json` + wrapper script, captures 5 extended hook events, and ships prompts and outcomes in the reasoning payload so the timeline renders Cursor turns end-to-end * **Auto-detect the coding agent** — run `setup` with no `--target` flag and the CLI picks Claude Code, Cursor, or Codex based on your environment * **`frameworkName` helper** — installer and runtime messages now reflect whichever agent you're wiring in * **`traceId` lifted into metadata** — every event carries the trace ID the dashboard groups by, regardless of source * **Apache-2.0** — the SDK is now Apache-2.0 licensed (was MIT). README rewritten to drop Claude-only framing See the [quickstart](/quickstart) for the new multi-target flow and [events](/concepts/events) for the shared payload shape. ### 0.4.2 — 2026-05-10 **Fix: explicit `--privacy=N` example in non-TTY welcome menu.** After 0.4.1 shipped, an alpha tester's friend tried to install via Claude Code chat and the AI agent reading the wizard output didn't know how to chain commands. The welcome menu now includes: ``` Pick a number (1, 2, or 3) or a name (minimal / standard / full), then re-run with that choice as a flag. For example, to pick Standard: npx -y @voightxyz/sdk setup --privacy=2 ``` Six lines of code, unblocks the AI-agent install flow. ### 0.4.1 — 2026-05-08 **3-step progressive flow for non-TTY (Claude Code chat).** The 0.4.0 wizard relied on readline. Inside Claude Code chat (or any non-TTY shell) it silently defaulted to `standard` privacy, never showed the welcome menu, and gave users no way to choose. 0.4.1 reshapes the non-TTY path: * Step 1 — print welcome + privacy menu, exit * Step 2 — accept `--privacy=N`, print API key instructions, exit * Step 3 — accept both `--privacy` and `--key`, write settings.json, show done message Each step prints clear instructions. AI agents in chat can chain them automatically. Also dropped the "Restart Claude Code" line from the done message — Claude Code hot-reloads `settings.json` on most modern versions. ### 0.4.0 — 2026-05-08 **3-level privacy capture + local PII scrubbing.** The biggest release since the SDK was first published. New capabilities: * **Three privacy levels** picked at install (Minimal / Standard / Full), via the setup wizard or `VOIGHT_PRIVACY` env var * **`scrubPii()`** — 12 PII patterns + Luhn-validated credit cards, run on every string in every event under Standard mode, before the SDK transmits anything * **`applyPrivacy(payload, level)`** — single integration point in `hook.ts`, allowlist-based for Minimal (forward-compat), deep-scrub for Standard * **Per-event `metadata.privacyLevel` stamp** — every event ships with its level chip so the dashboard can audit retroactively * **77 new unit tests** (61 → 138 SDK suite) * **E2E verified** against the actual subprocess flow + wire capture, not just unit mocks See [privacy overview](/privacy/overview) and [PII patterns](/privacy/pii-patterns). ### 0.3.10 — 2026-05-06 **Fix: match real Claude Code rejection phrasing in denial detector.** The 0.3.9 denial-detection patterns were synthetic. 0.3.10 corrects them against captured rejection logs. **Architectural caveat**: PostToolUse doesn't fire on user\_rejected — that path is dead code in production. We're sitting on this until we can capture the right hook flow. ### 0.3.9 — 2026-05-06 **Permission-denial classification (5 patterns).** Added a denial classifier that promotes failed tool calls to a distinct `denial` type when the failure shape matches a known pattern (user rejected, settings blocked, sandbox denied, requires approval, hook blocked). The dashboard renders these with a `DENIED` badge separate from real runtime errors. **Post-mortem**: shipped against invented patterns. See architectural caveat above. ### 0.3.8 — 2026-05-05 **Git context capture.** Every event now carries `metadata.git = { branch, sha, shortSha, remote, dirty }` when running inside a git repo. Cached 30s per cwd to avoid spawning git on every event. Captures the exact code state that produced the trace. ### 0.3.7 — 2026-05-05 **`ScheduleWakeup` recognition.** When the agent calls `ScheduleWakeup`, the SDK records the parameters. The next `UserPromptSubmit` that matches gets tagged `promptSource: 'system'` so the dashboard can distinguish autonomous loop ticks from real user prompts. Renders as a `SYSTEM` badge in the timeline. ### 0.3.5 — 2026-05-05 **Agent response capture.** The Stop hook now reads the local transcript backward to find the agent's final text + stop reason + last thinking block. Surfaces them in the dashboard's event detail panel (masked by default, eye toggle to reveal). ### 0.3.4 — 2026-05-04 **Marker file (`.voight-agent-id`) for rename-proof identity.** Server returns the agent's CUID on every event response. SDK writes it to `/.voight-agent-id`. Future events match by primary key — rename-proof, folder-rename-proof. ### 0.3.3 — 2026-05-04 **Explicit `metadata.tokensBreakdown` for exact pricing.** The flat `tokens.input` field conflates `inputBase`, `cacheCreation`, and `cacheRead` at the full rate. New breakdown field gives the backend exact prices (cache\_read at 0.10×, cache\_creation at 1.25×, input at 1.00×). ### 0.3.2 — 2026-05-04 **ESM fs imports (fix silent fail on transcripts >4MB).** ### 0.3.1 — 2026-05-03 **Lenient transcript matcher** (handles edge cases in attribution). ### 0.3.0 — 2026-05-03 **Transcript-based token capture.** The SDK now walks the local transcript JSONL on every PostToolUse, finds the assistant message that initiated the current tool, and pulls real token usage. Dedup via assistant message UUID — 1 message → N tools attributes once. ## Earlier versions 0.1.x and 0.2.x were the initial integration drafts. Not documented here — start at 0.3.0. ## Stay updated * npm: [`@voightxyz/sdk`](https://www.npmjs.com/package/@voightxyz/sdk) · [`@voightxyz/openai`](https://www.npmjs.com/package/@voightxyz/openai) · [`@voightxyz/anthropic`](https://www.npmjs.com/package/@voightxyz/anthropic) * GitHub: [`voightxyz/voight-sdk`](https://github.com/Voightxyz/voight-sdk) · [`voightxyz/voight-openai`](https://github.com/Voightxyz/voight-openai) · [`voightxyz/voight-anthropic`](https://github.com/Voightxyz/voight-anthropic) (releases tagged) * Twitter: [@voightxyz](https://x.com/voightxyz) # Agents Source: https://docs.voight.xyz/concepts/agents What an agent is in Voight, how identity is tracked, and how multi-agent setups work. An **agent** is any entity that produces events — a coding agent session, an autonomous bot, an ElizaOS character, anything that calls `voight.log()` or has the SDK wired in. ## Agent identity Each agent has a unique `id` (a [cuid](https://github.com/paralleldrive/cuid2)) assigned by the server the first time it's seen. From that point on, the SDK matches events to that agent by `id`, not by name — meaning **renaming or moving the project folder doesn't break the timeline**. How identity is resolved by the SDK, in priority order: 1. **`VOIGHT_AGENT_ID` env override** — explicit, highest priority. Useful for CI and multi-agent setups where identity is controlled from outside. 2. **`.voight-agent-id` marker file** — a plain-text file in the project's cwd holding the cuid. Written by the SDK after the first successful event so subsequent events match by primary key. 3. **Framework label fallback** — `claude-code:` for first-time installs, `claude-code:` if cwd is unavailable. The server normalises: known cuid → match by `id`; otherwise try `snsName`, then `displayName`, then create a new agent row. ## How agents are created Three paths: 1. **Tool-emitted** — the SDK auto-creates an agent the first time it sees an event from a project (the default for Claude Code, Cursor, Codex). 2. **SDK-instrumented** — you import `@voightxyz/sdk` in your code and explicitly pass an `agentId` to the constructor. 3. **Imported from the public Explorer** (part of Voight Agents, the web3 product): agents registered in the Solana [Agent Registry](https://solscan.io/account/8oo4dC4JvBLwy5tGgiH3WwK4B9PWxL9Z4XjA2jzkQMbQ) are imported by Voight's indexer with `source='agent-registry'`. The observability platform never requires this. ## Agent metadata Each agent carries two metadata blobs: * **`metadata` (registry-derived)**: written by the Explorer indexer for agents imported from the public registry. Includes `agentUri` (external pointer), `feedback_count`, `response_count`, `revoke_count`. * **`enriched`**: written by a separate metadata-enrichment worker that fetches the `agentUri` JSON. Includes display name, avatar, capabilities, services declared. These two are intentionally separated: two different workers write them, they never race. ## Multi-agent setups You can run as many agents as you want. The dashboard aggregates across all of them while letting you filter per-agent. Common multi-agent patterns: * **Per-project agent** — every Claude Code session in a different repo becomes its own agent. Default behaviour. * **Per-bot agent** — a long-running autonomous bot gets one stable `agentId` (e.g. SNS name `trading-bot.sol`). * **Per-flow agent** — split an autonomous system into multiple agents by responsibility (e.g. `market-watcher.sol`, `executor.sol`). ## Agents on the public Explorer (Voight Agents) The [public Explorer](https://voight.xyz/explore) belongs to Voight Agents, the web3 product. It indexes every agent registered in the Solana Agent Registry, including ones not using Voight for telemetry, and cross-references them with yours. Registering an agent there is optional and is never required by the observability platform. Agents stay private to your dashboard unless you opt in to the Explorer by registering them. ## Soft delete + isolation When you delete an agent from the dashboard: * The agent is soft-deleted (`Agent.deletedAt` timestamp set, row not removed) * Subsequent events ingested for that `agentId` return `410 Gone` to the SDK * The agent disappears from all dashboard reads (events, sessions, traces, errors, alerts) * **Other agents from the same user keep ingesting normally** — isolation is per-agent, not per-API-key The delete flow requires typing the agent's displayName to confirm. No undo. ## Next * [Events](/concepts/events) — the atomic unit; every prompt, tool call, decision, transaction is one event * [Traces](/concepts/traces) — events grouped by `traceId`, one trace per agent turn * [Sessions](/concepts/sessions) — traces grouped by `sessionId`, one session per process lifetime # Events Source: https://docs.voight.xyz/concepts/events The atomic unit of Voight — every agent action is one event. An **event** is the smallest unit of data Voight captures. Every prompt, tool call, decision, transaction, error, or anomaly your agent produces is one event row. ## Event types The `type` field is one of: | Type | When it fires | | - | - | | `decision` | Reasoning step, user prompt, agent finishing a turn. Anything that isn't a tool execution or transaction. | | `action` | A tool call was attempted or completed (PreToolUse / PostToolUse from Claude Code). | | `tool` | Same as `action` — legacy label, library mode often uses this. | | `tx` | A transaction was signed and submitted, for agents that move funds. | | `reasoning` | Free-form thinking trace, often paired with tool calls. | | `error` | A tool failed, or the agent crashed. | The SDK chooses the right type automatically based on what fired the hook. ## Event payload Every event includes: ```ts theme={null} { agentId: string // stable agent identifier (CUID or label) timestamp: string // agent-supplied ISO timestamp type: EventType // see above reasoning?: string // human-readable description toolExecuted?: string // name of the tool, if applicable toolsConsidered?: string[] // alternatives the agent rejected transaction?: string // transaction signature or reference, if applicable amount?: { token, value } // payment / swap amount outcome?: 'pending' | 'success' | 'failed' durationMs?: number // for PostToolUse events errorMessage?: string // raw error text when outcome=failed model?: string // LLM model used (claude-opus-4-7, etc.) tokens?: { input, output, total } metadata?: { // free-form additional data source: 'claude-code' | 'cursor' | 'sdk' | ... hookEvent?: 'PreToolUse' | 'PostToolUse' | 'Stop' | ... sessionId?: string traceId?: string cwd?: string git?: { branch, sha, remote, dirty } detail?: { /* per-tool structured detail */ } tokensBreakdown?: { inputBase, cacheCreation, cacheRead, output } privacyLevel?: 'minimal' | 'standard' | 'full' // anything else you want to attach } } ``` ## What gets captured at each privacy level | Field | Minimal | Standard | Full | | - | :-: | :-: | :-: | | `toolExecuted`, `outcome`, `durationMs` | ✓ | ✓ | ✓ | | `tokens`, `metadata.tokensBreakdown`, `model` | ✓ | ✓ | ✓ | | `metadata.sessionId`, `metadata.traceId`, `metadata.phase` | ✓ | ✓ | ✓ | | `reasoning`, `metadata.detail` | ✗ | ✓ scrubbed | ✓ raw | | `errorMessage`, `metadata.response_preview` | ✗ | ✓ scrubbed | ✓ raw | | `metadata.cwd`, `metadata.git` | ✗ | ✓ | ✓ | | `input.prompt`, Stop event `responseText`/`thinkingPreview` | ✗ | ✓ scrubbed | ✓ raw | Token counts and USD spend are pure numerics — they pass through every level unchanged. The dashboard's KPIs and charts work identically regardless of which level you pick. See the [privacy overview](/privacy/overview) for the full breakdown. ## Cost attribution Voight computes USD cost per event from the token breakdown: * **Path A — exact** (`metadata.tokensBreakdown` from SDK 0.3.3+): each token flavour priced at its real rate. `cacheRead` at 0.10× base input, `cacheCreation` at 1.25× base, `inputBase` at 1.00×, `output` at the output rate. * **Path B — heuristic** (`metadata.tokens` with `source: 'claude-code'`): assumes 95% of input is `cache_read`, applies approximate discount. Used as fallback for older SDK versions. * **Path C — flat** (library callers): no breakdown, prices `input` and `output` at full rates. Pricing tables are hard-coded per model (Anthropic, OpenAI, Google) and verified against the providers' published docs. ## Anomaly detection A scheduler runs every 5 minutes and computes 4 anomaly rules per agent over the last 7 days: | Rule | Trigger | | - | - | | `event_surge` | Events in last hour > 3× the 7-day hourly baseline (autonomous agents only — coding sessions are too bursty) | | `error_rate_high` | ≥10% failed runs in 1h with ≥5 completed runs | | `cost_spike` | Today's spend ≥3× the daily 7-day average AND today's spend ≥\$1 | | `orphan_spike` | ≥20% orphaned PreToolUse events with no matching PostToolUse, AND ≥2× baseline | When triggered, an `Alert` row is created/updated with severity (low / medium / high based on deviation), dedup by `|` fingerprint. Alerts have a full lifecycle: `triggeredAt`, `lastTriggeredAt`, `acknowledgedAt`, `snoozedUntil`, `resolvedAt`. Alert delivery channels (Telegram, email, webhooks) ship in **v1.0**. Today alerts persist + surface in the dashboard's Alerts page. ## Pre/Post pairing Each tool call fires two hook events from Claude Code: * `PreToolUse` (before execution) — outcome `pending`, fingerprint `sessionId|toolName|toolInput` * `PostToolUse` (after execution) — same fingerprint, with `durationMs` and `outcome` filled in The dashboard pairs them by fingerprint and renders one row per tool call, with the Pre suppressed if the Post arrived. If a `PreToolUse` is orphaned for >120s (no matching Post), Voight classifies it as `orphanedPre` and counts it toward the error rate. ## Next * [Traces](/concepts/traces) — events grouped by `traceId`, one trace = one agent turn * [Sessions](/concepts/sessions) — events grouped by `sessionId`, one session = one process lifetime # Per-user spend Source: https://docs.voight.xyz/concepts/per-user-spend Track AI cost and traffic per end-user — one line of code, GDPR-safe, works with any auth system. If you sell to other developers shipping an AI product (Auth0 of LLMs, multi-tenant SaaS, B2B copilots), the question your customers ask first is: > *"What's the cost per end-user this month?"* Per-user spend is the answer. **One line of code at your request boundary** tags every LLM call with the user it belongs to. The dashboard's **Users sub-tab** then aggregates spend, traces, tokens, and tool use per user — without you handing Voight any PII you don't already own. ## The 1-line pattern Wrap your provider client once (the [OpenAI](/ai-apps/openai) or [Anthropic](/ai-apps/anthropic) SDK), then at your request boundary call `withTrace` and pass a `tags.userId`: ```ts theme={null} import { withTrace } from '@voightxyz/openai' // or '@voightxyz/anthropic' app.post('/api/chat', async (req, res) => { await withTrace( async () => { // your existing handler — every LLM call inside this block // is automatically tagged with userId, plan, org, etc. const reply = await openai.chat.completions.create({ ... }) res.json({ reply }) }, { routeTag: 'POST /api/chat', tags: { userId: req.user.id, // ← the one line plan: req.user.plan, org: req.user.organizationId, }, }, ) }) ``` That's it. The wrapper stamps `metadata.tags = { userId, plan, org }` on every event inside the `withTrace` block via `AsyncLocalStorage` — no manual passing through call sites, no middleware to wire. ## Why this design We considered three alternatives and rejected them: | Pattern | Why we didn't | Why explicit tagging won | | - | - | - | | Auto-detect user from request headers | Fragile (every auth system different), and means Voight reads your auth context | You stay in control of what's tagged | | Require a "Voight user object" via dashboard | Forces a sync flow, breaks for SSO/SCIM apps | Zero coupling to user-management | | Capture client IP / fingerprint | GDPR landmine, useless for B2B SaaS | Tags are your IDs, not ours | **You decide what `userId` is** — a database ID, an Auth0 `sub`, a Clerk `user.id`, a hashed email, anything. Voight stores it as a tag string. We never see the underlying user record. ## Examples for common auth systems ```ts theme={null} import { withTrace } from '@voightxyz/openai' import { auth } from 'express-oauth2-jwt-bearer' app.use(auth({ audience: 'api', issuerBaseURL: '...' })) app.post('/api/chat', async (req, res) => { await withTrace( async () => { /* your handler */ }, { routeTag: 'POST /api/chat', tags: { userId: req.auth.payload.sub, // "auth0|abc123" org: req.auth.payload['https://app/org_id'], }, }, ) }) ``` ```ts theme={null} import { withTrace } from '@voightxyz/openai' import { auth } from '@clerk/nextjs/server' export async function POST(req: Request) { const { userId, orgId } = await auth() return withTrace( async () => { /* your handler */ }, { routeTag: 'POST /api/chat', tags: { userId, org: orgId, plan: req.headers.get('x-plan') }, }, ) } ``` ```ts theme={null} import { withTrace } from '@voightxyz/openai' import { auth } from '@/auth' export async function POST(req: Request) { const session = await auth() return withTrace( async () => { /* your handler */ }, { routeTag: 'POST /api/chat', tags: { userId: session?.user?.id ?? 'anonymous', plan: session?.user?.plan ?? 'free', }, }, ) } ``` ```ts theme={null} import { withTrace } from '@voightxyz/openai' import jwt from 'jsonwebtoken' app.post('/api/chat', async (req, res) => { const token = req.headers.authorization?.replace('Bearer ', '') const claims = jwt.verify(token, process.env.JWT_SECRET) as any await withTrace( async () => { /* your handler */ }, { routeTag: 'POST /api/chat', tags: { userId: claims.sub, plan: claims.plan }, }, ) }) ``` ```ts theme={null} // Public marketing assistant — no user, but you still want // per-session attribution and abuse signals. await withTrace( async () => { /* handler */ }, { routeTag: 'POST /api/public-chat', tags: { userId: `anon_${req.cookies.session_id}`, // stable per browser source: 'marketing-site', }, }, ) ``` The Users sub-tab will list anonymous users alongside authenticated ones, prefixed by your chosen tag value. You decide the convention. ## What you see in the dashboard Open the [Users sub-tab](https://voight.xyz/dashboard/ai-apps) under **AI Apps**. You get one row per unique `userId` value, sorted by spend (descending by default): | Column | Aggregated from | | - | - | | **User** | `metadata.tags.userId` — your tag value, shown verbatim | | **Spend (USD)** | Sum of cost per event for that user in the time window | | **Traces** | Number of `withTrace` blocks that user triggered | | **Tokens** | Input + output + cache reads + cache creations | | **Last seen** | `MAX(timestamp)` for any event tagged with that user | | **Top model** | The model that user spent the most on | Click any row → drilldown to that user's traces (every `withTrace` block they triggered), then into individual events (every LLM call, with prompt, response, tokens, latency). The **User filter pill** in the top bar lets you narrow the entire AI Apps section (Overview, Traces, Models, Tools) to one user — same way the Provider and Agent pills work. ## Empty state — why don't I see any users? If you've wired the wrapper but the Users tab shows zero users, it's almost always one of these: 1. **You're not calling `withTrace`** — the wrapper captures everything, but `tags` only flow into events that ran inside a `withTrace` block. Calls made outside it have no user attribution. Wrap your request boundary. 2. **You're not passing `tags.userId`** — `withTrace` accepts an optional `tags` map. Without `userId`, the event has no user to attribute to. 3. **Time window** — the Users tab respects the dashboard's window selector (default 28 days). Switch to 7d or 1d if you only deployed today. 4. **Privacy: minimal** — Minimal mode strips prompts and responses but keeps tokens, costs, and tags. Users tab works in Minimal. If you set up something stricter (custom redaction layer), confirm your tags aren't being filtered out. A typical fix is one line of code (the `tags.userId` field on `withTrace`). After your next request the user appears in the dashboard within \~2 seconds. ## Multiple tags, multiple dimensions `tags` is a flat string-to-string map. You can attribute by any dimension you care about: ```ts theme={null} tags: { userId: req.user.id, plan: req.user.plan, // 'free' / 'pro' / 'enterprise' org: req.user.organizationId, // multi-tenant feature: 'summarize', // which feature triggered this LLM call env: process.env.NODE_ENV, // 'production' vs 'staging' region: process.env.FLY_REGION, } ``` The dashboard surfaces `userId` as the primary user dimension (drives the Users sub-tab). The other tags are queryable via the API — `GET /v1/me/ai-apps/overview?tag.plan=pro` returns metrics scoped to Pro users only. Future dashboard releases will surface tag-based segmentation natively (cost-per-plan, cost-per-feature). ## FAQ Tag values are stored verbatim as strings. **Choose what to send accordingly.** * ✅ Database IDs, Auth0 `sub`, Clerk IDs — these are pseudonymous identifiers, not personal data on their own * ✅ Hashed emails (SHA-256), opaque session IDs * ⚠️ Raw email addresses, names, IPs — personal data; only send if your DPA covers it * ⚠️ Anonymous user fingerprints — may be personal data depending on jurisdiction A user-deletion request maps to deleting all events tagged with that user. The [Data handling](/privacy/data-handling) page covers retention and deletion in detail. Yes — that's the primary use case. Tag every request with `userId` (the end-user inside your customer's tenant) **and** `org` (the customer tenant). Then: * Filter the dashboard by `org=acme-corp` to see one tenant's traffic * Drill into individual users inside that tenant * Export per-tenant cost reports via `GET /v1/me/ai-apps/users?tag.org=acme-corp` Each tenant gets a Voight account if they want their own dashboard, but you can also operate a single Voight account for all customers and surface per-tenant views internally. If your app is public (a marketing site assistant, a free demo), still call `withTrace` — just use a stable browser-scoped identifier for `userId`: ```ts theme={null} tags: { userId: `anon_${cookies.sessionId}` } ``` This gives you abuse signals (one anon hammering 1000 calls/hour) and per-session cost visibility without auth. No. `withTrace` uses Node's `AsyncLocalStorage` to maintain the tag scope — no synchronous overhead, no extra network calls. Tags are appended to the event payload that the wrapper was going to send anyway. The captured numbers are identical to what you'd see without tags. No — `withTrace` runs server-side (where the wrapped OpenAI / Anthropic client lives). The tag value is set once at the request boundary in your backend and propagates to every LLM call that handler triggers. If you have a serverless setup (Next.js Route Handlers, Vercel Functions, AWS Lambda), `withTrace` still works — `AsyncLocalStorage` is part of Node's runtime, not tied to long-lived processes. Two paths: 1. **You operate one Voight account**, tag by `org`, and expose internal per-tenant views to your customers via your own admin UI (queryable via `/v1/me/ai-apps/*`). 2. **Each customer has their own Voight account** with their own API key. You run two wrappers in parallel (yours for ops; theirs for their customer-facing dashboard). Most customers start at (1) and graduate to (2) when a single enterprise tenant needs SOC 2 access logs of their own. ## Wiring it end-to-end If you haven't installed the wrapper yet, the order is: 1. **Install the wrapper** for your provider — [OpenAI](/ai-apps/openai) or [Anthropic](/ai-apps/anthropic) 2. **Wrap your client** at module load time (one place in your codebase) 3. **Call `withTrace` at your request boundary** with `tags.userId` 4. **Open the dashboard** → AI Apps → Users sub-tab 5. **Profit** (literally — you now know which users to upsell, throttle, or upgrade) The [AI Apps overview](/ai-apps/overview) walks through the full dashboard surface — Overview / Traces / Models / Tools / Users — and how the tags you set here drive every panel. ## Next * [AI Apps overview](/ai-apps/overview) — the dashboard section that surfaces per-user data * [Tracing](/ai-apps/tracing) — the `withTrace` + `log` API in full * [OpenAI SDK](/ai-apps/openai) — wrap your OpenAI client * [Anthropic SDK](/ai-apps/anthropic) — wrap your Anthropic client * [Track OpenAI and Anthropic costs per user](https://voight.xyz/guides/llm-cost-per-user): the end-to-end guide # Sessions Source: https://docs.voight.xyz/concepts/sessions One session = one Claude Code launch (or one autonomous bot run). A **session** is a group of events that share a `sessionId` — the lifetime of one IDE launch or one autonomous bot run. ## How sessions differ from traces | | Trace | Session | | - | - | - | | **Boundary** | One user prompt → next user prompt | One process lifetime (Claude Code launch → exit) | | **Cardinality** | Many per session | One per process | | **Typical duration** | Seconds to minutes | Minutes to hours | | **What it groups** | All work the agent did for one ask | Everything that happened in one editor session | A session contains many traces. A trace contains many events. ## Session lifecycle * **Claude Code**: opens a fresh `session_id` when you launch the editor or start a new chat. Every hook event during that run carries it. * **Library mode (autonomous bots)**: you control the session ID. Pass `metadata.sessionId` explicitly on each `voight.log()` call, or let the SDK group events by other means. ## Sessions grouped by day If you launch Claude Code multiple times in the same day with the same project, each launch becomes its own session row. The dashboard's `/dashboard/sessions` page groups these by `(agentId, dayKey)`: * One card per agent per day * Badge `+N more` if there were multiple launches * Click to drill into the individual sessions This keeps the page scannable when you're running many short sessions. ## Session detail page `/dashboard/sessions/` shows: * Header: agent, start/end time, total duration, total events, errors, tx count * Timeline of all traces in the session * Inspector panel with aggregated KPIs (tokens, cost, p50/p95 latency) * Per-trace breakdown ## Session status A session is one of: | Status | When | | - | - | | `active` | Last event \< 5 minutes ago | | `idle` | Last event 5-30 minutes ago | | `done` | Last event > 30 minutes ago, no errors | | `errored` | Any event has `outcome: 'failed'` and not yet acknowledged | | `paused` | An anomaly alert paused the agent (autonomous flows) | ## Filtering Available filters on `/dashboard/sessions`: * **Time window**: 24h / 7d / 30d / all * **Agent**: any agent in your account * **Status**: active / idle / done / errored The same FilterBar component drives Audit log + Overview live timeline, so once you learn it in one place it works everywhere. ## API ```bash theme={null} curl -H "Authorization: Bearer $PRIVY_JWT" \ "https://api.voight.xyz/v1/me/sessions?limit=50" curl -H "Authorization: Bearer $PRIVY_JWT" \ "https://api.voight.xyz/v1/me/sessions/" ``` See the [`/me/sessions` reference](/api-reference/me) for full schema. # Traces Source: https://docs.voight.xyz/concepts/traces One trace = everything the agent did to answer one prompt. A **trace** is a group of events that share a `traceId` — the agent's full response to one user prompt, from the first reasoning step to the final tool call. ## Trace lifecycle 1. **`UserPromptSubmit` fires** — a new `traceId` is generated (`t__<8-hex-random>`) and persisted to `$TMPDIR/voight-traces/.txt`. 2. **Tool events inherit the `traceId`** — `PreToolUse`, `PostToolUse`, and any reasoning steps read the file and stamp their events with the same `traceId`. 3. **`Stop` event ends the trace** — the Stop hook fires when the agent's turn is done. 4. **Next `UserPromptSubmit` starts a new trace** — the file is overwritten with a fresh `traceId`. The cycle repeats. One trace equals one logical task the agent worked on, regardless of how many tool calls or reasoning steps it took. ## What a trace looks like A trace card on `/dashboard/traces` shows: ``` ● Voight dev agent 49s ago "refactor src/auth.ts to use JWT" t_mp01q1r2_8e3... 2m18s 9 tools · 0 err · 0 files ``` Clicking opens a full timeline of every event in the trace, with: * Prompt at the top (masked by default — click the eye to reveal) * Each tool call inline with timing + outcome * Reasoning steps interleaved * Cost USD aggregated * Files touched listed * Git context (branch + sha) pinned ## Text-response traces If the agent answers conversationally (no tool calls), the trace contains only the prompt + a Stop event with `responseText`. The dashboard shows these as **"Text response"** traces with a distinct badge so you can scan past them quickly. ## Wakeup / autonomous traces `ScheduleWakeup` callbacks (the autonomous-loop mechanism in Claude Code's `/loop` mode) arrive as `UserPromptSubmit` events. To distinguish them from real user prompts: * The SDK records the wakeup parameters (`delaySeconds`, `reason`) when the agent calls `ScheduleWakeup` * When the next `UserPromptSubmit` matches a pending wakeup, the event is tagged `promptSource: 'system'` * The dashboard renders these traces with a `SYSTEM` badge so you can spot autonomous loop ticks at a glance ## Privacy chips per-trace Every event in a trace ships with `metadata.privacyLevel` (Minimal / Standard / Full). The dashboard renders a chip per-event so you can audit retroactively: did *this specific trace* get captured under the level I expected? Useful when you change your privacy level mid-project — the old events keep their chip, the new ones get the new level. ## Trace search + filter The Traces page (`/dashboard/traces`) supports: * **Time window**: 24h / 7d / 30d / all * **Free-text search**: matches agent label, project path, prompt text, trace ID * **Per-card eye toggle**: reveal individual prompts without exposing all of them at once * **Global Show/Hide**: bulk reveal for private review, bulk hide before going on-camera Default is masked. Refresh / navigate-away resets to masked. ## API To fetch traces programmatically: ```bash theme={null} curl -H "Authorization: Bearer $PRIVY_JWT" \ "https://api.voight.xyz/v1/me/traces?since=7d" ``` See the [`/me/traces` reference](/api-reference/me) for full schema. ## Next * [Sessions](/concepts/sessions) — traces grouped by `sessionId`, one session per process lifetime * [Events](/concepts/events) — the atomic unit; every event carries a `traceId` * [How to read an agent trace](https://voight.xyz/guides/read-ai-agent-traces): from a failed run to the root cause, step by step # DAEMON Source: https://docs.voight.xyz/integrations/daemon DAEMON — the AI-native Solana IDE — ships Voight as its observability stream for agent runs, tool calls, approval decisions, terminal activity, and Solana transactions. [**DAEMON**](https://www.daemonide.tech) is the AI-native Solana development environment — an Electron-based desktop IDE that consolidates editor, terminal, wallet, deploy pipeline, and AI agents into one workspace. Open source under MIT, available for macOS, Windows, and Linux. DAEMON integrates Voight as its **observability stream** for AI agent runs, tool calls, approval decisions, terminal activity, Solana transactions, errors, and per-session traces. Their integration guide lives at [daemonide.tech/docs/voight-observability](https://www.daemonide.tech/docs/voight-observability). ## What DAEMON emits to Voight DAEMON maps its native activity to Voight's [event taxonomy](/concepts/events). Every action the IDE performs — whether the Claude agent reasoning over a patch, the integrated terminal running `solana program deploy`, or the user approving a Phantom signature — lands as a typed event you can filter and trace. | Event type | DAEMON activity captured | | - | - | | `reasoning` | Claude / Codex chat turns, agent analysis — with `model` and duration | | `action` | Agent lifecycle, file patches, terminal commands, filesystem operations | | `tool` | Tool execution (Shipline steps, MCP tool calls) with outcome + timing | | `decision` | Approval workflows — policy metadata captured | | `tx` | Solana transactions — amount, token, status, signature | | `error` | Runtime failures with source context | Each event carries a `sessionId` in `metadata` so a single DAEMON workspace run groups cleanly in `/dashboard/traces`. ## Architecture DAEMON uses Voight's [raw HTTP path](/sdk/http-api) — `POST https://api.voight.xyz/v1/events` — directly from the **Electron main process**. The `VOIGHT_KEY` lives in DAEMON's secure storage and is **never exposed to renderer code** or to the agent runtime. Renderer-side JavaScript and any running agent see only the IPC events DAEMON exposes; the key and the ingestion endpoint stay in main. This is the cleanest pattern for any Electron / Tauri / sidecar architecture that wants to observe an in-process agent without leaking credentials into the runtime the agent has access to. ## Setup inside DAEMON Users configure the integration in-app — no code changes: 1. **Generate a Voight key** at [voight.xyz/dashboard/settings](https://voight.xyz/dashboard/settings). The key is shown once and prefixed `vk_`. 2. **Open DAEMON Settings → Integrations → Voight Observability** and paste the key. 3. **Pick a privacy level** — defaults to `standard` (recommended). 4. **Send a test event** — DAEMON ships one synthetic event and verifies it lands in your Voight dashboard before any live agent run. 5. **Run an agent** — every subsequent event streams to Voight in real time. The full DAEMON-side walkthrough lives at [daemonide.tech/docs/voight-observability](https://www.daemonide.tech/docs/voight-observability). ## Privacy — three levels, scrubbed locally DAEMON implements Voight's [three-level privacy model](/privacy/overview) in the main process, so scrubbing happens **before transmission** — sensitive content never leaves the machine in clear form. | Level | Behaviour | | - | - | | **Minimal** | Timing + outcomes only. No prompts, no responses, no file paths, no commands. | | **Standard** (default) | Full content with local pattern scrubbing applied to prompts, responses, terminal output, and metadata. | | **Full** | Raw content as captured. Internal debugging only. | The scrubber redacts private keys, JWTs, API credentials, email addresses, phone numbers, and payment card numbers — see [PII patterns](/privacy/pii-patterns) for the complete list (12 named patterns + Luhn-validated cards). ## Retry + status handling DAEMON respects Voight's ingestion contract exactly: | Status | DAEMON behaviour | | - | - | | `202 Accepted` | Event ingested | | `400` | Bad payload — skip retry, surface in DAEMON's local error log | | `401` | Invalid `VOIGHT_KEY` — DAEMON shows a credentials error | | `410` | Agent deleted in Voight — DAEMON stops sending for that agent | | `429` | Rate-limited — back off honouring `Retry-After` | | `5xx` | Transient — retry with exponential backoff | Failed dispatches never block the agent — Voight ingestion is fire-and-forget by design. ## Why this combination works DAEMON gives you the workspace where Solana agents run: Claude / Codex reasoning over real code, Shipline pushing programs to devnet/mainnet, Phantom signing transactions, MCP tools touching Helius / Jupiter / Metaplex / PumpFun. That's a dense, high-stakes surface — agents proposing on-chain actions with real money attached. Voight gives that surface a memory. Every agent decision, every tool the agent reached for, every approval the human gave or denied, every transaction signature, every error — reviewable in a timeline, grouped by session, filterable by model / outcome / tool, costed automatically. The two products compose without either side needing to know the other's internals: DAEMON speaks Voight's HTTP contract from the main process, Voight stores and renders. ## Links * [DAEMON website](https://www.daemonide.tech) · [DAEMON GitHub](https://github.com/daemonide) (MIT) * [DAEMON's Voight integration guide](https://www.daemonide.tech/docs/voight-observability) * [Voight HTTP API](/sdk/http-api) — the surface DAEMON uses * [Event types reference](/concepts/events) * [Privacy overview](/privacy/overview) · [PII patterns](/privacy/pii-patterns) # Nosana Source: https://docs.voight.xyz/integrations/nosana Run AI agents on Nosana's decentralized GPU network and let Voight detect them, correlate them on-chain, and showcase them with the Nosana-Powered badge. **[Nosana](https://nosana.com)** is a decentralized GPU network on Solana: every job that runs on it is a real on-chain account anyone can verify. Voight closes the loop for AI agents running there: the **[`@voightxyz/nosana`](https://www.npmjs.com/package/@voightxyz/nosana)** SDK detects the job from inside the container, correlates it with its on-chain account, and Voight's public explorer shows the agent as **Nosana-Powered**, with an entry in the MPL Agent Registry. Verifiable compute in, verifiable agents out. One call reads the job id a Nosana node injects into every container. Zero dependencies. The job id IS a Solana account: state, GPU market, node, price, and timings, verifiable by anyone. The agent's explorer profile carries the badge, linked to the job for independent verification. Looking to **run** an agent on a Nosana GPU rather than observe one? Voight Agents can be hosted on a dedicated Nosana GPU with a local model, in one click: see [GPU hosting](/agents/gpu-hosting). This page covers the other direction: observing agents that already run there. ## How it fits together ```mermaid theme={null} flowchart LR A["Nosana node injects
NOSANA_ID into the container"] --> B["detectNosana()
job id = on-chain account"] B --> C["fetchNosanaJob()
state · market · node · price · IPFS"] C --> D["nosanaAttributes()
stable nosana.* attributes"] D --> E["Voight event ingest"] E --> F["Nosana-Powered badge
voight.xyz/explore"] E --> G["MPL Agent Registry entry
on Solana mainnet"] H["Read-only scanner
chain + IPFS, no SDK needed"] -. discovers .-> E ``` Two ways in, same pipeline: agents that embed the SDK report themselves with live telemetry, and the read-only scanner discovers agents straight from public chain + IPFS data, no integration required. ## Quickstart ```bash theme={null} npm install @voightxyz/nosana ``` ```ts theme={null} import { correlateNosana, nosanaAttributes } from '@voightxyz/nosana' const nosana = await correlateNosana() if (nosana) { const attrs = nosanaAttributes(nosana) // { 'nosana.job_id': '…', 'nosana.state': 'RUNNING', 'nosana.market': '…', … } // merge attrs into the metadata of any event you send to Voight } ``` Send any event to Voight carrying those `nosana.*` attributes and the pipeline does the rest: the agent's profile is stamped at ingest, the badge renders in the [public explorer](https://voight.xyz/explore), and the ecosystem stats count it under `nosanaPowered`. Zero runtime dependencies, ESM + CJS + TypeScript types. If you'd rather not touch your agent's code, chain the ready-made [boot beacon](https://github.com/Voightxyz/Nosana-SDK/tree/main/examples) into your start command: one line, any framework. ## Detection reference A Nosana node injects the job id into every `container/run` operation. Grounded in the [`nosana-ci/nosana-cli`](https://github.com/nosana-ci/nosana-cli) source, not guessed: | Variable | Injected by | Meaning | | - | - | - | | `NOSANA_ID` | The Nosana node, into every job container | **The job's on-chain account address** (base58 Solana pubkey). Primary signal. | | `NOSANA_JOB_ID`, `JOB_ID` | Gateway sidecar / explicit job definitions | Accepted fallbacks. | | `DEPLOYMENT_ID` | Load-balanced deployments | Groups replicas of the same deployment. | `detectNosana()` validates the id as a base58 pubkey before producing any link: a malformed value still detects (the env var is the signal) but never renders a broken URL. ## On-chain correlation `fetchNosanaJob()` reads the job account over plain JSON-RPC and decodes it locally, no web3.js, no anchor. What comes back: | Field | Meaning | | - | - | | `state` | `QUEUED` · `RUNNING` · `COMPLETED` · `STOPPED` (running derived from a claimed, unfinished job) | | `market` / `node` | GPU market the job was posted to, and the node running it | | `payer` / `project` | Who paid, and which project posted it: the ownership anchor | | `priceNos` | Job price in NOS | | `timeStart` / `timeEnd` / `timeoutSeconds` | Unix timings | | `ipfsJobCid` / `ipfsResultCid` | IPFS CIDs of the job definition and result, reconstructed from the on-chain digests | | `dashboardUrl` / `explorerUrl` | Nosana dashboard and Solscan links | * The account owner must be the Nosana Jobs program (`nosJhNRqr2bc9g1nfGDcXXTXvYUmxD4cVwy2pMWhrYM`). * The 8-byte **Anchor discriminator** must match `JobAccount`: the program owns several account types (markets, runs), and an owner check alone would decode a market queue into garbage. Anything else is rejected. * The layout is taken from the program source (`nosana-programs` → `nosana-jobs/src/state.rs`), and the decoder is validated against live mainnet accounts, including IPFS CIDs that resolve to the actual job definitions. * `correlateNosana()` never throws on RPC failures: an observability hiccup must never break an agent. Voight can also enrich server-side from the same public account. ## What shows up in Voight * **Explorer badge.** The agent's card and profile show **Nosana-Powered** in Nosana green; the profile badge links straight to the job on Nosana's dashboard so anyone can verify the GPU workload. * **Ecosystem stats.** [`GET /v1/stats`](https://api.voight.xyz/v1/stats) reports `nosanaPowered`, and the agents API accepts a `nosana=true` filter. * **Agentic Registry.** Nosana-powered agents get registered in the MPL Agent Registry (Metaplex) with a public `agent_uri` carrying the job linkage: on-chain identity pointing at on-chain compute, with the Voight × Nosana card as its image. ## Framework identification Voight recognizes the agent's framework two ways. **Active always wins**: | Path | How | Precision | | - | - | - | | **Active** | `VOIGHT_FRAMEWORK` env (`elizaos`, `hermes`, `openclaw`, …) with the beacon or your own events | Exact | | **Passive** | Job definitions are public on IPFS: image, command, and env reveal the framework | Inferred | Passive signals recognized today: **ElizaOS** (`eliza` images, `--character` commands), **Mastra** (incl. Nosana's `agent-challenge` starter), **LangGraph/LangChain**, **CrewAI**, **Hermes**, and custom agents (`agent` in the image name). Model servers and GPU infra (vLLM, Ollama, ComfyUI, Folding\@home) are classified out, and anything ambiguous is skipped: the classifier never lists a random training job as an "agent". Full signal table and how to make your agent identifiable: [frameworks spec](https://github.com/Voightxyz/Nosana-SDK/tree/main/frameworks). ## Proven live on mainnet The full loop has run twice on real Nosana GPU jobs. The latest (August 2, 2026, RTX 3060 node), every artifact public and independently verifiable: | Step | Evidence | | - | - | | Nosana GPU job | [`4KQFyrvw…fQw7`](https://dashboard.nosana.com/jobs/4KQFyrvw6eGr2uRUvccR2UcG5ovAJW6ys6DuyUoifQw7) — the job logs show the SDK detecting its own id | | Agent in the explorer | [`voight.xyz/agent/cmsc4g119…`](https://voight.xyz/agent/cmsc4g119xw34clddokzlg1g2) — **Agent Test Nosana**: badge, the Voight × Nosana card, and verification links | | Ecosystem stats | [`/v1/stats`](https://api.voight.xyz/v1/stats) → `nosanaPowered` | | MPL Agent Registry | [`7A3cNLx7…aGKK`](https://www.metaplex.com/agents/7A3cNLx7WiJqGcXPKVsH5z4wVpCUTN2A63dPz9o9aGKK) minted on Solana mainnet with the Voight × Nosana card | What the agent printed from inside the GPU container on the first acceptance run (July 22), verbatim: ```text theme={null} [probe] Voight × Nosana acceptance probe [probe] NOSANA_ID = 96zb2aU1VjCEsnZHDX8GCtzLk5afVFGrQjPPxKva7YMp [probe] detection: {"jobId":"96zb2aU1VjCEsnZHDX8GCtzLk5afVFGrQjPPxKva7YMp", "source":"NOSANA_ID","isAddress":true, "dashboardUrl":"https://dashboard.nosana.com/jobs/96zb2aU1VjCEsnZHDX8GCtzLk5afVFGrQjPPxKva7YMp"} [probe] voight ingest → 200 ``` Job definitions are public on IPFS: anything you put in a job's env (like an ingest API key) is world-readable. Use scoped, revocable keys and rotate them after a run. ## Links * npm: [`@voightxyz/nosana`](https://www.npmjs.com/package/@voightxyz/nosana) * Source, probe, and scanner: [github.com/Voightxyz/Nosana-SDK](https://github.com/Voightxyz/Nosana-SDK) * Nosana: [nosana.com](https://nosana.com) · [dashboard.nosana.com](https://dashboard.nosana.com) # Integrations Source: https://docs.voight.xyz/integrations/overview Products that ship Voight as their built-in observability stream for AI agents — no code required by their end users. This section catalogues the products that integrate Voight directly into their workflow, so their users get observability for agent runs, tool calls, decisions, and transactions without writing any instrumentation code themselves. If you're building an agent platform, IDE, runtime, or anything else that runs LLM-driven code and want to ship observability without rolling your own pipeline, the [HTTP API](/sdk/http-api) is the surface most integrations use — POST events to `https://api.voight.xyz/v1/events` from any runtime that speaks JSON over HTTPS (Node, Python, Rust, Go, Electron main process, Tauri sidecar, Deno). ## Current integrations | Product | What it is | Page | | - | - | - | | **DAEMON** | AI-native Solana IDE — Electron, MIT, mac/Win/Linux | [DAEMON](/integrations/daemon) | ## Adding your integration Voight ships first-party SDKs for OpenAI, Anthropic, the Vercel AI SDK, plus hooks for Claude Code / Cursor / Codex. If your product doesn't fit those surfaces — for example, you're building a desktop IDE, an agent runtime, or a no-code platform — the [HTTP path](/sdk/http-api) is the cleanest fit. For n8n, the [n8n guide](https://voight.xyz/guides/debug-failed-n8n-workflows) shows the setup end to end. The event shape, privacy levels, retry contract, and dashboard rendering are identical to what the SDKs use internally. To get listed here, reach out at [x.com/voightxyz](https://x.com/voightxyz) with a link to your integration docs. We mirror your page here and cross-link both ways. # Introduction Source: https://docs.voight.xyz/introduction Observability and debugging for AI coding agents, autonomous workflows and production LLM apps, with per-user cost attribution. Voight is the observability and debugging platform for AI agents and autonomous systems. It captures every prompt, tool call, model response, decision and error your agents and AI applications make, renders them as a live timeline, groups them into traces, and turns failures into an issue queue with AI diagnosis of the root cause. If you ship LLM features to end-users, Voight also answers the question nobody else does cleanly: **what does each user cost me?** One line of code at your request boundary, no PII handoff required. Full pattern on the [per-user spend](/concepts/per-user-spend) page. ## Six surfaces, one event stream | Surface | Lives at | What it's for | Reference | | - | - | - | - | | **Dashboard** | `voight.xyz/dashboard` | Live timeline, agents, issues, alerts, cost + token KPIs | [Events](/concepts/events) | | **AI Apps** | `voight.xyz/dashboard/ai-apps` | Production LLM-app observability: Overview / Traces / Models / Tools / Users | [AI Apps overview](/ai-apps/overview) | | **Coding SDK** | `@voightxyz/sdk` on npm | Hook Claude Code or Cursor, stream Codex sessions, or instrument any TS/JS agent you build yourself | [Quickstart](/quickstart) | | **App SDKs** | `@voightxyz/openai` · `@voightxyz/anthropic` · `@voightxyz/vercel-ai` | Capture LLM calls from production apps: direct provider wrappers or via the Vercel AI SDK | [OpenAI](/ai-apps/openai) · [Anthropic](/ai-apps/anthropic) · [Vercel AI](/ai-apps/vercel-ai) | | **Python SDK** | `bitfrost` on PyPI | Drop-in OpenTelemetry observability for Python LLM apps: openai / anthropic / litellm / smolagents, standalone or shipping to Voight | [Bitfrost](/python/bitfrost) | | **HTTP API** | `POST /v1/events` | Language-agnostic event ingestion: Python, Go, Rust, anything | [API reference](/api-reference/events) | ## What Voight is for Three audiences, one platform: * **You're building an AI coding agent or autonomous bot.** Wire the [coding SDK](/quickstart) into your IDE or import [library mode](/sdk/library-mode): every prompt, tool call and error lands in the dashboard with zero code changes. * **You're shipping an LLM-powered feature to end-users.** Wrap your OpenAI / Anthropic client with the [App SDKs](/ai-apps/openai), or register [`@voightxyz/vercel-ai`](/ai-apps/vercel-ai) once if you're on the [Vercel AI SDK](https://sdk.vercel.ai). Every call gets captured with full token/cost breakdown. Add [`withTrace`](/ai-apps/tracing) at your request boundary to group calls per request and attribute cost per end-user. * **You sell AI-powered tools to other developers.** The [per-user spend](/concepts/per-user-spend) pattern lets you answer "what does each of my customer's customers cost me?" without integrating an analytics SDK on the client side. The same event model is built to extend beyond software agents: robots, drones and other physical autonomous systems are the next surface. ## Primitives * **Agent**: an autonomous entity emitting events. Identified by a folder marker, env var, or explicit `agentId`. * **Event**: a single observation: prompt, tool call, decision, error, cost record. * **Trace**: a group of events sharing a `traceId`, typically one user request or workflow. * **Session**: a time-bounded run of an agent. * **Tag**: a free-form `key=value` pair attached to events inside a [`withTrace`](/ai-apps/tracing) block; the foundation of per-user / per-tenant / per-feature attribution. ## Privacy Three capture levels: **Minimal** (metadata only), **Standard** (full content with local PII scrubbing), **Full** (raw). Picked once at install, switchable per session via env var. The dashboard renders a per-event chip showing which level captured each row. The numeric data (tokens, USD, latency, tool names, tags) passes through every level unchanged. KPIs and charts work identically in all three. You only lose prompt/response content in Minimal. See the [privacy overview](/privacy/overview) for the field-by-field breakdown. ## Who builds Voight Voight is built by Galaxyhub Labs Inc. (Delaware, 2025), founded by Dangel Rodriguez with a team of four across Spain and London, and is part of Xiaomi Orbit and Z.ai Startups. Traction, timeline, programs and recognition, with dates and links: [voight.xyz/company](https://voight.xyz/company). ## Don't have agents yet? [Voight Agents](/agents/overview) is a separate product: autonomous agents that Voight hosts and runs for you, live in about a minute on cloud or GPU. It exists so you have something to observe from day one, and you can point this platform at any Voight Agent like at any other agent. ## Next * [Quickstart](/quickstart): events streaming in 30 seconds (any of four install paths) * [AI Apps overview](/ai-apps/overview): the dashboard section for production LLM apps * [Per-user spend](/concepts/per-user-spend): one line of code to attribute cost per end-user * [Guides](https://voight.xyz/guides): dated, practical guides on debugging and running AI agents in production # MCP for Voight Agents Source: https://docs.voight.xyz/mcp/agents Deploy, operate and orchestrate your Voight Agents from Claude Code, Cursor, Codex, VS Code or Claude Desktop with the @voightxyz/agents-mcp server. `@voightxyz/agents-mcp` gives your coding assistant the tools to work with the agents on your Voight account: list them, talk to one or to ten at once, deploy new ones on Voight Cloud or a Nosana GPU, schedule their work, renew and delete them. ```text theme={null} "Deploy three research agents, give each a topic, and merge what they find." "Which of my agents are running, and how much did they use this week?" "Schedule a daily 8:00 brief on my research agent." "Delete the test agents we deployed this morning." ``` The package is open source: [github.com/Voightxyz/agents-mcp](https://github.com/Voightxyz/agents-mcp). It talks only to `api.voight.xyz`. ## 1. Create a key Open [agent.voight.xyz](https://agent.voight.xyz), open the account menu and choose **MCP / API**. Pick the access level and generate the key. It is shown once. | Access | What the assistant can do | Expires | | - | - | - | | **Agents: operate** | List agents, read usage and tasks, chat, wake GPUs | Never | | **Agents: full** | Everything above, plus deploy, renew, schedule tasks and delete | After 90 days | Ingest keys (the ones the observability SDK uses) do not work with this server. ## 2. Add the server to your client ```bash theme={null} claude mcp add --env VOIGHT_API_KEY=vk_your_key --transport stdio voight-agents -- npx -y @voightxyz/agents-mcp ``` To share a project config without the secret, commit a `.mcp.json` that reads the key from each person's environment: ```json theme={null} { "mcpServers": { "voight-agents": { "command": "npx", "args": ["-y", "@voightxyz/agents-mcp"], "env": { "VOIGHT_API_KEY": "${VOIGHT_API_KEY}" } } } } ``` `~/.cursor/mcp.json` (or `.cursor/mcp.json` in a project): ```json theme={null} { "mcpServers": { "voight-agents": { "command": "npx", "args": ["-y", "@voightxyz/agents-mcp"], "env": { "VOIGHT_API_KEY": "vk_your_key" } } } } ``` `~/.codex/config.toml`: ```toml theme={null} [mcp_servers.voight_agents] command = "npx" args = ["-y", "@voightxyz/agents-mcp"] startup_timeout_sec = 30 [mcp_servers.voight_agents.env] VOIGHT_API_KEY = "vk_your_key" ``` `.vscode/mcp.json`. VS Code asks for the key once and stores it securely: ```json theme={null} { "inputs": [ { "type": "promptString", "id": "voight-api-key", "description": "Voight API key", "password": true } ], "servers": { "voight-agents": { "command": "npx", "args": ["-y", "@voightxyz/agents-mcp"], "env": { "VOIGHT_API_KEY": "${input:voight-api-key}" } } } } ``` `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/`, Windows: `%APPDATA%\Claude\`): ```json theme={null} { "mcpServers": { "voight-agents": { "command": "npx", "args": ["-y", "@voightxyz/agents-mcp"], "env": { "VOIGHT_API_KEY": "vk_your_key" } } } } ``` `~/.gemini/settings.json`. Gemini CLI does not pass variables named like `*KEY*` from your shell, so set the key in the `env` block: ```json theme={null} { "mcpServers": { "voight-agents": { "command": "npx", "args": ["-y", "@voightxyz/agents-mcp"], "env": { "VOIGHT_API_KEY": "vk_your_key" } } } } ``` Then ask your assistant: **"List my Voight agents."** ## Tools | Tool | What it does | Key | Spends | | - | - | - | - | | `list_agents` | Agents on the account: id, state, hosting, model, days left | operate | No | | `get_agent` | One agent in detail: channels, expiry, on-chain identity, Nosana job, persona | operate | No | | `get_credits` | Credit balance, plan and the price of one deploy | operate | No | | `get_agent_usage` | Turns, tokens and cost for one agent or the whole account | operate | No | | `list_tasks` | Tasks of an agent, with the result of the last run | operate | No | | `list_gpu_markets` | GPU markets with hourly price and live availability | operate | No | | `chat_with_agent` | Sends one message and waits for the reply | operate | Inference | | `get_reply` | Collects the reply of a turn that was still running | operate | No | | `message_agents` | Sends instructions to up to 10 agents at once | operate | Inference | | `get_replies` | Collects several pending replies in one call | operate | No | | `wake_agent` | Starts the GPU of a stopped Nosana agent | operate | Can bill a GPU hour | | `wait_for_agent` | Waits for a starting agent, and says when to stop waiting | operate | No | | `quote_agent_deploy` | What a deploy would cost, and whether the account may do it | full | No | | `deploy_agent` | Deploys an agent on Voight Cloud or a Nosana GPU | full | **Yes** | | `quote_agent_renewal` | What a renewal would cost, and whether it is due | full | No | | `renew_agent` | Extends hosting by another period | full | **Yes** | | `create_task`, `update_task`, `delete_task` | Work an agent runs on its own: once, daily or weekly | full | Each run is a normal turn | | `delete_agent` | Permanently deletes an agent | full | No | Start the server with `--read-only` to expose only the tools that read. ## Deploying from your assistant Spending always takes two tools, so your client asks for permission at the moment credits move: 1. `quote_agent_deploy` answers what it costs, your balance, and whether the account may deploy it. It spends nothing. 2. `deploy_agent` repeats the quoted amounts. If the real price is higher, the deploy is refused and nothing is charged. 3. `wait_for_agent` follows the start (about a minute on Voight Cloud, about four on a GPU) and tells the assistant when to stop waiting. **Every deploy and renewal made through the MCP is charged** at the regular agent price, on every account. Free and trial agents are claimed on the web. Asking twice for the same deploy returns the agent that is already starting instead of a second one and a second charge. GPU hosting on Nosana is invite-only for now: deploying on a GPU works for invited accounts; everyone can deploy on Voight Cloud. See [GPU hosting](/agents/gpu-hosting). ## Limits Per account, across all its keys, enforced by the API: | Limit | Value | | - | - | | Deploys | 10 per 24 hours | | Renewals | 10 per 24 hours, and only in the 7 days before an agent expires | | Agent deletes | 10 per 24 hours | | Task creates, edits and deletes | 30 per 24 hours | | Live agents deployed through keys | 15, of which 3 on GPU | | Tasks per agent | 50 | ## Orchestrating several agents `message_agents` sends one instruction to many agents, or a different one to each, and waits once for all of them. Agents still working come back as `running` with a `turn_ref`; `get_replies` collects them. Nothing is sent to an agent that is busy or not ready, so a message is never queued behind another or lost. ## Tasks A task is an instruction the agent runs on its own: once, every day or every week (UTC). Each run is a normal agent turn. * On an agent connected to **GitHub, LinkedIn or X**, a task created from the MCP is saved **paused**, and a person enables it in the dashboard. A key can pause such an agent's tasks, and edit a task it created that never ran, but it can never enable one or change one a person approved. * **Revoking a key does not stop the tasks it scheduled.** Pause or delete them from the MCP or from the dashboard. ## Deleting `delete_agent` is permanent: the agent, its memory and its tasks are gone, and a used period is not refunded. It needs the agent's exact name, and it **only works on agents that were deployed with an API key**. Agents built in the dashboard cannot be deleted from any MCP session. A GPU deploy that has not started yet is refunded when deleted; a cloud agent that is still starting cannot be deleted until it is up. ## Security and privacy * **Scoped keys.** An operate key cannot deploy, renew, schedule or delete. A full key can, expires after 90 days, and is bounded by the limits above. Neither can touch billing, top up, or manage API keys. Revoke a key at any time in **MCP / API**. * **The rules live on the server.** Charging, limits, duplicate detection, what a key may delete and the task rules are enforced by the Voight API. A model can repeat any confirmation it is asked for, so nothing in the package is a security boundary. * **The key goes to one place.** The package only sends requests to `https://api.voight.xyz`; any other endpoint is refused. * **Stored text is fenced.** Agent replies, task results, personas and task instructions come back inside a block marked as untrusted content, and the model is told to treat them as data. * **What leaves your machine.** The messages you send to an agent go to Voight, as they would from the dashboard. What comes back is handed to your MCP client and therefore to the model provider behind it. Account email and wallet are never returned. ## Known limits * Chat turns in progress are kept in the memory of the local server. If your client restarts it while an agent is working, the pending reply is lost (the agent still finishes). * Hourly tasks, editing an agent's persona or model, retrying a failed deploy, connectors and attachments are not exposed yet. ## Next * [Deploy agents from Claude Code with MCP](https://voight.xyz/guides/deploy-ai-agents-with-mcp): the full walkthrough, from first deploy to scheduled tasks * [Voight Agents overview](/agents/overview): what an agent can do today # MCP for Observability Source: https://docs.voight.xyz/mcp/observability Read your Voight traces, issues, alerts and AI Apps metrics from your coding assistant. Coming soon. **Coming soon.** The observability server is being built after the [Agents server](/mcp/agents). This page describes what it will do so you know what to expect; nothing here is installable yet. `@voightxyz/mcp` will let your assistant read what Voight observes about your AI apps and agents, in plain language: ```text theme={null} "Which of my traces failed in the last hour, and why?" "Show me the open issues on the checkout agent." "What did the last alert on production say?" ``` ## Planned tools | Tool | What it does | | - | - | | `get_account` | Plan and usage, without email or wallet | | `list_agents`, `get_agent` | The agents and apps Voight observes | | `list_traces`, `get_trace` | Recent traces, and one trace as a summary, a timeline or its errors | | `list_issues`, `get_issue` | The cross-agent issue queue and one issue with its diagnosis | | `list_alerts` | Recent alerts | | `explain_trace` | Voight's explanation of a trace, cached when it already exists | | AI Apps aggregates | Overview, models and tools, as numbers | Reading will be free. AI diagnosis and explanations follow the plan's quota. ## Keys It will use a separate access level, **Observability: read**, created in the Voight dashboard Settings. Ingest keys will keep working only for sending events, as today. # Voight MCP Source: https://docs.voight.xyz/mcp/overview Connect Claude Code, Cursor, Codex, VS Code or Claude Desktop to Voight through the Model Context Protocol: one server to run your agents, one to read your observability. Voight ships [Model Context Protocol](https://modelcontextprotocol.io) servers so your coding assistant can work with Voight in plain language instead of through the dashboard. Each server is a small package you add to your client with one line; it talks to `api.voight.xyz` with an API key you create in Voight. Deploy, operate and orchestrate your agents from your assistant. Available now. Read traces, issues and alerts from your assistant. Coming soon. ## How it works 1. You create an **API key** in Voight with the access level the server needs. 2. You add the server to your client (Claude Code, Cursor, Codex, VS Code, Claude Desktop or Gemini CLI). It runs locally with `npx`; nothing else to install. 3. Your assistant gets a set of **tools** and uses them when you ask: "list my agents", "deploy one on a GPU", "which of my traces failed today". The servers are thin. Everything that protects your account (what a key may do, spending limits, what it can delete) is enforced by the Voight API, not by the package on your machine. ## Which clients | Client | Setup | | - | - | | Claude Code | One `claude mcp add` command | | Cursor | A block in `~/.cursor/mcp.json` | | Codex CLI | A block in `~/.codex/config.toml` | | VS Code | A block in `.vscode/mcp.json`, key stored securely | | Claude Desktop | A block in `claude_desktop_config.json` | | Gemini CLI | A block in `~/.gemini/settings.json` | Each server's page has the exact snippet per client. ## Remote connectors The servers above run on your machine. A hosted version with "Sign in with Voight", so Voight can be added as a connector in claude.ai and ChatGPT without any key, is planned next. # Pricing Source: https://docs.voight.xyz/pricing Built for teams ready to scale. Three tiers. No per-event surprises. No enterprise gating of the basics. **\$0** forever * 10K events / month * 7-day retention * 3 agents * Public Explorer presence * Community Discord **\$19 / month flat** Built for teams ready to scale. * 300K events / month * 90-day retention * 15 agents * Custom alerts * Full audit export (PDF + JSON) * Private agent option * A/B agentic testing (coming soon) **Custom** For regulated deployments and prop desks. **Everything in Pro, plus:** * SLA & priority support * SOC 2 Type II materials * SSO & audit access controls * Dedicated infrastructure * Compliance exports (EU AI Act, SEC) ## Founding members First 100 integrations receive **Pro free for 30 days**. Badge in Explorer. Today: **20 / 100 claimed**. ## Currently enforced The quotas above are aspirational pre-launch. Today: * **Events ingestion**: rate-limited by burst (60/min Free, 600/min Pro) to prevent abuse. Monthly quotas not yet enforced server-side. * **Agents**: no hard cap today. The UI doesn't prevent you from creating more. * **Retention**: enforced. Events older than your tier's window are auto-purged. * **Custom alerts**: built-in rules (event surge, error rate, cost spike, orphan spike) work for all tiers. Custom rule creation is a Pro feature, ships v1.0. * **PDF audit exports**: CSV is shipped today (Overview + Audit log). PDF + JSON ship v1.0. Monthly event quotas + Stripe billing ship in **v1.0**. ## What changes at each tier The runtime behaviour of the SDK is the same across all tiers. Pricing changes: * How much data Voight retains and surfaces * How many alert rules you can author * How many private (non-Explorer) agents you can run * Compliance materials availability ## Self-hosting If you want to run Voight against your own infrastructure (private deployment, regulated environment), Enterprise customers get self-host runbooks + Helm charts. Reach out for details. ## FAQ Every `POST /v1/events` call. A single Claude Code session with 50 tool calls = 50 events + 1 prompt + 1 stop = \~52 events. An autonomous trading bot doing 10 swaps + 30 reasoning steps = \~40 events. Rule of thumb: 1 event ≈ one row in the dashboard's audit log. Stripe checkout ships in v1.0. Today, [reach out](mailto:support@voight.xyz) for Founding Member status (Pro free for 30 days, first 100 integrations). Yes, any time. Retention drops to the new tier's window on the next billing cycle. Events outside the new window get purged. Today: nothing — quotas are advisory. v1.0: events are rejected with `402 Payment Required` past quota; the dashboard shows a banner prompting upgrade. Not yet. We're focused on making Free generous enough that most use cases fit. If 10K events / month isn't sufficient for an open-source / research project, [reach out](mailto:support@voight.xyz). ## Next [Get started for free](https://voight.xyz/dashboard) — generate an API key and start streaming in under a minute. No card required. # Data handling Source: https://docs.voight.xyz/privacy/data-handling Where data lives, who can access it, retention, deletion. ## What gets sent When the SDK ships an event to Voight's backend, the payload is whatever survived the privacy filter at the level you picked. See the [privacy overview](/privacy/overview) for the field-by-field breakdown. ## Transport * TLS 1.2+ over HTTPS to `api.voight.xyz` * API key authentication via `Authorization: Bearer vk_...` header * API keys are SHA-256 hashed in the database; the plaintext value is never persisted server-side ## Storage * **Database**: Postgres 14+ hosted on [Railway](https://railway.app), located in their US-East region * **Event metadata**: stored as JSON in `Event.metadata` column (`Json?` in Prisma) * **Token usage**: stored as breakdown fields * **Content (under Standard or Full)**: stored verbatim after scrubbing * **API key plaintext**: never stored. Only `keyHash` (sha256) and `keyPrefix` (first 8 chars, for UI display) ## Who can access your data * **You**: via your Privy session (JWT signed by `auth.privy.io`) → `/v1/me/*` endpoints * **Voight operators (us)**: technical staff with database access. We don't proactively read your events but we can if we need to debug an issue. This is the trust gap the privacy levels exist to mitigate. * **No one else by default**: agents are tied to your `userId`. Cross-user reads are blocked at the API layer. If you publish an agent on the public Explorer (part of Voight Agents, by registering it in the Solana Agent Registry), its metadata becomes public, but its **events** stay private. Only the agent's identity, capabilities, and aggregated reputation are surfaced. ## Retention Per pricing tier: | Tier | Event retention | | - | - | | Free | 7 days | | Pro | 90 days | | Enterprise | Custom (typically 1 year+) | Events older than your tier's retention window are automatically purged from the database via a scheduled job. This is irreversible — export anything you want to keep before the window closes (Audit log → Export CSV). ## Deletion ### Delete one agent Open `/dashboard/agents/` → trash icon top-right → modal asks you to type the agent's displayName to confirm. What happens: * The agent is **soft-deleted** (`Agent.deletedAt` timestamp set, row not removed from the DB) * Subsequent events ingested for that `agentId` return `410 Gone` to the SDK (ingestion block) * The agent disappears from all dashboard reads — events, sessions, traces, errors, alerts * Other agents in your account keep ingesting normally — isolation is per-agent * The original events stay in the database for the retention window, then get purged ### Delete your account Email `support@voight.xyz` requesting account deletion. We: 1. Soft-delete all your agents (same flow as above) 2. Soft-delete your user row 3. Purge events on the next retention cycle 4. Confirm by email when complete No self-serve account deletion in the dashboard yet — that ships in v1.0 alongside Stripe billing. ## Public registration (Voight Agents) Registering a Voight Agent in the Solana Agent Registry (optional, part of the web3 product) creates: * A Metaplex Core asset with your agent's metadata (name, description, agentUri) * A signature from your Solana wallet * An entry indexable by anyone **This is public, on-chain, and immutable.** You should only register agents you're explicitly comfortable making public. Your dashboard events remain private regardless of whether the agent itself is registered. ## Compliance posture Voight is being built with EU AI Act and SEC compliance in mind: * **Article 12 (immutable event logs)**: every event stores a SHA-256 hash of its normalised payload, so exports can be verified and an edit after the fact is detectable. External anchoring of those hashes is on the roadmap. * **Article 14 (human oversight)**: HITL routing via `voight.check()` + `voight.enforce()` (shipping v1.0). Today these are no-ops returning `{ allow: true }`. * **Audit exports**: CSV today, PDF + JSON in Pro tier (shipping v1.0). All exports include content hashes for verification. * **SOC 2 Type II**: roadmap, Enterprise tier. Voight is **not yet** SOC 2 certified. [Reach out](mailto:support@voight.xyz) if your use case requires it today. ## Known gaps * **No end-to-end encryption today.** Storage is plaintext after scrubbing. End-to-end encryption with user-managed keys is on the v0.2 enterprise roadmap (`Umbra` integration). * **Backups** of the Postgres database are managed by Railway and follow their retention. We don't currently expose backup retention to users. * **GDPR right-to-delete** is honored on email request but not yet self-serve. * **Anonymized aggregates** (counts, usage patterns) may be used for product analytics. We never use raw event content for this. If you have a specific compliance question or use case, email `support@voight.xyz`. # Privacy overview Source: https://docs.voight.xyz/privacy/overview The 3-level capture model — pick what leaves your machine. Most observability tools capture everything by default and store it on the operator's server. Voight inverts that: you pick the capture level on install, the SDK runs the filter on your machine before any HTTP request leaves it, and every event ships with a chip showing which level captured it. ## Capture levels | Category | Minimal | Standard | Full | | - | :-: | :-: | :-: | | Tool names | ✓ | ✓ | ✓ | | Tokens (counts) | ✓ | ✓ | ✓ | | USD spend | ✓ | ✓ | ✓ | | Model name | ✓ | ✓ | ✓ | | Outcome / duration | ✓ | ✓ | ✓ | | Throughput / charts | ✓ | ✓ | ✓ | | Bash commands | ✗ | ✓ scrub | ✓ raw | | File paths | ✗ | ✓ scrub | ✓ raw | | User prompts | ✗ | ✓ scrub | ✓ raw | | Agent responses | ✗ | ✓ scrub | ✓ raw | | Thinking blocks | ✗ | ✓ scrub | ✓ raw | | Error messages | ✗ | ✓ scrub | ✓ raw | | Git branch / remote / sha | ✗ | ✓ | ✓ | | Cwd path | ✗ | ✓ | ✓ | | API keys / secrets | n/a | ✗ scrubbed | ⚠️ raw | Numeric and tag data — tokens, USD, model name, latency, outcomes, tool names — pass through every level unchanged. Dashboard KPIs and charts work identically regardless of which level you pick. ## How the filter runs 1. **`resolvePrivacyLevel()`** reads `VOIGHT_PRIVACY` (set in `settings.json` or env) and returns `minimal | standard | full`. Defaults to `full` for SDK ≤0.3.10 (backwards compat). 2. **`applyPrivacy(payload, level)`** transforms the event: * `full` — passthrough; stamps `metadata.privacyLevel: 'full'` * `minimal` — rebuild from an allowlist of safe fields (tool name, tokens, outcome, duration); drop everything else * `standard` — deep-walk every string and run [`scrubPii()`](/privacy/pii-patterns) over each 3. **Every event ships `metadata.privacyLevel`** so the dashboard can render a per-event chip — your audit trail that scrubbing happened. Runs in the SDK subprocess on your machine, before the HTTP request leaves it. Source: [`voight-sdk/src/privacy.ts`](https://github.com/Voightxyz/voight-sdk/blob/main/src/privacy.ts). ## Picking your level The setup wizard prompts you on install: ```bash theme={null} npx -y @voightxyz/sdk setup ``` Switch any time: ```bash theme={null} npx -y @voightxyz/sdk setup --privacy=minimal npx -y @voightxyz/sdk setup --privacy=standard npx -y @voightxyz/sdk setup --privacy=full ``` Or override per-session via env var: ```bash theme={null} VOIGHT_PRIVACY=minimal claude # or 'standard' / 'full' ``` The level lives in `~/.claude/settings.json` under `env.VOIGHT_PRIVACY`. ## Per-event audit chips Each event in the dashboard carries a chip next to its kind tag: * `MIN` — metadata-only capture * `STD` — full content + PII scrubbing applied * `FULL` — raw capture Hover for a tooltip listing what was kept, dropped, or redacted. Changing your level mid-project doesn't rewrite history — old events keep their original chip, new ones get the new chip. ## Limitations By design, the privacy model does not: * **Prevent the operator from seeing data once transmitted.** Standard mode scrubs PII on your machine, but the operator still receives whatever's left. If that's not acceptable, pick Minimal. * **Encrypt event content end-to-end.** Transport is TLS; storage is plaintext in Postgres. E2E with user-managed keys is on the v0.2 roadmap (`Umbra` integration). * **Enforce server-side quotas.** Pricing tier limits are advisory today; enforced server-side starting v1.0 when Stripe billing lands. ## Backwards compatibility SDK ≤0.3.10 defaults to **Full** (original behaviour). No silent privacy upgrade — switching levels requires re-running setup explicitly. Done deliberately to avoid the trust-eroding "we just scrubbed your API key without telling you" scenario. Existing `settings.json` files keep working unchanged. The `VOIGHT_PRIVACY` field is additive. ## Next * [PII patterns](/privacy/pii-patterns) — the 12 patterns and Luhn-validated card detection used by Standard * [Data handling](/privacy/data-handling) — where data lives, who can access it, retention, deletion # PII patterns Source: https://docs.voight.xyz/privacy/pii-patterns The 12 patterns and Luhn-validated card detection used by Standard mode. When **Standard** mode is active, every string field in every event is run through [`scrubPii()`](https://github.com/Voightxyz/voight-sdk/blob/main/src/privacy.ts) before the SDK ships it. Patterns run in a deliberate order (most specific first, multi-line first). Matched substrings are replaced with a tagged token. ## Patterns | Pattern | Replacement | | - | - | | PEM private key block | `[REDACTED-PRIVATE-KEY]` | | JWT | `[REDACTED-JWT]` | | Anthropic key (`sk-ant-...`) | `[REDACTED-API-KEY]` | | OpenAI key (`sk-...` / `sk-proj-...`) | `[REDACTED-API-KEY]` | | Stripe live keys (`sk_live_...` / `pk_live_...`) | `[REDACTED-API-KEY]` | | GitHub fine-grained PAT (`github_pat_...`) | `[REDACTED-API-KEY]` | | GitHub classic PAT (`ghp_...`) | `[REDACTED-API-KEY]` | | AWS access key (`AKIA...`) | `[REDACTED-API-KEY]` | | Slack token (`xoxb-` / `xoxp-` / `xoxa-`) | `[REDACTED-API-KEY]` | | Voight key (`vk_...`) | `[REDACTED-API-KEY]` | | Email (`name@host.tld`) | `[REDACTED-EMAIL]` | | Phone, E.164 (`+1234567890`) | `[REDACTED-PHONE]` | | Credit card (13–19 digits + Luhn) | `[REDACTED-CARD]` | Regex source: [`voight-sdk/src/privacy.ts`](https://github.com/Voightxyz/voight-sdk/blob/main/src/privacy.ts). ## Examples ```ts theme={null} import { scrubPii } from '@voightxyz/sdk' scrubPii(`curl -H "Authorization: Bearer sk-ant-api03-AbCd...X" https://api.example.com`) // → curl -H "Authorization: Bearer [REDACTED-API-KEY]" https://api.example.com scrubPii(`paid with 4242424242424242 — confirmation @ alice@example.com`) // → paid with [REDACTED-CARD] — confirmation @ [REDACTED-EMAIL] scrubPii(`auth=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NSJ9.signature`) // → auth=[REDACTED-JWT] scrubPii(`call +14155552671 to confirm`) // → call [REDACTED-PHONE] to confirm ``` ## Deliberate non-matches To avoid false positives that would harm dev workflows: | Input | Not redacted | Reason | | - | - | - | | `email_template` | not an email | no `@` in middle | | `support@app` | not an email | no TLD | | `sk_test_xxx` | not redacted | Stripe test key — not real money | | `AKIAfoo` | not an AWS key | real keys are uppercase | | 16-digit order numbers | not a card | fails Luhn checksum | | 10-digit numbers without `+` | not a phone | E.164 requires `+` prefix | The test suite at [`tests/unit/privacy.test.ts`](https://github.com/Voightxyz/voight-sdk/blob/main/tests/unit/privacy.test.ts) verifies positive matches and adversarial negatives. ## Properties * **Idempotent** — re-running `scrubPii()` on already-scrubbed text is stable. * **Local** — runs in the SDK subprocess on your machine; no network or filesystem I/O. * **Bounded** — designed for a 2KB event payload in under 10ms on a modern laptop. Regexes are anchored with word boundaries to avoid catastrophic backtracking. * **Conservative** — the pattern set is small (\~13 regexes). Industry libraries like [gitleaks](https://github.com/gitleaks/gitleaks), [detect-secrets](https://github.com/Yelp/detect-secrets), and [trufflehog](https://github.com/trufflesecurity/trufflehog) ship 400+; we don't. ## Stricter than Standard If Standard's scrubbing isn't strict enough for your case, **Minimal** drops content fields entirely: `reasoning`, `errorMessage`, `input`, `metadata.detail`, `metadata.response_preview`, `metadata.responseText`, `metadata.cwd`, `metadata.git`. What's left is tool names, timings, token counts, and identifiers. See the [privacy overview](/privacy/overview) capture-level table. ## Requesting new patterns Open an issue at [voightxyz/voight-sdk](https://github.com/Voightxyz/voight-sdk) if a credential format you care about isn't covered (crypto wallet private keys, mnemonic phrases, API tokens for AI providers we haven't added, etc.). We err conservative — a false positive is worse than a near-miss. # Bitfrost (Python) Source: https://docs.voight.xyz/python/bitfrost Drop-in OpenTelemetry observability for Python LLM apps — standalone, or shipping to Voight. `bitfrost` is a Python package that turns the OpenTelemetry spans your LLM libraries already emit into a clean, queryable event stream — in your terminal, in a local web dashboard, in SQLite, or shipped to Voight. One line to start, no account required. It's the Python counterpart to the JavaScript [`@voightxyz/vercel-ai`](/ai-apps/vercel-ai) exporter: same backend, same dashboard, different runtime. Events from both land side-by-side under the same agent. ## Install ```bash theme={null} pip install bitfrost # core pip install 'bitfrost[cli,serve]' # + CLI, TUI, and local web dashboard pip install 'bitfrost[all]' # everything ``` Python 3.10–3.13, MIT licensed. ## Quick start ```python theme={null} import bitfrost bitfrost.quickstart(agent="my-app") # auto-detects openai / anthropic / litellm / smolagents ``` Every LLM call now streams to your terminal — color-coded, with tokens, latency, and an estimated cost. No network calls, no account. ## Ship to Voight The hosted dashboard is one opt-in backend: ```python theme={null} import bitfrost from bitfrost.backends.voight import VoightBackend bitfrost.instrument_auto(backend=VoightBackend(api_key="vk_..."), agent="my-app") ``` Set `VOIGHT_KEY` in the environment and you can drop the `api_key` argument. Events appear under **AI Apps** alongside your other SDKs. ## Backends | Backend | Import | Use it for | | - | - | - | | Console | `bitfrost.backends.console.ConsoleBackend` | live, color-coded terminal output | | SQLite | `bitfrost.backends.sqlite.SQLiteBackend` | persistent local log; powers `bitfrost serve` | | JSONL | `bitfrost.backends.jsonl.JSONLBackend` | one JSON object per line; replay-able | | OTLP/HTTP | `bitfrost.backends.otlp.OTLPBackend` | POST events as JSON to any collector or webhook | | Voight | `bitfrost.backends.voight.VoightBackend` | hosted dashboards (opt-in) | | Tee | `bitfrost.backends.tee.TeeBackend` | fan out to several at once | ## Auto-instrument helpers ```python theme={null} import bitfrost bitfrost.instrument_openai() bitfrost.instrument_anthropic() bitfrost.instrument_litellm() bitfrost.instrument_smolagents() bitfrost.instrument_auto() # every supported lib that's installed ``` Each accepts `backend`, `agent`, `session_id`, and `privacy`. ## CLI ```bash theme={null} bitfrost watch capture.db # live tail bitfrost replay capture.jsonl # re-render a captured run bitfrost query capture.db "SELECT model, COUNT(*) FROM events GROUP BY model" bitfrost tui capture.db # full-screen terminal dashboard bitfrost serve capture.db # local web dashboard ``` ## Privacy Three levels, applied in-process before any event is sent: * `minimal` — metadata only, no prompt/response content * `standard` *(default)* — content kept, PII scrubbed (12 patterns + Luhn) * `full` — everything verbatim (local debugging only) ```python theme={null} bitfrost.instrument_openai(privacy="minimal") ``` ## Standalone by design Bitfrost's core has no dependency on any hosted service. The terminal output, SQLite log, JSONL files, CLI, TUI, and web dashboard all work with zero network access. Voight is one backend among several — recommended for managed dashboards and per-user cost attribution, never required. See the [README](https://github.com/Voightxyz/bitfrost) and the [cookbook](https://github.com/Voightxyz/bitfrost/blob/main/docs/cookbook.md) for more. # Quickstart Source: https://docs.voight.xyz/quickstart Get events from your agent or LLM app into the Voight dashboard in 30 seconds. Four install paths. Same dashboard, same privacy model, same API. Pick the one that matches what you're building. ## Step 1 — Install ### A · Coding agent (Claude Code, Cursor, Codex) A wizard writes the right config into your coding agent. No code changes. ```bash theme={null} npx -y @voightxyz/sdk setup ``` Verified working today: **Claude Code**, **Cursor**, and **Codex**. The wizard auto-detects which one you're running (or pass `--target=claude|cursor|codex`). Claude Code and Cursor are wired through hooks; Codex connects through its native OpenTelemetry export, pointed at Voight's OTLP receiver: see the [Codex guide](/sdk/codex) for details. ### B · Autonomous agent (ElizaOS, Solana Agent Kit, custom) Install the SDK as a library and call `voight.log()` from your agent code. ```bash theme={null} npm install @voightxyz/sdk ``` ```ts theme={null} import { voight } from '@voightxyz/sdk' voight.init({ key: process.env.VOIGHT_KEY }) await voight.log({ kind: 'decision', agentId: 'my-trading-bot', input: 'should I rebalance?', }) ``` Full reference in [Library mode](/sdk/library-mode). ### C · Production LLM app (OpenAI / Anthropic in user-facing flows) If your app already uses `openai` or `@anthropic-ai/sdk` (or both), wrap your clients once and every model call is captured — prompts, tokens, cache reads, tool calls, latency, errors. Per-user spend attribution with one extra line of code. Two ways to install: **C1 · Wizard** (recommended — 30 seconds) ```bash theme={null} cd your-app npx -y @voightxyz/sdk init ``` The wizard detects `openai` and `@anthropic-ai/sdk` in your `package.json`, asks for your Voight API key, validates it against `api.voight.xyz`, and writes `src/lib/voight.ts` with the wrapped clients ready to import. Tailors the usage snippet to your framework (Next.js or vanilla) and prints the right install command for your package manager (pnpm / yarn / bun / npm). Provider keys (`OPENAI_API_KEY` / `ANTHROPIC_API_KEY`) stay in your app's env — Voight never sees them. Full deep-dive in [Wizard](/ai-apps/wizard). **C2 · Manual** (you prefer to wire it yourself) Skip the wizard and install the wrappers directly: ```bash theme={null} # pick the one matching your provider (or both) npm install openai @voightxyz/openai npm install @anthropic-ai/sdk @voightxyz/anthropic ``` ```ts theme={null} import OpenAI from 'openai' import { wrapOpenAI, withTrace } from '@voightxyz/openai' const openai = wrapOpenAI(new OpenAI(), { agent: 'production-chat-api', privacy: 'standard', }) app.post('/api/chat', async (req, res) => { await withTrace( async () => { const reply = await openai.chat.completions.create({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: req.body.prompt }], }) res.json({ reply }) }, { routeTag: 'POST /api/chat', tags: { userId: req.user.id, plan: req.user.plan }, }, ) }) ``` Set `VOIGHT_KEY` in your runtime environment (or in `.env.local`). The `tags` map drives [per-user spend tracking](/concepts/per-user-spend) — the dashboard's **Users sub-tab** populates with per-customer cost as soon as your first request lands. Full reference: [OpenAI SDK](/ai-apps/openai) · [Anthropic SDK](/ai-apps/anthropic). ### D · Any language over HTTP For Python, Go, Rust, or anything that speaks JSON — POST events directly, no SDK required. ```bash theme={null} curl https://api.voight.xyz/v1/events \ -H "authorization: Bearer $VOIGHT_KEY" \ -H "content-type: application/json" \ -d '{ "kind": "decision", "agentId": "my-agent", "input": "hello" }' ``` Full schema in the [HTTP API reference](/api-reference/events). ## Step 2 — Pick your privacy level For path A the wizard prompts you. For path B pass it on `voight.init({ privacy: 'standard' })`. For path C set it on `wrapOpenAI(..., { privacy: 'standard' })`. For path D include `metadata.privacyLevel` on each request — scrubbing is your responsibility on the raw HTTP path. * **Minimal** — metadata only. No prompts, responses, file paths, cwd, or git context leave your machine. * **Standard** ★ — full content with local PII scrubbing. Credentials and personal info redacted before transmission. * **Full** — everything captured as-is. The [privacy overview](/privacy/overview) breaks down what each level keeps, scrubs, or drops, field by field. ## Step 3 — Generate your API key When the wizard asks for a key — or before you initialize the library / send your first request — open [voight.xyz/dashboard/settings](https://voight.xyz/dashboard/settings) and click **+ Generate key**. Copy the `vk_...` secret. **You'll only see it once**, so save it to a password manager. Paste it into the wizard, or set it as `VOIGHT_KEY` in your runtime environment. ## Step 4 — You're connected Open [voight.xyz/dashboard](https://voight.xyz/dashboard). Within seconds you should see: * A new agent in the **Agents** list (auto-named from your install path, renameable) * Events streaming in **Overview** and **Audit log** * Token counts, cost USD, and model tags on each event * A per-event privacy chip (`MIN` / `STD` / `FULL`) confirming which level captured it **For path C (App SDKs):** also open [voight.xyz/dashboard/ai-apps](https://voight.xyz/dashboard/ai-apps) for the dedicated production-app surface — Overview / Traces / Models / Tools / Users. The Users sub-tab populates as soon as you pass `tags.userId` inside `withTrace`. See [AI Apps overview](/ai-apps/overview) for the full surface. ## Non-TTY install for path A If `stdin` isn't an interactive TTY (running the wizard from inside a chat agent, CI, or any non-interactive shell), setup runs in **3-step progressive mode** instead of an interactive prompt: ```bash theme={null} # See the menu npx -y @voightxyz/sdk setup # Pick a privacy level npx -y @voightxyz/sdk setup --privacy=2 # Paste your key npx -y @voightxyz/sdk setup --privacy=2 --key=vk_YOUR_KEY ``` Each step prints instructions and exits cleanly. The chain reads naturally for both humans and AI agents. ## Troubleshooting 1. Confirm the SDK version: `npx -y @voightxyz/sdk@latest --version` (should be 0.4.2 or higher). 2. For path A — check your IDE's settings file (`~/.claude/settings.json` for Claude Code): `env.VOIGHT_KEY` should be set and `hooks` should contain entries calling `npx -y @voightxyz/sdk hook`. **Restart your IDE** afterwards — hooks are read on startup. 3. For path B / C — confirm your code is actually calling `voight.log()` or POSTing, and that `VOIGHT_KEY` is set in the runtime environment. 4. Check the dashboard's **Audit log** for the most recent events — they should appear within \~2 seconds of being sent. Force the latest with an explicit version: ```bash theme={null} npx -y @voightxyz/sdk@0.4.2 setup ``` Or clear the npx cache: ```bash theme={null} rm -rf ~/.npm/_npx ``` You're probably on Minimal mode, which strips content fields by design. Re-run setup with Standard or Full: ```bash theme={null} npx -y @voightxyz/sdk setup --privacy=standard ``` Or for path B / C, pass the level on `voight.init({ privacy: 'standard' })`. Some coding agents flag unfamiliar packages installed with `-y` for safety. The two-line copy block in your [dashboard onboarding](https://voight.xyz/dashboard/settings) includes a framing line ("Set up Voight observability here:") that gives the agent the intent it needs to skip the warning. ## Next * [Concepts → Events](/concepts/events) — the data model * [Privacy overview](/privacy/overview) — pick the right level for your workflow * [Library mode](/sdk/library-mode) — deep dive on path B * [OpenAI SDK](/ai-apps/openai) · [Anthropic SDK](/ai-apps/anthropic) — deep dive on path C * [Per-user spend](/concepts/per-user-spend) — one line of code to attribute cost per end-user * [HTTP API](/sdk/http-api) — deep dive on path D * [Claude Code observability guide](https://voight.xyz/guides/claude-code-observability): what to look at once events are flowing # Codex Source: https://docs.voight.xyz/sdk/codex Connect OpenAI Codex to Voight through its native OpenTelemetry export: one config block, every session lands as a full trace. [Codex](https://developers.openai.com/codex) (OpenAI's coding agent, in its CLI, TUI and Desktop forms) ships native, opt-in OpenTelemetry export. Voight runs an OTLP receiver, so connecting the two takes one config block: no plugin, no hooks, no code changes. Once connected, every Codex session streams in as a single trace: the prompt that started it, each tool call with duration and outcome, every model call with token usage (input, output, cached, reasoning), permission decisions, and API failures. The [Smart Traces](/concepts/traces) tooling (waterfall view, deterministic diagnosis, AI explain) works on Codex sessions out of the box. Verified against codex-cli 0.146. ## Setup with the wizard (recommended) ```bash theme={null} npx -y @voightxyz/sdk setup --target=codex ``` The wizard asks for your privacy level and your Voight API key (`vk_...`, from [voight.xyz/dashboard](https://voight.xyz/dashboard) → Settings → Generate key), then writes the `[otel]` block into `~/.codex/config.toml`. A one-time backup of your previous config is kept at `config.toml.voight-backup`. Restart Codex (quit the Desktop app, or start a new CLI session) and run any task. The session appears in [your dashboard](https://voight.xyz/dashboard/traces) within seconds. Running inside a Codex terminal? Plain `npx -y @voightxyz/sdk setup` auto-detects the target. ## Manual setup Append this to `~/.codex/config.toml`: ```toml theme={null} [otel] log_user_prompt = true [otel.exporter.otlp-http] endpoint = "https://api.voight.xyz/v1/otel/logs" protocol = "json" headers = { Authorization = "Bearer vk_YOUR_KEY" } ``` Two details matter: * `protocol = "json"` is required. Voight's receiver answers binary protobuf with a 415 telling you exactly this. * `log_user_prompt = true` exports the text of your prompts so traces show what each session was about. Set it to `false` and Codex sends a redacted placeholder instead (Voight never stores the placeholder). ## What gets captured | Codex telemetry | Lands in Voight as | | - | - | | `codex.conversation_starts` | Session start (model, sandbox and approval policy) | | `codex.user_prompt` | The initiating prompt (text only with `log_user_prompt = true`) | | `codex.sse_event` token summaries | One LLM call per turn: input / output / cached / reasoning tokens | | `codex.tool_result` | Tool call with name, duration, outcome and output snippet | | `codex.tool_decision` | Permission decision (denied ones get the DENIED badge) | | `codex.api_request` failures | Error events with HTTP status | Each record carries Codex's `conversation.id`, which becomes the Voight trace id: one session, one trace. Streaming deltas, websocket chatter and startup phases are deliberately dropped: they would bury the timeline in noise without telling you anything a token summary does not. ## Privacy Codex attaches your OpenAI account id and email to every telemetry record. Voight's receiver strips `user.email`, `user.account_id` and all `auth.*` attributes before anything touches storage; they are never persisted. ## Troubleshooting * **Nothing shows up**: make sure you restarted Codex after editing the config, and that the block says `protocol = "json"`. * **You already export OTel elsewhere**: Codex supports one exporter per signal. The wizard detects a foreign `[otel]` section and refuses to overwrite it; you choose which backend keeps the logs signal. * **`codex exec` (non-interactive) sessions**: log export works; Codex does not emit metrics there, which Voight does not use anyway. ## Next * [Codex observability guide](https://voight.xyz/guides/codex-observability): reading a Codex session as a trace * [Smart Traces](/concepts/traces): how traces are grouped and summarized # HTTP API Source: https://docs.voight.xyz/sdk/http-api For any runtime that can POST JSON. Voight accepts raw HTTP `POST` from any runtime that speaks JSON over HTTPS — curl, Python, Go, Rust, browsers, embedded devices. No dependency required. ## Endpoint ``` POST https://api.voight.xyz/v1/events ``` ## Authentication ``` Authorization: Bearer vk_YOUR_API_KEY Content-Type: application/json ``` Generate your `vk_` key at [voight.xyz/dashboard/settings](https://voight.xyz/dashboard/settings). ## Minimal payload ```bash theme={null} curl https://api.voight.xyz/v1/events \ -H "authorization: Bearer $VOIGHT_KEY" \ -H "content-type: application/json" \ -d '{ "agentId": "trading-bot.sol", "type": "tx", "toolExecuted": "jupiter.swap", "transaction": "4zK...9Fp" }' ``` Response: ```json theme={null} { "id": "evt_clz3...", "accepted": true, "agentId": "cmoq...", "eventId": "evt_clz3..." } ``` The server returns the resolved `agentId` (a CUID) — store it locally if you want subsequent events to skip label resolution. ## Full payload schema ```json theme={null} { "agentId": "trading-bot.sol", "timestamp": "2026-05-12T14:02:54.084Z", "type": "tx", "input": { "prompt": "...", "context": { } }, "reasoning": "free-form trace", "toolsConsidered": ["jupiter.swap", "orca.swap"], "toolExecuted": "jupiter.swap", "transaction": "4zK...9Fp", "amount": { "token": "SOL", "value": 1.5 }, "outcome": "success", "durationMs": 184, "errorMessage": "optional, when outcome=failed", "model": "claude-opus-4-7", "metadata": { "source": "my-trading-bot", "strategy": "mean-reversion", "tokensBreakdown": { "inputBase": 50, "cacheCreation": 100, "cacheRead": 850, "output": 200 }, "privacyLevel": "standard" } } ``` All fields except `agentId` are optional. The server defaults `type: 'decision'` if omitted. ## Privacy on the HTTP path The HTTP path **does not** apply PII scrubbing — that's a client-side operation that requires the SDK. If you're calling HTTP directly, you're responsible for scrubbing sensitive content before it leaves your machine. To have the dashboard render audit chips on HTTP events, include `metadata.privacyLevel: 'minimal' | 'standard' | 'full'`. The scrubbing itself is your responsibility on this path. If you're already on Node, [library mode](/sdk/library-mode) exposes `scrubPii()` directly — same patterns, no hook handler required. ## Examples ### Python ```python theme={null} import requests import os def log_event(payload): response = requests.post( "https://api.voight.xyz/v1/events", headers={ "authorization": f"Bearer {os.environ['VOIGHT_KEY']}", "content-type": "application/json", }, json=payload, timeout=5, ) response.raise_for_status() return response.json() log_event({ "agentId": "py-bot", "type": "action", "toolExecuted": "fetch_market_data", "outcome": "success", "durationMs": 412, }) ``` ### Go ```go theme={null} package main import ( "bytes" "encoding/json" "net/http" "os" ) func logEvent(payload map[string]any) error { body, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", "https://api.voight.xyz/v1/events", bytes.NewBuffer(body)) req.Header.Set("Authorization", "Bearer "+os.Getenv("VOIGHT_KEY")) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() return nil } ``` ### Rust ```rust theme={null} use reqwest::Client; use serde_json::json; async fn log_event(payload: serde_json::Value) -> reqwest::Result<()> { let client = Client::new(); let api_key = std::env::var("VOIGHT_KEY").unwrap(); client.post("https://api.voight.xyz/v1/events") .bearer_auth(&api_key) .json(&payload) .send() .await? .error_for_status()?; Ok(()) } ``` ### Browser / fetch ```ts theme={null} await fetch('https://api.voight.xyz/v1/events', { method: 'POST', headers: { 'authorization': `Bearer ${VOIGHT_KEY}`, 'content-type': 'application/json', }, body: JSON.stringify({ agentId: 'browser-agent', reasoning: 'user clicked the swap button', }), }) ``` CORS is configured to allow `https://voight.xyz` and `https://www.voight.xyz`. For other origins, you'll need server-side proxying. ## Status codes | Code | Meaning | | - | - | | `202 Accepted` | Event received and persisted (returns `{ id, accepted: true, agentId }`) | | `400 Bad Request` | Payload failed Zod validation (response includes details) | | `401 Unauthorized` | Missing or invalid API key | | `410 Gone` | The agent has been soft-deleted, ingestion blocked | | `429 Too Many Requests` | Rate-limited (honor `Retry-After` header) | | `500 Server Error` | Backend issue — retry with exponential backoff | ## Rate limits Per pricing tier: | Tier | Events / month | | - | - | | Free | 10,000 | | Pro | 300,000 | | Enterprise | Unlimited | Quotas are advisory today, enforced server-side starting v1.0. Burst rate-limiting (per-second) is in place to prevent abuse. ## Next * [Library mode](/sdk/library-mode) — for JS/TS; adds retry, typed errors, and PII scrubbing * [`POST /v1/events` reference](/api-reference/events) — full schema with every field documented * [Debug failed n8n workflows](https://voight.xyz/guides/debug-failed-n8n-workflows): an n8n error workflow that sends every failure here with one HTTP Request node # Library mode Source: https://docs.voight.xyz/sdk/library-mode Import @voightxyz/sdk in TypeScript/JavaScript agents you build yourself. For agents you build yourself in TypeScript or JavaScript (autonomous bots, workflow agents, custom frameworks), import the SDK and call `voight.log()` directly. Library mode is provider-agnostic — wrap any LLM call (OpenAI / Anthropic / [GLM](/ai-apps/glm) / Mistral / your-own-proxy) and emit the event yourself. The backend `MODEL_PRICING` table ships entries for the major providers, so cost / token attribution lights up automatically as long as you set `model` on the event. ## Install ```bash theme={null} npm install @voightxyz/sdk ``` Requirements: * Node.js 18+ (uses global `fetch`) * Bun, Deno, browsers, and Cloudflare Workers all supported ## Quick start ```ts theme={null} import { Voight } from '@voightxyz/sdk' const voight = new Voight({ agentId: 'trading-bot.sol', // SNS domain recommended for autonomous agents apiKey: process.env.VOIGHT_KEY, // from voight.xyz/dashboard }) await voight.log({ reasoning: 'SOL/USDC spread 8bps — arb window open', toolExecuted: 'jupiter.swap', transaction: '4zK…9Fp', amount: { token: 'SOL', value: 1.5 }, outcome: 'success', }) ``` Returns `{ ok: true, eventId, agentId }` on success, `{ ok: false, error: { code, message } }` on failure. ## Constructor options | Option | Type | Default | Purpose | | - | - | - | - | | `agentId` | string | — | Your agent's stable identifier. Recommend a SNS domain for autonomous bots (`trading-bot.sol`), a stable label for services (`market-watcher`). | | `apiKey` | string | — | From the dashboard. Required for ingestion. | | `endpoint` | string | `https://api.voight.xyz` | Override for self-hosted deployments. | | `defaults` | object | `{}` | Metadata merged into every event (e.g. `{ env: 'prod', region: 'us-east' }`). | | `swallowErrors` | boolean | `true` | When true, `voight.log()` never throws — failures return `{ ok: false }`. Recommended for production. | | `fetch` | function | `globalThis.fetch` | Inject a custom fetch (for tests, older runtimes). | | `privacyLevel` | `'minimal' \| 'standard' \| 'full'` | `'full'` | For library mode, defaults to Full. Pass `'standard'` to get local PII scrubbing. | ## The `log()` method ```ts theme={null} interface LogInput { type?: 'reasoning' | 'tool' | 'tx' | 'decision' | 'action' | 'error' input?: { prompt?: string; context?: Record } reasoning?: string toolsConsidered?: string[] toolExecuted?: string transaction?: string // transaction signature or reference, if applicable amount?: { token: string; value: number } outcome?: 'pending' | 'success' | 'failed' durationMs?: number errorMessage?: string model?: string tokens?: { input?: number; output?: number; total?: number } metadata?: Record // anything custom timestamp?: string | number // ISO string or unix ms } ``` All fields optional. The server normalises and defaults `type: 'decision'` if you don't pass one. Retry behaviour: * 3 attempts by default * Exponential backoff * Respects `Retry-After` header on 429s * Swallowed by default (`swallowErrors: true`) — recommended for production agents that shouldn't crash on observability failures ## Examples ### Trading bot ```ts theme={null} const voight = new Voight({ agentId: 'arbitrage.sol', apiKey: process.env.VOIGHT_KEY, defaults: { strategy: 'mean-reversion', env: 'prod' }, }) // 1. Reasoning step await voight.log({ type: 'reasoning', reasoning: `SOL/USDC spread at ${spreadBps}bps, threshold ${thresholdBps}bps — open arb`, }) // 2. Tool / tx await voight.log({ type: 'tx', toolExecuted: 'jupiter.swap', transaction: txSignature, amount: { token: 'SOL', value: 1.5 }, outcome: 'success', }) // 3. Error try { await maybeFlakyOperation() } catch (err) { await voight.log({ type: 'error', outcome: 'failed', errorMessage: (err as Error).message, }) } ``` ### ElizaOS character (skill plugin) ```ts theme={null} // Until @voightxyz/eliza-skill ships, you can wire it manually: import { Voight } from '@voightxyz/sdk' const voight = new Voight({ agentId: 'eliza-research-bot', apiKey: process.env.VOIGHT_KEY, }) agent.on('beforeAction', async (action) => { await voight.log({ type: 'action', toolsConsidered: [action.name] }) }) agent.on('afterAction', async (action, result) => { await voight.log({ type: 'action', toolExecuted: action.name, outcome: result.ok ? 'success' : 'failed', durationMs: result.elapsed, }) }) ``` A native ElizaOS plugin (`@voightxyz/eliza-skill`) is on the roadmap and will simplify this to one line. ### LangChain / LangGraph agent ```ts theme={null} import { Voight } from '@voightxyz/sdk' const voight = new Voight({ agentId: 'lc-agent', apiKey: process.env.VOIGHT_KEY }) const callbacks = [{ handleToolStart: async (tool, input) => { await voight.log({ type: 'action', toolsConsidered: [tool.name] }) }, handleToolEnd: async (output) => { await voight.log({ type: 'action', outcome: 'success' }) }, handleLLMEnd: async ({ generations, llmOutput }) => { await voight.log({ type: 'reasoning', reasoning: generations[0]?.[0]?.text, tokens: { input: llmOutput?.tokenUsage?.promptTokens, output: llmOutput?.tokenUsage?.completionTokens, }, }) }, }] ``` ## Error types ```ts theme={null} type VoightError = | { code: 'invalid_payload'; message: string } // your payload failed Zod validation | { code: 'unauthorized'; message: string } // bad API key | { code: 'rate_limited'; message: string } // 429 | { code: 'network'; message: string } // fetch failed | { code: 'server'; message: string } // 5xx | { code: 'unknown'; message: string } ``` When `swallowErrors: false`, these throw. Otherwise they return as `{ ok: false, error }`. ## Privacy in library mode By default, library callers send events as **Full** (no scrubbing). To opt into Standard scrubbing per-event: ```ts theme={null} import { Voight, scrubPii } from '@voightxyz/sdk' // Manual scrubbing await voight.log({ reasoning: scrubPii(`API call with token ${apiKey}`), }) // Or set a privacy level at construction time and let the SDK handle it const voight = new Voight({ agentId: 'my-bot', apiKey: process.env.VOIGHT_KEY, privacyLevel: 'standard', // applies scrubPii to every event automatically }) ``` The same 12 PII patterns + Luhn-validated card detection apply. See the [PII patterns reference](/privacy/pii-patterns). ## `check()` and `enforce()` — v1.0 ```ts theme={null} const decision = await voight.check({ action: 'swap', context: { amount: 50 } }) if (!decision.allow) return // blocked by policy ``` Today these return `{ allow: true, reason: 'not-implemented' }` so you can instrument call sites now and flip a flag later. Real implementation ships in v1.0. ## Next * [HTTP API](/sdk/http-api) — for runtimes that don't use npm at all * [`POST /v1/events`](/api-reference/events) — full HTTP API schema for event ingestion # GDPR Source: https://docs.voight.xyz/trust/gdpr How Voight aligns with EU Regulation 2016/679 — controller identity, lawful basis, rights, transfers, and the full compliance documentation as a downloadable PDF. Voight processes personal data in line with **Regulation (EU) 2016/679** (the General Data Protection Regulation). This page summarises our posture; the full technical and procedural detail is in the downloadable PDF below (35 pages, versioned, dated). ## Quick reference | Field | Value | | - | - | | Data Controller | Voight Inc. (USA) | | GDPR Main Establishment | Spain (Article 4(16)) | | Supervisory Authority | [AEPD — Agencia Española de Protección de Datos](https://www.aepd.es) | | Lawful Basis | Contract — Article 6(1)(b) | | Sub-processors | Vercel, Railway, Privy | | International Transfers | 2021 EU Standard Contractual Clauses | | Privacy Contact | `team@voight.xyz` | | Document Version | 1.0 — May 2026 | ## Download the full document **35 pages · 130 KB · Version 1.0 · May 2026** Full alignment record covering controller identity, scope of processing, lawful basis, data subject rights, international transfers (TIA included), breach procedure, sub-processor register, and Records of Processing Activities (Article 30). ## What's in the PDF The full compliance document covers, in order: 1. Document control and revision history 2. Executive summary 3. Identity of the Controller — legal form, establishment, supervisory authority 4. Scope of processing — Voight as controller (account data) vs Voight as processor (telemetry) 5. Lawful basis under Article 6 6. Purposes of processing 7. Data subject rights — how to exercise each of the ten rights under Articles 12–22 8. Data retention — tier-based (7d Free / 90d Pro / 1y Enterprise) 9. Security measures (Article 32) — encryption, access control, PII scrubbing 10. Sub-processors register (Article 28) 11. International data transfers — 2021 SCCs + supplementary measures (Articles 44–49) 12. Personal data breach procedure (Articles 33–34) 13. Cookies and ePrivacy 14. Children's data (Article 8) 15. Records of Processing Activities (Article 30) 16. Governance and accountability Plus four annexes — sub-processor register, personal data catalogue, Transfer Impact Assessment summary, and document version history. ## Key commitments at a glance * **One lawful basis.** Voight relies only on contract performance (Article 6(1)(b)). No consent fatigue, no behavioural profiling, no marketing opt-ins. * **Privacy by design.** The SDK runs three local privacy filters before any payload leaves the customer process. See [Privacy overview](/privacy/overview) and [PII patterns](/privacy/pii-patterns). * **Minimal sub-processors.** Three only: Vercel (hosting), Railway (database), Privy (auth). All SOC 2 Type II, all with published GDPR DPAs. * **No data sale.** Personal data is never sold, never shared with advertisers, never used for any purpose other than delivering the service. * **30-day response window.** Data subject requests are honoured within 30 days as required by Article 12(3). * **No DPO and no EU Representative required.** Voight does not meet the Article 37 DPO threshold and is established in the EU via Spain, removing the Article 27 representative requirement. ## How to exercise your rights Email `team@voight.xyz` from the address tied to your account. State the right you wish to exercise (access, rectification, erasure, restriction, portability, objection) and any specific detail. We respond within 30 days. For erasure of specific agents and their events, the dashboard offers self-service controls — see [Data handling](/privacy/data-handling). Account-level deletion is handled by email. ## Lodge a complaint If you believe Voight has not honoured your rights, you can lodge a complaint with our lead supervisory authority: **Agencia Española de Protección de Datos (AEPD)** C/ Jorge Juan, 6, 28001, Madrid, Spain [www.aepd.es](https://www.aepd.es) · [sedeagpd.gob.es](https://sedeagpd.gob.es) Or with the supervisory authority of your country of residence (Article 77 GDPR). ## See also * [Privacy overview](/privacy/overview) — the 3-level capture model * [PII patterns](/privacy/pii-patterns) — the 13 regexes Standard mode runs * [Data handling](/privacy/data-handling) — storage, retention, deletion * [Trust & Security](/trust/overview) — all our compliance frameworks # NIST AI RMF Source: https://docs.voight.xyz/trust/nist-ai-rmf How Voight aligns with the NIST AI Risk Management Framework (AI RMF 1.0) and its Generative AI Profile — mapped across all four functions, with Voight sitting in the Measure layer. Voight aligns with the **NIST AI Risk Management Framework (AI RMF 1.0)** and its **Generative AI Profile** (NIST-AI-600-1). The full document maps Voight across all four framework functions and is honest about where an observability platform fits — and where it doesn't. ## Quick reference | Field | Value | | - | - | | Framework | NIST AI RMF 1.0 (AI 100-1) | | Companion | Generative AI Profile (NIST-AI-600-1) | | Home function | **Measure** — Voight is a measurement instrument | | Also supports | Map (operational ground truth), Manage (alerts + traces) | | Informs | Govern (evidence, not governance) | | Honest gap | Bias & fairness evaluation — application-level, not Voight | | Security Contact | `team@voight.xyz` | | Document Version | 1.0 — June 2026 | ## Download the full document **19 pages · Version 1.0 · June 2026** All four AI RMF functions mapped to concrete Voight capabilities, a dedicated Generative AI Profile section, a coverage matrix, the seven trustworthiness characteristics, and an explicit statement of what Voight does *not* do. ## Where Voight sits in the four functions The AI RMF organises AI risk management into four functions. Voight's alignment is concentrated — and honest about it: | Function | Voight role | Strength | | - | - | :-: | | **GOVERN** | Informs with evidence | Supporting | | **MAP** | Supplies real production behaviour | Moderate–Strong | | **MEASURE** | Implements (this is Voight's home) | **Strongest** | | **MANAGE** | Supplies alerts, traces, records | Strong | An observability platform is a **Measure** instrument that radiates outward into Map and Manage, and informs Govern with evidence. It is not — and does not claim to be — a governance programme. ## The honest framing Two principles run through the document: 1. **Voight is a Measure-function tool first.** Measurement and tracking of AI risk over time is, almost exactly, what an observability platform does. Voight supports Map and Manage with operational evidence and informs Govern — but does not constitute governance. 2. **Observability is evidence, not governance.** Voight produces the data and audit trail an AI RMF programme runs on. It does not set your risk tolerance, write your policies, or make your risk-treatment decisions. Those stay with your organisation. ## Generative AI Profile The document includes a dedicated section on the NIST Generative AI Profile (NIST-AI-600-1, July 2024). Because Voight is built specifically to observe generative-AI applications, several Profile risk areas — information security, data privacy, value-chain integration, and resource consumption — fall squarely in its measurement remit. ## Voight's own AI governance Today, Voight does not operate an AI system within its own product — it observes its customers' AI systems. The document records Voight's platform-governance baseline and a forward commitment: when Voight's roadmap AI features (Smart Trace, Prompt Scorer, Debug Agent) reach production, this document will be re-versioned to apply the AI RMF to each before it ships. ## See also * [GDPR](/trust/gdpr) — Voight's data protection alignment * [OWASP LLM Top 10](/trust/owasp-llm) — risk-by-risk security alignment * [Privacy overview](/privacy/overview) — the 3-level local capture model * [Trust & Security](/trust/overview) — all our compliance frameworks # Trust & Security Source: https://docs.voight.xyz/trust/overview How Voight aligns with the major data protection and AI safety frameworks — published, versioned, and downloadable. Voight publishes its compliance posture against the major data protection and AI safety frameworks. Each one below is a public document, versioned and dated, that you can review before deciding to send Voight your team's telemetry. All four frameworks are published, with a full downloadable PDF each. **Available · v1.0 · May 2026** Full alignment with Regulation (EU) 2016/679. Spain main establishment, AEPD supervisory authority, 2021 SCCs for international transfers, 30-day rights window. [Read summary →](/trust/gdpr) **Available · v1.0 · May 2026** Alignment against the OWASP Top 10 for LLM Applications (v2.0). A risk-by-risk map of where Voight helps you detect, investigate, and constrain each risk — and an honest account of the two it can't. [Read summary →](/trust/owasp-llm) **Available · v1.0 · June 2026** Mapping of Voight against the NIST AI Risk Management Framework 1.0 and its Generative AI Profile — all four functions, with Voight in the Measure layer. [Read summary →](/trust/nist-ai-rmf) **Available · v1.0 · June 2026** Controls mapped to the AICPA Trust Services Criteria — Security, Availability, Confidentiality — with six known gaps published, not hidden. Voight is not Type II certified today; the audit is on the roadmap. [Read summary →](/trust/soc2-readiness) ## Why we publish these Three reasons: 1. **It's the right thing to do.** Customers handing us telemetry deserve to see how we treat it before they send the first event. 2. **It saves your procurement team time.** A versioned PDF is faster to forward to your auditor than a back-and-forth email thread. 3. **It keeps us honest.** Publishing a control set is the cheapest way to keep that control set true over time. ## What ties it all together The same principles run through every document: * **One lawful basis** for personal data: contract performance. * **Three sub-processors**, all SOC 2 Type II: Vercel, Railway, Privy. * **Privacy by design**: the SDK runs three local filters before any payload leaves your machine — see [Privacy overview](/privacy/overview). * **No data sale.** Personal data is never sold or shared with advertisers. * **Versioned, dated documents.** When a material change happens, we publish a new version and announce it in the [changelog](/changelog). ## Reporting concerns Found a security issue, a privacy concern, or something missing from these documents? Email `team@voight.xyz`. We acknowledge within 48 hours. For data protection complaints, the formal route is through our lead supervisory authority, the Spanish AEPD ([www.aepd.es](https://www.aepd.es)). # OWASP LLM Top 10 Source: https://docs.voight.xyz/trust/owasp-llm How Voight aligns with the OWASP Top 10 for LLM Applications (v2.0) — a risk-by-risk map of where Voight helps you detect, investigate, and constrain each risk, and an honest account of the two it can't address. Voight relates to the **OWASP Top 10 for LLM Applications (v2.0)** from two angles, and the full document addresses both honestly: how Voight's own platform is secured, and how Voight helps you address each risk in your own LLM application. ## Quick reference | Field | Value | | - | - | | Framework | OWASP Top 10 for LLM Applications, v2.0 | | Voight's own role | Not an LLM application today; observability platform | | Strongest alignments | LLM02 (Sensitive Info Disclosure), LLM10 (Unbounded Consumption) | | Honest gaps | LLM04 (Data/Model Poisoning), LLM08 (Vector/Embedding) | | Posture | Detection & monitoring — not inline prevention | | Security Contact | `team@voight.xyz` | | Document Version | 1.0 — May 2026 | ## Download the full document **19 pages · Version 1.0 · May 2026** A risk-by-risk map of all ten OWASP LLM risks, Voight's own security posture, a coverage matrix, and an explicit statement of what Voight does *not* do. ## The honest framing Two principles run through the document: 1. **Observability is detection and monitoring, not prevention.** For most risks, Voight helps you *see* and *investigate* a problem — it does not sit inline to *block* it. Where this matters, the document says so. 2. **No claimed coverage we don't have.** Two of the ten risks fall outside what an observability platform addresses, because Voight neither trains models nor manages vector stores. The document states this plainly rather than stretch a weak angle. ## Coverage at a glance | Code | Risk | Voight alignment | | - | - | :-: | | LLM01 | Prompt Injection | Strong (detective) | | LLM02 | Sensitive Information Disclosure | **Strongest** | | LLM03 | Supply Chain | Moderate | | LLM04 | Data and Model Poisoning | Limited | | LLM05 | Improper Output Handling | Strong (detective) | | LLM06 | Excessive Agency | Strong (detective) | | LLM07 | System Prompt Leakage | Strong (telemetry) | | LLM08 | Vector and Embedding Weaknesses | Limited | | LLM09 | Misinformation | Moderate | | LLM10 | Unbounded Consumption | **Strong** | ## Voight's own posture Today, Voight does not operate a large language model within its own product — it is the observability platform that *receives* LLM telemetry. The document records Voight's platform security baseline (shared with the [GDPR documentation](/trust/gdpr)) and a forward commitment: when Voight's roadmap LLM features (Smart Trace, Prompt Scorer, Debug Agent) reach production, this document will be re-versioned to assess each against the relevant risks, holding our own features to the same standard we help our customers meet. ## See also * [GDPR](/trust/gdpr) — Voight's data protection alignment * [Privacy overview](/privacy/overview) — the 3-level local capture model behind LLM02 * [PII patterns](/privacy/pii-patterns) — the 13 patterns Standard mode scrubs * [Trust & Security](/trust/overview) — all our compliance frameworks # SOC 2 Readiness Source: https://docs.voight.xyz/trust/soc2-readiness Voight's controls mapped to the AICPA Trust Services Criteria — Security, Availability, Confidentiality — with known gaps published, not hidden. The honest answer to 'do you have SOC 2?' **Voight is not SOC 2 certified — and this page doesn't pretend otherwise.** What we publish instead is a full **readiness assessment**: every implemented control mapped to the Trust Services Criteria a SOC 2 auditor would evaluate, with the gaps stated plainly. A Type II examination is on the roadmap, triggered by enterprise demand. ## Quick reference | Field | Value | | - | - | | Framework | AICPA Trust Services Criteria (2017, rev. 2022) | | Criteria in scope | Security (Common Criteria) · Availability · Confidentiality | | Out of scope | Processing Integrity · Privacy (covered by [GDPR](/trust/gdpr), a stricter regime) | | Certification status | **Not certified** — readiness phase, Type II on roadmap | | Inherited attestations | Vercel, Railway, Privy — all SOC 2 Type II | | Known gaps | 6, published in §6 of the document | | Security Contact | `team@voight.xyz` | | Document Version | 1.0 — June 2026 | ## Download the full document **17 pages · Version 1.0 · June 2026** Control-by-control mapping across CC1–CC9, Availability, and Confidentiality; the six known gaps; inherited vendor controls; and the documented path to a Type II report. ## Readiness vs. certified — the difference | | Readiness (this) | SOC 2 Type II | | - | - | - | | Produced by | Voight (self-assessment) | Licensed CPA firm | | Attests | Controls designed & documented | Controls operated effectively over 3–12 months | | Independent verification | None | Yes | A readiness document that lists no gaps is not credible. Ours lists six — including the structural ones (segregation of duties in a founding-size team, no independent pentest yet) — together with the path to closing each. ## What's already in place * **Encryption everywhere** — TLS 1.3 in transit, AES-256 at rest, API keys hashed * **No password storage** — authentication delegated to Privy (SOC 2 Type II) * **Local-first privacy** — 3-level PII scrubbing before telemetry leaves your process * **Incident response** — written procedure with T+0 → T+7d timeframes * **Supply chain** — Dependabot monitoring, npm provenance attestations, defined remediation SLAs * **Audited foundations** — all three infrastructure vendors hold current SOC 2 Type II reports ## The path to a Type II report 1. Readiness (this document) ✓ 2. Gap closure (tracked in revisions) 3. Compliance platform onboarding (continuous evidence) 4. Optional Type I examination 5. 3–12 month observation window 6. Type II report — available to customers under NDA We have deliberately not committed to a public date. The trigger is enterprise demand: when a material engagement requires the report, the examination gets scheduled and this page gets updated. ## See also * [GDPR](/trust/gdpr) — data protection alignment (covers the Privacy criterion) * [OWASP LLM Top 10](/trust/owasp-llm) — LLM security alignment * [NIST AI RMF](/trust/nist-ai-rmf) — AI risk management alignment * [Trust & Security](/trust/overview) — all our compliance frameworks