Deploying your own network

The system on a chain of your own, in the order the steps have to happen; the contracts and services deploy to any EVM chain. deploying.md has the longer notes on each step, Operating the network the running system, and Security review the hardening record for the code you are deploying.

1. Prepare the chain and the keys

Pick the chain (RPC_URL, CHAIN_ID). Fund a deployer key and keep it on a hardware wallet or behind a multisig: it is used once, in step 2, then hands everything to the Timelock. Every key the network signs, pays and lists with, where each comes from and its exact .env line: Keys to launch.

The deploy needs addresses on the chain or of your choosing: the chain's USDC (USDC_ADDRESS; a test network without one sets TESTNET_USD=1 and gets zUSD, the testnet's own dollar, which the deployer mints with npm run zusd -- mint <address> <amount>), the canonical Permit2 (PERMIT2_ADDRESS, 0x000000000022D473030F116dDEE9F6B43aC78BA3 on every chain), the chain's canonical ERC-4337 EntryPoint (ENTRYPOINT_ADDRESS), the ZAI token (ZAI_ADDRESS), the fee and slash sink (TREASURY_ADDRESS) and the DisputeManager arbitrator (ARBITRATOR_ADDRESS). Last, who signs gas sponsorships (SPONSOR_SIGNER_ADDRESS): the key of your sponsorship service. Without it the deploy skips the paymaster and users pay their own gas.

The token. On a real chain the deploy mints AIOUMToken, capped, with the whole initial supply to the treasury and the DAO able to mint up to the cap later: ZAI_INITIAL_SUPPLY and ZAI_CAP, both required, or ZAI_ADDRESS for a token deployed before. Nothing is open-mint outside the devnet. Tokens reach anyone the way the treasury hands them out: a sale, grants, a pool on a DEX, the staking pool. When the deployer is the treasury, the deploy fills the faucet's wallet and the staking pool at once (FAUCET_ADDRESS/FAUCET_ZAI, BACKER_ADDRESS/BACKER_ZAI).

The testnet faucet. npm run faucet gives 1,500 ZAI once per wallet, enough for the own stake and ten listing bonds. What stops a script from minting addresses and draining it: one drip per address ever; the address must already hold gas on the chain, which it needs to stake anyway and which the gas faucets gate by account; a proof of work per claim bound to the address, about a second of hashing each and hours for thousands; the claim signed by the address; a few claims per IP a day; and a budget of drips a day for the whole faucet, so a drain is bounded by the day. The private node claims for its wallet from the app's Stake tab, solving the work itself, so the wallet's key never leaves it.

2. Deploy the contracts

offchain/src/deploy.ts is env-driven. Setting DEPLOYER_PRIVATE_KEY or USDC_ADDRESS (or REAL_CHAIN=1) puts it in real-chain mode: no mock tokens, no demo minting, the canonical Permit2 and EntryPoint, and a post-deploy [verify] gate. See offchain/.env.example for every key.

cd offchain
RPC_URL=... CHAIN_ID=... DEPLOYER_PRIVATE_KEY=0x... \
USDC_ADDRESS=0x... PERMIT2_ADDRESS=0x000000000022D473030F116dDEE9F6B43aC78BA3 ZAI_ADDRESS=0x... \
ENTRYPOINT_ADDRESS=0x... TREASURY_ADDRESS=0x... ARBITRATOR_ADDRESS=0x... \
SPONSOR_SIGNER_ADDRESS=0x... \
npm run deploy

On a real chain the script refuses to run without TREASURY_ADDRESS and ARBITRATOR_ADDRESS (neither may default to the deployer) or a canonical ENTRYPOINT_ADDRESS (it never deploys a throwaway EntryPoint). USE_COMMIT_REVEAL_ARBITRATOR=1 deploys the open, staked commit-reveal jury as the DisputeManager arbitrator. With SPONSOR_SIGNER_ADDRESS set it deploys AIOUMVerifyingPaymaster, unfunded until step 7. DEPOSIT_FEE_BPS sets the conversion fee on deposits from other chains and assets (100 = 1%, the default; at most 1,000); the DepositForwarderFactory deployed with it is written as depositFactory and depositFeeBps. It finishes by writing offchain/deployments.json, the one file to publish straight away: the app, the SDK, the services and the subgraph all read it.

