Sitowise
Payouts

Balances

A balance is real ETH, held by the factory contract and attributed to one node id. It is not a promise, a ledger entry or a figure waiting to be approved. Only the node owner can move it, and they move it themselves.

The three numbers

Every node carries three figures, and all three live in the contract. One call returns them together.

NumberWhat it isMoves whenCan decrease
balanceETH held for this node and withdrawable right nowA credit adds to it, a withdrawal empties itYes, to zero, and only by the owner withdrawing
totalReceivedEverything ever credited to the nodeA distribution round credits itNo, it only advances
totalWithdrawnEverything ever paid out against the nodeA withdrawal confirmsNo, it only advances
SitowiseFactory
function nodeInfo(uint256 id) external view returns (
    address nodeOwner,
    uint64  createdAt,
    uint256 balance,
    uint256 totalReceived,
    uint256 totalWithdrawnByNode
);

The relationship between them holds by construction: balance == totalReceived - totalWithdrawn, because the only two operations that exist are a credit that raises the first two and a withdrawal that zeroes the balance while raising the third by the same amount.

Where the ETH actually is

Balances are created by creditBatch(ids, amounts), which the distributor calls. It is a payable call, and msg.value must equal the sum of amounts exactly, otherwise it reverts with ValueMismatch before a single balance is touched.

SitowiseFactory.creditBatch, the check that matters
uint256 sum;
for (uint256 i; i < n; ++i) {
    uint256 amt = amounts[i];
    if (amt == 0) revert BadInput();
    if (amt > type(uint128).max) revert AmountTooLarge();
    sum += amt;
}
if (sum != msg.value) revert ValueMismatch();

That is the whole point of doing it this way. A balance cannot exist without the ETH behind it, because the transaction that creates the balance is the transaction that delivers the money. There is no window in which the contract owes something it does not hold. A zero amount, a length mismatch, an empty batch or an unknown node id all revert with BadInput, so a malformed round credits nothing rather than crediting part of itself. How rounds are funded is on Distribution.

Balances for a wallet

The chart reads GET /api/nodes/:address for any address you enter. Each bar is one node: the filled part is still on the contract, the outlined part has already been withdrawn.

Value credited per node

No data yetEnter a wallet address to plot the nodes it holds.

0 nodes0.000000 ETH creditedFilled: on contract. Outlined: withdrawn.
Live from the public API. No address is filled in by default, and nothing is drawn until you enter one.

Who can move a balance

  • You, by calling withdraw or withdrawAll yourself. The contract compares node.owner with msg.sender and nothing else, so there is no message, signature or approval anybody else could hold that would let them move it.
  • The distributor can add to a balance and can decide not to add to it again. It has no path that lowers one and no path that withdraws.
  • The contract owner can pause minting, change roles and take free funds, meaning contract ETH attached to no node. Node balances are outside what that role can reach, by the bound described below.

Outstanding, free balance and solvency

outstanding is the sum of every node balance. It goes up by exactly what creditBatch delivers and down by exactly what a withdrawal sends, so it is always the total the contract owes to nodes.

Everything above that is free: freeBalance() returns address(this).balance - outstanding, and rescue(to, amount) reverts with ExceedsFree for anything larger. That single bound is the reason the owner key cannot reach holder money under any sequence of calls. It is enforced by a fuzz invariant, and the invariant is verified by mutation: widening the bound to the full balance makes the test suite fail. The rest of what the owner can and cannot do is on Security model.

isSolvent() returns whether the contract holds at least outstanding. Under normal operation it is always true, because credits arrive with their own funding. It is worth reading anyway: it is the one call that checks the invariant the whole design rests on, and it costs nothing to make.

Nothing on this page depends on the operator staying online. The balance is in the contract, the check is a public view function, and the withdrawal is your own transaction. See Withdrawing.

Units and rounding

Everything is wei. Balances are stored as uint128 and passed to the contract as exact integers, so nothing is rounded anywhere in the accounting path. An amount above type(uint128).max is rejected rather than silently truncated, because a truncated credit would put a balance on a node that the delivered ETH did not match.

Display rounds to six decimal places and truncates rather than rounding up, so the number shown is never more than you can actually withdraw. If a node holds a very small balance, the display can read 0.000000 while the underlying figure is non-zero; the withdrawal path uses the exact figure regardless, and takes all of it.

What the API returns, field shapes rather than real figures
{
  "id": 1,
  "chainNodeId": "1",
  "balanceWei":    "0",   // node.balance,       withdrawable now
  "cumulativeWei": "0",   // node.totalReceived, ever credited
  "withdrawnWei":  "0"    // node.totalWithdrawn, ever paid out
}

Wei figures are strings in JSON on purpose. They routinely exceed what a double can hold exactly, and a client that parses them as numbers will silently lose precision. Parse them with a big-integer type. The full response, including the mint transaction, is on GET /api/nodes/:address.

Verifying a balance

All three numbers are on chain, so none of them has to be taken on trust from this website or the API.

cast
# owner, createdAt, balance, totalReceived, totalWithdrawn for one node
cast call $FACTORY "nodeInfo(uint256)(address,uint64,uint256,uint256,uint256)" $NODE_ID

# combined withdrawable balance across every node of a wallet
cast call $FACTORY "balanceOfOwner(address)(uint256)" $YOUR_ADDRESS

# what the contract owes to nodes, what is unattached, and whether it is covered
cast call $FACTORY "outstanding()(uint256)"
cast call $FACTORY "freeBalance()(uint256)"
cast call $FACTORY "isSolvent()(bool)"

The same reads are available in the Read Contract tab of the explorer, which is why the contract is verified there. Addresses are on Addresses and every function is listed on Factory interface.

Several nodes at once

balanceOfOwner(who) adds up the balances of every node a wallet owns, which is the total the dashboard shows. Each node still holds its own balance underneath, and withdrawAll(to) empties all of them into one destination in a single transaction. See Withdrawing.