Merkl Developer Portal logo

Key-value stores

Create and manage your own whitelists, blacklists and other eligibility lists via the self-serve Key-Value Store API

A key-value store is an offchain list of address → value entries that a campaign reads on every reward computation. They back the dynamic lists described in Customization options: whitelists, blacklists, boosts, referrer maps and forwarding. Changes propagate within roughly two hours, with no onchain transaction.

This page is the technical reference for the stores: the endpoints you call, the bodies you send, and the hook parameters that wire a store to a campaign. For what each option does, see Customization options.

Historically, Merkl provisioned every store for you. Allowlisted integrators can now create and manage their own stores programmatically with their API key, using the self-serve flow documented below.

Prerequisites

  1. An API key. All self-serve requests authenticate with the X-API-Key header. See Auth & rate limits. The store you create is owned by your key's address; only that key can write to it.
  2. A creator quota. Self-serve creation is allowlisted. The Merkl team grants your address a quota that bounds:
    • maxConfigs: how many stores you may own,
    • maxSizeLimit: the largest a single store may be (number of entries),
    • allowedTypes: which store types you may create (e.g. WHITELIST, BLACKLIST, BOOST, REFERRER).

To request or adjust a quota, contact the Merkl team with your address and the lists you intend to manage. Without a quota, creation returns 403.

Creating a store

POST /v4/key-value-stores/self

Headers:

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

Body:

{
  "key": "my-whitelist",
  "type": "WHITELIST",
  "sizeLimit": 50000,
  "publicRead": true
}
FieldRequiredNotes
keyyesA short suffix for your store (^[a-zA-Z0-9_-]+$). It is namespaced to your creator's allowlisted address. See below.
typeyesOne of your quota's allowedTypes. Determines the JSON shape of entry values.
sizeLimitnoMax entries. Defaults to min(100000, maxSizeLimit); must not exceed your quota's maxSizeLimit.
publicReadnoIf true (default), anyone can read the entries; if false, only you can.

You do not set the owner or access mode: the server forces the store's owner to the allowlisted address of your API key's creator and its access mode to API_KEY (only your key can write entries).

The response returns the store's canonical key, the owning address joined to your suffix, e.g. 0xAbC…DeF_my-whitelist, along with the resolved type, accessMode (API_KEY), sizeLimit, publicRead and ownerAddress.

The prefix is the address that holds your creator's quota, which is not necessarily the address you signed up with. Do not derive the canonical key yourself. Always read it from the create response or from GET /v4/key-value-stores/self.

Use the returned canonical key for every later call: adding entries, updating, deleting. The key you submitted is only the suffix. Namespacing guarantees your keys never collide with another integrator's.

Managing your stores

ActionEndpoint
List your storesGET /v4/key-value-stores/self
Update a storePATCH /v4/key-value-stores/self/{key}
Delete a store (and its entries)DELETE /v4/key-value-stores/self/{key}

{key} is the canonical key from the create response. PATCH accepts description, publicRead and sizeLimit (never below the store's current entry count, and never above the maxSizeLimit of the quota held by that store's own ownerAddress). You can only manage stores your creator owns. Every address linked to your creator sees and manages the same stores, whichever of them owns a given store.

Adding and removing entries

Once a store exists, manage its membership with the standard entry endpoints, authenticated with the same API key:

PUT /v4/key-value-stores/{key}/entries/batch, where {key} is your canonical key; up to 1000 entries per request, upsert.

Headers:

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

Body:

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

Use DELETE /v4/key-value-stores/{key}/entries/{address} to remove a single entry. The value is a JSON string, and its shape must match the store type exactly. It is validated at write time, so a malformed body fails immediately with a 400:

TypevalueMeaning
WHITELIST{"isWhitelisted": true}address is eligible
BLACKLIST{"isBlacklisted": true}address is excluded
BOOST{"boostBigInt": "<bigint>"}boost for address, base 9 (1× = "1000000000"), strictly positive
REFERRER{"referrer": "0x..."}address is the invitee, value.referrer their referrer
FORWARDER{"forwardTo": "0x..."}forward address's rewards to forwardTo

Addresses are stored lowercased, so request casing doesn't matter. Writes are rejected once the store reaches its sizeLimit. Full endpoint details are in the API reference.

Reading entries

GET /v4/key-value-stores/{key}/entries lists a store's entries, paginated with page and pageSize (defaulting to 0 and 100). Pass value to filter by an exact value match.

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

Response:

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

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

If the store was created with publicRead: true (the default), anyone can list its entries without authentication, convenient for a public referrer map, but avoid it for a whitelist backing a private LP deal. Otherwise reads require the owner's X-API-Key, the same key used to write entries.

Access modes: who can write

Every store has an access mode that governs who may write its entries:

Access modeWho can writeBatch upsertHow it's created
API_KEYOnly the owner's API key, for any addressYes (≤1000/request)Self-serve (above) or by the Merkl team
PUBLICAny wallet, but only its own entry (via wallet signature)NoBy the Merkl team only

