SDK & HTTP API reference

The names you import and the endpoints you call: the TypeScript SDK (offchain/src/sdk/) and the HTTP surfaces of the facilitator and the node runtime, with the exact signature, request body and error for each.

The runtime is Node 22, TypeScript ESM (import specifiers end in .js), ethers v6 and express. The ports are Anvil 8545, node runtime 8546, facilitator 8547. The spec is offchain/SPEC.md, and the contract reference is contracts.md.

Run the local development network (cwd offchain/):

anvil                  # local development chain on :8545 (chainId 31337)
npm run deploy         # writes deployments.json
npm run node           # node runtime on :8546
npm run facilitator    # x402 facilitator on :8547
npm run demo           # end-to-end demo

SDK

Import everything from the package entrypoint:

import {
  loadDeployments,
  getContract,
  getFactory,
  abiOf,
  signVoucher,
  recoverVoucherSigner,
  voucherDomain,
  VOUCHER_TYPES,
  artifacts,
  SessionStatus,
  ETH,
  PERMIT2_CANONICAL,
  DEPLOYMENTS_PATH,
  type Deployments,
  type ServiceInfo,
  type Voucher,
} from "./sdk/index.js";

sdk/index.ts re-exports types, voucher, contracts, and the generated artifacts.

Deployments

const DEPLOYMENTS_PATH: string;                            // resolve(cwd, "deployments.json")
function loadDeployments(path?: string): Deployments;      // defaults to DEPLOYMENTS_PATH

loadDeployments reads and parses the deploy manifest. It throws deployments.json not found at <path> — run `npm run deploy` first. if the file is missing.

Deployments (from sdk/types.ts) — addresses of every protocol contract plus the USDC, ZAI and Permit2 in use (mock tokens on the local development network), the ERC-4337 stack, and the commit-reveal arbitrator when deployed:

interface Deployments {
  chainId: number;
  rpcUrl: string;
  aioum: string;
  usdc: string;
  permit2: string;
  staking: string;
  identity: string;
  reputation: string;
  serviceRegistry: string;
  payment: string;
  dispute: string;
  validation: string;
  treasury: string;
  // ERC-4337 account abstraction (present when the AA stack is deployed)
  entryPoint?: string;
  accountFactory?: string;
  paymaster?: string;
  // CommitRevealArbitrator (present when deployed with USE_COMMIT_REVEAL_ARBITRATOR=1)
  arbitrator?: string;
}

Contract helpers (sdk/contracts.ts)

type ContractName = keyof typeof artifacts;

function abiOf(name: ContractName): any[];
function getContract(name: ContractName, address: string, runner: ContractRunner): Contract;
function getFactory(name: ContractName, signer: Signer): ContractFactory;
  • abiOf(name) — the ABI array for a contract from the generated artifacts.
  • getContract(name, address, runner) — an ethers Contract bound to runner (a provider for reads, a signer for writes).
  • getFactory(name, signer) — an ethers ContractFactory (ABI + bytecode) for deploying.

ContractName is the union of artifact keys: "StakingManager", "ServiceRegistry", "ReputationRegistry", "IdentityRegistry", "ValidationRegistry", "PaymentManager", "DisputeManager", "CommitRevealArbitrator", "EntryPoint", "AIOUMAccount", "AIOUMAccountFactory", "AIOUMPaymaster", "AIOUMVerifyingPaymaster", "MockERC20", "MockUSDC", "MockPermit2". The artifacts object is auto-generated (scripts/gen-abis.mjs); each entry has { abi, bytecode }.

const payment = getContract("PaymentManager", deployments.payment, provider);
const session = await payment.getSession(1n);

Voucher signing (sdk/voucher.ts)

Mirrors PaymentManager.sol exactly: domain name "AIOUMPayments", version "1", chainId, verifyingContract = PaymentManager; type PaymentVoucher(uint256 sessionId,uint256 cumulativeAmount).

const VOUCHER_TYPES = {
  PaymentVoucher: [
    { name: "sessionId", type: "uint256" },
    { name: "cumulativeAmount", type: "uint256" },
  ],
} as const;

