Skip to content
Star on GitHub
Agent statusRun cost
Style
Accent
Sound

Built by DesignMe, free to use under the MIT licence.

Expert design and dev support for startups and enterprises.

Book a call

Run cost

Live credit and token spend, with a cap that pauses the run. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.

$npx designme add run-cost
Credits usedIdle
0 / 2,000
Input tokens0
Output tokens0
Tool calls0
01

Install

Add it to your project with the CLI.

HTML, CSS and JavaScript

$npx designme add run-cost

React

$npx designme add run-cost --react

The CLI copies the source into your project, so you own it and can change anything. The HTML version lands in ./designme with the shared core and a demo.html. The React version lands in src/components/designme and needs React 19; it works in Next.js, Vite and Astro.

02

About

What it is for and what it does.

Run cost shows how many credits an agent run has used against its budget, as a big number and a bar with a marker where the agent should pause. Under the bar it breaks the spend down, for example into input tokens, output tokens and tool calls, and a switch turns the pause on or off.

  • When agent runs cost money or credits and people want to watch the spend while the run is going.
  • When a long run should stop at a set share of the budget and wait for someone to approve more.
  • When people ask what a run's cost is made of and you want to show the token and tool numbers behind it.
03

Usage

Import it and drive it from your own state.

Import

import { RunCost } from '@/components/designme/run-cost/RunCost';

Example

The demo above as a React component, 48 lines you can copy.

'use client';
// The animated preview from designme.dev, rebuilt from the React component: an example of driving it from your own state.
import { useEffect, useRef, useState } from 'react';
import { RunCost, type RunCostStatus } from './RunCost';
import { useTakeover } from '../lib/useScript';

/* ---------- Run cost: the run starts by itself and burns credits until the cap pauses it; raise the cap to finish ---------- */
const BUDGET = 2000;

export default function RunCostDemo() {
  const [status, setStatus] = useState<RunCostStatus>('idle');
  const [capOn, setCapOn] = useState(true);
  const [n, setN] = useState({ used: 0, input: 0, output: 0, tools: 0 });
  // the ticker reads these without restarting
  const live = useRef({ n, capOn });
  useEffect(() => { live.current = { n, capOn }; });

  useEffect(() => {
    if (status !== 'running') return;
    const id = setInterval(() => {
      const { n: p, capOn: cap } = live.current, limit = cap ? BUDGET * 0.9 : BUDGET;
      const next = { used: Math.min(BUDGET, p.used + 8 + Math.random() * 18), input: p.input + 120 + Math.random() * 220, output: p.output + 30 + Math.random() * 70, tools: p.tools + (Math.random() < 0.12 ? 1 : 0) };
      if (next.used >= limit) { next.used = limit; setStatus(limit < BUDGET ? 'paused' : 'spent'); }
      live.current.n = next; setN(next);
    }, 90);
    return () => clearInterval(id);
  }, [status]);

  // the run starts by itself once the card has settled, unless you take over first
  const ref = useRef<HTMLDivElement>(null), mine = useTakeover(ref);
  useEffect(() => { const t = setTimeout(() => { if (!mine()) setStatus(s => (s === 'idle' ? 'running' : s)); }, 1500); return () => clearTimeout(t); }, [mine]);

  const act = () => {
    if (status === 'running') return setStatus('stopped');
    // Raise cap turns the pause off and carries on to the full budget
    if (status === 'paused') { setCapOn(false); live.current.capOn = false; }
    if (n.used >= BUDGET) setN({ used: 0, input: 0, output: 0, tools: 0 });
    setStatus('running');
  };

  return (
    <div ref={ref} style={{ width: '100%' }}>
      <RunCost used={n.used} budget={BUDGET} capOn={capOn} onCapChange={setCapOn} status={status} onAction={act}
        rows={[{ label: 'Input tokens', value: n.input }, { label: 'Output tokens', value: n.output }, { label: 'Tool calls', value: n.tools }]} />
    </div>
  );
}
04

Props

Everything RunCost takes.

