— / How it works /

Every round is arithmetic you can re-run

This page is the whole mechanism, not a summary of it. If something here doesn't match what the engine actually does, the page is wrong and we want to know.

1 · Round lifecycle

A round has two halves separated in time, and that separation is the entire fairness argument. We commit to a block before it exists; later, that block's hash decides everything. Nothing in between is ours to choose.

Openclose block announced
Closethat block mines
Snapshotbalances at one block
Drawweighted, capped
PayMARSCOIN transfers

The round file is written to disk at open, before the close block exists. That published file is the commitment. Everything after it is derived, and every derivation is reproducible from public chain data.

2 · Where randomness comes from

There is no operator seed. The entropy is the hash of the close block, and the round bound itself to that block number before the block was mined:

// seed for round N
seed = keccak256( closeBlockHash ‖ roundId )

We can't influence a BNB Chain block hash, and we fixed the block number in advance, so we can't shop for a favourable one either. The winners are fully determined the instant that block mines — before we have done anything at all.

Why not commit-reveal

The obvious scheme is to publish keccak256(serverSeed) up front and reveal the seed later. We built that first and then threw it away, because it has a hole: it proves the seed wasn't swapped within a round, but not that the round was ever published. Seeing winners we disliked, we could kill the process, drop the round, and start again with a fresh seed.

A commitment you can decline to open is not a commitment. Seedless has no secret at all, so there is nothing to withhold. That's the whole reason it replaced commit-reveal here.

Why not Chainlink VRF

VRF is sound, but it bills per request. At a 30-minute cadence that's roughly 48 requests a day, forever, for randomness a block hash already provides at zero cost once the close block is committed ahead of time.

Why the hash is read off-chain

Solidity's blockhash() only reaches back 256 blocks. A design that reads the close-block hash on-chain therefore has a hard deadline to settle within — miss it and the draw becomes unrecoverable. We read the hash through eth_getBlockByNumber instead, which has no such limit. Same ungrindable entropy, no deadline.

3 · Which block gets counted

The close block is public the moment a round opens. If it were also the snapshot block, it would be trivially gameable: buy at close−1, sell at close+1, collect a ticket you never really held. So the close block doesn't get snapshotted. It selects one:

snapshotBlock = closeBlock − ( closeBlockHash mod lookbackBlocks )

lookbackBlocks is announced when the round opens, so the range is known. Which block inside it counts is not knowable until the close block mines — and by then that block is already in the past, so nobody can react to it. Holding across the whole window is the only way to be certain of a ticket.

Verified uniform Over 20,000 simulated block hashes the selected block landed inside the announced range every time, spread across it with a maximum bucket deviation of 4.9% from uniform.

4 · How balances are read

Every balance in a snapshot is read with a single Multicall3 call pinned to the snapshot block. Pinning is what makes the snapshot atomic — a transfer landing mid-read cannot cause a wallet to be counted twice or zeroed.

The candidate set comes from indexing Transfer logs from the token's deploy block, but balances are never derived from those events. That distinction matters:

  • Event replay is cheaper, and it is what most implementations do.
  • It silently drifts on reflection-style tokens, where balanceOf is computed from an index rather than stored — common in this corner of the market.
  • Reading balanceOf at a pinned block is exact by construction, whatever the token does internally.

5 · Eligibility & weighting

Prizes are for people who traded the token. Two filters enforce that.

eligible = balance ≥ MIN_ENTRY
           and address ∉ EXCLUDED
           and address has no contract code
weight   = min( balance, cap )

Contracts are excluded automatically

Liquidity pools, bonding curves, routers, lockers, bridges and exchange deposit contracts all hold large balances, and none of them is a trader. Rather than maintain a list of them — which is always out of date, and always missing the one that matters — any address with contract code is barred from the draw.

Why the blanket rule instead of a list On a live dry run against a real BNB Chain token, before this rule existed, a PancakeSwap USDT pair won a prize slot outright. On the same token the scan found 16 contracts eligible, several capped at the maximum 5% share each. A predecessor project that missed just its bonding curve had that one address win roughly 92% of draws.

Smart-contract wallets — multisigs, account-abstraction wallets — are caught by the same rule. If you want those eligible, ALLOW_CONTRACT_WINNERS=true turns the filter off. Most projects should leave it on.

Plain wallets you exclude by hand

Deployer, treasury, dev, marketing, airdrop and payout wallets are usually ordinary wallets, so no automatic rule catches them. They go in excluded.txt, one per line with a comment saying why. npm run preflight scans the live token before launch and tells you what's still eligible that shouldn't be.

The whale cap, solved rather than applied

Weighting purely by balance lets one wallet dominate the winner feed, so a single wallet's weight is capped at a fixed share of the draw. The subtlety is that capping removes weight, which shrinks the denominator, which lifts the capped wallet's share back up.