function voucherDomain(chainId: number, paymentManager: string): TypedDataDomain;

signVoucher

async function signVoucher(
  signer: Signer,
  chainId: number,
  paymentManager: string,
  sessionId: bigint,
  cumulativeAmount: bigint,
): Promise<Voucher>;

The client signs a cumulative amount for a session — the per-call payment authorization. Returns a Voucher:

interface Voucher {
  sessionId: bigint;
  cumulativeAmount: bigint;
  signature: string;
}

recoverVoucherSigner

function recoverVoucherSigner(
  chainId: number,
  paymentManager: string,
  v: Voucher,
): string;

The node recovers the signer address before serving a call, to confirm the voucher came from the session's client. (Same recovery the contract's settle performs on-chain.)

const v = await signVoucher(client, chainId, deployments.payment, sessionId, 1_000_000n);
const who = recoverVoucherSigner(chainId, deployments.payment, v);  // === client address

Constants & enums (sdk/types.ts)

const ETH = "0x0000000000000000000000000000000000000000";          // ETH sentinel asset
const PERMIT2_CANONICAL = "0x000000000022D473030F116dDEE9F6B43aC78BA3";

enum SessionStatus { None = 0, Open = 1, Settled = 2, Disputed = 3, Refunded = 4 }

interface ServiceInfo {
  id: bigint; operator: string; category: string;
  descriptorURI: string; pricePerCall: bigint; active: boolean;
}

HTTP APIs

All bodies are JSON. uint256 values cross the wire as decimal strings (JSON numbers cannot safely hold them).

Facilitator (:8547)

A non-custodial x402 relayer (facilitator/server.ts). It holds only a gas key (FACILITATOR_PRIVATE_KEY or RELAYER_PRIVATE_KEY on a live chain, Anvil account index 3 on the local development network; wrapped in NonceManager) and never touches user funds: a client signs an EIP-3009 transfer authorization that moves USDC straight into the PaymentManager, and the facilitator only submits openSessionWithAuthorization as relayer and returns the parsed sessionId.

GET /health

Readiness: the RPC is reachable and the relayer holds at least FACILITATOR_RELAYER_MIN_WEI (default 0.1 ETH). Returns 503 with the same body when either check fails. GET /live is the bare liveness probe and returns { "ok": true }.

Response 200:

{ "ok": true, "checks": { "rpc": true, "relayerFunded": true }, "blockNumber": 123, "relayer": "0x...", "relayerBalance": "...", "inflight": 0, "authRequired": false }

POST /open/eip3009

Relay an EIP-3009 receiveWithAuthorization to open a session gaslessly. The client must have signed the authorization (funds move from from → PaymentManager); the relayer pays gas only.

Request body — all fields required:

{
  "serviceId":   "1",
  "asset":       "0x...",        // USDC token address
  "amount":      "1000000",      // deposit, smallest unit (1 USDC = 1000000)
  "from":        "0x...",        // payer who signed the authorization
  "validAfter":  "0",
  "validBefore": "1999999999",
  "nonce":       "0x...",        // bytes32
  "v": 27, "r": "0x...", "s": "0x..."
}

Response 200:

{ "sessionId": "1", "txHash": "0x..." }

