Customization Options

Personalize reward distribution to make your campaign unique

Merkl campaigns are highly configurable. Beyond the default reward logic, you can layer in eligibility filters, access controls, boosts, privacy modes, and advanced flows like referrals or raffles to shape exactly who earns what.

The most common options can be enabled directly in Merkl Studio when configuring your campaign.

If your use case requires something not exposed in the UI — for example, whitelisting based on a custom onchain condition, restricting forwarding to specific protocols, or wiring a custom boost API — reach out to the Merkl team and we'll help wire it in.

Below is a non-exhaustive overview of what's possible, with examples. The full set of options — each with its technical reference — lives in the schema explorer in our developer docs.

Eligibility

Blacklisting

You can exclude specific addresses from receiving rewards. This is typically used to filter out exploiters, sanctioned wallets, internal team addresses, or contracts you don't want to incentivize.

Edit settings button on a campaign card

Campaign settings — Options panel

If a forwarder is blacklisted, all users behind it become ineligible too.

Example — blacklisting a staking contract:

  • A user holds 10 USDA but has staked 6 USDA in a blacklisted staking contract (a forwarder).
  • Only the 4 USDA in the user's wallet qualifies for rewards.

Whitelisting

You can restrict rewards to a specific set of addresses — individual users, forwarders, or a mix.

Example — Uniswap V3 with three Automated Liquidity Managers acting as forwarders:

  • The campaign creator whitelists two of the three managers and one direct user.
  • Only liquidity provided through those two managers, plus the whitelisted user's direct position, earn rewards.
  • Among whitelisted addresses, rewards are still split proportionally to liquidity share.

Whitelisting overrides blacklisting. As soon as a whitelist is set, every address not on it is implicitly excluded.

Multiple campaigns can run on the same opportunity (e.g. a Uniswap v4 ETH-USDC pool) with different eligibility rules. Two users in the same pool may earn from different campaigns depending on whose whitelist they're on. Users should check campaign details on the opportunity page to confirm what they're eligible for.

Dynamic whitelist & blacklist via the Key-Value Store API

A campaign's whitelist and blacklist can always be updated by overriding the campaign onchain. For lists that change frequently — large allowlists, ongoing KYC additions, exploit-driven blacklists — onchain updates are inconvenient.

Merkl exposes a Key-Value Store API that lets you mutate eligibility lists offchain. Each store has a unique key, a type (WHITELIST or BLACKLIST), and an access mode. Once the campaign is wired to reference a store, the engine reads its current contents on every reward computation, so changes propagate within roughly two hours.

Setup (one-time, handled by the Merkl team):

  1. Merkl provisions a store for you with a chosen key, type, and API_KEY access mode, and links it to your campaign's hook.
  2. You receive the store key and an X-API-Key credential.

Updating entriesPUT /v4/key-value-stores/{key}/entries/batch

Headers:

X-API-Key: your-api-key
Content-Type: application/json

Body (up to 1000 entries per request):

{
  "entries": [
    {
      "address": "0xA9DdD91249DFdd450E81E1c56Ab60E1A62651701",
      "value": "{\"isBlacklisted\":true}"
    },
    {
      "address": "0xeaC6A75e19beB1283352d24c0311De865a867DAB",
      "value": "{\"isBlacklisted\":true}"
    }
  ]
}

The endpoint is an upsert: existing entries are overwritten, new ones are inserted. To remove an address from the list, call DELETE /v4/key-value-stores/{key}/entries/{address}.

The value is a JSON string whose shape depends on the store type:

  • BLACKLIST{"isBlacklisted": true}
  • WHITELIST{"isWhitelisted": true}

If the same campaign is wired to both a whitelist and a blacklist store, whitelist precedence still applies: as soon as the whitelist contains at least one entry, every other address is implicitly excluded.

This is the recommended pattern for private LP deals: as whitelisted addresses don't appear in the Merkl app.

Protocol whitelist / blacklist

Because reward forwarding can route a single campaign's rewards across many downstream protocols, you can also restrict eligibility at the protocol level — only forward to (or exclude) positions held in specific protocols, rather than enumerating addresses.

