Get release news

Spend caps and metering

max_per_run_usd and max_per_day_usd caps on priced tool calls that pass through the gate, enforced by the runtime against cost metered at the MCP gate. Read the scope before relying on a cap.

On this page

Tip

Step by step

Spend caps for priced tools prices an MCP server, sets both caps and shows what they do not cover.

Warning

What is metered, and what is not

Caps count priced tool calls that pass through the gate: limits are enforced against cost metered at the MCP gate proxy, for servers that declare a pricing block. Nothing else is metered. On a priced server, only tools/call is metered: methods listed under methods (resources/read, prompts/get, …) are forwarded unmetered. Traffic through sandbox.network.allowed_hosts, including every direct call to a model API, is allowlisted but not counted: a cap declared without a priced MCP server measures nothing at all. Traffic to hosts you allow isn't gated. If an allowed host shares a server with an MCP server, a request can reach that MCP server past the gate. See limitations 2 and 3.

How metering worksLink to this section

The caps count calls that pass through the gate to servers you price. Spending is a meter, not a bill. Constle moves no money: it multiplies the usage each priced MCP server reports in its own reply by the price you declare in the Agentfile, adds it up, and stops the run when the total passes a limit you set or a priced call cannot be counted. A server that under-reports its usage lowers the figure. The figure is an estimate, and the provider's bill is the source of truth.

Constle never passes your MCP servers' real addresses into the sandbox. The agent calls them through the gate. When a server declares pricing, the gate meters the reply to each of its tool calls that pass through the gate and charges the cost against the caps in spending. JSON-RPC error replies and HTTP 4xx replies are not charged, and the run goes on. A call answered with a 3xx or 5xx status, a call that gets no reply after it was sent, and a call whose agent disconnects before the server begins its reply, if the server has not begun it 30 seconds later, are not charged either. Such a call is recorded as a metering failure, and the run stops.

agent.yamlyaml
mcp:
  servers:
    - id: web-search
      url: "https://mcp-search.example.com/mcp"
      tools: ["search"]
      pricing:
        meters:
          - usage_path: "result.usage.input_tokens"
            usd_per_unit: "0.00000300"
          - usage_path: "result.usage.output_tokens"
            usd_per_unit: "0.00001500"

spending:
  max_per_run_usd: "0.50"
  max_per_day_usd: "5.00"   # requires identity.did
  • usage_path is a dot path into the reply, where a number indexes a list (result.content.0.usage.input_tokens). There are no wildcards: the count never depends on a fuzzy match.
  • usd_per_unit is an exact decimal, like "0.00000300", with at most 8 decimal places, and nothing is rounded. Money is counted in whole units of 10⁻⁸ dollars, so totals never drift.
  • The cost of one response is the sum over all meters, because real APIs price input and output differently.
  • Pricing is server-wide. A result missing a declared usage value is a metering failure that stops the run: a server that could omit its usage field could zero its own count. To mix free and priced tools from one upstream, declare its URL twice under two ids with disjoint tools allowlists, one priced and one not.
  • A pricing block with an empty meters list is rejected: it would read as "priced" while metering nothing.

The capsLink to this section

Field Scope Enforcement
max_per_run_usd one run Enforced. Crossing it stops the run, recording spending_limit_reached and then terminated_by_limit, as the time limit does.
max_per_day_usd one UTC day, across runs Enforced, durably. Requires identity.did. Crossing it during a run stops the run, like the per-run cap.
max_per_month_usd a month Declared only. Parsed and validated, never enforced. Declaring it prints a warning.
alerts.warn_at_pct_of_daily a threshold of the daily cap Warns only. The log gets one spending_limit_reached warning the first time a run's day total reaches it.

Amounts are exact decimals, like "0.50". Nothing is rounded, and a cap of "0" is refused: leave the field out instead.

When a run is killedLink to this section

The cap trips when the running total exceeds it. Metering is post-hoc (a response's cost is only known once the response has arrived), so the charge that crosses the cap is still incurred and still recorded. The ledger records the charge, and enforcement stops what happens next. A run's priced calls that pass through the gate go to the server one at a time, so a run overshoots by at most the one call that crosses. Concurrent runs of the same identity can each overshoot max_per_day_usd by one call.

The daily ledgerLink to this section

With a daily cap, each identity has its own daily ledger under ~/.constle/spending/, locked so concurrent runs add up. It is keyed by DID, and rejected without identity.did, because keying it by name would let a rename reset it. Two behaviours follow:

  • A run whose accumulated daily spend already meets or exceeds the cap is refused before the sandbox starts, with a spending_limit_reached event recording action: run_refused.
  • If the ledger can't be read, the run doesn't start.

The warnings you will seeLink to this section

constle validate and constle run say so whenever a cap would not do what it looks like it does:

Situation Warning
Limits declared, no priced MCP server Limits are not enforced: nothing to meter
Limits declared, priced servers present, allowed_hosts non-empty Limits cover only the priced servers, and allowed_hosts traffic is unmetered
Priced servers present, no limits declared Usage is metered but nothing is enforced
max_per_month_usd declared ⚠️ warning: declared, not enforced by this runtime: spending.max_per_month_usd

The quickstart example hits the first row, which is why its run summary prints NOT ENFORCED.

Why allowed_hosts traffic is not meteredLink to this section

Metering that traffic would require Constle to TLS-intercept the agent's connections, letting the runtime read everything the agent says to every allowlisted host, which is far beyond what cost metering needs. Constle refuses to do that, and states the consequence instead: an agent that spends money over allowed_hosts rather than through a priced MCP server has no spending enforcement at all.