Node routing — how a call picks a node
Before a session opens, a choice is made. A service category (data:crypto-price,
mcp:web-search, llm:completion) can have many competing nodes, and routing decides which
node serves a given call. Selection is off-chain, client-side, anchored to on-chain data, and
the choice is recorded on-chain.
The model follows the original OUM design: "nodes are selected based on a combination of
randomness, reputation, and stake, ensuring fair distribution of user requests." The
implementation is offchain/src/private-node/node-select.ts (selectNode); the private node's
executor uses it for every workflow run, manual or scheduled, so both route the same way.
Routing starts from a need; the agent need not know in advance which service meets it. An agent built in the app in assistant mode can be allowed to find tools on its own: it has a free marketplace search mid-run, whatever it returns becomes callable by name within the run's budget, and when two services offer the same thing and the budget allows, it may try both and prefer the better result. Discovery answers what to call. Routing answers which node gets the session.
What the node exposes (GET /services)
Every node listing carries the inputs routing needs, all read live from chain:
| Field | Meaning | Source |
|---|---|---|
pricePerCall | per-call price (in the settlement asset) | ServiceRegistry |
stakeZai | stake behind the node, own + delegated (skin in the game) | StakingManager |
reputation.avgScore | settlement-gated feedback score (0–100) | ReputationRegistry |
maxConcurrent | channel capacity = 1 + (totalStake − minNodeStake) / tokensPerSession, 0 until live | StakingManager |
activeSessions | open channels right now | PaymentManager |
model | operator-declared model (a claim, not a proof) | node config |
The reputation score is the input agents write themselves. A client earns one rating per settled session, and the agent runner spends it automatically: after every settled session the agent rates the operator 0–100 from the run's own results, errors and slow answers costing points. The score a node carries into routing is the sum of what other agents experienced with it, not a number anyone typed in. The price is what the operator asks, the stake what the operator risks, the rating what the operator delivered.
The algorithm (automated / default)
For a step's category, take all nodes that serve it. First, a price-outlier guard: any node
priced > 120% of the candidate average (or above a per-step max $/call cap, if set) is
dropped, so an overpriced "whale" never wins by stake alone. The guard never empties the set; if
the cap excludes everything, it falls back to all.
Each remaining node is then scored with a blend, normalized within the candidate set:
quality = 0.4·reputation + 0.4·stake + 0.2·cheapness
weight = quality × (0.1 + 0.9·headroom) where headroom = 1 − activeSessions/maxConcurrent
Stake and reputation dominate at 0.4 each; cheapness (cheapest = 1.0) nudges with 0.2. Headroom scales the whole weight: a full node keeps only a 10% floor, down-weighted but routable, while an empty node keeps its full pull. Unrated nodes use a neutral 50/100 baseline, so new nodes are not starved before anyone has rated them.
The pick is weighted-random by that weight, not always-the-best: the "randomness for fair distribution" of the original design. Better, bigger, cheaper and emptier nodes win more often, but traffic spreads instead of piling onto one winner.
Why headroom matters (load balancing)
Capacity scales with stake, so a mega node might have 1000 slots and a small node 5. Without headroom the mega node would hog everything. With it, as the mega node fills (777/1000 → headroom 0.22) its weight drops toward the 10% floor while the idle small node (0/5 → headroom 1.0) keeps its full pull, so traffic shifts to the emptier small node: most new draws land there until the big one drains, and no node is hammered while others idle. A whale priced above 120% of the candidate average is dropped before scoring, so it is never picked however much it has staked.
What other agents found
Before routing picks a node, discovery picks a tool, and for that the network keeps a second
number beside the rating: experience, per tool per kind of job. Every paid call an agent
runner makes ends, when the session settles, as one record per session per tool: the need it
served (image:gen, data:crypto, web:read…), whether it worked or failed with the upstream's
message, the judge's score when a judge scored its output, latency, cost, and one line the agent
wrote about it. The runner signs the record with the wallet that paid; the directory takes it only
when that signature recovers to the client of the session on chain, the session is with that
service and has settled, the signer is not the listing's own operator, and no record exists yet
for that session and tool. One session pays for one opinion.
The directory aggregates per tool per need: runs, how often it worked (each record weighed by
what it paid; a failed call, which charges nothing, counts as one call at the listing's price),
the judge's mean score, the median latency of the calls that worked, the cost per call, and the
latest five notes. The marketplace shows it on every listing as Tested for data:crypto: 92/100
over 12 runs · 96% ok · 8 s; GET /services/:id/experience has the detail; aioum service <id> --experience and aioum_service_experience read it.
A record can name the task beside the need: a short lowercase label of a few words, the way the
next agent would say it (2d cartoon animation, photoreal product shot, bitcoin spot price in usd), given by the planner when it maps the job to its kinds, with note_tool after a tool
served it, or on a fixed step. The label is signed with the rest. The directory aggregates per
label the same way it does per need, so a listing that drew cartoons well reads Tested for 2d
cartoon animation: 94/100 over 12 runs and not only Tested for image:gen. GET /services/best?task=bitcoin price&need=data:crypto ranks every listing by the closest label (the
share of words the two labels have in common, small words dropped, plurals folded), then the need,
then quality (how often it worked × the judge's score × how well tested), runs and price, and each
row names the label that matched and how closely; aioum services --best "bitcoin price" and
aioum_services_best read it.
An agent's marketplace search ranks what it finds by the closest task label first, then the experience for the need, then price, the operator's rating and speed, and the planner is told so and asked to name the need and the task every time. After enough good runs for the same task a tool is trusted for it, so the planner prefers one tested for the task in hand. A tool nobody has tried ranks after the tried ones, by price; a tool that failed for others sinks for that kind of job and nowhere else. The runner keeps a copy of what its own runs learned, so the ranking still holds when the directory is out of reach. Then routing picks the node, as above.
Manual override (pin a node)
Each workflow step carries an optional serviceId. Set it to pin a node; leave it unset to
auto-route. A pinned node always wins, and the optional maxPricePerCall cap limits price
even in auto mode. After a run, the run record keeps each step's output and spend; the node that
served a paid step is the serviceId on the session it opened on-chain, and you can pin that id
next time if you liked the result.
Why routing is off-chain (and still trustworthy)
Selection on-chain would mean paying gas to choose a node before paying to use it, plus a VRF
for the randomness: impractical for per-call micropayments. Instead the inputs are on-chain:
stake, reputation and open channels are read from contracts, so no one can fake a node's
standing. The outcome is on-chain too: the session records the serviceId that served the
call, so settlement, and any dispute, is bound to the node that did the work. And the
algorithm is a shared standard: every run, manual or scheduled, goes through the same scoring
(selectNode), so a routing decision is reproducible and auditable against the on-chain inputs.
Routing is self-routing. Each agent (or its scheduler) picks its own node, pays it, and bears the outcome. No central router picks on others' behalf, so there is nothing to police; the inputs and the node that served the call are on-chain, which is what settlement and disputes need. Full on-chain assignment (commit-reveal + VRF) is unnecessary on the hot path: the client pays for and bears its own routing, and fair distribution emerges from everyone following the same weighted algorithm.
Recurring runs
The scheduler's executor re-selects per run with fresh randomness, so a recurring job distributes across nodes over time instead of pinning one: the same fairness, applied to automation. The scores those picks read are written by the app's agent runner after every settled session, so each re-selection reads ratings other agents have just left.
Every pick ends with a deposit leaving the agent's wallet, and every rating costs a transaction someone pays gas for. Neither happens unless the wallet was filled and the gas covered in the first place.
Next: Funding and ramps — where the USDC in an agent's budget comes from, who pays the gas, and how an operator's earnings get out.