Usages
A usage balance is the ledger's memory of a metered resource: what an agent consumed, what a meter measured, and what a bill recognized. The three totals are held in order at every commit, so metering usage that never happened is not a policy someone has to remember to write. It is refused.
The conservation chain
Every usage balance carries three running totals in minor units, and the ledger admits a commit only if the chain still holds afterward:
0 <= billed <= metered <= consumed
consumedis the ground-truth event. The agent used the resource.meteredis the measurement recognition of that consumption. Metering more than was consumed is phantom usage, and it is refused.billedis the financial recognition of that metering. Billing more than was metered is billing with no meter basis, and it is refused.
The three are recognized at different moments, so the chain is an ordering, not an equality. Under-recognition (billed < metered < consumed) is a lawful transient: consumption in flight, waiting to be measured and billed. reconciled is true only when all three are equal, and reaching that steady state is reconciliation's job, not an admission rule. Do not treat an unreconciled balance as a fault to fix.
Corrections run backwards. A negative delta (a metering correction, a credit memo) is admitted exactly when the post-state still satisfies the chain, so a counter never goes below zero.
A usage balance is opened on the admin plane, not by an agent. The Rust admin client and the CLI open it, name its single currency, and bind it to an owning party. An agent receives a usage id; it never mints one.
One balance, three totals
consumed $100.00 the agent used it
metered $65.00 a meter measured it
billed $40.00 an invoice recognized it
unmetered $35.00 consumed, not yet metered
unbilled $25.00 metered, not yet billed
reconciled false the three are not equal yet
Metered spend is one intent
The three moves ride the intent draft, beside the journal legs, and they are judged in the same commit under the same law. Consuming a metered resource, measuring it, and billing for it is one intent, not three.
The moves
- Name
consume_usage- Type
- UsageMove | optional
- Description
A signed delta on the balance's
consumedtotal: the agent used the resource.
- Name
meter_usage- Type
- UsageMove | optional
- Description
A signed delta on the balance's
meteredtotal: the consumption was measured.
- Name
bill_usage- Type
- UsageMove | optional
- Description
A signed delta on the balance's
billedtotal: the metering was billed for.
A UsageMove is a usage id plus an Amount, denominated in the balance's own currency. A move in any other currency is refused. An absent move is omitted from the envelope entirely, never sent as null.
All three at once is the lawful case, not an edge case: the moves of one intent are validated together against the combined post-state, so a single intent may consume, meter, and bill the same amount. Any subset works too. Metering something an earlier intent consumed is one move, not three.
A violating move refuses the whole intent
A move that breaks the chain is a 422 substrate_rejected with kind: "validation", and nothing posts: not the usage moves, and not the journal legs that rode with them. The ledger is left byte-identical. All the checks run before any mutation, so there is no partial commit to unwind.
The refusal arrives in compliance English with the exact figures, which is what you show a human.
Consume, meter, and bill in one intent
use axorum_client::{AxorumClient, IntentDraft, UsageMove};
use axorum_substrate::{Amount, Entry, Side};
// The usage id was handed to the agent. The admin plane opened the balance.
let spend = Amount::new(1_000, "USD");
let draft = IntentDraft::new(agent, attestation, transaction, action)
.entries(vec![
Entry { account: expense, side: Side::Debit, amount: spend.clone() },
Entry { account: cash, side: Side::Credit, amount: spend.clone() },
])
.consume_usage(UsageMove { usage: usage.clone(), amount: spend.clone() })
.meter_usage(UsageMove { usage: usage.clone(), amount: spend.clone() })
.bill_usage(UsageMove { usage: usage.clone(), amount: spend })
.justification("1,000 inference calls, metered and billed at $10.00");
// One commit. The money and the usage chain are judged together.
let outcome = client.submit_pinned(draft).await?;
The moves on the envelope
{
"transaction": "txn_7w5b4trnc7b2ja027jyyb64395",
"entries": [
{
"account": "acc_2n4kqx21g5bty927z2tj955css",
"side": "Debit",
"amount": { "minor": 1000, "currency": "USD" }
},
{
"account": "acc_5j8dmv93k1nzq047x8ph622bkt",
"side": "Credit",
"amount": { "minor": 1000, "currency": "USD" }
}
],
"consume_usage": {
"usage": "usage_2n4kqx21g5bty927z2tj955css",
"amount": { "minor": 1000, "currency": "USD" }
},
"meter_usage": {
"usage": "usage_2n4kqx21g5bty927z2tj955css",
"amount": { "minor": 1000, "currency": "USD" }
},
"bill_usage": {
"usage": "usage_2n4kqx21g5bty927z2tj955css",
"amount": { "minor": 1000, "currency": "USD" }
},
"action": { "action": "post", "bindings": {} },
"evidence": [],
"justification": "1,000 inference calls, metered and billed at $10.00",
"policy": "pol_2s5479gmf7bhrsavd5awacb0fg"
}
Refused: 422, and nothing posted
{
"error": "substrate_rejected",
"kind": "validation",
"message": "billing exceeds metered usage for usage balance 'usage_2n4kqx21g5bty927z2tj955css': attempted billed total 6200 exceeds metered total 5000 (already billed 4000; 1000 metered but not yet billed)"
}
Read a usage balance
The three totals, the two headrooms that follow from them, whether the chain has reconciled, and the balance narrated in plain English. This is a party-scoped read: the client presents its attestation, and a client holding none fails locally before a request is sent.
Path parameters
- Name
id- Type
- string
- Description
The usage-balance id (
usage_…). A malformed id is a400 invalid_usage_id.
Query parameters
- Name
format- Type
- string | optional
- Description
json(the default), ortextfor the compliance-English narration on its own.
Response
- Name
usage- Type
- string
- Description
The usage-balance id, echoed back.
- Name
currency- Type
- string
- Description
The single currency every move against this balance must match.
- Name
consumed- Type
- integer
- Description
The running total consumed, in minor units.
- Name
metered- Type
- integer
- Description
The running total metered. Never above
consumed.
- Name
billed- Type
- integer
- Description
The running total billed. Never above
metered.
- Name
unmetered- Type
- integer
- Description
The un-metered headroom (
consumed − metered): what may still be metered.
- Name
unbilled- Type
- integer
- Description
The un-billed headroom (
metered − billed): what may still be billed.
- Name
reconciled- Type
- boolean
- Description
Whether the three totals are equal.
- Name
text- Type
- string
- Description
The balance narrated in compliance English, with exact figures. Prefer it whenever you have a human to show.
Decode the counters as big integers
Every counter is a 128-bit integer, emitted as a bare JSON number, the same rule as Amount.minor. It matters more here than anywhere else on the plane: consumed takes no ceiling, so a busy meter's total can lawfully outgrow u64. bigint in TypeScript, int in Python, i128 in Rust.
Read a usage balance
use axorum_client::AdminClient;
// The agent-plane Rust client carries usage moves on the draft; the usage
// read is the admin plane's, where the balance was opened.
let admin = AdminClient::builder("http://127.0.0.1:8080")
.token(admin_token)
.build()?;
let report = admin.usage_balance(&usage).await?;
// The ledger's own narration, in compliance English with exact figures.
println!("{}", report.text);
Response
{
"usage": "usage_2n4kqx21g5bty927z2tj955css",
"currency": "USD",
"consumed": 10000,
"metered": 6500,
"billed": 4000,
"unmetered": 3500,
"unbilled": 2500,
"reconciled": false,
"text": "Usage balance 'usage_2n4kqx21g5bty927z2tj955css', denominated in USD.\nConsumed to date: $100.00. Metered to date: $65.00. Billed to date: $40.00.\n$35.00 consumed but not yet metered; $25.00 metered but not yet billed.\nThe conservation law holds: billing does not exceed metering, and metering does not exceed consumption. The balance reconciles when the three totals are equal.\n"
}
Response: unknown, or not yours (404)
{
"error": "unknown_usage",
"message": "no such usage balance"
}
Ownership, and the refusal that tells you nothing
A usage balance is bound to an owning party when it is opened, and only that party may move it or read it.
The read is a 404, and it is deliberately blind
A balance owned by another party, a balance opened with no owner at all, and a balance that was never opened all answer the same 404 unknown_usage, with the same message. The cases are indistinguishable on purpose: splitting them would let any attested agent probe candidate usage ids across a tenant boundary and learn which ones are real, and whose. A usage id is never an existence oracle.
So a 404 means one thing only: you cannot read this balance. Surface it as it stands. There is nothing further in it to infer, and in particular it is not evidence that the balance is absent.
A 403 on this route is not an ownership answer. It means the attestation itself was rejected: a bad signature, an expired token, an untrusted issuer. Ownership never produces a 403 here.
The write is refused by the law, not by the boundary
A consume, meter, or bill move against a balance the acting party does not own is refused by the substrate law itself, as a 422 substrate_rejected. The refusal names no owner and discloses nothing about the balance, and it is checked before the currency comparison, so a non-owner cannot provoke a currency mismatch and learn what the balance is denominated in.
Errors
- Name
400- Type
- invalid_usage_id
- Description
The path segment is not a usage-balance id.
- Name
401- Type
- unknown_agent
- Description
No attestation was presented, or the attested
agent://URI is not bound to a party.
- Name
403- Type
- attestation
- Description
The attestation was rejected. Not an ownership verdict.
- Name
404- Type
- unknown_usage
- Description
No such balance, or it is not yours. The two are indistinguishable.
- Name
422- Type
- substrate_rejected
- Description
On the intent path: a move broke the chain, moved the wrong currency, or landed on a balance the acting party does not own. Carries
kind: "validation". Nothing posted.
A move on a balance you do not own: 422
{
"error": "substrate_rejected",
"kind": "validation",
"message": "the usage balance 'usage_2n4kqx21g5bty927z2tj955css' is not available to the acting party: a usage balance may be consumed, metered, or billed only by the party that owns it"
}