Sunday, October 25, 2026

Claude Code on the Subscriptions You Already Pay For

Claude Code on the Subscriptions You Already Pay For

Last time I showed a team of Claude Code sessions with a different vendor behind each seat, paid for with API keys. API keys get expensive. If you already have a subscriber account with a quota, you might as well use it. So I did, with Claude and with Kimi. Here are the steps.


Why

The diversity post ran every non-Anthropic seat on a per-token key: OpenRouter, Ollama Cloud, the vendors' own APIs. The bill for a day of demos was two dollars and change. A team that works all day, every day, does not stay at two dollars. And I was already paying flat rates for three coding subscriptions, Claude Max, Kimi Code and SuperGrok, with quota I was not using.

The result is a nine-seat team, four on Claude Max, five on the Kimi Code plan, nothing on a meter. Grok could not join, and step 1 says why.

What follows is the order I would do it in next time, not the order I stumbled through it.


Step 1: find out which plans will issue a key that bills the plan

This is the step I got wrong first, so it goes first.

Claude Code needs one of two things to talk to a provider: a login, or a key plus a base URL. Your Claude subscription is the login case and needs nothing. Every other vendor is the key case, and the question is not "does the vendor issue keys" but "what does the key bill against."

A key is a credential. It says who you are. The meter is what the vendor charges when that credential is used. Moonshot's developer platform issues keys that bill per token. The Kimi Code plan issues keys from its own console, and those bill against the plan's quota, the same five-hour rolling window the kimi command-line tool draws from. Same key format, different meter.

The console at kimi.com/code/console says so:

API Keys
We only show the key once—save it immediately. Don't share it or expose it in client-side code. If we detect a leak, we'll disable the key to protect your account. You can create up to 5 API Keys

API ID    Key    Name    Created    Last active    Status    Action
19b49397-...    sk-ki...qAUFm    ApocryiaAI_KIMI    2025-12-23 11:21:02    -    Enabled

I had created that key in December and never used it, because I assumed it was the metered kind. It is not. Five keys per plan, and the only thing they can be billed to is the plan.

The endpoint for plan keys is https://api.kimi.ai/coding/. It speaks the Anthropic Messages dialect, which is what Claude Code sends. GET /coding/v1/models with the plan key returns four ids, and they match the "Kimi Code" group in the kimi tool's picker:

Picker name Model id
K3 k3
K3-256k k3-256k
K2.8 Preview kimi-for-coding
K2.7 Code Highspeed kimi-for-coding-highspeed

Below those the picker lists kimi-k3:cloud and three others under "ollama." That is the metered Ollama Cloud shelf from the last post. The line in the picker is the line between the two meters.

Grok fails this step. SuperGrok sign-in is accepted by xAI's own Grok Build tool and by Warp, and nothing else. The xAI API is per-token. There is a community proxy that fronts the subscription, and it is unofficial, so it is not going behind a seat on my machine. If xAI ever issues plan keys the way Moonshot does, the roster grows by one line.

One thing I did not do and will not: lift the OAuth tokens the kimi tool stores after sign-in and feed them to Claude Code. Anthropic published a policy in February that subscription OAuth is for Claude Code and claude.ai only, and I treat Moonshot's sign-in the same way. The console key exists so you do not have to.


Step 2: write the roster

One file, one seat per line, the model written the way the vendor's picker shows it:

DEVELOPER | K3-256k
DEVELOPER_2 | K2.7 Code Highspeed
DEVELOPER_3 | Claude Sonnet
DEVELOPER_4 | K2.7 Code Highspeed
DEVELOPER_5 | K2.7 Code Highspeed
DBA | K2.8 Preview
REVIEWER | Claude Opus
CRITIC   | Claude Fable
PMO  | Claude Sonnet <-- this will be my base

Each seat gets a folder of the same name with an AGENTS.md that says what the role is and how to sign messages. The PMO line is me, the interactive terminal I sit in. It shares the roster with the eight background seats because they all run under the same config directory, and the launcher skips it.

Five developers on Kimi and the judges on Claude is not an accident. The developers do the volume, and the Kimi plan had the headroom. The Reviewer and the Critic have to disagree with the developers, so they run on a different lab's weights. That rule has not changed since the first team I wrote about.


Step 3: one seat file per provider

A Claude seat needs no file. It is logged in through the subscription like any session.

A Kimi seat needs this, mode 0600, and the key lives here and nowhere else:

{
  "env": {
    "ANTHROPIC_BASE_URL": "https://api.kimi.ai/coding/",
    "ANTHROPIC_API_KEY": "",
    "ANTHROPIC_AUTH_TOKEN": "<Kimi Code console key>",
    "ANTHROPIC_DEFAULT_HAIKU_MODEL": "kimi-for-coding-highspeed",
    "ANTHROPIC_SMALL_FAST_MODEL": "kimi-for-coding-highspeed",
    "CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT": "1"
  },
  "crossSessionInbound": "accept",
  "dialogExpiry": "never"
}
mkdir -p ~/.claude-seats && chmod 700 ~/.claude-seats
chmod 600 ~/.claude-seats/kimi-code.json

