A workspace spends its own key on the paid built-in tools
Release notes 2026-09-15 — vocion-core#316 (merged): web search, page fetch, crawling and image generation bill the workspace's own Tavily, Brave, Firecrawl or OpenAI key when one is stored, and the server's key only when none is. One definition of spendable; null and failure are different answers.
Shipped: vocion-core#316 (with 2b300239, which lets a workspace store the keys), merged to main. Part of the 2026-09-15 release notes.
The outcome
On a shared deployment, a tenant's agents should spend the tenant's money. Web search, page fetching, site crawling and image generation bill per call; until now every call spent the deployment operator's key, whoever asked. A workspace that pastes its own Tavily, Brave, Firecrawl or OpenAI key now spends its own account on those calls — exactly as it already did for its model provider key.
How the key is chosen
Three layers, each with one job:
| Layer | Answers | Reads env? |
|---|---|---|
spendablePlatformKey(orgId, platform) in ApiTokenService | does this org hold a key a call could spend? | no |
resolveToolProviderKey(provider, orgId) in packages/core/src/libs/tools/orgKey.ts | the same question, by tool provider name | no |
the provider module (tavily.ts, …) | which key this call spends | yes |
The choice is one line beside the call that spends it:
const orgKey = opts?.orgId ? await resolveToolProviderKey('tavily', opts.orgId) : null;
const apiKey = orgKey ?? process.env.TAVILY_API_KEY;
The two rules the PR is careful about
One definition of "spendable". "Give me the key" and "is there one?" are two questions about the same row; answering them in separate code lets them disagree. A live api_token row is only half the answer — it must also decrypt with the current vault key — so both questions go through one function.
Null and failure are different answers. null means "no key stored", and ?? correctly falls through to the server's key. A vault that will not open means "this workspace may hold a key we cannot see", and that is an error, not a fall-through — silently billing the operator for a tenant whose key exists but is unreadable is the failure this design refuses.
Where a workspace sets it
The dashboard's tool-provider key card (the same place a workspace stores its model key). Keys live in the credential vault, encrypted with VOCION_CREDENTIAL_VAULT_KEY; the server never logs them.
Tenant impact
None for a workspace with no stored tool key: the server's key is used as before. A workspace that stores one starts paying its own provider for those calls on the next turn.
This replaced an eight-PR stack with one commit against main, because the stack made each piece small at the cost of making the whole thing hard to follow — the PR's review history explains the trade.
Where this lives in the repo
- Resolution:
libs/tools/orgKey.ts(resolveToolProviderKey),services/ApiTokenService.ts(spendablePlatformKey) - Providers that spend it:
libs/tools/—tavily.ts,brave.ts,firecrawl.ts, the image tool - Platform registry (which providers a workspace may store a key for):
libs/platforms/registry.ts - The dashboard card:
features/tools/ToolProviderKeyCard.tsx(+ test) - PRs and commits: #316,
2b300239