Node quickstart — bring your API, get paid (no code)
The demo node staked, listed one built-in tool and settled three paid calls. Now a node that sells your API, model or data, from a JSON file and a Docker command, with no TypeScript and no changes to the repo: ten minutes from "I have an API" to agents paying you per call. Every step is the shortest thing that works, on the local development network or the AIOUM network; wallet setup, pricing, the AI-assistant path and the operator playbook in full: Sell a service.
1. Describe your services (one JSON file)
One file, services.json, describes a node. Assemble it from the gallery or write it by hand:
cd offchain
npm run templates -- add weather-open-meteo jina-reader --out services.json # from the gallery…
cp services.example.json services.json # …or by hand
The template gallery holds ninety tested listings, most of them crypto (feeds, DEX quotes, token data, RWA, prediction markets, chain reads, social bots), and the app's Earn page assembles them in a browser. Each entry is a service with its own on-chain listing, price and stake, and one or more tools built from adapters, the bridge between what you have and what an agent can pay for:
| Adapter | Wraps | You provide |
|---|---|---|
rest | any HTTP API | URL template ({arg} placeholders), optional headers/body, responsePath |
llm-openai | any OpenAI-compatible endpoint (OpenAI, OpenRouter, vLLM, LM Studio) | model, apiKeyEnv, optional baseUrl/system |
ollama | a local model via Ollama | model (that's it — your box, your weights, per-call revenue) |
llm-planner | an OpenAI-compatible endpoint sold as a planner: the model that plans an assistant agent's turns, paid per turn | model, optional apiKeyEnv/baseUrl; a planner:* category so builders find it |
fal | any of fal.ai's 1,000+ image/video/audio models | model slug + FAL_KEY in env; mode: "queue" for long jobs — see Resell fal.ai |
apify | any of Apify's 20,000+ Actors (scrapers, automations) | actor + APIFY_TOKEN in env; mode: "async" for long runs — see Resell web data |
brightdata | Bright Data Web Unlocker / SERP zones, or a Web Scraper API dataset | zone (or mode: "scraper" + datasetId) + BRIGHTDATA_API_KEY in env — see Resell web data |
A complete service is a few lines. This one resells a weather API at a fraction of a cent per call:
{ "services": [{
"key": "weather",
"category": "data:weather",
"priceUsdc": 0.002,
"tools": [{
"type": "rest",
"name": "current",
"url": "https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}¤t_weather=true",
"responsePath": "current_weather"
}]
}]}
Secrets never go in the file. Write "x-api-key": "env:MY_UPSTREAM_KEY", set
MY_UPSTREAM_KEY in the environment, and the node refuses to boot if it is missing. Responses are
hashed into the results transcript like any built-in tool, so
a resold API is verifiable automatically.
Once the listing is live, agents that find their own tools see what yours does and costs in their
marketplace search, call it within their budget, and rate you on-chain after every settled session,
so a healthy upstream and a tight responsePath earn you the next call.
2. Configure the node
The node is configured through .env:
cp .env.example .env # then set:
RPC_URL is your chain RPC; on the local development network, http://host.docker.internal:8545.
The operator key is OPERATOR_PRIVATE_KEY, or better OPERATOR_KEYSTORE_FILE +
OPERATOR_KEYSTORE_PASSWORD_FILE, encrypted at rest. The node signs every transaction with that
key, and on the AIOUM network refuses to start without one of the two; on the local development
network omit it and Anvil's test accounts are used. AIOUM_NODE_TOKEN is the bearer token for
the operator control plane (/settle, /metrics): set it before exposing the node publicly.
The last piece is deployments.json, the contract addresses for your chain, next to
docker-compose.yml. On the AIOUM network that is its published deployments.json, the file
npm run deploy writes and Deploying the contracts publishes; on the local
development network it is the one npm run deploy wrote. The container mounts it read-only, and
the node refuses to boot without it.
Every other setting (rate limits, settlement retries, HA, relay, privacy jitter) is listed with
its default in the operator configuration reference. The operator
wallet needs the ZAI it stakes of its own (stakeZai per service, at least 100; the listing is
live at 10,000 total, own plus what backers delegate) and a little gas; on the local development
network both are auto-minted.
3. Run it
docker compose up -d # builds the image, mounts services.json, persists /data
curl -s localhost:8546/health # staked? RPC reachable? → ok:true
Without Docker, AIOUM_SERVICES_CONFIG=./services.json npm run multinode does the same. If the
container exits immediately, the log names the missing file.
From here the node carries the whole job, per service: it stakes (minting first on the local
development network), registers your identity and the on-chain listing, serves x402-gated calls,
records transcripts and anchors results roots, settles earned receipts durably (crash-safe,
fee-bumped), and prunes expired evidence at every boot. You watch /health and your balance.
4. Operate it
Scaling out (HA). The catalogue runtime (npm run multinode, the Docker image's command) is
one instance per operator key: it takes a PID lock on its data dir at every boot and has no settle
lease, because two settlers on one key fight over nonces. Shared-store mode belongs to the
single-service runtime, npm run node (src/node/server.ts): set AIOUM_STORE=sqlite, give
each instance its own AIOUM_NODE_PORT, and the instances share one receipt/transcript database.
A receipt accepted by any instance raises the floor for all, the anchored results root covers
every instance's calls in one canonical order, and a TTL settle lease elects the single
settler: kill the leader and a peer takes over within AIOUM_SETTLE_LEASE_MS, which
npm run demo:ha verifies live.
Monitoring. GET /metrics serves Prometheus metrics (bearer-gated when the token is set):
calls served, 402s, upstream errors and unsettled-receipt exposure, how many sessions hold a
receipt you have earned and not yet collected. /live and /health are for orchestrators.
State. Everything the node must not lose lives in the /data volume: receipts are earned
money and transcripts are evidence. Back it up, never share it between instances, and wipe it
when you reset the local development network, because session ids restart.
Prices and splits. Edit services.json and restart. Listings are idempotent: on boot the
node reuses your live listing in the same category and registers one only if none exists; a
changed split is written on-chain with setServiceSplit. A new price changes what the node
charges and advertises in /services, but the on-chain pricePerCall stays as first registered;
the node calls updateService only to mark a listing inactive when you remove it with
DELETE /admin/services/:key.
Troubleshooting
| Symptom | Cause |
|---|---|
boot fails needs env var X | an adapter secret (env:X) is unset — set it in .env |
another … instance already runs | second container on one data dir/key — stop it |
calls 402 with session not open | client's session expired/settled — client reopens |
results root required on settle | only the single-service runtime (npm run node) hits this, when it was started with AIOUM_COMMIT_RESULTS=0 on a commitRequired service — re-enable it; the catalogue node always anchors the root before settling |
Between docker compose up and ok:true the node staked, took an identity, listed each service
and started settling.
Next: Run a service node — what the node does on every boot, seen from the contracts, and the choices that are yours.