SLOPSHOPPER

rtk-quota

Band showing rtk gain: tokens saved and quota preserved

newspinnerprocess
★ 1v0.3.5no licenseupdated 2026-10-03Lunik/gmz-claude-marketplace/rtk-quota
A shopper browsing a rack in a slop shop
README

rtk-quota

Shows what rtk saved you, from rtk gain --quota.

rtk ┃ Tokens saved 566.8K ┃ Quota preserved 9.4%

Cells

CellMeaning
Tokens savedLifetime tokens saved by rtk.
Quota preservedShare of your subscription quota those savings represent.

Options

OptionDefaultMeaning
tier20xSubscription tier passed to rtk gain --tier: pro, 5x or 20x.

pro is detected automatically from claude auth status. A Max plan cannot be told apart between 5x and 20x, so the option decides.

Behavior

  • Row in the bottom-right grid. Refreshed at session start and after every turn, and once on the first render so it also fills after a plugin reload.
  • Shows - until rtk gain has answered.

Requirements

rtk and claude on PATH.

Source 2 files
hooks/register.tsx 89 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Gain } from '../types'
5
6const gain = atom({ plugin: 'rtk-quota', key: 'gain' } as const, null as Gain | null)
7
8export const parse = (out: string): Gain | null => {
9  const saved = /Tokens saved[^:]*:\s*(\S+)/.exec(out)?.[1]
10  const preserved = /Quota preserved:\s*(\S+)/.exec(out)?.[1]
11  return saved && preserved ? { saved, preserved } : null
12}
13
14// `claude auth status` subscriptionType -> rtk tier; max (5x or 20x) is not told apart, so the option decides
15export const tierOf = (stdout: string, fallback: string) => {
16  try {
17    return JSON.parse(stdout).subscriptionType === 'pro' ? 'pro' : fallback
18  } catch {
19    return fallback
20  }
21}
22
23// ponytail: module var resets on hot reload; costs one extra `claude auth status`
24let authOut = ''
25// ponytail: after a hot reload no session.start/turn.complete fires; one lazy fetch on first render fills the row
26let primed = false
27
28async function refresh($: EngineInterface, fallback: string) {
29  if (!authOut) {
30    const auth = await $.process.run(['claude', 'auth', 'status'], { timeoutMs: 15_000 }).catch(() => null)
31    authOut = auth?.stdout ?? ''
32  }
33  const tier = tierOf(authOut, fallback)
34  const { stdout } = await $.process.run(['rtk', 'gain', '--quota', '--tier', tier])
35  const g = parse(stdout)
36  if (g) {
37    await update($, gain, () => g)
38    $.ui.invalidate('ui.render')
39  }
40}
41
42export const register: Register = (on, options) => {
43  on('session.start', async ($, e, next) => {
44    await refresh($, String(options.tier ?? '20x')).catch(() => {})
45    return next(e)
46  })
47
48  on('turn.complete', async ($, e, next) => {
49    await refresh($, String(options.tier ?? '20x')).catch(() => {})
50    return next(e)
51  })
52
53  // stacked above the usage bar (SessionMode, bottom right)
54  on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
55    const g = await read($, gain)
56    if (!g && !primed) {
57      primed = true
58      void refresh($, String(options.tier ?? '20x')).catch(() => {})
59    }
60    const other = await next(e)
61    const { Box, Text } = $.ui.resolve(e)
62    const line = (
63      <Box>
64        <Box width={11}><Text bold color="#7dcfff">rtk</Text></Box>
65        <Box>
66          <Text color="#565f89"> ┃ </Text>
67          <Box width={24}>
68            <Text dimColor>Tokens saved </Text>
69            <Text bold color={g ? '#c0caf5' : '#565f89'}>{g?.saved ?? '-'}</Text>
70          </Box>
71        </Box>
72        <Box>
73          <Text color="#565f89"> ┃ </Text>
74          <Box>
75            <Text dimColor>Quota preserved </Text>
76            <Text bold color={g ? '#c0caf5' : '#565f89'}>{g?.preserved ?? '-'}</Text>
77          </Box>
78        </Box>
79      </Box>
80    )
81    return other.type === 'engine' ? line : (
82      <Box flexDirection="column" alignItems="flex-start">
83        {other}
84        {line}
85      </Box>
86    )
87  })
88}
89
types/index.d.ts 8 lines
1export type Gain = { saved: string; preserved: string }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'rtk-quota': { gain: Gain | null }
6  }
7}
8