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
- agentstring
The 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.- attestationstring
The PASETO v4.public capability attestation proving the agent may act. See authentication.
- transactionstring
The client-minted transaction id (
txn_…) — the substrate's idempotency key. Re-submitting a committed id replays its outcome rather than posting twice.- entriesarray
The accounting journal legs. Each leg posts a positive
amountto oneside(DebitorCredit) of oneaccount. Debits and credits must balance per currency.- consume_usageobject | optional
A 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 | optional
A usage move on the balance's
meteredtotal: the consumption was measured. Metering pastconsumedis phantom usage, and it is refused.- bill_usageobject | optional
A usage move on the balance's
billedtotal: the metering was billed for. Billing pastmeteredis billing with no meter basis, and it is refused.- actionobject
The 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.- evidencearray
Lowercase-hex SHA-256 digests of external evidence the agent references.
- justificationstring
The agent's stated reason. Hashed at the gateway: the hash goes on-ledger, the text is retained off-ledger by you.
- policystring
The 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
- 200outcome
Judged. Includes a recorded refusal. Also the replay of an already-committed id.
- 401unknown_agent
The
agent://URI is not bound to a party.- 403attestation
The attestation was rejected: bad signature, expired, wrong audience, or an untrusted issuer.
- 409stale_policy
The pin did not hold, and nothing was written. Also
policy_pin_mismatchandno_active_policy. Carriespinnedandactive.- 421not_primary
Cluster mode: this mutation reached a non-primary replica. Carries
primary_client_addr.- 422unprocessable
Well-formed HTTP that could not be recorded:
invalid_agent_uri,capability_mapping,multi_currency_mandate_charge,substrate_rejected(kind: "validation"), orgateway_error.- 503commit_unavailable
Cluster mode: the commit could not be confirmed. It may or may not have landed — retry (submission is idempotent) or read the transaction.
Submit an intent
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}
Re-pin by hand
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.
A money movement and its usage moves
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}