3. Hand ownership to the Timelock

Deploy a TimelockController with the guardian multisig as proposer and canceller, and AIOUMGovernor(token, timelock) with the proposer role, so proposals flow Governor → Timelock → contracts. Then transfer every governance contract to the Timelock:

cd offchain
DAO_OWNER=0x<timelock> TIMELOCK_ADDRESS=0x<timelock> CONFIRM_HANDOVER=1 npm run handover

TIMELOCK_ADDRESS pins the target: the script refuses a mismatch, 0x0 (which would freeze every setter forever), the deployer itself, or an address with no contract code. The transfer is single-step and irreversible: from then on no single key can change the rules an agent's budget depends on. Renounce the deployer's Timelock admin role so only the Governor can queue, then verify, in CI and at release:

TIMELOCK_ADDRESS=0x<timelock> npm run verify-deploy

It exits non-zero unless the Timelock owns every governance contract, the arbitrator and treasury are the configured addresses, Permit2 and the EntryPoint are canonical, and no accept-everything paymaster holds a deposit.

From here on, all wiring (setRewarder, setDisputeManager, setRewardAsset, setPaymentManager, setArbitrator) and every parameter change is a Timelock proposal.

4. Run the services

The services take the work off the person's plate: the node that serves paid calls, the facilitator and bundler that pay gas so an agent never holds ETH, the deposit service that turns what a person sends from any chain into USDC in their AI wallet, the indexer that answers an agent's own searches during a run, the agent runner behind the Deploy Agents pages, the relay, billing, TLS at the edge. One command stands all of them up on a server:

cp offchain/.env.example .env       # the chain, DOMAIN, the keys ([Keys to launch](./appendix/launch-keys.md))
scripts/deploy-network.sh install   # Foundry, Node 22, every package built, Caddy, mdbook
scripts/deploy-network.sh deploy    # contracts, catalogue, one unit per service, TLS, the app

deploy checks that the RPC answers for CHAIN_ID; deploys the contracts when deployments.json has no entry for that chain (step 2, from the same .env) and otherwise checks they have code and, with TIMELOCK_ADDRESS set, runs npm run verify-deploy; reads the machine (npm run hardware:check: CPU, RAM, GPUs, Docker and its NVIDIA runtime, Ollama) so a local model or a GPU job is listed only when the host meets what its template requires; builds the catalogue the node lists, every keyless template plus what each key in .env unlocks (npm run catalogue:bootstrap; MCP servers only with AIOUM_CATALOGUE_MCP=verified or all, and never one marked hostAccess, which an operator lists by hand with allowHost: true on the listing); writes one systemd unit per service (aioum-<name>; a pm2 process where there is no systemd) carrying only the settings that service reads, its secrets in a file of its own; writes the Caddyfile for DOMAIN, the app at the apex and node., indexer., agents., facilitator., bundler., deposits., backer., relay., billing. in front of the services, certificates automatic; builds the app with the NEXT_PUBLIC_* addresses; starts everything and waits on each service's own /live. It ends with the URLs, the services it left out, each with the key that starts it (on a real chain a service without its key never starts), and the listings skipped for lack of keys. Run it again after editing .env: only what changed restarts. status, logs <name>, update (pull, rebuild, deploy) and down round it out; DRY_RUN=1 prints every unit and the Caddyfile it would write. The private node is off by default (PRIVATE_NODE=1 with a passphrase runs one on the server); the monitor is started by hand.

The same script runs a single node for a provider joining a network that already exists: SERVICES=multinode with AIOUM_SERVICES_CONFIG pointing at the listings and AIOUM_INDEXER_URL at the network's directory, which the node announces itself to (Integrate your service directly).

