.cursor/skills/add-hosted-key/SKILL.md
When a tool has hosted key support, Sim provides its own API key if the user hasn't configured one (via BYOK or env var). Usage is metered and billed to the workspace.
| Step | What | Where |
|---|---|---|
| 1 | Register BYOK provider ID | tools/types.ts, app/api/workspaces/[id]/byok-keys/route.ts |
| 2 | Research the API's pricing and rate limits | API docs / pricing page (before writing any code) |
| 3 | Add hosting config to the tool | tools/{service}/{action}.ts |
| 4 | Hide API key field when hosted | blocks/blocks/{service}.ts |
| 5 | Add to BYOK settings UI | BYOK settings component (byok.tsx) |
| 6 | Summarize pricing and throttling comparison | Output to user (after all code changes) |
Add the new provider to the BYOKProviderId union in tools/types.ts:
export type BYOKProviderId =
| 'openai'
| 'anthropic'
// ...existing providers
| 'your_service'
Then add it to VALID_PROVIDERS in app/api/workspaces/[id]/byok-keys/route.ts:
const VALID_PROVIDERS = ['openai', 'anthropic', 'google', 'mistral', 'your_service'] as const
Before writing any getCost or rateLimit code, look up the service's official documentation for both pricing and rate limits. You need to understand:
creditsUsed, costDollars, tokensUsed, or similar in the response body or headersRetry-After header, error body format, etc.Search the API's docs/pricing page (use WebSearch/WebFetch). Capture the pricing model as a comment in getCost so future maintainers know the source of truth.
Our rate limiter (lib/core/rate-limiter/hosted-key/) uses a token-bucket algorithm applied per billing actor (workspace). It supports two modes:
per_request — simple; just requestsPerMinute. Good when the API charges flat per-request or cost doesn't vary much.custom — requestsPerMinute plus additional dimensions (e.g., tokens, search_units). Each dimension has its own limitPerMinute and an extractUsage function that reads actual usage from the response. Use when the API charges on a variable metric (tokens, credits) and you want to cap that metric too.When choosing values for requestsPerMinute and any dimension limits:
N hosted keys, so the effective API-side rate per key is (total requests) / N. But per-workspace limits are enforced before key selection, so they apply regardless of key count.hosting Config to the ToolAdd a hosting object to the tool's ToolConfig. This tells the execution layer how to acquire hosted keys, calculate cost, and rate-limit.
hosting: {
envKeyPrefix: 'YOUR_SERVICE_API_KEY',
apiKeyParam: 'apiKey',
byokProviderId: 'your_service',
pricing: {
type: 'custom',
getCost: (_params, output) => {
if (output.creditsUsed == null) {
throw new Error('Response missing creditsUsed field')
}
const creditsUsed = output.creditsUsed as number
const cost = creditsUsed * 0.001 // dollars per credit
return { cost, metadata: { creditsUsed } }
},
},
rateLimit: {
mode: 'per_request',
requestsPerMinute: 100,
},
},
Keys use a numbered naming pattern driven by a count env var:
YOUR_SERVICE_API_KEY_COUNT=3
YOUR_SERVICE_API_KEY_1=sk-...
YOUR_SERVICE_API_KEY_2=sk-...
YOUR_SERVICE_API_KEY_3=sk-...
The envKeyPrefix value (YOUR_SERVICE_API_KEY) determines which env vars are read at runtime. Adding more keys only requires bumping the count and adding the new env var.
Always prefer using cost data returned by the API (e.g., creditsUsed, costDollars). This is the most accurate because it accounts for variable pricing tiers, feature modifiers, and plan-level discounts.
When the API reports cost — use it directly and throw if missing:
pricing: {
type: 'custom',
getCost: (params, output) => {
if (output.creditsUsed == null) {
throw new Error('Response missing creditsUsed field')
}
// $0.001 per credit — from https://example.com/pricing
const cost = (output.creditsUsed as number) * 0.001
return { cost, metadata: { creditsUsed: output.creditsUsed } }
},
},
When the API does NOT report cost — compute it from params/output based on the pricing docs, but still validate the data you depend on:
pricing: {
type: 'custom',
getCost: (params, output) => {
if (!Array.isArray(output.searchResults)) {
throw new Error('Response missing searchResults, cannot determine cost')
}
// Serper: 1 credit for <=10 results, 2 credits for >10 — from https://serper.dev/pricing
const credits = Number(params.num) > 10 ? 2 : 1
return { cost: credits * 0.001, metadata: { credits } }
},
},
getCost must always throw if it cannot determine cost. Never silently fall back to a default — this would hide billing inaccuracies.
If the API returns cost info, capture it in transformResponse so getCost can read it from the output:
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
results: data.results,
creditsUsed: data.creditsUsed, // pass through for getCost
},
}
},
For async/polling tools, capture it in postProcess when the job completes:
if (jobData.status === 'completed') {
result.output = {
data: jobData.data,
creditsUsed: jobData.creditsUsed,
}
}
In the block config (blocks/blocks/{service}.ts), add hideWhenHosted: true to the API key subblock. This hides the field on hosted Sim since the platform provides the key:
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your API key',
password: true,
required: true,
hideWhenHosted: true,
},
The visibility is controlled by isSubBlockHiddenByHostedKey() in lib/workflows/subblocks/visibility.ts, which checks the isHosted feature flag.
When a block has multiple operations but some operations should not use a hosted key (e.g., the underlying API is deprecated, unsupported, or too expensive), use the duplicate apiKey subblock pattern. This is the same pattern Exa uses for its research operation:
hosting config from the tool definition for that operation — it must not have a hosting object at all.apiKey subblock in the block config with opposing conditions:// API Key — hidden when hosted for operations with hosted key support
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your API key',
password: true,
required: true,
hideWhenHosted: true,
condition: { field: 'operation', value: 'unsupported_op', not: true },
},
// API Key — always visible for unsupported_op (no hosted key support)
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your API key',
password: true,
required: true,
condition: { field: 'operation', value: 'unsupported_op' },
},
Both subblocks share the same id: 'apiKey', so the same value flows to the tool. The conditions ensure only one is visible at a time. The first has hideWhenHosted: true and shows for all hosted operations; the second has no hideWhenHosted and shows only for the excluded operation — meaning users must always provide their own key for that operation.
To exclude multiple operations, use an array: { field: 'operation', value: ['op_a', 'op_b'] }.
Reference implementations:
blocks/blocks/exa.ts): research operation excluded from hosting — lines 309-329blocks/blocks/google_maps.ts): speed_limits operation excluded from hosting (deprecated Roads API)Add an entry to the PROVIDERS array in the BYOK settings component so users can bring their own key. You need the service icon from components/icons.tsx:
{
id: 'your_service',
name: 'Your Service',
icon: YourServiceIcon,
description: 'What this service does',
placeholder: 'Enter your API key',
},
After all code changes are complete, output a detailed summary to the user covering:
getCost approach — how we calculate cost, what fields we depend on, and any assumptions or estimates (especially when the API doesn't report exact dollar cost).rateLimit config — what we set for requestsPerMinute (and dimensions if custom mode), why we chose those values, and how they compare to the API's limits.Present this as a structured summary with clear headings. Example:
### Pricing
- **API charges**: $X per 1M tokens (input), $Y per 1M tokens (output) — varies by model
- **Response reports cost?**: No — only token counts in `usage` field
- **Our getCost**: Estimates cost at $Z per 1M total tokens based on median model pricing
- **Risk**: Actual cost varies by model; our estimate may over/undercharge for cheap/expensive models
### Throttling
- **API limits**: 300 RPM per key (paid tier), 60 RPM (free tier)
- **Per-key or per-account**: Per key — more keys = more throughput
- **Our config**: 60 RPM per workspace (per_request mode)
- **With N keys**: Effective per-key rate is (total RPM across workspaces) / N
- **Headroom**: Comfortable — even 10 active workspaces at full rate = 600 RPM / 3 keys = 200 RPM per key, under the 300 RPM API limit
This summary helps reviewers verify that the pricing and rate limiting are well-calibrated and surfaces any risks that need monitoring.
BYOKProviderId in tools/types.tsVALID_PROVIDERS in the BYOK keys API routehosting config added to the tool with envKeyPrefix, apiKeyParam, byokProviderId, pricing, and rateLimitgetCost throws if required cost data is missing from the responsetransformResponse or postProcess if API provides ithideWhenHosted: true added to the API key subblock in the block config{PREFIX}_COUNT and {PREFIX}_1..N