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:

bash
{
  "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

bash
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:

bash
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:

bash
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.

Get API keyDocumentation

api.zukko.pro · fetch_*

Raycast extension with your own Claude/GPT API key · Zukko