Access Control

You can gate eligibility on arbitrary onchain conditions or events. Multiple access controls can be combined within the same campaign.

View functions

Almost any onchain condition exposed via a view function can be plugged into a campaign's eligibility check. The examples below are commonly used and ready to wire in.

Token snapshot

Reward only users who hold at least a minimum amount of a given token at a specific snapshot block.

OFAC compliance

Filter out addresses flagged by the U.S. Office of Foreign Assets Control by pointing the campaign at a sanctions registry contract such as the Chainalysis Sanctions Oracle on Ethereum.

World ID

Restrict participation to humans verified via World ID to filter out bots and Sybil farms.

Health factor

On lending protocols like Aave, you can require a health factor below a chosen threshold so that only addresses with active borrows qualify.

Onchain events

Eligibility can also be derived from indexed onchain events. The engine builds a set of qualifying addresses from the events you care about and intersects it with the campaign's recipients.

Smart accounts

Many smart-wallet factories emit a creation event when a user deploys their account. Indexing those events lets you run a campaign that only rewards users on a specific smart-wallet stack.

Token holding (duration-gated)

You can require recipients to hold a minimum amount of a given token (which can differ from the reward token) for a minimum duration before becoming eligible.

Example — campaign requires 500 stUSD held for 30 days:

  • User holding 600 stUSD for 44 days → eligible.
  • User holding 400 stUSD for 60 days → not eligible (fails amount).
  • User holding 800 stUSD for 12 days → not eligible (fails duration).

This is useful for steering rewards toward long-term holders rather than short-term flippers.

Privacy

You can launch private campaigns where eligibility rules and configuration are hidden from public view. Only the campaign creator and explicitly authorized addresses can see the details.

Common use cases:

  • Confidential airdrops where the eligibility criteria themselves are sensitive.
  • Private payments and settlements.
  • Strategic incentive programs with undisclosed terms.

Because all Merkl rewards are aggregated in the Distributor contract and processed alongside high transaction volume, it's computationally infeasible to correlate a specific deposit with an individual claim — providing strong privacy guarantees.

You can let recipients see their own allocation, or restrict visibility entirely to the creator and authorized addresses. See the Private Campaigns guide for setup details.

Boosts

Token / NFT boost

You can boost rewards for users who hold a specific token or NFT — similar to Curve's vote-escrowed model, but more flexible (no hard 2.5× cap).

Boost formula:

B=b×R×vV×r+1B = b \times \frac{R \times v}{V \times r} + 1

Where:

  • B: boost multiplier applied to the user's rewards.
  • b: custom boost factor chosen by the campaign creator.
  • R: total rewards per epoch.
  • r: user's rewards per epoch.
  • V: total supply of the boost token / NFT.
  • v: user's holdings of the boost token / NFT.

Example: to achieve a 10% boost, set the boost multiplier to 1.1.

NFT-based boosting is also supported — reach out to the Merkl team to set it up.

API boost

For dynamic boosts that depend on data Merkl can't see directly (offchain scoring, KYC tiers, partner-controlled allowlists), you can expose an HTTP endpoint that the Merkl engine calls during reward computation.

Four boost composition modes are available:

FunctionFormula
MULTIPLYamount = amount × boost
MULTIPLY_WITH_OFFSETamount = amount × (1 + boost)
ADDamount = amount + boost
REPLACEamount = boost

Implementation

The customization option takes the following parameters:

  • url — endpoint the engine will POST to.
  • boostingFunctionMULTIPLY, MULTIPLY_WITH_OFFSET, ADD, or REPLACE.
  • sendScores — whether to include each address's current campaign score in the request.
  • defaultBoost — fallback behavior when an eligible address is missing from your response. Either:
    • ZERO_ADDRESS: use the boost value you returned for 0x000…000 as the default. Since the zero address is always excluded from reward computation, it's safe to repurpose. You must include an entry for the zero address when using this mode.
    • ERROR: abort the campaign computation if any eligible address is missing from the response.

