Why a ledger for AI spending has to say yes or no at the exact instant of the payment.
An agent breaks that assumption. You tell it to buy the team's coffee supplies. It does, it finishes the errand, and it is gone. Nobody signed, nobody reviewed, and the charge is already on your books. There is no one left to ask.
You are probably not that company. You are building the software it runs on: an accounts payable, spend, or procurement platform on the buying side, or a billing or monetization platform on the selling side. Your customers' agents are starting to spend and get paid, and they will come to you for the controls.
Axorum is what you embed to give them those controls. It is a subledger, not a general ledger: it sits on each participant's own books, the buyer's and the seller's alike, and records agent spending in detail.
Read your books throughout as the books of the company your platform serves. The controls are the ones your customers will ask you for. If you are that finance team rather than the platform, the course still reads straight. You are the customer in that sentence.
There are two sides to every payment: the arithmetic of the books, and the authority. A customs checkpoint is the metaphor for this whole module. At a border, the goods must be declared correctly, and you must be allowed to bring them in. Fail either side and nothing crosses. That is exactly how Axorum handles every payment.
Axorum is a ledger built for this. Instead of letting the money move and auditing it later, it decides at the instant of the payment, and it records why it said yes.
Let the money move, write it in a log, and read the log next month. That works when a person chose to spend, because you can go and ask them.
Both sides are checked before the payment is ever recorded: the arithmetic of the books, and the authority to spend. The agent can vanish. The decision, and the reason for it, are already on the record.
A payment is recorded only if it passes every one of the ledger's laws at the same moment. There are five today. If any one of them fails, nothing is written down, the way a single denied stamp at customs hands your bags back.
Four of the five are the arithmetic of your books: the entry balances, the budget holds, revenue matches what was earned, and the bill matches the meter. The fifth is the authority: was this allowed under the rules your team wrote. Two of them do the work on this first coffee purchase. You will meet all five up close in Module 5.
It has to balance. Every debit is matched by an equal credit, per currency. Money is not created or lost, only moved.
The payment was actually allowed under the written policy in force at that moment. The rules your team wrote said this agent could spend here.
The spending stays within a cumulative budget. The total ever spent against a mandate can never pass its ceiling. (Module 5.)
Revenue recognized as earned never exceeds what was taken in advance. The GAAP matching rule, enforced. (Module 5.)
What you bill never runs past what you metered, and what you meter never runs past what was actually delivered. No charge for service that never happened. (Module 5.)
Most systems ask one question: can this payment move money? Axorum also asks whether your own rules allow it, and whether the budget still holds. It answers all of them before anything is recorded, and it answers them in one step. As your books come to need new laws, they join that same single check.
Our running example uses real rules: purchases are allowed, but any purchase over $1,000 is forbidden, and every purchase creates a duty to file a receipt. Keep those numbers in mind. They come back in later modules.
The decision, the refusal or the approval, and the reason all happen before the money leaves. Here is that sequence for a single purchase of coffee supplies, $500, from the moment the agent submits it to the moment it is posted. Click through each step.
Notice the split, because your auditor already has a name for it. The Gateway (passport control) checks who is asking and on whose authority. The Vault (the books) applies the laws. The part that checks the authority is not the part that records the money, so neither one can quietly cover for the other. Here are those same seven steps as cards you can scan:
Buy coffee supplies, $500, submitted with its ID and the pass that grants it the authority to spend.
Is this a real sender, and was this agent actually granted the authority to spend?
Confirmed: a real sender, and someone authorized it. Only now does the request reach the books.
Do the debits equal the credits? The $500 leaving must equal the $500 arriving.
Do the rules your team wrote allow this purchase? It is under $1,000, so it is within policy.
They fire only when a purchase spends against a budget or moves deferred revenue. This plain coffee order does neither, so they pass instantly. You will see them do real work in Module 5.
The payment is posted to the books, Approved, along with the reason it was allowed.
It records who authorized it, which rule allowed it, and the reason that was given. That is why the agent can disappear and you can still answer your auditor.
The agent does not simply send "pay $500." It hands over a declared package, the customs form, and every field on it is something you would want in front of you at the close: who is asking (the agent's ID badge), on whose authority (a tamper-evident digital pass, issued by the company that vouches for the agent; alter one character of it and it stops verifying), a receipt number (so a retry can never double-charge), the entries (the debits and the credits), what it wants to do (make a purchase), the evidence (any supporting documents it is relying on), the reason (in the agent's own words), and which rulebook it believes is in force. The reason is sealed at the door, so it cannot be quietly rewritten later.
The business rule: an agent may not spend unless it declares all eight of these, in one package, up front.
Who is asking, by a unique address that names the agent.
On whose authority, carried on a tamper-evident digital pass.
A receipt number, so a retry can never double-charge.
The entries: the actual debits and credits this payment would post.
The action: what it is trying to do, for example make a purchase.
The evidence: any supporting document it is relying on, sealed so it cannot be swapped later.
The reason: why the agent says this spend is justified. Sealed at the door.
The rulebook: which version of your policy the agent believes is in force.
pub struct IntentEnvelope {
/// The agent's `agent://` URI (parsed and validated at the gateway).
pub agent: String,
/// The PASETO v4.public capability attestation.
pub attestation: String,
/// The client-minted transaction id (the substrate idempotency key).
pub transaction: TransactionId,
/// The accounting journal legs.
pub entries: Vec<Entry>,
/// The proposed deontic action and its typed bindings.
pub action: ActionTerm,
/// Hashes of external evidence the agent references.
pub evidence: Vec<ContentHash>,
/// The agent's stated reason (hashed at the gateway; the hash goes on-ledger,
/// the text is stored off-ledger by the caller).
pub justification: String,
/// The policy the agent believes is in force (the staleness pin).
pub policy: PolicyId,
}The laws run in order, and the first failure ends the payment. Law 1 confirms the entry balances and lays out where each amount would be posted. Law 2 looks up the rulebook that is actually in force at that moment, and if there is no rulebook in force at all, the payment is refused: no action can be judged against rules that do not exist. Only after every applicable law holds is anything written to the books.
Law 1, the entry balances: confirm the debits equal the credits, and lay out where each amount will be posted.
If it does not balance, the payment stops here and nothing is recorded.
Law 2, someone authorized it: look up the rulebook in force at this moment.
If no rulebook is in force, the payment is refused. Nothing can be judged against rules that do not exist.
Only after both hold does the payment reach the books.
// Conservative law (validate fully; produce the posting plan).
let plan = self.plan_postings(&entries)?;
check_conservation(&entries)?;
// Normative law.
let active_id = self
.policies
.active_id()
.ok_or(SubstrateError::NoActivePolicy)?
.clone();Each question puts you in a situation you would recognise from a close, and asks you to trace what the ledger does.
The part that checks who is asking is walled off from the part that records the money. In Module 2 · Meet the Cast, each character is introduced by name, and the line between the messy outside world and the tidy books turns out to be a control you already know: segregation of duties.
Six characters, and one hard line down the middle: the part that decides never touches the messy outside world.
One line runs down the middle of Axorum, and every part of it sits on one side of that line or the other. The part that deals with the messy outside world is not the part that decides whether a payment is allowed, and it is not the part that records the money.
Think of a airlock into a clean room. Outside is the messy world: wind, dust, things nobody can predict. Inside, nothing is left to chance and every action is exactly repeatable.
Passes to verify. Clocks to read. Networks that drop messages. Nothing here is guaranteed to happen the same way twice.
Only arithmetic and rules. Nothing in here depends on when you ask, or on which machine you ask.
Axorum splits in two: the edge, which handles the messy outside world, and the part that decides, which is sealed off from it. The line is deliberate. It is what lets your books be copied across machines in Module 6.
Inside, the answer is repeatable: the same question always gets the same answer, today, tomorrow, and on any machine. Hold on to that. Modules 6 and 7 both rest on it.
Every part of Axorum has exactly one job, and each one lives on a known side of the airlock. Here is the cast.
The bouncer. Checks who is asking and whether they are allowed in, using the seal on their pass and the clock. This is the airlock itself. (Module 3 shows exactly how it checks who is asking.)
Your spending policy, written in a short rule language. What is allowed, what is forbidden, and what creates a duty someone still has to discharge. (Module 4 opens this up.)
Reads the Rulebook against the payment actually proposed and records the outcome: Approved, Refused, Approved by exception, Obligation opened, or Obligation closed.
The books. The ledger that records a payment only if both sides hold: the arithmetic balances, and the rules your team wrote allow it.
Several copies of the books, on separate machines, that must agree on the order of events before anything counts. (Module 6.)
Holds real payments back from the payment rails until the Vault says yes. (Module 7.)
Each character has exactly one job, and you already know why that matters. Your auditor calls it segregation of duties. The part that checks who is asking is not the part that records the money, so neither one can quietly cover for the other.
The clock and the seal never cross the line: they are used up at the door and thrown away. Three characters live inside the clean room, the Rulebook, the Judge and the Vault, and not one of them reads a clock, checks a seal, or waits on a network. The Gateway does all of that out front. It verifies the seal on the pass, confirms the pass has not expired, strips both away, and hands the part that decides a plain request with nothing unpredictable left in it.
The three inner rooms (Rulebook, Judge, Vault) are the clean room. Nothing in there reads a clock, checks a seal, or leaves anything to chance.
The Gateway is the airlock in front of them: it checks the seal on the pass and the expiry date.
It then hands a clean, stripped-down request to the books.
The messy world stops here, at the door, and never leaks inward.
//! Everything inside the replicated state machine (`axorum-deontic`,
//! `axorum-eval`, `axorum-substrate`) is **deterministic**: no crypto, no
//! wall-clock, no randomness. `axorum-agent` is the **non-deterministic
//! boundary** in front of it: it verifies signatures and expirations, translates
//! a verified wire envelope into a deterministic
//! `TransactionProposal`, and drives `axorum_substrate::Ledger`. Crypto and time
//! live **here** and never leak inward.That is why your books can live on more than one machine. Hand several copies of the books the same events in the same order and they arrive at the same balances, to the last cent. Module 6 shows what that is worth on the day a machine dies.
Before anything reaches the clean room, the Gateway asks four questions, in this order. Who is asking? Look up the agent's address and find the party it acts for. Is it allowed to act, and is its pass still in date? Check the seal, trace it back to a company you trust, confirm the pass has not expired, and confirm it actually covers a purchase. Are we both reading the same rulebook? The rules the agent assumed must be the rules in force. Only then does the request cross the line to be recorded.
1. Identity. Who is asking? Look up the agent's address and find the party it acts for.
2. Authority. Is it allowed, and is its pass still in date? Check the tamper-evident digital pass, trace its seal back to a company you trust, and confirm against the clock that it has not expired.
3. Policy pin. Are we both reading the same edition of the Rulebook? The rules the agent assumed must be the ones in force.
6. Commit. Only now does the request cross into the clean room to be recorded.
//! 1. **Identity** — parse + validate the `agent://` URI; resolve its registry
//! binding to a `PartyId` and roles.
//! 2. **Authority** — verify the PASETO attestation (signature chases to a
//! trusted root; token subject is this URI), check expiry against the injected
//! `wall_now`, and require capability coverage for the action.
//! 3. **Policy pin** — the pinned policy must be the one in force.
//! ...
//! 6. **Commit** — the deterministic substrate commit (checks #3–4).Step 2 is the only place the clock is read (wall_now in the listing above), the only place the seal is checked, and the place the pass must be shown to cover the purchase being made. All of it is spent here, at the door. By step 6 it is gone, and the clean room never sees any of it.
By the time a payment reaches the books, nothing unpredictable is left in it. Watch the Gateway hand over to the Vault, and watch the airlock door close.
The Vault never learns there was a seal or a clock. By the time a request reaches it, everything unpredictable has been spent and thrown away at the door. That is what lets a second machine keep the same books and reach the same answer, to the last cent.
Three situations, each asking the same question: which side of the airlock does this belong on?
Six months from now, your auditor will ask who approved a payment, and the agent that made it will no longer exist. Module 3, Who Are You?, stays at the door and answers that question: how an agent proves who it is, and that it was allowed to act at all, before a single dollar moves.
How the books prove that a vanishing agent is who it claims to be, and is allowed to spend at all.
Six months from now, your auditor will ask who approved a payment. The agent that made it will not exist. Axorum answers anyway, because the proof of who authorized it was taken at the instant of the payment and travels with the record.
A person has fingerprints and a memory. A company has a headquarters and a domain name. An agent has neither. It is started, copied, moved between clouds, and shut down, often with no human watching: it finishes its errand and disappears. So who it was has to be settled while it is still standing at the counter, because afterwards there is no one left to ask.
Module 1's customs officer inspected the goods: the five laws that check the arithmetic of the books and the authority to spend. This module is the booth right next to it, passport control, which settles who is asking before any of that begins.
Most agents today are identified by where they run: a web address, a network address. Move the agent to another server and that identity breaks. The paper behind Axorum's naming puts it bluntly:
The fix is an old one from the early internet (Saltzer, 1982): names should be resolved to addresses, not used as addresses. Give the agent a stable name up front, and keep a separate lookup for wherever it happens to be running today.
Every arrival raises two separate questions, and the whole module hangs on telling them apart.
The name. A stable name that survives the agent moving anywhere. The Gateway looks it up on its roster.
The proof. A pass the Gateway can check on the spot: proof that the sender really is that name, and may act.
Both are settled at the Gateway (Module 2's bouncer), before anything touches the books.
An agent's name identifies the agent, never the machine it happens to be running on, so moving it cannot break the record. A passport names you, not your house: you can move cities, switch jobs, cross borders, and the name on the passport keeps identifying you. An agent name works the same way, and it has three parts: who vouches for the agent, what it is for, and which exact one it is.
Take one full agent name and pull it into its three parts.
Who vouches for it: anthropic.com, the company that stands behind it and the only one that can issue a pass for it.
What it does: assistant/chat, its capability path.
Which exact one: llm_chat_01h455…, a serial number, unique to this one agent.
let uri = AgentUri::parse(
"agent://anthropic.com/assistant/chat/llm_chat_01h455vb4pex5vsknk084sn02q"
).unwrap();
assert_eq!(uri.trust_root().host_str(), "anthropic.com");
assert_eq!(uri.capability_path().as_str(), "assistant/chat");
assert_eq!(uri.agent_id().prefix().as_str(), "llm_chat");Who stands behind this agent, and answers for it
What the agent is for
Which exact one, down to the serial number
Move the agent from one data center to another and its name does not change. A separate directory simply updates where it is running now. Your passport name survives a move; your phone number survives switching carriers.
A valid name is not enough: the name also has to be one you already know. A bouncer with a guest list does two things, reads the name off the ID and checks it against the list, and the Gateway does exactly that. A garbled name gets nowhere. A well-formed name that is not on your roster gets nowhere either. In both cases nothing has touched the books.
First, is this even a real agent:// name?
If it cannot be read, stop here: InvalidAgentUri. A garbled name gets nowhere.
Now look the name up in the registry (the roster).
The lookup returns which party the entry will land against, and which roles the agent carries.
If the name is not on the roster, stop: UnknownAgent. A stranger with a well-formed name is still turned away.
// Check #1: identity.
let uri = AgentUri::parse(&envelope.agent).map_err(|_| GatewayError::InvalidAgentUri {
uri: envelope.agent.clone(),
})?;
let (party, roles, bound_mandate) = {
let (party, roles, mandate) =
self.registry
.resolve(&uri)
.ok_or_else(|| GatewayError::UnknownAgent {
uri: uri.canonical(),
})?;
(party.clone(), roles.clone(), mandate.cloned())
};uri.canonical() is the name written in one canonical form, so a formatting quirk can never make a lookup miss. Bad name or unknown name, the request is turned away before a single balance moves.
Being on the roster says the name is known. It does not prove the sender is that agent. The pass does that: it carries a seal only the company vouching for the agent could have made, and any officer can check the seal on the spot, without phoning the issuing country.
The company that vouches for the agent issues a pass saying "this agent is ours, and it may do X."
Only the company that vouches for the agent can make it. Tamper with the pass and the seal breaks.
Anyone can confirm the pass is genuine on the spot, with no call back to the issuer.
So the pass is asked three questions, and it has to answer all three: is it genuine and made out to this exact agent, is it still in date, and does it actually cover what the agent is trying to do? Fail any one and the request is turned away at the door.
Is the pass genuine, and does it name THIS agent? Check the seal against what the vouching company published, and confirm the pass is made out to this exact name.
Is it still in date? Measured against a time handed in from outside, never a clock this part reads for itself.
Does it cover this action? The permissions written on the pass must cover what the agent is trying to do — and they are written inside the agent's own name, so a pass can narrow what this agent may do but never borrow authority from outside it.
// Check #2 — authority (verification is clock-free; expiry uses wall_now).
let claims = self.verifier.verify_for_uri(&envelope.attestation, &uri)?;
check_expiration(claims.exp, wall_now)?;
capability::ensure_covered(
&claims.capabilities,
uri.capability_path(),
&envelope.action.action,
)?;A pass from acme.com can be checked by anyone and issued only by Acme, the way a French passport is valid worldwide and issued only by France. Nothing about that check depends on the agent, or on the company that vouched for it, being reachable at the moment you make it.
A pass clears the door only if all four of these hold at the same time, and each one exists to defeat a particular cheat. A border officer does not wave a passport through just because it carries a seal.
trusted + owns namespace — Sealed by a company Axorum trusts, and one that owns this agent's namespace. A seal for acme.com cannot vouch for evil.com.
this exact agent — Made out to this name. You cannot borrow someone else's pass.
not expired — Still in date, measured against a clock handed in from outside. The part that decides reads no clock of its own.
grants this action — Coverage: the pass must actually permit what is being attempted.
The first hurdle has a subtle edge to it. The company that sealed the pass must be the same one named at the front of the agent's name:
Is the company that sealed the pass (iss, the issuer) the same one named at the front of the agent's name?
If not, refuse it: IssuerNamespaceMismatch.
Acme may vouch for agent://acme.com/… agents only, never for a rival's.
The passport rule: France issues French passports, not German ones.
if claims.trust_root() != Some(claims.iss.as_str()) {
return Err(AttestationError::IssuerNamespaceMismatch {
issuer: claims.iss.clone(),
uri_trust_root: claims.trust_root().unwrap_or_default().to_string(),
});
}Every step can turn the request away, and the Vault at the end only ever meets requests that cleared all of them.
Between the pass and the books, the Gateway also gathers any standing authority a principal handed down: delegation. It is how "my boss said I could" becomes something the machine can check.
When the name or the pass fails, the request is turned away at the Gateway and nothing is written to the books. It is not a verdict from the rulebook, because the request never reached the rulebook: no entry, no balance moved, nothing to explain at the close.
Notice where the messy part lives. The expiry check compares two times that are both handed in from outside, which is the rule Module 2 set: the part that decides never reads a clock of its own.
The check is given both times: when the pass expires (exp) and what time it is now (now).
If now is still before the expiry, the pass is in date.
Otherwise it has expired, and it is refused.
It never reads a clock itself. The clock stays outside, so the same question always gets the same answer.
pub fn check_expiration(exp: DateTime<Utc>, now: DateTime<Utc>) -> Result<(), AttestationError> {
if now < exp {
Ok(())
} else {
Err(AttestationError::TokenExpired {
expired_at: exp.to_rfc3339(),
})
}
}The record keeps the agent's name, the company that vouched for it, the pass it presented with its seal, the authority that pass granted, and the rule that was in force at the moment. Answering "who approved this" needs none of them to still be running.
Each question drops you into a new situation. Reason it out the way the Gateway would.
The Gateway now knows who the agent is and that it may act. The next question is what the rules allow: the rulebook's short rule language of "you may / you must not / you must." That is Module 4.
Green light, red light, yield sign: how your spending policy becomes rules the ledger applies to every payment, and reads the same way every time.
Your spending policy stops being a document nobody enforces. It becomes rules the ledger applies at the instant of every payment, and you can read those rules yourself. The ceiling from Module 1, no purchase over $1,000.00, is one of them.
You write the rule the way you would say it out loud, and it is translated into a short rule language: a handful of words, chosen so that a rule can only be read one way.
Think of driving. Every rule on the road is one of exactly three signs, and your spending policy works the same way: you may, you must not, you must.
You may. A green light: the payment is allowed to happen. In the rulebook: permitted.
You must not. A red light: the payment is refused, and the reason is on the record. In the rulebook: forbidden.
You must, by a deadline. A yield sign: a duty comes due, and the ledger tracks it until it is discharged. In the rulebook: obligated.
These three words are what turn your written policy into something enforced. "Make sure receipts get filed" is a line in a manual that nobody applies to a payment. "A buyer is obligated to file a receipt, at a higher priority than the permission to buy" means exactly one thing, and the ledger applies it to every purchase.
You can read your own rulebook, start to finish. Two rules decide whether a purchase is allowed at all. The first, permit_purchase: anyone in the buyer role may make a purchase, at a weak priority of 1. The second, forbid_over_ceiling: a buyer is forbidden to purchase when the amount is over $1,000.00, at a strong priority of 10. Amounts are held in whole cents, so nothing rounds: 100000 is $1,000.00.
That is Axorum's demo policy as it stands, written in a short rule language. The text you can read is the text the ledger applies: there is no second version behind it.
Rule one: the permission to buy.
Anyone in the buyer role may make a purchase: a green light.
It carries a weak priority of 1.
That is rule one, in full.
Rule two: the ceiling.
A buyer is forbidden to purchase: a red light,
but only when the amount is over $1,000.00. (Amounts are held in whole cents, so nothing rounds: 100000 is $1,000.00.)
It carries a strong priority of 10.
That is rule two, in full.
rule permit_purchase {
permitted any_in_role(buyer) do purchase
priority 1
}
rule forbid_over_ceiling {
forbidden any_in_role(buyer) do purchase
when { ( param(amount) > amount(100000, USD) ) }
priority 10
}The higher priority wins, and only where it applies. Both rules cover the same action, a purchase, so the ceiling's priority of 10 beats the permission's priority of 1. But the ceiling only overrides the permission when the amount is actually over $1,000.00.
The ceiling rule's when condition does not hold ($500.00 is not over $1,000.00), so only the permission applies. The payment is recorded as Approved.
Now both rules apply. Priority 10 beats priority 1, so the ceiling wins. The payment is Refused, nothing crosses the books, and the reason is on the record.
An exception that beats the default is how your own spending policy already reads: generally allowed, except above a limit. The specific rule beats the general one, and only in the cases it was written to cover.
Drag each word of the rule language onto the road sign it belongs to.
Not every rule stops a payment. Some open a duty that comes due, and the ledger tracks it until it is discharged. The demo policy carries one, receipt_duty: every purchase obliges the buyer to file a receipt within five of the ledger's own steps. The ledger counts its own events, not calendar days, so the same sequence always plays out the same way. Miss the deadline and the rulebook opens a penalty duty in its place, late_penalty. The rule carries a priority of 5.
A green light lets you go and a red light stops you. A yield sign says something else: you must act, and there is a deadline.
Rule three: the receipt duty.
A buyer is obligated to file a receipt: a yield sign.
What starts the clock? Any purchase. That is the trigger.
The deadline: within 5 of the ledger's own steps.
If the deadline passes with no receipt, late_penalty opens a new duty to pay.
It carries a priority of 5.
That is the obligation, in full.
rule receipt_duty {
obligated any_in_role(buyer) do file_receipt
trigger purchase
within(5)
on_violation late_penalty
priority 5
}An obligation is a promise the ledger remembers. In the next module, the Vault collects on this duty, and if the receipt never lands, the broken promise becomes a new penalty duty. The whole rulebook is built from a handful of words: permitted, forbidden, obligated, trigger, within, priority, and nothing else.
Each question is a payment arriving at the checkpoint. Read the rulebook the way the ledger reads it, and decide what gets recorded.
You have read the rulebook. Next, the Vault enforces it. There are two sides to every payment, the arithmetic of the books and the authority, and both are checked before a single balance moves: if either side fails, nothing crosses the books. That same checkpoint comes to collect on the receipt duty.
Every payment passes five inspections in one step, or nothing is recorded.
Your own accounting rules and your own spending policy are checked together, before any money moves. Five inspectors stand at the checkpoint you met in Module 1, and every one of them has to stamp the payment.
Your transaction only crosses if all five inspectors stamp it OK. If any single one refuses, the payment is turned away and nothing is written down. The Vault is a scrupulous bookkeeper running five checks in a single instant.
Debits equal credits, in every currency on the entry.
The payment was allowed under the rules your team wrote.
The running total spent stays under the budget granted.
You never count more as earned than customers actually prepaid.
You never bill past the meter, and never meter past what was delivered.
There are two sides to every payment. Four of these laws are the arithmetic of the books: the entry balances, the budget holds, revenue matches what was earned, the bill matches the meter. The fifth is the authority: allowed or forbidden under the rules your team wrote. Every one of them is checked before a single balance moves, and new laws join the same single check as your books need them.
Debits equal credits, in every currency, or nothing posts. The first inspector keeps a balance scale. For each currency, the total debits (money out) must exactly equal the total credits (money in). This is conservation: money is never created or destroyed, it only moves.
Debits out $1,000.00 = Credits in $1,000.00 The scale is level. Inspector 1 stamps it OK.
The ledger walks every currency on the entry and compares the two totals. If one currency is off by a single cent, nothing posts, and the refusal names the currency that is off and both of its totals.
Take each currency on the entry, one scale at a time.
If the money going out does not exactly match the money coming in...
...refuse the entry, and put the reason on the record...
...naming the exact currency that is off...
...and reporting both totals, so your team can see the mismatch.
Only when every currency balances does the entry post.
for (currency, (debits, credits)) in totals {
if debits != credits {
return Err(SubstrateError::ConservationViolation {
currency,
debits,
credits,
});
}
}
Ok(())Dollars are weighed against dollars, euros against euros. A payment in two currencies has to balance on both scales. That is the trap in the first quiz question.
A payment can balance perfectly and still be refused. The second inspector does not look at the money at all: she asks whether the payment was allowed under the rules your team wrote, and the Judge reads the rulebook in force to return a verdict. These are the same rules you wrote in Module 4, now applied at the instant of the payment.
The rules allow it. The payment posts.
Allowed, and it leaves a duty to discharge later.
The rules forbid it. Nothing crosses the books, and the reason is on the record.
Refused is the only outcome that stops a payment. A forbidden payment that a person with the authority overrode does still post to your books, recorded as Approved by exception, with the rule it broke and the person who overrode it named on the record.
Read this payment against the rulebook in force right now, not the one from last quarter.
If the rules cannot be read at all, stop: nothing is recorded.
The payment posts on every outcome except a Refused.
let decision = self.evaluate_active(&active_id, &proposal)?;
let posted = decision.verdict != Verdict::ForbiddenRejected;Law 1 asks whether the numbers add up. Law 2 asks a different question: was anyone allowed to do this? A perfectly balanced payment to a vendor your policy forbids is refused here, with the reason on the record. A refusal is as much a record as a payment.
What gets checked is the running total, not the size of the latest charge. The third inspector holds a mandate: a prepaid debit card loaded with a budget. No matter how many small swipes an agent makes, the running total can never pass the card's ceiling. The budget is cumulative: it watches the sum of everything spent, not just the size of the latest charge.
Mandate ceiling $5,000.00 Already spent: $4,980.00 Left: $20.00 A new $40.00 swipe would push the total to $5,020.00, over the ceiling. Refused.
The ledger adds this charge to everything already spent against the mandate. If the new running total would cross the ceiling, the payment is refused before the money moves, and the refusal records the ceiling, the amount already spent, and the amount attempted.
Would this charge be admitted? Start with the total already spent against the mandate...
...and add this new charge on top of it...
...(refusing outright if the figures are too large to add at all).
If that new running total climbs past the ceiling...
...refuse, and report the ceiling, what was already spent, and what was attempted.
The sum of everything, across every payment, can never exceed what was authorized.
pub fn admits(&self, delta: i128) -> Result<(), SubstrateError> {
let next = self
.executed
.checked_add(delta)
.ok_or(SubstrateError::Overflow)?;
if next > self.ceiling {
return Err(SubstrateError::AuthorizationExceeded {
mandate: Box::new(self.id.clone()),
ceiling: self.ceiling,
executed: self.executed,
attempted: next,
});
}A rule that says "no single purchase over $1,000.00" is defeated by fifty small purchases. A cumulative ceiling is not: it stops the running total the instant it would cross the line. This is the control to require before anyone hands an agent a budget, and it is the difference between preventing the overspend and reading about it at the close.
Recognized revenue can never exceed what was actually prepaid, and the ledger enforces that itself. The fourth inspector guards deferred revenue: money taken in advance but not yet earned, like a gift card you sold but the customer has not redeemed. You can only recognize (count as earned) as much as was actually prepaid.
Deferred (prepaid) $500.00 gift card sold Recognized (earned) $500.00 fully redeemed Trying to recognize $50.00 more would book $550.00 as earned against only $500.00 prepaid. Refused.
If an entry would book more as earned than was ever prepaid, it is refused, and the refusal records which deferral it touched, the prepaid total, the earned total, and the amount attempted. Your quarter cannot be overstated by an agent, or by anyone else posting through the ledger.
If, after this entry, the earned amount would exceed the prepaid amount...
...and the entry is booking more as earned, not less...
...refuse the whole entry...
...naming which deferral is affected...
...and reporting the prepaid total, the earned total, and what was attempted.
The matching principle, enforced by the ledger itself, at the instant of the entry.
if next_deferred < next_recognized {
if recognize_delta > 0 {
return Err(SubstrateError::RecognitionExceedsDeferred {
deferral: Box::new(self.id.clone()),
deferred: next_deferred,
recognized: self.recognized,
attempted: recognize_delta,
});
}Nobody can be billed for service that was never delivered, and the ledger enforces that itself. The fifth inspector reads a meter. Your water company may bill you only for what the meter read, and the meter may only read what actually came through the pipe. Axorum keeps that same discipline for every metered service an agent buys or sells, whether it is API calls, compute time, or seats.
The ledger tracks three running totals for each metered service: what was consumed (the resource the agent actually used), what was metered (the measurement of that consumption), and what was billed (the money charged for that measurement). The order can never run backward. Billing may never pass metering, and metering may never pass consumption.
Metered service: api-tokens Consumed (actually delivered)1,000 Metered (measured)1,000 Billed (charged)900 A charge for 300 more would take billing to 1,200 against only 1,000 metered. Refused.
Metering more than was consumed is phantom usage: a charge for delivery that never happened, and the fraud this law exists to prevent. Billing more than was metered is a charge with no meter reading behind it, which is what the revenue recognition rules forbid: you may not bill beyond the measured service you actually delivered.
The three totals are not required to match at every moment. Service delivered but not yet measured, and service measured but not yet billed, are ordinary and lawful: that is work in flight, and reconciliation closes the gap. What is checked at the instant of every transaction is the order, and that none of the three totals ever falls below zero. Corrections run the same gauntlet: a metering correction or a credit memo is admitted only if the whole chain still holds afterward, exactly as a refund is.
If this transaction would measure more than was actually delivered...
...refuse it, naming the metered service...
...and reporting what was delivered, what was already measured, and what was attempted.
Then ask the same question of the bill: would it climb past the meter?
If it would, refuse, and report the metered total, the amount already billed, and the amount attempted.
No phantom usage, and no charge without a meter reading behind it.
if next_metered > next_consumed {
return Err(SubstrateError::MeteringExceedsConsumed {
usage: Box::new(self.id.clone()),
consumed: next_consumed,
metered: self.metered,
attempted: next_metered,
});
}
if next_billed > next_metered {
return Err(SubstrateError::BillingExceedsMetered {
usage: Box::new(self.id.clone()),
metered: next_metered,
billed: self.billed,
attempted: next_billed,
});
}The charge above comes back refused, in a sentence your team can hand to a customer or an auditor as it stands: "billing exceeds metered usage for usage balance 'api-tokens': attempted billed total 1200 exceeds metered total 1000 (already billed 900; 100 metered but not yet billed)." Nothing posts. The customer is not billed, your revenue is not overstated by a single unit that was never delivered, and the reason is on the record.
Nothing posts unless everything holds. The five inspectors do not stand in separate rooms; they check the same payment in one pass. All five laws are checked in full first, and only then does anything change. Step through the checkpoint below.
A commit is all or nothing, in one step. Every check runs before a single balance moves. You will never find the books half posted, with the money gone and the receipt never filed. If any law fails, nothing crosses the books, and the refusal is recorded with its reason.
Some payments that pass all five laws leave a duty behind, and the ledger collects on it by itself. Passing the checkpoint creates an obligation: file a receipt, deliver by a date. The Vault files it and comes back to collect, with no human chasing it.
A purchase lands at tick 1 and passes all five laws.
The rule attaches a duty: file a receipt, due at tick 6.
Advancing the clock runs a sweep that scans every open duty for one whose deadline just passed.
Receipt filed in time? The duty is discharged and the obligation closes. Missed? It is breached, and a penalty duty is created automatically.
Six scenarios. Each one asks you to apply the five laws to a situation you have not seen yet.
Your books cannot sit on one machine that can die. These same five laws are about to run on several copies of the books, which must agree on the order of events before anything counts. Because the same question always gets the same answer here, those copies stay identical, to the last cent.
Your books cannot sit on one machine that can die, and no copy of them can quietly disagree with the others.
Machines die: a power supply, a bad disk, a spilled coffee. If your books live on that one machine, they die with it. So the Vault, the books you met in Module 5, runs as several copies, each on its own machine, and each copy holds the whole book rather than a slice of it.
Copies are only worth having if they stay identical. If one of them can quietly hold a different version of the truth, your books are not safer, they are arguable. That is the work of this module: keeping books that live on more than one machine in step, payment after payment.
Every musician plays the same notes in the same order, so they stay together without watching each other. One leader calls the cadence. If that leader stumbles, the next player in a fixed order picks it up: no vote, no argument. That is how several copies of the books stay identical.
Each machine holds a full copy of the books, and the copies stay together because of the property Module 2 built in: the same question always gets the same answer.
Every copy is handed the identical list of events to apply.
They apply those events in the same order, never one musician a beat ahead.
The same question always gets the same answer, so the same events in the same order must land on the same books, to the last cent.
Your engineers can read the same idea in the code that keeps the copies in step:
This is the band's rulebook: a published, decades-old method for keeping several copies in agreement.
It attaches to the ledger from Module 5 and lets it be copied across machines.
Every copy holds the whole book, not a slice of it.
What the copies agree on is the order the events arrived in, and nothing else.
The same question always gets the same answer, so the same order lands on the same books.
//! A **pure, deterministic Viewstamped Replication (Revisited)** replica state
//! machine, plus the binding that makes an `axorum_substrate::Ledger` replicated.
//! N replicas each hold a ledger; consensus sequences the op stream; every
//! replica applies the same ops in the same order — and the substrate's
//! determinism (proven by the VOPR) does the rest.A payment does not count because one machine says so. It counts when a majority of the copies have agreed where it sits in the order. The copies that must agree are called the Committee. One of them, the copy calling the order, proposes the position of each payment, and the rest follow and confirm.
Watch them post one real payment: #42, the coffee purchase. The lead copy waits for a majority to confirm before anything is declared final.
Three moves, and a payment is real only once the second one has happened:
The lead copy announces the next payment and its position in the order: log this as number 42.
Each other copy replies that it has logged it. A majority of those confirmations is what makes payment #42 real.
Once a majority has agreed, every copy applies it. All of them post the same payment, in the same place in the order.
If the copy calling the order fails, the other copies keep posting payments without it, and nothing a majority had already agreed on is lost. A band does not hold a vote when the leader stumbles: the next player in line picks up the cadence. The copies do the same, counting off a numbered era of leadership.
There is no vote and no argument, because every copy already knows who is next. Your engineers can read why this method was chosen over the more common one:
The method covers four situations, including the one that matters at close: the lead copy goes dark and another has to take over.
It was chosen over the more common method because it can recover after a crash without reading anything back off the disk.
That suits books that are held in memory and never have to trust what was saved.
Leadership is a fixed rotation, next in line, not a random election, so the handover is predictable and two copies never end up calling the order at once.
//! normal operation, view change, recovery, and state transfer. Chosen over Raft
//! for its no-disk recovery (safety never depends on persisted state — a perfect
//! match for a pure in-memory core) and deterministic round-robin leadership
//! (`primary = view mod N`, no randomised election timers).Agreement keeps the copies matching while the machines are running. What carries your books through a data centre losing power is the disk: a payment is written to disk before anyone is told it went through, so a payment you were told had posted is still there when the lights come back on.
Each payment is written down before it is applied, so a machine that dies halfway through finishes the job when it comes back.
A payment is physically written to disk before anyone is told it went through. If the power fails one millisecond later, the payment is still there when the machine comes back.
Forcing every payment onto the disk on its own would be slow under load. So a burst of payments is bundled and pushed down together in a single write: one write per burst, not one per payment. Nothing is reported as done until that write has finished, so the guarantee does not change:
The agreement between the copies is untouched: it still handles one payment at a time.
What gets bundled is the writing to disk, a whole burst of payments at once.
Each payment's note is queued, not pushed to the disk on its own.
Then one write forces the entire burst onto the disk.
Only then is anyone told the payments went through. Nothing is released early.
A burst of payments costs about one disk write instead of one each, and the guarantee is unchanged.
//! The pure consensus core still processes one op per transition — no protocol
//! change. **Durability**, however, is batchable: the durable binding exposes
//! `propose_batch` and `on_messages`, which run a run of `propose` / `on_message`
//! transitions each with a *deferred* write-ahead, then a **single** `fsync`
//! makes the whole batch durable before any effect is released (J-WA,
//! amortized). ... a burst of N ops costs ~1 fsync, not N.The copies never have to agree on the answer. They only have to agree on the order the payments arrived in. Because the same question always gets the same answer, the books then match on their own, to the last cent.
Three situations from a close. Reason from what the band taught you.
Your books now survive a dead machine and a power cut. In Module 7, real money moves across a payment rail, and it cannot move until the payment has committed here: all five laws checked, a majority of the copies agreed, and the payment on disk. Nothing is released onto the rail before that.
The money cannot move without the ledger's approval. Not as a policy, not as a discipline, but as a locked gate with no way around it.
No money leaves unless the ledger has already approved the payment. Picture a subway turnstile: it stays locked by default, it unlocks only when a valid ticket is stamped, and there is physically no lever to shove it open by hand.
The rail moves money only when the ledger stamps the payment as approved. No stamp, no unlock. The gate fails closed.
A locked turnstile. The ledger holds the only ticket stamp. A payment the ledger has not approved cannot pass, and there is no manual override.
Keep the two apart, because this module is the easiest place to confuse them. The rail moves the money. Axorum decides whether it may. Axorum does not move money, hold funds, or settle anything: it sits on the books, underneath whichever rail is already in use, and it withholds the approval when a payment breaks a law. Everything that follows is about the approval, never the custody of a single dollar.
The agent no longer talks to the merchant directly. A gate sits between them, and it holds the only ticket stamp.
This module uses x402 as the worked example because it is the one you can watch end to end today. The gate is not tied to it. AP2 and ACP are the other standards agents are settling on, and they attach at the same seam: the payment is held, the ledger rules on it, and only an approved payment goes out. Swapping the rail changes the paperwork at the edge, never the laws in the middle.
The gate never signs and pays on demand. When the merchant asks to be paid first, the gate puts that payment through Axorum's five laws before anything is signed. Step through it.
The agent points at the gate instead of the merchant, so every payment it wants to make goes through the gate first. The gate is a stand-in for the merchant: it holds the request, sends the payment to the ledger, and signs only if the ledger approves. Nothing is signed on the way in.
The buying agent points at the gate, not at the merchant.
When the merchant asks to be paid, the gate catches the request.
Before anything is signed, the gate puts that payment through the ledger's laws.
There are two sides to it: the arithmetic of the books, and the authority to spend. Both are checked in one step, and nothing posts unless every law holds. (The comment above is written in the engineers' older wording. All five laws run today, and a simple payment like the coffee purchase in Module 1 rests on them.)
The payment posts as approved, or no authorization to pay is ever created.
//! instead of at the merchant; the sidecar reverse-proxies the request to one
//! configured upstream. When the merchant answers with an [x402] `402` demand,
//! the sidecar **gates that payment through Axorum's dual-law ledger before
//! anything is signed**: the intent must commit as permitted — conservation and
//! policy, atomically — or no payment authorization is ever created.To sign a payment you need a permit, and nobody can make one. The only way to get a permit is to ask the ledger through a single door, and it hands one back only when the payment posted to the books and the rules allowed it. That is a rule the system cannot break, not a step someone has to remember to run.
A refusal gets no signature. Neither does an approval by exception: if a person with the authority overrides the rulebook and pushes a forbidden payment through, it is recorded in your books as Approved by exception, and the gate still will not sign it onto the rail. Posting to your books and moving money on the rail are two different events, and the gate holds the second one.
Signing a payment requires a permit, and no part of the system can build one.
The one way to get a permit is to ask the ledger, through a single door.
It hands one back only when the payment posted to the books and the rules allowed it.
A refusal gets no permit. Neither does a forbidden payment that a person overrode: the override is recorded in your books, and it still is not signed.
No permit, no signature, and the merchant sees nothing. The gate fails closed.
//! The **no-bypass invariant** lives here too. A payment can only be signed with
//! a `Permit`, and a `Permit` has no public constructor: the sole way to obtain
//! one is `authorize_payment`, which yields `Ok` **only** when the ledger both
//! posted the transaction and returned a clean `permitted` verdict. Every other
//! outcome — a forbidden rejection, or a commit carrying conditions this gate
//! will not auto-sign — returns a `LedgerRefusal`, and the upstream merchant
//! never sees a signature. The gate fails closed.This is not a control someone has to remember to run. There is no flag, no admin switch, and no path around it, because the part that signs a payment cannot run at all without the ledger's approval. The gate stays locked by default.
Axorum sits under the rails, on each participant's own books. Click any block to see what it does. You have met all of them across this course.
Card payments as well? A second gate goes at the boundary. The books, the five laws, and the copies that must agree stay exactly as they are.
New spending policy? Edit the Rulebook. The Judge applies whatever rules are in force at the instant of the payment, with no surgery on the system itself.
Need more resilience? Add machines to the Committee. Because the same question always gets the same answer, every copy of the books stays identical, to the last cent.
Three situations. Each one asks you to use what you now know about the turnstile, not to recite it.
Remember the coffee purchase from Module 1, the one we used to ask "who approved this?" With the turnstile in place, that same purchase can be real money on the x402 rail, settled by an outside facilitator: the agent asks, the five laws are checked in one step, and only an approved, balanced, in-budget payment is ever signed.
The ledger now checks every action, keeps copies of the books that agree with each other, and gates real money so that it fails closed. A company adopting it always asks one last question: how does all of this land in the books I already keep? That is the finale: Module 8, Books That Agree.
How the ledger hands its records to the books you already keep, and proves the two never drift apart.
Axorum's records flow into the general ledger you already run, and reconciliation then checks the two against each other, line by line. That is the last thing a finance team asks about before adopting it: how does all of this land in the books I already keep?
Every business already runs a general ledger inside an accounting system like NetSuite, SAP, or QuickBooks, an ERP. Axorum does not replace it. It keeps the one thing that ledger has no column for: whether the spending was allowed, and on whose authority.
Axorum records what was allowed: who was permitted to spend, under whose authority, and which duties came due. Your general ledger stays the book of record for what was posted. Reconciliation lays the two side by side and proves they agree, or names the line that does not.
Two scorekeepers sit at opposite ends of the court, each keeping their own sheet. One tracks what every player was cleared to do; the other tracks what actually went on the board. Neither trusts the other blindly, and at the end you lay the two sheets side by side, line by line.
Records what was allowed: who was permitted, under whose authority, which duties came due. The half your books do not keep.
Records what was posted: the actual money on the books. The official financial half.
That comparison, line by line, is reconciliation. It is what proves the two sheets agree.
Every committed transaction leaves the ledger as one sealed export record, and it carries five things: the journal entry, the outcome, which rule decided and under whose authority, every duty the transaction opened, and a plain English summary of the whole event.
It is vendor-neutral and carries a version stamp, so any system, now or years later, knows exactly how to read it.
One committed transaction, packaged into one envelope an auditor could read cold.
The entries: the journal entry, every row of it, and it has to balance.
The outcome: was the spending allowed, and did it leave a duty behind?
Who decided: which rule applied, and under whose authority.
The duties: each obligation opened, and what became of it.
The summary: the whole event in one plain English sentence.
pub struct ExportRecord {
pub identity: RecordIdentity, // (5) txn id, commit tick, policy hash, schema version
pub verdict: VerdictRecord, // (2) the normative verdict + its English sentence
pub legs: Vec<ConservationLeg>, // (1) the multi-row journal entry
pub provenance: Provenance, // (3) which rule decided, under whose authority
pub obligations: Vec<ObligationLifecycle>, // (4) each duty created + its fate
pub narrative: String, // the full compliance-English narration
}Your general ledger records the money. It has no column for whether the action was allowed in the first place. Axorum keeps that answer, and every committed transaction carries exactly one of five outcomes: Approved, Refused, Approved by exception (a person with the authority overrode the rule, and is named), Obligation opened, or Obligation closed.
The five outcomes your general ledger has no column for.
Approved: allowed under the rules in force, and it posts clean.
Refused: the rules forbid it, nothing crosses the books, and the reason is on the record. Or approved by exception, where a person with the authority overrode the rule and the override is recorded against their name.
An obligation opened: the transaction leaves a duty still owed...
...or an obligation closed: the duty is discharged. This is the record a plain ledger never keeps.
pub enum ExportVerdict {
Permitted,
ForbiddenRejected,
ForbiddenRecorded,
ObligatedPending,
ObligatedFulfilled,
}The record your accounting system receives and the record your data team analyzes are the same record. Nothing is re-keyed between them, and no amount is ever rounded on the way, because every figure is carried as exact whole cents.
The same record ships two ways, because rows and columns answer different questions. One record at a time, whole, for the connector that posts a single transaction into your books. Or stacked as columns, so a warehouse can total ten million of them at once. Ask what agents spent last quarter against those ten million records and you are reading the same figures your auditor would.
One published column layout, carrying the same version stamp as the single-record form.
One step restacks a pile of envelopes into columns...
...one writes them out where a warehouse can read them directly...
...and one reads them back. The round trip changes nothing, and a test holds it to that.
The amounts stay exact whole numbers of cents, natively.
The single-record form has to write large amounts as careful text to avoid a figure that quietly rounds; here the exact figure is a column in its own right.
pub fn schema() -> SchemaRef; // the one columnar shape, version stamped in
pub fn record_batch(records) -> RecordBatch; // envelopes restacked as columns
pub fn write_parquet(records, writer); // ...and onto disk, warehouse-ready
pub fn read_parquet(reader) -> Vec<ExportRecord>; // and back out, unchanged
// the money column, exactly:
req("amount_minor", DataType::Decimal128(38, 0))The promise is the record, not the file it travels in, and tests hold both forms to it. Every field in the column layout matches the single-record form one for one, and every outcome, direction, and duty word is checked, word for word, against the same vocabulary; rename a field or retype a column and the build fails. It is this module's thesis in miniature: two independent copies of the same story, provably in agreement.
The envelopes reach the other scorekeeper through a resumable feed: a numbered, append-only logbook that never gets re-paginated. Your connector reads from its bookmark, takes what is new, posts it into your books, and saves a new bookmark for next time.
Each reader keeps a bookmark: give me everything since where I left off.
The reader never writes back. It cannot change the ledger, only observe it.
It will never skip a record, though after a crash it may hand you the same one twice; your connector recognizes the repeat by its transaction number.
Give me everything since where I left off. That is the whole request.
It takes the ledger's committed records...
...and starts a fresh, empty batch to fill.
It walks only the new tail, from your bookmark onward. Nothing already delivered is read again.
For each new transaction...
...it writes out the plain English account of what happened and why...
...and packs the finished envelope into the batch.
It only ever reads and packs. It never writes back, so a connector cannot alter your ledger.
It hands back the new records plus an advanced bookmark. Save it, and a restart resumes exactly here.
pub fn read_from(ledger: &Ledger, ctx: &ReportContext, cursor: Cursor) -> Result<Batch, CdcError> {
let records = ledger.records();
let mut projected = Vec::new();
for record in &records[cursor.pos()..] {
if let LedgerRecord::Transaction { id, .. } = record {
let explanation = explain(ledger, ctx, id)?;
projected.push(export(&explanation));
}
}
Ok(Batch { records: projected, cursor: Cursor::at(records.len()) })
}Reconciliation gives every line one of five outcomes, and one of them is a control you do not have today: it names anything that reached your books with no authorization behind it. The envelopes have been posted into your general ledger; reconciliation now lays Axorum's record of what was authorized against your ledger's record of what was posted.
Line up the two sheets and each pairing lands in exactly one bucket. Four are routine. One is a red flag.
Both sheets agree: same transaction, same amount. Nothing to do.
Same transaction, but the money is off. Axorum says one figure, the GL posted another.
Right amount, wrong period. The two sheets disagree on when it landed.
THE RED FLAG. The GL posted something Axorum never authorized: a payment nobody approved reached your books.
Axorum approved it, but the GL never posted it. The books are missing a line.
Every reconciled line becomes one of these five outcomes.
Matched: it agrees. Move on.
Amount break: the two figures do not match, and the line keeps both numbers.
Timing break: same money, different period.
Posted-not-authorized: the red flag, and it carries the reason.
Authorized-not-posted: Axorum approved it, the books never recorded it.
pub enum ReconOutcome {
Matched { transaction_id, external_ref, amount_rendered },
AmountBreak { transaction_id, external_ref, axorum_amount_minor, posted_amount_minor },
TimingBreak { transaction_id, external_ref, amount_rendered, axorum_period, posted_period },
PostedNotAuthorized { external_ref, amount_rendered, reason },
AuthorizedNotPosted { transaction_id, amount_rendered },
}The red flag is not a cryptic error code. It comes out as a plain English sentence naming the amount and the exact rule that was violated, ready to drop straight into an auditor's report.
When your books posted something a rule forbade...
...the break names the amount and the exact rule that was broken.
A real figure, a real rule name, not a mystery code.
The break is already a sentence your auditor can read.
PostedNotAuthorizedReason::AxorumForbade { deciding_rule: Some(rule) } => format!(
"Governance exception: {posting} recorded {}, but Axorum forbade this action under the rule \u{2018}{rule}\u{2019}.",
entry.amount_rendered,
),Posted but never authorized is the line every finance team with spending agents should watch. A payment that reached the books with no authorization behind it is the exposure you cannot see today, and this is the control that names it.
Your approved vendor list and your closed months become rules the ledger enforces at the instant of a payment. Axorum imports your master data, the chart of accounts, your approved counterparties, and which fiscal periods are open, and keeps its own copy of each list. The rulebook from Module 4 then names those lists directly.
A reference set is a named list Axorum keeps a copy of.
Only pay vendors on the approved list.
Only post to accounts that exist on the chart.
Only post into a period that is still open.
And here is a real rule: forbidden to post when now is not in the open periods.
The rule consults your imported list at the instant of the transaction, not at month end.
pub enum ReferenceSet {
ApprovedCounterparties, // only pay approved counterparties
GlAccounts, // only post to accounts on the chart
OpenFiscalPeriods, // only post into an open period
}
// the DSL rule it powers:
forbidden … when { ( not now in open fiscal periods ) }If a reference set was never imported, the answer is "no", not "maybe." A rule that asks whether this vendor is approved, against a list that does not exist, fails closed, and the payment is refused. Missing data can never become an accidental yes.
This is Module 4's rulebook, now speaking your accounting system's own language. "Only pay approved vendors" and "only post into an open month" stop being wishes in a policy document and become rules the ledger enforces at the instant money moves.
Eight modules, one sentence each. Read them top to bottom and you have the whole control: an agent that spends and vanishes, stopped at the instant the money moves, with every decision on the record.
An agent that spends and vanishes has to be checked at the instant money moves. Audit it after the fact and there is no one left to ask.
Each part has one job. The part that decides is walled off from the part that deals with the outside world, which is segregation of duties.
A stable identity, plus a tamper-evident digital pass the gateway checks before anything moves.
Your spending policy in a short rule language a machine reads: what an agent may do, must not do, and must do next.
The entry balances, someone authorized it, the budget holds, revenue matches what was earned, the bill matches the meter. All five, or nothing posts.
Your books do not sit on one machine that can die, and no copy of them can quietly disagree with the others.
Policy becomes a gate on real money, and the gate stays locked by default. There is no override switch.
Records flow into the ledger you already keep, and reconciliation proves the two agree, or names the line that does not.
Five situations. Each asks you to use what you now understand about handing records to your books and proving they agree.
You now know what your customers will demand before they let an agent spend inside your product: every payment checked against their own rules before the money moves, every refusal recorded in a sentence their auditor can read, and a reconciliation that names anything posted to their books without authorization. That is a subledger, and it is not the product you set out to build. You embed it, under your own interface, on your customers' books. Bring us one agent workflow your customers are nervous about, and we will show you the record it would leave behind.