Stake & Delegate

Every listing runs behind an operator whose ZAI stands as security for the answers it sells. That stake is what lets a person hand an agent a budget to buy from operators it has never met: if a node serves a bad answer and loses the dispute, the stake pays. Two people put that ZAI up. The operator locks ZAI against their own node, which makes it active and able to accept sessions at all. A delegator holds ZAI, runs nothing, and backs someone else's node for a share of what its buyers spend. The network's staking node is a delegator anyone can run: a pool of ZAI that reviews a listing and backs it, so a seller goes live with 100 ZAI of their own. All are paid in the settlement assets clients pay with, USDC, USDT or ETH, never in ZAI; the token is only slashable security. Everything lives in one contract, StakingManager: staking, delegating, claiming, leaving, and what a slash does to each of you.

Wallet rather than terminal: the Stake tab of the app's ZAI page (/zai?tab=stake) backs an operator, claims rewards and unbonds with a cooldown by calling exactly these functions. The node behind the stakes: Run a node; where the reward money comes from: Payments.

A node is keyed by its operator address. There is no "node ID" — nodes[operator] is the node.

Key concepts

Six parameters govern everything. Self-stake is the ZAI an operator locks against their own node; at least minSelfStake (100 ZAI) makes them an operator who can list. Delegated stake is ZAI anyone else locks behind that node, tracked as shares of the node's delegated pool rather than a fixed token amount, so a slash dilutes every delegator pro-rata instead of falling on whoever withdraws last. A node is live, accepting sessions, once self-stake plus delegated stake reaches minNodeStake (10,000 ZAI): delegation is what switches most nodes on. Commission is the operator's cut, in bps and capped at maxCommissionBps (20%), taken from the delegators' portion of rewards. Capacity is how many sessions the node may hold open at once, maxConcurrent(operator), derived from total stake above the node floor as 1 + (totalStake - minNodeStake) / tokensPerSession with tokensPerSession at 100 ZAI, so delegated stake both switches a node on and grows it. Unbonding means staked ZAI never leaves instantly: it enters a cooldown of unbondingPeriod (7 days) before you can claim it.

The listing bond is separate from stake. Every listing holds listingBond (100 ZAI) in the registry while it is up: pulled from the operator when the listing is registered, locked for bondUnbonding (14 days) once it is taken down, returned on withdrawBond after that. A listing put back before the clock runs out keeps the bond it holds, so up, down and up again locks the same ZAI throughout. Stake bounds the money a node holds; the bond prices the listing itself, so one stake cannot put ten thousand entries on the marketplace for nothing. Every one of these numbers is set by governance: StakingManager.setParams and ServiceRegistry.setListingBond, by the Timelock after handover; a change reaches nodes and listings from then on.

The deploy script sets these values. Read them live from the contract rather than hardcoding them:

cast call $STAKING "minSelfStake()(uint256)"        --rpc-url http://127.0.0.1:8545
cast call $STAKING "minNodeStake()(uint256)"        --rpc-url http://127.0.0.1:8545
cast call $STAKING "tokensPerSession()(uint256)"    --rpc-url http://127.0.0.1:8545
cast call $STAKING "unbondingPeriod()(uint256)"     --rpc-url http://127.0.0.1:8545
cast call $STAKING "maxCommissionBps()(uint16)"     --rpc-url http://127.0.0.1:8545
cast call $REGISTRY "listingBond()(uint256)"        --rpc-url http://127.0.0.1:8545
cast call $REGISTRY "bondUnbonding()(uint64)"       --rpc-url http://127.0.0.1:8545

1. Operator: self-stake a node

If you run a node, this step makes you an operator; backing switches the node on, unless you stake the whole node floor yourself. Staking is a two-step ERC-20 flow, approve the StakingManager to pull your ZAI, then stake, and the node software performs it on boot, so these commands check what it did, or stake by hand before it starts.

// StakingManager.sol
function stake(uint256 amount) external;            // pulls `amount` ZAI, adds to self-stake
function setCommission(uint16 bps) external;        // bps <= maxCommissionBps

cast

# Approve, then stake 12,000 ZAI of your own (the whole node floor plus spare capacity;
# 100 is enough to list and let backers bring the total to 10,000).
cast send $ZAI "approve(address,uint256)" $STAKING 12000ether \
  --rpc-url http://127.0.0.1:8545 --private-key $OPERATOR_PK

cast send $STAKING "stake(uint256)" 12000ether \
  --rpc-url http://127.0.0.1:8545 --private-key $OPERATOR_PK

