Resources›Intents
Resources
Intents
An intent is what an agent submits: the entries it proposes to post, the action it claims the authority to take, and the policy it believes is in force. Axorum judges it at the commit point and records the outcome — including a refusal.
The intent envelope
Everything the agent knows, plus the one claim it makes about the service's state: the policy pin.
Properties
agentstringThe agent's
agent://URI, e.g.agent://example.com/agent/clerk_01h455vb4pex5vsknk084sn02q. It must be bound to a party, or the request is a401 unknown_agent.attestationstringThe PASETO v4.public capability attestation proving the agent may act. See authentication.
transactionstringThe client-minted transaction id (
txn_…) — the substrate's idempotency key. Re-submitting a committed id replays its outcome rather than posting twice.entriesarrayThe accounting journal legs. Each leg posts a positive
amountto oneside(DebitorCredit) of oneaccount. Debits and credits must balance per currency.consume_usageobject | optionalA usage move on the balance's
consumedtotal: the agent used a metered resource. A usage id plus anAmount. Omitted entirely when absent, nevernull. See usages.meter_usageobject | optionalA usage move on the balance's
meteredtotal: the consumption was measured. Metering pastconsumedis phantom usage, and it is refused.bill_usageobject | optionalA usage move on the balance's
billedtotal: the metering was billed for. Billing pastmeteredis billing with no meter basis, and it is refused.actionobjectThe proposed deontic action and its typed bindings, e.g.
{"action": "post", "bindings": {}}. The action must map to a capability the attestation grants, or the request is a422 capability_mapping.evidencearrayLowercase-hex SHA-256 digests of external evidence the agent references.
justificationstringThe agent's stated reason. Hashed at the gateway: the hash goes on-ledger, the text is retained off-ledger by you.
policystringThe policy the agent believes is in force (
pol_…) — the staleness pin. A stale pin is refused409and nothing is written.
POST/api/v1/intentsSubmit an intent
Commits at the current server tick. The response is the outcome: the verdict, whether the entries posted, the policy that judged it, and every obligation the actor now owes.
A refusal is a 200
An intent the policy in force forbids is recorded and answered 200 OK with posted: false and a ForbiddenRejected verdict. The refusal record is the product — preventive evidence that a control stopped something before it happened, not an alarm reporting a loss.
Branch on verdict and posted. Never on the status code.
Statuses
200outcomeJudged. Includes a recorded refusal. Also the replay of an already-committed id.
401unknown_agentThe
agent://URI is not bound to a party.403attestationThe attestation was rejected: bad signature, expired, wrong audience, or an untrusted issuer.
409stale_policyThe pin did not hold, and nothing was written. Also
policy_pin_mismatchandno_active_policy. Carriespinnedandactive.421not_primaryCluster mode: this mutation reached a non-primary replica. Carries
primary_client_addr.422unprocessableWell-formed HTTP that could not be recorded:
invalid_agent_uri,capability_mapping,multi_currency_mandate_charge,substrate_rejected(kind: "validation"), orgateway_error.503commit_unavailableCluster mode: the commit could not be confirmed. It may or may not have landed — retry (submission is idempotent) or read the transaction.
1use std::collections::BTreeMap;23use axorum_client::{AxorumClient, IntentDraft};4use axorum_deontic::{ActionTerm, ActionType};5use axorum_substrate::{Amount, Entry, Side, TransactionId};67let client = AxorumClient::builder("http://127.0.0.1:8080").build()?;8let transaction = TransactionId::from_uuid(uuid::Uuid::now_v7());910let draft = IntentDraft::new(11 "agent://example.com/agent/clerk_01h455vb4pex5vsknk084sn02q",12 attestation,13 transaction,14 ActionTerm::new(ActionType::new("post")?, BTreeMap::new()),15)16.entries(vec![17 Entry { account: cash, side: Side::Debit, amount: Amount::new(50_000, "USD") },18 Entry { account: revenue, side: Side::Credit, amount: Amount::new(50_000, "USD") },19])20.justification("invoice 2214, net 30, within the standing purchase mandate");2122let outcome = client.submit_pinned(draft).await?;
1{2 "agent": "agent://example.com/agent/clerk_01h455vb4pex5vsknk084sn02q",3 "attestation": "v4.public.eyJhZ2VudCI6ImFnZW50Oi8vZXhhbX...",4 "transaction": "txn_7w5b4trnc7b2ja027jyyb64395",5 "entries": [6 {7 "account": "acc_2n4kqx21g5bty927z2tj955css",8 "side": "Debit",9 "amount": { "minor": 50000, "currency": "USD" }10 },11 {12 "account": "acc_5j8dmv93k1nzq047x8ph622bkt",13 "side": "Credit",14 "amount": { "minor": 50000, "currency": "USD" }15 }16 ],17 "action": { "action": "post", "bindings": {} },18 "evidence": ["3b7f1c2e9a48d05f6b1e7c3a92d4f80b5e6c1a97d2f3b8e40c5a7d19f6b2e8c3"],19 "justification": "invoice 2214, net 30, within the standing purchase mandate",20 "policy": "pol_2s5479gmf7bhrsavd5awacb0fg"21}
1{2 "transaction": "txn_7w5b4trnc7b2ja027jyyb64395",3 "verdict": "Permitted",4 "posted": true,5 "policy": "pol_2s5479gmf7bhrsavd5awacb0fg",6 "obligations_pending": [],7 "justification_hash": "9f2cbe41e0a3c7d1f4b8e2a95c7d3f6018bb24e7c9a1d5f30e8b7c2a4d9f1e6b",8 "provenance": { "tick": 4471 }9}
1{2 "transaction": "txn_7w5b4trnc7b2ja027jyyb64395",3 "verdict": "ForbiddenRejected",4 "posted": false,5 "policy": "pol_2s5479gmf7bhrsavd5awacb0fg",6 "obligations_pending": [],7 "justification_hash": "9f2cbe41e0a3c7d1f4b8e2a95c7d3f6018bb24e7c9a1d5f30e8b7c2a4d9f1e6b",8 "provenance": { "tick": 4472 }9}
The policy pin
The pin is a claim about the service's state, so getting it wrong is a 409 — and on a 409, nothing is written. There is no partial commit to unwind.
Two codes, one meaning:
stale_policy— the pre-propose fast path. The pinned policy is not the one in force.policy_pin_mismatch— the race-closing check at apply time. The policy in force changed between the request being checked and it being recorded.
Both carry pinned and active. Both are answered identically on the standalone and cluster paths. In both cases: re-read GET /api/v1/policies/active and resubmit against the policy now in force, carrying the same transaction id.
Bound the loop at three attempts. Each attempt pins against the policy the service just named, so a second failure means the policy moved again in the microseconds since.
1{2 "error": "policy_pin_mismatch",3 "message": "This request was checked against policy 'pol_2s54…', but policy 'pol_9k18…' took effect before it could be recorded. Nothing was recorded — resubmit the request against the policy now in force.",4 "pinned": "pol_2s5479gmf7bhrsavd5awacb0fg",5 "active": "pol_9k18tzq4c7m2ha055xvbn31rjd"6}
1// What `submit_pinned` does for you, bounded at three attempts.2match client.submit_intent(draft.clone().pin(policy)).await {3 Err(AxorumError::StalePolicy(failure)) => {4 let policy = failure.active.clone();5 client.submit_intent(draft.pin(policy)).await?6 }7 other => other?,8};
Metered spend is one intent
An agent that consumes a metered resource carries the usage moves on the same draft as the journal legs. They are judged in the same commit, under the usage-balance conservation law 0 <= billed <= metered <= consumed.
All three moves at once is the lawful case, not an edge case: the moves of one intent are validated together against the combined post-state. Any subset works too, and metering something an earlier intent consumed is one move, not three.
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 refusal is a plain-English sentence carrying the exact figures.
An absent move is omitted from the envelope entirely. The key is not there, and it is never null. The usages page covers the law, the read, and who may move a balance.
1use axorum_client::{IntentDraft, UsageMove};2use axorum_substrate::{Amount, Entry, Side};34let spend = Amount::new(1_000, "USD");56let draft = IntentDraft::new(agent, attestation, transaction, action)7 .entries(vec![8 Entry { account: expense, side: Side::Debit, amount: spend.clone() },9 Entry { account: cash, side: Side::Credit, amount: spend.clone() },10 ])11 .consume_usage(UsageMove { usage: usage.clone(), amount: spend.clone() })12 .meter_usage(UsageMove { usage: usage.clone(), amount: spend.clone() })13 .bill_usage(UsageMove { usage: usage.clone(), amount: spend })14 .justification("1,000 inference calls, metered and billed at $10.00");1516let outcome = client.submit_pinned(draft).await?;
1{2 "transaction": "txn_7w5b4trnc7b2ja027jyyb64395",3 "entries": [4 {5 "account": "acc_2n4kqx21g5bty927z2tj955css",6 "side": "Debit",7 "amount": { "minor": 1000, "currency": "USD" }8 },9 {10 "account": "acc_5j8dmv93k1nzq047x8ph622bkt",11 "side": "Credit",12 "amount": { "minor": 1000, "currency": "USD" }13 }14 ],15 "consume_usage": {16 "usage": "usage_2n4kqx21g5bty927z2tj955css",17 "amount": { "minor": 1000, "currency": "USD" }18 },19 "meter_usage": {20 "usage": "usage_2n4kqx21g5bty927z2tj955css",21 "amount": { "minor": 1000, "currency": "USD" }22 },23 "bill_usage": {24 "usage": "usage_2n4kqx21g5bty927z2tj955css",25 "amount": { "minor": 1000, "currency": "USD" }26 },27 "action": { "action": "post", "bindings": {} },28 "evidence": [],29 "justification": "1,000 inference calls, metered and billed at $10.00",30 "policy": "pol_2s5479gmf7bhrsavd5awacb0fg"31}
1{2 "error": "substrate_rejected",3 "kind": "validation",4 "message": "metering exceeds consumption for usage balance 'usage_2n4kqx21g5bty927z2tj955css': attempted metered total 7500 exceeds consumed total 7000 (already metered 5000; 2000 consumed but not yet metered)"5}