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 moreTypeScript
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 morePython
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 moreWhat 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, andactionload-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
ForbiddenRejectedcomes back as a successful outcome withposted: 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.
| Package | Version | What it gained |
|---|---|---|
axorum-client (crates.io) | 0.4.0 | consume_usage, meter_usage and bill_usage on IntentDraft. The balance read is AdminClient::usage_balance, on the admin plane. |
@axorum/client (npm) | 0.3.0 | usage(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.0 | usage(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.0 | A 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.
TypeScript
The TypeScript client decodes minor units as bigint, because JSON.parse
would silently round a large balance to the nearest double.
Python
The Python client exposes a sync client and an async one over the same
contract; Python's arbitrary-precision int decodes minor units without
special handling.
Your first intent
A client, and one intent
1use axorum_client::{AxorumClient, AxorumError, IntentDraft};23let client = AxorumClient::builder("https://ledger.example.com")4 .timeout(std::time::Duration::from_secs(5))5 .build()?;67let outcome = client.submit_pinned(draft).await?;89// 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
minoris a decimal string, like every other amount this server accepts. A refused usage move is a genuineisErrorresult, 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