PropTypeWhat it does
usedrequirednumberCredits used so far
budgetrequirednumberThe whole budget, like 2000
capAtnumberWhere the agent pauses, as a percentage of the budget. Defaults to 90
capOnbooleanWhether the agent pauses at the cap. Leave it out to let the switch change it
defaultCapOnboolean
onCapChange(on: boolean) => void
statusRunCostStatusrunning is blue, paused (at the cap) amber, spent (the whole budget) red
statusLabelstringWords in the status tag. Defaults to Idle, Running, Stopped, "Paused at 90%" or "Budget used"
rowsRunCostRow[]The breakdown under the bar, like input tokens, output tokens and tool calls
actionLabelstringWords on the button. Defaults to Start run, Stop, Resume, Raise cap or Start over, by status
onAction() => voidCalled by the button. What it does is up to you: start, stop, resume, raise the cap
titlestring
barebooleanSits flat in a chat thread (the default, as on designme.dev). Set false to give it a card of its own
classNamestring
05

Code

The full source, as the CLI installs it.

RunCost.tsxReact · 79 lines
'use client';
import { cx } from '../lib/cx';
import { useControllable } from '../lib/hooks';
import { Swap, Tip } from '../lib/motion';
import '../designme.css';
import './run-cost.css';

export type RunCostStatus = 'idle' | 'running' | 'stopped' | 'paused' | 'spent';

export interface RunCostRow {
  /** Like "Input tokens" */
  label: string;
  value: number;
}

export interface RunCostProps {
  /** Credits used so far */
  used: number;
  /** The whole budget, like 2000 */
  budget: number;
  /** Where the agent pauses, as a percentage of the budget. Defaults to 90 */
  capAt?: number;
  /** Whether the agent pauses at the cap. Leave it out to let the switch change it */
  capOn?: boolean;
  defaultCapOn?: boolean;
  onCapChange?: (on: boolean) => void;
  /** running is blue, paused (at the cap) amber, spent (the whole budget) red */
  status?: RunCostStatus;
  /** Words in the status tag. Defaults to Idle, Running, Stopped, "Paused at 90%" or "Budget used" */
  statusLabel?: string;
  /** The breakdown under the bar, like input tokens, output tokens and tool calls */
  rows?: RunCostRow[];
  /** Words on the button. Defaults to Start run, Stop, Resume, Raise cap or Start over, by status */
  actionLabel?: string;
  /** Called by the button. What it does is up to you: start, stop, resume, raise the cap */
  onAction?: () => void;
  title?: string;
  /** Sits flat in a chat thread (the default, as on designme.dev). Set false to give it a card of its own */
  bare?: boolean;
  className?: string;
}

const fmt = (n: number) => Math.round(n).toLocaleString('en-US');
const TONE: Partial<Record<RunCostStatus, string>> = { running: 'dm-b', paused: 'dm-warn', spent: 'dm-bad' };
const ACTION: Record<RunCostStatus, string> = { idle: 'Start run', running: 'Stop', stopped: 'Resume', paused: 'Raise cap', spent: 'Start over' };

