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

  • Name
    agent
    Type
    string
    Description

    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.

  • Name
    attestation
    Type
    string
    Description

    The PASETO v4.public capability attestation proving the agent may act. See authentication.

  • Name
    transaction
    Type
    string
    Description

    The client-minted transaction id (txn_…) — the substrate's idempotency key. Re-submitting a committed id replays its outcome rather than posting twice.

  • Name
    entries
    Type
    array
    Description

    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.

  • Name
    consume_usage
    Type
    object | optional
    Description

    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.

  • Name
    meter_usage
    Type
    object | optional
    Description

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

  • Name
    bill_usage
    Type
    object | optional
    Description

    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.

  • Name
    action
    Type
    object
    Description

    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.

  • Name
    evidence
    Type
    array
    Description

    Lowercase-hex SHA-256 digests of external evidence the agent references.

  • Name
    justification
    Type
    string
    Description

    The agent's stated reason. Hashed at the gateway: the hash goes on-ledger, the text is retained off-ledger by you.

  • Name
    policy
    Type
    string
    Description

    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/intents

Submit 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

  • Name
    200
    Type
    outcome
    Description

    Judged. Includes a recorded refusal. Also the replay of an already-committed id.

  • Name
    401
    Type
    unknown_agent
    Description

    The agent:// URI is not bound to a party.

  • Name
    403
    Type
    attestation
    Description

    The attestation was rejected: bad signature, expired, wrong audience, or an untrusted issuer.

  • Name
    409
    Type
    stale_policy
    Description

    The pin did not hold, and nothing was written. Also policy_pin_mismatch and no_active_policy. Carries pinned and active.

  • Name
    421
    Type
    not_primary
    Description

    Cluster mode: this mutation reached a non-primary replica. Carries primary_client_addr.

  • Name
    422
    Type
    unprocessable
    Description

    Well-formed HTTP that could not be recorded: invalid_agent_uri, capability_mapping, multi_currency_mandate_charge, substrate_rejected (kind: "validation"), or gateway_error.

  • Name
    503
    Type
    commit_unavailable
    Description

    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
use std::collections::BTreeMap;

use axorum_client::{AxorumClient, IntentDraft};
use axorum_deontic::{ActionTerm, ActionType};
use axorum_substrate::{Amount, Entry, Side, TransactionId};

let client = AxorumClient::builder("http://127.0.0.1:8080").build()?;
let transaction = TransactionId::from_uuid(uuid::Uuid::now_v7());

let draft = IntentDraft::new(
    "agent://example.com/agent/clerk_01h455vb4pex5vsknk084sn02q",
    attestation,
    transaction,
    ActionTerm::new(ActionType::new("post")?, BTreeMap::new()),
)
.entries(vec![
    Entry { account: cash, side: Side::Debit, amount: Amount::new(50_000, "USD") },
    Entry { account: revenue, side: Side::Credit, amount: Amount::new(50_000, "USD") },
])
.justification("invoice 2214, net 30, within the standing purchase mandate");

let outcome = client.submit_pinned(draft).await?;

Request

{
  "agent": "agent://example.com/agent/clerk_01h455vb4pex5vsknk084sn02q",
  "attestation": "v4.public.eyJhZ2VudCI6ImFnZW50Oi8vZXhhbX...",
  "transaction": "txn_7w5b4trnc7b2ja027jyyb64395",
  "entries": [
    {
      "account": "acc_2n4kqx21g5bty927z2tj955css",
      "side": "Debit",
      "amount": { "minor": 50000, "currency": "USD" }
    },
    {
      "account": "acc_5j8dmv93k1nzq047x8ph622bkt",
      "side": "Credit",
      "amount": { "minor": 50000, "currency": "USD" }
    }
  ],
  "action": { "action": "post", "bindings": {} },
  "evidence": ["3b7f1c2e9a48d05f6b1e7c3a92d4f80b5e6c1a97d2f3b8e40c5a7d19f6b2e8c3"],
  "justification": "invoice 2214, net 30, within the standing purchase mandate",
  "policy": "pol_2s5479gmf7bhrsavd5awacb0fg"
}

