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 ethersContractbound torunner(a provider for reads, a signer for writes).getFactory(name, signer)— an ethersContractFactory(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(themax(...)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:
- Open — the client signs an EIP-3009 authorization and
POSTs it to the facilitator/open/eip3009, which relaysPaymentManager.openSessionWithAuthorizationand returns asessionId. - Pay per call — for each tool call, the client
signVouchers a highercumulativeAmountand includes it underparams.paymentofPOST /mcptools/call. The node verifies it (recoverVoucherSigner), serves the tool, and stores the voucher. - Settle — the node
POSTs thesessionIdto/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.