Sitowise
Protocol

Distribution

A round is decided off chain and settled on chain. The worker works out who gets what, and one payable call writes those amounts and delivers the ETH behind them in the same transaction. After that the value is on the node, and only its owner can move it.

What a round is

Value reaches nodes in rounds. A round has an off-chain half and an on-chain half, in that order.

  1. The worker decides the amounts. It reads which nodes are active and splits the round’s value across them. Nothing about that step needs the chain, and doing it in the database is what makes it cheap to run often.
  2. The distributor settles them. One call to creditBatch(uint256[] ids, uint256[] amounts) carries every id, every amount, and the ETH for all of them at once.
  3. The ledger records the round: a row in distributions with the mode and the node count, one row in credits per node, and each node’s cumulative figure increased by its amount. That is what the dashboard and the public API read.

The ledger half is a single database transaction on purpose. A half-applied round would leave the ledger claiming value it never credited, and the database enforces the rest: cumulative figures are monotonic by trigger, and withdrawn can never exceed cumulative by constraint. The ledger is a mirror, though, not the authority. The authority is the contract, and the two are reconciled against each other rather than trusted.

Rounds that have actually happened

The chart reads GET /api/distributions, the same public endpoint anyone can call, and buckets the rounds it returns. If nothing has been distributed in the window it says so rather than drawing a flat line at zero.

Credited value over time

No data yetNo distribution rounds have been recorded in the last 24 hours.

Last 24 hours0.000000 ETH0 rounds
Value credited per hour over the last day, or per day over the last week. Live data from the public API.

Settlement: one payable call

creditBatch is payable, and that is the whole design. The contract sums the amounts before it writes anything and refuses the call unless msg.value matches that sum exactly. A node balance therefore cannot exist without the ETH backing it sitting in the contract, and no later step is needed to make a credit real.

SitowiseFactory.creditBatch
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();

for (uint256 i; i < n; ++i) {
    uint256 id = ids[i];
    Node storage node = _node[id];
    if (node.owner == address(0)) revert BadInput();

    uint128 amt = uint128(amounts[i]);
    uint128 newBalance = node.balance + amt;
    node.balance = newBalance;
    node.totalReceived += amt;
    emit Credited(id, amt, newBalance);
}

outstanding += sum;
totalDistributed += sum;

Each node in the batch emits Credited(id, amount, newBalance): what it just received, and what it holds afterwards. That second figure is the one that matters, because it is withdrawable the moment the transaction confirms. There is no unlock, no claim window, no approval and nothing to request from Sitowise; the owner calls withdraw or withdrawAll from their own wallet whenever they want. See Withdrawing.

The batch is validated before any storage is touched, so a malformed round costs the distributor gas and changes nothing. An empty batch, mismatched array lengths, a zero amount or an id that was never minted all revert with BadInput; an amount above type(uint128).max reverts with AmountTooLarge, because balances are uint128 and a silent truncation would credit a node less than the ETH backing it.

outstanding rises by the same sum. It is what the contract considers owed to holders, and rescue can only ever touch the balance above it, so the number that records what was credited is also the number that stops the owner taking it back. Read Security model for the full analysis, and isSolvent() is the public view anyone can call to check the contract still covers every balance it has written.

Why the amounts are decided off chain

Splitting a round across active nodes is arithmetic. Doing it in a contract would mean publishing a rule the operator then cannot change, paying gas to evaluate it, and still needing an off-chain job to trigger it. Doing it in the worker costs nothing and keeps the chain doing the two things only it can do: hold the money, and record who it belongs to.

The credits themselves are not batched off chain to save gas. On this chain that would buy nothing. Measured at 0.0297 gwei with ETH around $2,450:

ActionGasPaid by
creditBatch, batch overheadAbout 30kThe distributor
creditBatch, per node in the batchAbout 8kThe distributor
withdraw, one nodeAbout 55k, roughly $0.004The node owner

At a sixty second tick that is roughly $3 a day of batch overhead plus about $0.42 a day per node, which is small next to the payouts themselves and is paid by Sitowise, not out of anyone’s balance. An earlier version of this protocol settled balances with signed off-chain messages to avoid exactly this cost. At these gas prices that complexity bought nothing and was removed, and the withdrawal path has no signature, no server and no expiry in it as a result.

Treasury mode and swaps mode

DIST_MODE decides where a round’s value comes from. treasury is the name of the mode in which Sitowise funds the round out of its own funds; there is no treasury contract, and the money for a credit is sent by the distributor in the creditBatch call itself either way.

treasuryswaps
Source of valueFunded by SitowiseRead from the hook’s SwapAccrued events for the period
Amount per roundSet by SitowiseWhatever the swaps actually produced
How it is credited and withdrawnIdenticalIdentical
Currently runningYesNot until pools are attached to the hook

The mode of every round is recorded with the round itself and returned in the mode field of GET /api/distributions, so the history says which source funded what. That is part of the public record rather than something you have to ask for.