Response — permitted

{
  "transaction": "txn_7w5b4trnc7b2ja027jyyb64395",
  "verdict": "Permitted",
  "posted": true,
  "policy": "pol_2s5479gmf7bhrsavd5awacb0fg",
  "obligations_pending": [],
  "justification_hash": "9f2cbe41e0a3c7d1f4b8e2a95c7d3f6018bb24e7c9a1d5f30e8b7c2a4d9f1e6b",
  "provenance": { "tick": 4471 }
}

Response — refused (200)

{
  "transaction": "txn_7w5b4trnc7b2ja027jyyb64395",
  "verdict": "ForbiddenRejected",
  "posted": false,
  "policy": "pol_2s5479gmf7bhrsavd5awacb0fg",
  "obligations_pending": [],
  "justification_hash": "9f2cbe41e0a3c7d1f4b8e2a95c7d3f6018bb24e7c9a1d5f30e8b7c2a4d9f1e6b",
  "provenance": { "tick": 4472 }
}

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.

A stale pin — 409, nothing written

{
  "error": "policy_pin_mismatch",
  "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.",
  "pinned": "pol_2s5479gmf7bhrsavd5awacb0fg",
  "active": "pol_9k18tzq4c7m2ha055xvbn31rjd"
}

Re-pin by hand

// What `submit_pinned` does for you, bounded at three attempts.
match client.submit_intent(draft.clone().pin(policy)).await {
    Err(AxorumError::StalePolicy(failure)) => {
        let policy = failure.active.clone();
        client.submit_intent(draft.pin(policy)).await?
    }
    other => other?,
};

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
use axorum_client::{IntentDraft, UsageMove};
use axorum_substrate::{Amount, Entry, Side};

let spend = Amount::new(1_000, "USD");

let draft = IntentDraft::new(agent, attestation, transaction, action)
    .entries(vec![
        Entry { account: expense, side: Side::Debit, amount: spend.clone() },
        Entry { account: cash, side: Side::Credit, amount: spend.clone() },
    ])
    .consume_usage(UsageMove { usage: usage.clone(), amount: spend.clone() })
    .meter_usage(UsageMove { usage: usage.clone(), amount: spend.clone() })
    .bill_usage(UsageMove { usage: usage.clone(), amount: spend })
    .justification("1,000 inference calls, metered and billed at $10.00");

let outcome = client.submit_pinned(draft).await?;

Request: the moves ride the envelope

{
  "transaction": "txn_7w5b4trnc7b2ja027jyyb64395",
  "entries": [
    {
      "account": "acc_2n4kqx21g5bty927z2tj955css",
      "side": "Debit",
      "amount": { "minor": 1000, "currency": "USD" }
    },
    {
      "account": "acc_5j8dmv93k1nzq047x8ph622bkt",
      "side": "Credit",
      "amount": { "minor": 1000, "currency": "USD" }
    }
  ],
  "consume_usage": {
    "usage": "usage_2n4kqx21g5bty927z2tj955css",
    "amount": { "minor": 1000, "currency": "USD" }
  },
  "meter_usage": {
    "usage": "usage_2n4kqx21g5bty927z2tj955css",
    "amount": { "minor": 1000, "currency": "USD" }
  },
  "bill_usage": {
    "usage": "usage_2n4kqx21g5bty927z2tj955css",
    "amount": { "minor": 1000, "currency": "USD" }
  },
  "action": { "action": "post", "bindings": {} },
  "evidence": [],
  "justification": "1,000 inference calls, metered and billed at $10.00",
  "policy": "pol_2s5479gmf7bhrsavd5awacb0fg"
}

A chain violation: 422, and nothing posted

{
  "error": "substrate_rejected",
  "kind": "validation",
  "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)"
}

Was this page accurate?