Usage metering & budgets
Per-run model-call usage as a first-class streamed signal, hard spend budgets, and the metering gateway at the inference boundary.
Every run durably records not just what it produced, but what it consumed. Each model call a run's child agents make is captured as a usage record, streamed as a run event while the run executes, aggregated onto the run, and included in terminal payloads — so dashboards, delegated callers, and budget enforcement all work off real numbers, never log scraping.
Usage records and the run aggregate
A usage record is one observed model call:
{
"ts": "2026-07-08T12:00:00.000Z",
"provider": "anthropic",
"model": "claude-opus-4-7",
"promptTokens": 6,
"completionTokens": 1744,
"totalTokens": 1750,
"costMicros": 130890,
"nodeId": "factory",
"source": "runner"
}Records fold into the run's persisted aggregate the moment they arrive:
"usage": {
"totalTokens": 1750,
"promptTokens": 6,
"completionTokens": 1744,
"costMicros": 130890,
"calls": 1,
"byModel": { "claude-opus-4-7": { "totalTokens": 1750, "costMicros": 130890, "calls": 1 } },
"byProvider": { "anthropic": { "totalTokens": 1750, "costMicros": 130890, "calls": 1 } }
}costMicros is micro-USD (1 000 000 = $1). When the provider reports the call's cost it is used verbatim (costSource: "provider"); otherwise the Hub estimates from a list-price table for well-known models (costSource: "price-table") and records no cost at all for unknown models — totals are a floor, never a guess.
Where usage appears:
GET /api/runsandGET /api/runs/{id}— theusageaggregate (andbudget+ computedbudgetStatus) on every run object.GET /api/runs/{id}/usage— the aggregate, the budget, per-call records,budgetStatus, andbudgetStop.GET /api/usage/summary— the windowed fleet rollup (below).run.usageevents on the run's event stream and timeline, emitted in the same write that updates the aggregate.- Terminal response-endpoint payloads —
usage,budget, andbudgetStopfields, so delegated callers can bill or cap against real consumption. - MCP:
get_run_usageandget_usage_summary, plusget_run_status/list_runsresponses. - CLI:
runyard usage(fleet rollup) andrunyard usage <runId>(one run's detail). - The web app: a usage chip on run cards, spent-vs-limit budget pairing on run detail, and fleet-wide totals in
GET /api/dashboard(stats.usage).
The fleet rollup: GET /api/usage/summary
One call answers "what is this deployment spending, and on what?" for a time window (?days=, default 30, max 365):
{
"window": { "days": 30, "since": "2026-06-14T00:00:00.000Z" },
"totals": { "totalTokens": 1234000, "costMicros": 8120000, "calls": 310, "meteredRuns": 42 },
"byWorkflow": [
{ "workflow": "research", "name": "Research", "totalTokens": 800000, "costMicros": 5000000, "calls": 190, "meteredRuns": 20, "lastRunAt": "2026-07-13T09:00:00.000Z" }
],
"budgetStopped": 2
}byWorkflow is sorted by spend (highest first); budgetStopped counts runs that hit their budget inside the window. MCP: get_usage_summary; CLI: runyard usage --days 7. Totals follow the same floor semantics as everywhere else — unknown-model calls contribute tokens but no cost.
Budgets
Run creation accepts an optional hard spend ceiling — as a top-level budget field or input.budget, on direct runs, negotiated runs, drafts, schedules, endpoint submissions, and reruns (reruns inherit the previous run's budget unless overridden):
{ "input": { "title": "Audit checkout flow" }, "budget": { "maxTokens": 200000, "maxCostMicros": 2000000 } }Budgets are hard stops, not advisories:
- Before each gateway-metered call, the Hub refuses to forward (
402) if the aggregate has already reached the ceiling. - After every accepted usage record, the Hub re-evaluates; a breach emits
run.budget.exceededand terminates the run with the distinct terminal statusbudget_exceeded— never a genericfailed— with the stop reason onrun.errorandbudgetStop. - The runner observes the terminal status on its next poll and cancels the detached engine run, and the gateway refuses further calls from that run's token (
403).
An invalid budget is rejected with 400 budget_invalid at creation (and surfaces as a preflight blocker in negotiation) — a requested cap is never silently dropped.
The CLI takes budgets as flags: runyard run research --max-tokens 200000 --max-cost 2 (--max-cost is US dollars; the same flags work on runyard preflight).
Every budgeted run also carries a computed budgetStatus — spent vs limit, per dimension — so no client re-derives the arithmetic:
"budgetStatus": {
"maxTokens": 200000, "tokensUsed": 164000, "tokensRemaining": 36000, "tokensPercentUsed": 82,
"percentUsed": 82, "nearLimit": true
}percentUsed is the worst dimension (the one that will stop the run first) and nearLimit trips at 80%. The web run list shows a "Budget 82% used" chip on near-limit runs and "Stopped at budget" after a stop; the run detail page pairs spent / limit with the percentage. Runs that stopped on their budget appear in the needs-attention queue for 7 days so the recovery path (raise the budget, re-run) is visible, not archaeological.
Capture paths: what is metered today
Usage is captured at the inference boundary, through two paths:
| Path | Source label | Coverage | Key custody |
|---|---|---|---|
| Metering gateway | gateway | Runs that select metering: "gateway" with the pi harness (piModel, piBaseUrl, piApiKeyEnv) | Provider key stays on the Hub; the child only holds a run-scoped gateway token |
| Runner-observed engine telemetry | runner | Every claude / codex / pi CLI call the Smithers engine reports via its structured TokenUsageReported events | Keys delivered per-run via the encrypted secretEnv channel (as before) |
Gateway-metered runs are pinned at claim time: the Hub mints a stateless per-run token, withholds the named provider key from the child environment entirely, and the runner materializes a per-run pi configuration whose only provider is the Hub's OpenAI-compatible gateway (POST /api/gateway/openai/v1/chat/completions; an Anthropic-shaped /api/gateway/anthropic/v1/messages is also served). The gateway authenticates the token, enforces the budget before forwarding, calls the run's configured upstream with the Hub-held key, streams the response back, and records usage from the provider's own response metadata (including streamed SSE usage frames).
Runner-observed runs (the default, metering: "observed") report the engine's per-call usage telemetry to POST /api/runs/{id}/usage with a stable per-call requestId, so replays after a runner restart never double-count. This is real, agent-reported usage from the engine's event stream — but the child still holds its own credentials (e.g. a Claude subscription token), so it is metered without being egress-locked.
Not yet metered: model calls that bypass both paths — e.g. a child agent calling a provider directly with an ad-hoc key from a tool script, or CLI paths the engine does not report usage for. Full egress locking (denying children any network path except the gateway) is a follow-up; the current sandbox shares the host network so the gateway pin is config-level, not network-level.
Selecting gateway metering
Gateway metering rides the same per-run harness selection surface as the pi custom-endpoint fields:
{
"input": {
"agentHarness": "pi",
"piProvider": "venice",
"piModel": "llama-3.3-70b",
"piBaseUrl": "https://api.venice.ai/api/v1",
"piApiKeyEnv": "VENICE_API_KEY",
"metering": "gateway"
}
}Preflight blocks the run unless the selection is complete (pi harness, model, endpoint URL, and the Hub-secret name for the key) — a run never launches half-pinned. The endpoint key must be stored as a Hub secret; with gateway metering it is decrypted only inside the Hub per call and never enters the child environment.
Tokens & Scopes
Bearer-token authentication, the scope vocabulary and presets (including read-only), the bootstrap token, and how browser and Telegram sessions work.
Secrets
The encrypted secret store - admin-managed, listable by name only, delivered to runs at claim time and never returned by the API.