What each unit carries, and the settings behind it:

ServiceStartProduction settings
Marketplace nodenpm run multinode (:8556) with the catalogue from npm run catalogue:bootstrapOperator key from a keystore or OPERATOR_PRIVATE_KEY: on a real chain one key serves every listing, holding ZAI for its stake and ETH for gas. AIOUM_NODE_TOKEN on /settle (generated once by the deploy when unset). Every upstream key from .env. Voucher store on persistent disk (AIOUM_NODE_DIR; AIOUM_STORE=sqlite to share one store between instances). Behind TLS. Monitor /health.
Facilitatornpm run facilitator (:8547)Fund the gas key (FACILITATOR_PRIVATE_KEY or RELAYER_PRIVATE_KEY). Set FACILITATOR_API_KEY, FACILITATOR_MAX_AMOUNT and FACILITATOR_RATE_MAX; FACILITATOR_TRUST_PROXY=1 behind a proxy. Run several on distinct keys behind the load balancer; it is non-custodial, so anyone can run one.
Bundlernpm run bundler (:8548)Fund BUNDLER_PRIVATE_KEY (or RELAYER_PRIVATE_KEY). Run several, like the facilitator. Keep the paymaster's EntryPoint deposit funded or sponsored operations revert.
Deposit servicenpm run deposits (:8551)Fund DEPOSITS_PRIVATE_KEY, the sweeper key that pays gas for every sweep and holds no user funds. Set LIFI_API_KEY for the route provider (LIFI_API_URL to point at another endpoint). Ledger on persistent disk (AIOUM_DEPOSITS_DIR). Loopback by default; AIOUM_DEPOSITS_HOST=0.0.0.0 only behind TLS, with the app's NEXT_PUBLIC_DEPOSITS_URL pointed at it. It serves every chain the provider delivers to your chain from; on a chain the provider does not deliver to, it says so at start and only USDC sent straight to a deposit address is credited.
Staking nodenpm run backer (:8552)Fund BACKER_PRIVATE_KEY: the ZAI the pool delegates behind listings (slashable like any delegation), a little USDC for the sample call it makes through each node, and gas. Set the reviewer: ANTHROPIC_API_KEY, an OpenAI-compatible endpoint (AIOUM_AGENTS_OPENAI_*), a local Ollama, or a judge listing in BACKER_REVIEW_SERVICE; without one it approves nothing. BACKER_MAX_PER_OPERATOR_ZAI caps what one operator gets; BACKER_RUBRIC is what the model reads. Ledger on persistent disk (AIOUM_BACKER_DIR). Loopback by default; AIOUM_BACKER_HOST=0.0.0.0 only behind TLS, with the app's NEXT_PUBLIC_BACKER_URL pointed at it.
Discovery indexernpm run indexer (:8549)The app's NEXT_PUBLIC_INDEXER_URL; behind TLS.
Shared runnerAIOUM_AGENTS_MODE=shared npm run agentsRuns the agents of people who sign in with their wallets; each person's account pays, within a cap they sign, and your fee per run reaches you through its listing (shared-runner template). AIOUM_AGENTS_KEYS_SECRET seals their spending keys at rest: keep it. AIOUM_AGENTS_URL is the address the listing names; behind TLS. Records on persistent disk (AIOUM_AGENTS_DIR); the skills index from npm run skills:index in AIOUM_SKILLS_DIR. A person's own runner needs nothing from you: it runs inside their private node.
Paid reviewerAIOUM_AGENTS_MODE=own AIOUM_AGENTS_REVIEWER=1 npm run agentsBonds ZAI (AIOUM_REVIEWER_PRIVATE_KEY, AIOUM_REVIEWER_BOND_ZAI) and reviews the listings the chain draws it for, paid in ZAI; a judge listing or a local Ollama scores the calls.
Planner listinga planner-* template in a node's services.jsonA marketplace listing that plans for assistants, paid per turn. Anyone may run one; the key stays with its operator.
Private nodesee Your private nodeExpose only over TLS: PRIVATE_NODE_HOST=0.0.0.0 behind Caddy, OS firewall closed to :8770. Keep encrypted, user-controlled backups of secrets.enc, memory.json and jobs.json.
Relaynpm run relay (:8553)AIOUM_RELAY_PRIVATE_KEY, a key of its own (it stakes and settles like a node); AIOUM_RELAY_TARGETS the catalogue node; AIOUM_RELAY_MARGIN_BPS.
Billingnpm run billing (:8550)SPONSOR_SIGNER_KEY (its address is SPONSOR_SIGNER_ADDRESS at deploy, so the manifest has a verifying paymaster), BILLING_TREASURY_KEY, BILLING_ADMIN_TOKEN.
TLSwritten by deploy from DOMAIN (deploy/Caddyfile is the hand-written shape)DOMAIN and *.DOMAIN pointed at the host, ports 80/443 open, every other port closed; certificates are automatic.
Monitornpm run monitor (by hand; not a unit yet)ALERT_WEBHOOK_URL for alerts, TIMELOCK_ADDRESS to watch governance, MONITOR_SCRAPE_TOKEN if /metrics is exposed.
The appnpm run build && npm start in dapp/NEXT_PUBLIC_APP_URL, the site's public address: the sitemap, robots.txt and link previews name pages by it. NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID from cloud.walletconnect.com, so phones pair by QR (without it only injected wallets connect, and the console reports the placeholder on every page).

