Guides
Quickstart
This page walks one intent from end to end: read the policy in force, pin it, submit, read the verdict. That is the whole loop. Everything else in this reference is a detail of one of those four steps.
Install
Rust
The crate re-exports the wire types, so building an envelope and reading an outcome needs no second dependency.
TypeScript
The package ships its own types. minor units decode as bigint, never
number — see balances for why that is not optional.
Python
The package exposes a sync client and an async one. The examples here use the sync client.
Install
1cargo add axorum-client
GET/api/v1/policies/activeRead the policy in force
An intent must name the policy it expects to be judged against. That claim is the pin, and it is not the agent's to invent: it is a statement about the service's state. So the loop starts by asking.
{"policy": null} is a valid answer, not a failure — "no policy is in force" is a true and useful fact about a ledger. Submitting against it is a 409 no_active_policy.
Read the policy in force
1use axorum_client::AxorumClient;23let client = AxorumClient::builder("http://127.0.0.1:8080").build()?;45let policy = client.active_policy().await?; // Option<PolicyId>
Mint the transaction id
The transaction id is minted by the client, once, before any attempt — not by the service. It is the substrate's idempotency key: every re-pinned retry carries the same id, so an attempt that in fact landed replays its stored outcome instead of posting a second time.
Mint it before the first attempt, not inside the retry. An id minted per attempt is not an idempotency key; it is a way to double-post.
POST/api/v1/intentsSubmit the intent
The envelope carries what the agent knows: who is acting (agent and attestation), under which id, what it proposes in the books (entries), what it claims the authority to do (action), the external evidence it relies on, and its justification — plus the policy pin.
Debits and credits must balance per currency. The justification is hashed at the gateway: the hash goes on-ledger, the text stays with you.
The pin loop
If the pinned policy is not the one in force, the service answers 409 — stale_policy at the pre-check, or policy_pin_mismatch if the policy moved in the race between the check and the record — and nothing is written. Re-read the active policy, re-stamp the pin, resubmit. Three attempts, then stop: a service rotating policy faster than that is not one a client can chase, and looping forever would turn a hot rotation into a self-inflicted outage.
Every SDK does that loop for you. submit_pinned reads the policy in force, stamps the pin, and re-pins on a 409, carrying the same transaction id through every attempt.
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()?;89// Minted once, by us, before any attempt — the idempotency key.10let transaction = TransactionId::from_uuid(uuid::Uuid::now_v7());1112let draft = IntentDraft::new(13 "agent://example.com/agent/clerk_01h455vb4pex5vsknk084sn02q",14 attestation, // the PASETO v4.public capability token15 transaction,16 ActionTerm::new(ActionType::new("post")?, BTreeMap::new()),17)18.entries(vec![19 Entry { account: cash, side: Side::Debit, amount: Amount::new(50_000, "USD") },20 Entry { account: revenue, side: Side::Credit, amount: Amount::new(50_000, "USD") },21])22.justification("invoice 2214, net 30, within the standing purchase mandate");2324// Reads the policy in force, stamps the pin, re-pins on a 409.25let outcome = client.submit_pinned(draft).await?;
Read the verdict
Branch on the verdict, never on the status code. A 200 means the intent reached the commit point and was judged. It does not mean the entries posted: posted is false exactly when the verdict is ForbiddenRejected, and that refusal was recorded.
The five verdicts:
- Permittedverdict
Allowed and posted. No open duties.
- ForbiddenRejectedverdict
Refused. Nothing crossed the books,
postedisfalse, and it is still a 200.- ForbiddenRecordedverdict
Posted under an authorized override. The violation is on the record, named.
- ObligatedPendingverdict
Posted, and it opened an obligation.
- ObligatedFulfilledverdict
Posted, and it discharged an open one.
Branch on the verdict
1// A refusal is an `Ok`, not an `Err`.2if outcome.posted {3 println!("recorded and posted: {:?}", outcome.verdict);4} else {5 println!("recorded as refused: {:?}", outcome.verdict);6}78for duty in &outcome.obligations_pending {9 println!("now owed: {} by {}", duty.rule, duty.actor);10}
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": 4471 }9}
Next
- Intents — the envelope field by field, and every status it can answer with.
- Errors — the
snake_casecode vocabulary, and which codes are worth a retry. - Authentication —
agent://URIs and PASETO attestations.