Raycast: an extension using Claude and GPT on your own key
Raycast AI bills by subscription seat; your own key bills by actual usage. Building a TypeScript extension that talks to the API directly, bypassing built-in AI.
4 min · · Updated · IDE · инструменты
Built-in Raycast AI is a flat-rate subscription, not billing by actual usage. If you want specific control over which model answers and what it costs, it's simpler to write your own extension that calls the API directly with your own key — Raycast allows this as a plain TypeScript extension with its own network calls.
Extension structure
raycast-zukko/
package.json
src/
ask.tsx
package.json describes the command and its configuration — the Preferences panel where the user's key goes:
{
"name": "zukko-ask",
"title": "Ask AI",
"commands": [
{ "name": "ask", "title": "Ask", "mode": "view" }
],
"preferences": [
{
"name": "apiKey",
"type": "password",
"title": "Zukko API Key",
"description": "A key like fetch_...",
"required": true
},The key is stored as a password-type preference — Raycast encrypts it in its own store rather than keeping it in plain text in the extension's config file.
The ask.tsx command
import { ActionPanel, Action, Form, getPreferenceValues, showToast, Toast } from "@raycast/api";
import { useState } from "react";
interface Prefs { apiKey: string; model: string; }
export default function Command() {
const { apiKey, model } = getPreferenceValues<Prefs>();
const [answer, setAnswer] = useState("");
async function ask(prompt: string) {
const toast = await showToast({ style: Toast.Style.Animated, title: "Asking..." });
const r = await fetch("https://api.zukko.pro/v1/chat/completions", {
method: "POST",A fast model for an interface that lives on speed
Raycast is all about instant response, which is why the example defaults to the light glm-5.3-flash ($0.04 per million tokens) instead of a heavier, longer-context model. For short commands like "rephrase this," "shorten this," "translate one line," the latency gap between glm-5.3-flash and, say, claude-opus-5-5 may be noticeable — check on your own commands; for an extension that gets opened for a second at a time, speed often matters more. It's worth leaving the heavier model as a separate dropdown option for when you actually need it — a question involving an image (Claude and GPT models read images; GLM has no vision), say, or working through a long code file inside the 1,000,000-token context (GLM's window is 128K).
Caching genuinely pays off with Raycast's usage pattern
Raycast extensions tend to get called with the same prompt over and over — "explain this regex," "shorten this title." The X-Zukko-Cache: true header in the example above turns on caching of identical requests for up to 7 days: a repeat call with an exactly matching request body (same text, same model, same parameters) comes back instantly and free, with X-Zukko-Cache: hit in the response. A difference of even one character is a cache miss; cache hits are not written to /v1/usage. For an extension built around fast UX, that's the difference between a one-second hang and an instant answer.
Handling common errors in the Raycast UI
Raycast doesn't forgive a bare exception well — better to translate error codes into a clear toast right away:
if (r.status === 401) {
toast.title = "Check the API key in the extension's preferences";
} else if (r.status === 402) {
toast.title = "Balance is empty — top up at zukko.pro/app";
} else if (r.status === 404) {
toast.title = "Model not found, check the name in preferences";
} else if (r.status === 503) {
toast.title = "Model is unavailable right now — pick another in preferences";
} else if (r.status === 429) {
toast.title = "Key's daily or monthly limit reached (or the provider is overloaded)";
}The gateway has no rate limit of its own: a 429 from Zukko means key_daily_limit_reached (resets at 00:00 UTC) or key_monthly_limit_reached; any other 429 comes from the provider. A disabled model answers 503 model_disabled.
Keys can be issued or rotated in the dashboard at https://zukko.pro/app/api-keys — the same page for a given key also lets you set its daily/monthly limit and allowed-model list, useful if the extension isn't just for one person but shared across a team through a single, scoped-down key.
Adding image generation straight from Raycast
Image generation through Zukko is temporarily unavailable right now (the current model list is at GET https://api.zukko.pro/v1/models). When it returns, it's worth keeping it as its own command with a clear price warning: image models are billed per image rather than per token, so they shouldn't be folded into the text ask command.
Checking spend right from the extension
It's worth adding a second command that simply hits /v1/balance and shows what's left in a Raycast HUD — no need to switch to a browser to see whether it's time to top up:
const r = await fetch("https://api.zukko.pro/v1/balance", {
headers: { "Authorization": `Bearer ${apiKey}` }
});
const { balance_cents } = await r.json();
await showHUD(`Balance: $${(balance_cents / 100).toFixed(2)}`);The X-Fetch-Balance-Cents header that comes back with every regular (non-streaming) ask response can also be written to the extension's local cache and shown next to the answer — that way the remaining balance is visible right after each model call, with no separate request. Note that, despite the name, its value is in micro-dollars (1,000,000 = $1), so divide by 1_000_000, not 100. The exact balance in cents is the balance_cents field from /v1/balance.
Capping spend on a shared team key
If the key in Preferences is shared across a team, set a daily dollar spend limit on the key's dashboard page (https://zukko.pro/app/api-keys) — that protects the budget from one person accidentally looping calls through a fast model. Keep the allowed-model list on that same page in sync with the dropdown in package.json, so the extension never offers a model the key's policy forbids — otherwise the user gets a 403 model_not_allowed_for_key instead of an answer.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- JetBrains AI Assistant not working: causes and workaroundsAI Assistant will not activate, stays silent or is off for some files. We go through the causes in the JetBrains docs and connect your own models via a gateway.
- DeepSeek in VS Code: connecting through Cline, Kilo Code and ContinueWe connect DeepSeek to Cline, Kilo Code and Continue with a single key, choose between Flash and Pro, and estimate the cost of a working day with an agent in the editor.
- GitHub Copilot alternatives with your own API key: what to chooseCline, Kilo Code, Continue and Cursor work with your own key. We compare where they live, how they connect to a gateway and how to estimate spend.
- Invalid API key in opencode: why the key is rejected and how to checkThe Invalid API key error in opencode almost always comes down to four causes. We test the key outside opencode and find where the chain breaks.