Appearance
How it works
This page covers the architecture and design of JPool's incentive campaigns: how campaigns are structured, how solvency is guaranteed, and how the pieces fit together.
Components
- Campaign: one incentive campaign, tied to a specific target. Created by an owner.
- Reward stream: one reward token, with its own vault and payout rules. A campaign can pay in up to 5 different tokens.
- Position: one participant's stake in a campaign (how much they hold and what they've earned).
- Stake held: the number the delegate reports for a participant. Rewards are computed from it. It's an attested amount, not tokens the campaign locks up.
Roles
Three roles, deliberately kept separate so no one can overreach:
| Role | Can do | Cannot do |
|---|---|---|
| Admin | Set a protocol fee, treasury, and global pause | Touch any campaign's funds |
| Owner | Create/fund/close a campaign, add reward tokens, extend a campaign horizon, reclaim unused surplus, choose/rotate a delegate | Report how much stake anyone holds |
| Delegate | Report each participant's stake held, close exited positions | Move funds or take over a campaign |
Admin handover is two-step (propose, then accept) to avoid handing control to a wrong or dead address. The owner picks a delegate and can swap it at any time.
Trust model
The model splits responsibility cleanly:
- It guarantees, on-chain: every campaign stays solvent, and earned rewards can only be claimed by the participant who earned them.
- It trusts the delegate for: reporting stake held by participants honestly and for defining what the target actually is.
Solvency and ownership of earned rewards are enforced on-chain, so neither one depends on trusting anybody. The only open question is whether the delegate's attestations are accurate, and that is the single thing participants trust the delegate for.
A delegate is just a public key, so anything that can sign can fill the role: an off-chain service, or another campaign signing for itself. A validator running its own campaign, for example, can act as its own delegate and attest the stake delegated to it. That stays safe even though such a delegate is self-interested, because the on-chain guarantees do not care who the delegate is: no delegate can move funds or take back rewards a participant has already earned. The worst a dishonest one can do is report the wrong stake.
So participants choose which delegate to trust, and that trust is strictly bounded. The delegate's reports shape future earning, but they can never cost a participant the rewards already earned.
Solvency guarantee
Every stream tracks one number: the reserve. It's everything currently owed, which includes rewards earned but not yet claimed, plus future rewards already promised, plus the fee on top.
The rule is: the vault must always hold at least the reserve.
Three mechanisms keep it true:
- Reserve before you promise. A fixed campaign reserves a participant's whole future reward the moment their stake is reported (and refuses the update if the vault can't cover it). A proportional campaign reserves emission as it happens, out of the pre-funded pot.
- Always round in the vault's favor. Every division rounds so the campaign never promises a fraction more than it has. Leftovers stay in the vault as reclaimable change, never as a shortfall.
- Verify at runtime. After any payout the campaign re-reads the vault and reverts the whole transaction if the balance ever dropped below the reserve threshold.
Whatever the owner over-funded can be reclaimed as unused surplus. This applies only to the genuinely unowed part (the vault minus the reserve), so reclaiming can never touch a participant's earned rewards.
Emission schedules
A reward rate doesn't have to be constant. Each stream carries a schedule of up to 12 steps. Rewards can incrementally increase, decrease, or change either way in phases. A flat rate is just a one-step schedule.
A schedule has a start, and the campaign pays nothing for the time before it. This makes adding a new reward token to a running campaign safe: the new token only pays from the moment it's added forward, never retroactively.
Time can be measured in seconds or slots. The owner choses it when creating a campaign.
Multiple reward tokens
A campaign can pay out in up to 5 reward tokens.
- The first token is the campaign's core. It can be either fixed (a guaranteed rate on each participant's stake) or proportional (a fixed pot split among participants in proportion to their stake).
- Every token added afterward is automatically proportional: a bonus pot beside the core. A campaign can carry only one fixed reward, so a second guaranteed-rate token has to run as its own fixed campaign on the same target.
The everyday shape is a fixed core plus one or two proportional bonuses. When the campaign horizon is extended, all tokens extend together: the fixed core re-reserves each participant's reward for the new window, and every proportional bonus has its budget topped up to cover it. Each token keeps its own vault.
Fees
A protocol fee (a percentage set by the admin) is taken on each claim and sent to the treasury. The fee is reserved alongside the reward, so charging it never threatens the solvency promise. Streams with a zero fee skip the treasury transfer entirely.
Campaign lifecycle
A position goes through three stages: holding (held > 0, earning) then exited (held set to 0, stops earning but keeps what it earned) then closed (after the participant has claimed everything).
A campaign can only close once all its positions are closed and nothing is still owed. So closing can never leave a participant unable to claim, or leave earned rewards locked in the campaign.
Commitment lockup & early-exit penalty
A campaign can optionally reward commitment with an early-exit penalty. It's off by default. The owner sets two values once, at creation, and they never change afterward:
- Lockup period: how long a participant should stay to earn without penalty.
- Maximum penalty: the largest cut an early exit can incur, as a percentage of the reward at stake.
The penalty only ever reshapes who receives reward tokens when someone leaves early. It never touches stake, never affects solvency, and a claim is never blocked. Every penalty goes to the protocol treasury, never to the owner or to other participants.
The commitment clock
The lock tracks stake over time, not a one-time stamp, so it can't be gamed with a cheap early seed. Every time a participant's stake increases (first entry, a top-up, or rejoining after an exit), their lock resets to the full lockup period counted from that moment. It's never pushed past the campaign's end, and a reduction never moves it earlier. Staying put lets the clock run down; adding more re-commits the whole position. The deliberate cost is that a participant who keeps compounding stays locked as long as they keep adding.
Two penalties, one destination
Leaving early can mean two different things, penalized separately, both paid to the treasury:
- Pulling stake while locked. When the delegate reports a lower stake before the lock ends, a slice of the reward earned so far is taken, scaled by how much was pulled and how early. The delegate's report moves no tokens, so this penalty is booked as pending and swept to the treasury on the participant's next claim, or by anyone permissionlessly if the participant never claims.
- Claiming while locked. Claiming before the lock ends takes a time-decayed cut of the reward being claimed: near the maximum at the start of the lock, shrinking to zero at maturity. It's transferred to the treasury in the same claim.
Both cuts bite only earned reward, never stake and never other participants' rewards. Waiting always costs less, so there's no cliff to time around: hold to maturity and you keep everything.
Global pause
A global pause can stop new activity (new campaigns, new reward tokens, held updates, horizon extensions) in an emergency. Claims, reclaims, and closes are never paused, so funds can always be withdrawn.
How it fails (safely)
A campaign is built to fail safely. When an operation can't complete correctly it reverts the entire transaction instead of applying a partial, inconsistent change, so a bug surfaces as a rejected transaction rather than as silently corrupted state. The rejections an integrator will actually run into:
- Insolvency guard: any operation that would leave a vault holding less than it owes reverts the whole transaction, so a shortfall is never left behind silently.
- Under-funded promise: reporting new stake on a fixed reward, or extending the reward window, is rejected when the vault can't cover the larger obligation it creates. Fund the vault first, then retry.
- Lapsed or non-advancing window: extending the reward window is rejected once the current window has already ended (there is no reviving it after the fact), or when the proposed new end isn't strictly later than the current one.
- Wrong time to reclaim or close: reclaiming surplus from a proportional reward is refused before that reward's window has ended, and closing a campaign is refused while any position is still open or anything is still owed.
- Missing treasury account on a penalty: a claim that owes anything (its own early-claim haircut, or a pending reduction penalty it's sweeping) requires the treasury's token account, even on a zero-fee campaign, and reverts without it. A plain claim after the lock has expired, with nothing pending, needs no treasury account.
- Nothing to do: claiming or reclaiming zero is rejected outright, not passed as a silent no-op.
Instructions
Complete set of campaign instructions:
| Instruction | Who | What it does |
|---|---|---|
initialize_config | Admin | Set up the global fee, treasury, and pause state |
update_config | Admin | Change global fee, treasury, or pause state |
accept_admin | Proposed admin | Accept a pending admin transfer |
create_campaign | Owner | Open a campaign with its first reward stream |
add_reward_stream | Owner | Add another reward token (proportional) to a live campaign |
extend_horizon | Owner | Push the reward window forward, optionally re-pricing |
set_delegate | Owner | Rotate the attestation authority |
reclaim_unused | Owner | Pull back over-funded, unowed vault balance |
close_campaign | Owner | Tear down a fully-settled campaign and recover rent |
set_held | Delegate | Create or update a participant's held amount |
claim | Participant | Collect earned reward for one token |
collect_penalties | Anyone | Sweep booked early-exit penalties to the treasury |
close_position | Delegate | Clean up a position that has fully exited and claimed |
Design decisions
These are intentional trade-offs:
- Rewards are never clawed back. If a participant earns rewards and walks away without claiming, the campaign stays open. This is the cost of the promise that earned rewards always remain the participant's. The one exception is a campaign with an early-exit penalty, where leaving before the lock ends diverts a slice of earned reward to the treasury (never stake, and never to the owner).
- No participant opt-in. The delegate creates a position the first time it reports held. Participants only ever claim. There's no separate sign-up step.
- Classic SPL and Token-2022. Both token programs work as reward tokens. Mints with unsafe extensions (transfer fees, transfer hooks) are rejected when a campaign is created, and every transfer is amount-checked, so a transfer always delivers exactly its face amount.
- Pause freezes new activity, not accrual. Existing positions keep earning while paused, which is safe because rewards are always pre-reserved or budget-capped.