Self-serve stores are always API_KEY: only your key writes, and it may write any address, which is what powers bulk whitelists, referrer maps, or a creator-managed forwarding store. PUBLIC stores are the exception: Merkl provisions them so that end users can manage their own entry directly, authenticated by a wallet signature rather than the store owner's key. The canonical example is the global forwarding store below.

The global forwarding store

global-forwarding is a single, Merkl-owned PUBLIC store of type FORWARDER. It lets any wallet redirect its own Merkl rewards to another address across every campaign at once. See the user-facing guide. Each entry is address → {"forwardTo": "0x..."}.

Because it is PUBLIC, writes do not use the store owner's API key. Each wallet authenticates with a JWT from the wallet-signature login flow and may only write its own entry: a write targeting any other address returns 403. Batch upsert is not available on PUBLIC stores; write one entry at a time.

ADDR=0xYourAddress
API=https://api.merkl.xyz/v4

# 1. Get the message to sign
curl "$API/auth/nonce/$ADDR"

# 2. Sign the returned `message` with your wallet (personal_sign), then log in
#    (stores a jwt cookie). Connecting your wallet on the Merkl app runs this same flow.
curl -X POST "$API/auth/login" -c cookies.txt -H 'Content-Type: application/json' \
  -d '{"address":"'$ADDR'","message":"<message from step 1>","signature":"0x<sig>"}'

# 3. Enable forwarding for your own address
curl -X PUT "$API/key-value-stores/global-forwarding/entries" -b cookies.txt \
  -H 'Content-Type: application/json' \
  -d '{"address":"'$ADDR'","value":"{\"forwardTo\":\"0xDestination\"}"}'

# Disable forwarding
curl -X DELETE "$API/key-value-stores/global-forwarding/entries/$ADDR" -b cookies.txt

Creator-managed forwarding stores

To forward rewards on behalf of others (e.g. routing a vault's or contract's rewards to the real beneficiaries), a campaign creator uses their own FORWARDER store instead of the global one: create it via self-serve (or have Merkl provision it), push entries in bulk with your API key, and reference its canonical key from the campaign's forwarding hook. Because it is API_KEY mode, you can write any address and use batch upsert. See the forwarding hook, and Precedence for how it combines with other redirections.

Wiring a store to a campaign

Creating a store does not by itself affect any campaign. A campaign must reference the store's canonical key in its hook for the engine to apply it. Merkl Studio asks for the key and the parameters below when you add the matching option.

Store typeCampaign option
WHITELIST / BLACKLISTDynamic lists. A non-empty whitelist implicitly excludes every other address.
BOOSTBoost hook
REFERRERReferral hook
FORWARDERForwarding hook

Boost hook

ParameterDescription
keyCanonical key of the BOOST store
boostingFunctionMULTIPLY (amount × boost), ADD (amount + boost), or REPLACE (amount = boost)
defaultBoostFallback for an address absent from the store. ZERO_ADDRESS: use the entry stored for 0x000…000; include that entry when you use this mode. The zero address never earns rewards, so it is safe to repurpose. ERROR: abort the computation if an eligible address is missing.

Several campaigns can point at the same store, each with its own boostingFunction. The API boost uses the same parameters with an HTTP endpoint instead of a store.

Referral hook

Each REFERRER entry maps one invitee to one referrer. Re-submitting an invitee overwrites its referrer. Self-referrals are ignored. Merkl Studio asks for percentages and converts them to base 9 ("1000000000" = 100%).

ParameterDescription
keyCanonical key of the REFERRER store
boostForReferrerscore, proportional, or flatBoostNonParticipating
valueForBoostForReferrerBoost value for referrers
boostForInvitedscore, proportional, flatBoostNonParticipating, or community
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
cumulativeBoostUser who is both referrer and invitee: true = both bonuses apply, false = only the better one

The boost configuration is fixed for the campaign's lifetime. Only the referral pairs stay mutable. For what each boost type does, see Referral program.

Forwarding hook

The hook references a creator-managed FORWARDER store by its canonical key. Forwarding is single-hop and runs before fees and distribution.

Common errors

StatusCause
401Missing or invalid X-API-Key.
403: not allowlistedNo address of your creator has a quota.
403: type not allowedtype is not in your quota's allowedTypes.
403: quota reachedYour creator already owns maxConfigs stores, counted across all of its addresses.
403: not ownerThe API key doesn't belong to the store's owner (writes, or reads on a private store).
404: unknown storeNo store exists for that key.
404: entry not foundGET .../entries/{address} on an address with no entry.
400: malformed valueThe value doesn't match the store type's required shape.
400: size limit reachedA write would push the store past its sizeLimit.
400: sizeLimit exceeds quotaRequested sizeLimit is above your maxSizeLimit.
400: key already existsYou already have a store with that suffix.
400: sizeLimit below entry countYou tried to lower sizeLimit below the store's current number of entries.