agentmesh · build · budgets and allowances

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.

budgets and credits: two instruments, one system

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).

running the adapter instead of an sdk?

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.