5. Index

subgraph/docker-compose.yml is a complete graph-node + IPFS + Postgres stack. Fill subgraph/networks.json with the deployed addresses and start block from step 2, then:

cd subgraph
docker compose up -d
npm run codegen && npm run build
npm run create-local && npm run deploy-local

The subgraph serves history and analytics over GraphQL at http://localhost:8000/subgraphs/name/aioum/aioum-v2; point your own dashboards at it. The explorer console and the app's Activity tab read events from the RPC and the registry from the indexer; the Marketplace and the agent runner's mid-run searches read the indexer too, so run it (step 4) and set NEXT_PUBLIC_INDEXER_URL. The Graph's decentralized network is the alternative to self-hosting: same graph deploy, different --node.

6. Where your node answers

A listing on the chain says who sells, what, at what price and with what stake. It did not say where the node answers: the directory learned that from a seed list and from nodes that announced themselves, so each directory had a list of its own. Now the node writes its address on chain. EndpointRegistry holds one record per operator, written by that operator's key and by nobody else: the URL, and the fingerprint of the certificate the node serves. Every directory reads the chain first; the seed list is a cache for the first probe. A node with its address on chain is found by any directory anyone runs, and nobody decides who is listed.

No domain needed. A certificate authority issues certificates for names, which is the only reason a server usually needs one. Here the trust comes from the chain instead. At an https address on an IP, https://203.0.113.9:8546, the node makes a certificate for itself at first start, serves it, and writes its SHA-256 fingerprint on chain beside the address. Every client in the network, the agent runner, the directory, the relay and the staking node, reads that fingerprint from the chain and accepts the node's certificate only when it matches. Encrypted and authenticated by the operator's key, with no DNS and no certificate authority anywhere. deploy-network.sh does this when DOMAIN is unset: the node's address is this server's public IP, it listens on every interface, and AIOUM_NODE_TLS=self. The keys live in the node's data folder under tls/; a new certificate means a new fingerprint on chain, written at the next start. Only port 443 or the node's port needs to be open.