# Confirm you are an operator, whether the node is live, and its capacity.
cast call $STAKING "isOperator(address)(bool)"      $OPERATOR --rpc-url http://127.0.0.1:8545
cast call $STAKING "isActiveNode(address)(bool)"    $OPERATOR --rpc-url http://127.0.0.1:8545
cast call $STAKING "maxConcurrent(address)(uint256)" $OPERATOR --rpc-url http://127.0.0.1:8545

isActiveNode is selfStake >= minSelfStake && selfStake + delegatedStake >= minNodeStake: delegated stake counts toward going live, not only toward capacity. With 10,200 ZAI self-staked you get 1 + (10200 - 10000) / 100 = 3 concurrent sessions, and 50,000 ZAI allow 401; with 100 of your own and 9,900 delegated you are live with one.

ethers (this is what the node runtime does on boot)

The node software makes the same two calls in offchain/src/node/stake.ts, idempotently, so a restart never stakes twice. It tops your own stake up to a target: AIOUM_SELF_STAKE (or a listing's stakeZai), else 10,000 when your wallet holds that much, else 100:

const need = target - selfStake;                       // nothing to do at or above the target
if ((await aioum.allowance(operator, deployments.staking)) < need) {
  await (await aioum.approve(deployments.staking, need)).wait();
}
await (await staking.stake(need)).wait();
// isOperator(operator) === true; isActiveNode(operator) once own + delegated >= minNodeStake

Set your commission

Commission is the slice of the delegators' reward portion the operator keeps: your pay for the delegated stake that raises your capacity. Set it any time after the node exists; it applies to rewards notified from then on.

# 10% commission (1000 bps). Reverts if bps > maxCommissionBps.
cast send $STAKING "setCommission(uint16)" 1000 \
  --rpc-url http://127.0.0.1:8545 --private-key $OPERATOR_PK

The network's staking node

Most operators never find their first delegators by hand. They list with 100 ZAI of their own and press Request stake on the Earn page's go-live step (or run aioum stake request <serviceId>, or ask Claude with aioum_stake_request). The network's staking node, a pool of ZAI anyone can run (npm run backer, port 8552; the pool's key in BACKER_PRIVATE_KEY), reviews the listing for real and, on a yes, delegates the rest. Every request goes through the same states, each with a timestamp: requested → reviewing → approved or rejected → backing → live or failed; withdrawn later, if the listing degrades. The review:

  1. The listing is on-chain, active, and served by a node that lists its tools.
  2. The operator holds the own floor, minSelfStake.
  3. No slash against the operator and no penalty on their record; no open dispute.
  4. The price sits within the gallery's band for its category: a quarter of the cheapest to four times the dearest listing there (its family when the category is new).
  5. The pool can cover the gap under its cap per operator (BACKER_MAX_PER_OPERATOR_ZAI, 20,000 ZAI) and holds enough.
  6. One sample call through the operator's node with the template's sample arguments, when the listing comes from the gallery.
  7. A model reads the listing against the rubric (offchain/src/backer/rubric.md): does what it says, real, useful, priced like its neighbours, honest; never fake engagement, account trading, spam, fraud or nonsense. Its reasons are stored with the request and shown on the card and in the CLI. The model is the node runner's: ANTHROPIC_API_KEY, an OpenAI-compatible endpoint, a local Ollama, or a judge listing on the marketplace named in BACKER_REVIEW_SERVICE and paid per review. Without one, no request is approved.

On a yes the pool delegates the gap to minNodeStake plus two sessions of headroom (BACKER_HEADROOM_SESSIONS × tokensPerSession), makes one paid call through the node, and the listing is live: with 100 ZAI of the operator's own, the pool adds 10,100, the node stands at 10,200 ZAI total with capacity for three sessions. The pool earns the delegator share like any delegator and is slashed like one. Every minute (BACKER_REVIEW_MS) it re-checks each node it backs: three failed health checks in a row, a dispute raised against the operator, or a slash, and it starts undelegating with the reason recorded; the card reads Backing withdrawn and says why. GET /requests/:id and GET /requests?operator= on the staking node hand every state, check, reason and transaction to the app, the CLI and Claude.


2. Delegator: stake on someone else's node

Choosing the node is the whole decision, and the marketplace gives you an honest basis: after every settled session the agent that bought from a node rates its operator on-chain, 0–100, from the errors and slow answers it saw, so ratings come from agents and real results, not from the operator. Back the ones whose ratings were earned. The mechanics are approve-then-call as for self-staking, except you pass the operator's address, and the node must already exist (nodes[operator].exists).

function delegate(address operator, uint256 amount) external;
cast send $ZAI "approve(address,uint256)" $STAKING 5000ether \
  --rpc-url http://127.0.0.1:8545 --private-key $DELEGATOR_PK

cast send $STAKING "delegate(address,uint256)" $OPERATOR 5000ether \
  --rpc-url http://127.0.0.1:8545 --private-key $DELEGATOR_PK
await (await aioum.approve(deployments.staking, amount)).wait();
await (await staking.delegate(operatorAddress, amount)).wait();

Shares, not balances

Internally your delegation is minted as shares of the node's delegated pool, priced against the pool's current backing assets:

minted = (totalShares == 0) ? amount : amount * totalShares / delegatedStake

After a slash each share is worth fewer ZAI, so the same ZAI deposit buys more shares: you enter at the post-slash price, never paying for a loss the pool took before you arrived. Your position's value in ZAI right now (reflecting any slashing):

cast call $STAKING "delegatedAssetsOf(address,address)(uint256)" $OPERATOR $DELEGATOR \
  --rpc-url http://127.0.0.1:8545

Edge case: if a pool is fully slashed (shares outstanding but zero backing assets), new delegate calls revert with "pool wiped; await exits" until the remaining delegators exit.


3. Multi-asset rewards & claiming

Here the money arrives. Every time a node settles a session, PaymentManager forwards the delegator share (10% of the gross at the default split, set per service by the operator) into StakingManager.notifyReward(operator, asset, amount) (Payments → fee split). Rewards are credited only after the contract has received the asset, so accrued rewards are always backed by tokens it holds.

notifyReward splits each reward:

// inside notifyReward(operator, asset, amount)
operatorBase   = amount * selfStake / totalStake;     // operator's stake-weighted slice
delegatorsBase = amount - operatorBase;               // the rest belongs to delegators
commission     = delegatorsBase * commissionBps / BPS;// operator's commission off the top
toDelegators   = delegatorsBase - commission;         // distributed across delegated shares

So the operator accrues operatorBase + commission, and delegators share toDelegators pro-rata by share (MasterChef-style accRewardPerShare). If a node has no delegators, the operator keeps the whole reward.

Rewards accrue per asset: USDC, USDT and ETH (asset address(0)) accumulate separately, because buyers pay in whichever they hold. Claim each asset you want.

Check what's claimable

# Pending delegator reward (includes both settled + un-settled accrual).
cast call $STAKING "pendingDelegatorReward(address,address,address)(uint256)" \
  $OPERATOR $DELEGATOR $USDC --rpc-url http://127.0.0.1:8545

# Operator's claimable balance for an asset.
cast call $STAKING "operatorClaimable(address,address)(uint256)" \
  $OPERATOR $USDC --rpc-url http://127.0.0.1:8545

Claim

function claimDelegatorRewards(address operator, address asset) external; // delegator
function claimOperatorRewards(address asset) external;                    // operator
# Delegator claims their USDC rewards from a node.
cast send $STAKING "claimDelegatorRewards(address,address)" $OPERATOR $USDC \
  --rpc-url http://127.0.0.1:8545 --private-key $DELEGATOR_PK

# Operator claims their USDC rewards. Use the zero address for ETH.
cast send $STAKING "claimOperatorRewards(address)" $USDC \
  --rpc-url http://127.0.0.1:8545 --private-key $OPERATOR_PK
cast send $STAKING "claimOperatorRewards(address)" 0x0000000000000000000000000000000000000000 \
  --rpc-url http://127.0.0.1:8545 --private-key $OPERATOR_PK

Claiming pays out in that asset directly: an ERC-20 transfer for tokens, raw ETH for address(0). Your delegated ZAI stake is untouched; only accrued rewards are withdrawn.


4. Unbonding (cooldown before withdrawal)

Staked ZAI never leaves instantly, because a stake that could vanish the moment a dispute was raised would secure nothing. Operators and delegators alike initiate an unstake, wait the unbondingPeriod (7 days), then withdraw.

Operator

function initiateUnstake(uint256 amount) external; // moves amount into cooldown
function withdrawUnstaked() external;              // claims all matured cooldowns
# Begin unbonding 2,000 ZAI. Reverts if it would leave you with a non-zero stake below minSelfStake.
cast send $STAKING "initiateUnstake(uint256)" 2000ether \
  --rpc-url http://127.0.0.1:8545 --private-key $OPERATOR_PK

# ...wait out unbondingPeriod (on the local development network, fast-forward Anvil):
cast rpc evm_increaseTime 604800 --rpc-url http://127.0.0.1:8545   # 7 days
cast rpc evm_mine --rpc-url http://127.0.0.1:8545

# Withdraw everything that has matured.
cast send $STAKING "withdrawUnstaked()" \
  --rpc-url http://127.0.0.1:8545 --private-key $OPERATOR_PK

initiateUnstake enforces remaining == 0 || remaining >= minSelfStake: you stay an operator or wind fully down, never stuck with a sliver of own stake below your floor. Taking your own stake down may drop the node's total under minNodeStake; it then stops taking new sessions until it is backed again.

Delegator

The delegator path is symmetric but takes the operator address. You request an asset amount, converted to shares at the current ratio (post-slash you withdraw the slashed value, never the nominal original):

function initiateUndelegate(address operator, uint256 amount) external;
function withdrawUndelegated(address operator) external;
cast send $STAKING "initiateUndelegate(address,uint256)" $OPERATOR 2000ether \
  --rpc-url http://127.0.0.1:8545 --private-key $DELEGATOR_PK

# ...after unbondingPeriod...
cast send $STAKING "withdrawUndelegated(address)" $OPERATOR \
  --rpc-url http://127.0.0.1:8545 --private-key $DELEGATOR_PK

Initiating an unstake or undelegate first settles your pending rewards into your claimable balance, so unbonding never forfeits them; claim them with the functions in §3. Multiple in-flight cooldowns are fine: withdraw* sweeps every matured request at once.


5. Slashing risk

The other side of the reward, and the reason the stake means anything to the agents buying from the node. If a node loses a dispute, DisputeManager calls slash(operator, amount, beneficiary) (only it can). The loss is applied pro-rata across self-stake and the delegated pool:

// StakingManager.slash(operator, amount, beneficiary)
fromLive = amount > live ? live : amount;      // live stake first; any shortfall comes out of the operator's pending self-unbonding queue, newest entries first
fromSelf = fromLive * selfStake / totalStake;  // operator eats their share
fromDel  = fromLive - fromSelf;                // the pool eats the rest
n.selfStake      = selfStake - fromSelf;
n.delegatedStake = del - fromDel;            // shares UNCHANGED → every delegator diluted pro-rata

For an operator the loss comes straight out of self-stake, and if the node's total drops below minNodeStake, or their own below minSelfStake, the node stops being live and cannot open sessions until re-staked or backed again. Unbonding is no escape: if live stake cannot cover the slash, the remainder comes from the operator's pending self-unbonding requests, newest first, up to the full amount. Delegators' own undelegation queues are not reached. For a delegator the loss arrives as dilution: total shares stay constant while the pool's backing ZAI shrinks, so each share is worth less. There is no opt-out; backing a node means sharing its slashing risk in proportion to your share of the pool, and delegatedAssetsOf (§2) always shows the live value. Accrued rewards are not touched; only staked ZAI principal is at risk. Slashed ZAI goes to the dispute beneficiary (the wronged client or treasury), not burned.

So: delegate to operators whose ratings agents earned for them, size your delegation to the loss you can absorb, and spread it across several nodes so no single node's misbehaviour touches more than a slice.


Function reference

ActionCallerFunction
Self-stakeoperatorstake(uint256 amount)
Set commissionoperatorsetCommission(uint16 bps)
Begin self-unbondoperatorinitiateUnstake(uint256 amount)
Withdraw matured self-unbondoperatorwithdrawUnstaked()
Claim operator rewardsoperatorclaimOperatorRewards(address asset)
Delegateanyonedelegate(address operator, uint256 amount)
Begin undelegatedelegatorinitiateUndelegate(address operator, uint256 amount)
Withdraw matured undelegatedelegatorwithdrawUndelegated(address operator)
Claim delegator rewardsdelegatorclaimDelegatorRewards(address operator, address asset)
Position value (ZAI)viewdelegatedAssetsOf(address operator, address delegator)
Pending rewardviewpendingDelegatorReward(address operator, address delegator, address asset)
Node capacityviewmaxConcurrent(address operator)
Is node activeviewisActiveNode(address operator)

The slash in §5 happens on nobody's say-so. A dispute is decided by an open, staked jury, and sitting on that jury is the other way ZAI earns.

Next: Paid reviews — bond ZAI behind your own judgement: review listings, and be paid by the listers.