sessionId is decoded from the SessionOpened event in the receipt. A missing or malformed field (or a disallowed asset, an amount over the facilitator's cap, or an already-expired window) responds 400 { "error": "<message>" }; a reused (from, nonce) pair responds 409; too many relays in flight responds 503; a relay still unconfirmed after the fee bumps responds 504; a receipt without a SessionOpened event responds 502; and a reverted relay responds 502 { "error": "relay failed", "reason": "<short reason>" }.

Node runtime (:8546)

An MCP-style JSON-RPC tool server fronted by x402 payments (node/server.ts). On boot, setupNode() makes the operator a live, registered service on-chain (idempotent: stake → identity → service). The runtime persists the latest client-signed voucher per session under AIOUM_NODE_DIR and redeems it via /settle.

GET /health

Readiness: the RPC answers and the node is live, own stake at minSelfStake and total stake, own plus delegated, at minNodeStake. Returns 503 (with "ok": false, a hint saying what would make the node live, or an error message when the RPC call fails) when either check fails, so a load balancer drains the node instead of routing paid calls it cannot serve. role is single, or leader/follower when the node runs with a shared store.

Response 200:

{ "ok": true, "operator": "0x...", "serviceId": "1", "role": "single", "blockNumber": 123, "selfStake": "...", "totalStake": "...", "minSelfStake": "...", "minNodeStake": "...", "live": true, "staked": true }

GET /live

Liveness plus operator identity. No on-chain calls; the probe an orchestrator uses to decide whether the process is up.

Response 200:

{ "ok": true, "operator": "0x...", "serviceId": "1" }

GET /services

The service(s) this node offers: the listing an agent's marketplace search reads, with the per-call price it sees before it decides to call.

Response 200 — array:

[
  {
    "serviceId":      "1",
    "category":       "mcp:demo-tools",
    "descriptorURI":  "ipfs://demo",
    "pricePerCall":   "1000000",
    "acceptedAssets": ["0x..."]
  }
]

POST /mcp

JSON-RPC 2.0 endpoint. The body must have jsonrpc: "2.0" and a string method; otherwise the server returns JSON-RPC error -32600 (Invalid Request). Supported methods: tools/list and tools/call (payment-gated). Unknown methods return -32601.

tools/list — list available tools.

Request:

{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }

Response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [
      {
        "name": "wordcount",
        "description": "Count the number of whitespace-separated words in the provided text.",
        "inputSchema": { "type": "object", "properties": { "text": { "type": "string" } }, "required": ["text"] }
      },
      {
        "name": "echo",
        "description": "Echo back the provided text unchanged.",
        "inputSchema": { "type": "object", "properties": { "text": { "type": "string" } }, "required": ["text"] }
      }
    ]
  }
}

tools/call — run a tool. Payment-gated: params.payment must carry a voucher whose cumulativeAmount covers the next call (storedSpent + pricePerCall).

Request:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "wordcount",
    "arguments": { "text": "hello world" },
    "payment": {
      "sessionId":        "1",
      "cumulativeAmount": "1000000",   // decimal string (uint256)
      "signature":        "0x..."      // client EIP-712 PaymentVoucher signature
    }
  }
}

Success response:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": { "content": [ { "type": "text", "text": "2" } ] }
}

The x402 payment gate (node/tools.ts verifyPayment) checks, against the on-chain session:

  • session.status == Open;
  • recoverVoucherSigner(...) == session.client;
  • max(session.spent, lastStoredCumulative) + pricePerCall <= cumulativeAmount <= session.deposited (the max(...) prevents replaying an already-served voucher for a free call).

On a payment failure the node returns the x402 signal — a JSON-RPC error with code 402 and the payment requirements in data:

{
  "jsonrpc": "2.0",
  "id": 2,
  "error": {
    "code": 402,
    "message": "payment required: insufficient cumulative amount",
    "data": { "required": "2000000", "asset": "0x...", "sessionId": "1" }
  }
}

required is the cumulative amount the client must sign for the call to succeed. Other JSON-RPC errors: -32602 (bad/unknown tool or params), -32601 (method not found), -32603 (internal error).

POST /vouchers

Hand the node a voucher to keep: the client's signature over (sessionId, cumulativeAmount), checked against the session on chain (open, on a service of this node, the amount within what is spent and deposited, signed by the session's client or its smart account) and stored when the node holds nothing higher. A client hands the zero voucher right after opening a session, so a session it then leaves without a call (a failed call, a crash) is one the node can settle for 0 on its own once it goes quiet (AIOUM_NODE_IDLE_SESSION_S, node configuration): the deposit comes back and the operator's slot frees without waiting on the inactivity timeout. Not bearer-gated: a voucher under what the node holds changes nothing.

{ "sessionId": "1", "cumulativeAmount": "0", "signature": "0x..." }

Response 200 { "ok": true, "sessionId": "1", "held": "0", "idleAfterS": 120 } (held is the amount the node now holds for the session). Errors: 400 (missing or malformed fields, an amount outside the session's range), 403 (not the client's signature), 404 (not a session on this node), 409 (the session is not open).