No address at all: behind a relay. A node on a machine at home should never write that machine's address, because a home address is where you live and the chain never forgets. So a node can dial out instead. Set AIOUM_NODE_TUNNEL to a relay's tunnel address, the record a relay writes on chain with its own certificate, and the node keeps one outbound connection to it, proves its operator key by signing the relay's nonce, and is reached at https://<operator>.<relay>:<port>, which is what it writes on chain. A buyer's bytes go to the relay and through the tunnel untouched: the TLS session is end to end to the node's own certificate, pinned from the chain, so the relay carries what it cannot read or alter, and the node's address is known to the relay alone. A relay on a bare IP is named <operator>.<a-b-c-d>.aioum, a form every client in the network resolves itself; a relay with a domain is <operator>.relay.example.com. Any relay (npm run relay) hosts tunnels on AIOUM_RELAY_TUNNEL_PORT, 8554, one public TLS port that also fronts its own API, and lists the nodes behind it at GET /tunnel. The app's "Where your node answers" card offers the relay way first and the direct address second, with what each one writes on chain said plainly.

A name is for browsers. A browser trusts certificate authorities only, so a node that people reach from a browser directly needs a name with a CA-issued certificate: point node.yourdomain.com at the server, run Caddy (deploy/Caddyfile), set AIOUM_NODE_PUBLIC_URL=https://node.yourdomain.com, and the node writes the name with a zero fingerprint, checked the ordinary way. The app reaches a pinned node through its own server for the health probe and the payment challenge, so the marketplace pages work either way.

The node needs AIOUM_NODE_PUBLIC_URL, the address other machines reach it at. It writes the record when it starts and leaves it alone while unchanged. On a real chain without it the node writes nothing and says so. The app shows the state under Earn, "Where your node answers": the saved address, what the chain holds, and whether they agree. Changing it there starts the node again with the new address.

A node's IP is visible to whoever resolves its name or reads its record, as every server's is, unless it sits behind a relay. Keep every other port closed, and the private node on loopback or behind its own name and token. If an address is attacked, stand the node up elsewhere, or behind a relay: it republishes at boot and every directory follows within a minute, stake and reputation untouched.

ENS and web3 names. An ENS name does not replace any of this. Agents reach a node by the record on chain, which already holds what a name would resolve to, with no mainnet dependency. ENS can be a label an operator adds later. It is not transport.

What is central and what is not. The chain holds listings, stakes, payments, addresses and certificates. The directory is a cache anyone can run (npm run indexer); the app reads one through NEXT_PUBLIC_INDEXER_URL, and the subgraph is the on-chain fallback. A relay is an operator like any other: anyone runs one, a node picks which, and a relay cannot read what it carries. Nothing else stands between a buyer and a seller.

7. Gasless accounts and budgets

AIOUMAccount, AIOUMAccountFactory and AIOUMVerifyingPaymaster were deployed in step 2. Your sponsorship service signs time-boxed sponsorships (validUntil / validAfter) for the operations you pay for; the paymaster rejects every other UserOp, so the deposit is spent only on what you signed, and rotating sponsorSigner invalidates every outstanding sponsorship. Fund the paymaster's EntryPoint deposit once the service is up, and let the monitor alert when it falls below MONITOR_PAYMASTER_MIN_DEPOSIT_WEI.

Session keys are hot, capped and expiring: the on-chain spendCap and validUntil bound what a leaked key can spend, revokeSessionKey ends one early, and the app keeps them in memory, never in localStorage. Say so in your own user documentation. Budgets and session keys has the details.

8. User data on private nodes

The private node's memory is personal context: what the agent has learned about its person, the secrets it holds for them and the jobs it runs for them. Treat it as user data. It is encrypted at rest by default, the person can export and delete it, and only the minimal, consented per-call inputs ever go to marketplace nodes. Its growth is bounded; all it asks is a regular backup on the same encrypted, user-controlled terms as secrets.enc, memory.json and jobs.json in step 4.

9. Keys

The node's key is not the owner's key. A node holds a hot signer key (OPERATOR_SIGNER_KEY) that settles sessions, writes the node's address and certificate, and proves the node to directories and relays. It can neither stake, register, take down nor withdraw: those stay with the owner's key, which names the signer on chain once (OperatorSigners) and otherwise never touches a server. deploy-network.sh does the split when OPERATOR_SIGNER_KEY is in .env: the owner's key registers the catalogue and names the signer in a run that exits before anything listens, and the unit runs with the signer alone. The private node does the same for the selling node it starts: a signer made once in its encrypted store, named by your wallet, and the wallet's key never in the process that serves buyers. A node compromised is worth its upstream keys and its transcripts, never its stake: the validator's split of signing and withdrawal keys.

