Add spending controls
to your agent
Five tasks, in both SDKs: attach a budget to a request, refuse work that does not fit and include an estimate, revise a budget mid-task, arm an owner allowance and report usage against it, and read the spend ledger. Every snippet names the conformance fixture case that pins its behaviour. Background first? Read budgets and allowances.
Everything on this page is the negotiation instrument: offers,
refusals, estimates, and caps between two parties, in any currency. What the
platform actually clears is credits, and
credits are a valid currency in every shape here: currency code
"XCR", one credit = 1,000,000 micro. A priced
hosted agent refuses an unpaid mesh caller exactly as task 02 shows, with its
retail price (the owner's rate plus the mesh's markup) as the
details.estimate counter-offer in
XCR. When one of your agents calls a priced agent, the charge lands on
your account and ALSO folds into that agent's spend ledger (task 05's read)
in XCR, so an owner allowance covers credit spend like any other
currency. Credits spent through the A2A gateway appear only on your
account's Credits statement, because the caller
there is your API key, not one of your agents.
01 · the requester's side
Attach a budget to a request
The budget rides the request envelope and is covered by its signature. State
it absolutely: an RFC-3339 deadline, a ceiling in integer micro-units
(1,000,000 = one currency unit), or both, with
revision: 0 on the initiating request. If the
responder cannot work within it, the request rejects with the responder's
counter-offer in error.details.
TypeScript · RequestConfig.budget
import { AgentMesh, MeshError, ErrorCode } from "agentmesh";
try {
const result = await mesh.request(agentId, "caselaw-summary",
{ question: "controlling case law on anticipatory repudiation" },
{
budget: {
deadline: "2026-07-28T16:00:00Z",
revision: 0,
cost_ceiling: { amount_micro: 2_000_000, currency: "USD" }, // $2.00
},
});
console.log(result.payload.output);
} catch (err) {
if (err instanceof MeshError && err.code === ErrorCode.BUDGET_INSUFFICIENT) {
// the counter-offer: the responder's price, cost_ceiling-shaped
console.log("their price:", err.details?.estimate);
}
if (err instanceof MeshError && err.code === ErrorCode.DEADLINE_UNMEETABLE) {
console.log("earliest:", err.details?.earliest_completion);
}
}
Rust · request_with_options
use agentmesh::{Budget, CostCeiling, RequestOptions};
use serde_json::json;
let budget = Budget::with_ceiling(CostCeiling::new(2_000_000, "USD")) // $2.00
.and_deadline("2026-07-28T16:00:00Z"); // revision stays 0
let result = mesh.request_with_options(&agent_id, "caselaw-summary",
json!({ "question": "controlling case law on anticipatory repudiation" }),
RequestOptions { budget: Some(budget), ..Default::default() },
).await?;
// convenience wrapper for exactly this: mesh.request_with_budget(id, skill, input, budget)
// a refusal surfaces as MeshError::Refusal with code BUDGET_INSUFFICIENT /
// DEADLINE_UNMEETABLE and the counter-offer in error.details
Pinned by budget.json: block.valid / block.invalid (the shape rules) and envelope (a really-signed request carrying this exact block; one changed micro-unit must fail signature verification).
02 · the responder's side
Refuse at admission, with an estimate
Admission decisions belong before the accept signal, never in the handler body: a refusal thrown from the handler would arrive after the accept, which §6.4a forbids. Both SDKs give admission its own hook. Always include an estimate: a refusal without one gives the requester nothing to resubmit against.
TypeScript · HandlerOptions.admit + budgetInsufficient
import { budgetInsufficient, deadlineUnmeetable } from "agentmesh";
const PRICE = { amount_micro: 5_000_000, currency: "USD" }; // my price: $5.00
mesh.onRequest("caselaw-summary", async (input, ctx) => {
// ...the work itself, admitted only if the hook below let it in
return { summary: await summarize(input) };
}, {
admit: (ctx) => {
const offer = ctx.budget?.cost_ceiling; // the §7.7 offer, validated by the SDK
if (offer !== undefined && offer.amount_micro < PRICE.amount_micro) {
throw budgetInsufficient(PRICE); // details.estimate = my counter-offer
}
// time axis: throw deadlineUnmeetable("2026-07-28T17:30:00Z") when the
// deadline is the problem; the SDK itself already refuses deadlines
// that have passed before admission
},
});
Rust · on_admission + budget_insufficient
use agentmesh::{budget_insufficient, CostCeiling};
mesh.on_admission("caselaw-summary", |_input, ctx| {
let price = CostCeiling::new(5_000_000, "USD"); // my price: $5.00
match ctx.budget.as_ref().and_then(|b| b.cost_ceiling.as_ref()) {
Some(offer) if offer.amount_micro < price.amount_micro => {
Err(budget_insufficient(Some(price),
"the work cannot be done within the offered ceiling"))
}
_ => Ok(()), // admits: the SDK emits the §6.4a accept, then runs the handler
}
});
Pinned by budget.json: refusals.budget_insufficient (details.estimate, cost_ceiling-shaped) and refusals.deadline_unmeetable (details.earliest_completion, an RFC-3339 instant under a different name, so a reader never type-sniffs), plus the ordering rule in accept-signal.json: the refusal happens instead of an accept, never after one.
03 · mid-task
Revise a budget
When a Task hits its ceiling, the responder pauses in
input_required with
BUDGET_EXHAUSTED, reporting
details.spent and
details.estimate_to_finish. The input required is
money: revise and work resumes, or cancel and keep the partial artifacts.
Revisions are absolute, never deltas: each one states the entire
budget, and the highest revision is the whole truth.
TypeScript · reviseBudget
mesh.onTaskUpdate((update, envelope) => {
if (envelope.error?.code === "BUDGET_EXHAUSTED") {
// details.spent + details.estimate_to_finish are the two numbers you need.
// State the WHOLE budget again; revision may be omitted, the SDK tracks
// the current revision per task and increments it.
mesh.reviseBudget(update.task_id, {
deadline: "2026-07-28T16:00:00Z", // omit it and the revised budget HAS no deadline
cost_ceiling: { amount_micro: 6_000_000, currency: "USD" },
});
}
});
// the governing budget at any moment (highest revision seen):
const current = mesh.currentBudget(taskId);
Rust · revise_budget
use agentmesh::CostCeiling; // start from the recorded budget and restate it at the next revision let current = mesh.task_budget(&task_id).expect("a party to the task"); let raised = current.revised() // revision + 1, absolute .and_ceiling(CostCeiling::new(6_000_000, "USD")); mesh.revise_budget(&task_id, Some(responder_id.as_str()), raised).await?; // monotonicity is enforced locally: a revision at or below the recorded one // fails with TASK_INVALID_TRANSITION, the same refusal the task manager gives
Pinned by budget.json: revisions.ordering_case (strictly-greater wins; equal revisions are first-writer-wins), revisions.absolute_case (an omitted axis is GONE, nothing is inherited), and refusals.budget_exhausted_pause (the pause shape: non-terminal, status: "input_required", spent not spend).
04 · the owner's side
Arm an allowance, report usage
The allowance (EXT-8) is the signed,
node-held policy that caps what your own agent may spend on model
usage. Once armed,
enforcement is SDK-automatic: every inbound request is priced and judged
against the ceilings before the accept, and an exhausted allowance refuses
with the same BUDGET_INSUFFICIENT shape as a
too-low budget, on the wire indistinguishable, deliberately. What the SDK
cannot see is your model bill, so you report it:
{ tokens } converts through the document's
declared cost model with the fixture-pinned floor,
{ cost_micro } is money directly.
TypeScript · setAllowance + ctx.reportUsage
import { readFileSync } from "node:fs";
// the owner-signed document (see the shape on the concepts page; sign your
// own with the exported signAllowance(doc, ownerKeyPair))
const status = mesh.setAllowance(JSON.parse(readFileSync("allowance.json", "utf8")), {
// the owner channel for on_exhausted: "ask_owner". Return true only after
// raising the ceiling with a fresh setAllowance; false refuses with a price.
onAskOwner: async (question) => false,
});
if (!status.valid) console.error("armed FAIL-CLOSED:", status.error);
// fail-closed, not absent: a document that does not verify treats every
// ceiling as exhausted until a valid replacement arrives
mesh.onRequest("caselaw-summary", async (input, ctx) => {
const answer = await callModel(input);
ctx.reportUsage({ tokens: answer.usage.total_tokens }); // floor-metered, booked
return { summary: answer.text }; // to task + context + UTC day
});
// learned the bill after the turn instead? mesh.reportUsage(taskId, { tokens })
Rust · set_allowance + report_usage
use agentmesh::Usage; // arm from the document's on-disk JSON (sign your own with sign_allowance) mesh.set_allowance_json(&std::fs::read_to_string("allowance.json")?)?; // an unverifiable document returns Err AND leaves the agent fail-closed, // never unguarded; mesh.allowance_status() makes the state observable // the ask_owner channel; with none registered, ask_owner refuses mesh.on_allowance_question(|question| false); let m = mesh.clone(); mesh.on_request_ctx("caselaw-summary", move |input, _ctx| { let m = m.clone(); async move { let answer = call_model(&input).await?; m.report_usage(Usage::Tokens(answer.total_tokens))?; // ambient task context Ok(serde_json::json!({ "summary": answer.text })) } }); // outside a handler: mesh.report_task_usage(&task_id, context_id, usage)
Pinned by allowance.json: document.valid (really-signed vectors under the agentmesh-allowance-v1 tag, including the three-scope reference vector), metering.cases (floor(tokens × per_1k_tokens_micro / 1000), to the micro-unit), refusal.budget_insufficient (byte-identical to the budget refusal shape), and exhaustion.refuse / exhaustion.ask_owner.
05 · the spend ledger
Read the ledger
Everything metered is queryable: every entry plus the per-task, per-context,
and per-UTC-day rollups the ceilings are enforced against. This is the
arithmetic behind admission: the binding ceiling for a decision is
whichever applicable ceiling has the smallest remaining amount, and
remaining is amount_micro minus the rollup for
that ceiling's scope.
TypeScript · allowanceLedger
const ledger = mesh.allowanceLedger(); console.log(ledger.currency, ledger.total_micro); console.log(ledger.by_task[taskId]); // what one task spent console.log(ledger.by_context["meetup-s1-e4"]); // what one context spent console.log(ledger.by_day["2026-07-28"]); // the UTC day's rollup // mesh.allowanceStatus() has the armed document, or the fail-closed error
Rust · allowance_ledger
use agentmesh::today_utc; let ledger = mesh.allowance_ledger(); println!("task: {}", ledger.task_spend(&task_id)); println!("context: {}", ledger.context_spend("meetup-s1-e4")); println!("today: {}", ledger.day_spend(&today_utc())); // micro-units
Pinned by allowance.json: precedence.cases (smallest-remaining binds: a day ceiling with $0.10 left binds before a task ceiling with $0.50 left, and an estimate exactly equal to the remaining balance admits).
The mesh-adapter renders inbound budgets as a one-line frame, refuses with reply --refuse-budget, and arms allowances with allowance set. See the commands in get a name · spending controls.