AIOUM off-chain integration spec
Mirrored from
offchain/SPEC.mdin the repository — edit that file, not this page.
Every service builds against the SDK in offchain/src/sdk and the addresses in
offchain/deployments.json, written by the deploy step (src/deploy.ts) when the local development
network comes up. Do not re-deploy.
Shared facts (local development network)
- RPC
http://127.0.0.1:8545, chainId31337. Helpers insrc/config.ts. - Accounts by index (Anvil mnemonic): deployer=0, operator/node=1, client/agent=2, treasury=9, relayer=3, bundler relayer=21.
- Use
signer(index, provider())to get a wallet; wrap deploy/multi-tx senders inNonceManager. - Assets:
USDC=deployments.usdc(6 decimals),ETH=0x000…0. - SDK exports:
loadDeployments(),getContract(name,addr,runner),abiOf(name),signVoucher(signer,chainId,paymentAddr,sessionId,cumulativeAmount),recoverVoucherSigner(chainId,paymentAddr,voucher),ETH,SessionStatus.
Ports
- Node runtime: 8546. Facilitator: 8547. Bundler relay: 8548.
The bundler relay (src/bundler/server.ts, npm run bundler) exists because
EntryPoint.handleOps needs an EOA caller: the browser POSTs a signed packed
PackedUserOperation to /sendUserOperation and a gas-only relayer EOA (Anvil index 21 by default,
BUNDLER_RELAYER_INDEX; BUNDLER_PRIVATE_KEY on a live chain) submits
handleOps.
Voucher semantics (the heart of payment)
A session is prepaid. For call n, cumulativeAmount = pricePerCall * n. The client signs
each cumulative amount (EIP-712 PaymentVoucher). The node stores the latest voucher and
redeems it via PaymentManager.settle(sessionId, cumulativeAmount, signature). The node must
verify on-chain: session.status==Open, recoverVoucherSigner(...)==session.client,
storedSpent+pricePerCall <= cumulativeAmount <= session.deposited.
Facilitator HTTP API (non-custodial; holds only a gas key = index 3)
GET /health→{ ok:true, relayer:"0x.." }POST /open/eip3009body{ serviceId, asset, amount, from, validAfter, validBefore, nonce, v, r, s }→ submitsPaymentManager.openSessionWithAuthorization(...)as the relayer (pays gas; funds move fromfromto the contract via the authorization — never through the facilitator), waits, parses theSessionOpenedevent →{ sessionId, txHash }. Errors → HTTP 400{ error }.
Node runtime HTTP API (MCP-style JSON-RPC + x402)
On startup (operator = index 1, idempotent): stake 12000 ZAI if not active, register identity,
register a service (category:"mcp:demo-tools", pricePerCall = 1 USDC = 1_000_000,
acceptedAssets [USDC]). Cache the serviceId.
GET /health→{ ok:true, operator, serviceId }GET /services→[{ serviceId, category, descriptorURI, pricePerCall, acceptedAssets }]POST /mcpJSON-RPC 2.0:tools/list→{ tools:[{ name:"wordcount", description, inputSchema }, { name:"echo", ... }] }tools/callparams{ name, arguments, payment:{ sessionId, cumulativeAmount, signature } }. No/insufficient payment → HTTP402 Payment Requiredwith headerWWW-Authenticate: AIOUMVoucher network=…, asset=…, amount=…, payTo=…and a body that carries both the JSON-RPC error{ code:402, message, data:{ required, asset, sessionId } }(back-compat) and an x402accepts:[{ scheme:"aioum-voucher", network, asset, maxAmountRequired, payTo, extra:{ voucher domain+types, howToPay } }]block, so a generic HTTP client can learn how to pay. Valid → store voucher, run tool, return{ content:[{type:"text",text}] }.
POST /settlebody{ sessionId }→ node callssettle(...)with its latest voucher →{ txHash, cumulativeAmount }. (Node may also auto-settle on a timer.)
Demo agent (src/demo/run.ts) — the end-to-end story
- Load deployments; client = signer(2). Snapshot USDC balances of client/operator/treasury/staking.
GET node /services→ pick serviceId + pricePerCall.- Open a gasless session via the facilitator: build an EIP-3009 authorization for a 10 USDC
deposit (the local development network's MockUSDC ignores the signature, so pass
v=0,r=0x0,s=0x0,validAfter=0,validBefore=2^48, randomnonce),POST /open/eip3009→ sessionId. - For n in 1..3:
cumulative = pricePerCall*n;signVoucher(...);POST /mcp tools/call(toolwordcounton some text); print the result. POST node /settle. Re-read balances and print the money flow (client refund 7, operator net + delegator-share, treasury fee).
Keep everything ESM with .js import extensions. Verify with npm run typecheck (cwd offchain).
Do NOT start long-running servers in your build step — the integrator runs them.