Merkl Developer Portal logo

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.

ParameterDescription
urlEndpoint the engine POSTs to
boostingFunctionMULTIPLY, ADD, or REPLACE, as for the boost hook. To multiply by an offset boost, use MULTIPLY and return 1 + boost.
sendScoresInclude each address's current campaign score in the request
defaultBoostZERO_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:

UserWeightRange
A100–10
B2010–30
C3030–60
D4060–100
E50100–150

The total weight is 150. If the PRNG draws 107, it falls in 100–150, so user E wins.