/** Credits a run has used against its budget, with a cap the agent pauses at and the tokens behind the number. */
export function RunCost({ used, budget, capAt = 90, capOn, defaultCapOn = true, onCapChange, status = 'idle', statusLabel, rows, actionLabel, onAction, title = 'Credits used', bare = true, className }: RunCostProps) {
  const [cap, setCap] = useControllable(capOn, defaultCapOn, onCapChange);
  const label = statusLabel ?? { idle: 'Idle', running: 'Running', stopped: 'Stopped', paused: `Paused at ${capAt}%`, spent: 'Budget used' }[status];
  return (
    <div data-dm="" className={cx('dm-rc', !bare && 'dm-panel', className)} style={bare ? { paddingInline: 0 } : undefined}>
      <div className="dm-rc-h">
        <span className="dm-muted">{title}</span>
        <span className={cx('dm-rc-st dm-tag', TONE[status])}><span className="dm-dot" /><Swap className="dm-rc-stt">{label}</Swap></span>
      </div>
      <div className="dm-rc-big"><span className="dm-rc-n dm-num">{fmt(used)}</span><span className="dm-muted"> / {fmt(budget)}</span></div>
      <div className="dm-rc-track" role="meter" aria-label={title} aria-valuemin={0} aria-valuemax={budget} aria-valuenow={Math.round(used)}>
        <div className="dm-rc-fill" style={{ width: Math.max(0, Math.min(100, used / budget * 100)) + '%', background: status === 'paused' ? 'var(--dm-warn)' : 'var(--dm-accent)' }} />
        <Tip label="The agent pauses here"><div className="dm-rc-cap" style={{ left: capAt + '%', opacity: cap ? 1 : 0.2 }} /></Tip>
      </div>
      {rows && (
        <div className="dm-rc-rows">
          {rows.map(r => <div key={r.label}><span>{r.label}</span><span className="dm-num">{fmt(r.value)}</span></div>)}
        </div>
      )}
      <div className="dm-rc-f">
        <label className="dm-rc-cap-t" onClick={e => { e.preventDefault(); setCap(!cap); }}>
          <button type="button" className="dm-toggle" role="switch" aria-checked={cap} aria-label={`Pause at ${capAt} percent`} />
          <span>Pause at {capAt}%</span>
        </label>
        <button type="button" className={cx('dm-pill dm-sm dm-rc-go', status === 'running' || status === 'stopped' ? 'dm-soft' : 'dm-primary')} onClick={onAction}>
          <Swap>{actionLabel ?? ACTION[status]}</Swap>
        </button>
      </div>
    </div>
  );
}
run-cost.cssReact · 17 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Run cost ---------- */
.dm-rc{padding:18px}
.dm-rc-h{display:flex;align-items:center;justify-content:space-between;font-size:12.5px}
.dm-rc-st{transition:background .3s,color .3s}
.dm-rc-big{margin-top:6px;font-size:13px}
.dm-rc-n{font-size:14px;font-weight:500;letter-spacing:-.8px}
.dm-rc-track{position:relative;height:6px;border-radius:var(--dm-r-xs);background:var(--dm-chip);margin:14px 0 var(--dm-s-4)}
.dm-rc-fill{height:100%;width:0;border-radius:var(--dm-r-xs);background:var(--dm-accent-mark,var(--dm-accent));transition:background .4s}
.dm-rc-cap{position:absolute;left:90%;top:-5px;bottom:-5px;width:2px;border-radius:1px;background:var(--dm-fg2);transition:opacity .3s}
.dm-rc-cap::before{content:"";position:absolute;inset:-6px -8px}
.dm-rc-rows{display:flex;flex-direction:column;gap:2px}
.dm-rc-rows div{display:flex;justify-content:space-between;padding:5px 0;font-size:13px;color:var(--dm-fg2)}
.dm-rc-rows .dm-num{color:var(--dm-fg)}
.dm-rc-f{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:var(--dm-s-3);padding-top:var(--dm-s-3);border-top:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}
.dm-rc-cap-t{display:flex;align-items:center;gap:9px;font-size:13px;color:var(--dm-fg2);cursor:pointer}
run-cost.htmlHTML · 19 lines
<div class="card bare" data-c="run-cost">
  <div class="stage-in">
    <div class="rc panel">
      <div class="rc-h"><span class="muted">Credits used</span><span class="rc-st tag"><span class="dot"></span><span class="rc-stt">Idle</span></span></div>
      <div class="rc-big"><span class="rc-n num">0</span><span class="muted"> / 2,000</span></div>
      <div class="rc-track"><div class="rc-fill"></div><div class="rc-cap" data-tip="The agent pauses here"></div></div>
      <div class="rc-rows">
        <div><span>Input tokens</span><span class="num rc-in">0</span></div>
        <div><span>Output tokens</span><span class="num rc-out">0</span></div>
        <div><span>Tool calls</span><span class="num rc-tools">0</span></div>
      </div>
      <div class="rc-f">
        <label class="rc-cap-t"><button class="toggle" role="switch" aria-checked="true" aria-label="Pause at 90 percent"></button><span>Pause at 90%</span></label>
        <button class="pill primary sm rc-go">Start run</button>
      </div>
    </div>
  </div>
