Limits
One wallet may hold at most 25 nodes. The cap is in the contract, not in the website, and this page explains what it is for, what bounds it, and what it honestly cannot do.
The cap
if (_owned[to].length >= maxPerWallet) revert WalletLimit();The check runs before any state is written, so an over-cap mint costs the relayer gas and changes nothing. It is checked against the address the node would be created for, which is the address that sent the payment. The current value is a public read:
cast call $FACTORY "maxPerWallet()(uint256)"
cast call $FACTORY "MAX_PER_WALLET_CEILING()(uint256)"
cast call $FACTORY "nodeCountOf(address)(uint256)" $WALLETThe owner can change maxPerWallet with setMaxPerWallet, but only within a hard constant the contract carries and no call can move:
function setMaxPerWallet(uint256 v) external onlyOwner {
if (v == 0 || v > MAX_PER_WALLET_CEILING) revert BadInput();
maxPerWallet = v;
emit MaxPerWalletChanged(v);
}MAX_PER_WALLET_CEILING is 100. Raising maxPerWallet affects future mints only; lowering it below what a wallet already holds does not remove anything, it simply stops that wallet getting more. Nodes are never destroyed by an admin setting, and every change emits MaxPerWalletChanged.
Why there is a cap at all
Three reasons, in order of how much they actually matter.
withdrawAllhas to fit in a block. The sweep loops over every node the caller owns, so the cost of that one transaction grows with the cap. That is why the ceiling exists as a constant rather than a setting: an unbounded cap could push the sweep past the block gas limit and strand a wallet that had accumulated too many nodes. Per-nodewithdrawalways works regardless, so even then nothing would be lost, but a holder should never have to discover that.- Distribution cost scales with node count. Every active node is an entry in every round, and during the launch period every node is funded by Sitowise. An unbounded number of nodes behind a single wallet turns a launch into a drain.
- It is a speed bump. It raises the cost of trivially accumulating a large share of nodes from one address.
Other limits
| Limit | Value | Enforced by |
|---|---|---|
| Nodes per wallet | 25 | The contract, in mintFor |
| Ceiling on that setting | 100, and the owner cannot exceed it | The contract, MAX_PER_WALLET_CEILING |
| Node price | Exactly 0.02 ETH per node, no more and no less | The watcher. Payment happens outside the contract, so anything but the exact amount is held for review rather than turned into a node. |
| One payment, one node | A payment transaction hash can back exactly one node | The contract, paymentRefUsed, reverting RefAlreadyUsed |
| Single credit amount | Below 2128 wei, because a node balance is a uint128 | The contract, in creditBatch, reverting AmountTooLarge |
| Credit backing | A round’s msg.value must equal the sum of its amounts | The contract, reverting ValueMismatch |
| Withdrawal amount | The node’s whole balance. There is no partial withdrawal and no amount argument. | The contract |
| Public API requests | Per IP, per minute: 120 for /api/stats and /api/me, 60 for the other public reads, 20 for /api/auth/nonce, /api/auth/verify and /api/nodes/sync | The application. See the API overview. |
The rate limiter counts in the memory of the process that serves the request, so the effective quota is per instance and a cold start resets it. It exists to stop one machine scraping the whole ledger, not as a security boundary. Every response carries x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset, so a client never has to guess where it stands.
Pausing
The owner can pause node creation with setPaused(true). While paused, mintFor reverts with IsPaused and nothing else changes.
Withdrawals have no pause switch. No code path in the contract lets anyone stop an owner withdrawing their own balance, which is the asymmetry described on Security model. A withdrawal can only fail in ways that name themselves: NotNodeOwner if the caller does not own the node, BadInput on a zero destination, NothingToWithdraw when the balance is zero, and TransferFailed if the destination rejects the ETH.