Payout System
SignalNet uses a pool-normalized payout system to ensure fair distribution of rewards while preventing insolvency.
Overview
Each round has a fixed reward pool funded by the tournament creator. Payouts are computed off-chain, normalized against the pool, and committed on-chain as a merkle root. Users claim their payouts with merkle proofs.
Payout Calculation
Phase 1: Raw Scores
Each submission is scored independently:
Final Score = w_ic × IC + w_tc × TC + w_mmc × MMC
Default weights: IC 40%, TC 35%, MMC 25%. The high TC+MMC weight (60%) means signal uniqueness matters more than raw accuracy.
Phase 1.5: Diversity Penalty (Anti-Sybil)
After scoring, the engine computes pairwise Spearman correlation between every pair of submissions. If two submissions from different users have similarity above 85%, the later one receives a diversity penalty:
diversity_factor = max(0, 1 - max_similarity_to_any_other_submission)
adjusted_score = final_score × diversity_factor
Example: If your signal has 0.95 correlation with another user's signal, your diversity factor = 0.05, and your adjusted score is reduced by 95%. Identical signals (correlation = 1.0) receive zero payout.
This makes sybil attacks economically irrational — duplicate accounts earn almost nothing.
Phase 2: Raw Payouts
Raw Reward = Final Score × Stake × (PayoutMultiplier / 10000)
With PayoutMultiplier = 2500 (25%), a score of +0.10 on a 1,000 SIGNAL stake yields:
Raw Reward = 0.10 × 1,000 × 0.25 = 25 SIGNAL
For negative scores, the loss is capped:
Raw Loss = min(|Score| × Stake × 0.25, Stake × MaxLoss / 10000)
Phase 3: Pool Normalization
This is the critical step. Raw payouts are computed independently per user, so the sum of positive rewards could exceed the available pool.
Budget = Reward Pool + Total Slashed
If total positive rewards exceed the budget:
Scale Factor = Budget / Total Positive Rewards
Scaled Reward = Raw Reward × Scale Factor
All positive payouts are scaled down proportionally. Negative payouts (slashing) are not affected — they've already been capped at max_loss_bps.
Phase 4: Final Payout
If reward > 0: Payout = Stake + (Reward × Scale Factor)
If reward < 0: Payout = Stake - Loss
If reward = 0: Payout = Stake
Examples
Normal Round (budget sufficient)
Pool: 50,000 SIGNAL. Three participants:
| User | Stake | Score | Raw Reward | Payout |
|---|---|---|---|---|
| Alice | 1,000 | +0.10 | +25 | 1,025 |
| Bob | 2,000 | +0.04 | +20 | 2,020 |
| Carol | 500 | -0.08 | -10 | 490 |
Total positive rewards: 45. Budget: 50,000 + 10 = 50,010. Scale factor: 1.0 (no scaling needed).
Oversubscribed Round (budget insufficient)
Pool: 1,000 SIGNAL. Fifty participants all score well:
| Total Stake | Avg Score | Total Raw Rewards | Budget | Scale Factor | |
|---|---|---|---|---|---|
| 80,000 | +0.06 | 1,200 | 1,000 | 0.833 |
Every positive reward is multiplied by 0.833. A user who would have earned 30 SIGNAL now earns 25 SIGNAL. The pool is fully distributed but never exceeded.
Genesis Round (no slashing)
With max_loss_bps = 0:
- No tokens are slashed from negative scorers
- Budget = Reward Pool only (no slash supplement)
- Negative scorers still get their full stake back
- All rewards come purely from the pool
Design Decisions
Why normalize instead of first-come-first-served?
Without normalization, the last users to claim could find the contract empty. Pool normalization ensures every participant gets their fair share, computed before any claims begin.
Why include slashed tokens in the budget?
Slashed tokens are locked in the StakeVault. Rather than leaving them stranded, they supplement the reward pool. This creates a natural feedback loop: poor signals fund rewards for good signals.
Why not make it purely zero-sum?
A pure zero-sum system (losers fund winners) discourages participation — nobody wants to play a game where the expected value is zero. The reward pool creates positive expected value for skilled contributors, which bootstraps the network.
Why cap losses?
Uncapped losses would deter participation, especially from smaller contributors. The 25% cap ensures contributors can recover from a bad round without being wiped out.
Contract Flow
1. Tournament creator calls createRound() → deposits reward pool
2. Users call submitSignal() → stakes locked in StakeVault
3. After resolution, manager computes normalized payouts off-chain
4. Manager posts merkle root via postResults()
5. Users call claimReward(proof) → StakeVault transfers payout
6. Manager can reclaim unused pool tokens after claim deadline
Parameters
| Parameter | Default | Genesis |
|---|---|---|
| Payout multiplier | 25% (2500 bps) | 25% |
| Max loss | 25% (2500 bps) | 0% |
| Min stake | 100 SIGNAL | 100 |
| Max stake | 10,000 SIGNAL | 2,000 |
| Max models per user | 3 | 3 |