AIOUM Protocol v2 — Architecture
Mirrored from
ARCHITECTURE.mdin the repository — edit that file, not this page.
A decentralized, metered marketplace/runtime for agent services. Nodes run services (an MCP tool server, a resold API, a model) and front the upstream costs; any connected agent pays per-call micropayments in USDC / USDT / ETH. The volatile ZAI token is used only to secure nodes (stake/slash) and to govern the DAO — never as a payment asset.
This is a ground-up redesign of the original ZAI contracts, an early prototype with multiple fund-loss bugs (see § Bug → fix).
1. Module map (src/)
| Contract | Role |
|---|---|
token/AIOUMToken.sol | ERC20 + Votes + Permit + Capped + Burnable. Staking & governance asset. owner = Timelock. |
governance/AIOUMGovernor.sol | OZ Governor executing through a TimelockController. Token-weighted DAO. |
registry/IdentityRegistry.sol | ERC-8004 identity — one portable, resolvable on-chain identity per address. |
registry/ReputationRegistry.sol | ERC-8004 reputation — settlement-gated client feedback + dispute penalties. |
registry/ValidationRegistry.sol | ERC-8004 validation — staked validators attest to job results (verification hook). |
staking/StakingManager.sol | Node stake/delegation, multi-asset reward accounting, unbonding, slashing. |
registry/ServiceRegistry.sol | On-chain catalogue of services (replaces NodeRegistration). Listing requires active stake. |
payments/PaymentManager.sol | Metered sessions, client-signed EIP-712 vouchers, ETH/Permit2/EIP-3009 funding, per-service fee split. |
disputes/DisputeManager.sol | Optimistic challenge → slash + reputation penalty + client refund; resolveStale timeout backstop if the arbitrator never rules. |
external/IPermit2.sol, external/IERC3009.sol | Thin interfaces to canonical Permit2 / EIP-3009 (USDC). |
Tested across 11 suites (226 tests total — protocol regressions, exposure-scaled economics, verifiability, commit-reveal and validator-quorum arbitration, account abstraction and the verifying paymaster, delegated-slashing, governance, fuzz + payment invariants; test/AIOUMProtocol.t.sol holds the 42 protocol tests, incl. explicit regression tests for every
critical bug in the old code). forge test → all green.
2. End-to-end flow
register identity stake ZAI list service
operator ───────────────────► IdentityRegistry StakingManager ServiceRegistry
│ (capacity = f(stake))
client/agent ▼
│ openSession(serviceId, USDC, deposit) ──► PaymentManager (funds escrowed)
│ ── per call: sign EIP-712 voucher(sessionId, cumulativeAmount) off-chain ──► node
│ │ serves the call (MCP/API/model)
│ (node redeems latest voucher) ▼
└─────────────────────► settle(sessionId, amount, clientSig)
├─ operatorNet → node (= gross − protocolFee − serviceDelegatorShare)
├─ protocolFee → DAO treasury (global; 5% at deploy, governance-settable)
├─ delegatorShare → StakingManager.notifyReward (stakers; operator-set per service, 10% default)
└─ unspent → client refund
reputation.recordSettlement(client, node) (earns 1 feedback)
dispute (non-delivery): client bonds → DisputeManager.raiseDispute → arbitrator rules →
upheld ⇒ node slashed + penalised + client refunded.
liveness backstop: after `disputeTimeout` (3-day default) anyone can `resolveStale` a
stalled dispute → client refunded + bond returned, NO slash (TimedOut).
Why the amount is trustworthy: the client signs each cumulative amount (EIP-712, domain-separated by chainId + the PaymentManager address). The node can only ever redeem an amount the client actually authorized, so payment amounts can't be forged or replayed. The client only signs for calls it received, so the node's worst case for non-payment is a single call. This replaces the original's "node unilaterally asserts the message count."
3. Settlement & assets (USDC / USDT / ETH)
Payments are asset-agnostic. Four funding paths converge on one escrow:
| Path | Function | Notes |
|---|---|---|
| ETH | openSessionETH | native value. |
| Any ERC-20 (approval) | openSession | standard approve + transferFrom. |
| Any ERC-20, incl. USDT | openSessionWithPermit2 | one-time Permit2 approval, then signature pulls — the "accept any token" layer. Relayer-submittable (gasless for payer). |
| USDC (gasless) | openSessionWithAuthorization | EIP-3009 receiveWithAuthorization (the x402 "exact" scheme). Relayer-submittable. |
USDT lacks EIP-2612/EIP-3009, which is exactly why Permit2 is the universal path. Adding a
new settlement asset is a governance call (StakingManager.setRewardAsset) + node opt-in
(ServiceRegistry.setAcceptedAsset).
4. Staking, rewards & slashing
- Stake (ZAI): operators self-stake; delegators delegate to a node.
maxConcurrent = 1 + (totalStake − minNodeStake) / tokensPerSession— capacity is bought with stake, enforced by the PaymentManager (not a node-settable counter). - Rewards (settlement assets):
notifyRewardis called by the PaymentManager after the asset is transferred in, so accrued rewards can never exceed the held balance. A node's reward splits by stake weight between the operator (self-stake + commission) and delegators (MasterChefaccRewardPerShareover the delegated pool, per asset). Reward-debt is re-synced on every balance change → no double-count, no underflow lock-out. - Fee split (per service): the protocol fee is global (5% at deploy, governance-settable
via
setFees,onlyOwner), but the stakers' (delegator) share is set per service by the operator viaPaymentManager.setServiceSplit(serviceId, delegatorShareBps)(onlyServiceOperator); the 10% globaldelegatorShareBpsis only the default.operatorNet = gross − protocolFee − serviceDelegatorShare. - Slashing (ZAI):
slashis callable only by the DisputeManager and cuts pro-rata across operator self-stake and the delegated pool. Delegations are tracked as shares; a slash removes backing assets while shares stay constant, diluting every delegator proportionally (delegated-PoS model). Already-accrued rewards are never slashed; capped at the node's total stake.delegatedAssetsOfreturns a delegator's current post-slash value. - Unbonding: governance-set cooldown (default 7 days — the original used a 2-minute test value). Per-account unbonding arrays are cleared on claim → no global-loop gas DoS.
5. Governance
Every privileged function is onlyOwner where owner = TimelockController — never an
EOA (the original was single-EOA onlyOwner everywhere). Flow:
ZAI holders ──vote──► AIOUMGovernor ──queue──► TimelockController(delay) ──execute──► protocol
guardian multisig = Timelock proposer/canceller (emergency veto)
The guardian multisig holds the Timelock proposer/canceller roles as an emergency veto; governance can revoke them to leave the Governor in sole control. Quorum 4%, ~1-day voting delay, ~7-day voting period (all governance-tunable).
6. Verification model (crypto-economic first, escalate as needed)
Trustlessly proving a node ran the work is unsolved; the base layer is crypto-economic (cheapest, most decentralized, works for any workload) and we escalate only where stakes justify the cost:
- Base: stake + reputation + optimistic challenge (
DisputeManager). Cost-to-cheat =stake at risk × detection probability> gain. No hardware-vendor trust (no TEEs). - API-relay services: zkTLS / TLSNotary proofs that the node returned a real upstream response — submitted as dispute evidence. Trust root = TLS CAs (light), not Intel.
- Small/deterministic compute: zkML where practical (classifiers, embeddings, small models). Not used for large-LLM forward passes (impractical in 2026).
- High-value jobs: redundant N-node execution +
ValidationRegistrystake-weighted attestations.
ValidationRegistry lets staked validators vote on a result hash. DisputeManager.setArbitrator
installs the network's arbitrator, CommitRevealArbitrator — the open, staked commit-reveal jury
(docs/design/arbitration.md); ValidationQuorumArbitrator is its predecessor.
7. Off-chain components (shipped in this repo)
The on-chain protocol is the trust layer. The rest of the stack ships here too
(offchain/, packages/):
- Facilitator (x402): a non-custodial relayer that verifies/relays signed payment authorizations (Permit2 / EIP-3009) and can sponsor gas. It never holds funds — payment authorizations name the node as recipient — so self-running it (and running several) removes the Coinbase dependency without adding custody risk.
- Account abstraction (UX): ERC-4337 smart accounts (
src/aa/) with budget-scoped, expiring session keys (cap enforced on-chain) so an agent pays per call within bounds without per-call human signing, a self-run signature-gated verifying paymaster so users never hold the gas token (the local development network can also run an accept-everything paymaster for convenience), and an in-repo bundler relay (offchain/src/bundler/). - Publishable packages (
packages/):@aioum/sdk— a viem-based agent-payments SDK ("authorize one budget-capped key, then pay per call") — and@aioum/mcp— an MCP server that exposes marketplace services as paid tools to any MCP client (Claude Desktop, agent frameworks). Seepackages/sdk/README.mdandpackages/mcp/README.md. The node runtime answers unpaid calls with a real HTTP 402 + x402acceptschallenge, so standard x402 clients interoperate. - Node runtime: hosts the service as an MCP server, watches
SessionOpened, serves calls against incoming vouchers, and submitssettle. Replaces the old XMTP relay. Each service declares amodelstring (a claim, surfaced via/services/tools/list//health; onlydescriptorURIis stored on-chain). The private node also runs scheduled workflows as cron, executed by a budget-capped agent wallet. - Routing (off-chain): with multiple nodes per service, the client picks one by
weighted-random over stake + reputation + open-channel headroom (
1 − active/maxConcurrent), preferring cheaper nodes, excluding price outliers (>120% of average), honouring an optional per-step max $/call and manual pin overrides;/servicesexposesmaxConcurrent+activeSessions, and the chosen node is recorded on-chain in the session. - Directory & favourites:
/explorelists all nodes with their declared model, supports ★ favourites (local) and an All/Favourites filter. - Indexer: a subgraph over
Registered/ServiceRegistered/ session & reputation events for discovery (an "app store" for agent services).
8. Decentralization posture
Decentralization is a config choice per axis, not a property of the stack:
| Axis | This build |
|---|---|
| Compute | Node self-hosts the real service (not a proxied corporate API). |
| Settlement asset | USDC/USDT/ETH; governance can add further settlement assets (StakingManager.setRewardAsset). |
| Chain | App-rollup (OP Stack / Arbitrum Orbit) secured by ZAI. |
| Verification | Crypto-economic + zkTLS/zkML — no hardware-vendor trust. |
| Governance | Timelock + multisig → DAO (no single EOA). |
| Identity/discovery | On-chain ERC-8004 registries + subgraph. |
9. Bug → fix map
Every critical/high finding from the review of the original contracts, and how v2 closes it:
| Original bug | v2 fix |
|---|---|
channeloperations.createChannel public, credited balance with no deposit → drain | No public create; a session exists only after funds are actually pulled (ETH/Permit2/3009/approval). |
staking.distributeRewards public → inflate accrual & drain RewardsManager | notifyReward is rewarder-only and credited only after the asset is received (accrued ≤ held). |
staking.initializeNodeStake public → overwrite/seize any node | Node positions are created only by the operator via stake(); no overwrite path. |
staking.claimRewards() zeroed rewards, transferred nothing | Claims transfer via _payout (ERC-20 safeTransfer / ETH call), asserted by tests. |
No slash() existed (security premise unimplemented) | slash implemented, DisputeManager-gated, with reputation penalty. |
| Dead signature code; node asserts message count; 2-min dispute window | Client-signed EIP-712 vouchers (domain-separated); amount can't be forged/replayed. |
registration.updateChannelCount node-settable → bypass capacity | Capacity derived from stake (maxConcurrent), enforced in PaymentManager. |
restakeRewards didn't update rewardDebt → double-count; updateDelegatorRewards underflow lock | MasterChef accounting with debt re-synced on every balance change; no underflow path. |
Unbounded withdrawUnstakedTokens loop; div-by-zero in node discovery | Per-account unbonding arrays cleared on claim; EnumerableSet + paginated discovery. |
onlyOwner on a single EOA everywhere; "DAO arbitrator" unimplemented | Timelock/DAO ownership + a real DisputeManager arbitrator. |
CEI violations / raw transfer | CEI + ReentrancyGuard + SafeERC20 throughout. |
approveTokens approved the contract to itself (no-op) | Removed; funding uses transferFrom/Permit2/EIP-3009 correctly. |
10. Deployment order
AIOUMToken(treasury, timelock, initialSupply, cap)TimelockController(delay, [multisig], [multisig], admin=0)andAIOUMGovernor(token, timelock); grant Governor the proposer role.StakingManager(aioum, timelock, minNodeStake, tokensPerSession, unbonding, maxCommissionBps)IdentityRegistry(),ReputationRegistry(timelock),ServiceRegistry(staking),ValidationRegistry(staking, minValidatorStake)PaymentManager(services, staking, reputation, PERMIT2, timelock, treasury, protocolFeeBps, delegatorShareBps, inactivityTimeout)DisputeManager(payment, staking, reputation, timelock, arbitrator, treasury, bondToken, bondAmount, slashAmount, penaltyWeight, disputeTimeout)- Wire (via Timelock):
staking.setRewarder(payment),staking.setDisputeManager(dispute),staking.setRewardAsset(USDC/USDT/ETH, true),reputation.setPaymentManager(payment),reputation.setDisputeManager(dispute),payment.setDisputeManager(dispute).
Permit2 canonical address (same on every EVM chain via CREATE2):
0x000000000022D473030F116dDEE9F6B43aC78BA3.
11. What ships
- Delegated-stake slashing via share accounting (§4), with unit tests and fuzz properties.
CommitRevealArbitrator— the open, staked commit-reveal jury installed as theDisputeManagerarbitrator viasetArbitrator(test/CommitRevealArbitrator.t.sol);ValidationQuorumArbitrator, its stake-weighted predecessor, remains deployable (test/ValidationQuorumArbitrator.t.sol).- Dispute liveness backstop:
DisputeManager.disputeTimeout+setDisputeTimeout(onlyOwner)resolveStaleforce-close (refund + bond return, no slash,TimedOutstatus).
- Per-service profit split (
PaymentManager.setServiceSplit,onlyServiceOperator) over the global, governance-set protocol fee. - Off-chain stack in
offchain/: a hardened non-custodial x402 facilitator (EIP-3009 validation, asset allowlist, amount cap, per-IP rate-limit, nonce replay guard, optional bearer auth), the MCP-style node runtime (declared model, scheduled workflows, weighted-random routing,/exploredirectory + favourites), the client SDK (EIP-712 vouchers), and an end-to-end demo (14 services across competing nodes) that runs on the local development network. - ERC-4337 account abstraction (
src/aa/):AIOUMAccountwith budget-scoped, expiring session keys (spend cap enforced on-chain), CREATE2 factory, the signature-gatedAIOUMVerifyingPaymasterand an in-repo bundler relay — e2e-tested throughEntryPoint.handleOps(test/AccountAbstraction.t.sol,test/VerifyingPaymaster.t.sol), deployed byoffchain/src/deploy.ts, documented indocs/guides/account-abstraction.md. - Env-driven deploy to any EVM chain (
USDC_ADDRESS/PERMIT2_ADDRESS/DEPLOYER_PRIVATE_KEY/REAL_CHAIN+ post-deploy verify), TLS Caddyfile, subgraphdocker-compose— seedocs/deploying.md. - Discovery subgraph in
subgraph/(graph-cligenerates its bindings).
Upcoming work is listed in NEXT-STEPS.md; the security record is in SECURITY.md.