POST /settle

Redeem the latest stored voucher for a session via PaymentManager.settle(...). The node must hold a voucher for the session (set by a prior successful tools/call, or handed through /vouchers or with this request).

Request:

{ "sessionId": "1" }

With cumulativeAmount and signature beside the id, the client settles for what it owes: the voucher is taken first as /vouchers would (never under what the node holds), so a session on which nothing was served settles for 0 and its whole deposit returns at once.

Response 200:

{ "txHash": "0x...", "cumulativeAmount": "2000000" }

The route is the operator's control plane: with AIOUM_NODE_TOKEN set it requires Authorization: Bearer <token> (a missing or wrong token gets 401 { "error": "unauthorized" }); unset is dev mode and the node warns at boot. Errors 400 { "error": "<message>" }: missing sessionId or no stored voucher for the session. If the settle does not land in this pass (a revert, a stuck tx, or the request hit an instance that is not the settle leader), the voucher stays queued for a fee-bumped retry and the response is 202 { "queued": true, "cumulativeAmount": "2000000" }.


The other services

Every other part of the stack is a plain JSON API in the same style, and @aioum/sdk's control client wraps each route.

Catalogue node (:8556, multinode.ts)

Beside /health, /live, /services, /mcp, /settle and /metrics as above (each /services record adds template, provider, pricing, load, stakeZai, reputation, maxConcurrent, activeSessions and tools): GET /media/<id>.<ext> serves what a tool saved (images, audio, a job's output files) at AIOUM_NODE_PUBLIC_URL; GET /identity?nonce=&url= signs the directory's challenge; GET /templates[?assemble=a,b] returns the gallery and an assembled services.json. The admin API (bearer AIOUM_NODE_TOKEN; new listings on a live chain only with AIOUM_ADMIN_ALLOW_ADD=1): GET /admin/services, POST /admin/preview {spec, tool, args, env} (one real call, nothing listed), POST /admin/services {spec, env} (stake, register, serve; answers the serviceId), DELETE /admin/services/:key, POST /admin/import/openapi (the importer: a spec URL or text → listings to review), POST /admin/skills ({key, files: [{path, content, encoding?}]}, {key, zipBase64} or {dir} for a folder already on the node; answers {ok, dir, name, description, license, files} from the SKILL.md), and GET /admin/verify-domain?domain= (the DNS TXT record and the /.well-known/aioum.json body to add for the node's operator, and what each check finds now).

Runner (:8770/runner, your own inside the private node; a shared runner at its listed address; agents/server.ts)

GET  /agents?owner=       POST /agents           GET|PUT|DELETE /agents/:id
PUT  /agents/:id/schedule POST /agents/:id/publish   POST /agents/:id/unpublish
POST /agents/:id/runs     GET  /agents/:id/runs?limit=   POST /agents/:id/invoke
GET  /runs?limit=         GET  /runs/:id         POST /runs/:id/stop
GET  /runs/:id/events     (SSE: run, step, now, done; ?from=N or Last-Event-ID resumes)
GET  /runs/:id/spend      GET /agents/:id/spend?period=24h|7d|all
GET  /projects?owner=&agentId=   POST /projects   GET|DELETE /projects/:id
POST /projects/:id/start|pause|resume   PUT /projects/:id/stages {stages, budgetUsdc?}   (the stages not yet done, replaced; a done stage is kept, also when sent back by its id; a stage's `after` names what it waits for, by 1-based position in the list sent or by id: no `after` means every earlier stage, `after: []` means nothing, so stages meant to run together each carry `after: []`)
PUT  /projects/:id/review {maxRounds?, acceptScore?, accepted?}   (more rounds or another bar on a paused or finished project; accepted: false withdraws a pass; start runs the rounds left)
GET  /projects/:id/events (SSE: project, stage, run, step, now, run-done, done)
GET  /projects/:id/spend?period=
GET  /skills?q=&tag=&category=&limit=&offset=   GET /skills/:id   GET /skills/:id/files
POST /super {owner, prompt, budgetUsdc, planner?, plannerModel?, start?}   (the Super Agent from a prompt)
POST /super/:id/planner {planner, plannerModel?}   (Model on the Models row)
GET  /starters?kind=agent|pipeline   POST /starters/:id/create {owner, budget} | {owner, input, budgetUsdc, agentBudgetsUsdc, start?}
GET  /tools[?refresh=1&view=card] (`view=card`: name, tool, kind, price, live, local, model and a line of description, about 20 MB for the whole network instead of 86, gzipped for a reader that accepts it; the app's builder reads it) · /planners[?refresh=1] (with catalogue[]: every model you could plan with, available or with why not, and claude: your Claude plan, its sign-in and model; refresh reads the plan again) · /wallet · /stats · /health · /live
POST /planners/claude/login (start the sign-in to your Claude plan on the runner's machine: 202 with url when the CLI printed one, 409 with detail when it cannot start)
GET  /experience?serviceId=&limit=   (the runner's own copy of what its runs learned about tools)

An agent (POST /agents) carries name, description, instructions, mode (assistant|workflow), planner (auto|anthropic|openai|marketplace) with plannerService, plannerModel (with anthropic or openai: the model the runner's key or its local endpoint plans with), tools: [{serviceId, tool}], access (only|prefer|open), deny, skills (index ids or svc:<serviceId>), skillsAccess, data: {memory, files}, dataAccess, steps, outputTemplate, budget: {capUsdc, perRunMaxUsdc} (both required on create, never defaulted: Set the budget you are willing to spend otherwise), schedule: {everyMinutes, input, enabled} and payAcross (pay in any token, on by default); the runner adds learnedSkills and boughtSkills. A project carries agentId, title, brief, budgetUsdc (required) and stages, typed ({title?, instruction, after?}: after the stages it waits for, as 1-based positions in the list sent or stage ids; absent, every stage before it; [], it can start at once) or "auto" for the agent's planner to propose three to eight with their after: a planner listing is paid one planning turn, a run of kind plan on the agent with its receipt, and the project's proposed names the planner, that run and its cost. A stored stage carries after as ids. A stage starts as soon as the stages it waits for are done, beside every other stage that is ready; the project's remainder is split among the stages started together. POST /super takes prompt and budgetUsdc (and planner: marketplace:<serviceId>, anthropic or openai) and answers {agent, project, planner: {kind, label, why}, started}: the agent fully loose with the budget as both caps, the project auto-staged (stagesNote when the planner could propose none and the project is one stage), started unless start is false. POST /starters/:id/create takes budget for an agent starter and budgetUsdc plus agentBudgetsUsdc (a cap per agent key) for a pipeline; it answers 409 with missing and needs when a pipeline's part is not there (needs[].kind is listing, connector, install or gpu, the last with where: "node" | "listing") and 400 without a budget. A run's steps carry kind, costUsdc, doing, media and, on a judge's step, the parsed judge; a finished run carries spendByService; /health reports privateNode: {url, up, token, tools} and skills: {indexed, cached, built, generatedAt}. A fixed step may carry need, the kind of job it does (data:crypto, image:gen), and task, the specific task as a short lowercase label (bitcoin spot price in usd); an assistant's planner has note_tool (the tool's name, the need, the task label, one line) and search_marketplace takes task beside need. When a run's sessions settle the runner posts one experience per session per tool to the indexer's POST /experience and says so in an info step; /tools carries each tool's experience per need and per task label (a task entry carries task) and rating.

Discovery indexer (:8549, indexer/server.ts)

GET /services[?category=&q=&live=1&official=1&limit=&offset=&view=card] (every listing by default, limit caps, limit=all is the default said out loud, offset pages, and every answer carries X-Total-Count, the count after the filters and before the page; view=card is the list a page of cards needs: every field, the first eight tools with their type, name, transport and the first 120 characters of their description, and toolCount, 14 MB for the whole directory instead of 45 (the tool descriptions MCP servers list are the weight), and a reader that calls tools takes the default view; the whole-directory answers are gzipped for a reader that accepts it, 12 MB and 2.5 MB; the order is live and reachable first, domain-verified operators next, then any rated listing before any unrated one, then the rating, stake and price, and among the unrated an operator's listings are dealt out one per operator per round, so a flood without ratings spreads thin instead of filling a page; each record with title (the template's, the name a listing shows), demand, provider, official and experience: {byNeed, byTask}, an aggregate per need and per task label), GET /services/best?task=&need=&live=1&limit= ({task, need, count, rows: [{serviceId, category, operator, nodeUrl, live, priceUsdc, tool, need, task, taskScore, needMatch, count, okRate, avgJudge, p50LatencyMs, avgCostUsdc, quality}]}: every listing ranked by the closest task label, then the need, quality, runs and price; task and taskScore say which label matched and how closely, 0 to 1), GET /services/:id, GET /services/:id/demand, GET /services/:id/experience ({serviceId, count, tools: [{tool, need, count, okRate, avgJudge, p50LatencyMs, avgCostUsdc, notes}], byNeed, byTask, records, keptDays}), POST /experience ({experience, signer, signature}: the record {serviceId, tool, need, task?, agentId, runId, sessionId, outcome, error?, judgeScore?, latencyMs, costUsdc, calls, note, at}, task a lowercase label of at most 48 characters, signed EIP-191 over aioum-experience: plus its JSON with the fields sorted by name; 201 with the listing's experience, else 400 for the shape or signature, 403 when the session's client is not the signer or the signer is the listing's operator, 404 for a session not on chain, 409 when the session has not settled for an ok, or the session and tool are already recorded), GET /relays, GET /operators/:addr, GET /nodes ({onChain: [{nodeUrl, operators, cert}], seeds, announced}: the addresses operators wrote on chain with the fingerprint of the certificate the node serves, read first and each restricted to that operator's listings; the seed list, a cache; the nodes that announced themselves, last), POST /refresh [{nodeUrl}], /live, /health. Every listing carries nodeSource (chain, seed or announced) and, for a node that serves its own certificate, nodeCert, the SHA-256 fingerprint a client pins to (sdk/pins.ts: setPin(url, fingerprint), installPins(), followChainPins(provider, endpointsAddress); a fetch to a pinned host is accepted only when the certificate matches, with no certificate authority; a name <operator>.<a-b-c-d>.aioum connects to a.b.c.d, a relay, with the name as the server name, and the relay forwards the bytes to that node's tunnel).

Staking node (:8552, backer/server.ts)

POST /requests {serviceId} (202; the review starts), GET /requests/:id, GET /requests?operator=&serviceId=&state=&limit=, GET /health (the pool's balance, what it holds behind operators, the reviewer, the rubric), GET /rubric, GET /live. A request moves requested → reviewing → approved | rejected → backing → live | failed, withdrawn later, each state with its time; the record holds the checks, the review's model and reasons, the stake and the delegation transaction.

Deposit service (:8551, deposits/server.ts)

Chains, tokens, quotes, the deposit address, the ledger and the sweep as the funding guide describes; for cards, GET /card/partners (the partners with keys, how each reports delivery, its fee, the webhook URLs to register, whether this chain can take a card payment), POST /card {wallet, partner?} ({ok, partner, name, url, expiresAt, deposit}: a single-use link, good for five minutes) and POST /card/webhook/:partner. A ledger entry's source is route, direct, devnet or card, the last with partner, orderId and partnerStatus.

Private node (:8770, private-node/server.ts)

GET /health (open), GET /connections, POST /connect, POST /disconnect, GET /oauth/:provider/callback (open; the provider's redirect), POST /mcp (tools/list, tools/call), GET /workspace/<path>, GET /schedules, POST /schedule, POST /schedule/:id/run|toggle, DELETE /schedule/:id, GET|POST /memory, DELETE /memory/:id, POST /forget-all, POST /export, POST /restore; GET /selling (your selling node: state, listings, and address: {mode, relay, publicUrl, effective, onChain, published, tls, self, cert}), POST /selling/go-live, POST /selling/stop, PUT|DELETE /selling/address ({url}, an address other machines reach, loopback and private ranges refused, at an IP on the node's own port; the node is started again, serves its own certificate at an https IP address, and writes the address and fingerprint on chain; or {relay}, a relay's tunnel address on chain: the node dials out to it and writes https://<operator>.<relay>:<port>, never this machine's address), GET /selling/relays (the relays on chain that host tunnels: [{relay, url, operator, nodes}]), /selling/node/* (DELETE /selling/node/admin/services/:key takes a listing down through the wallet's registering run); GET /selling also carries signer: {address, onChain}, the hot key the selling node runs with; everything but /health and the callback behind the bearer token.

Faucet (:8555, faucet/server.ts)

GET /health ({chainId, drip, balance, dripsLeft, claimsToday, perDay, powBits, minGasWei, total}), GET /challenge?address= ({nonce, message, powBits, minGasWei}, the nonce good for ten minutes), POST /claim ({address, nonce, signature, pow}: the signature over aioum-faucet:<chainId>:<address>:<nonce>, pow such that sha256(nonce:address:pow) shows powBits leading zero bits; 409 claimed already, 400 nonce, work or signature, 403 not the address's signature, 402 no gas, 429 the IP's or the day's limit, 503 empty; {ok, claim: {txHash, amount}}), GET /claims (the last hundred, without IPs). The private node's GET|POST /wallet/faucet claims for its own wallet.

Relay (:8553, relay/server.ts)

POST /mcp (relay_call, x402-gated), POST /settle, GET /services, GET /quote, GET /live ({ok, relay, serviceId, tunnel}: tunnel is its tunnel address), GET /tunnel ({url, host, port, nodes: [{operator, since, bytesIn, bytesOut, streams}]}, the nodes behind it), /metrics. The tunnel host (AIOUM_RELAY_TUNNEL_PORT, 8554; AIOUM_RELAY_PUBLIC_HOST, the name or IP the nodes' names carry): one TLS port where a node dials in with the server name tunnel.aioum and proves its operator keys over the relay's nonce (aioum-tunnel:<nonce>:<host>), a buyer's connection with a node's name as the server name is forwarded raw to that node's tunnel, and any other connection reaches the relay's own API, TLS terminated with the relay's certificate or plain http as it came. Frames: type, stream id, length, payload.

The control client (@aioum/sdk)

controlFromEnv() reads the AIOUM_* addresses and new AIOUMControl(cfg) exposes one method per route above, the ones the CLI and the MCP server are built on: status, activity, wallets; services, service, serviceDemand, serviceExperience, serviceBest, operator, refreshIndex, tools, pay; agentList|Get|Create|Update|Delete|Schedule|Publish|Unpublish, agentRun, runs, runGet, runStop, runWatch, projectWatch, runSpend, agentSpend, projectSpend, starters, superAgent, starterCreate, pipelineCreate, skills, skill, planners, project*; sellTemplates, sellPreview, sellAdd, sellSkillUpload, sellList, sellRemove, sellVerifyDomain; stakeOperators, stakePositions, stakeDelegate, stakeClaim, stakeUndelegate, stakeWithdraw, backerHealth, stakeRequest (follows the states, onState), stakeRequestGet, stakeRequests; jury*; deposit* with depositCardPartners and depositCard; node*. readEvents and parseFrame read a run's event stream.


End-to-end flow

The demo ties the pieces together in the order an agent uses them:

  1. Open — the client signs an EIP-3009 authorization and POSTs it to the facilitator /open/eip3009, which relays PaymentManager.openSessionWithAuthorization and returns a sessionId.
  2. Pay per call — for each tool call, the client signVouchers a higher cumulativeAmount and includes it under params.payment of POST /mcp tools/call. The node verifies it (recoverVoucherSigner), serves the tool, and stores the voucher.
  3. Settle — the node POSTs the sessionId to /settle; the contract pays the operator/treasury/stakers, refunds the client's remainder, and records the settlement for reputation. That record grants the client its one rating for the session, and an agent run by the app spends it straight away, rating the operator from the run's own errors and slow answers.

Every step ends in a contract: the facilitator's relay lands in openSessionWithAuthorization, the digest recoverVoucherSigner recomputes is the one settle verifies on-chain, and the split /settle triggers is the PaymentManager's own distribution.

Next: Contracts — every function, event and access rule behind the endpoints above.