Your node: workspace, models, wallet and accounts

One machine is yours: it runs your private node, and everything on it is a tool your agents get beside the paid listings, free, unmetered and private. A workspace with files, a shell, a browser and ffmpeg; models from Hugging Face installed on your hardware; a wallet and an exchange account with caps you set; your ad, analytics and publishing accounts through your own OAuth apps; your AI subscriptions and keys; the skills an agent loaded; a trainer for models of your own. The marketplace never sees any of it. The marketplace page shows them under Your node, one card per connector with Connect or Install, and the builder's Your node step lists their tools for an agent to tick.

Start the node in offchain/ with npm run private-node (Your private node). The runner reaches it at AIOUM_PRIVATE_NODE_URL (http://127.0.0.1:8770) with the token from ~/.aioum-private/token; the app asks for that token once, under Your node, when it runs on another machine. A run's log shows every step on your node as Local · tool, free.

The workspace

~/.aioum-private/workspace (PRIVATE_NODE_WORKSPACE) is the folder your agents work in. Tools: ws.list {path?}, ws.read {path}, ws.write {path, content}, ws.run {cmd, timeoutMs?} (a shell inside the root, 120 s and 1 MB of output by default, up to 600 s), ws.fetch {url, path} (a download), ws.screenshot {url, path?, fullPage?} and ws.browse {url} (a headless Chromium through Playwright: npm i playwright && npx playwright install chromium in offchain/, or PLAYWRIGHT_MODULE), and ws.ffmpeg {args} (ffmpeg on the PATH, with the hardware decoder the optimizer found put in front of the input). A path outside the root is refused. Every file has a URL, /workspace/<path> on the node behind its token, so a result an agent renders shows inline on its page and a pipeline hands files from stage to stage. Whatever an agent writes, downloads, screenshots or renders lands here; skills it loads go under skills/. A workspace file reaches the GPU rented for a project too: name it as a job's input (/workspace/<path>) and the runner ships it to the pod.

Hugging Face models on your machine

hf.install {repo, kind, task?, quant?} installs a model from huggingface.co:

  • kind: "llm" pulls it into Ollama, ollama pull hf.co/<repo>[:<quant>]. Ollama must answer at OLLAMA_HOST (127.0.0.1:11434); else "Ollama is not reachable … Install it from https://ollama.com/download and start it".
  • kind: "pipeline" makes a Python venv at ~/.aioum-private/models/venv with transformers, diffusers, torch, torchvision, accelerate, safetensors and soundfile (a few hundred MB the first time), downloads the model, and serves it through a small worker on 127.0.0.1:8790 that loads pipeline(task, model) on demand. Without python3: "install Python 3.9 or newer".

The install is a job: the answer carries its id and hf.status {id} reports progress and the log; nothing is pretended. Before anything downloads the node reads the model's size from the Hub and refuses one your machine cannot hold, naming the largest quantization that fits, a smaller variant of the family, or a rented GPU (the compute:gpu listings). hf.list shows what is installed and whether Ollama, Python and the worker are up; hf.remove {repo} removes one; hf.run {repo, inputs} runs it (an LLM through Ollama's /api/generate, a pipeline through the worker; images and audio land in the workspace under hf/), tuned for the machine. On the marketplace, the Hugging Face models card takes a repo id, LLM, through Ollama or Pipeline, in Python, and Install.

What the machine can do, and the settings that fit it

npm run hardware:check reads the machine, CPU cores, RAM, GPUs and VRAM (nvidia-smi, rocm-smi, Apple Silicon through system_profiler), disk, Docker and the NVIDIA runtime, Ollama, ffmpeg's hardware encoders, python3, and writes .deploy/hardware.json. The catalogue reads it: a local template with requires (gpu, vramGb, ramGb, cpus) is listed only when the machine meets it, else skipped with "needs a GPU with ≥ 12 GB (this machine: none); rent one: the compute:gpu listings". hardware.check {refresh?} on the node shows the same report.

hardware.optimize {kind, model?, …} gives the settings that fit, kind one of ollama, diffusers, whisper, ffmpeg, docker or train, each rule cited from the project's own documentation: the largest quantization and context that fit for Ollama; dtype, attention slicing, VAE tiling and CPU offload for diffusers; compute type and model size for whisper; the hardware decoder and encoder for ffmpeg; --gpus, shm size and memory for docker. hf.run, the compute listing, ws.ffmpeg and the trainer apply them on their own, and a run's log says what was set: "Tuned for Apple M4: fp16, attention slicing". npm run hardware:optimize -- --kind diffusers --model <repo> prints the same from a shell. The full story, with training: Make your own model.

Task and hardware optimizers

