System Architecture
Behind the signed receipt sits a map: every contract and off-chain piece, who may call what, and the exact path a session takes from your agent's first signature to the operator's settlement. The receipt is the only thing that ever moves money.
AIOUM Protocol v2 is a decentralized, metered marketplace and runtime for AI agent services. Node operators run a service — an MCP tool server, a resold API, or a model — and are paid per-call micropayments in USDC / USDT / ETH. The ZAI token only secures nodes (slashable stake) and governs the DAO; it is never a payment asset. When your morning-brief agent buys a price feed, the money is a stablecoin, the operator's honesty is their stake, and the two never mix.
The full design record, with the rationale behind every module, is ARCHITECTURE.md. To run the pieces, see Run a node and the off-chain SPEC.
1. Component diagram
Your agent is the box in the top-left corner. Everything else exists so that its signature is worth something to a stranger.
┌──────────────────── OFF-CHAIN ────────────────────┐
│ │
┌──────────┐ signs EIP-712 vouchers ┌────────────────┐ │
│ Agent / │ ────────────────────────► │ Node runtime │ serves call │
│ Client │ ◄──── x402 (HTTP 402) ──── │ (MCP server) │ (tool/API/model)│
│ SDK │ └───────┬────────┘ │
└────┬─────┘ │ submits settle() │
│ signs Permit2 / EIP-3009 auth │ │
│ ┌─────────────────┐ │ ┌────────────┐ │
└──────────►│ Facilitator │ │ │ Subgraph │ │
│ (x402 relayer, │ open*() │ │ (indexer / │ │
│ non-custodial, │───────┐ │ │ discovery) │ │
│ pays gas only) │ │ │ └─────▲──────┘ │
└─────────────────┘ │ │ events │ │
│ │ │ │ │
└────────────────────┼────┼──────────────────┼───────┘
▼ ▼ │
════════════════════════════════════ ON-CHAIN (EVM) ══════════════╪═══════════
│
┌──────────────┐ ┌───────────────────────┐ │
│ AIOUMGovernor │ queue │ TimelockController │ owner = DAO │
│ (token vote) │────────►│ (delay; guardian │────────────────┼──┐
└──────────────┘ │ multisig = proposer) │ onlyOwner │ │ governs
▲ votes └───────────┬────────────┘ on every │ │ every
│ │ owns │ │ privileged
┌────────┴─────┐ ▼ │ │ setter
│ AIOUMToken │ ┌──────────────────────────────────────────┐ │ │
│ ERC20Votes + │ │ CORE CONTRACTS │ │ │
│ Permit/Capped│◄─────┤ │◄┼──┘
│ /Burnable │ stake │ ServiceRegistry ──► StakingManager │ │
└──────────────┘ ZAI │ ▲ ▲ ▲ │ │
│ │ getService │ │ slash │ │
│ ┌────┴──────┐ reward │ │ │ │
│ │ Payment │───notify───┘ │ │ │
┌──────────────┐ │ │ Manager │ │ │ │
│ Identity │ │ │ │──record───► ReputationReg │ │
│ Registry │ │ └────┬──────┘ Settlement ▲ │ │
│ (ERC-8004) │ │ │ markDisputed/ │ penalize │ │
└──────────────┘ │ │ resolveDispute │ │ │
│ ▼ │ │ │
┌──────────────┐ │ ┌──────────────┐ slash+penalty+refund │ │
│ Validation │◄──────┼──│ DisputeMgr │─────────────┘ │ │
│ Registry │ stake │ └──────┬───────┘ │ │
│ (attest) │ weight│ │ arbitrator = ... │ │
└──────────────┘ │ ▼ │ │
│ CommitReveal Arbitrator ─or─► Validation │ │
│ (staked jury) Quorum Arbitrator │ │
└──────────────────────────────────────────┘ │
events ──────────────────────┘
Solid arrows are on-chain calls (or token/stake flows); the off-chain box holds the participants and relayers that drive those calls and consume emitted events. From your side: the agent signs, the facilitator carries the signature to the chain and pays the gas, the node serves and settles, the discovery indexer turns the registry and each node's live catalogue into the marketplace your agent searches, and the subgraph keeps the history.
2. On-chain / off-chain split
The on-chain protocol is the trust layer: it holds escrow, enforces capacity, splits payments, accrues rewards and adjudicates disputes and slashing. The off-chain stack is UX and transport: it never has custody and cannot move funds beyond what a signature already authorizes, which makes it safe to leave running while you are not watching.
| Layer | Component | Role | Source |
|---|---|---|---|
| On-chain | AIOUMToken | Staking + governance asset (ERC20 Votes/Permit/Capped/Burnable). | src/token/AIOUMToken.sol |
| On-chain | AIOUMGovernor + TimelockController | Token-weighted DAO; owns every privileged setter. | src/governance/AIOUMGovernor.sol |
| On-chain | StakingManager | Self-stake, delegation (shares), multi-asset rewards, unbonding, slashing. | src/staking/StakingManager.sol |
| On-chain | ServiceRegistry | On-chain catalogue of services; listing needs the operator's own stake floor, a listing is live at the node floor of total stake. | src/registry/ServiceRegistry.sol |
| On-chain | PaymentManager | Metered sessions, EIP-712 vouchers, ETH/Permit2/EIP-3009 funding, fee split. | src/payments/PaymentManager.sol |
| On-chain | IdentityRegistry | ERC-8004 identity — one portable on-chain identity per address. | src/registry/IdentityRegistry.sol |
| On-chain | EndpointRegistry | Where each operator's node answers — one URL per operator, written by its key; directories read it first. | src/registry/EndpointRegistry.sol |
| On-chain | ReputationRegistry | ERC-8004 reputation — settlement-gated feedback + dispute penalties. | src/registry/ReputationRegistry.sol |
| On-chain | ValidationRegistry | ERC-8004 validation — staked validators attest to result hashes. | src/registry/ValidationRegistry.sol |
| On-chain | DisputeManager | Optimistic challenge → slash + reputation penalty + client refund. | src/disputes/DisputeManager.sol |
| On-chain | ValidationQuorumArbitrator | Stake-weighted validator quorum; drop-in arbitrator for DisputeManager. | src/disputes/ValidationQuorumArbitrator.sol |
| Off-chain | Node runtime | MCP-style server; watches SessionOpened, serves calls against vouchers, calls settle; /services exposes the declared model, maxConcurrent (= 1 + (totalStake − minNodeStake)/tokensPerSession) and live activeSessions. | offchain/src/node/ |
| Off-chain | Router | Weighted-random node selection by stake + reputation + channel headroom, cheaper-preferred, with manual-pin override (link: routing). | private-node executor (offchain/src/private-node/node-select.ts) |
| Off-chain | Private node | User-run executor: runs scheduled (cron) recurring workflows via a budget-capped agent wallet that pays marketplace steps (open session → sign voucher → call → settle) and stops at the budget. | offchain/src/private-node/ |
| Off-chain | Facilitator | Non-custodial x402 relayer; submits open* auths, sponsors gas, never holds funds. | offchain/src/facilitator/server.ts |
| Off-chain | SDK | EIP-712 voucher signing, contract handles, artifacts. | offchain/src/sdk/ |
| Off-chain | Subgraph | Indexes Registered / ServiceRegistered / session & reputation events for history and analytics; the discovery indexer (offchain/src/indexer/) is what an agent's marketplace search reads. | subgraph/ |
The agent runtime behind the app (offchain/src/agents/) sits on the client side of this split. It
holds the agent's budget, opens sessions, signs vouchers and settles. An assistant-mode agent
allowed to find tools on its own gets a free search of the live marketplace during a run; whatever
it finds becomes callable within the budget you set, and where two listings offer the same thing it
may try both and keep the better result. After every settled session it rates the operator
on-chain, 0–100, from the errors and slow answers it met, so the reputation the router weighs comes
from real results.
The AIOUM network is an EVM chain (Base, chain id 8453, per offchain/networks.json); the
local development network (scripts/devstack.sh up) is Anvil (chainId 31337). Payments settle
in stablecoins/ETH.
3. How the contracts relate (and the privileged-caller wiring)
Every privileged function across the protocol is onlyOwner, where owner is the
TimelockController — never an EOA. Beyond that, contracts grant each other single-caller
permissions: only the PaymentManager can credit rewards, only the DisputeManager can slash.
The deployer sets these wires once, then hands ownership of every module to the Timelock, so
from then on only governance can change them. "Who could take my deposit?" Nobody holds a key
that can.
3.1 Call graph (who calls whom)
PaymentManager ──► ServiceRegistry.getService / isAcceptedAsset (validate listing & asset)
PaymentManager ──► StakingManager.isActiveNode / maxConcurrent (enforce capacity)
PaymentManager ──► StakingManager.notifyReward (route delegatorShare; rewarder-only)
PaymentManager ──► ReputationRegistry.recordSettlement (grant one feedback allowance)
ServiceRegistry ─► StakingManager.isOperator / isActiveNode (listing needs the own floor; live at the node floor)
DisputeManager ──► PaymentManager.markDisputed / resolveDispute (freeze / settle the session)
DisputeManager ──► StakingManager.slash (cut stake on upheld; dispute-only)
DisputeManager ──► ReputationRegistry.penalize (record penalty; dispute-only)
CommitRevealArbitrator ──► DisputeManager.resolve (as the configured arbitrator; ValidationQuorumArbitrator is the drop-in alternative)
anyone ──► DisputeManager.resolveStale (force-close after disputeTimeout; refund, no slash)
ValidationRegistry / Arbitrator ──► StakingManager.selfStakeOf (read validator weight)
3.2 The privileged-caller table
| Guarded function | Contract | Only callable by | Set via |
|---|---|---|---|
notifyReward | StakingManager | rewarder (= PaymentManager) | StakingManager.setRewarder(payment) |
slash | StakingManager | disputeManager (= DisputeManager) | StakingManager.setDisputeManager(dispute) |
recordSettlement | ReputationRegistry | paymentManager (= PaymentManager) | ReputationRegistry.setPaymentManager(payment) |
penalize | ReputationRegistry | disputeManager (= DisputeManager) | ReputationRegistry.setDisputeManager(dispute) |
markDisputed, resolveDispute | PaymentManager | disputeManager (= DisputeManager) | PaymentManager.setDisputeManager(dispute) |
resolve | DisputeManager | arbitrator (CommitRevealArbitrator; swappable) | DisputeManager.setArbitrator(a) |
resolveStale | DisputeManager | anyone (only after disputeTimeout) | — (permissionless backstop) |
setServiceSplit | PaymentManager | serviceOperator (the service's own operator) | per-service, set by operator |
setRewardAsset, setParams, setRewarder, setDisputeManager | StakingManager | owner (Timelock) | constructor dao |
setFees, setTreasury, setInactivityTimeout, setDisputeManager | PaymentManager | owner (Timelock) | constructor dao |
setDisputeTimeout, setParams, setArbitrator | DisputeManager | owner (Timelock) | constructor dao |
mint | AIOUMToken | owner (Timelock) | constructor daoOwner |
The invariant that makes the wiring safe: a node position is created only by its operator
(stake()), rewards are credited only after the asset is received (notifyReward is
rewarder-only and accrued ≤ held), and stake is reduced only on an upheld dispute (slash
is disputeManager-only). No public "create"/"credit"/"seize" path exists.
3.3 Wiring sequence (deploy order)
AIOUMToken(treasury, timelock, initialSupply, cap)TimelockController(delay, [guardian], [address(0) = anyone may execute], admin = deployer)+AIOUMGovernor(token, timelock); grant the Governor the proposer role and the guardian the canceller role, then the deployer renounces admin.StakingManager(aioum, deployer, minSelfStake, minNodeStake, tokensPerSession, unbonding, maxCommissionBps)— the deployer owns it during bootstrapIdentityRegistry(),EndpointRegistry(),ReputationRegistry(deployer),ServiceRegistry(staking),ValidationRegistry(staking, minValidatorStake, timelock)PaymentManager(services, staking, reputation, PERMIT2, deployer, treasury, protocolFeeBps, delegatorShareBps, inactivityTimeout)DisputeManager(payment, staking, reputation, deployer, arbitrator, treasury, bondToken, bondAmount, slashAmount, penaltyWeight, disputeTimeout)- Wire, while the deployer still owns these modules:
staking.setRewarder(payment),staking.setDisputeManager(dispute),staking.setRewardAsset(USDC/USDT/ETH, true),reputation.setPaymentManager(payment),reputation.setDisputeManager(dispute),payment.setDisputeManager(dispute). - Hand every deployer-owned module to the Timelock:
staking,reputation,paymentanddisputeeachtransferOwnership(timelock). From here only governance can re-wire or re-parameterise.
Deploy parameters: minSelfStake 100 ZAI (an operator's own, enough to list), minNodeStake
10,000 ZAI (total stake, own + delegated, at which a node is live), tokensPerSession 100 ZAI, unbonding 7 days,
listingBond 100 ZAI held per listing while it is up with bondUnbonding 14 days once it is taken down,
maxCommission 20%, protocolFee 5% (500 bps → treasury, global, governance-settable via
setFees onlyOwner), delegatorShare 10% (1000 bps → node's stakers) as the default,
which each service operator can re-tune per operator-set via
setServiceSplit(serviceId, delegatorShareBps) (onlyServiceOperator), disputeTimeout 3 days,
inactivityTimeout 1 day, and the demo service's pricePerCall 1 USDC. Permit2 canonical
address (same on every EVM chain): 0x000000000022D473030F116dDEE9F6B43aC78BA3.
4. End-to-end session data flow
A session is prepaid escrow drawn down by client-signed vouchers. The client signs each cumulative amount; the node can only redeem what the client authorized, so amounts cannot be forged or replayed and the node's worst case for non-payment is a single unpaid call. One session, from the operator's setup through your agent's calls to the settlement that pays everyone:
── setup (operator) ─────────────────────────────────────────────────────────
1. IdentityRegistry.register(metadataURI)
2. AIOUMToken.approve(staking) ; StakingManager.stake(amount >= minSelfStake) // live once own + delegated >= minNodeStake
3. ServiceRegistry.registerService(category, descriptorURI, pricePerCall, [assets])
── route (private-node executor, off-chain) ──────────────────────────────────
3b. when several nodes offer the service, a weighted-random router picks one by
stake + reputation + open-channel headroom (1 − activeSessions/maxConcurrent),
cheaper-preferred, with a >120%-of-avg price-outlier guard and an optional
per-step max $/call; a manual pin overrides. See ./guides/routing.md.
── open (client/agent, possibly via facilitator) ────────────────────────────
4. PaymentManager.openSession*(serviceId, asset, deposit) ── funds escrowed
guards: service active, asset accepted, node active, activeSessionCount
< StakingManager.maxConcurrent(operator)
emits SessionOpened(sessionId, client, operator, serviceId, asset, deposited)
the chosen node is thereby recorded on-chain in the session
── per call (off-chain, no gas) ─────────────────────────────────────────────
5. client signs EIP-712 PaymentVoucher(sessionId, cumulativeAmount = pricePerCall * n)
6. client POSTs the voucher + request → node runtime (MCP /mcp tools/call)
7. node verifies status==Open, signer==client, spent+price <= cumulative <= deposited,
serves the call, keeps the latest (highest) voucher
── settle (node) ────────────────────────────────────────────────────────────
8. PaymentManager.settle(sessionId, cumulativeAmount, clientSig)
recover ECDSA over _hashTypedDataV4(VOUCHER_TYPEHASH) == session.client
status → Settled ; activeSessionCount[operator] -= 1
split of gross = cumulativeAmount:
├─ operatorNet = gross − protocolFee − serviceDelegatorShare → operator
├─ protocolFee = gross * protocolFeeBps / 1e4 → treasury (global; 5% at deploy, governance-settable)
├─ delegatorShare = gross * serviceDelegatorShareBps / 1e4 → StakingManager.notifyReward
│ (per-service via setServiceSplit; default 10%)
└─ refund = deposited − gross → client
ReputationRegistry.recordSettlement(client, operator, gross) (earns 1 feedback)
emits SessionSettled(...)
── liveness backstop (client) ───────────────────────────────────────────────
9. if node idle past inactivityTimeout: PaymentManager.reclaim(sessionId)
→ refunds (deposited − spent) to client
The feedback allowance earned at step 8 is what the agent spends next: once the session settles, it rates the operator 0–100 from the run it just had, and the router at step 3b reads that rating the next time it chooses between nodes.
The four funding paths converge on one escrow via _open:
| Path | Function | Notes |
|---|---|---|
| ETH | openSessionETH | native msg.value. |
| Any ERC-20 (approval) | openSession | standard approve + transferFrom. |
| Any ERC-20, incl. USDT | openSessionWithPermit2 | one-time Permit2 approval, then signature pulls — relayer-submittable. |
| USDC (gasless) | openSessionWithAuthorization | EIP-3009 receiveWithAuthorization (x402 "exact" scheme). |
USDT lacks EIP-2612/EIP-3009, which is why Permit2 is the universal "accept any token" path.
The delegatorShare is forwarded into StakingManager.notifyReward after the asset is
transferred in, so accrued rewards can never exceed the held balance; an asset that is not an
allowed reward asset falls back to the treasury.
5. Trust & verification model
There is no cheap, general-purpose proof that a node ran the work, so the protocol rests on a crypto-economic base (cheapest, most decentralized, works for any workload) and escalates to cryptographic proof only where the stakes justify the cost. There is no hardware-vendor trust (no TEEs).
Base (always on) Escalation (where it helps) Enforcement
┌───────────────────┐ ┌─────────────────────────────┐ ┌──────────────────┐
│ stake (AIOUMToken) │ │ zkTLS / TLSNotary proof that │ │ DisputeManager │
│ + ReputationReg │ ────► │ the node returned a REAL │ ────► │ raiseDispute → │
│ + optimistic │ │ upstream API response │ │ arbitrator rules │
│ challenge │ ├─────────────────────────────┤ │ ├ upheld: │
│ │ │ zkML for small/deterministic │ │ │ slash + │
│ cost-to-cheat = │ │ compute (classifiers, │ │ │ penalize + │
│ stake-at-risk × │ │ embeddings, small models) │ │ │ refund client │
│ detection-prob │ ├─────────────────────────────┤ │ └ rejected: │
│ > gain │ │ ValidationRegistry: N-node │ │ bond → node │
│ │ │ redundant exec + stake- │ └──────────────────┘
└───────────────────┘ │ weighted attestations │
└─────────────────────────────┘
The base is always on. Every node has slashable ZAI at stake and an on-chain reputation fed
from two directions: the ratings agents issue after every settled session, from the errors and
slow answers they met, and the penalties disputes record. A client not served honestly bonds
and challenges via DisputeManager.raiseDispute(sessionId, evidenceURI), which freezes the
session (PaymentManager.markDisputed). Cost-to-cheat, stake-at-risk × detection-probability,
must exceed the gain.
Above the base, escalation follows the workload. API-relay services get zkTLS / TLSNotary: a
proof that the node returned a real upstream response, submitted as dispute evidence, with
the TLS CA set as trust root rather than a hardware vendor. Small, deterministic compute
(classifiers, embeddings, small models) gets zkML where practical; large-LLM forward passes
stay on the crypto-economic base. High-value jobs get redundant execution plus the
ValidationRegistry, where staked validators (selfStake >= minValidatorStake) attest to a
result hash via respond(id, ok), weighted by stake.
Adjudication. DisputeManager.arbitrator is the CommitRevealArbitrator: staked jurors
commit a sealed vote, reveal it, and finalize forwards the stake-weighted result to
DisputeManager.resolve once quorum and a strict majority are reached; the DAO (owner) rules only
when quorum fails or the vote ties (escalate). The module is swappable via setArbitrator, e.g.
for the ValidationQuorumArbitrator, where staked validators vote with weight equal to their
self-stake. See arbitration.
On an upheld dispute the DisputeManager calls PaymentManager.resolveDispute(_, true)
(client refunded), StakingManager.slash(operator, amount, disputeManager) with
amount = max(slashAmount, stakeAtRaise × slashBps) (pro-rata across self-stake and the
delegated pool; delegation shares stay constant so every delegator is diluted proportionally —
the delegated-PoS model), and ReputationRegistry.penalize(...); the recovered ZAI is split
challengerRewardBps (20% by default; 100% when the session had already settled and there was
no escrow to refund) to the client and the rest to the treasury, and the honest challenger's
bond is returned. On a rejected dispute the session is unfrozen and the bond compensates
the node.
Stalled disputes can't freeze funds. If the arbitrator never rules, once disputeTimeout
(3 days, setDisputeTimeout onlyOwner) has elapsed anyone may call
DisputeManager.resolveStale(disputeId): the client is refunded, the challenger's bond
returned, no slash, and the dispute marked TimedOut. A permissionless liveness backstop,
so a jury that does not show up can never hold your deposit hostage.
6. Governance flow
AIOUMToken is ERC20Votes; holders delegate voting power and the DAO executes every privileged
change through a timelock. Every protocol owner is the TimelockController, so no single
EOA can move a parameter, and every fee, stake and window changes only after a public vote and
a public delay.
ZAI holders ──delegate votes──► AIOUMGovernor
│ propose → vote (quorum 4%, ~1d delay, ~7d period)
│ queue
▼
TimelockController (delay)
│ owner of every core contract
guardian multisig ──────┤ (Timelock proposer/canceller: emergency veto
(bootstrap) │ during bootstrap)
▼ execute
StakingManager.setParams / setRewardAsset / setRewarder / setDisputeManager
PaymentManager.setFees / setTreasury / setInactivityTimeout / setDisputeManager
ReputationRegistry.setPaymentManager / setDisputeManager
DisputeManager.setArbitrator / setParams
AIOUMToken.mint (governance emission, hard-bounded by cap())
AIOUMGovernor is configured with votingDelay ~1 day (7200 blocks), votingPeriod ~7 days
(50400 blocks), proposalThreshold 100,000 ZAI, and a 4% quorum — all governance-tunable. The
bootstrap path runs a multisig-controlled Timelock and progressively decentralizes to full
Governor control. Adding a new settlement asset is a governance call
(StakingManager.setRewardAsset) plus a node opt-in (ServiceRegistry.setAcceptedAsset).
7. See also
The full design, module map and rationale are in ARCHITECTURE.md; the threat model and security checklist are in SECURITY.md; the SDK, facilitator and node-runtime integration spec is offchain/SPEC.md; and operator setup, staking and service listing are walked through in Run a node.
You have the map: which contract holds your deposit, which one may touch it, and how your agent's signature turns into a settlement.
Next: Technology and chains — the standards, toolchain and chains behind every box on this map.