Request body sent by the engine:

// sendScores = true
let body: { address: string; score: string }[]; // checksummed addresses

// sendScores = false
let body: { addresses: string[] }; // checksummed addresses

The engine first POSTs the full address list. If your endpoint fails or times out, it halves the payload and retries — recursively — until requests succeed. To keep this efficient, support at least 250 addresses per request.

Expected response shape:

const data: {
  address: string;
  boost: string; // bigint in BASE 9 — e.g. 1× = "1000000000"
}[];

Endpoint requirements:

  • Always return valid JSON in the format above — a malformed response aborts reward computation.
  • Don't include duplicate addresses — only the first boost value is used.
  • Boost values are in base 9: is "1000000000", is "2000000000", 0.5× is "500000000".

Dynamic whitelist via API boost

You can implement a whitelist that grows over time without redeploying the campaign. To do so:

  • Use MULTIPLY as the boosting function.
  • Return "1000000000" (1× in base 9) for whitelisted addresses.
  • Return "0" for the zero address with defaultBoost = ZERO_ADDRESS — every non-whitelisted address inherits a 0× boost.
  • Make sure addresses are checksummed.
[
  { "address": "0x1234567890AbcdEF1234567890aBcdef12345678", "boost": "1000000000" },
  { "address": "0xabcdef1234567890abcdef1234567890abcdef1234", "boost": "1000000000" },
  { "address": "0x0000000000000000000000000000000000000000", "boost": "0" }
]

Dynamic blacklist via API boost

Mirror of the above — keep a mutable blacklist without recreating the campaign:

  • Use MULTIPLY as the boosting function.
  • Return "0" for blacklisted addresses.
  • Return "1000000000" (1×) for the zero address with defaultBoost = ZERO_ADDRESS — every non-blacklisted address inherits a 1× boost.
[
  { "address": "0x1234567890AbcdEF1234567890aBcdef12345678", "boost": "0" },
  { "address": "0xabcdef1234567890abcdef1234567890abcdef1234", "boost": "0" },
  { "address": "0x0000000000000000000000000000000000000000", "boost": "1000000000" }
]

Advanced Logic

Bridge liquidity tracking

Campaigns can be restricted to users who bridged liquidity from another chain before participating, so that only genuinely new cross-chain liquidity is rewarded — not movements within the same chain.

Currently supported with Jumper, but any bridge can be wired in on request.

Referral programs

You can run an onchain referral program on top of a campaign — rewarding both the referrer and the invitee, with optional whitelist gates and customizable splits.

Highlights:

  • Unlimited programs — run as many as you need in parallel.
  • Onchain referral codes — users generate unique codes to share.
  • White-label integration — embed the flow in your own frontend (contact Merkl).
  • Cross-protocol — works on any protocol or behavior already supported by Merkl.
  • Customizable splits — distribute rewards across users, referrers, invitees, or even non-participating addresses.
  • Participation gating — optional whitelist, optional fee to mint a referral code.
  • Onchain confirmation — users sign a transaction to commit their referral, preventing spoofing.

Reach out to the Merkl team for help wiring up a referral program.

Referral program via the Key-Value Store API

You can also run a referral program managed entirely offchain. You push invitee → referrer pairs to a key-value store, and the engine applies your boost configuration on every reward computation:

  • No onchain referral codes, no campaign redeployment.
  • Referral pairs can be added, changed, or removed at any time.
  • Changes propagate within roughly two hours.

Referral boosts redistribute the campaign budget — they shift rewards toward referrers and invitees. The total distributed amount never exceeds what you funded.

Setting it up

  1. Merkl provisions the store (one-time): the Merkl team creates a key-value store of type REFERRER for you and gives you its key.
  2. You configure the hook in Merkl Studio when creating the campaign: select the Referral Program (API) option (under Advanced Logic), enter the store key, and choose your boost configuration (see below). The boost configuration is fixed for the campaign's lifetime — only the referral pairs stay mutable.
  3. You push referral pairs via the API, whenever you want, authenticated with your Merkl API key.

