
Campaign options
Technical reference for campaign options — leaderboard visibility, API boosts, and raffles
This page is the technical reference for campaign options that need more than a toggle in Merkl Studio. For what each option does and when to use it, see Customization options.
Options backed by a key-value store (dynamic whitelists and blacklists, boosts, referrals, forwarding) are documented with the stores, in Key-value stores.
Leaderboard visibility
Add the leaderboardVisibility option to hide a campaign's leaderboard, ranks, and per-address reward breakdowns. The campaign stays public.
{
"options": {
"leaderboardVisibility": {
"optionKey": "leaderboardVisibility",
"hideLeaderboard": true
}
}
}
Set creatorCanView: false on the option to withdraw the creator's access too. An access grant does not restore it, because the access check runs the withdrawal before it checks grants. Campaign parameters cannot change after creation: to change the option, create a new configuration, or ask the Merkl team for a live campaign.
Gated endpoints
These endpoints return 401 to unauthorized callers: /v4/rewards, /v4/rewards/count, /v4/rewards/total, /v4/rewards/rank, /v4/rewards/campaign/:campaignId/list, and their deprecated v3 equivalents. The campaign creator keeps access, as does any address with an access grant (POST /v4/campaigns/:id/access/grant). Recipients do not.
These endpoints omit the gated rows silently, with no error. They return 401 only when the caller sets campaignId to a gated campaign they can otherwise see:
/v4/leaves/:recipient/breakdowns(rows and count)/v4/historical/tree,/v4/historical/campaign/:campaignId,/v4/historical/recipient/:recipient/v4/reallocations/breakdown
The campaign-addressed reallocation routes return 401: /v4/reallocations/report/:campaignId, POST /v4/reallocations/report/:campaignId/walk, and /v4/reallocations/jobs/:jobId once the job holds the report.
A recipient still sees and claims their own reward amount. Only the campaign attribution is masked, under the shared "Private Campaigns" label.
Open surfaces
These surfaces stay open: GET /v4/rewards/token (token-scoped, across every campaign that pays it), GET /v4/claims and the onchain Merkle proofs, GET /v4/analytics/positions/by-identifier, the unclaimed-total endpoints (/v4/rewards/unclaim, its batch form, and the deprecated /v3/campaignUnclaimed), the deprecated GET /v3/campaignClaims (no auth check, kept open for an existing integration), and the creator dashboard's recipient count. On a Uniswap V3 campaign, per-position reward rows drop instead of showing masked.
This option hides the leaderboard views. It does not make reward data confidential. The mask leaves the reason field intact, which encodes the pool and position for pool-based campaign types, and GET /v4/users/:address/rewards accepts any address. Combined with GET /v4/analytics/positions/by-identifier, a caller can reconstruct much of the recipient list. Use private campaigns when the data must stay confidential.
API boost
The engine POSTs eligible addresses to your HTTP endpoint during reward computation and applies the boosts you return.
| Parameter | Description |
|---|---|
url | Endpoint the engine POSTs to |
boostingFunction | MULTIPLY, ADD, or REPLACE, as for the boost hook. To multiply by an offset boost, use MULTIPLY and return 1 + boost. |
sendScores | Include each address's current campaign score in the request |
defaultBoost | ZERO_ADDRESS or ERROR, as for the boost hook. With ZERO_ADDRESS, your response must include the zero address. |
Request body:
// sendScores = true
let body: { address: string; score: string }[]; // checksummed addresses
// sendScores = false
let body: { addresses: string[] }; // checksummed addresses
Response:
const data: {
address: string;
boost: string; // base 9 bigint: 1× = "1000000000", 0.5× = "500000000"
}[];
Endpoint requirements:
- Always return valid JSON in this format. A malformed response aborts the computation.
- Do not return duplicate addresses. The engine uses only the first value.
- Accept at least 250 addresses per request. The engine sends all addresses in one request. On failure or timeout, it splits the list in half and retries recursively.
Dynamic whitelist
Use MULTIPLY and defaultBoost = ZERO_ADDRESS. Return "1000000000" for whitelisted addresses and "0" for the zero address, so every other address inherits a 0× boost:
[
{ "address": "0x1234567890AbcdEF1234567890aBcdef12345678", "boost": "1000000000" },
{ "address": "0x0000000000000000000000000000000000000000", "boost": "0" }
]
Dynamic blacklist
Use MULTIPLY and defaultBoost = ZERO_ADDRESS. Return "0" for blacklisted addresses and "1000000000" for the zero address, so every other address inherits a 1× boost:
[
{ "address": "0x1234567890AbcdEF1234567890aBcdef12345678", "boost": "0" },
{ "address": "0x0000000000000000000000000000000000000000", "boost": "1000000000" }
]
Raffle
Seed
The seed is the block hash of the first block after the timestamp the campaign creator sets.
Winning numbers
The engine draws winning numbers with the XORShift128Plus PRNG, seeded with the block hash. The step is the minimum user score divided by 1000, or 1 when the raffle does not use scores.
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 number maps to a cumulative-weight range across the participant list, sorted by address. The participant whose range contains the number wins.
Example with 5 users and 1 winner:
| User | Weight | Range |
|---|---|---|
| A | 10 | 0–10 |
| B | 20 | 10–30 |
| C | 30 | 30–60 |
| D | 40 | 60–100 |
| E | 50 | 100–150 |
The total weight is 150. If the PRNG draws 107, it falls in 100–150, so user E wins.