Applied in one pass against the pre-cap total, a 5% cap measured 6.12% of the weight actually used. The cap is therefore solved to a fixed point instead, so the configured number means what it says. Measured after the fix: 4.99%.

Why weight by balance at all

Equal odds per wallet sounds fairer and is the opposite. It pays anyone who splits one balance across many wallets, which costs a few cents in gas. Weighting by balance makes splitting pointless by construction. Simulated over 600,000 winner slots:

ActorShare of slotsResult
Whale holding 23% of eligible float4.03%cap holding (configured 5%)
Honest holder, 100,0000.71%baseline
Sybil: same 100,000 split 100 ways0.74%1.012× payout — splitting gains nothing
500 dust wallets below the minimum0.00%excluded

6 · The draw

Entries are sorted by address so the draw reproduces regardless of map iteration order. Winners are drawn without replacement — one wallet cannot take two prizes in a round.

// per winner slot i
r_i    = keccak256( seed ‖ i )
ticket = r_i mod remainingWeight   // modulo-bias rejected
winner = entry whose cumulative interval covers ticket

Draws use rejection sampling rather than a bare modulo, so the low end of the range isn't very slightly favoured. Prizes are split by a fixed schedule, and integer-division dust goes to first place rather than being stranded — the payouts always sum to exactly the pool.

7 · Payout

Winners are paid by sequential MARSCOIN transfers from a hot payout wallet. Sequential, not parallel: parallel sends sharing a nonce is the classic way to get replacement-underpriced errors and silently dropped payouts.

The round file is written before each transaction is awaited, and any winner already carrying a transaction hash is skipped on re-run. If the process dies mid-payout, restarting resumes — it does not double-pay.

Prizes arrive whole MARSCOIN taxes pool trades, not wallet-to-wallet transfers. We verified this against live transfers on BNB Chain: single-leg transfers between externally-owned accounts, no tax leg. A 1,000 MARSCOIN prize arrives as 1,000 MARSCOIN, not 970.

8 · Verify a round yourself

Every round is published in full. You need nothing from us but the file and any BNB Chain node.

  1. Fetch the round: /api/round/<id>. It contains the close block, its hash, the snapshot block, the seed, every eligible balance and every winner.
  2. Check the hash is real. Read that block from any node and compare hashes. This is the step that can't be faked from the file alone.
  3. Check the snapshot block was derived, not chosen. Recompute closeBlock − (closeBlockHash mod lookbackBlocks).
  4. Check the seed. keccak256(closeBlockHash ‖ roundId).
  5. Re-run the draw over the published snapshot and compare the winners.
  6. Optionally confirm the balances themselves by reading balanceOf at the snapshot block — that catches a snapshot that was tampered with rather than mis-drawn.

The engine ships this as a command, and it does steps 2 through 5 against the live chain:

npm run verify // last round
npm run verify -- 42 // a specific round

9 · Parameters

These are the knobs. Live values for a running deployment are shown on the home page.

Round lengthROUND_MINUTES
Winners per roundWINNERS
Minimum balance to enterMIN_ENTRY
Max share of draw per walletWHALE_CAP_BPS
Snapshot lookback rangeSNAPSHOT_LOOKBACK_PCT
Prize size, in dollarsprize.txt
Prize split across winnersPRIZE_SPLIT
Blocks behind head before actingCONFIRMATIONS
Never eligibleEXCLUDED_ADDRESSES

The prize is set in dollars per round and converted to MARSCOIN at the market rate when the round closes, so it tracks whatever the operator decides that round rather than a number frozen at boot. If the payout wallet can't cover the target, the prize is reduced before the draw — the figure published in the round file is always the figure paid, never an advertised amount that couldn't be honoured.

Round length is converted to a block count using measured block time, not a hardcoded constant. BNB Chain block time has changed materially over the chain's life — at the time of writing it measures around 0.45s, where a hardcoded 3s would make a "30-minute" round run roughly six times too long.

10 · Limits & failure modes

Things this design does not solve, stated plainly.

LimitWhat it means for you
Payouts are off-chainThe draw is verifiable, but sending the prize is a transaction we make. Nothing forces it on-chain. If the payout wallet is empty or the process is down, prizes stall.
The prize pool is pro-cyclicalTax is the only funding source. Quiet trading means small pools. There is no treasury underwriting it, and a raffle funded purely by volume has no floor.
MARSCOIN is not oursIts vault, its rules. It can change how or whether it pays SPCXB, and we would find out the same way you would.
ReorgsHandled by staying a set number of confirmations behind the head before treating a block as final — not by assuming they don't happen.
RPC dependenceSnapshots need an endpoint that serves eth_getLogs reliably. A degraded provider delays a round; it does not change who wins, because the close block is already fixed.
It's a memecoinThe mechanism being fair says nothing about the token being a good idea. Only hold what you can afford to lose.