NEWWatch an agent's payment get refused, and read the reason, in the live browser demo.Open the demo
AxorumAxorum
SDK language

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 a 401 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 amount to one side (Debit or Credit) of one account. Debits and credits must balance per currency.

consume_usageobject | optional

A usage move on the balance's consumed total: the agent used a metered resource. A usage id plus an Amount. Omitted entirely when absent, never null. See usages.

meter_usageobject | optional

A usage move on the balance's metered total: the consumption was measured. Metering past consumed is phantom usage, and it is refused.

bill_usageobject | optional

A usage move on the balance's billed total: the metering was billed for. Billing past metered is 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 a 422 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 refused 409 and 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_mismatch and no_active_policy. Carries pinned and active.

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"), or gateway_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

POST/api/v1/intents
1use std::collections::BTreeMap;
2
3use axorum_client::{AxorumClient, IntentDraft};
4use axorum_deontic::{ActionTerm, ActionType};
5use axorum_substrate::{Amount, Entry, Side, TransactionId};
6
7let client = AxorumClient::builder("http://127.0.0.1:8080").build()?;
8let transaction = TransactionId::from_uuid(uuid::Uuid::now_v7());
9
10let 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");
21
22let 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

POST/api/v1/intents
1use axorum_client::{IntentDraft, UsageMove};
2use axorum_substrate::{Amount, Entry, Side};
3
4let spend = Amount::new(1_000, "USD");
5
6let 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");
15
16let 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}