Three choices in that file cost me time.

The key goes in ANTHROPIC_AUTH_TOKEN, not ANTHROPIC_API_KEY. Both work against the endpoint. The difference is what Claude Code does on startup. A value in ANTHROPIC_API_KEY triggers an approval dialog the first time, and the dialog masks every key as if Anthropic issued it:

  Detected a custom API key in your environment

  ANTHROPIC_API_KEY: sk-ant-...[last 20 characters of the Kimi key]

  Do you want to use this API key?

  ❯ Yes
    No (recommended)

That sk-ant- prefix is cosmetic. The Kimi key starts sk-kimi-, and I spent ten minutes convinced a dead Anthropic key had leaked in from my shell before I matched the suffix. A background seat has no terminal to answer that dialog in, so it would sit there forever. Bearer token: no dialog, no stall.

The small models point at the highspeed id. Claude Code uses a fast model for background work like summarising and title generation. Left at the default it asks Kimi's endpoint for a Haiku it does not have.

The window enforcement is off because Claude Code does not know the context size of k3-256k and would otherwise clamp it.

The two keys outside env are the same two the last post ended on: accept messages from other sessions without asking, and never expire that answer.


Step 4: the launcher

team_up.sh reads the roster, maps each picker name to a model id and a seat file, and launches a background session from inside the seat's folder.

#!/usr/bin/env bash
# team_up.sh — launch the seats listed in ./agents as background Claude Code sessions.
#
#   ./team_up.sh            launch every seat in ./agents (PMO is skipped: that is the human's terminal)
#   ./team_up.sh DBA CRITIC launch only these seats
#   ./team_down.sh          stop them all (conversations kept; `claude attach NAME` resumes)
#
# One config dir for the whole team (the default ~/.claude, so your PMO terminal shares the roster).
# Provider is set per seat:
#   Claude models  -> subscription login, no settings file
#   Kimi Code      -> ~/.claude-seats/kimi-code.json (0600, plan key inside; billed to the Kimi Code plan)
#   Ollama cloud   -> ~/.claude-seats/ollama.json
#
# ./agents format, one per line:   SEAT | Display model name
# Display names are mapped below. Add a row there when you add a model to the picker.

set -u
cd "$(dirname "$0")"
SEATS=${HOME}/.claude-seats

model_for() {            # display name -> "model-id seatfile-or-empty"
  case "$(echo "$1" | tr '[:upper:]' '[:lower:]' | xargs)" in
    k3-256k)                       echo "k3-256k kimi-code" ;;
    k3)                            echo "k3 kimi-code" ;;
    "k2.8 preview")                echo "kimi-for-coding kimi-code" ;;
    "k2.7 code highspeed")         echo "kimi-for-coding-highspeed kimi-code" ;;
    "claude sonnet")               echo "sonnet" ;;
    "claude opus")                 echo "opus" ;;
    "claude fable")                echo "fable" ;;
    kimi-k3:cloud|kimi-k2.7-code:cloud|deepseek-v4-pro:0813-cloud|deepseek-v4.1-flash:cloud)
                                   echo "$1 ollama" ;;
    *) return 1 ;;
  esac
}