</div>
run-cost.cssHTML · 16 lines
/* ---------- Run cost ---------- */
.rc{padding:18px}
.rc-h{display:flex;align-items:center;justify-content:space-between;font-size:12.5px}
.rc-st{transition:background .3s,color .3s}
.rc-big{margin-top:6px;font-size:13px}
.rc-n{font-size:14px;font-weight:500;letter-spacing:-.8px}
.rc-track{position:relative;height:6px;border-radius:var(--r-xs);background:var(--chip);margin:14px 0 var(--s-4)}
.rc-fill{height:100%;width:0;border-radius:var(--r-xs);background:var(--accent-mark,var(--accent));transition:background .4s}
.rc-cap{position:absolute;left:90%;top:-5px;bottom:-5px;width:2px;border-radius:1px;background:var(--fg2);transition:opacity .3s}
.rc-cap::before{content:"";position:absolute;inset:-6px -8px}
.rc-rows{display:flex;flex-direction:column;gap:2px}
.rc-rows div{display:flex;justify-content:space-between;padding:5px 0;font-size:13px;color:var(--fg2)}
.rc-rows .num{color:var(--fg)}
.rc-f{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:var(--s-3);padding-top:var(--s-3);border-top:1px solid color-mix(in srgb,var(--line) 50%,transparent)}
.rc-cap-t{display:flex;align-items:center;gap:9px;font-size:13px;color:var(--fg2);cursor:pointer}
run-cost.jsHTML · 32 lines
DM.def('run-cost', (r, c) => {
  // the label crossfades on its own span, so the button itself never blurs
  const go = $('.rc-go', r); go.innerHTML = '<span class="rc-gl">Start run</span>'; const gl = () => $('.rc-gl', go);
  const tg = $('.toggle', r), cap = $('.rc-cap', r), fill = $('.rc-fill', r), st = $('.rc-st', r), stt = $('.rc-stt', r);
  let used = 0, inT = 0, outT = 0, tools = 0, running = false, limit = 1800, loop = 0;
  const fmt = n => Math.round(n).toLocaleString('en-US');
  const paint = () => { $('.rc-n', r).textContent = fmt(used); fill.style.width = (used / 2000 * 100) + '%'; $('.rc-in', r).textContent = fmt(inT); $('.rc-out', r).textContent = fmt(outT); $('.rc-tools', r).textContent = tools; };
  const status = (t, cls) => { st.className = 'rc-st tag ' + (cls || ''); swap(stt, t); };
  const capOn = () => tg.getAttribute('aria-checked') === 'true';
  $('.rc-cap-t', r).onclick = e => { e.preventDefault(); tg.setAttribute('aria-checked', !capOn()); cap.style.opacity = capOn() ? 1 : .2; limit = capOn() ? 1800 : 2000; if (!running && used < limit && used > 0) go.disabled = false; };
  function stop(t, cls){ running = false; clearInterval(loop); status(t, cls); }
  go.onclick = () => {
    if (running){ stop('Stopped'); swap(gl(), 'Resume'); return; }
    if (used >= 2000){ used = inT = outT = tools = 0; paint(); }
    running = true; status('Running', 'b'); swap(gl(), 'Stop'); go.className = 'pill soft sm rc-go'; fill.style.background = 'var(--accent-mark,var(--accent))';
    loop = c.every(90, () => {
      used = Math.min(2000, used + 8 + Math.random() * 18); inT += 120 + Math.random() * 220; outT += 30 + Math.random() * 70; if (Math.random() < .12) tools++;
      paint();
      if (used >= limit){
        used = limit; paint();
        if (limit < 2000){ stop('Paused at 90%', 'warn'); fill.style.background = 'var(--warn)'; swap(gl(), 'Raise cap'); go.className = 'pill primary sm rc-go'; go.onclick2 = true; }
        else { stop('Budget used', 'bad'); swap(gl(), 'Start over'); go.className = 'pill primary sm rc-go'; }
      }
    });
  };
  const orig = go.onclick;
  go.addEventListener('click', e => { if (go.onclick2 && !running){ go.onclick2 = false; e.stopImmediatePropagation(); tg.setAttribute('aria-checked','false'); cap.style.opacity = .2; limit = 2000; orig(); } }, true);
  paint();
  // the run starts by itself once the card has settled, so the spend can be seen climbing to the cap
  c.auto(1500, () => { if (!running && !used) go.click(); });
});
06

Questions

What people ask about it.

Does the component pause the agent when it reaches the cap?
No. It shows the cap and whether it is on. Your code compares used with the cap, stops the agent and sets status to 'paused'.
What should the button do?
Whatever fits the status, through onAction. Its label suggests the usual move, for example Stop while running or Raise cap when paused. The demo turns the cap off and runs on to the full budget when Raise cap is pressed.
Can I show money instead of credits?
The numbers are shown as whole numbers with thousands separators and no currency sign. You can change title to name the unit, for example "Cents used".
07

More in Agent status

Show what the agent is doing, how far along it is, and what it is working with.

ThinkingThe live trace, written as the agent works, then folded away.
Reasoning trailThe agent's plan, notes, sources and edits, after it thinks.
Agent lanesSeveral agents working in parallel, each with its own lane and hand-offs.
Agent treeAn orchestrator and its sub-agents, with cost rolled up per branch.
Agent inboxEvery run that needs you, is running or is done, in one list.
Trace viewOne agent run as a waterfall of spans, with latency and tokens.
Browser sessionThe agent browsing for you, step by step, with Pause and Take over.
Run summaryWhat the agent did, what changed and what needs you.
All 18 in Agent status →