Overview
SLUICEBOX is a mining game on Robinhood Chain. The founder forwards the Pons creator fees from $SLUICE to a public contract, the SluiceVault, by plain ETH transfer (every transfer is a public tx on the vault address); anyone can call harvest() to split what arrived 80/20 into the pool. Every hour the settler (a server key with narrow powers) takes a budget from the pool and buries it as nuggets inside four voxel claims. Where the nuggets are is fixed by a secret seed whose hash is written on-chain before the round starts and revealed after it ends.
You paste a Robinhood Chain address, stake a claim, and your browser searches for SHA-256 proofs. One accepted proof digs one voxel; which voxel is taken from the proof's own hash, so nobody can aim. Dig a voxel that holds a nugget and the vault pays that nugget's ETH to your address. The rest of the budget ("dust") is split by credited work at the end of the round.
No login, no signature, no wallet connection, no token needed. Holding $SLUICE changes nothing about mining.
- Round
- 3,600 s, aligned to the UTC hour. Round id
r = floor(unix_seconds / 3600). - Claims
- 4 per round: North Bar (0), Bench (1), Bedrock (2), Tailrace (3). Each 24 × 24 × 12 = 6,912 voxels.
- Budget split
- 75% nuggets, 25% dust. Budget =
min(poolFree, maxRoundBudget), max 2 ETH at start. - Fee in
- Founder → vault by public ETH transfer.
harvest()(anyone) splits whatever arrived: 80% pool, 20% infra. The pool share can only be raised, never lowered. - Difficulty
- 20–48 leading zero bits, per claim, retargeted every 30 s.
- Payout threshold
- Nuggets pay instantly. Dust pays when your balance reaches 0.0005 ETH; below that it carries over.
Challenge
Before hashing you ask the server for a challenge. It is a short-lived ticket bound to your address and your claim.
POST /api/challenge
{ "wallet": "0x…40 hex", "claim": 0..3 }
→ { "round": 497520, "challenge_id": "0x…64 hex", "wallet": "0x…", "claim": 2,
"difficulty": 24, "issued_at": 1792000000000, "expires_at": 1792000030000, "max_proofs": 8 }
- TTL 30 s. After
expires_ata proof for this challenge is rejected withexpired. Ask for a new one before it runs out; the miner keeps up to 8 tickets so a late proof still counts. - 8 proofs per challenge. The ninth is rejected with
challenge_full; ask again. - Difficulty is the claim's current difficulty at issue time and is frozen for that ticket.
- Errors:
paused(server paused, retry in 30 s),no_open_round(between rounds, retry in 1 s),rate_limited(HTTP 429 withRetry-After),bad_wallet,bad_claim.
Message & validity
The hashed message is 72 bytes: a 64-byte prefix (exactly one SHA-256 block, so a GPU can precompute the midstate) followed by an 8-byte nonce.
prefix = challenge_id ‖ wallet ‖ claim ‖ 0x00 × 11 (64 bytes)
message = prefix ‖ nonce_le64 (72 bytes)
hash = SHA-256(message)
valid ⇔ leading_zero_bits(hash) ≥ difficulty
The wallet is inside the hash, so a proof found for one address is worthless for any other (another_wallets_proof). The claim is inside the hash, so a proof cannot be moved to a different claim (wrong_claim). challenge_id is 32 random bytes from the server, so work cannot start before the ticket exists.
Expected hashes per proof at difficulty d is 2^d: about 16.8 million at 24 bits, 1.07 billion at 30 bits.
Proof
POST /api/proof
{ "round": 497520, "challenge_id": "0x…", "wallet": "0x…", "claim": 2,
"nonce": "000000000a1b2c3d", // the 64-bit nonce as 16 hex chars (number order, zero-padded)
"hash": "0000003f…64 hex" } // SHA-256(message), lowercase hex
→ { "accepted": true, "credit": "16777216",
"dig": { "claim": 2, "voxel": 3811, "x": 15, "y": 6, "z": 14 },
"nugget": null | { "wei": "12100000000000000", "index": 3811 } }
→ { "accepted": false, "reason": "below_target" }
The server rebuilds the 72-byte message from the ticket it issued and recomputes SHA-256 itself; your hash field must equal it (hash_mismatch). The nonce is a 64-bit unsigned integer: it is written in the JSON as its 16-character hex value, and placed in the message as 8 little-endian bytes.
Dig placement
Which voxel your proof digs is read from the proof's own hash, after acceptance, so neither you nor the server can aim:
voxel = uint64(hash[8:16], big-endian) mod remaining(claim)
remaining(claim) is the number of voxels still standing in that claim at the moment of acceptance, and voxel is an index into the claim's ordered list of still-standing voxels. The order is the natural order index = y × 576 + z × 24 + x (y = depth 0–11, z and x = 0–23) with dug voxels removed. Every accepted proof is logged in order, so the whole hour is replayable from the log.
Bytes 0–7 of the hash are mostly zeros (that is the difficulty), which is why placement uses bytes 8–15.
Credit
Every accepted proof credits 2^difficulty hash-equivalents to your address for the round, regardless of what it dug. Your dust share at the end of the round is:
dust_share = your_credit ÷ round_credit × dust_budget (dust_budget = 25% of the round budget)
Rejected proofs credit nothing. Credit is per round; it does not carry over, but unpaid dust balance does (see payouts).
Difficulty retarget
Each claim has its own difficulty, re-evaluated every 30 s and clamped to 20–48 bits. The goal is that a claim is roughly dug out by the end of the hour, no matter how many miners are on it:
targetProofsPerSecond = max( remainingVoxels / max(remainingSeconds, 60), 0.2 )
nextDifficulty = clamp( round( log2( observedHashrate / targetProofsPerSecond ) ), 20, 48 )
observedHashrate is the server's estimate from accepted proofs on that claim over the last window (Σ 2^difficulty ÷ seconds). More miners on a claim → higher hashrate → harder → each miner digs fewer voxels. That is the crowding mechanic: a busy claim is dug out fast but the nuggets are shared among many; a quiet claim is slow but yours. New challenges carry the new difficulty; tickets already issued keep theirs. Dry and preview rounds use the same rule.
Rounds & claims
At every hour boundary the settler runs this sequence:
- Settle the previous round: compute dust shares, queue dust payouts, send
revealRound(r−1, seed). - Harvest: split any ETH that was forwarded to the vault since the last harvest (anyone may also call
harvest()at any time). - Budget =
min(poolFree(), maxRoundBudget). If it is 0 the round is dry. - Draw a seed: 32 random bytes. Derive the nuggets (below).
- Commit:
commitRound(r, commitment, budget). The budget moves from the pool into the round. The vault refuses a budget abovepoolFree()and refuses round ids that do not increase.
24 hours after a reveal, anyone may call closeRound(r): whatever of the budget was not paid returns to the pool. If a round is never revealed (the settler lost the seed), anyone may close it 7 days after its commit; the whole budget returns to the pool. Payouts for a round are possible from its commit until its close (so nuggets pay during the hour and dust settles after the reveal).
- Claim geometry
- 24 (x) × 24 (z) × 12 (y, depth).
index = y × 576 + z × 24 + x, 0 ≤ index < 6,912. - Nuggets per claim
K = 3 + (r mod 3), so 3, 4 or 5.- Nugget budget
floor(budget × 7500 / 10000), split equally: each claim getsfloor(nuggetBudget / 4).- Minimum nugget
- 0.0005 ETH. If the split would make a nugget smaller, that claim gets fewer paid nuggets (the positions still exist as pyrite).
- Exhausted claim
- When all 6,912 voxels are dug the claim is "worked out": proofs for it are rejected with
claim_exhausted. Re-stake another claim.
Nugget derivation
Everything about the nuggets of round r follows from the 32-byte seed. All hashes are keccak-256 over abi.encodePacked bytes; uint32(h) means the low 32 bits of the hash read as a number (the last 4 bytes, big-endian; the same as Solidity's uint32(uint256(h))).
Positions
j = 0, positions = []
while positions.length < K:
pos = uint32( keccak256( seed ‖ uint64(r) ‖ uint8(claim) ‖ uint32(j) ) ) mod 6912
if pos not in positions: positions.push(pos)
j += 1
Nugget i of a claim is positions[i] (i = 0 is the largest). Duplicates are skipped by moving to the next j; i only counts accepted positions.
Sizes
B = claim nugget budget (wei)
u_i = uint32( keccak256( seed ‖ "size" ‖ uint64(r) ‖ uint8(claim) ‖ uint32(i) ) )
j_i = 8000 + floor( u_i × 4000 / 2^32 ) // jitter in [8000, 12000)
W_i = j_i × 2^(K−1−i) // geometric, halving each step
amount_i = floor( B × W_i / Σ W ) // integer wei
amount_0 += B − Σ amount_i // rounding remainder to the largest
if amount_(K−1) < 0.0005 ETH: redo with K−1 (down to 0) // extra positions are pyrite (0 wei)
With this split the largest nugget is always at least 40% of the claim's nugget budget, and the amounts add up to exactly B. The "size" tag is the 4 ASCII bytes 73 69 7a 65.
The verify box on the ledger runs exactly this code in your browser against any revealed round.
Commitment
commitment = keccak256( abi.encodePacked( uint64 round, bytes32 seed ) ) // 8 + 32 = 40 bytes
Written on-chain by commitRound before the round opens; checked on-chain by revealRound, which refuses any seed that does not hash to the commitment and can only be called once per round. Because the positions are a pure function of (seed, round), once the commitment is on-chain the server cannot move, add or remove a nugget. After the reveal anyone can recompute every position and every size and compare them with the payouts that were made.
What the commitment does not prove: that the seed was drawn fairly. A dishonest settler could grind seeds before committing. The round's budget and the number of nuggets are fixed by the rules, so grinding can only change where nuggets sit, and digs are placed by your proof hashes, which the settler does not control. See risks.
Payouts
- Nuggets
- Queued the moment the voxel is dug. The settler sends
payout(round, [to…], [wei…], kind = 0)in batches every 60 s. You see the tx hash in the feed and on the ledger. - Dust
- Computed at round end from credit, added to your balance. When the balance reaches 0.0005 ETH it is sent with
payout(kind = 1); below that it carries over to the next round. - Cap on-chain
- For every round,
Σ paid ≤ round.budgetis enforced by the vault. The settler cannot pay more than what was committed. - Failed push
- If your address rejects ETH (a contract without
receive()), the amount is recorded asowed[you]; callpull()on the vault to collect. - Recipients per tx
- At most 200 per
payoutcall.
Reject reasons
A rejected proof returns { "accepted": false, "reason": "…" } with one of these exact strings:
duplicateThis (challenge_id, wallet, nonce) was already accepted. Each nonce counts once.
expiredThe challenge's 30 s TTL passed before the proof arrived. Ask for a new challenge.
challenge_full8 proofs were already accepted for this challenge. Ask for a new one.
unknown_challengeThe server never issued this challenge_id (or restarted and forgot it). Ask again.
round_closedThe round in the proof has ended and is settling or settled. Only the open round accepts proofs.
wrong_roundThe round field does not match the round the challenge belongs to.
below_targetThe hash has fewer leading zero bits than the challenge's difficulty.
hash_mismatchThe server's own SHA-256 of the rebuilt message differs from your hash. Nonce, address or claim does not match the ticket.
another_wallets_proofThe wallet field differs from the one the challenge was issued to. Proofs cannot be transferred.
wrong_claimThe claim field differs from the one in the challenge.
malformedMissing field, bad hex, nonce not 16 hex chars, hash not 64 hex chars, or an address that is not 20 bytes.
settlingThe hour boundary is being processed. Retry in a second.
claim_exhaustedEvery voxel of this claim is already dug. Stake another claim.
When the pool is empty: dry rounds
If poolFree() is 0 at the hour boundary, or before the vault exists at all (preview mode), the round is dry: budget 0, nothing is committed on-chain, the nugget positions are still derived from a seed so the claims look and dig the same, but every nugget is pyrite (0 ETH) and dust is 0. Work in a dry round counts for the season leaderboard only. The ledger and the game mark dry rounds everywhere.
The pool refills only from harvest(), which splits the creator fees the founder has forwarded to the vault, fees that $SLUICE trading on Pons has actually generated. There is no manual pace and no top-up promise: the vault pays only what Pons fees have filled. If the founder stops forwarding, rounds go dry; nothing else changes.
Anti-abuse
- Dedup on
(challenge_id, wallet, nonce); the wallet is in the hash input, so share theft is impossible. - Challenge rate limits per IP and per wallet; HTTP 429 with
Retry-After. The miner backs off up to 30 s. - Wallets must be valid 20-byte addresses and may not be the vault, the zero address or a precompile.
- Proofs are verified serially per challenge; the settler only ever pays what the vault's per-round cap allows.
Your CPU
Mining uses your CPU. You choose the share (10–100%); the miner runs max(1, round(cores × share)) web workers, each hashing 65,536 nonces per batch with a hand-written SHA-256. It stops the moment the tab closes or you press stop; nothing runs in the background. An optional WebGPU miner can use your GPU instead; every GPU hit is re-verified in JavaScript before it is sent. Expect fan noise and battery drain at high shares.
Not financial advice. Unaudited contracts. Nugget positions are committed on-chain before each round and revealed after it. Mining uses your CPU (you pick the %), stops when the tab closes; the vault pays only what Pons fees have actually filled; dry rounds pay nothing.