want=("$@")
while IFS='|' read -r seat display; do
  seat=$(echo "$seat" | xargs); display=$(echo "${display%%<*}" | xargs)   # drop "<-- note" tails
  [ -z "$seat" ] && continue
  [ "$seat" = "PMO" ] && continue
  if [ ${#want[@]} -gt 0 ]; then printf '%s\n' "${want[@]}" | grep -qx "$seat" || continue; fi
  if ! spec=$(model_for "$display"); then echo "SKIP $seat: unknown model '$display'"; continue; fi
  set -- $spec; model=$1; provider=${2:-}
  sf=""; [ -n "$provider" ] && sf="$SEATS/$provider.json"
  if [ -n "$sf" ] && [ ! -r "$sf" ]; then echo "SKIP $seat: missing $sf"; continue; fi
  [ -d "$seat" ] || { echo "SKIP $seat: no folder"; continue; }
  printf '%-12s %-28s %s\n' "$seat" "$model" "${provider:-subscription}"
  ( cd "$seat" && env -u ANTHROPIC_API_KEY \
      claude --bg --name "$seat" --model "$model" --permission-mode auto \
      ${sf:+--settings "$sf"} \
      "Read AGENTS.md. Reply READY $seat and name the model you are running on. Then wait for messages." \
      2>&1 | grep -E 'logs|rror' | head -1 | sed 's/^/    /' )
done < agents

echo; echo "roster:"; claude agents 2>/dev/null || claude agents --json | python3 -c '
import json,sys
for a in json.load(sys.stdin): print(" ", a.get("name"), a.get("id"), a.get("state"))'

Four things in that launch line matter. The rest is plumbing.

  • env -u ANTHROPIC_API_KEY strips the stale key from my shell before Claude Code sees it. The Kimi seats would take it over the seat file, and the Claude seats would try to bill it instead of using the login.
  • --bg puts the session in the background under a supervisor that owns the config directory. The last post covered what that does to your environment. It is why the seat file carries the environment rather than the shell.
  • --name "$seat" is the name the other sessions message. --model is the id from the mapping. For the Claude seats, sonnet, opus and fable resolve to the current model of that tier.
  • --settings "$sf" is the whole provider switch. Leave it off and the session is an ordinary subscription session.

team_down.sh is twelve lines that stop the same seats by id with their conversations kept, so claude attach DBA later picks up where it left off. The unknown-name branch in the mapping is deliberate. When the picker grows a model I want a SKIP line, not a guess.


Step 5: launch, then check the transcript, not the model

I launched DBA and REVIEWER first as a pilot, one from each plan, then the other six:

DEVELOPER    k3-256k                      kimi-code
      claude logs 2fe4891a      show recent output
DEVELOPER_2  kimi-for-coding-highspeed    kimi-code
      claude logs 6553636b      show recent output
DEVELOPER_3  sonnet                       subscription
      claude logs d44d55f3      show recent output
DEVELOPER_4  kimi-for-coding-highspeed    kimi-code
      claude logs dfd7cd13      show recent output
DEVELOPER_5  kimi-for-coding-highspeed    kimi-code
      claude logs a12fd128      show recent output
CRITIC       fable                        subscription
      claude logs c4913ca2      show recent output

Each seat's first job was to read its role file and say what it was running on. The replies, trimmed, from each seat's own transcript:

DEVELOPER    **READY DEVELOPER** — running on **Kimi K3-256k** (`k3-256k`). AGENTS.md read ...
DEVELOPER_2  [Kimi] READY DEVELOPER_2 — running **K2.7 Code Highspeed** (`kimi-for-coding-highspeed`). Waiting for messages.
DEVELOPER_3  READY DEVELOPER_3. I'm running as Claude Sonnet 5.5 (`claude-sonnet-5-5`), ROLE_CLIENT `DEVELOPER_3_CLAUDE`. ...
DEVELOPER_4  READY DEVELOPER_4  Model: **Kimi K2.7 Code Highspeed** (`kimi-for-coding-highspeed`)
DEVELOPER_5  READY DEVELOPER_5. Running on **Kimi K2.7 Code Highspeed** (`kimi-for-coding-highspeed`). Waiting for messages.
DBA          READY DBA — running on **Kimi** (model: `kimi-for-coding`, seat role_client `DBA_KIMI`). ...
REVIEWER     READY REVIEWER. I'm running on Claude Opus 5.5 (`claude-opus-5-5`), which matches the "REVIEWER | Claude Opus" line in your roster. I'll sign as `[Claude / Reviewer]` ...
CRITIC       **READY CRITIC** — running on **Claude Fable 5.1** (`claude-fable-5-1`). Seat confirmed by `pwd` ...

A model telling you what it is running on is not proof. The transcript is. Every session writes a JSONL file under the config directory's projects folder, and the first assistant turn records the model id the server answered with:

rg -m1 '"model"' ~/.claude/projects/-<seat-folder-path-with-slashes-as-dashes>/*.jsonl

One tag per seat:

Seat model in its transcript Plan
DEVELOPER k3-256k Kimi Code
DEVELOPER_2 kimi-for-coding-highspeed Kimi Code
DEVELOPER_3 claude-sonnet-5-5 Claude Max
DEVELOPER_4 kimi-for-coding-highspeed Kimi Code
DEVELOPER_5 kimi-for-coding-highspeed Kimi Code
DBA kimi-for-coding Kimi Code
REVIEWER claude-opus-5-5 Claude Max
CRITIC claude-fable-5-1 Claude Max

Nine sessions on one machine, two plans, zero per-token meters, and /list-agents from my terminal shows all eight of the others.

One small thing in the READY lines that is not small. DEVELOPER_2 signed as [Kimi]. REVIEWER said it would sign as [Claude / Reviewer]. DEVELOPER_4 signed nothing. All eight read the same signing rule in the same shared file, and three models read it three ways. If you write role files for a mixed-vendor team, write the signature rule like a regex, not like a suggestion.


What it cost, and what will bite

Nothing on a meter. The Kimi console's usage page is the only place to see what the five seats drew, because the endpoint sends no rate-limit headers in its responses. I checked.

What will bite first is the window. Five Kimi seats and my own kimi terminal share one plan's five-hour quota. During the launch, with every seat running its full start-up sweep at once, one seat hit this:

✻ 429 The engine is currently overloaded, please try again later · Retrying in 0s · attempt 3/10
If it persists, check your inference gateway (api.kimi.ai).

It cleared on retry, and five probes against the endpoint in the same minute all returned 200, so I read it as load on their side under a burst from mine, not the quota. But a team that works all day on one plan will meet the quota, and when it does the Kimi seats stall together while the Claude seats keep going. Sizing the roster is sizing the window.


Related reading


Keith is a database consultant and infrastructure engineer with 25+ years of open-source experience. He writes about MySQL, Proxmox, AI memory, and building technology you can actually inspect.


No comments:

Post a Comment