Deployer: hardware wallet or multisig, used once in step 2, hands ownership to the Timelock in step 3 and renounces its admin role there. Facilitator and bundler relayers: funded hot keys that only pay gas and never hold funds, one per instance, rotated and rate-limited (step 4); the deposit sweeper is the same kind, and a forwarder pays only the treasury and the wallet, so it never holds user funds. Sponsor signer: held by the sponsorship service (step 7), rotated through the paymaster owner, a multisig whose only other job is keeping the EntryPoint deposit funded. Node operator: stakes and settles, the operator's own responsibility, capacity equal to stake. User session keys: hot, capped and expiring (step 7). Private-node passphrase: encrypts secrets and memory (step 8), user-held, never logged.

10. Security and operations

The full picture, keys, the pause, the runbook, is one page: Security and operations.

Four habits. Keep forge test, fuzz and invariant suites included, in CI, and extend the invariant handlers whenever you change payments, staking or disputes; a revenue-share or attribution split added to settlement changes the money path, so cover it with the invariant tests (escrow solvency, splits sum to the gross, no dust leak) before shipping. Keep the guardian multisig as Timelock proposer and canceller through bootstrap, and publish the milestones on which control widens to the full Governor. Run a bug bounty and keep an incident-response runbook. Leave the rate limits and in-flight caps on the facilitator and bundler in place; node capacity is bounded by stake, so a node cannot hold more client money than it can answer for.

Third-party code never runs on the host outside a container: a compute listing runs a buyer's job under docker run --rm with the listing's limits, and a registry MCP server is verified only inside a container with nothing of the host mounted (Operating the network).

Launch checklist

Before the first person hands an agent a budget on your network, every line is ticked.

[ ] Contracts deployed with canonical Permit2, the chain's USDC and the canonical EntryPoint (no mocks)
[ ] Owner = Timelock on every contract (npm run handover); deployer + Timelock admin renounced
[ ] npm run verify-deploy passes, and runs in CI/release
[ ] scripts/deploy-network.sh deploy ends with no FAILED line; nodes behind TLS; voucher store on persistent disk; /health monitored
[ ] Facilitator + bundler funded, hardened, several instances on distinct keys
[ ] Sponsorship service live; paymaster deposit funded; deposit alert set
[ ] Deposit service up: sweeper key funded, route provider key set, NEXT_PUBLIC_DEPOSITS_URL set; card partner keys where cards are taken
[ ] Staking node up: pool funded, a reviewer set, NEXT_PUBLIC_BACKER_URL set
[ ] hardware.json read at deploy; no hostAccess MCP server listed without allowHost; nothing third-party runs outside a container
[ ] Private node over TLS only; secrets/memory/jobs backed up (encrypted, user-controlled)
[ ] Subgraph deployed (addresses + start block); indexer up and NEXT_PUBLIC_INDEXER_URL set; monitor alerting to a webhook
[ ] App built with NEXT_PUBLIC_APP_URL and NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID set; /robots.txt, /sitemap.xml and a link preview checked
[ ] Session-key handling documented for users (hot, capped, expiring)
[ ] Bug bounty + incident runbook in place
[ ] Every node has `AIOUM_NODE_PUBLIC_URL` set, https, and its address on chain (`GET /nodes` on the indexer lists it under `onChain`, with its certificate fingerprint when it serves its own)
[x] Guardian/pause wired: the deploy sets `GUARDIAN_ADDRESS` on every pausable contract, `verify-deploy` checks it; governance decentralization milestones published

See also: Operating the network · deploying.md · SECURITY.md.

Next: Keys to launch — every key the network signs, pays and lists with, where each comes from, what it costs and its exact .env line.