SLOPSHOPPER

Paid API meter

Counts the session's calls to paid generation APIs (image, video, voice, LLM) and adds up reported costs

newbandguard
★ 1v0.1.0MITupdated 2026-10-21benjaminb10/encore-mods/plugins/paid-api-meter
A shopper browsing a rack in a slop shop
README

Paid API meter

A Claude Code mod from Encore Mods. Know when a session is spending money outside your Claude plan.

  • Counts calls to paid generation APIs made through MCP tools or curl: fal, ElevenLabs, HeyGen, Replicate, OpenAI, Runway, Veo, Suno, Stability, Luma, Kling and more.
  • Shows $ 3 paid API calls · ~$0.42 above the prompt. The amount is added up only when a response reports its cost.
  • Nothing shows until the first paid call.

Install

/plugin marketplace add benjaminb10/encore-mods
/plugin install paid-api-meter@encore-mods

Then /reload-plugins or restart Claude Code. Needs Claude Code 2.1.287 or later.

Options

extra_services (words in MCP tool names) and extra_hosts (API hosts for curl), both comma-separated. Set it in /plugin, or with /plugin configure paid-api-meter@encore-mods.

Try it without installing

git clone https://github.com/benjaminb10/encore-mods
claude --plugin-dir encore-mods/plugins/paid-api-meter
Source 1 files
hooks/register.js 75 lines
1// paid-api-meter: counts the calls this session makes to paid generation APIs (image, video, voice,
2// LLM providers), through MCP tools or curl, and shows the total above the prompt. When a response
3// reports what the call cost, the amount is added up too.
4
5const DEFAULT_SERVICES = [
6  'fal', 'elevenlabs', 'heygen', 'replicate', 'openai', 'runway', 'veo', 'suno', 'treg', 'stability',
7  'midjourney', 'luma', 'kling', 'ideogram', 'deepgram', 'assemblyai',
8]
9const DEFAULT_HOSTS = [
10  'api.elevenlabs.io', 'fal.run', 'queue.fal.run', 'api.replicate.com', 'api.heygen.com', 'api.openai.com',
11  'generativelanguage.googleapis.com', 'api.runwayml.com', 'api.stability.ai', 'api.lumalabs.ai',
12  'api.deepgram.com', 'api.assemblyai.com',
13]
14const PRICE = /"?(?:cost|price|charged|spent|amount_usd|cost_usd|credits_used)[a-z_]*"?\s*[:=]\s*"?\$?\s*(\d+(?:\.\d+)?)/i
15
16let toolRe = null
17let hostRe = null
18let calls = 0
19let cost = 0
20
21const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
22
23export function build(services, hosts) {
24  return {
25    tool: new RegExp('^mcp__.*(' + services.map(escape).join('|') + ')', 'i'),
26    host: new RegExp('(' + hosts.map(escape).join('|') + ')', 'i'),
27  }
28}
29
30export function isPaid(e, re) {
31  const tool = String(e.tool || '')
32  if (tool === 'Bash') return /\b(curl|wget|http|xh)\b/.test(String(e.command || '')) && re.host.test(String(e.command || ''))
33  return re.tool.test(tool)
34}
35
36export function isFree(text) {
37  return /"cost(_usd)?"\s*:\s*0(\.0+)?[,}\s]/.test(String(text))
38}
39
40export function priceOf(text) {
41  const m = String(text).match(PRICE)
42  return m ? Number(m[1]) || 0 : 0
43}
44
45const list = (s) => String(s || '').split(',').map((x) => x.trim()).filter(Boolean)
46
47export function register(on, options = {}) {
48  const re = build([...DEFAULT_SERVICES, ...list(options.extra_services)], [...DEFAULT_HOSTS, ...list(options.extra_hosts)])
49  toolRe = re.tool
50  hostRe = re.host
51
52  on('tool.call', async ($, e, next) => {
53    const paid = isPaid(e, { tool: toolRe, host: hostRe })
54    const result = await next(e)
55    if (paid && !result.deny && !result.isError) {
56      let text = ''
57      try { text = JSON.stringify(result).slice(0, 200000) } catch {}
58      // A call whose response says it cost nothing (a free endpoint, a status poll) is not a paid call
59      if (isFree(text)) return result
60      calls += 1
61      cost += priceOf(text)
62      $.ui.invalidate('ui.render')
63    }
64    return result
65  }).catch(() => undefined)
66
67  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
68    if (e.props.isWorking || !calls) return next(e)
69    const { Box, Text } = $.ui.resolve(e)
70    const label = '$ ' + calls + ' paid API call' + (calls === 1 ? '' : 's') + (cost ? ' · ~$' + cost.toFixed(2) : '')
71    const theirs = await next(e)
72    return Box({ flexDirection: 'column', children: [Text({ dimColor: true, children: [label] }), theirs] })
73  })
74}
75