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

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.

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

GET/api/v1/policies/active
1use axorum_client::AxorumClient;
2
3let client = AxorumClient::builder("http://127.0.0.1:8080").build()?;
4
5let 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 409stale_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

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()?;
8
9// Minted once, by us, before any attempt — the idempotency key.
10let transaction = TransactionId::from_uuid(uuid::Uuid::now_v7());
11
12let draft = IntentDraft::new(
13 "agent://example.com/agent/clerk_01h455vb4pex5vsknk084sn02q",
14 attestation, // the PASETO v4.public capability token
15 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");
23
24// 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, posted is false, 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}
7
8for 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_case code vocabulary, and which codes are worth a retry.
  • Authenticationagent:// URIs and PASETO attestations.