AIOUM Protocol v2 — Architecture

Mirrored from ARCHITECTURE.md in 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/)

ContractRole
token/AIOUMToken.solERC20 + Votes + Permit + Capped + Burnable. Staking & governance asset. owner = Timelock.
governance/AIOUMGovernor.solOZ Governor executing through a TimelockController. Token-weighted DAO.
registry/IdentityRegistry.solERC-8004 identity — one portable, resolvable on-chain identity per address.
registry/ReputationRegistry.solERC-8004 reputation — settlement-gated client feedback + dispute penalties.
registry/ValidationRegistry.solERC-8004 validation — staked validators attest to job results (verification hook).
staking/StakingManager.solNode stake/delegation, multi-asset reward accounting, unbonding, slashing.
registry/ServiceRegistry.solOn-chain catalogue of services (replaces NodeRegistration). Listing requires active stake.
payments/PaymentManager.solMetered sessions, client-signed EIP-712 vouchers, ETH/Permit2/EIP-3009 funding, per-service fee split.
disputes/DisputeManager.solOptimistic challenge → slash + reputation penalty + client refund; resolveStale timeout backstop if the arbitrator never rules.
external/IPermit2.sol, external/IERC3009.solThin 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:

PathFunctionNotes
ETHopenSessionETHnative value.
Any ERC-20 (approval)openSessionstandard approve + transferFrom.
Any ERC-20, incl. USDTopenSessionWithPermit2one-time Permit2 approval, then signature pulls — the "accept any token" layer. Relayer-submittable (gasless for payer).
USDC (gasless)openSessionWithAuthorizationEIP-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): notifyReward is 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 (MasterChef accRewardPerShare over 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 via PaymentManager.setServiceSplit(serviceId, delegatorShareBps) (onlyServiceOperator); the 10% global delegatorShareBps is only the default. operatorNet = gross − protocolFee − serviceDelegatorShare.
  • Slashing (ZAI): slash is 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. delegatedAssetsOf returns 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:

  1. Base: stake + reputation + optimistic challenge (DisputeManager). Cost-to-cheat = stake at risk × detection probability > gain. No hardware-vendor trust (no TEEs).
  2. 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.
  3. Small/deterministic compute: zkML where practical (classifiers, embeddings, small models). Not used for large-LLM forward passes (impractical in 2026).
  4. High-value jobs: redundant N-node execution + ValidationRegistry stake-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). See packages/sdk/README.md and packages/mcp/README.md. The node runtime answers unpaid calls with a real HTTP 402 + x402 accepts challenge, so standard x402 clients interoperate.
  • Node runtime: hosts the service as an MCP server, watches SessionOpened, serves calls against incoming vouchers, and submits settle. Replaces the old XMTP relay. Each service declares a model string (a claim, surfaced via /services / tools/list / /health; only descriptorURI is 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; /services exposes maxConcurrent + activeSessions, and the chosen node is recorded on-chain in the session.
  • Directory & favourites: /explore lists 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:

AxisThis build
ComputeNode self-hosts the real service (not a proxied corporate API).
Settlement assetUSDC/USDT/ETH; governance can add further settlement assets (StakingManager.setRewardAsset).
ChainApp-rollup (OP Stack / Arbitrum Orbit) secured by ZAI.
VerificationCrypto-economic + zkTLS/zkML — no hardware-vendor trust.
GovernanceTimelock + multisig → DAO (no single EOA).
Identity/discoveryOn-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 bugv2 fix
channeloperations.createChannel public, credited balance with no deposit → drainNo public create; a session exists only after funds are actually pulled (ETH/Permit2/3009/approval).
staking.distributeRewards public → inflate accrual & drain RewardsManagernotifyReward is rewarder-only and credited only after the asset is received (accrued ≤ held).
staking.initializeNodeStake public → overwrite/seize any nodeNode positions are created only by the operator via stake(); no overwrite path.
staking.claimRewards() zeroed rewards, transferred nothingClaims 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 windowClient-signed EIP-712 vouchers (domain-separated); amount can't be forged/replayed.
registration.updateChannelCount node-settable → bypass capacityCapacity derived from stake (maxConcurrent), enforced in PaymentManager.
restakeRewards didn't update rewardDebt → double-count; updateDelegatorRewards underflow lockMasterChef accounting with debt re-synced on every balance change; no underflow path.
Unbounded withdrawUnstakedTokens loop; div-by-zero in node discoveryPer-account unbonding arrays cleared on claim; EnumerableSet + paginated discovery.
onlyOwner on a single EOA everywhere; "DAO arbitrator" unimplementedTimelock/DAO ownership + a real DisputeManager arbitrator.
CEI violations / raw transferCEI + ReentrancyGuard + SafeERC20 throughout.
approveTokens approved the contract to itself (no-op)Removed; funding uses transferFrom/Permit2/EIP-3009 correctly.

10. Deployment order

  1. AIOUMToken(treasury, timelock, initialSupply, cap)
  2. TimelockController(delay, [multisig], [multisig], admin=0) and AIOUMGovernor(token, timelock); grant Governor the proposer role.
  3. StakingManager(aioum, timelock, minNodeStake, tokensPerSession, unbonding, maxCommissionBps)
  4. IdentityRegistry(), ReputationRegistry(timelock), ServiceRegistry(staking), ValidationRegistry(staking, minValidatorStake)
  5. PaymentManager(services, staking, reputation, PERMIT2, timelock, treasury, protocolFeeBps, delegatorShareBps, inactivityTimeout)
  6. DisputeManager(payment, staking, reputation, timelock, arbitrator, treasury, bondToken, bondAmount, slashAmount, penaltyWeight, disputeTimeout)
  7. 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 the DisputeManager arbitrator via setArbitrator (test/CommitRevealArbitrator.t.sol); ValidationQuorumArbitrator, its stake-weighted predecessor, remains deployable (test/ValidationQuorumArbitrator.t.sol).
  • Dispute liveness backstop: DisputeManager.disputeTimeout + setDisputeTimeout (onlyOwner)
    • resolveStale force-close (refund + bond return, no slash, TimedOut status).
  • 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, /explore directory + 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/): AIOUMAccount with budget-scoped, expiring session keys (spend cap enforced on-chain), CREATE2 factory, the signature-gated AIOUMVerifyingPaymaster and an in-repo bundler relay — e2e-tested through EntryPoint.handleOps (test/AccountAbstraction.t.sol, test/VerifyingPaymaster.t.sol), deployed by offchain/src/deploy.ts, documented in docs/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, subgraph docker-compose — see docs/deploying.md.
  • Discovery subgraph in subgraph/ (graph-cli generates its bindings).

Upcoming work is listed in NEXT-STEPS.md; the security record is in SECURITY.md.