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