The store key

  • What it is — the store's unique, human-readable name (e.g. my-protocol-referrals), chosen with the Merkl team when the store is provisioned. Your campaign's hook references it.
  • Not a secret — the key appears in API URLs and in your campaign's hook configuration. It only identifies the store.
  • Write access — the store has an owner address (yours). Write requests must carry the owner's Merkl API key in the X-API-Key header. There is no store-specific key: it's the same Merkl API key you'd use on other authenticated endpoints. That key is the secret — keep it safe, and rotate it through the Merkl team if compromised.
  • Read access — store contents are publicly readable by default: anyone who knows the key can list who referred whom. Ask the Merkl team for private reads if that's a concern.
  • Reusable — several campaigns can point at the same store: maintain your referral pairs once, and each campaign applies its own boost configuration on top.

API integration

Each entry maps one invitee (the entry address) to their referrer (in the value). The value must be exactly {"referrer": "<address>"}, JSON-encoded as a string — it is validated at write time, so a malformed body fails immediately with a 400.

Add or update referrals (batch, up to 1000 entries per request):

curl -X PUT https://api.merkl.xyz/v4/key-value-stores/my-protocol-referrals/entries/batch \
  -H "X-API-Key: $MERKL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entries": [
      { "address": "0x1111111111111111111111111111111111111111", "value": "{\"referrer\":\"0x2222222222222222222222222222222222222222\"}" },
      { "address": "0x3333333333333333333333333333333333333333", "value": "{\"referrer\":\"0x2222222222222222222222222222222222222222\"}" }
    ]
  }'

Response:

{ "success": true, "count": 2 }

The endpoint is an upsert: re-submitting an address overwrites its referrer. An invitee has exactly one referrer (the latest written); a referrer can have any number of invitees.

Remove a referral:

curl -X DELETE https://api.merkl.xyz/v4/key-value-stores/my-protocol-referrals/entries/0x1111111111111111111111111111111111111111 \
  -H "X-API-Key: $MERKL_API_KEY"

Response: { "success": true }

Read current pairs (paginated, no auth needed if the store is publicly readable):

curl "https://api.merkl.xyz/v4/key-value-stores/my-protocol-referrals/entries?page=0&pageSize=100"

Response:

{
  "entries": [
    {
      "address": "0x1111111111111111111111111111111111111111",
      "value": "{\"referrer\":\"0x2222222222222222222222222222222222222222\"}",
      "createdAt": "2026-06-12T09:00:00.000Z",
      "updatedAt": "2026-06-12T09:00:00.000Z"
    }
  ],
  "total": 1,
  "page": 0,
  "pageSize": 100
}

You can also fetch a single entry with GET /v4/key-value-stores/{key}/entries/{address} (returns 404 if absent).

Good to know:

  • Addresses are stored lowercased — casing doesn't matter in requests.
  • Self-referrals (address equal to referrer) are accepted by the API but ignored by the engine.
  • Common errors: 400 malformed value or store size limit reached (default 100,000 entries), 401 missing/invalid API key, 403 API key doesn't belong to the store owner, 404 unknown store key.
  • Pairs are read at each reward computation — a new pair affects the next computation (within ~2 hours), not past ones.

Boost configuration

You set these parameters in the Merkl Studio campaign form, not via the API: when adding the Referral Program (API) option, the form asks for each field below — boost values are entered as percentages (Studio converts them to base 9 for you).

ParameterDescription
keyKey-value store holding the referral pairs
boostForReferrerBoost type applied to referrers (see Referrer boosts)
valueForBoostForReferrerBoost value for referrers
boostForInvitedBoost type applied to invitees (see Invitee boosts)
valueForBoostForInvitedBoost value for invitees
maximumBoostReferrerCap on the referrer bonus, as a share of their own base rewards (0 = uncapped)
maximumBoostInvitedCap on the invitee bonus, as a share of their own base rewards (0 = uncapped)
defaultBoostUsers outside the program: score = unaffected, zero = earn nothing (referral-gated campaign)
cumulativeBoostUser who is both referrer and invitee: true = both bonuses apply, false = only the better side counts

