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

Guides

SDKs

Three clients, one contract. Each is held to the same conformance suite, run against a real service — the same routes, the same policy evaluation, the same error bodies — rather than a mock that would only assert what its author believed the service emits.

Rust

Available

axorum-client

The reference implementation. Pins the policy, re-pins on a 409, and surfaces every structured error field the contract makes load-bearing.

Read more

TypeScript

Available

@axorum/client

The same contract for Node and the edge. Decodes minor units as a big integer, so a large balance does not silently lose precision.

Read more

Python

Available

axorum-client

The same contract, sync and async. Held to the Rust client’s conformance suite against a real service, not a mock.

Read more

What an SDK does for you

The agent plane is plain HTTP and you can speak it with curl — carrying your attestation as Authorization: Bearer <attestation> on the party-scoped reads, and reaching the service over TLS, since it refuses plaintext anywhere but loopback. What the clients add is the three places a hand-rolled caller gets it wrong.

The pin loopsubmit_pinned

Reads the policy in force, stamps the pin, and re-pins on a 409 — carrying the same client-minted transaction id through every attempt, bounded at three.

Lossless errorsstructured fields

The contract makes pinned, active, primary_client_addr, kind, currencies, uri, and action load-bearing. A client that collapses non-2xx responses into a generic error shape destroys exactly the fields you need to recover. The SDKs decode the body themselves and keep every one.

A refusal is not an errorOk / resolve / return

ForbiddenRejected comes back as a successful outcome with posted: false, not as a thrown exception. You branch on the verdict.

The usage surface

The clients speak contract 4.0.0, which added the usage-law surface. The addition is purely additive: no existing signature changed, and an intent that carries no usage move serializes byte-identically to one built against the previous release.

PackageVersionWhat it gained
axorum-client (crates.io)0.4.0consume_usage, meter_usage and bill_usage on IntentDraft. The balance read is AdminClient::usage_balance, on the admin plane.
@axorum/client (npm)0.3.0usage(usageId), returning a UsageBalanceReport whose counters are bigint. The three moves on the draft. The invalid_usage_id and unknown_usage wire codes.
axorum-client (PyPI, imported as axorum)0.3.0usage(usage) on both the sync and async clients. The three moves on IntentDraft and IntentEnvelope, omitted from the dict when absent. UsageMove and UsageBalanceReport.
@axorum/mcp (npm)0.3.0A sixth tool, get_usage, and three optional submit_intent inputs.

A usage balance is opened on the admin plane, which the agent-plane clients do not speak. An agent receives a usage id; it never mints one. That is why there is no newUsageId anywhere in these packages.

Install

The selector above — or any tab below — sets your language for the whole reference.

Install

1cargo add axorum-client

Rust

The draft/envelope split is deliberate: an IntentDraft holds everything the agent knows, and pin — a pure function — stamps the one thing only the service knows. That is what makes envelope construction testable without a service running.

Your first intent

A client, and one intent

1use axorum_client::{AxorumClient, AxorumError, IntentDraft};
2
3let client = AxorumClient::builder("https://ledger.example.com")
4 .timeout(std::time::Duration::from_secs(5))
5 .build()?;
6
7let outcome = client.submit_pinned(draft).await?;
8
9// A refusal is an `Ok`. Branch on the verdict, never the status.
10if outcome.posted {
11 println!("posted: {:?}", outcome.verdict);
12} else {
13 println!("refused, and the refusal is on the record: {:?}", outcome.verdict);
14}

Rust

The reference implementation. axorum-client re-exports the wire types, so building an envelope and reading an outcome needs no second dependency. The client is cheap to clone — it shares one connection pool — so one instance can be held by many tasks.

TypeScript

@axorum/client targets Node 20+ and the edge, with no runtime dependencies: the platform's fetch carries the request. Wire types are generated from the frozen contract and re-exported. Every method accepts an AbortSignal; failures are instanceof-able error classes with a stable code. The one thing it will never do is hand you a number for a monetary amount: minor is a 128-bit integer and decodes as bigint.

Python

The axorum-client distribution installs the axorum package: AxorumClient and AsyncAxorumClient over the same contract, both context managers, fully typed. Failures raise an exception hierarchy rooted at AxorumError — with is_retriable and requires_repin telling you what to do next — while a refusal returns like any other outcome, with posted false.

Counters on a usage report are plain ints, so they are exact at any magnitude. That matters here more than anywhere else on the plane: consumed takes no ceiling, and a busy meter's total can lawfully outgrow u64.

MCP

@axorum/mcp exposes the agent plane to an MCP-capable model as six tools: submit_intent, get_active_policy, get_transaction, get_obligations, get_balance, and get_usage. It is built on @axorum/client, so it inherits the pin loop, the typed errors, and the refusal-is-not-an-error rule.

Each tool description is written as a prompt, not as documentation: it is the text the model reads when deciding what to call and what to pass. So submit_intent teaches the law rather than restating the schema. Metered spend is one intent, 0 <= billed <= metered <= consumed, metering past consumption is phantom usage, billing past metering has nothing behind it, and breaking either refuses the entire intent, journal legs included. It also says the thing a model would otherwise get wrong: an unreconciled chain is a lawful transient, not a fault to correct.

get_usage{ usage: string }

One usage balance. The result leads with the ledger's own compliance-English narration, then states the headroom that bounds the next lawful move, so a model learns its limit from a sentence rather than by subtracting figures out of a JSON blob. Counters are decimal strings: consumption takes no ceiling, so a total can outgrow what a JSON number holds exactly.

submit_intentconsume_usage / meter_usage / bill_usage

Three optional inputs, each a usage id plus an amount whose minor is a decimal string, like every other amount this server accepts. A refused usage move is a genuine isError result, unlike a policy refusal: the intent never reached a verdict. It carries the ledger's plain-English refusal with its exact figures, and the advice not to retry it unchanged.

A model that gets unknown_usage back is told to conclude one thing only: it cannot read that balance. Never that the balance is absent. See usages for why the refusal is deliberately blind.

1pnpm add @axorum/mcp