Award non-transferable reward credits
Compose optional eligibility rules and issue RWD from actual settled output.
Example hook: RewardCredits (RWD). examples/hooks/rewards demonstrates an external composed hook. It selects BeforeSwap and AfterSwap, evaluates optional eligibility gates, and calculates credits from the output actually delivered by a swap. The core supplies context and account permissions; reward economics are not native pool rules. Pools without this hook earn no RWD through it.
Prerequisites: callback context, state permissions, and your first integration.

Configure the reward unit and budgets
RewardRules selects one eligible_output_mint, a PurchaseRewardRate, a lifetime issuance cap, and a per-participant cap. Its public constructor leaves window and allowlist disabled. Optional gates reuse the earlier example policies; they do not become requirements of every pool.
For actual output in base units:
calculated = floor(actual_output × numerator × 10^reward_decimals
/ (denominator × 10^purchased_decimals))
available = min(lifetime_cap − lifetime_issued,
participant_cap − participant_issued)
granted = min(calculated, available)The rate uses checked integer/wide arithmetic, positive numerator/denominator, and decimal counts from 0 through 18. Caps and issued totals use reward base units. Corrupted totals above a cap reject; a valid exhausted budget grants zero and preserves an otherwise valid swap. Credits rounded away are not accumulated as fractional entitlement.
- 1.25 tokensEligible actual output
- 2.5 RWDCalculated at 2 RWD per token
- 0.5 RWDGranted after remaining caps
Issuance totals are cumulative. Burning or consuming a token balance does not reopen lifetime or participant issuance budgets. The reward example does not promise a monetary peg, redemption reserve, or AMM liquidity for these credits.
Compose gates and effects inside one hook
The example authenticates the core callback, validates its account/context bindings, checks an optional trading window, and then checks optional Merkle membership. BeforeSwap may prepare sponsored accounts, but never issues credits. AfterSwap repeats the deterministic eligibility evaluation over the same operation Clock and uses settled.actual_output for issuance.
The rate applies only when output_mint matches the configured eligible mint. The opposite direction grants zero. Enabled gates still apply to the operation; an unrewarded direction is not an automatic bypass. With allowlist disabled, the payload must contain no membership proof. With it enabled, the proof binds the actor, pool, protocol, eligible mint, root version, namespace, and expiry.
This is composition within one selected program. The core does not attach an ordered list of independent hook programs or supply a built-in rule registry. A composed program owns its ordering, failure semantics, and state. The reward example uses PolicyOnly and returns no dynamic fee override.
Provision the exact reward accounts
RewardAddresses::derive(hook, pool, actor) uses stable pool/actor identities. It does not predict the next pool operation sequence.
Choose one immutable preparation mode:
| Mode | Pool capabilities and adapter | Who prepares accounts? |
|---|---|---|
| Prepared (default) | reward_capabilities(), RewardHookQuoteAdapter | Participant, before contextual quote/swap |
| Sponsored (opt-in) | reward_capabilities_for(true), SponsoredRewardQuoteAdapter | Hook, inside the first swap, funded by its sponsor PDA |
The following six-account table describes prepared mode.
| Extra, in callback order | Identity | Requested access |
|---|---|---|
| Configuration | Hook PDA [b"policy", pool, &[0]] | Read-only PolicyPda |
| Issuer | Hook PDA [b"reward-issuer", pool] | HookState, writable only AfterSwap |
| Participant | Hook PDA [b"reward-user", pool, actor] | HookState, writable only AfterSwap |
| RWD mint | Hook PDA [b"reward-mint", pool], owned by Token-2022 | ExternalState, writable only AfterSwap |
| RWD destination | Actor's Token-2022 associated account for the reward mint | ExternalState, writable only AfterSwap |
| Token-2022 program | Declared executable ID | Read-only ExternalProgram |
Use reward_capabilities() and reward_account_access() together. They declare six extras, at most four writable in AfterSwap, hook writes only after settlement, external reads before settlement, external writes afterward, and the Token-2022 program in both swap phases. The core hook PDA and read-only pool precede these extras.
Provisioning has two explicit instructions:
| Instruction | Ordered accounts and privileges |
|---|---|
configure_rewards(&rules) | Creator (writable signer), pool (read-only), configuration/issuer/mint (writable), eligible purchased mint, Token-2022 program, System Program (read-only) |
REWARD_PREPARE_TAG | Actor (writable signer), pool/configuration (read-only), participant (writable), reward mint (read-only), destination (writable), Token-2022/Associated Token/System programs (read-only) |
Setup requires the pool creator and the exact reward capabilities. It validates the eligible pool mint and purchased decimals, creates immutable configuration and issuer state, then creates a NonTransferable reward mint controlled by the issuer PDA. There is no freeze authority. Participant preparation creates actor-bound state and the canonical Token-2022 ATA. Its ATA creation is idempotent; the full participant-creation instruction is not advertised as repeatable.
Use the executable builders in examples/hooks/rewards/tests/svm_support/instructions.rs for the precise account order and instruction encoding. SwapArgs.hook_accounts must equal reward_account_access() and the supplied callback extras must follow that same order.
Prepare the reward accounts before swapping. Callback CPI receives neither the payer nor the actor's signing privilege. The hook's issuer PDA is its separate mint authority. Core vaults, actor settlement accounts, pool mints, and other protected accounts cannot be substituted as reward extras.
Sponsor account creation for public quotes
Set rules.sponsored_preparation = true before configuration. Fund reward_rent_address(pool) using a normal SOL transfer. This System-owned PDA has seeds [b"reward-rent", pool] under the hook; funding is separate from pool creation. The current example has no sponsor-withdrawal instruction.
reward_account_access_for(true) and sponsored_reward_keys(pool, actor) describe ten extras. Participant state becomes CreatableHookState, writable before and after; the ATA also becomes writable before and after. After the six accounts above, add the sponsor (writable BeforeSwap), System Program, Associated Token Program, and ActorReadOnly view. Pool capabilities explicitly authorize all three external programs. The actor view carries no signature or writing power.
BeforeSwap validates identity and eligibility before using the sponsor's own PDA signature to create missing accounts. AfterSwap issues RWD. Repeated swaps preserve counters and do not repay rent. Both swap directions can trigger preparation, including a sale that earns zero credits. Insufficient sponsor funds or a substituted account rolls back the entire transaction.
Rent sponsorship is per new address; small trades from many fresh wallets can consume the budget. Issuance caps do not cap sponsored account creation. The sponsor must keep funds available; the ordinary quote does not check that balance. Normal transaction fees still belong to the transaction fee payer.
Preserve actual settlement and issuance identity
BeforeSwap has no settled output. AfterSwap carries the input/output mints, actual output, actual consumed input, unspent input, fee amount/rate, and before/after pool snapshots. Issuance uses output rather than the requested maximum input, a quote estimate, or the transaction fee.
The prepared and ordinary sponsored profiles require an authenticated nondefault actor matching the local input authority and output owner. Their participant state is bound to that actor and pool. The routed profile below separates the execution actor from the signed beneficiary. Issuer and participant state track the last processed operation sequence; stale/replayed context rejects. The sequence is a core operation identifier, not randomness or a preallocated reward-request address.
RWD uses the Token-2022 NonTransferable mechanism. Its destination ownership must remain immutable. The standard prevents transfers while allowing the owner to burn and close eligible accounts; that does not implement an application-specific redemption or claim flow. See the Token-2022 extension guide.
The traded pool assets still use classic SPL Token. A separate reward CPI does not make Token-2022 assets valid pool reserves. Minting and hook-state updates remain provisional: a later failure must roll them back with the swap.
Keep rewards with the wallet through a router
A router may spend from its own PDA and intermediate token accounts. In the prepared and ordinary sponsored profiles, that PDA is the local actor and would receive the credits. Non-transferable rewards cannot be forwarded from it later.
Choose routed_reward_capabilities(rules.sell_fee_ppm) at pool creation and inject
RoutedRewardQuoteAdapter to bind rewards to a separate wallet. This explicit
profile uses sponsored preparation and public eligibility; allowlists remain
unsupported by this profile. Existing pools retain their immutable profile.
The wallet signs a transaction containing RoutedRewardIntent: beneficiary, pool,
top-level router, next operation sequence, deadline slot and exact swap accounts,
mints, amounts and price bounds. Both callbacks authenticate the core invocation
and inspect the canonical Instructions sysvar. The beneficiary must be a signer
of the original transaction, and the exact intent must occur in the current
signed top-level instruction. A PDA's CPI signature cannot satisfy that proof.
The participant PDA and reward ATA belong to the verified beneficiary. The router downgrades the beneficiary to readonly/nonsigner for CPI, so the hook receives no wallet signing power. Rent comes from the sponsor; issuance uses settled output and the existing caps. Missing, changed, expired or replayed consent rejects. A later transaction failure rolls back minting and rent with the swap.
| Integration | Contextual execution entrypoint | Verified scope |
|---|---|---|
| Metis | swap_with_context | Native builder and a local custody probe |
| Titan | generate_swap_instruction_with_context, build_leg_with_context | Local Titan template with PDA custody |
| DFlow | build_native_instruction_with_context | Native builder and local pass-through CPI |
In the local custody fixture, 10,000 input atoms buy 9,871 output atoms. The wallet receives 19,742,000 reward atoms and the custody PDA receives zero. These results use a disposable test reward mint, not a public GEM deployment.
Public router adoption remains separate. DFlow's pinned interface still requires a Grapl execution mapping, and no local test establishes listing or discovery in an aggregator's live service. This contract authorizes rewards for a particular Grapl leg; the router remains responsible for its final output delivery. A pending intent must be rebuilt after another pool operation changes its sequence.
Preserve quote compatibility
RewardHookQuoteAdapter in examples/hooks/rewards/src/metis/ implements the external effect interface. It requires an actor and complete swap details, derives the six-account plan, reuses reward evaluation and token validation, and predicts issuer/participant bytes plus mint/destination balances. Reward credits are separate effects; they must never be added to Quote.out_amount for the AMM output asset. Missing actor/state or unsupported effect evaluation must reject explicitly. See Metis compatibility for the generic integration boundary.
SponsoredRewardQuoteAdapter supports standard wallet-free refresh/quotes using configuration, issuer and mint snapshots. It validates public rules, then binds ten execution accounts to the real SwapParams.user. The hook creates the missing recipient accounts during execution. This adapter returns no wallet reward-byte predictions; actual issuance and participant caps are enforced on-chain. It rejects allowlist-enabled configurations and nonempty proof payloads. A public trading window remains supported.
Probabilistic rewards remain a separate design
The deterministic example has no lottery. A probabilistic extension would first commit eligibility, then obtain verified randomness asynchronously, authenticate fulfillment, and consume the request once. No provider has been selected or integrated for that flow.
Clock, blockhash, caller-selected seeds, and a same-transaction path that can abort after a loss are not acceptable randomness sources for the proposed flow. Sequence numbers can bind requests but do not supply entropy. Random selection, consumption, claims, or a reference reserve would remain responsibilities of the external hook/application, with their own verification.
Next, review authentication, permissions, and the ABI reference before extending the example.
Fund sponsorship from sales
Sale funding is optional and defaults to sell_fee_ppm = 0. This example supports a classic TOKEN/WSOL pool: the eligible reward token is TOKEN, purchases earn RWD, and sales may contribute part of their WSOL output to the sponsor. Other quote assets such as USDC reject this funding mode; they require a separate conversion mechanism.
rules.sponsored_preparation = true;
rules.sell_fee_ppm = 5_000; // 0.5% of gross WSOL output.
let capabilities = reward_rule_capabilities(&rules);Set these immutable capabilities at pool creation, then configure the hook with matching rules. The standard SponsoredRewardQuoteAdapter selects the same fee rule and builds the required account plan. Manual clients use funded_reward_access(true) and funded_reward_keys(pool, actor, true).
- BeforeSwap prepares missing reward accounts and a temporary WSOL PDA
[b"reward-fee", pool]. The sponsor pays its rent; the actor stays readonly and nonsigner inside the hook. - The hook requests
floor(gross_output * sell_fee_ppm / 1_000_000). Core transfers net output to the seller and the contribution to the temporary vault, under the independent immutablemax_output_fee_ppmceiling. - AfterSwap closes the WSOL vault to
[b"reward-rent", pool]. This unwraps the contribution into native SOL and returns the temporary rent. A failure anywhere rolls all account and token changes back.
Quote.out_amount and settled.actual_output are net. minimum_out protects that net amount. HookQuoteResult.output_fee_amount reports the contribution separately in output-token units; Jupiter's ordinary fee_amount/fee_mint continue describing the input-denominated AMM fee. LP accounting is unchanged, and the seller pays both configured charges.
The fee-enabled plan has 13 extras. The sponsor is writable in both swap phases; the extra roles are the temporary WSOL account, classic Token Program, and a PoolMintReadOnly view of the native mint. Purchases do not create the temporary fee vault or pay this contribution.
The sponsor still needs initial SOL: BeforeSwap pays rent before the sale settles. Small floor-rounded fees may be zero. Sales replenish the budget, but wallet creation demand can exceed revenue; a quote does not reserve or verify the sponsor balance. Normal transaction fees remain the transaction fee payer's responsibility. There is no sponsor-withdrawal instruction in this example.
Rewards on direct Prism markets
Prism can fill a pair directly, such as WSOL → TOKEN, without an intermediate
CLMM pool. It can also participate in a larger route. Maker inventory remains in
maker-owned token accounts until settlement; executable output is limited by
balance, delegated allowance, strategy budget and price range.
RoutedRewardQuoteAdapter supports both deposited CLMM and Prism markets. Create
the market with routed_reward_capabilities(rules.sell_fee_ppm) and use the same
signed-beneficiary intent described above. The Prism builder resolves the
beneficiary's execution accounts after quoting and preserves signed price and
minimum-output bounds. The reward ATA belongs to the wallet, including when the
execution authority is a router PDA.
For a TOKEN/WSOL market, a configured sale contribution can fund the sponsor in native SOL. Local SBF tests reconciled 4,945,104 lamports collected from a sale and 3,605,280 lamports spent preparing a different buyer's reward accounts. That buyer received 19,838,000 test reward atoms; the custody PDA received zero. A second buy reused those accounts without another rent charge. These are fixture quantities, not a promised emission rate or economic return.
Validation covers the Metis native builder, a local Titan custody template, and DFlow native/pass-through CPI builds. It does not establish adoption by public routers. The sponsor still needs bootstrap SOL, and stablecoin fees need a separate conversion before they can pay rent. Tests reject forged/replayed consent and revoked allowance, and roll back rewards, fees and rent when a later instruction fails.
Rent is an account-storage deposit, not a fee per swap. Preparation only funds missing accounts (or a rent shortfall); repeated swaps reuse existing accounts. Closing and recreating a token account can require a deposit again. The reward participation PDA has no close instruction in this example. Actual rent amounts come from the execution environment's rent configuration, so the local figures above are not a fixed quote for another cluster.