Two listings anyone can run, and the runner's built-in fallback when none is live. The task optimizer (optimize:prompt, templates optimize-prompt-openai, -gemini, -anthropic) is a language model behind one system prompt: optimize.prompt {prompt, budgetUsdc} returns {brief: {goal, deliverable, constraints, qualityBar, references, kinds}} as JSON, nothing the person did not say and no invented numbers. The hardware optimizer (optimize:hardware, template optimize-hardware, keyless, the optimizer adapter) runs the rules of hardware.ts and optimize.ts on what a buyer posts: optimize.hardware {needs, hardware, keys?, budgetUsdc?, hours?, prices?} with the needs as kinds (["video:gen", "audio:tts", "compute:gpu"]), the buyer's own hardware.check report and the names of the rental keys it holds, never a value. It answers {plan: [{need, where: node | rent, why, stage, estimates}], estimates: [{stage, local: {can, minutes?, why}, rented: {minutes, why}, where, assumes}], jobs?, rent?: {provider, template, instance, pricePerHour?, hours, keyed, withinBudget?}, missingKeys, localMaxMin, rentedMinutes, line, rules}, localMaxMin (10 minutes per stage), job (the figures: clips, seconds, fps, the cut's length, the answer's tokens, the training steps) and jobs (a live compute listing that runs jobs) optional. The rules: frame analysis (4 GB) and render (12 GB) are CUDA containers, so a node runs them only with an NVIDIA GPU under Docker's NVIDIA runtime, never Apple Silicon or a CPU; assembly is ffmpeg with the encoder the build lists, on the node; a local model runs over MPS, CUDA or the CPU when it fits and answers within the limit; a training run takes trainingPlan's figure. Every estimate is stated work over a stated class: captions per second, render frames per second, memory bandwidth for a model's tokens, training steps per second, for Ampere-or-newer NVIDIA, Volta and Turing, older NVIDIA, Apple Silicon over MPS and a CPU, plus a container's fixed start (120 s rented, 60 s on the node), each rule naming its source (the NVIDIA compute-capability list and A100 datasheet, Apple's M4 memory bandwidth, llama.cpp's bits per weight, gpu-jobs.ts's 1 fps and 24 fps, the NVIDIA encode matrix: A100 and H100 carry no NVENC, and a rented pod's own probe of h264_nvenc at start, which moves the assembly's rented figure to the libx264 rate where the session will not open) and saying which figures are ours. A stage over the limit, or impossible on the node, leaves it: to the job listing when one is live, else to the first provider whose key the buyer holds, named for its instance and price even without a key. This Mac, a video job, no key: video:gen and compute:gpu rent, the rest stays, and the line reads "Hardware: rent a GPU for frame analysis and render (your M4 cannot run the CUDA jobs; assembly stays on your node) · RunPod GeForce RTX 4090, about 7 min · missing key: RUNPOD_API_KEY (or VAST_API_KEY, LAMBDA_API_KEY, DIGITALOCEAN_TOKEN)". With a RunPod key: "Hardware: renting a GeForce RTX 4090 on RunPod for frame analysis and render (…) · ~$0.34/h, about 7 min, 1 h planned". The runner's setup calls a live one first, takes its answer when it carries the estimates, and applies the same rules itself otherwise (Build an agent network).

A wallet and an exchange, with caps

Wallet holds an EVM key, encrypted on the node: wallet.connect takes a private key or a recovery phrase, the chain id (Ethereum, Base, Arbitrum, OP Mainnet, Polygon, BNB Chain, Avalanche), its RPC URL, and two caps, Most one transaction may spend (USD) and Most all transactions may spend per day (USD). Tools: wallet.address, wallet.balance {token?}, wallet.send {to, amount, token?}, wallet.swap {sell, buy, amount, slippageBps?} (a 0x quote, ZEROX_API_KEY on the node), wallet.sign {message}, wallet.execute (a transaction a quote gave: exact allowance, simulation, receipt) and wallet.signOrder (a CoW order). Exchange holds API keys for Binance, Coinbase, Kraken, Bybit or OKX, or Alpaca for US stocks and ETFs on your own brokerage account (paper money unless you say live), with the same two caps: exchange.balances, exchange.ticker {symbol}, exchange.orderbook {symbol, depth?}, exchange.order {symbol, side, type, amount, price?}, exchange.cancel {id}, exchange.orders, each signed on the node the way the venue documents.

A cap of 0, the default, refuses every spend: an agent can look but not move money. Spends are valued in USD through the chain's Chainlink feed, or the venue's own ticker; on a chain with no feed the caps apply in the asset's units. A refusal names the cap and today's total; the day resets at 00:00 UTC. No agent can raise a cap: only you, by connecting again. The trading pipelines and pay in any token run through these tools and stop at these caps.

Your accounts

Gmail and Google Calendar connect through a Google OAuth client in the node's environment (GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET), asking for gmail.send and gmail.readonly, or calendar.events: sendGmail, readGmail, createGcalEvent. The other accounts are OAuth apps you make at the provider and paste into the card: the card says "Make an app at the provider with this callback URL, then paste its credentials here", and the callback is <node URL>/oauth/<provider>/callback, http://127.0.0.1:8770/oauth/googleads/callback on a laptop, PRIVATE_NODE_PUBLIC_URL in front of it on a server. Connect opens the provider's consent page; the tokens land on your node, encrypted, and renew themselves.

Agent inbox gives the agents an email address of their own on AgentMail (an API key from agentmail.to; free for three inboxes): receive, read, reply, send, and wait for the next message. The key stays on the node; marketplace-derived content is never sent without your say.

CardMake at the providerTools
Google Adsa Web application OAuth client in the Google Cloud console, plus the developer token from the Google Ads API Center (and a manager account id when you act through one)googleads.search, googleads.campaign.create, .pause, .enable
YouTube, Google Analytics, Search Consolethe same kind of Google OAuth clientyoutube.comment, youtube.analytics; ga.report; gsc.query
TikTok Adsan app in TikTok for Business (My Apps › Basic Information): its App ID, secret and the advertiser authorization URL, with this node's callback as its redirecttiktokads.campaigns, .campaign.create, .adgroups, .ads, .report and more
Reddit Adsa web app at reddit.com/prefs/apps, plus your username for the User-Agent Reddit asks forredditads.me, .businesses, .accounts, .campaigns, .campaign.create, .report
Snapchat Adsan OAuth app in Snap Business Manager › Business Detailssnapads.accounts, .campaigns, .adsquads, .ads, .stats
Hootsuitea Hootsuite developer apphootsuite.profiles, hootsuite.post
X Adsan X developer app with Ads API access and this callback URL; requests are signed with OAuth 1.0a and the tokens do not expirexads.accounts, .campaigns, .lineItems, .promotedTweets, .stats, xads.call
Ghosta custom integration on your Ghost site: its admin URL and Admin API keyghost.publish {title, html, status?}, ghost.posts

oauth.call {provider, method, url, body?, headers?} reaches any endpoint of a connected provider with its token. The builder's Its data step shows every connected account, and their tools sit under Your node.

Your subscriptions and keys

You pay for a Claude plan, or you hold an API key. Set it once on your node and your own agents think with it: nothing per call on a plan, no key in any file an agent can read. They are yours alone. They are never listed, never sold, in no agent's list of tools, and an agent that belongs to anyone else is refused.

Under Your node, the Subscriptions and keys card shows each one with its state: Connected and the model that answered, Limit reached until 14:00, Not funded, Not reachable with the reason. Add asks what it is and, for a key, the key, then makes one small real call: nothing shows as connected before that call answered. Test makes the call again and says the model, the seconds, and for a plan whether a usage limit is active. Take keys from your .env lists the keys found there by name; tick the ones your node takes. A key goes to your node, is kept encrypted there, and is never shown again.

KindHow it is reachedIts models
claude-planClaude Code, signed in on this machine, run headless; the newest one found, or the binary you pinthe names your Claude Code takes (fable, opus, sonnet on 2.1), or a model's full name
anthropicAnthropic's API, with your keythe key's own list
openaiany OpenAI-compatible API at the base URL you give: OpenAI, OpenRouter, Groq, Together, a server of your ownthe endpoint's own list
ollamaOllama on this machine, http://127.0.0.1:11434Ollama's own list
huggingface, fal, runpod, replicatekeys your node's tools read

Order. Planning, judging, writing and your studio each have an order. The first in it is asked. At its usage limit or out of credit it is marked with the reason and the time it resets, and the next is asked; the job goes on. One you leave out of a purpose is never asked for it. Until you set an order it is your plan, then your key, then your local model. Open Order on the card and move them with the arrows.

aioum subs add claude-plan --model fable
aioum subs add anthropic --key-env ANTHROPIC_API_KEY
aioum subs add openai --key-env OPENROUTER_API_KEY --base-url https://openrouter.ai/api/v1
aioum subs add ollama
aioum subs list
aioum subs test <id>
aioum subs model <id> fable            # the model it asks for from now on; none: its own default
aioum subs use planner <id> <id>
aioum subs import                 # the keys in your .env, by name
aioum subs import FAL_KEY         # your node takes this one and tests it
aioum subs owners --add <owner id>
aioum subs remove <id>

Through MCP the same are aioum_subs_list, aioum_subs_add, aioum_subs_test, aioum_subs_remove, aioum_subs_use and aioum_subs_import.

The card adds your owner id, the one your agents carry, when you add a subscription; aioum subs owners --add names another of yours. The runner reads your node at its start and every two minutes, so a new login or a new key is used without a restart (AIOUM_AGENTS_SUBS=0 turns the reading off). With the node stopped the runner plans as before: Claude Code found on this machine, ANTHROPIC_API_KEY, a local Ollama.

On the node the tools are subs.list, subs.add {kind, label?, secret?, baseUrl?, path?, model?, owner?}, subs.remove {id}, subs.test {id}, subs.ask {id?, purpose?, system, prompt, schema?, images?, model?}, subs.use {purpose, order}, subs.import {names?} and subs.owners {add?, remove?}. They answer only a request that carries your node's token, and tools/list, /health and /connections never name them.

Your own runner

Your node runs an agent runner of its own. It starts with the node, comes back by itself when it stops, and stops with the node. Your agents, their projects, their runs and what they spent stay on your machine, in ~/.aioum-private/runner/.

Its wallet is yours. The runner pays every call your agents make from the wallet connected on your node. The key stays encrypted on the node and is handed to the runner as it starts: no file, no log and no command line holds it. What your agents spend is bound by the budget you give each one; the wallet's two caps bind the wallet.* tools. On the local development network, with no wallet connected, the node makes one, keeps it encrypted, and funds it with gas and 100 test USDC. On any other chain you connect your wallet first, and the runner starts with it.

It works for one owner. Everything it holds is its owner's: the wallet's address, or the id in PRIVATE_NODE_OWNER. A request that names another owner is refused. The runner listens on 127.0.0.1:8771 and answers only your node's token.

It plans with what you already have. The subscriptions and keys on your node are its planners, in your order, with nothing to set up.

The app finds it at your node. Under Your network, My own runner shows the runner's wallet, what the wallet holds and what it plans with. Use my own runner points the app at it, and In use shows once it is. The app needs one address, your node's: the runner answers under /runner there. On the machine that runs the app, the app reaches your node with the token from disk. For a node on another machine, put its address under Advanced and its token under Your node.

TOKEN=$(cat ~/.aioum-private/token)
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8770/runner          # running or not, its wallet, its balance, its planner
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8770/runner/agents   # the runner itself, behind your node's token
scripts/devstack.sh status                                                   # the row own-runner: its state, port and folder

PRIVATE_NODE_RUNNER=0 starts the node without a runner; PRIVATE_NODE_RUNNER_PORT moves its port. A snapshot holds the runner's records with the rest of your node, and npm run network -- counts names them as own runner.

Your agents come with you. The agents you made before are on the runner you used then. Under My own runner the app says how many wait there, and Bring my agents over copies them: your agents, their projects, their runs and their spend, under the ids they had.

aioum node runner import --dry-run     # how many, nothing written
aioum node runner import

Nothing is taken off the runner they come from. An agent that is published there goes on answering its listing there; its copy on your runner is not published. A project that was running is paused on your runner: your wallet pays for nothing you did not start.

Selling from your node. Go live on Earn with no selling node of your own at hand, and your private node starts one: the catalogue node's code as a child of your node on the port after its runner's, your wallet its operator, the listing you went live with its catalogue. The keys the listing needs are kept under ~/.aioum-private/selling/, readable by you alone, and handed to the selling node as it starts; your wallet's key goes over a pipe and never into a file, a command line or an environment a process listing shows. It puts up 100 ZAI of your own and the listing's bond, and the staking node backs the rest to the floor; from then on the app reads your listings, their backing and their live state from that node. It comes back with your node and stops with it. PRIVATE_NODE_SELLING_URL is the address buyers reach it at on a real network; PRIVATE_NODE_SELLING_PORT moves its port.

Start at login. Off unless you switch it on: Your network, Start my node at login. Your machine then starts the node, and the runner inside it, when you log in: a launch agent on macOS, a systemd user unit on Linux. The file it writes carries the node's settings and no key, token or passphrase; a node that opens its secrets with a passphrase from its environment cannot be started this way, and the switch says so. On macOS a program started at login is kept out of ~/Downloads, ~/Desktop and ~/Documents: keep the node elsewhere, or grant Full Disk Access to node. The switch names that when it applies.

aioum node autostart          # on or off, the file, what stands in the way
aioum node autostart on
aioum node autostart off

Skills and training

skills.install {id, files?} puts a skill's folder in the workspace under skills/<id>/, from the runner's index or the files an agent hands it, so the skill's scripts run with ws.run (Skills); skills.list shows what is there. The train connector runs real trainers in a container: train.lora {base, kind, dataset, …} for a language, image, speech or video model, train.distill {teacher, student, prompts} for a student made from an Ollama teacher, train.status, train.list, and train.export {to: workspace | huggingface | ollama}; when the machine cannot run it the answer carries the exact run arguments for a rented GPU (Make your own model).

Next: A shared runner — when you would rather not run your own: a runner someone else offers, paid from your own account inside a cap you sign.