Under the hood, boost values are stored in base 9: "1000000000" = 100%, "100000000" = 10%. This is what you'll see when reading the campaign's hook configuration from the API.

Referrer boosts

All examples use the same scenario: Alice referred Bob and Carol. Base rewards this epoch: Alice 100, Bob 50, Carol 30.

  • score — no referrer bonus. Alice earns her base 100.
  • proportional — the referrer earns value% of their invitees' rewards, only if the referrer participates in the campaign.
    • Example (10%): Bob and Carol earn 80 together → Alice gets a bonus of 8, for a total of 108. If Alice had no position, she'd get nothing.
  • flatBoostNonParticipating — same formula, but the referrer doesn't need to participate: the bonus is credited even to addresses with no position. Use this for affiliate-style programs.
    • Example (10%): Alice never deposited anything. Bob and Carol earn 80 → Alice still receives 8.

Both boost type fields are required, so score is how you disable one side. Combining score on both sides with defaultBoost = zero turns the program into a pure eligibility filter: no bonuses, but only referral participants earn.

Leave maximumBoostReferrer at 0 (uncapped) when using flatBoostNonParticipating. The cap is a share of the referrer's own base rewards — zero for a non-participating referrer — so any non-zero cap silently wipes out the bonus.

Invitee boosts

  • score — no invitee bonus. Bob earns his base 50. Use it when only referrers should earn extra.
  • proportional — the invitee earns value% of their own rewards, capped at value% of their referrer's rewards. A referrer with no position grants no boost.
    • Example (20%): Bob's bonus = min(20% × 50, 20% × Alice's 100) = 10 → Bob earns 60.
  • flatBoostNonParticipating — the invitee earns value% of their own rewards, unconditionally.
    • Example (20%): Bob's bonus = 20% × 50 = 10 → Bob earns 60, even if Alice has no position.
  • community — the invitee earns value% of the combined rewards of everyone referred by the same referrer (themselves included). The bigger the cohort, the bigger everyone's boost.
    • Example (20%): Alice's cohort (Bob + Carol) earns 80 → Bob's bonus = 20% × 80 = 16 → Bob earns 66. Carol gets the same 16 bonus.

Raffle

Instead of distributing rewards proportionally, you can run a raffle that picks a set of lucky winners.

Customization

  • Frequency — daily, weekly, or any cadence you choose.
  • Number of winners — one grand prize, or any number of winners per draw.
  • Selection method:
    • Equal odds — every participant has the same chance.
    • Whales first — chances are weighted by campaign score.
  • Parallel raffles — run multiple draws simultaneously, each with its own rules.

Reproducibility

Seed. The seed is the block hash of the first block after the timestamp set by the campaign creator.

Winning numbers. Generated using the XORShift128Plus PRNG, seeded with the block hash above. The step equals the minimum user score divided by 1000 (or 1 when score isn't used).

function getSelectedNumbers(
  seed: number,
  numberOfWinners: number,
  totalScore: number,
  step: number,
): number[] {
  const random = new XORShift128Plus(seed)
  const selectedNumbers = []
  for (let i = 0; i < numberOfWinners; i++) {
    selectedNumbers.push(random.randBelow(totalScore / step) * step)
  }
  return selectedNumbers
}

Picking winners. Each generated number maps to a cumulative-weight range across the participant list (sorted by address). The participant whose range contains the number wins.

For snapshots and airdrops, the participant list is known up front. For dynamic campaigns, a common trick is to first run the same campaign with the same parameters but without the raffle option and a small budget — the resulting list of recipients then becomes your input set for the raffle.

Worked example — 5 users, weighted raffle, 1 winner

UserWeight
A10
B20
C30
D40
E50

1. Total weight: 10 + 20 + 30 + 40 + 50 = 150.

2. Random number: the PRNG draws a number in [0, 150). Suppose it picks 107.

3. Cumulative ranges:

UserRange
A0–10
B10–30
C30–60
D60–100
E100–150

4. Winner: 107 falls in 100–150User E wins.