AIOUM off-chain integration spec

Mirrored from offchain/SPEC.md in 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, chainId 31337. Helpers in src/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 in NonceManager.
  • 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/eip3009 body { serviceId, asset, amount, from, validAfter, validBefore, nonce, v, r, s } → submits PaymentManager.openSessionWithAuthorization(...) as the relayer (pays gas; funds move from from to the contract via the authorization — never through the facilitator), waits, parses the SessionOpened event → { 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 /mcp JSON-RPC 2.0:
    • tools/list → { tools:[{ name:"wordcount", description, inputSchema }, { name:"echo", ... }] }
    • tools/call params { name, arguments, payment:{ sessionId, cumulativeAmount, signature } }. No/insufficient payment → HTTP 402 Payment Required with header WWW-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 x402 accepts:[{ 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 /settle body { sessionId } → node calls settle(...) 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

  1. Load deployments; client = signer(2). Snapshot USDC balances of client/operator/treasury/staking.
  2. GET node /services → pick serviceId + pricePerCall.
  3. 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, random nonce), POST /open/eip3009 → sessionId.
  4. For n in 1..3: cumulative = pricePerCall*n; signVoucher(...); POST /mcp tools/call (tool wordcount on some text); print the result.
  5. 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.